@optimuslabs/harness-map-staging 1.5.44 → 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.
@@ -31,27 +31,51 @@ function esc(value) {
31
31
  .replace(/'/g, ''');
32
32
  }
33
33
 
34
+ /** Manrope, the website's typeface, shipped beside this script (SIL Open Font License, Manrope-OFL.txt). */
35
+ const FONT_FILE = 'manrope-latin-wght-normal.woff2';
36
+
37
+ /**
38
+ * The website's typeface as an embedded @font-face, read from the font file beside this script
39
+ * (osascript's own arguments name the script's path), so the window needs no network. '' when it
40
+ * cannot be read: the window then falls back to the system font.
41
+ */
42
+ function fontFace() {
43
+ try {
44
+ const args = ObjC.deepUnwrap($.NSProcessInfo.processInfo.arguments) || [];
45
+ const script = args.find((a) => String(a).endsWith('agent_block_window.jxa'));
46
+ if (!script) return '';
47
+ const data = $.NSData.dataWithContentsOfFile(String(script).replace(/agent_block_window\.jxa$/, FONT_FILE));
48
+ if (!data || data.isNil()) return '';
49
+ const b64 = data.base64EncodedStringWithOptions(0).js;
50
+ return `@font-face { font-family: "Manrope"; src: url(data:font/woff2;base64,${b64}) format("woff2"); font-weight: 200 800; }`;
51
+ } catch (e) {
52
+ return '';
53
+ }
54
+ }
55
+
56
+ /**
57
+ * The website's Manrope and logo, its brand green for the primary action and its 8px radius, on the
58
+ * window's own high-contrast colours: solid red and green boxes, near-white text. The website's
59
+ * zinc theme was tried and was harder to read in a small window.
60
+ */
34
61
  const STYLE = `
35
62
  :root { color-scheme: dark; }
36
63
  * { box-sizing: border-box; }
37
64
  html, body { margin: 0; background: transparent; }
38
- body { font: 14px/1.45 -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif; color: #e8e8ea;
65
+ body { font: 14px/1.45 Manrope, -apple-system, BlinkMacSystemFont, sans-serif; color: #e8e8ea;
39
66
  -webkit-user-select: none; cursor: default; }
40
67
  .card { background: #2b2d31; }
41
- header { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px;
68
+ header { display: flex; align-items: center; justify-content: space-between; padding: 14px 20px;
42
69
  border-bottom: 1px solid #3a3c41; }
43
- .brand { display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 15px; color: #cfd0d3; }
44
- .logo { flex: none; width: 24px; height: 24px; border-radius: 50%; background: #c9f2cf; color: #15361c;
45
- display: grid; place-items: center; font: 700 12px/1 ui-rounded, -apple-system, sans-serif; }
46
- .logo.big { width: 44px; height: 44px; font-size: 18px; }
70
+ .logo { display: block; height: 40px; width: auto; margin: -4px 0 -4px -6px; }
47
71
  .scope { font-size: 13px; color: #b7b8bc; }
48
72
  main { padding: 22px 20px 18px; }
49
73
  h1 { font-size: 22px; line-height: 1.25; font-weight: 700; margin: 0 0 16px; color: #fff; }
50
- .box { border-radius: 10px; padding: 14px 16px; margin-bottom: 14px; }
74
+ .box { border-radius: 8px; padding: 14px 16px; margin-bottom: 14px; }
51
75
  .reason { background: #3a2224; border: 1px solid #7a3d3d; display: flex; gap: 12px; }
52
76
  .reason .icon { flex: none; width: 18px; height: 18px; margin-top: 2px; border: 1.6px solid #f0a8a8;
53
- border-radius: 50%; color: #f0a8a8; display: grid; place-items: center; font: 700 11px/1 sans-serif; }
54
- .reason strong { display: block; color: #fff; font-size: 15px; margin-bottom: 4px; }
77
+ border-radius: 50%; color: #f0a8a8; display: grid; place-items: center; font: 700 11px/1 Manrope, sans-serif; }
78
+ .reason strong { display: block; color: #fff; font-size: 15px; font-weight: 700; margin-bottom: 4px; }
55
79
  .reason p { margin: 0; color: #dedee0; }
56
80
  .reason hr { border: 0; border-top: 1px solid #7a3d3d; margin: 12px 0 10px; }
57
81
  .findings { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0; }
@@ -63,24 +87,25 @@ const STYLE = `
63
87
  .row { display: flex; align-items: center; gap: 16px; }
