@link-assistant/hive-mind 2.10.0 → 2.10.2
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/CHANGELOG.md +12 -0
- package/README.hi.md +2 -0
- package/README.md +2 -0
- package/README.ru.md +2 -0
- package/README.zh.md +2 -0
- package/package.json +1 -1
- package/src/claude.lib.mjs +0 -4
- package/src/cleanup.mjs +18 -6
- package/src/codex.lib.mjs +0 -4
- package/src/configure-claude.mjs +3 -0
- package/src/credential-sanitization-core.lib.mjs +231 -0
- package/src/development-log.lib.mjs +39 -6
- package/src/fix.mjs +3 -0
- package/src/github-error-reporter.lib.mjs +13 -8
- package/src/github-issue-auto-close.lib.mjs +2 -1
- package/src/github-merge-issue-close.lib.mjs +2 -1
- package/src/github.lib.mjs +29 -18
- package/src/hive-screens.mjs +3 -0
- package/src/instrument.mjs +14 -0
- package/src/interactive-mode.lib.mjs +25 -40
- package/src/lib.mjs +89 -50
- package/src/log-upload.lib.mjs +22 -4
- package/src/post-finish-sanitization-sweep.lib.mjs +5 -5
- package/src/review.mjs +3 -1
- package/src/sentry.lib.mjs +27 -8
- package/src/session-monitor.lib.mjs +6 -10
- package/src/session-resume.lib.mjs +43 -23
- package/src/session-store.lib.mjs +3 -1
- package/src/solve.auto-pr.lib.mjs +11 -21
- package/src/solve.error-handlers.lib.mjs +2 -1
- package/src/solve.progress-monitoring.lib.mjs +20 -9
- package/src/solve.results.lib.mjs +9 -15
- package/src/start-screen.mjs +3 -0
- package/src/task.issue-creation.lib.mjs +5 -3
- package/src/task.mjs +21 -8
- package/src/telegram-bot.mjs +6 -3
- package/src/telegram-command-execution.lib.mjs +2 -2
- package/src/telegram-isolation.lib.mjs +2 -0
- package/src/telegram-log-command.lib.mjs +38 -4
- package/src/telegram-safe-reply.lib.mjs +7 -4
- package/src/telegram-solve-queue.lib.mjs +5 -6
- package/src/telegram-tokens-command.lib.mjs +1 -1
- package/src/token-sanitization.lib.mjs +177 -14
- package/src/tool-comments.lib.mjs +5 -5
- package/src/youtrack/youtrack-sync.mjs +4 -3
package/src/github.lib.mjs
CHANGED
|
@@ -7,8 +7,8 @@ import { log, maskToken, cleanErrorMessage, isENOSPC, ghCmdRetry } from './lib.m
|
|
|
7
7
|
import { reportError } from './sentry.lib.mjs';
|
|
8
8
|
import { describeRequestedThinking, githubLimits, timeouts } from './config.lib.mjs';
|
|
9
9
|
import { batchCheckPullRequestsForIssues as batchCheckPRs, batchCheckArchivedRepositories as batchCheckArchived } from './github.batch.lib.mjs';
|
|
10
|
-
import { isSafeToken, isHexInSafeContext, getGitHubTokensFromFiles, getGitHubTokensFromCommand, sanitizeOutput, sanitizeLogContent } from './token-sanitization.lib.mjs';
|
|
11
|
-
export { isSafeToken, isHexInSafeContext, getGitHubTokensFromFiles, getGitHubTokensFromCommand, sanitizeOutput, sanitizeLogContent }; // Re-export for backward compatibility
|
|
10
|
+
import { isSafeToken, isHexInSafeContext, getGitHubTokensFromFiles, getGitHubTokensFromCommand, sanitizeOutput, sanitizeLogContent, sanitizeForPublication, writeSanitizedPublicationFile } from './token-sanitization.lib.mjs';
|
|
11
|
+
export { isSafeToken, isHexInSafeContext, getGitHubTokensFromFiles, getGitHubTokensFromCommand, sanitizeOutput, sanitizeLogContent, sanitizeForPublication, writeSanitizedPublicationFile }; // Re-export for backward compatibility
|
|
12
12
|
import { uploadLogWithGhUploadLog } from './log-upload.lib.mjs';
|
|
13
13
|
import { formatResetTimeWithRelative } from './usage-limit.lib.mjs'; // See: https://github.com/link-assistant/hive-mind/issues/1236
|
|
14
14
|
// Import model info helpers (Issue #1225)
|
|
@@ -336,7 +336,6 @@ export async function attachLogToGitHub(options) {
|
|
|
336
336
|
repo,
|
|
337
337
|
$,
|
|
338
338
|
log,
|
|
339
|
-
sanitizeLogContent,
|
|
340
339
|
verbose = false,
|
|
341
340
|
errorMessage,
|
|
342
341
|
customTitle = `🤖 ${SOLUTION_DRAFT_LOG_MARKER}`,
|
|
@@ -452,7 +451,7 @@ export async function attachLogToGitHub(options) {
|
|
|
452
451
|
if (verbose) {
|
|
453
452
|
await log(' 🔍 Sanitizing log content to mask GitHub tokens...', { verbose: true });
|
|
454
453
|
}
|
|
455
|
-
let logContent = await
|
|
454
|
+
let logContent = await sanitizeForPublication(rawLogContent);
|
|
456
455
|
|
|
457
456
|
// Escape code blocks in the log content to prevent them from breaking markdown formatting
|
|
458
457
|
if (verbose) {
|
|
@@ -637,17 +636,21 @@ ${logContent}
|
|
|
637
636
|
// Create temp log file with sanitized content (no compression, just gh-upload-log)
|
|
638
637
|
const tempLogFile = `/tmp/solution-draft-log-${targetType}-${Date.now()}.txt`;
|
|
639
638
|
// Use the original sanitized content for upload since it's a plain text file
|
|
640
|
-
await
|
|
639
|
+
await writeSanitizedPublicationFile(tempLogFile, rawLogContent);
|
|
641
640
|
|
|
642
641
|
// Use gh-upload-log default auto mode and shared repository fallback.
|
|
643
642
|
const uploadDescription = `Solution draft log for https://github.com/${owner}/${repo}/${targetType === 'pr' ? 'pull' : 'issues'}/${targetNumber}`;
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
643
|
+
let uploadResult;
|
|
644
|
+
try {
|
|
645
|
+
uploadResult = await uploadLogWithGhUploadLog({
|
|
646
|
+
logFile: tempLogFile,
|
|
647
|
+
isPublic: isPublicRepo,
|
|
648
|
+
description: uploadDescription,
|
|
649
|
+
verbose,
|
|
650
|
+
});
|
|
651
|
+
} finally {
|
|
652
|
+
await fs.unlink(tempLogFile).catch(() => {});
|
|
653
|
+
}
|
|
651
654
|
|
|
652
655
|
if (uploadResult.success) {
|
|
653
656
|
// Use rawUrl for direct file access (single chunk) or url for repository (multiple chunks)
|
|
@@ -782,12 +785,16 @@ ${sessionNote}
|
|
|
782
785
|
*${NOW_WORKING_SESSION_IS_ENDED_MARKER}, feel free to review and add any feedback on the solution draft.*`;
|
|
783
786
|
}
|
|
784
787
|
const tempCommentFile = `/tmp/log-upload-comment-${targetType}-${Date.now()}.md`;
|
|
785
|
-
await
|
|
788
|
+
await writeSanitizedPublicationFile(tempCommentFile, logUploadComment);
|
|
786
789
|
// Issue #1625: post via postTrackedCommentFromFile so the returned
|
|
787
790
|
// comment ID is registered in-memory and excluded from the
|
|
788
791
|
// "did the AI post anything?" check.
|
|
789
|
-
|
|
790
|
-
|
|
792
|
+
let posted;
|
|
793
|
+
try {
|
|
794
|
+
posted = await postTrackedCommentFromFile({ $, owner, repo, targetNumber, bodyFile: tempCommentFile });
|
|
795
|
+
} finally {
|
|
796
|
+
await fs.unlink(tempCommentFile).catch(() => {});
|
|
797
|
+
}
|
|
791
798
|
if (posted.ok) {
|
|
792
799
|
const status = getLogUploadTerminalStatus({ errorMessage, errorDuringExecution, isUsageLimit });
|
|
793
800
|
await log(` ${status.emoji} ${status.label} uploaded to ${targetName} as ${isPublicRepo ? 'public' : 'private'} ${uploadTypeLabel}${chunkInfo}${posted.commentId ? ` (comment id=${posted.commentId})` : ''}`);
|
|
@@ -846,12 +853,16 @@ async function attachRegularComment(options, logComment) {
|
|
|
846
853
|
const logStats = await fs.stat(logFile);
|
|
847
854
|
|
|
848
855
|
const tempFile = `/tmp/log-comment-${targetType}-${Date.now()}.md`;
|
|
849
|
-
await
|
|
856
|
+
await writeSanitizedPublicationFile(tempFile, logComment);
|
|
850
857
|
|
|
851
858
|
// Issue #1625: track the posted comment ID so it's excluded from the
|
|
852
859
|
// AI-authored-comment check in --auto-attach-solution-summary.
|
|
853
|
-
|
|
854
|
-
|
|
860
|
+
let posted;
|
|
861
|
+
try {
|
|
862
|
+
posted = await postTrackedCommentFromFile({ $, owner, repo, targetNumber, bodyFile: tempFile });
|
|
863
|
+
} finally {
|
|
864
|
+
await fs.unlink(tempFile).catch(() => {});
|
|
865
|
+
}
|
|
855
866
|
|
|
856
867
|
if (posted.ok) {
|
|
857
868
|
const status = getLogUploadTerminalStatus({ errorMessage, errorDuringExecution, isUsageLimit });
|
package/src/hive-screens.mjs
CHANGED
package/src/instrument.mjs
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
// Lazy-load config only when needed to avoid loading use-m at module initialization
|
|
2
2
|
// This prevents network fetches that can hang during --help or --version
|
|
3
|
+
import { sanitizeCredentialText } from './credential-sanitization-core.lib.mjs';
|
|
4
|
+
|
|
5
|
+
const sanitizeEventValue = (value, seen = new WeakSet()) => {
|
|
6
|
+
if (typeof value === 'string') return sanitizeCredentialText(value);
|
|
7
|
+
if (!value || typeof value !== 'object') return value;
|
|
8
|
+
if (seen.has(value)) return value;
|
|
9
|
+
seen.add(value);
|
|
10
|
+
for (const [key, item] of Object.entries(value)) {
|
|
11
|
+
value[key] = sanitizeEventValue(item, seen);
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
};
|
|
3
15
|
|
|
4
16
|
// Check if Sentry should be disabled
|
|
5
17
|
const shouldDisableSentry = () => {
|
|
@@ -86,6 +98,8 @@ if (!shouldDisableSentry()) {
|
|
|
86
98
|
|
|
87
99
|
// Before send hook to filter out sensitive data
|
|
88
100
|
beforeSend(event) {
|
|
101
|
+
sanitizeEventValue(event);
|
|
102
|
+
|
|
89
103
|
// Filter out sensitive environment variables
|
|
90
104
|
if (event.contexts && event.contexts.runtime && event.contexts.runtime.env) {
|
|
91
105
|
const sensitiveKeys = ['API_KEY', 'TOKEN', 'SECRET', 'PASSWORD', 'ANTHROPIC'];
|
|
@@ -45,12 +45,12 @@ import { formatInteractiveMcpServersList, getInteractiveMcpDiagnostics } from '.
|
|
|
45
45
|
// Use the session-started marker as the single source of truth for the
|
|
46
46
|
// header string, keeping posting and filtering in lock-step.
|
|
47
47
|
import { INTERACTIVE_SESSION_STARTED_MARKER, trackToolCommentId } from './tool-comments.lib.mjs';
|
|
48
|
-
// Issue #1745: every comment body posted by the AI bridge MUST flow
|
|
49
|
-
//
|
|
48
|
+
// Issue #1745/#2111: every comment body posted by the AI bridge MUST flow
|
|
49
|
+
// through the strict publication sanitizer before leaving the process. The leak in
|
|
50
50
|
// xlab2016/space_db_private#20 happened because raw bash-tool stdout
|
|
51
51
|
// (including TELEGRAM_BOT_TOKEN=...) was published verbatim. See
|
|
52
52
|
// docs/case-studies/issue-1745/analysis.md for the full timeline.
|
|
53
|
-
import { containsKnownToken, getAllKnownLocalTokens,
|
|
53
|
+
import { containsKnownToken, getAllKnownLocalTokens, sanitizeForPublication } from './token-sanitization.lib.mjs';
|
|
54
54
|
import { reportInteractiveLeak } from './telegram-leak-notifier.lib.mjs';
|
|
55
55
|
|
|
56
56
|
/**
|
|
@@ -73,15 +73,6 @@ export const createInteractiveHandler = options => {
|
|
|
73
73
|
log,
|
|
74
74
|
verbose = false,
|
|
75
75
|
execFile: execFileFn,
|
|
76
|
-
// Issue #1745: dangerous-skip flags. All default to false; passing them
|
|
77
|
-
// through lets the operator opt out of pattern-based sanitization (for
|
|
78
|
-
// controlled debugging in private repos) while keeping active-token
|
|
79
|
-
// masking on by default.
|
|
80
|
-
skipOutputSanitization = false,
|
|
81
|
-
skipActiveTokensOutputSanitization = false,
|
|
82
|
-
// Pre-existing user content carve-out (issue body / non-bot comments /
|
|
83
|
-
// pre-existing code). When provided, sanitizer leaves these tokens untouched.
|
|
84
|
-
excludeTokens = [],
|
|
85
76
|
// Issue #1843: when true (default), base64 tool-result images are embedded
|
|
86
77
|
// inline; when false they degrade to a metadata note. See createImageRenderer.
|
|
87
78
|
imageUploadEnabled = true,
|
|
@@ -158,39 +149,33 @@ export const createInteractiveHandler = options => {
|
|
|
158
149
|
hits = [];
|
|
159
150
|
}
|
|
160
151
|
|
|
161
|
-
let sanitized;
|
|
162
152
|
try {
|
|
163
|
-
sanitized = await
|
|
164
|
-
knownTokens,
|
|
165
|
-
skipOutputSanitization,
|
|
166
|
-
skipActiveTokensOutputSanitization,
|
|
167
|
-
excludeTokens,
|
|
168
|
-
});
|
|
169
|
-
} catch (err) {
|
|
170
|
-
await log(`⚠️ Interactive mode: sanitizeCommentBody failed: ${err.message} — falling back to raw body MASKED`);
|
|
171
|
-
// Fail closed: if sanitization fails entirely, drop the body to a safe
|
|
172
|
-
// placeholder rather than leaking. Better to lose detail than secrets.
|
|
173
|
-
sanitized = '[redacted: sanitization failed]';
|
|
174
|
-
}
|
|
153
|
+
const sanitized = await sanitizeForPublication(body);
|
|
175
154
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
155
|
+
if (hits.length > 0) {
|
|
156
|
+
await log(`🚨 Interactive mode: known-local token(s) detected in outbound comment — sanitizer masked them. Sources: ${hits.map(h => h.source).join(', ')}`);
|
|
157
|
+
try {
|
|
158
|
+
await reportInteractiveLeak({
|
|
159
|
+
owner,
|
|
160
|
+
repo,
|
|
161
|
+
prNumber,
|
|
162
|
+
tokenHits: hits,
|
|
163
|
+
log,
|
|
164
|
+
});
|
|
165
|
+
} catch (err) {
|
|
166
|
+
if (verbose) {
|
|
167
|
+
await log(`⚠️ Interactive mode: leak notifier failed: ${err.message}`, { verbose: true });
|
|
168
|
+
}
|
|
189
169
|
}
|
|
190
170
|
}
|
|
191
|
-
}
|
|
192
171
|
|
|
193
|
-
|
|
172
|
+
return sanitized;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
// Publication is an external trust boundary. Legacy skip/exclusion
|
|
175
|
+
// options accepted by createInteractiveHandler must never weaken it.
|
|
176
|
+
await log('⚠️ Interactive mode: credential sanitization failed; GitHub mutation blocked.');
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
194
179
|
};
|
|
195
180
|
|
|
196
181
|
/**
|
package/src/lib.mjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { ensureUseM } from './use-m-bootstrap.lib.mjs';
|
|
3
|
+
import { createCredentialStreamSanitizer, maskToken, sanitizeCredentialText } from './credential-sanitization-core.lib.mjs';
|
|
4
|
+
|
|
5
|
+
export { maskToken };
|
|
3
6
|
|
|
4
7
|
// Shared library functions for hive-mind project
|
|
5
8
|
|
|
@@ -36,7 +39,8 @@ if (typeof globalThis.use === 'undefined') {
|
|
|
36
39
|
await ensureUseM();
|
|
37
40
|
}
|
|
38
41
|
|
|
39
|
-
const
|
|
42
|
+
const fsModule = await use('fs');
|
|
43
|
+
const fs = fsModule.promises;
|
|
40
44
|
|
|
41
45
|
// Global reference for log file (can be set by importing module)
|
|
42
46
|
export let logFile = null;
|
|
@@ -83,15 +87,20 @@ export const log = async (message, options = {}) => {
|
|
|
83
87
|
return;
|
|
84
88
|
}
|
|
85
89
|
|
|
90
|
+
const sanitizedMessage = sanitizeCredentialText(message);
|
|
91
|
+
|
|
86
92
|
// Write to file if log file is set
|
|
87
93
|
// Issue #1572: Handle multi-line messages by timestamping each line,
|
|
88
94
|
// so continuation lines don't appear without timestamps in the log file
|
|
89
95
|
if (logFile) {
|
|
90
96
|
const timestamp = new Date().toISOString();
|
|
91
97
|
const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
|
|
92
|
-
const lines =
|
|
98
|
+
const lines = sanitizedMessage.split('\n');
|
|
93
99
|
const logMessage = lines.map(line => `${prefix} ${line}`).join('\n');
|
|
94
|
-
|
|
100
|
+
try {
|
|
101
|
+
await fs.appendFile(logFile, logMessage + '\n', { mode: 0o600 });
|
|
102
|
+
await fs.chmod(logFile, 0o600);
|
|
103
|
+
} catch (error) {
|
|
95
104
|
// Silent fail for file append errors to avoid infinite loop
|
|
96
105
|
// but report to Sentry in verbose mode
|
|
97
106
|
if (global.verboseMode) {
|
|
@@ -101,7 +110,7 @@ export const log = async (message, options = {}) => {
|
|
|
101
110
|
logFile,
|
|
102
111
|
});
|
|
103
112
|
}
|
|
104
|
-
}
|
|
113
|
+
}
|
|
105
114
|
}
|
|
106
115
|
|
|
107
116
|
// Write to console based on level
|
|
@@ -110,15 +119,15 @@ export const log = async (message, options = {}) => {
|
|
|
110
119
|
try {
|
|
111
120
|
switch (level) {
|
|
112
121
|
case 'error':
|
|
113
|
-
console.error(
|
|
122
|
+
console.error(sanitizedMessage);
|
|
114
123
|
break;
|
|
115
124
|
case 'warning':
|
|
116
125
|
case 'warn':
|
|
117
|
-
console.warn(
|
|
126
|
+
console.warn(sanitizedMessage);
|
|
118
127
|
break;
|
|
119
128
|
case 'info':
|
|
120
129
|
default:
|
|
121
|
-
console.log(
|
|
130
|
+
console.log(sanitizedMessage);
|
|
122
131
|
break;
|
|
123
132
|
}
|
|
124
133
|
} finally {
|
|
@@ -151,18 +160,20 @@ export const setupVerboseLogInterceptor = () => {
|
|
|
151
160
|
if (logFile && args.length > 0) {
|
|
152
161
|
const firstArg = String(args[0]);
|
|
153
162
|
if (firstArg.includes('[VERBOSE]')) {
|
|
154
|
-
const message = args.map(a => String(a)).join(' ');
|
|
163
|
+
const message = sanitizeCredentialText(args.map(a => String(a)).join(' '));
|
|
155
164
|
const logMessage = `[${new Date().toISOString()}] [VERBOSE] ${message}`;
|
|
156
165
|
_writingFromLog = true;
|
|
157
|
-
fs.appendFile(logFile, logMessage + '\n'
|
|
158
|
-
|
|
159
|
-
|
|
166
|
+
fs.appendFile(logFile, logMessage + '\n', { mode: 0o600 })
|
|
167
|
+
.then(() => fs.chmod(logFile, 0o600))
|
|
168
|
+
.catch(() => {
|
|
169
|
+
// Silent fail to avoid infinite loops
|
|
170
|
+
});
|
|
160
171
|
}
|
|
161
172
|
}
|
|
162
173
|
|
|
163
174
|
// Always call original console.log (with guard flag set if [VERBOSE])
|
|
164
175
|
try {
|
|
165
|
-
originalConsoleLog(...args);
|
|
176
|
+
originalConsoleLog(...args.map(value => (typeof value === 'string' || Buffer.isBuffer(value) ? sanitizeCredentialText(value) : value)));
|
|
166
177
|
} finally {
|
|
167
178
|
_writingFromLog = false;
|
|
168
179
|
}
|
|
@@ -206,9 +217,12 @@ const invokeWriteCallback = (callback, error = null) => {
|
|
|
206
217
|
const appendInternalDiagnostic = async message => {
|
|
207
218
|
if (!logFile) return;
|
|
208
219
|
const prefix = `[${new Date().toISOString()}] [INTERNAL]`;
|
|
209
|
-
await fs
|
|
210
|
-
|
|
211
|
-
|
|
220
|
+
await fs
|
|
221
|
+
.appendFile(logFile, `${prefix} ${sanitizeCredentialText(message)}\n`, { mode: 0o600 })
|
|
222
|
+
.then(() => fs.chmod(logFile, 0o600))
|
|
223
|
+
.catch(() => {
|
|
224
|
+
// Silent fail to avoid recursive logging errors
|
|
225
|
+
});
|
|
212
226
|
};
|
|
213
227
|
|
|
214
228
|
const formatStreamDiagnostic = stream => {
|
|
@@ -278,14 +292,51 @@ export const setupStdioLogInterceptor = () => {
|
|
|
278
292
|
|
|
279
293
|
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
280
294
|
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
295
|
+
const stdoutSanitizer = createCredentialStreamSanitizer();
|
|
296
|
+
const stderrSanitizer = createCredentialStreamSanitizer();
|
|
281
297
|
installBrokenPipeGuard(process.stdout, 'stdout');
|
|
282
298
|
installBrokenPipeGuard(process.stderr, 'stderr');
|
|
283
299
|
|
|
300
|
+
// Node does not guarantee that the final write contains a newline. Flush
|
|
301
|
+
// retained records on both graceful and explicit exits so buffering never
|
|
302
|
+
// drops the sanitized tail of terminal output or its persistent log.
|
|
303
|
+
const flushPendingRecord = (sanitizer, originalWrite, streamName) => {
|
|
304
|
+
const sanitizedChunk = sanitizer.flush();
|
|
305
|
+
if (!sanitizedChunk) return;
|
|
306
|
+
try {
|
|
307
|
+
originalWrite(sanitizedChunk);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (!isBrokenPipeError(error)) throw error;
|
|
310
|
+
}
|
|
311
|
+
if (logFile && sanitizedChunk.trim()) {
|
|
312
|
+
try {
|
|
313
|
+
const logMessage = `[${new Date().toISOString()}] [${streamName.toUpperCase()}] ${sanitizedChunk}`;
|
|
314
|
+
fsModule.appendFileSync(logFile, logMessage + (sanitizedChunk.endsWith('\n') ? '' : '\n'), { mode: 0o600 });
|
|
315
|
+
fsModule.chmodSync(logFile, 0o600);
|
|
316
|
+
} catch {
|
|
317
|
+
// Exit-time logging cannot safely report another error.
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
const flushPendingOutput = () => {
|
|
322
|
+
flushPendingRecord(stdoutSanitizer, originalStdoutWrite, 'stdout');
|
|
323
|
+
flushPendingRecord(stderrSanitizer, originalStderrWrite, 'stderr');
|
|
324
|
+
};
|
|
325
|
+
process.once('beforeExit', flushPendingOutput);
|
|
326
|
+
process.once('exit', flushPendingOutput);
|
|
327
|
+
|
|
284
328
|
process.stdout.write = (chunk, encoding, callback) => {
|
|
285
|
-
|
|
329
|
+
const normalizedCallback = normalizeWriteCallback(encoding, callback);
|
|
330
|
+
const sanitizedChunk = stdoutSanitizer.write(chunk);
|
|
331
|
+
if (!sanitizedChunk) {
|
|
332
|
+
invokeWriteCallback(normalizedCallback);
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Write only the completed, sanitized record to the terminal.
|
|
286
337
|
const result = safeTerminalWrite({
|
|
287
338
|
originalWrite: originalStdoutWrite,
|
|
288
|
-
chunk,
|
|
339
|
+
chunk: sanitizedChunk,
|
|
289
340
|
encoding,
|
|
290
341
|
callback,
|
|
291
342
|
streamName: 'stdout',
|
|
@@ -293,12 +344,14 @@ export const setupStdioLogInterceptor = () => {
|
|
|
293
344
|
|
|
294
345
|
// Also append to log file if set, but skip if this write originated from log()
|
|
295
346
|
if (logFile && !_writingFromLog) {
|
|
296
|
-
const text =
|
|
347
|
+
const text = sanitizedChunk;
|
|
297
348
|
if (text.trim()) {
|
|
298
349
|
const logMessage = `[${new Date().toISOString()}] [STDOUT] ${text.replace(/\n$/, '')}`;
|
|
299
|
-
fs.appendFile(logFile, logMessage + '\n'
|
|
300
|
-
|
|
301
|
-
|
|
350
|
+
fs.appendFile(logFile, logMessage + '\n', { mode: 0o600 })
|
|
351
|
+
.then(() => fs.chmod(logFile, 0o600))
|
|
352
|
+
.catch(() => {
|
|
353
|
+
// Silent fail to avoid infinite loops
|
|
354
|
+
});
|
|
302
355
|
}
|
|
303
356
|
}
|
|
304
357
|
|
|
@@ -306,10 +359,17 @@ export const setupStdioLogInterceptor = () => {
|
|
|
306
359
|
};
|
|
307
360
|
|
|
308
361
|
process.stderr.write = (chunk, encoding, callback) => {
|
|
309
|
-
|
|
362
|
+
const normalizedCallback = normalizeWriteCallback(encoding, callback);
|
|
363
|
+
const sanitizedChunk = stderrSanitizer.write(chunk);
|
|
364
|
+
if (!sanitizedChunk) {
|
|
365
|
+
invokeWriteCallback(normalizedCallback);
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Write only the completed, sanitized record to the terminal.
|
|
310
370
|
const result = safeTerminalWrite({
|
|
311
371
|
originalWrite: originalStderrWrite,
|
|
312
|
-
chunk,
|
|
372
|
+
chunk: sanitizedChunk,
|
|
313
373
|
encoding,
|
|
314
374
|
callback,
|
|
315
375
|
streamName: 'stderr',
|
|
@@ -317,12 +377,14 @@ export const setupStdioLogInterceptor = () => {
|
|
|
317
377
|
|
|
318
378
|
// Also append to log file if set, but skip if this write originated from log()
|
|
319
379
|
if (logFile && !_writingFromLog) {
|
|
320
|
-
const text =
|
|
380
|
+
const text = sanitizedChunk;
|
|
321
381
|
if (text.trim()) {
|
|
322
382
|
const logMessage = `[${new Date().toISOString()}] [STDERR] ${text.replace(/\n$/, '')}`;
|
|
323
|
-
fs.appendFile(logFile, logMessage + '\n'
|
|
324
|
-
|
|
325
|
-
|
|
383
|
+
fs.appendFile(logFile, logMessage + '\n', { mode: 0o600 })
|
|
384
|
+
.then(() => fs.chmod(logFile, 0o600))
|
|
385
|
+
.catch(() => {
|
|
386
|
+
// Silent fail to avoid infinite loops
|
|
387
|
+
});
|
|
326
388
|
}
|
|
327
389
|
}
|
|
328
390
|
|
|
@@ -330,29 +392,6 @@ export const setupStdioLogInterceptor = () => {
|
|
|
330
392
|
};
|
|
331
393
|
};
|
|
332
394
|
|
|
333
|
-
/**
|
|
334
|
-
* Mask sensitive tokens in text
|
|
335
|
-
* @param {string} token - Token to mask
|
|
336
|
-
* @param {Object} options - Masking options
|
|
337
|
-
* @param {number} [options.minLength=12] - Minimum length to mask
|
|
338
|
-
* @param {number} [options.startChars=3] - Number of characters to show at start
|
|
339
|
-
* @param {number} [options.endChars=3] - Number of characters to show at end
|
|
340
|
-
* @returns {string} Masked token
|
|
341
|
-
*/
|
|
342
|
-
export const maskToken = (token, options = {}) => {
|
|
343
|
-
const { minLength = 12, startChars = 3, endChars = 3 } = options;
|
|
344
|
-
|
|
345
|
-
if (!token || token.length < minLength) {
|
|
346
|
-
return token; // Don't mask very short strings
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const start = token.substring(0, startChars);
|
|
350
|
-
const end = token.substring(token.length - endChars);
|
|
351
|
-
const middle = '*'.repeat(Math.max(token.length - (startChars + endChars), 3));
|
|
352
|
-
|
|
353
|
-
return start + middle + end;
|
|
354
|
-
};
|
|
355
|
-
|
|
356
395
|
/**
|
|
357
396
|
* Format timestamps for use in filenames
|
|
358
397
|
* @param {Date} [date=new Date()] - Date to format
|
package/src/log-upload.lib.mjs
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { ensureUseM } from './use-m-bootstrap.lib.mjs';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { sanitizeForPublication } from './token-sanitization.lib.mjs';
|
|
3
7
|
|
|
4
8
|
// Log upload module for hive-mind
|
|
5
9
|
// Uses gh-upload-log for uploading log files to GitHub
|
|
@@ -142,14 +146,24 @@ export const parseGhUploadLogOutput = outputValue => {
|
|
|
142
146
|
* @param {boolean} [options.verbose=false] - Enable verbose logging
|
|
143
147
|
* @returns {Promise<{success: boolean, url: string|null, rawUrl: string|null, type: 'gist'|'repository'|null, chunks: number, repositoryName?: string|null, repositoryPath?: string|null}>}
|
|
144
148
|
*/
|
|
145
|
-
export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description, verbose = false }) => {
|
|
149
|
+
export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description, verbose = false, runUpload = runGhUploadLogCommand }) => {
|
|
146
150
|
const result = { success: false, url: null, rawUrl: null, type: null, chunks: 1 };
|
|
151
|
+
let privateTempDirectory = null;
|
|
147
152
|
|
|
148
153
|
try {
|
|
154
|
+
const exactSourceBytes = await fs.readFile(logFile, 'utf8');
|
|
155
|
+
const sanitizedLog = await sanitizeForPublication(exactSourceBytes);
|
|
156
|
+
const sanitizedDescription = description ? await sanitizeForPublication(description) : description;
|
|
157
|
+
privateTempDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'hive-mind-log-upload-'));
|
|
158
|
+
await fs.chmod(privateTempDirectory, 0o700);
|
|
159
|
+
const privateLogFile = path.join(privateTempDirectory, 'sanitized.log');
|
|
160
|
+
await fs.writeFile(privateLogFile, sanitizedLog, { encoding: 'utf8', mode: 0o600 });
|
|
161
|
+
await fs.chmod(privateLogFile, 0o600);
|
|
162
|
+
|
|
149
163
|
const commandArgs = buildGhUploadLogArgs({
|
|
150
|
-
logFile,
|
|
164
|
+
logFile: privateLogFile,
|
|
151
165
|
isPublic,
|
|
152
|
-
description,
|
|
166
|
+
description: sanitizedDescription,
|
|
153
167
|
verbose,
|
|
154
168
|
});
|
|
155
169
|
|
|
@@ -157,7 +171,7 @@ export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description,
|
|
|
157
171
|
await log(` 📤 Running: ${formatGhUploadLogCommand(commandArgs)}`, { verbose: true });
|
|
158
172
|
}
|
|
159
173
|
|
|
160
|
-
const uploadResult = await
|
|
174
|
+
const uploadResult = await runUpload(commandArgs);
|
|
161
175
|
const output = (uploadResult.stdout?.toString() || '') + (uploadResult.stderr?.toString() || '');
|
|
162
176
|
|
|
163
177
|
if (uploadResult.code !== 0) {
|
|
@@ -291,6 +305,10 @@ export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description,
|
|
|
291
305
|
});
|
|
292
306
|
await log(` ❌ Error running gh-upload-log: ${error.message}`);
|
|
293
307
|
return result;
|
|
308
|
+
} finally {
|
|
309
|
+
if (privateTempDirectory) {
|
|
310
|
+
await fs.rm(privateTempDirectory, { recursive: true, force: true }).catch(() => {});
|
|
311
|
+
}
|
|
294
312
|
}
|
|
295
313
|
};
|
|
296
314
|
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* @module post-finish-sanitization-sweep
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import {
|
|
24
|
+
import { sanitizeForPublication, getSanitizationStats } from './token-sanitization.lib.mjs';
|
|
25
25
|
import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): caller passes $ already wrapped through wrapDollarWithGhRetry
|
|
26
26
|
|
|
27
27
|
/**
|
|
@@ -57,7 +57,7 @@ const detectBotLogin = async $ => {
|
|
|
57
57
|
* @param {Object} [args.sanitizationOptions] forwarded to sanitizeOutput
|
|
58
58
|
* @returns {Promise<{scanned:number, edited:number, errors:number}>}
|
|
59
59
|
*/
|
|
60
|
-
export const sweepPrConversationComments = async ({ $, owner, repo, prNumber, botLogin, log = async () => {}, sanitizationOptions = {} }) => {
|
|
60
|
+
export const sweepPrConversationComments = async ({ $, owner, repo, prNumber, botLogin, log = async () => {}, sanitizationOptions: _sanitizationOptions = {} }) => {
|
|
61
61
|
const stats = { scanned: 0, edited: 0, errors: 0 };
|
|
62
62
|
let response;
|
|
63
63
|
try {
|
|
@@ -86,7 +86,7 @@ export const sweepPrConversationComments = async ({ $, owner, repo, prNumber, bo
|
|
|
86
86
|
stats.scanned++;
|
|
87
87
|
let sanitized;
|
|
88
88
|
try {
|
|
89
|
-
sanitized = await
|
|
89
|
+
sanitized = await sanitizeForPublication(c.body);
|
|
90
90
|
} catch (err) {
|
|
91
91
|
await log(`⚠️ post-finish sweep: sanitize comment ${c.id} failed: ${err.message || err}`);
|
|
92
92
|
stats.errors++;
|
|
@@ -116,7 +116,7 @@ export const sweepPrConversationComments = async ({ $, owner, repo, prNumber, bo
|
|
|
116
116
|
* @param {Object} args
|
|
117
117
|
* @returns {Promise<{scanned:number, edited:number, errors:number}>}
|
|
118
118
|
*/
|
|
119
|
-
export const sweepPrDescription = async ({ $, owner, repo, prNumber, log = async () => {}, sanitizationOptions = {} }) => {
|
|
119
|
+
export const sweepPrDescription = async ({ $, owner, repo, prNumber, log = async () => {}, sanitizationOptions: _sanitizationOptions = {} }) => {
|
|
120
120
|
const stats = { scanned: 0, edited: 0, errors: 0 };
|
|
121
121
|
let response;
|
|
122
122
|
try {
|
|
@@ -142,7 +142,7 @@ export const sweepPrDescription = async ({ $, owner, repo, prNumber, log = async
|
|
|
142
142
|
stats.scanned++;
|
|
143
143
|
let sanitized;
|
|
144
144
|
try {
|
|
145
|
-
sanitized = await
|
|
145
|
+
sanitized = await sanitizeForPublication(body);
|
|
146
146
|
} catch (err) {
|
|
147
147
|
await log(`⚠️ post-finish sweep: sanitize PR body failed: ${err.message || err}`);
|
|
148
148
|
stats.errors++;
|
package/src/review.mjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { ensureUseM } from './use-m-bootstrap.lib.mjs';
|
|
3
|
+
import { log, setLogFile, getLogFile, formatAligned, extractToolErrorCore, setupStdioLogInterceptor } from './lib.mjs';
|
|
4
|
+
|
|
5
|
+
setupStdioLogInterceptor();
|
|
3
6
|
|
|
4
7
|
// Early exit paths - handle these before loading all modules to speed up testing
|
|
5
8
|
const earlyArgs = process.argv.slice(2);
|
|
@@ -44,7 +47,6 @@ const path = (await use('path')).default;
|
|
|
44
47
|
const fs = (await use('fs')).promises;
|
|
45
48
|
|
|
46
49
|
// Import shared functions from lib.mjs to follow DRY principle
|
|
47
|
-
import { log, setLogFile, getLogFile, formatAligned, extractToolErrorCore } from './lib.mjs';
|
|
48
50
|
import { parseCliArgumentsWithLino } from './cli-arguments.lib.mjs';
|
|
49
51
|
import { reportError } from './sentry.lib.mjs';
|
|
50
52
|
import * as memoryCheck from './memory-check.mjs';
|