@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.
- package/dist/agent_block_window/agent_block_window.jxa +73 -15
- package/dist/log_config_files/collection/plugin_collector.js +70 -55
- package/dist/log_config_files/runtime/client_event_reporter.js +12 -1
- package/dist/log_config_files/runtime/compliance_session_log.js +17 -7
- package/dist/log_config_files/runtime/prompt-collection-policy.js +42 -6
- package/dist/log_config_files/runtime/remediation_sync.js +19 -4
- package/dist/log_config_files/runtime/workspace_repo.js +10 -2
- package/dist/log_config_files/sender/batch_sender.js +9 -5
- package/dist/log_uuid/startup_sender.js +2 -0
- package/package.json +4 -3
|
@@ -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,
|
|
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:
|
|
68
|
+
header { display: flex; align-items: center; justify-content: space-between; padding: 14px 20px;
|
|
42
69
|
border-bottom: 1px solid #3a3c41; }
|
|
43
|
-
.
|
|
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:
|
|
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: #
|
|
74
|
-
.primary:hover { background: #
|
|
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: #
|
|
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><
|
|
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
|
-
|
|
27
|
-
|
|
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
|
|
38
|
-
if (depth > SKILL_MAX_DEPTH)
|
|
39
|
-
return;
|
|
40
|
-
let entries;
|
|
50
|
+
function readSkillDir(dir) {
|
|
41
51
|
try {
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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/
|
|
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",
|