64
88
  .grow { flex: 1; }
65
89
  .label { font-size: 11.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: #8fd39a; }
66
- .path h2 { margin: 4px 0 2px; font-size: 16px; color: #fff; }
90
+ .path h2 { margin: 4px 0 2px; font-size: 16px; font-weight: 700; color: #fff; }
67
91
  .path p { margin: 0; color: #b9bdb9; font-size: 13px; }
68
92
  .path p.sub { color: #d4e7d7; font-size: 13.5px; margin-bottom: 4px; }
69
93
  .path hr { border: 0; border-top: 1px solid #44574a; margin: 14px 0 12px; }
70
94
  .also { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; color: #b9bdb9; font-size: 13px; }
71
95
  .chip { background: #2f3134; border: 1px solid #4b4d52; border-radius: 7px; padding: 5px 12px; color: #e8e8ea; font-size: 13px; }
72
96
  button { font: inherit; cursor: pointer; border-radius: 8px; }
73
- .primary { background: #2f6fdb; color: #fff; border: 0; padding: 10px 22px; font-weight: 600; font-size: 14px; white-space: nowrap; }
74
- .primary:hover { background: #3a7bea; }
97
+ .primary { background: #22c55e; color: #052e16; border: 0; padding: 10px 22px; font-weight: 700; font-size: 14px; white-space: nowrap; }
98
+ .primary:hover { background: #16a34a; }
75
99
  .secondary { background: #3a3c41; color: #e8e8ea; border: 1px solid #4b4d52; padding: 8px 20px; font-size: 14px; }
76
100
  .secondary:hover { background: #44464b; }
77
- .link { background: none; border: 0; padding: 0; color: #7fb0ff; text-decoration: underline; font-size: 14px; }
101
+ .link { background: none; border: 0; padding: 0; color: #4ade80; text-decoration: underline; font-size: 14px; }
78
102
  footer { display: flex; align-items: center; justify-content: space-between; padding: 14px 20px;
79
103
  border-top: 1px solid #3a3c41; gap: 12px; }
80
104
  `;
81
105
 
106
+ /** The website's wordmark for dark backgrounds (frontend_v2/src/assets/optimus_logo_darkbg.svg). */
82
107
  function header(w, withScope) {
83
- return `<header><div class="brand"><span class="logo">o/</span>${esc(w.brand)}</div>${
108
+ return `<header><svg class="logo" role="img" aria-label="${esc(w.brand)}" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 600 214.5"> <ellipse fill="#7cea9c" cx="122.2" cy="107.3" rx="72.1" ry="72.2"/> <g> <path fill="#ffffff" d="M86.3,107.4c0-9.7,3.5-18,10.4-24.8,7-6.8,15.5-10.2,25.5-10.2s18.6,3.4,25.6,10.3c6.9,6.9,10.4,15.3,10.4,25.3s-3.5,18.6-10.4,25.4c-7,6.8-15.6,10.2-25.8,10.2s-18.6-3.5-25.4-10.4c-6.8-6.8-10.2-15.4-10.2-25.7ZM102.2,107.6c0,6.7,1.8,12.1,5.4,16,3.7,4,8.6,5.9,14.6,5.9s11-2,14.6-5.9c3.6-3.9,5.5-9.2,5.5-15.7s-1.8-11.8-5.5-15.7c-3.7-4-8.6-6-14.6-6s-10.8,2-14.5,6c-3.7,4-5.5,9.1-5.5,15.4Z"/> <path fill="#ffffff" d="M187.2,178.2h-15.6v-104.1h15.6v7.3c6.1-6.1,13.1-9.2,20.9-9.2s16.9,3.4,22.9,10.2c6.1,6.8,9.1,15.3,9.1,25.7s-3,18.5-9.1,25.3c-6,6.7-13.6,10-22.7,10s-14.9-3.2-21.2-9.5v44.2ZM224.2,108.3c0-6.5-1.8-11.7-5.3-15.8-3.6-4.1-8-6.2-13.4-6.2s-10.4,2-13.9,6c-3.6,4-5.3,9.2-5.3,15.6s1.8,11.5,5.3,15.6c3.5,4,8.1,6,13.8,6s9.8-2,13.4-6.1c3.6-4.1,5.4-9.1,5.4-15.2Z"/> <path fill="#ffffff" d="M270.9,88.7v52.9h-15.6v-52.9h-6.6v-14.5h6.6v-24.7h15.6v24.7h12.1v14.5h-12.1Z"/> <path fill="#ffffff" d="M288.3,46.1c0-2.7,1-5.1,3-7.1,2-2,4.4-3,7.1-3s5.2,1,7.2,3c2,1.9,3,4.3,3,7.1s-1,5.2-3,7.2c-1.9,2-4.3,3-7.1,3s-5.2-1-7.2-3c-2-2-3-4.4-3-7.3ZM306.2,74.2v67.4h-15.6v-67.4h15.6Z"/> <path fill="#ffffff" d="M323,74.2h15.6v6.2c3-3.1,5.6-5.3,7.7-6.4,2.3-1.2,5.1-1.7,8.5-1.7,7.6,0,13.6,3.3,18.1,10,4.9-6.6,11.5-10,19.9-10,15.2,0,22.8,9.2,22.8,27.6v41.7h-15.6v-37.4c0-6.5-.8-11-2.4-13.7-1.6-2.7-4.3-4.1-8-4.1s-7.4,1.6-9.4,4.8c-2,3.2-2.9,8.4-2.9,15.6v34.8h-15.6v-37.2c0-12-3.5-18-10.4-18s-7.6,1.6-9.6,4.9c-2,3.3-3,8.4-3,15.5v34.8h-15.6v-67.4Z"/> <path fill="#ffffff" d="M447.6,74.2v38.7c0,11.2,4.4,16.7,13.2,16.7s13.2-5.6,13.2-16.7v-38.7h15.6v39c0,5.4-.7,10.1-2,14-1.3,3.5-3.5,6.7-6.7,9.5-5.3,4.6-11.9,6.8-20.1,6.8s-14.7-2.3-20-6.8c-3.2-2.8-5.5-6-6.8-9.5-1.3-3.1-1.9-7.8-1.9-14v-39h15.6Z"/> <path fill="#ffffff" d="M545.5,85.6l-12.9,6.8c-2-4.2-4.5-6.2-7.5-6.2s-2.7.5-3.7,1.4c-1,.9-1.5,2.2-1.5,3.6,0,2.6,3,5.1,9,7.7,8.3,3.6,13.8,6.8,16.7,9.8,2.9,3,4.3,7,4.3,12.1s-2.4,11.9-7.2,16.3c-4.7,4.2-10.3,6.2-16.9,6.2-11.3,0-19.3-5.5-24-16.5l13.3-6.2c1.8,3.2,3.3,5.3,4.2,6.2,1.9,1.8,4.2,2.6,6.8,2.6,5.3,0,7.9-2.4,7.9-7.2s-2-5.3-6.1-7.7c-1.6-.8-3.1-1.5-4.7-2.3-1.6-.7-3.2-1.5-4.8-2.3-4.5-2.2-7.7-4.4-9.5-6.6-2.4-2.8-3.5-6.4-3.5-10.9s2-10.7,6-14.5c4.1-3.8,9.1-5.7,14.9-5.7,8.6,0,15,4.5,19.2,13.4Z"/> </g> <g> <path fill="#7cea9c" d="M207.4,149.9l-4.6,27.9h-4.9l4.6-27.9h4.9Z"/> <path fill="#7cea9c" d="M214,178.2c-1.6,0-3-.4-4.2-1.3-1.2-.8-2-2.1-2.5-3.7-.5-1.6-.5-3.6-.1-5.8.4-2.3,1.1-4.3,2.1-5.9,1-1.6,2.3-2.8,3.7-3.6s3-1.2,4.6-1.2,2.2.2,2.9.6c.7.4,1.3.9,1.6,1.5.4.6.6,1.1.8,1.6h.2l.6-3.5h4.9l-3.5,20.9h-4.8l.5-3.3h-.3c-.3.5-.8,1.1-1.4,1.6-.6.6-1.3,1.1-2.1,1.5s-1.9.6-3.1.6ZM216,174.2c1.1,0,2-.3,2.8-.9.8-.6,1.5-1.4,2.1-2.4.6-1,.9-2.2,1.2-3.6s.2-2.5,0-3.5c-.2-1-.6-1.8-1.3-2.3-.6-.6-1.5-.8-2.6-.8s-2.1.3-2.9.9c-.8.6-1.5,1.4-2.1,2.4s-.9,2.2-1.1,3.4c-.2,1.3-.2,2.5,0,3.5.2,1,.6,1.8,1.3,2.4.6.6,1.5.9,2.6.9Z"/> <path fill="#7cea9c" d="M239.9,178.2c-1.2,0-2.2-.2-3-.6s-1.3-.9-1.7-1.5-.7-1.1-.9-1.6h-.3l-.6,3.4h-4.8l4.6-27.9h4.9l-1.8,10.4h.2c.3-.5.8-1.1,1.3-1.7.5-.6,1.2-1.1,2-1.5s1.8-.6,3-.6,2.7.3,3.8,1c1.1.6,1.9,1.6,2.6,2.8s.9,2.8.9,4.5-.2,3.1-.7,4.6-1.1,3-1.9,4.3-1.9,2.3-3.2,3.1c-1.3.8-2.8,1.2-4.5,1.2ZM238.7,174.2c1.1,0,2.1-.3,2.9-.9.8-.6,1.5-1.3,2.1-2.2s1-1.9,1.2-2.9c.3-1,.4-2,.4-3,0-1.4-.3-2.5-.9-3.3-.6-.8-1.6-1.2-2.9-1.2s-2,.3-2.9.8-1.5,1.2-2.1,2.1c-.5.9-1,1.8-1.2,2.9-.3,1.1-.4,2.1-.4,3.1s.3,2.5.9,3.4c.6.8,1.6,1.2,2.8,1.2Z"/> <path fill="#7cea9c" d="M259.1,178.2c-1.6,0-3-.2-4.2-.7s-2.1-1.1-2.8-2c-.7-.9-1.1-1.9-1.2-3.1,0,0,0-.1,0-.2,0,0,0-.1,0-.2l4.6-.5c.1,1,.4,1.8,1,2.2.6.5,1.5.7,2.6.7s1.5-.1,2.2-.4,1.2-.6,1.7-1c.4-.4.7-.9.7-1.4s0-1-.4-1.4c-.4-.4-1-.6-1.9-.8l-3.4-.8c-1.8-.4-3.2-1.1-4-2.1-.8-1-1.2-2.2-1-3.7.1-1.3.6-2.5,1.5-3.5.9-1,2-1.7,3.3-2.2s2.8-.8,4.4-.8c2.3,0,4.1.5,5.3,1.4,1.2.9,2,2.1,2.2,3.6,0,0,0,.2,0,.3,0,0,0,.2,0,.3l-4.4.5c-.1-.7-.4-1.3-.9-1.8-.5-.5-1.3-.7-2.4-.7s-1.4.1-2,.3-1.2.5-1.6.9c-.4.4-.7.9-.7,1.4,0,.5,0,1,.4,1.4.4.4,1,.6,2,.9l3.4.7c1.8.4,3.2,1.1,4,2s1.2,2.1,1,3.6c-.1,1-.4,2-1,2.8-.5.8-1.3,1.5-2.2,2.1-.9.6-1.9,1-3.1,1.4-1.1.3-2.3.5-3.6.5Z"/> </g> </svg>${
84
109
  withScope ? `<div class="scope">${esc(w.scope)}</div>` : ''
85
110
  }</header>`;
86
111
  }
@@ -135,7 +160,7 @@ function formView(w) {
135
160
  }
136
161
 
137
162
  function pageHtml(p, selfTest) {
138
- return `<!doctype html><html><head><meta charset="utf-8"><style>${STYLE}</style></head><body>
163
+ return `<!doctype html><html><head><meta charset="utf-8"><style>${fontFace()}${STYLE}</style></head><body>
139
164
  <div class="card" id="card">${formView(p.words)}</div>
140
165
  <script>
141
166
  const post = (m) => window.webkit.messageHandlers.optimus.postMessage(JSON.stringify(m));
@@ -144,10 +169,39 @@ function pageHtml(p, selfTest) {
144
169
  document.querySelectorAll('[data-open]').forEach((b) => b.onclick = () => post({ action: 'open', agent: b.dataset.open, bundle_id: b.dataset.bundle }));
145
170
  document.addEventListener('keydown', (e) => { if (e.key === 'Escape') post({ action: 'cancel' }); });
146
171
  window.addEventListener('load', size);
172
+ // The web view keeps every click, so the window is moved from here: pressing the header (the
173
+ // logo bar, where a title bar would be) sends how far the pointer has moved on the screen.
174
+ const head = document.querySelector('header');
175
+ let from = null;
176
+ head.addEventListener('pointerdown', (e) => {
177
+ from = { x: e.screenX, y: e.screenY };
178
+ head.setPointerCapture(e.pointerId);
179
+ post({ type: 'drag', phase: 'start' });
180
+ });
181
+ head.addEventListener('pointermove', (e) => {
182
+ if (from) post({ type: 'drag', dx: e.screenX - from.x, dy: e.screenY - from.y });
183
+ });
184
+ const stop = () => { from = null; };
185
+ head.addEventListener('pointerup', stop);
186
+ head.addEventListener('pointercancel', stop);
147
187
  ${selfTest ? "window.addEventListener('load', () => setTimeout(() => post({ action: 'cancel' }), 300));" : ''}
148
188
  </script></body></html>`;
149
189
  }
150
190
 
191
+ /** Where the window was when the header was pressed. */
192
+ let dragOrigin = null;
193
+
194
+ /** Move the window by how far the pointer has moved since the header was pressed (screen y is down). */
195
+ function dragWindow(win, m) {
196
+ if (m.phase === 'start') {
197
+ const frame = win.frame;
198
+ dragOrigin = { x: frame.origin.x, y: frame.origin.y };
199
+ return;
200
+ }
201
+ if (!dragOrigin || typeof m.dx !== 'number' || typeof m.dy !== 'number') return;
202
+ win.setFrameOrigin($.NSMakePoint(dragOrigin.x + m.dx, dragOrigin.y - m.dy));
203
+ }
204
+
151
205
  function place(win, height) {
152
206
  win.setContentSize($.NSMakeSize(WIDTH, Math.min(Math.max(height, 120), 900)));
153
207
  win.center;
@@ -175,6 +229,10 @@ function run(argv) {
175
229
  place(win, m.height);
176
230
  return;
177
231
  }
232
+ if (m.type === 'drag') {
233
+ dragWindow(win, m);
234
+ return;
235
+ }
178
236
  result = m;
179
237
  },
180
238
  },
@@ -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.44",
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": {
@@ -15,11 +15,11 @@
15
15
  "dialog_prefs-staging": "dist/cli.js"
16
16
  },
17
17
  "scripts": {
18
- "build": "tsc -p tsconfig.json && node ../../../scripts/ensure-bin-executable.mjs && node -e \"const fs=require('fs');fs.mkdirSync('dist/agent_block_window',{recursive:true});fs.copyFileSync('src/agent_block_window/agent_block_window.jxa','dist/agent_block_window/agent_block_window.jxa')\"",
18
+ "build": "tsc -p tsconfig.json && node ../../../scripts/ensure-bin-executable.mjs && node -e \"const fs=require('fs');fs.mkdirSync('dist/agent_block_window',{recursive:true});for(const f of ['agent_block_window.jxa','manrope-latin-wght-normal.woff2','Manrope-OFL.txt'])fs.copyFileSync('src/agent_block_window/'+f,'dist/agent_block_window/'+f)\"",
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",