@optimuslabs/harness-map-staging 1.5.30 → 1.5.32

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.
@@ -13,6 +13,9 @@ function buildGetOptions(url, timeoutMs) {
13
13
  path: url.pathname + url.search,
14
14
  method: 'GET',
15
15
  timeout: timeoutMs,
16
+ // A fresh socket per GET: Node 19+ keep-alive would reuse a socket left idle
17
+ // during the inventory disk scan, and a server FIN then looks like a failed fetch.
18
+ agent: false,
16
19
  };
17
20
  }
18
21
  function buildBodyOptions(url, payload, method, timeoutMs) {
@@ -31,23 +34,36 @@ function buildBodyOptions(url, payload, method, timeoutMs) {
31
34
  timeout: timeoutMs,
32
35
  };
33
36
  }
34
- /** Execute a GET request. Returns statusCode=0 and empty body on network/timeout failure. */
35
- export function executeGet(urlStr, timeoutMs) {
37
+ function executeGetOnce(urlStr, timeoutMs) {
36
38
  const url = new URL(urlStr);
37
39
  const options = buildGetOptions(url, timeoutMs);
38
40
  const transport = url.protocol === 'https:' ? https.request : http.request;
39
41
  return new Promise((resolve) => {
42
+ let done = false;
43
+ const finish = (result) => {
44
+ if (done)
45
+ return;
46
+ done = true;
47
+ resolve(result);
48
+ };
40
49
  const req = transport(options, (res) => {
41
50
  let body = '';
42
51
  res.setEncoding('utf8');
43
52
  res.on('data', (chunk) => { body += chunk; });
44
- res.on('end', () => resolve({ statusCode: res.statusCode ?? 0, body }));
53
+ res.on('end', () => finish({ statusCode: res.statusCode ?? 0, body }));
45
54
  });
46
- req.on('error', () => resolve({ statusCode: 0, body: '' }));
47
- req.on('timeout', () => { req.destroy(); resolve({ statusCode: 0, body: '' }); });
55
+ req.on('error', () => finish({ statusCode: 0, body: '' }));
56
+ req.on('timeout', () => { req.destroy(); finish({ statusCode: 0, body: '' }); });
48
57
  req.end();
49
58
  });
50
59
  }
60
+ /** Execute a GET. Retries once on a dropped connection (status 0). */
61
+ export async function executeGet(urlStr, timeoutMs) {
62
+ const first = await executeGetOnce(urlStr, timeoutMs);
63
+ if (first.statusCode !== 0)
64
+ return first;
65
+ return executeGetOnce(urlStr, timeoutMs);
66
+ }
51
67
  /** Execute a POST/PATCH request. A Buffer payload is sent as `Content-Encoding: gzip`. Rejects on network error or timeout. */
52
68
  export function executeBody(urlStr, method, payload, timeoutMs) {
53
69
  const url = new URL(urlStr);
@@ -2,9 +2,9 @@ import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  const HOME_DIR = homedir();
5
- import { getFileCollectionPatterns, FILE_PATH_REGISTRY_FILE_PATTERNS_PATH } from '../../endpoint_client/index.js';
5
+ import { getFileCollectionPatterns, getSensitivePathsAuditCandidates, FILE_PATH_REGISTRY_FILE_PATTERNS_PATH, } from '../../endpoint_client/index.js';
6
6
  import { OPTIMUSLABS_LOGS_REL, LOG_GROUP_AUDIT } from '../../bootstrap_constants.js';
7
- import { runSensitivePathsAudit } from '../../log_sensitive_paths_audit.js';
7
+ import { CandidatesFetchError, runSensitivePathsAudit } from '../../log_sensitive_paths_audit.js';
8
8
  import { buildPathPermissionsAuditEntry, grantPathsFromConfig } from '../../log_path_permissions_audit.js';
9
9
  import { loadEndpointBase, getEndpointSource } from '../sender/endpoint_config.js';
10
10
  import { hookLogReplace, hookRunLog, hookLogError, readHookLog } from './hook_logger.js';
@@ -115,6 +115,8 @@ function filterToActiveWorktrees(configFiles) {
115
115
  return { configFiles: filtered, worktreeReport };
116
116
  }
117
117
  async function collectAllConfigFiles(endpointBase) {
118
+ // Same moment as file-patterns, not after the disk scan: that gap is what dropped this GET.
119
+ const candidatesPromise = getSensitivePathsAuditCandidates(endpointBase);
118
120
  const patternsResponse = await fetchPatterns(endpointBase);
119
121
  const configFiles = scanPatterns(patternsResponse);
120
122
  const seen = new Set(configFiles.map(configFileKey));
@@ -134,26 +136,45 @@ async function collectAllConfigFiles(endpointBase) {
134
136
  const skillsCliEntry = collectSkillsCliInstalled(PROJECT_ROOT, hookRunLog);
135
137
  if (skillsCliEntry)
136
138
  addUnseen(configFiles, seen, [skillsCliEntry]);
137
- return filterToActiveWorktrees(configFiles);
139
+ return { ...filterToActiveWorktrees(configFiles), candidatesPromise };
138
140
  }
139
141
  /** Adds the sensitive paths audit to the batch; false when it could not be run (nothing added). */
140
- async function addSensitivePathsAudit(endpointBase, configFiles) {
142
+ async function addSensitivePathsAudit(endpointBase, configFiles, candidatesPromise) {
141
143
  hookRunLog(`running sensitive paths audit`);
142
144
  const auditOutputDir = join(homedir(), OPTIMUSLABS_LOGS_REL, LOG_GROUP_AUDIT);
145
+ const prefetched = await candidatesPromise;
143
146
  try {
144
- const auditResult = await runSensitivePathsAudit(endpointBase, auditOutputDir, PROJECT_ROOT);
147
+ const auditResult = await runSensitivePathsAudit(endpointBase, auditOutputDir, PROJECT_ROOT, prefetched);
145
148
  hookRunLog(`sensitive_paths_audit: ${auditResult.paths.length} path(s)`);
146
149
  configFiles.push({ file_type: 'sensitive_paths_audit', file_path: 'sensitive_paths_audit.txt', raw_content: { paths: auditResult.paths } });
147
150
  return true;
148
151
  }
149
152
  catch (err) {
150
- // Send no audit rather than an empty one: the backend keeps the machine's last good audit.
151
- hookRunLog(`sensitive_paths_audit_error: ${err instanceof Error ? err.message : String(err)} (audit not sent)`);
153
+ // Keep the last good audit either way. Only a missed candidates GET is transient;
154
+ // a local write failure (EACCES, ENOSPC) is an endpoint fault and must still
155
+ // report a failed FunctionalRun so Sensor failing stays visible.
156
+ const message = err instanceof Error ? err.message : String(err);
157
+ hookRunLog(`sensitive_paths_audit_error: ${message} (audit not sent)`);
152
158
  hookLogError('sensitive_paths_audit', err, { endpoint: endpointBase, output_dir: auditOutputDir });
153
- await reportFunctionalFailureAwait('inventory', 'sensitive_paths_audit', err);
159
+ console.error(`sensitive_paths_audit: ${message}`);
160
+ if (!(err instanceof CandidatesFetchError)) {
161
+ await reportFunctionalFailureAwait('inventory', 'sensitive_paths_audit', err);
162
+ }
154
163
  return false;
155
164
  }
156
165
  }
166
+ async function collectInventory(endpointBase) {
167
+ const collected = await collectAllConfigFiles(endpointBase);
168
+ const sensitiveAudited = await addSensitivePathsAudit(endpointBase, collected.configFiles, collected.candidatesPromise);
169
+ ensureCursorUserSettingsSnapshotInBatch(collected.configFiles);
170
+ if (sensitiveAudited) {
171
+ addPathPermissionsAudit(collected.configFiles);
172
+ }
173
+ else {
174
+ hookRunLog('path_permissions_audit: skipped (no sensitive paths audit this run)');
175
+ }
176
+ return collected;
177
+ }
157
178
  /** POSIX mode of every collected file, plus granted folders (and their ancestors) and sensitive audit hits within a budget. stat() only. */
158
179
  function addPathPermissionsAudit(configFiles) {
159
180
  const visited = [];
@@ -251,7 +272,7 @@ async function main() {
251
272
  let configFiles;
252
273
  let worktreeReport = [];
253
274
  try {
254
- const collected = await collectAllConfigFiles(endpointBase);
275
+ const collected = await collectInventory(endpointBase);
255
276
  configFiles = collected.configFiles;
256
277
  worktreeReport = collected.worktreeReport;
257
278
  }
@@ -259,16 +280,6 @@ async function main() {
259
280
  hookRunLog(`patterns_error: ${err instanceof Error ? err.message : String(err)}`);
260
281
  throw err;
261
282
  }
262
- const sensitiveAudited = await addSensitivePathsAudit(endpointBase, configFiles);
263
- ensureCursorUserSettingsSnapshotInBatch(configFiles);
264
- if (sensitiveAudited) {
265
- addPathPermissionsAudit(configFiles);
266
- }
267
- else {
268
- // Built without the sensitive paths, it would replace the backend's last good permissions
269
- // audit and drop those paths' modes; skip it so both audits stay as last sent.
270
- hookRunLog('path_permissions_audit: skipped (no sensitive paths audit this run)');
271
- }
272
283
  const fileTypes = [...new Set(configFiles.map((c) => c.file_type))].join(', ');
273
284
  hookRunLog(`collected ${configFiles.length} config file(s) file_types=${fileTypes}`);
274
285
  for (const c of configFiles) {
@@ -333,4 +344,4 @@ async function logSingleFile(filePath) {
333
344
  }
334
345
  return sendConfigFile({ file_type: fileType, file_path: filePath, raw_content: rawContent }, hardwareUuid, authKey);
335
346
  }
336
- export { main, logSingleFile, createSignature, canonicalizePayload, sendConfigFile, sendConfigFilesBatch, BATCH_CHUNK_SIZE };
347
+ export { main, logSingleFile, createSignature, canonicalizePayload, sendConfigFile, sendConfigFilesBatch, BATCH_CHUNK_SIZE, addSensitivePathsAudit, };
@@ -9,10 +9,17 @@
9
9
  import { existsSync, writeFileSync, mkdirSync, statSync } from 'node:fs';
10
10
  import { join } from 'node:path';
11
11
  import { homedir } from 'node:os';
12
- import { getSensitivePathsAuditCandidates } from './endpoint_client/index.js';
12
+ import { getSensitivePathsAuditCandidates, } from './endpoint_client/index.js';
13
13
  import { OPTIMUSLABS_LOGS_REL, LOG_GROUP_AUDIT } from './bootstrap_constants.js';
14
14
  import { loadEndpointBase } from './log_config_files/sender/endpoint_config.js';
15
15
  const AUDIT_FILENAME = 'sensitive_paths_audit.txt';
16
+ /** Thrown when the candidates GET returned nothing usable. Transient: do not mark the sensor as failing. */
17
+ export class CandidatesFetchError extends Error {
18
+ constructor(endpointBase) {
19
+ super(`could not fetch sensitive path candidates from ${endpointBase}`);
20
+ this.name = 'CandidatesFetchError';
21
+ }
22
+ }
16
23
  function expandPath(template, cwd) {
17
24
  if (template.startsWith('~/')) {
18
25
  return join(homedir(), template.slice(2));
@@ -66,10 +73,13 @@ export function writeSensitivePathsAudit(outputDir, pathTemplates, cwd = process
66
73
  * Throws when the candidates can't be fetched: an audit run without them would report no
67
74
  * sensitive paths, and sending it would replace the machine's last good audit on the backend.
68
75
  */
69
- export async function runSensitivePathsAudit(endpointBase, outputDir = join(homedir(), OPTIMUSLABS_LOGS_REL, LOG_GROUP_AUDIT), cwd = process.cwd()) {
70
- const response = await getSensitivePathsAuditCandidates(endpointBase);
76
+ export async function runSensitivePathsAudit(endpointBase, outputDir = join(homedir(), OPTIMUSLABS_LOGS_REL, LOG_GROUP_AUDIT), cwd = process.cwd(), prefetched) {
77
+ let response = prefetched;
78
+ if (response == null) {
79
+ response = await getSensitivePathsAuditCandidates(endpointBase);
80
+ }
71
81
  if (!response) {
72
- throw new Error(`could not fetch sensitive path candidates from ${endpointBase}`);
82
+ throw new CandidatesFetchError(endpointBase);
73
83
  }
74
84
  const paths = writeSensitivePathsAudit(outputDir, response.paths, cwd);
75
85
  const outPath = join(outputDir, AUDIT_FILENAME);
@@ -0,0 +1,15 @@
1
+ /**
2
+ * pii-scrub shim: the one place that imports @optimuslabs/pii-scrub-staging.
3
+ *
4
+ * Loaded the way tofu.ts loads tofu: the sibling local dist when environment=development and it
5
+ * is built, the published npm package otherwise.
6
+ */
7
+ import path from "path";
8
+ import { fileURLToPath } from "url";
9
+ import { shouldUseLocalTofuDist } from "./tofu_environment.js";
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const localPiiScrubPath = path.join(__dirname, "../../pii-scrub/dist/index.js");
12
+ const piiScrub = (shouldUseLocalTofuDist(localPiiScrubPath)
13
+ ? await import(localPiiScrubPath)
14
+ : await import("@optimuslabs/pii-scrub-staging"));
15
+ export const { SCRUBBER_VERSION, containsPlaceholder, scrubFields, scrubText, scrubValue } = piiScrub;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optimuslabs/harness-map-staging",
3
- "version": "1.5.30",
3
+ "version": "1.5.32",
4
4
  "description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -49,17 +49,17 @@
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/node": "^24.10.1",
52
- "@vitest/coverage-v8": "^4.1.8",
53
- "@vitest/ui": "^4.1.8",
52
+ "@vitest/coverage-v8": "^4.1.12",
53
+ "@vitest/ui": "^4.1.12",
54
54
  "ts-node": "^10.9.2",
55
55
  "typescript": "^5.4.5",
56
- "vitest": "^4.1.8"
56
+ "vitest": "^4.1.12"
57
57
  },
58
58
  "dependencies": {
59
59
  "@optimuslabs/tofu-staging": "^0.1.18",
60
60
  "axios": "^1.20.0",
61
61
  "canonicalize": "^2.1.0",
62
- "smol-toml": "^1.7.1"
62
+ "smol-toml": "^1.9.1"
63
63
  },
64
64
  "publishConfig": {
65
65
  "access": "public"