@optimuslabs/harness-map-staging 1.5.45 → 1.5.46

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.
@@ -20,11 +20,24 @@ const SKILL_MAX_FILE_BYTES = 1024 * 1024;
20
20
  const SKILL_MAX_TOTAL_BYTES = 8 * 1024 * 1024;
21
21
  const SKILL_MAX_FILES = 500;
22
22
  const SKILL_MAX_DEPTH = 8;
23
+ // Credential-shaped names, lower case: the backend skill bundle's rule (_is_secret_name) and its
24
+ // credential files (CredentialFiles.FILE_NAMES), which it never stores the content of (OPT-1016).
25
+ const SECRET_NAMES = new Set([
26
+ '.env', '.npmrc', '.netrc', '.pgpass', '.git-credentials', '.htpasswd',
27
+ 'credentials', 'credentials.json', '.credentials.json', 'auth.json', 'auth.v2.file',
28
+ 'oauth_creds.json', 'mcp-auth.json', 'mcp_oauth_tokens.json',
29
+ 'id_rsa', 'id_dsa', 'id_ecdsa', 'id_ed25519',
30
+ 'secrets.json', 'secrets.yaml', 'secrets.yml', 'secrets.toml', 'secrets.env',
31
+ ]);
32
+ const SECRET_SUFFIXES = ['.pem', '.key', '.p12', '.pfx', '.jks', '.keystore'];
23
33
  /** Hidden files are collected, but credential-shaped ones are recorded without their contents. */
24
34
  function isSecretName(name) {
25
35
  const lower = name.toLowerCase();
26
- return lower === '.env' || lower.startsWith('.env.') || lower === '.npmrc' || lower === '.netrc'
27
- || lower === '.pgpass' || lower.endsWith('.pem') || lower.endsWith('.key');
36
+ if (SECRET_NAMES.has(lower) || lower.startsWith('.env.') || SECRET_SUFFIXES.some((s) => lower.endsWith(s)))
37
+ return true;
38
+ // Cloud credential files: OAuth client secrets and service-account keys.
39
+ return lower.endsWith('.json')
40
+ && (lower.startsWith('client_secret') || lower.startsWith('service-account') || lower.includes('serviceaccount'));
28
41
  }
