@optimuslabs/harness-map-staging 1.5.24 → 1.5.26
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.
- package/dist/approved_tenant_gate.js +302 -0
- package/dist/compliance_prompt_gate.js +315 -156
- package/dist/log_config_files/collection/claude_desktop_orgs_collector.js +173 -0
- package/dist/log_config_files/collection/codex_token_usage_collector.js +1 -1
- package/dist/log_config_files/collection/config_collector.js +1 -0
- package/dist/log_config_files/collection/cursor_token_usage_collector.js +127 -91
- package/dist/log_config_files/runtime/main_runner.js +79 -149
- package/dist/log_config_files/runtime/management_storage.js +46 -2
- package/dist/log_config_files/runtime/remediation_sync.js +156 -109
- package/package.json +1 -1
|
@@ -15,7 +15,7 @@ import { ensureAuthentication } from '../auth/auth_flow.js';
|
|
|
15
15
|
import { readJSONFile, readMarkdownFile } from '../readers/file_readers.js';
|
|
16
16
|
import { isVscdbVirtualPath, tryReadVscdbVirtualFile, summarizeComposerPayloadForDiagnostics, } from '../readers/vscdb_config_builder.js';
|
|
17
17
|
import { persistVscdbComposerContractFromPatternsResponse } from '../readers/vscdb_reader.js';
|
|
18
|
-
import { collectConfigFilesFromPatterns, collectMcpToolFiles, collectConfigFilesFromInstalledPlugins, collectPluginCacheMcpFiles, collectMcpFromClaudeJsonProjects, collectClaudeTokenUsage, collectCopilotTokenUsage, collectOpencodeTokenUsage, collectCodexTokenUsage, collectCursorTokenUsage, collectHermesTokenUsage, collectOpenclawTokenUsage, collectPiTokenUsage, collectCoworkDesktopVersion, collectGrokCliVersion, collectClaudeDesktopExtensionManifests, collectClaudeDesktopExtensionSettingsFiles, enrichClaudeDesktopExtensionsInstallationsUpload, determineFileTypeFromPath, } from '../collection/config_collector.js';
|
|
18
|
+
import { collectConfigFilesFromPatterns, collectMcpToolFiles, collectConfigFilesFromInstalledPlugins, collectPluginCacheMcpFiles, collectMcpFromClaudeJsonProjects, collectClaudeTokenUsage, collectCopilotTokenUsage, collectOpencodeTokenUsage, collectCodexTokenUsage, collectCursorTokenUsage, collectHermesTokenUsage, collectOpenclawTokenUsage, collectPiTokenUsage, collectCoworkDesktopVersion, collectClaudeDesktopOrgs, collectGrokCliVersion, collectClaudeDesktopExtensionManifests, collectClaudeDesktopExtensionSettingsFiles, enrichClaudeDesktopExtensionsInstallationsUpload, determineFileTypeFromPath, } from '../collection/config_collector.js';
|
|
19
19
|
import { ensureCursorUserSettingsSnapshotInBatch } from '../collection/ensure_cursor_user_settings_snapshot.js';
|
|
20
20
|
import { collectSkillsCliInstalled } from '../collection/skills_cli_collector.js';
|
|
21
21
|
import { collectWorkspaceVscdbs } from '../collection/mcp_tool_collector.js';
|
|
@@ -27,7 +27,34 @@ import { ensureWorkspaceRepoEnv, resolveProjectRoot } from './workspace_repo.js'
|
|
|
27
27
|
import { scanActiveWorktrees, activeWorktreeRootSet, filterConfigFilesToActiveWorktrees, } from './worktree_scanner.js';
|
|
28
28
|
import { createSignature, canonicalizePayload } from '../sender/signing.js';
|
|
29
29
|
const PROJECT_ROOT = resolveProjectRoot();
|
|
30
|
-
|
|
30
|
+
const configFileKey = (c) => `${c.file_type}\t${c.file_path}`;
|
|
31
|
+
/** Appends the entries not in the batch yet (by file type and path), recording each as seen. */
|
|
32
|
+
function addUnseen(configFiles, seen, entries) {
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
const key = configFileKey(entry);
|
|
35
|
+
if (seen.has(key))
|
|
36
|
+
continue;
|
|
37
|
+
seen.add(key);
|
|
38
|
+
configFiles.push(entry);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Machine-wide collectors, run in this order after the plugin caches: [hook log line, collector]. */
|
|
42
|
+
const HOME_COLLECTORS = [
|
|
43
|
+
['scanning Claude known projects from ~/.claude.json', collectMcpFromClaudeJsonProjects],
|
|
44
|
+
['aggregating Claude token usage from ~/.claude/projects transcripts', collectClaudeTokenUsage],
|
|
45
|
+
['aggregating Copilot token usage from ~/.copilot/session-state transcripts', collectCopilotTokenUsage],
|
|
46
|
+
['aggregating OpenCode token usage from ~/.local/share/opencode/opencode.db', collectOpencodeTokenUsage],
|
|
47
|
+
['aggregating Codex token usage from ~/.codex/sessions rollouts', collectCodexTokenUsage],
|
|
48
|
+
['aggregating Cursor token usage from state.vscdb', collectCursorTokenUsage],
|
|
49
|
+
['aggregating Hermes token usage from ~/.hermes/state.db', collectHermesTokenUsage],
|
|
50
|
+
['aggregating OpenClaw token usage from ~/.openclaw/agents/*/sessions/sessions.json', collectOpenclawTokenUsage],
|
|
51
|
+
['aggregating Pi token usage from ~/.pi/agent/sessions transcripts', collectPiTokenUsage],
|
|
52
|
+
["reading Claude Desktop app version for Cowork's Agent Profile", collectCoworkDesktopVersion],
|
|
53
|
+
["reading the orgs Claude Desktop's signed-in account has used", collectClaudeDesktopOrgs],
|
|
54
|
+
['reading Grok CLI version', collectGrokCliVersion],
|
|
55
|
+
];
|
|
56
|
+
/** The file-path-registry patterns from the API; throws when none came back. */
|
|
57
|
+
async function fetchPatterns(endpointBase) {
|
|
31
58
|
const patternsUrl = `${endpointBase.replace(/\/+$/, '')}${FILE_PATH_REGISTRY_FILE_PATTERNS_PATH}`;
|
|
32
59
|
hookRunLog(`fetching patterns from ${patternsUrl}`);
|
|
33
60
|
const patternsResponse = await getFileCollectionPatterns(endpointBase);
|
|
@@ -38,9 +65,12 @@ async function collectAllConfigFiles(endpointBase) {
|
|
|
38
65
|
hookRunLog(msg);
|
|
39
66
|
throw new Error(msg);
|
|
40
67
|
}
|
|
68
|
+
return patternsResponse;
|
|
69
|
+
}
|
|
70
|
+
function scanPatterns(patternsResponse) {
|
|
41
71
|
persistVscdbComposerContractFromPatternsResponse(patternsResponse);
|
|
42
72
|
hookRunLog(`scanning config files`);
|
|
43
|
-
|
|
73
|
+
return collectConfigFilesFromPatterns(patternsResponse.patterns, PROJECT_ROOT, hookRunLog, {
|
|
44
74
|
vscdbEntrySpecs: patternsResponse.vscdb_entry_specs,
|
|
45
75
|
vscdb_read_queries: patternsResponse.vscdb_read_queries,
|
|
46
76
|
vscdb_merge_from_composer_state: patternsResponse.vscdb_merge_from_composer_state,
|
|
@@ -50,163 +80,31 @@ async function collectAllConfigFiles(endpointBase) {
|
|
|
50
80
|
absolute_path_prefixes: patternsResponse.absolute_path_prefixes,
|
|
51
81
|
mcp_tool_glob_spec: patternsResponse.mcp_tool_glob_spec ?? null,
|
|
52
82
|
});
|
|
83
|
+
}
|
|
84
|
+
/** MCP tool caches: the global cache, Cursor's per-project caches and its workspace vscdb files. */
|
|
85
|
+
function addMcpToolCaches(patternsResponse, configFiles, seen) {
|
|
53
86
|
hookRunLog(`scanning MCP tool cache`);
|
|
54
87
|
const mcpToolGlobPattern = patternsResponse.patterns.find((p) => p.path_resolution === 'mcp_tool_glob');
|
|
55
88
|
const mcpToolSkipPrefixes = normalizePathSkipPrefixes(mcpToolGlobPattern?.path_skip_prefixes);
|
|
56
|
-
|
|
57
|
-
for (const m of collectMcpToolFiles(mcpToolSkipPrefixes, patternsResponse.client_path_constants)) {
|
|
58
|
-
const key = `${m.file_type}\t${m.file_path}`;
|
|
59
|
-
if (!existingPaths.has(key)) {
|
|
60
|
-
existingPaths.add(key);
|
|
61
|
-
configFiles.push(m);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
89
|
+
addUnseen(configFiles, seen, collectMcpToolFiles(mcpToolSkipPrefixes, patternsResponse.client_path_constants));
|
|
64
90
|
hookRunLog(`scanning Cursor project workspace MCP configs (~/.cursor/projects/*/mcp-cache.json)`);
|
|
65
|
-
|
|
66
|
-
const key = `${m.file_type}\t${m.file_path}`;
|
|
67
|
-
if (!existingPaths.has(key)) {
|
|
68
|
-
existingPaths.add(key);
|
|
69
|
-
configFiles.push(m);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
91
|
+
addUnseen(configFiles, seen, collectCursorProjectWorkspaceMcpConfigs(mcpToolSkipPrefixes, patternsResponse.client_path_constants, patternsResponse.workspace_vscdb_spec, HOME_DIR));
|
|
72
92
|
if (patternsResponse.workspace_vscdb_spec) {
|
|
73
93
|
hookRunLog(`scanning Cursor workspace vscdb files`);
|
|
74
|
-
|
|
75
|
-
const key = `${m.file_type}\t${m.file_path}`;
|
|
76
|
-
if (!existingPaths.has(key)) {
|
|
77
|
-
existingPaths.add(key);
|
|
78
|
-
configFiles.push(m);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
hookRunLog(`scanning installed plugins`);
|
|
83
|
-
if (!patternsResponse.client_path_constants)
|
|
84
|
-
throw new Error('client_path_constants required from API response');
|
|
85
|
-
configFiles.push(...collectConfigFilesFromInstalledPlugins(patternsResponse.client_path_constants));
|
|
86
|
-
hookRunLog(`scanning Claude plugin cache MCP manifests`);
|
|
87
|
-
for (const entry of collectPluginCacheMcpFiles(patternsResponse.client_path_constants, HOME_DIR)) {
|
|
88
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
89
|
-
if (!existingPaths.has(key)) {
|
|
90
|
-
existingPaths.add(key);
|
|
91
|
-
configFiles.push(entry);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
hookRunLog(`scanning Claude known projects from ~/.claude.json`);
|
|
95
|
-
for (const entry of collectMcpFromClaudeJsonProjects(HOME_DIR)) {
|
|
96
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
97
|
-
if (!existingPaths.has(key)) {
|
|
98
|
-
existingPaths.add(key);
|
|
99
|
-
configFiles.push(entry);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
hookRunLog(`aggregating Claude token usage from ~/.claude/projects transcripts`);
|
|
103
|
-
for (const entry of collectClaudeTokenUsage(HOME_DIR)) {
|
|
104
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
105
|
-
if (!existingPaths.has(key)) {
|
|
106
|
-
existingPaths.add(key);
|
|
107
|
-
configFiles.push(entry);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
hookRunLog(`aggregating Copilot token usage from ~/.copilot/session-state transcripts`);
|
|
111
|
-
for (const entry of collectCopilotTokenUsage(HOME_DIR)) {
|
|
112
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
113
|
-
if (!existingPaths.has(key)) {
|
|
114
|
-
existingPaths.add(key);
|
|
115
|
-
configFiles.push(entry);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
hookRunLog(`aggregating OpenCode token usage from ~/.local/share/opencode/opencode.db`);
|
|
119
|
-
for (const entry of collectOpencodeTokenUsage(HOME_DIR)) {
|
|
120
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
121
|
-
if (!existingPaths.has(key)) {
|
|
122
|
-
existingPaths.add(key);
|
|
123
|
-
configFiles.push(entry);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
hookRunLog(`aggregating Codex token usage from ~/.codex/sessions rollouts`);
|
|
127
|
-
for (const entry of collectCodexTokenUsage(HOME_DIR)) {
|
|
128
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
129
|
-
if (!existingPaths.has(key)) {
|
|
130
|
-
existingPaths.add(key);
|
|
131
|
-
configFiles.push(entry);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
hookRunLog(`aggregating Cursor token usage from state.vscdb`);
|
|
135
|
-
for (const entry of collectCursorTokenUsage(HOME_DIR)) {
|
|
136
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
137
|
-
if (!existingPaths.has(key)) {
|
|
138
|
-
existingPaths.add(key);
|
|
139
|
-
configFiles.push(entry);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
hookRunLog(`aggregating Hermes token usage from ~/.hermes/state.db`);
|
|
143
|
-
for (const entry of collectHermesTokenUsage(HOME_DIR)) {
|
|
144
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
145
|
-
if (!existingPaths.has(key)) {
|
|
146
|
-
existingPaths.add(key);
|
|
147
|
-
configFiles.push(entry);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
hookRunLog(`aggregating OpenClaw token usage from ~/.openclaw/agents/*/sessions/sessions.json`);
|
|
151
|
-
for (const entry of collectOpenclawTokenUsage(HOME_DIR)) {
|
|
152
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
153
|
-
if (!existingPaths.has(key)) {
|
|
154
|
-
existingPaths.add(key);
|
|
155
|
-
configFiles.push(entry);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
hookRunLog(`aggregating Pi token usage from ~/.pi/agent/sessions transcripts`);
|
|
159
|
-
for (const entry of collectPiTokenUsage(HOME_DIR)) {
|
|
160
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
161
|
-
if (!existingPaths.has(key)) {
|
|
162
|
-
existingPaths.add(key);
|
|
163
|
-
configFiles.push(entry);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
hookRunLog(`reading Claude Desktop app version for Cowork's Agent Profile`);
|
|
167
|
-
for (const entry of collectCoworkDesktopVersion(HOME_DIR)) {
|
|
168
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
169
|
-
if (!existingPaths.has(key)) {
|
|
170
|
-
existingPaths.add(key);
|
|
171
|
-
configFiles.push(entry);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
hookRunLog(`reading Grok CLI version`);
|
|
175
|
-
for (const entry of collectGrokCliVersion(HOME_DIR)) {
|
|
176
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
177
|
-
if (!existingPaths.has(key)) {
|
|
178
|
-
existingPaths.add(key);
|
|
179
|
-
configFiles.push(entry);
|
|
180
|
-
}
|
|
94
|
+
addUnseen(configFiles, seen, collectWorkspaceVscdbs(patternsResponse.workspace_vscdb_spec));
|
|
181
95
|
}
|
|
96
|
+
}
|
|
97
|
+
function addClaudeDesktopExtensions(configFiles, seen) {
|
|
182
98
|
hookRunLog(`merging disk-only Claude Desktop extensions into extensions-installations.json`);
|
|
183
99
|
const diskExtMerge = enrichClaudeDesktopExtensionsInstallationsUpload(configFiles, HOME_DIR);
|
|
184
100
|
hookRunLog(`claude desktop extensions: merged=${diskExtMerge.mergedCount} had_registry_upload=${diskExtMerge.hadRegistryUpload}`);
|
|
185
|
-
|
|
186
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
187
|
-
if (!existingPaths.has(key)) {
|
|
188
|
-
existingPaths.add(key);
|
|
189
|
-
configFiles.push(entry);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
101
|
+
addUnseen(configFiles, seen, collectClaudeDesktopExtensionSettingsFiles(HOME_DIR));
|
|
192
102
|
if (!diskExtMerge.hadRegistryUpload && diskExtMerge.mergedCount > 0) {
|
|
193
103
|
hookRunLog(`scanning Claude Desktop extension manifests on disk (no registry upload in batch)`);
|
|
194
|
-
|
|
195
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
196
|
-
if (!existingPaths.has(key)) {
|
|
197
|
-
existingPaths.add(key);
|
|
198
|
-
configFiles.push(entry);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
const skillsCliEntry = collectSkillsCliInstalled(PROJECT_ROOT, hookRunLog);
|
|
203
|
-
if (skillsCliEntry) {
|
|
204
|
-
const key = `${skillsCliEntry.file_type}\t${skillsCliEntry.file_path}`;
|
|
205
|
-
if (!existingPaths.has(key)) {
|
|
206
|
-
existingPaths.add(key);
|
|
207
|
-
configFiles.push(skillsCliEntry);
|
|
208
|
-
}
|
|
104
|
+
addUnseen(configFiles, seen, collectClaudeDesktopExtensionManifests(HOME_DIR));
|
|
209
105
|
}
|
|
106
|
+
}
|
|
107
|
+
function filterToActiveWorktrees(configFiles) {
|
|
210
108
|
const worktreeReport = scanActiveWorktrees(PROJECT_ROOT, HOME_DIR);
|
|
211
109
|
const activeRoots = activeWorktreeRootSet(worktreeReport);
|
|
212
110
|
const beforeFilter = configFiles.length;
|
|
@@ -214,6 +112,29 @@ async function collectAllConfigFiles(endpointBase) {
|
|
|
214
112
|
hookRunLog(`worktree_scan: active=${worktreeReport.length} config_files ${beforeFilter}→${filtered.length}`);
|
|
215
113
|
return { configFiles: filtered, worktreeReport };
|
|
216
114
|
}
|
|
115
|
+
async function collectAllConfigFiles(endpointBase) {
|
|
116
|
+
const patternsResponse = await fetchPatterns(endpointBase);
|
|
117
|
+
const configFiles = scanPatterns(patternsResponse);
|
|
118
|
+
const seen = new Set(configFiles.map(configFileKey));
|
|
119
|
+
addMcpToolCaches(patternsResponse, configFiles, seen);
|
|
120
|
+
hookRunLog(`scanning installed plugins`);
|
|
121
|
+
const clientPathConstants = patternsResponse.client_path_constants;
|
|
122
|
+
if (!clientPathConstants)
|
|
123
|
+
throw new Error('client_path_constants required from API response');
|
|
124
|
+
configFiles.push(...collectConfigFilesFromInstalledPlugins(clientPathConstants));
|
|
125
|
+
hookRunLog(`scanning Claude plugin cache MCP manifests`);
|
|
126
|
+
addUnseen(configFiles, seen, collectPluginCacheMcpFiles(clientPathConstants, HOME_DIR));
|
|
127
|
+
for (const [message, collect] of HOME_COLLECTORS) {
|
|
128
|
+
hookRunLog(message);
|
|
129
|
+
addUnseen(configFiles, seen, collect(HOME_DIR));
|
|
130
|
+
}
|
|
131
|
+
addClaudeDesktopExtensions(configFiles, seen);
|
|
132
|
+
const skillsCliEntry = collectSkillsCliInstalled(PROJECT_ROOT, hookRunLog);
|
|
133
|
+
if (skillsCliEntry)
|
|
134
|
+
addUnseen(configFiles, seen, [skillsCliEntry]);
|
|
135
|
+
return filterToActiveWorktrees(configFiles);
|
|
136
|
+
}
|
|
137
|
+
/** Adds the sensitive paths audit to the batch; false when it could not be run (nothing added). */
|
|
217
138
|
async function addSensitivePathsAudit(endpointBase, configFiles) {
|
|
218
139
|
hookRunLog(`running sensitive paths audit`);
|
|
219
140
|
const auditOutputDir = join(homedir(), OPTIMUSLABS_LOGS_REL, LOG_GROUP_AUDIT);
|
|
@@ -221,12 +142,14 @@ async function addSensitivePathsAudit(endpointBase, configFiles) {
|
|
|
221
142
|
const auditResult = await runSensitivePathsAudit(endpointBase, auditOutputDir, PROJECT_ROOT);
|
|
222
143
|
hookRunLog(`sensitive_paths_audit: ${auditResult.paths.length} path(s)`);
|
|
223
144
|
configFiles.push({ file_type: 'sensitive_paths_audit', file_path: 'sensitive_paths_audit.txt', raw_content: { paths: auditResult.paths } });
|
|
145
|
+
return true;
|
|
224
146
|
}
|
|
225
147
|
catch (err) {
|
|
226
148
|
// Send no audit rather than an empty one: the backend keeps the machine's last good audit.
|
|
227
149
|
hookRunLog(`sensitive_paths_audit_error: ${err instanceof Error ? err.message : String(err)} (audit not sent)`);
|
|
228
150
|
hookLogError('sensitive_paths_audit', err, { endpoint: endpointBase, output_dir: auditOutputDir });
|
|
229
151
|
await reportFunctionalFailureAwait('inventory', 'sensitive_paths_audit', err);
|
|
152
|
+
return false;
|
|
230
153
|
}
|
|
231
154
|
}
|
|
232
155
|
/** POSIX mode of every collected file, plus granted folders (and their ancestors) and sensitive audit hits within a budget. stat() only. */
|
|
@@ -334,9 +257,16 @@ async function main() {
|
|
|
334
257
|
hookRunLog(`patterns_error: ${err instanceof Error ? err.message : String(err)}`);
|
|
335
258
|
throw err;
|
|
336
259
|
}
|
|
337
|
-
await addSensitivePathsAudit(endpointBase, configFiles);
|
|
260
|
+
const sensitiveAudited = await addSensitivePathsAudit(endpointBase, configFiles);
|
|
338
261
|
ensureCursorUserSettingsSnapshotInBatch(configFiles);
|
|
339
|
-
|
|
262
|
+
if (sensitiveAudited) {
|
|
263
|
+
addPathPermissionsAudit(configFiles);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
// Built without the sensitive paths, it would replace the backend's last good permissions
|
|
267
|
+
// audit and drop those paths' modes; skip it so both audits stay as last sent.
|
|
268
|
+
hookRunLog('path_permissions_audit: skipped (no sensitive paths audit this run)');
|
|
269
|
+
}
|
|
340
270
|
const fileTypes = [...new Set(configFiles.map((c) => c.file_type))].join(', ');
|
|
341
271
|
hookRunLog(`collected ${configFiles.length} config file(s) file_types=${fileTypes}`);
|
|
342
272
|
for (const c of configFiles) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Paths and JSON persistence for ~/.optimuslabs/management:
|
|
3
3
|
* - remediation_instructions.json (remediations[])
|
|
4
4
|
*/
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
8
|
import { OPTIMUSLABS_MANAGEMENT_REL, OPTIMUSLABS_LOGS_REL } from '../../bootstrap_constants.js';
|
|
@@ -129,7 +129,51 @@ function parseInstructionsRaw(raw) {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
export function writeRemediationInstructionsFile(file) {
|
|
132
|
-
|
|
132
|
+
writeInstructionsFile(file, new Date().toISOString());
|
|
133
|
+
}
|
|
134
|
+
function writeInstructionsFile(file, remediationsWrittenAt) {
|
|
135
|
+
// Most callers write `{ remediations }` only; keep the approval lists they didn't pass.
|
|
136
|
+
const onDisk = readRemediationInstructionsFile();
|
|
137
|
+
const approved_tenants = 'approved_tenants' in file ? file.approved_tenants : onDisk.approved_tenants;
|
|
138
|
+
const unapproved_email_domains = 'unapproved_email_domains' in file ? file.unapproved_email_domains : onDisk.unapproved_email_domains;
|
|
139
|
+
const allowed_email_domains = 'allowed_email_domains' in file ? file.allowed_email_domains : onDisk.allowed_email_domains;
|
|
140
|
+
const approved_agents = 'approved_agents' in file ? file.approved_agents : onDisk.approved_agents;
|
|
141
|
+
atomicWriteJson(getRemediationInstructionsPath(), {
|
|
142
|
+
...file,
|
|
143
|
+
...(approved_tenants === undefined ? {} : { approved_tenants }),
|
|
144
|
+
...(unapproved_email_domains === undefined ? {} : { unapproved_email_domains }),
|
|
145
|
+
...(allowed_email_domains === undefined ? {} : { allowed_email_domains }),
|
|
146
|
+
...(approved_agents === undefined ? {} : { approved_agents }),
|
|
147
|
+
remediations_written_at: remediationsWrittenAt,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/** When the remediations on disk were written: recorded, or for an older file its mtime. */
|
|
151
|
+
function remediationsWrittenAtOf(onDisk) {
|
|
152
|
+
if (onDisk.remediations_written_at)
|
|
153
|
+
return onDisk.remediations_written_at;
|
|
154
|
+
const path = getRemediationInstructionsPath();
|
|
155
|
+
return existsSync(path) ? statSync(path).mtime.toISOString() : new Date().toISOString();
|
|
156
|
+
}
|
|
157
|
+
/** Replace one approval list, keeping the remediations and when they were written. */
|
|
158
|
+
function writeApprovalList(patch) {
|
|
159
|
+
const onDisk = readRemediationInstructionsFile();
|
|
160
|
+
writeInstructionsFile({ ...onDisk, ...patch }, remediationsWrittenAtOf(onDisk));
|
|
161
|
+
}
|
|
162
|
+
/** Replace only the unapproved email domains, keeping everything else on disk. */
|
|
163
|
+
export function writeUnapprovedEmailDomains(unapproved_email_domains) {
|
|
164
|
+
writeApprovalList({ unapproved_email_domains });
|
|
165
|
+
}
|
|
166
|
+
/** Replace only the approved agents, keeping everything else on disk. */
|
|
167
|
+
export function writeApprovedAgents(approved_agents) {
|
|
168
|
+
writeApprovalList({ approved_agents });
|
|
169
|
+
}
|
|
170
|
+
/** Replace only the email allowlist, keeping everything else on disk. */
|
|
171
|
+
export function writeAllowedEmailDomains(allowed_email_domains) {
|
|
172
|
+
writeApprovalList({ allowed_email_domains });
|
|
173
|
+
}
|
|
174
|
+
/** Replace only the approved-tenant list, keeping the remediations already on disk. */
|
|
175
|
+
export function writeApprovedTenants(approved_tenants) {
|
|
176
|
+
writeApprovalList({ approved_tenants });
|
|
133
177
|
}
|
|
134
178
|
export function readRemediationInstructionsFile() {
|
|
135
179
|
const path = getRemediationInstructionsPath();
|