29
42
  function skillMetaRecord(filePath, size, sha256, ver) {
30
43
  return {
@@ -34,72 +47,74 @@ function skillMetaRecord(filePath, size, sha256, ver) {
34
47
  raw_content: { source: 'file', size, ...(sha256 ? { sha256 } : {}), ...(ver ? { version: ver } : {}) },
35
48
  };
36
49
  }
37
- function collectSkillDirFiles(dir, results, constants, skillFilename, budget, depth) {
38
- if (depth > SKILL_MAX_DEPTH)
39
- return;
40
- let entries;
50
+ function readSkillDir(dir) {
41
51
  try {
42
- entries = readdirSync(dir, { withFileTypes: true });
52
+ return readdirSync(dir, { withFileTypes: true });
43
53
  }
44
54
  catch (err) {
45
55
  console.warn(`Error reading skill dir ${dir}:`, err instanceof Error ? err.message : String(err));
46
- return;
56
+ return null;
57
+ }
58
+ }
59
+ function sha256Of(full) {
60
+ try {
61
+ return createHash('sha256').update(readFileSync(full)).digest('hex');
62
+ }
63
+ catch {
64
+ return null;
47
65
  }
48
- for (const ent of entries) {
66
+ }
67
+ /** One extra skill file as a record, or null when it is skipped or unreadable. */
68
+ function skillFileRecord(full, name, constants) {
69
+ const ext = extname(name).toLowerCase();
70
+ if (SKILL_SKIP_EXTS.has(ext))
71
+ return null;
72
+ let size;
73
+ try {
74
+ size = statSync(full).size;
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ const ver = versionFromPluginCachePath(full, constants);
80
+ // A credential file's bytes are never read, so it is checked before the size cap and the hash.
81
+ if (isSecretName(name) || size > SKILL_MAX_FILE_BYTES)
82
+ return skillMetaRecord(full, size, '', ver);
83
+ if (!SKILL_TEXT_EXTS.has(ext)) {
84
+ const sha256 = sha256Of(full);
85
+ return sha256 === null ? null : skillMetaRecord(full, size, sha256, ver);
86
+ }
87
+ const content = readMarkdownFile(full);
88
+ if (content === null)
89
+ return null;
90
+ return { file_type: 'claude_skill', file_path: full, raw_content: { content, source: 'file', ...(ver ? { version: ver } : {}) } };
91
+ }
92
+ function collectSkillDirFiles(dir, results, constants, skillFilename, budget, depth) {
93
+ if (depth > SKILL_MAX_DEPTH)
94
+ return;
95
+ for (const ent of readSkillDir(dir) ?? []) {
49
96
  if (budget.files >= SKILL_MAX_FILES || budget.bytes >= SKILL_MAX_TOTAL_BYTES)
50
97
  return;
51
98
  const full = join(dir, ent.name);
52
- if (ent.isDirectory()) {
53
- if (!SKILL_SKIP_DIRS.has(ent.name))
54
- collectSkillDirFiles(full, results, constants, skillFilename, budget, depth + 1);
99
+ if (ent.isDirectory() && !SKILL_SKIP_DIRS.has(ent.name)) {
100
+ collectSkillDirFiles(full, results, constants, skillFilename, budget, depth + 1);
55
101
  continue;
56
102
  }
57
- if (ent.name === skillFilename)
58
- continue;
59
103
  // Extra skill files: skip nested symlinks so the walk stays inside this skill dir.
60
- if (!ent.isFile())
61
- continue;
62
- const ext = extname(ent.name).toLowerCase();
63
- if (SKILL_SKIP_EXTS.has(ext))
64
- continue;
65
- let size;
66
- try {
67
- size = statSync(full).size;
68
- }
69
- catch {
70
- continue;
71
- }
72
- const ver = versionFromPluginCachePath(full, constants);
73
- if (size > SKILL_MAX_FILE_BYTES) {
74
- results.push(skillMetaRecord(full, size, '', ver));
75
- budget.files += 1;
104
+ if (ent.name === skillFilename || !ent.isFile())
76
105
  continue;
77
- }
78
- if (!SKILL_TEXT_EXTS.has(ext) && ent.name !== skillFilename) {
79
- let sha256 = '';
80
- try {
81
- sha256 = createHash('sha256').update(readFileSync(full)).digest('hex');
82
- }
83
- catch {
84
- continue;
85
- }
86
- results.push(skillMetaRecord(full, size, sha256, ver));
87
- budget.files += 1;
88
- continue;
89
- }
90
- if (isSecretName(ent.name)) {
91
- results.push(skillMetaRecord(full, size, '', ver));
92
- budget.files += 1;
93
- continue;
94
- }
95
- const content = readMarkdownFile(full);
96
- if (content === null)
97
- continue;
98
- results.push({ file_type: 'claude_skill', file_path: full, raw_content: { content, source: 'file', ...(ver ? { version: ver } : {}) } });
99
- budget.files += 1;
100
- budget.bytes += Buffer.byteLength(content);
106
+ addSkillRecord(skillFileRecord(full, ent.name, constants), results, budget);
101
107
  }
102
108
  }
109
+ function addSkillRecord(record, results, budget) {
110
+ if (!record)
111
+ return;
112
+ results.push(record);
113
+ budget.files += 1;
114
+ const content = record.raw_content.content;
115
+ if (typeof content === 'string')
116
+ budget.bytes += Buffer.byteLength(content);
117
+ }
103
118
  function collectSkillFiles(skillsDir, results, constants) {
104
119
  const skillFilename = getPluginSkillFilename(constants);
105
120
  try {
@@ -228,4 +243,4 @@ function collectConfigFilesFromInstalledPlugins(constants) {
228
243
  }
229
244
  return results;
230
245
  }
231
- export { collectConfigFilesFromInstalledPlugins, collectPluginCacheMcpFiles };
246
+ export { collectConfigFilesFromInstalledPlugins, collectPluginCacheMcpFiles, isSecretName };
@@ -20,6 +20,7 @@ import { createSignature } from '../sender/signing.js';
20
20
  import { readStoredAuthKey } from '../auth/auth_key_store.js';
21
21
  import { resolveHardwareUuid } from './hardware_uuid.js';
22
22
  import { buildLogMetadataObject } from './log_metadata.js';
23
+ import { SCRUBBER_VERSION, scrubFields } from '../../pii_scrub.js';
23
24
  const CLIENT_EVENT_PATH = '/endpoint_security/api/client-event/';
24
25
  const SEND_TIMEOUT_MS = 4000;
25
26
  const BUFFER_BASENAME = 'client_events.jsonl';
@@ -198,6 +199,16 @@ function claimBufferedEvents() {
198
199
  return [];
199
200
  }
200
201
  }
202
+ /**
203
+ * An event as it is sent: its free-text fields (a failure's log, a command line, an error and its
204
+ * stack) scrubbed like tool-call content, and the scrubber's version in its metadata. Buffered
205
+ * events are scrubbed here too, on their way out.
206
+ */
207
+ function forWire(event) {
208
+ const scrubbed = scrubFields(event, ['command', 'message', 'detail', 'log_text']);
209
+ const metadata = (event.metadata ?? {});
210
+ return { ...scrubbed, metadata: { ...metadata, scrubber_version: SCRUBBER_VERSION } };
211
+ }
201
212
  async function postEvents(events) {
202
213
  if (events.length === 0)
203
214
  return true;
@@ -210,7 +221,7 @@ async function postEvents(events) {
210
221
  }
211
222
  const base = loadEndpointBase().replace(/\/+$/, '');
212
223
  const url = `${base}${CLIENT_EVENT_PATH}`;
213
- const signingPayload = { hardware_uuid: hardwareUuid, events };
224
+ const signingPayload = { hardware_uuid: hardwareUuid, events: events.map(forWire) };
214
225
  let signature = '';
215
226
  try {
216
227
  const authKey = readStoredAuthKey();
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  import { OPTIMUSLABS_LOGS_REL, LOG_GROUP_COMPLIANCE } from '../../bootstrap_constants.js';
5
5
  import { buildLogMetadataBlock } from './log_metadata.js';
6
+ import { SCRUBBER_VERSION, scrubText } from '../../pii_scrub.js';
6
7
  const COMPLIANCE_SUBDIR = LOG_GROUP_COMPLIANCE;
7
8
  const TIER_NOOP = 'noop';
8
9
  const TIER_SUCCESS = 'success';
@@ -318,19 +319,27 @@ function appendLineToSessionLogFile(logPath, message) {
318
319
  /**
319
320
  * Upload full session log to the server (success and failures tiers only, first finalize only).
320
321
  */
321
- export async function uploadComplianceSessionLogFromFinalize(result) {
322
+ /** The session log to upload, or null when this finalize does not upload one (and why is logged). */
323
+ function sessionLogToUpload(result) {
322
324
  if (result.tier !== TIER_SUCCESS && result.tier !== TIER_FAILURES)
323
- return;
325
+ return null;
324
326
  if (!result.moved)
325
- return;
327
+ return null;
326
328
  const logText = readComplianceSessionLog(result.logPath);
327
329
  if (!logText.trim())
328
- return;
330
+ return null;
329
331
  const logBytes = Buffer.byteLength(logText, 'utf8');
330
332
  if (logBytes > MAX_SESSION_LOG_UPLOAD_BYTES) {
331
333
  appendLineToSessionLogFile(result.logPath, `compliance_session_upload: skipped (${logBytes} bytes exceeds ${MAX_SESSION_LOG_UPLOAD_BYTES})`);
332
- return;
334
+ return null;
333
335
  }
336
+ return { logText, logBytes };
337
+ }
338
+ export async function uploadComplianceSessionLogFromFinalize(result) {
339
+ const toUpload = sessionLogToUpload(result);
340
+ if (!toUpload)
341
+ return;
342
+ const { logText, logBytes } = toUpload;
334
343
  const { readStoredAuthKey } = await import('../auth/auth_key_store.js');
335
344
  const { tryResolveHardwareUuid } = await import('./hardware_uuid.js');
336
345
  const { loadEndpointBase } = await import('../sender/endpoint_config.js');
@@ -349,10 +358,11 @@ export async function uploadComplianceSessionLogFromFinalize(result) {
349
358
  hardware_uuid: hardwareUuid,
350
359
  outcome,
351
360
  session_filename: sessionFilename,
352
- log_text: logText,
361
+ // Signed as scrubbed; the version sits outside the signed fields.
362
+ log_text: scrubText(logText),
353
363
  };
354
364
  const signature = createSignature(signingPayload, authKey.key);
355
- const body = JSON.stringify({ ...signingPayload, signature });
365
+ const body = JSON.stringify({ ...signingPayload, scrubber_version: SCRUBBER_VERSION, signature });
356
366
  const url = buildApiUrl(loadEndpointBase(), '/endpoint_security/api/remediation-activity-log/');
357
367
  try {
358
368
  const { statusCode } = await executeBody(url, 'POST', body, 30_000);
@@ -30,23 +30,59 @@ function collectPromptsFromConfig(cfg) {
30
30
  export function readCollectPrompts() {
31
31
  return collectPromptsFromConfig(readOrgConfig());
32
32
  }
33
- export function persistCollectPrompts(value) {
34
- if (typeof value !== "boolean")
35
- return;
33
+ /** The org's fingerprint key: hex, at least 128 bits. Keys the prompt hash and secret fingerprints. */
34
+ const FINGERPRINT_KEY = /^(?:[0-9a-f]{2}){16,}$/i;
35
+ /**
36
+ * The org key the backend sends in the startup and inventory responses (`fingerprint_key`),
37
+ * or undefined until it has arrived: the prompt hash stays plain SHA-256 and secrets get no
38
+ * fingerprint until then.
39
+ */
40
+ export function readFingerprintKey() {
41
+ const key = readOrgConfig().fingerprint_key;
42
+ return typeof key === "string" && FINGERPRINT_KEY.test(key) ? key : undefined;
43
+ }
44
+ function writeOrgConfig(update) {
36
45
  const filePath = orgConfigPath();
37
46
  try {
38
47
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
39
- const next = { ...readOrgConfig(), collect_prompts: value };
40
- delete next.hash_tool_call_prompts;
41
- fs.writeFileSync(filePath, `${JSON.stringify(next)}\n`, { encoding: "utf8", mode: 0o600 });
48
+ fs.writeFileSync(filePath, `${JSON.stringify(update(readOrgConfig()))}\n`, { encoding: "utf8", mode: 0o600 });
42
49
  }
43
50
  catch {
44
51
  // Best-effort: next heartbeat can retry.
45
52
  }
46
53
  }
54
+ export function persistCollectPrompts(value) {
55
+ if (typeof value !== "boolean")
56
+ return;
57
+ writeOrgConfig((current) => {
58
+ const next = { ...current, collect_prompts: value };
59
+ delete next.hash_tool_call_prompts;
60
+ return next;
61
+ });
62
+ }
63
+ function persistFingerprintKey(value) {
64
+ if (typeof value !== "string" || !FINGERPRINT_KEY.test(value) || value === readFingerprintKey())
65
+ return;
66
+ writeOrgConfig((current) => ({ ...current, fingerprint_key: value }));
67
+ }
68
+ /**
69
+ * Whether the organization scrubs what endpoints send (OPT-942), read by `@optimuslabs/pii-scrub`
70
+ * from this file: only an explicit `false` switches scrubbing off.
71
+ */
72
+ function persistScrubPii(value) {
73
+ if (typeof value !== "boolean" || readOrgConfig().scrub_pii === value)
74
+ return;
75
+ writeOrgConfig((current) => ({ ...current, scrub_pii: value }));
76
+ }
77
+ /**
78
+ * Store the org options a startup or inventory response carries: prompt collection, the org key
79
+ * and whether to scrub.
80
+ */
47
81
  export function persistCollectPromptsFromResponse(body) {
48
82
  if (!body || typeof body !== "object")
49
83
  return;
84
+ persistFingerprintKey(body.fingerprint_key);
85
+ persistScrubPii(body.scrub_pii);
50
86
  if (typeof body.collect_prompts === "boolean") {
51
87
  persistCollectPrompts(body.collect_prompts);
52
88
  return;
@@ -16,6 +16,8 @@ import { clearRemediationApplyQuarantine, readRemediationApplyTrackingFile, writ
16
16
  import { CURSOR_SCALAR_ITEMTABLE_FIELDS, buildVscdbPostApplyRawContent, persistVscdbComposerContractFromPatternsResponse, } from '../readers/vscdb_reader.js';
17
17
  import { sendConfigFile } from '../sender/batch_sender.js';
18
18
  import { parseWorktreeRootFromPath } from './worktree_scanner.js';
19
+ import { SCRUBBER_VERSION, containsPlaceholder, scrubValue } from '../../pii_scrub.js';
20
+ import { readFingerprintKey } from './prompt-collection-policy.js';
19
21
  import { reportAbsentWorktrees } from './worktree_absent.js';
20
22
  import { buildApiUrl, getFileCollectionPatterns } from '../../endpoint_client/registry_api.js';
21
23
  import { resolveRemediationConfigPath } from './remediation_config_path.js';
@@ -69,8 +71,21 @@ function readInstructions() {
69
71
  const { remediations } = readRemediationInstructionsFile();
70
72
  return { remediations: remediations };
71
73
  }
74
+ /**
75
+ * Instructions whose fix would write a scrubbed placeholder are never stored, so never applied.
76
+ * The server holds only the scrubbed copy of a config; writing a placeholder back would replace
77
+ * the user's real key or token with `[OL_REDACTED:...]`.
78
+ */
79
+ function withoutScrubbedFixes(remediations) {
80
+ return remediations.filter((inst) => {
81
+ if (!containsPlaceholder(remediationFixSpec(inst)?.checks ?? []))
82
+ return true;
83
+ hookRunLog(`remediation_instructions_skipped: uuid=${inst.uuid} reason=fix carries a scrubbed placeholder`);
84
+ return false;
85
+ });
86
+ }
72
87
  function writeInstructions(file) {
73
- writeRemediationInstructionsFile(file);
88
+ writeRemediationInstructionsFile({ ...file, remediations: withoutScrubbedFixes(file.remediations) });
74
89
  }
75
90
  /**
76
91
  * Merge persisted rows with API overlay; GET manifest for enforced UUIDs still missing rows.
@@ -1577,12 +1592,12 @@ export function reportAutofixApplied(remediationUuid, result, details) {
1577
1592
  payload.failure_reason = details.failure_reason.slice(0, 2000);
1578
1593
  if (details?.consecutive_failures != null)
1579
1594
  payload.consecutive_failures = details.consecutive_failures;
1580
- const bodyPayload = { ...payload, signature: '' };
1595
+ const bodyPayload = { ...payload, scrubber_version: SCRUBBER_VERSION, signature: '' };
1581
1596
  if (details?.config_snapshot_after && typeof details.config_snapshot_after === 'object') {
1582
- bodyPayload.config_snapshot_after = details.config_snapshot_after;
1597
+ bodyPayload.config_snapshot_after = scrubValue(details.config_snapshot_after, { fingerprintKey: readFingerprintKey() });
1583
1598
  }
1584
1599
  if (details?.config_snapshot_before && typeof details.config_snapshot_before === 'object') {
1585
- bodyPayload.config_snapshot_before = details.config_snapshot_before;
1600
+ bodyPayload.config_snapshot_before = scrubValue(details.config_snapshot_before, { fingerprintKey: readFingerprintKey() });
1586
1601
  }
1587
1602
  const signature = createSignature(payload, authKey.key);
1588
1603
  const body = JSON.stringify({ ...bodyPayload, signature });
@@ -3,9 +3,10 @@ import fs from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { hookRunLog } from './hook_logger.js';
6
+ import { scrubText } from '../../pii_scrub.js';
6
7
  const GIT_TIMEOUT_MS = 3000;
7
8
  const MAX_WALK_UP = 25;
8
- function gitRemoteInDir(dir) {
9
+ function readGitRemote(dir) {
9
10
  try {
10
11
  return execFileSync('git', ['remote', '-v'], {
11
12
  cwd: dir,
@@ -18,6 +19,13 @@ function gitRemoteInDir(dir) {
18
19
  return '';
19
20
  }
20
21
  }
22
+ /**
23
+ * `git remote -v` in `dir`. A remote can embed a token (`https://user:token@host/owner/repo`), so
24
+ * it is scrubbed here, where every remote this package reports comes from; host and path stay.
25
+ */
26
+ function gitRemoteInDir(dir) {
27
+ return scrubText(readGitRemote(dir));
28
+ }
21
29
  function gitToplevelFromDir(dir) {
22
30
  try {
23
31
  return execFileSync('git', ['rev-parse', '--show-toplevel'], {
@@ -51,7 +59,7 @@ export function resolveWorkspaceRepoFromEnv() {
51
59
  ]) {
52
60
  const value = (candidate || '').trim();
53
61
  if (value)
54
- return value;
62
+ return scrubText(value);
55
63
  }
56
64
  return '';
57
65
  }
@@ -4,7 +4,8 @@ import { loadEndpointBase } from './endpoint_config.js';
4
4
  import { hookRunLog } from '../runtime/hook_logger.js';
5
5
  import { canonicalCursorUserStateVscdbPath } from '../runtime/remediation_config_path.js';
6
6
  import { resolveWorkspaceRepoFromEnv } from '../runtime/workspace_repo.js';
7
- import { persistCollectPromptsFromResponse } from '../runtime/prompt-collection-policy.js';
7
+ import { persistCollectPromptsFromResponse, readFingerprintKey } from '../runtime/prompt-collection-policy.js';
8
+ import { SCRUBBER_VERSION, scrubText, scrubValue } from '../../pii_scrub.js';
8
9
  import fs from 'node:fs';
9
10
  import os from 'node:os';
10
11
  import path from 'node:path';
@@ -84,7 +85,8 @@ function buildChunkBody(chunk, hardwareUuid, authKey, hookRequestId, metadata) {
84
85
  const item = {
85
86
  file_type: c.file_type,
86
87
  file_path: canonicalCursorUserStateVscdbPath(c.file_path),
87
- raw_content: c.raw_content,
88
+ raw_content: scrubValue(c.raw_content, { fingerprintKey: readFingerprintKey() }),
89
+ scrubber_version: SCRUBBER_VERSION,
88
90
  };
89
91
  if (c.symlink_info !== undefined)
90
92
  item.symlink_info = c.symlink_info;
@@ -227,11 +229,11 @@ async function sendConfigFile(configFile, hardwareUuid, authKey, repoIdentifier)
227
229
  const endpoint = loadEndpointBase();
228
230
  const apiUrl = `${resolveApiBase(endpoint)}/endpoint_security/log-config-file/`;
229
231
  const uploadPath = canonicalCursorUserStateVscdbPath(configFile.file_path);
230
- const payload = { hardware_uuid: hardwareUuid, file_type: configFile.file_type, file_path: uploadPath, raw_content: configFile.raw_content };
232
+ const payload = { hardware_uuid: hardwareUuid, file_type: configFile.file_type, file_path: uploadPath, raw_content: scrubValue(configFile.raw_content, { fingerprintKey: readFingerprintKey() }) };
231
233
  if (configFile.symlink_info !== undefined)
232
234
  payload.symlink_info = configFile.symlink_info;
233
235
  const signature = createSignature(payload, authKey.key);
234
- const body = { ...payload, signature, key_id: authKey.key_id || '', metadata: { org_identifier: process.env.GITHUB_ORG || process.env.GH_ORG || '', organization_uuid: readOrganizationUuid(), repo_identifier: repoIdentifier ?? resolveWorkspaceRepoFromEnv() } };
236
+ const body = { ...payload, signature, key_id: authKey.key_id || '', metadata: { org_identifier: process.env.GITHUB_ORG || process.env.GH_ORG || '', organization_uuid: readOrganizationUuid(), repo_identifier: repoIdentifier ?? resolveWorkspaceRepoFromEnv(), scrubber_version: SCRUBBER_VERSION } };
235
237
  try {
236
238
  const response = await postStartupPayload(apiUrl, body);
237
239
  if (response.status !== 'accepted') {
@@ -273,8 +275,10 @@ async function sendHookRequestUpdateManifest(hardwareUuid, authKey, hookRequestI
273
275
  const payload = { hardware_uuid: hardwareUuid, hook_request_id: hookRequestId, manifest: manifestNormalized };
274
276
  const signature = createSignature(payload, authKey.key);
275
277
  const body = { ...payload, signature, key_id: authKey.key_id || '' };
278
+ // The hook's log holds its whole environment: scrubbed, outside the signed manifest.
276
279
  if (hookLog)
277
- body.hook_log = hookLog;
280
+ body.hook_log = scrubText(hookLog);
281
+ body.scrubber_version = SCRUBBER_VERSION;
278
282
  try {
279
283
  const data = (await patchPayload(apiUrl, body));
280
284
  if (data.status === 'accepted') {
@@ -4,6 +4,7 @@ import { flushBufferedClientEvents, getStoryId, reportFunctionalFailureAwait, re
4
4
  import { resolveHardwareUuid } from './hardware_uuid.js';
5
5
  import { writeAuthKey, readStoredAuthKey, loadEndpointBase, buildStartupEndpointUrl } from './auth_key_store.js';
6
6
  import { resolveUserProfile } from './user_profile.js';
7
+ import { SCRUBBER_VERSION } from '../pii_scrub.js';
7
8
  import { resolveWorkspaceRepo } from '../log_config_files/runtime/workspace_repo.js';
8
9
  import { collectInstalledHookHashes } from './installed_hook_hashes.js';
9
10
  import { persistCollectPromptsFromResponse } from '../log_config_files/runtime/prompt-collection-policy.js';
@@ -62,6 +63,7 @@ const getMetadata = () => ({
62
63
  user_profile: { ...resolveUserProfile() },
63
64
  // Every installed agent's hooks from disk, not just the launching agent's (OPTIMUS_HOOK_MANIFEST).
64
65
  hook_manifest: { ...parseHookManifest(), ...collectInstalledHookHashes() },
66
+ scrubber_version: SCRUBBER_VERSION,
65
67
  });
66
68
  function buildRequestBody(hardwareUuid, timestamp) {
67
69
  const metadata = getMetadata();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optimuslabs/harness-map-staging",
3
- "version": "1.5.45",
3
+ "version": "1.5.46",
4
4
  "description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,7 +19,7 @@
19
19
  "dev": "ts-node src/cli.ts",
20
20
  "lint": "eslint \"src/**/*.ts\"",
21
21
  "prepare": "npm run build",
22
- "pretest": "npm run build",
22
+ "pretest": "npm run build --prefix ../pii-scrub && npm run build",
23
23
  "test": "vitest run",
24
24
  "test:watch": "vitest"
25
25
  },
@@ -56,6 +56,7 @@
56
56
  "vitest": "^4.1.12"
57
57
  },
58
58
  "dependencies": {
59
+ "@optimuslabs/pii-scrub-staging": "^0.1.0",
59
60
  "@optimuslabs/tofu-staging": "^0.1.18",
60
61
  "axios": "^1.20.0",
61
62
  "canonicalize": "^2.1.0",