@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.10.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 4e41ee5: Sanitize credentials consistently across terminal output, logs, development-log artifacts, error telemetry, uploads, and GitHub publication boundaries.
|
|
8
|
+
|
|
9
|
+
## 2.10.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- a9b0967: Fix killed Telegram task resume instructions by recovering the last tool session from the complete task log, rejecting unrelated shared-directory session logs, and preserving the original slash-command alias through queued and restarted sessions.
|
|
14
|
+
|
|
3
15
|
## 2.10.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/README.hi.md
CHANGED
|
@@ -86,6 +86,8 @@ Hive Mind में औसत प्रोग्रामर से अलग
|
|
|
86
86
|
- कभी भी उत्पादन टोकन या क्रेडेंशियल का उपयोग न करना
|
|
87
87
|
- इस सिस्टम के साथ उपयोग किए गए सभी टोकन को रद्द और प्रतिस्थापित करने के लिए तैयार रहना
|
|
88
88
|
|
|
89
|
+
Hive Mind अपने प्रबंधित terminal, log, telemetry, upload और GitHub publication paths में पहचाने गए credentials को sanitize करता है। Scanner विफल होने पर publication रोक दिया जाता है, लेकिन यह किसी autonomous tool को किसी असंबंधित exfiltration path का उपयोग करने से नहीं रोकता। Masking contract, supported formats, maintenance process और incident-response guidance के लिए [Credential sanitization](./docs/CREDENTIAL-SANITIZATION.hi.md) देखें।
|
|
90
|
+
|
|
89
91
|
`hive.mjs` चलाने के लिए न्यूनतम सिस्टम आवश्यकताएँ:
|
|
90
92
|
|
|
91
93
|
```
|
package/README.md
CHANGED
|
@@ -86,6 +86,8 @@ We strongly recommend:
|
|
|
86
86
|
- Never using production tokens or credentials
|
|
87
87
|
- Being prepared to revoke and replace all tokens used with this system
|
|
88
88
|
|
|
89
|
+
Hive Mind sanitizes recognized credentials in its maintained terminal, log, telemetry, upload, and GitHub publication paths. Publication is blocked if the scanner fails, but this does not prevent an autonomous tool from using an unrelated exfiltration path. See [Credential sanitization](./docs/CREDENTIAL-SANITIZATION.md) for the masking contract, supported formats, maintenance process, and incident-response guidance.
|
|
90
|
+
|
|
89
91
|
Minimum system requirements to run `hive.mjs`:
|
|
90
92
|
|
|
91
93
|
```
|
package/README.ru.md
CHANGED
|
@@ -86,6 +86,8 @@ Hive Mind обладает высоким уровнем творчества,
|
|
|
86
86
|
- Никогда не использовать производственные токены или учётные данные
|
|
87
87
|
- Быть готовыми отозвать и заменить все токены, используемые в этой системе
|
|
88
88
|
|
|
89
|
+
Hive Mind скрывает распознанные учётные данные в обслуживаемых им путях вывода терминала, журналов, телеметрии, загрузок и публикаций GitHub. При сбое сканера публикация блокируется, однако это не мешает автономному инструменту использовать посторонний канал утечки. Соглашение о маскировании, поддерживаемые форматы, процесс сопровождения и рекомендации по реагированию на инциденты приведены в разделе [Санитизация учётных данных](./docs/CREDENTIAL-SANITIZATION.ru.md).
|
|
90
|
+
|
|
89
91
|
Минимальные системные требования для запуска `hive.mjs`:
|
|
90
92
|
|
|
91
93
|
```
|
package/README.zh.md
CHANGED
|
@@ -86,6 +86,8 @@ Hive Mind 具备与普通程序员无异的高度创造力。当需求不明确
|
|
|
86
86
|
- 切勿使用生产环境的 Token 或凭证
|
|
87
87
|
- 做好随时撤销并替换本系统所用所有 Token 的准备
|
|
88
88
|
|
|
89
|
+
Hive Mind 会在其维护的终端、日志、遥测、上传和 GitHub 发布路径中清理可识别的凭证。如果扫描器失败,发布会被阻止;但这无法阻止自主工具通过不相关的路径泄露数据。有关掩码约定、支持的格式、维护流程和事件响应指南,请参阅[凭证清理](./docs/CREDENTIAL-SANITIZATION.zh.md)。
|
|
90
|
+
|
|
89
91
|
运行 `hive.mjs` 的最低系统要求:
|
|
90
92
|
|
|
91
93
|
```
|
package/package.json
CHANGED
package/src/claude.lib.mjs
CHANGED
|
@@ -601,10 +601,6 @@ export const executeClaudeCommand = async params => {
|
|
|
601
601
|
$,
|
|
602
602
|
log,
|
|
603
603
|
verbose: argv.verbose,
|
|
604
|
-
// Issue #1745: thread the three independent dangerous-skip flags through
|
|
605
|
-
// so the comment-posting path can honor them; flags default to false.
|
|
606
|
-
skipOutputSanitization: argv['dangerously-skip-output-sanitization'] === true,
|
|
607
|
-
skipActiveTokensOutputSanitization: argv['dangerously-skip-active-tokens-output-sanitization'] === true,
|
|
608
604
|
// Issue #1843: upload & embed images by default; --no-interactive-image-upload opts out.
|
|
609
605
|
imageUploadEnabled: argv['interactive-image-upload'] !== false,
|
|
610
606
|
});
|
package/src/cleanup.mjs
CHANGED
|
@@ -39,6 +39,10 @@ import { isConfirmationYes, readConfirmationLine } from './confirmation.lib.mjs'
|
|
|
39
39
|
import { classifyEntries, summarize, formatBytes, describeReason, buildActiveMatchers, DEFAULT_PROTECTED_NAMES, formatEntryContext, formatTaskSummary, DEFAULT_DOCKER_ISOLATION_CLEANUP_MODE, describeDockerIsolationReason, formatDockerIsolationContainerSummary, normalizeDockerIsolationCleanupMode, planDockerIsolationCleanup } from './cleanup.lib.mjs';
|
|
40
40
|
import { getTempRoot, listTempEntries, getPathSize, readFolderGitInfo, listProcessHeldPaths, getActiveTasks, listSessionTasks, removePath, runSystemCleanup, collectProcessDebugReport, signalOrphanedAgentTrees, listDockerIsolationContainers, removeDockerContainer } from './cleanup.os.lib.mjs';
|
|
41
41
|
import { formatProcessDebugReport } from './process-debug.lib.mjs';
|
|
42
|
+
import { setupStdioLogInterceptor } from './lib.mjs';
|
|
43
|
+
import { sanitizeCredentialText } from './credential-sanitization-core.lib.mjs';
|
|
44
|
+
|
|
45
|
+
setupStdioLogInterceptor();
|
|
42
46
|
|
|
43
47
|
const args = process.argv.slice(2);
|
|
44
48
|
|
|
@@ -179,15 +183,22 @@ const scriptDir = path.dirname(process.argv[1]);
|
|
|
179
183
|
const logFile = path.join(scriptDir, `cleanup-${timestamp}.log`);
|
|
180
184
|
|
|
181
185
|
async function log(message, { level = 'info' } = {}) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
+
const sanitizedMessage = sanitizeCredentialText(message);
|
|
187
|
+
await fsp
|
|
188
|
+
.appendFile(logFile, `[${new Date().toISOString()}] [${level.toUpperCase()}] ${sanitizedMessage}\n`, { mode: 0o600 })
|
|
189
|
+
.then(() => fsp.chmod(logFile, 0o600))
|
|
190
|
+
.catch(() => {});
|
|
191
|
+
if (level === 'error') console.error(sanitizedMessage);
|
|
192
|
+
else if (level === 'warn' || level === 'warning') console.warn(sanitizedMessage);
|
|
193
|
+
else console.log(sanitizedMessage);
|
|
186
194
|
}
|
|
187
195
|
|
|
188
196
|
function vlog(message) {
|
|
189
197
|
if (options.verbose) return log(message);
|
|
190
|
-
return fsp
|
|
198
|
+
return fsp
|
|
199
|
+
.appendFile(logFile, `[${new Date().toISOString()}] [DEBUG] ${sanitizeCredentialText(message)}\n`, { mode: 0o600 })
|
|
200
|
+
.then(() => fsp.chmod(logFile, 0o600))
|
|
201
|
+
.catch(() => {});
|
|
191
202
|
}
|
|
192
203
|
|
|
193
204
|
/**
|
|
@@ -210,7 +221,8 @@ function computeSelfPaths(tempRoot) {
|
|
|
210
221
|
}
|
|
211
222
|
|
|
212
223
|
async function main() {
|
|
213
|
-
await fsp.writeFile(logFile, `# Cleanup Log - ${new Date().toISOString()}\n\n
|
|
224
|
+
await fsp.writeFile(logFile, `# Cleanup Log - ${new Date().toISOString()}\n\n`, { mode: 0o600 }).catch(() => {});
|
|
225
|
+
await fsp.chmod(logFile, 0o600).catch(() => {});
|
|
214
226
|
|
|
215
227
|
const tempRoot = getTempRoot();
|
|
216
228
|
await log('🧹 hive-mind cleanup');
|
package/src/codex.lib.mjs
CHANGED
|
@@ -911,10 +911,6 @@ export const executeCodexCommand = async params => {
|
|
|
911
911
|
$,
|
|
912
912
|
log,
|
|
913
913
|
verbose: argv.verbose,
|
|
914
|
-
// Issue #1745: pass the three independent dangerous-skip flags so the
|
|
915
|
-
// comment-posting path can honor them. All default to false.
|
|
916
|
-
skipOutputSanitization: argv['dangerously-skip-output-sanitization'] === true,
|
|
917
|
-
skipActiveTokensOutputSanitization: argv['dangerously-skip-active-tokens-output-sanitization'] === true,
|
|
918
914
|
// Issue #1843: upload & embed images by default; --no-interactive-image-upload opts out.
|
|
919
915
|
imageUploadEnabled: argv['interactive-image-upload'] !== false,
|
|
920
916
|
});
|
package/src/configure-claude.mjs
CHANGED
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { CONFIGURE_CLAUDE_HELP, formatVerifyReport, parseConfigureClaudeArgs, resolveSettingsPath, runConfigureClaude, verifyConfigureClaude } from './configure-claude.lib.mjs';
|
|
18
|
+
import { setupStdioLogInterceptor } from './lib.mjs';
|
|
19
|
+
|
|
20
|
+
setupStdioLogInterceptor();
|
|
18
21
|
|
|
19
22
|
const args = parseConfigureClaudeArgs(process.argv.slice(2));
|
|
20
23
|
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-free credential sanitization primitives.
|
|
3
|
+
*
|
|
4
|
+
* This module deliberately has no project imports. It is safe to use from the
|
|
5
|
+
* lowest-level terminal and logging code, before asynchronous scanners such as
|
|
6
|
+
* Secretlint have been initialized.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
10
|
+
|
|
11
|
+
export const CREDENTIAL_SANITIZATION_ERROR_CODE = 'ERR_CREDENTIAL_SANITIZATION';
|
|
12
|
+
export const CREDENTIAL_SANITIZATION_FAILURE_MESSAGE = 'Credential sanitization failed; publication was blocked.';
|
|
13
|
+
|
|
14
|
+
const MASKED_VALUE = /^(?:\[REDACTED\]|.{3}….{3})$/u;
|
|
15
|
+
|
|
16
|
+
export const maskToken = token => {
|
|
17
|
+
const value = String(token ?? '');
|
|
18
|
+
if (value.length <= 12) return '[REDACTED]';
|
|
19
|
+
return `${value.slice(0, 3)}…${value.slice(-3)}`;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const maskValue = value => {
|
|
23
|
+
if (!value || MASKED_VALUE.test(value)) return value;
|
|
24
|
+
return maskToken(value);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const escapeRegExp = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
28
|
+
|
|
29
|
+
const VENDOR_PATTERNS = Object.freeze([
|
|
30
|
+
// GitHub: classic, fine-grained, OAuth, user-to-server, server-to-server,
|
|
31
|
+
// refresh, and the stateless server token family.
|
|
32
|
+
/\b(?:github_pat_|gh[pousr]_)[A-Za-z0-9_]{16,}\b/g,
|
|
33
|
+
/\bghs_[A-Za-z0-9_]{16,}\b/g,
|
|
34
|
+
// GitLab personal/project/group/access, OAuth, deploy, runner, CI,
|
|
35
|
+
// feature-flag, session and agent token families.
|
|
36
|
+
/\b(?:glpat|gloas|gldt|glrt|glrtr|glcbt|glptt|glft|glimt|glagent|glwt|glsoat|glffct)-[A-Za-z0-9_-]{16,}\b/g,
|
|
37
|
+
// OpenAI and Anthropic.
|
|
38
|
+
/\bsk-(?:proj-|svcacct-|admin-)?[A-Za-z0-9_-]{16,}\b/g,
|
|
39
|
+
/\bsk-ant-(?:api\d{2}-)?[A-Za-z0-9_-]{16,}\b/g,
|
|
40
|
+
// AWS access-key IDs. Secret keys and session tokens are caught by their
|
|
41
|
+
// assignment/header context below.
|
|
42
|
+
/\b(?:A3T[A-Z0-9]|AKIA|AGPA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}\b/g,
|
|
43
|
+
// Google/GCP API keys and OAuth client secrets.
|
|
44
|
+
/\bAIza[0-9A-Za-z_-]{32,40}\b/g,
|
|
45
|
+
/\bGOCSPX-[0-9A-Za-z_-]{16,}\b/g,
|
|
46
|
+
// Azure application secrets are additionally caught by generic key names
|
|
47
|
+
// such as AccountKey and client_secret. SAS values need a structured rule
|
|
48
|
+
// below so the SharedAccessSignature scheme remains visible.
|
|
49
|
+
// Slack app, bot, user, workspace and configuration tokens.
|
|
50
|
+
/\b(?:xox[a-z]|xapp|xwfp)-[0-9A-Za-z-]{20,}\b/g,
|
|
51
|
+
// Stripe secret/restricted/publishable keys and webhook signing secrets.
|
|
52
|
+
/\b(?:sk|rk|pk)_(?:live|test)_[0-9A-Za-z]{16,}\b/g,
|
|
53
|
+
/\bwhsec_[0-9A-Za-z]{16,}\b/g,
|
|
54
|
+
// SendGrid, Twilio, npm and PyPI.
|
|
55
|
+
/\bSG\.[0-9A-Za-z_-]{10,}\.[0-9A-Za-z_-]{20,}\b/g,
|
|
56
|
+
/\bSK[0-9a-fA-F]{32}\b/g,
|
|
57
|
+
/\bnpm_[0-9A-Za-z]{20,}\b/g,
|
|
58
|
+
/\bpypi-[0-9A-Za-z_-]{30,}\b/g,
|
|
59
|
+
// Telegram, Discord, Hugging Face and common additional service formats.
|
|
60
|
+
/\b[0-9]{8,12}:[0-9A-Za-z_-]{30,}\b/g,
|
|
61
|
+
/\b[MN][0-9A-Za-z_-]{23,}\.[0-9A-Za-z_-]{6}\.[0-9A-Za-z_-]{20,}\b/g,
|
|
62
|
+
/\bhf_[0-9A-Za-z]{20,}\b/g,
|
|
63
|
+
/\bshpat_[0-9a-fA-F]{32}\b/g,
|
|
64
|
+
/\bdapi[0-9a-fA-F]{32}\b/g,
|
|
65
|
+
/\bsq0(?:atp|csp)-[0-9A-Za-z_-]{20,}\b/g,
|
|
66
|
+
// JWT/JWS access, ID, and CI job tokens.
|
|
67
|
+
/\beyJ[0-9A-Za-z_-]{8,}\.eyJ[0-9A-Za-z_-]{8,}\.[0-9A-Za-z_-]{8,}\b/g,
|
|
68
|
+
// Opaque CI/test credentials sometimes carry only an all-caps semantic
|
|
69
|
+
// marker and appear without a useful assignment name.
|
|
70
|
+
/\b(?:[A-Z0-9]+[_-])+(?:TOKEN|SECRET|PASSWORD|CREDENTIAL|API[_-]?KEY)(?:[_-][A-Z0-9]+)+\b/g,
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
const SENSITIVE_KEY = String.raw`(?:[A-Za-z0-9_.-]*(?:api[-_]?key|account[-_]?key|client[-_]?secret|consumer[-_]?secret|webhook[-_]?secret|access[-_]?token|refresh[-_]?token|auth[-_]?token|password|passwd|pwd|private[-_]?key|secret|token|session[-_]?key|session[-_]?token|cookie|docker[-_]?auth|registry[-_]?auth|shared[-_]?access[-_]?signature|sas[-_]?token)[A-Za-z0-9_.-]*|auth|authorization)`;
|
|
74
|
+
const SENSITIVE_ENV_NAME = /(?:API_?KEY|ACCOUNT_?KEY|CLIENT_?SECRET|CONSUMER_?SECRET|WEBHOOK_?SECRET|ACCESS_?TOKEN|REFRESH_?TOKEN|AUTH_?TOKEN|PASSWORD|PASSWD|PRIVATE_?KEY|SECRET|TOKEN|COOKIE|AUTH)$/i;
|
|
75
|
+
const QUOTED_ASSIGNMENT = new RegExp(`((?:["']?${SENSITIVE_KEY}["']?)\\s*(?:=>|[:=])\\s*)(["'])([^"'\\r\\n]*)(\\2)`, 'gi');
|
|
76
|
+
const UNQUOTED_ASSIGNMENT = new RegExp(`((?:["']?${SENSITIVE_KEY}["']?)\\s*(?:=>|[:=])\\s*)(?!["']|(?:Bearer|Basic|SharedAccessSignature)\\s)([^\\s,;}&'"\\r\\n]+)`, 'gi');
|
|
77
|
+
const XML_CREDENTIAL = new RegExp(`(<(${SENSITIVE_KEY})\\b[^>]*>)([\\s\\S]*?)(<\\/\\2\\s*>)`, 'gi');
|
|
78
|
+
const CLI_CREDENTIAL_QUOTED = new RegExp(`(--${SENSITIVE_KEY}(?:\\s+|=))(["'])([^"'\\r\\n]*)(\\2)`, 'gi');
|
|
79
|
+
const CLI_CREDENTIAL = new RegExp(`(--${SENSITIVE_KEY}(?:\\s+|=))(?!["'])([^\\s"'\\r\\n]+)`, 'gi');
|
|
80
|
+
const QUERY_CREDENTIAL = new RegExp(`([?&]${SENSITIVE_KEY}=)([^&#\\s]+)`, 'gi');
|
|
81
|
+
|
|
82
|
+
const replaceVendorSecrets = text => {
|
|
83
|
+
let output = text;
|
|
84
|
+
for (const pattern of VENDOR_PATTERNS) {
|
|
85
|
+
pattern.lastIndex = 0;
|
|
86
|
+
output = output.replace(pattern, match => maskValue(match));
|
|
87
|
+
}
|
|
88
|
+
return output;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const replaceCookieHeader = (_match, prefix, cookieText) => {
|
|
92
|
+
const sanitized = cookieText.replace(/(^|;\s*)([^=;\s]+)(=)([^;\r\n]*)/g, (pair, separator, name, equals, value) => {
|
|
93
|
+
return `${separator}${name}${equals}${maskValue(value.trim())}`;
|
|
94
|
+
});
|
|
95
|
+
return `${prefix}${sanitized}`;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Synchronously sanitize known vendor credentials and credential-like
|
|
100
|
+
* structured values. The operation is deterministic and idempotent.
|
|
101
|
+
*/
|
|
102
|
+
export const sanitizeCredentialText = (input, options = {}) => {
|
|
103
|
+
let output = String(input ?? '');
|
|
104
|
+
|
|
105
|
+
// Known active credentials are the strongest signal and are replaced before
|
|
106
|
+
// format matching, regardless of their shape or length.
|
|
107
|
+
const environmentTokens =
|
|
108
|
+
options.includeEnvironmentCredentials === false
|
|
109
|
+
? []
|
|
110
|
+
: Object.entries(process.env)
|
|
111
|
+
.filter(([name, value]) => SENSITIVE_ENV_NAME.test(name) && typeof value === 'string' && value.length > 0)
|
|
112
|
+
.map(([, value]) => value);
|
|
113
|
+
for (const token of [...environmentTokens, ...(options.knownTokens || [])]) {
|
|
114
|
+
const value = typeof token === 'string' ? token : token?.value;
|
|
115
|
+
if (!value || !output.includes(value)) continue;
|
|
116
|
+
output = output.replace(new RegExp(escapeRegExp(value), 'g'), maskValue(value));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Keep PEM markers for diagnostic context, but never preserve key material.
|
|
120
|
+
output = output.replace(/(-----BEGIN ([A-Z0-9 ]*PRIVATE KEY)-----)[\s\S]*?(-----END \2-----)/g, (_match, begin, _label, end) => `${begin}\n[REDACTED]\n${end}`);
|
|
121
|
+
// An interrupted process may never print the closing PEM marker. Treat the
|
|
122
|
+
// remainder as key material instead of releasing it during stream flush.
|
|
123
|
+
output = output.replace(/(-----BEGIN ([A-Z0-9 ]*PRIVATE KEY)-----)(?![\s\S]*?-----END \2-----)[\s\S]*/g, '$1\n[REDACTED]');
|
|
124
|
+
|
|
125
|
+
output = replaceVendorSecrets(output);
|
|
126
|
+
|
|
127
|
+
// Authentication headers and URL credentials.
|
|
128
|
+
output = output.replace(/((?:Proxy-)?Authorization\s*:\s*(?:Bearer|Basic)\s+)([^\s"',;]+)/gi, (_match, prefix, value) => `${prefix}${maskValue(value)}`);
|
|
129
|
+
output = output.replace(/(\b[a-z][a-z0-9+.-]*:\/\/[^:\s/@]+:)([^@\s/]+)(@)/gi, (_match, prefix, value, suffix) => `${prefix}${maskValue(value)}${suffix}`);
|
|
130
|
+
output = output.replace(/(\bSharedAccessSignature\s+)(sr=[^\s,;]+(?:&[^\s,;]+)+)/gi, (_match, prefix, value) => `${prefix}${maskValue(value)}`);
|
|
131
|
+
output = output.replace(/(\bSharedAccessSignature\s*=\s*)([^\s"',;]+)/gi, (_match, prefix, value) => `${prefix}${maskValue(value)}`);
|
|
132
|
+
|
|
133
|
+
// Cookie values are credentials at output boundaries even when their names
|
|
134
|
+
// are vendor-specific and therefore unknown to us.
|
|
135
|
+
output = output.replace(/((?:Set-)?Cookie\s*:\s*)([^\r\n]*)/gi, replaceCookieHeader);
|
|
136
|
+
|
|
137
|
+
// XML and JSON/YAML/TOML/INI/shell-style assignments.
|
|
138
|
+
output = output.replace(XML_CREDENTIAL, (_match, start, _key, value, end) => `${start}${maskValue(value.trim())}${end}`);
|
|
139
|
+
output = output.replace(QUOTED_ASSIGNMENT, (_match, prefix, quote, value) => `${prefix}${quote}${maskValue(value)}${quote}`);
|
|
140
|
+
output = output.replace(UNQUOTED_ASSIGNMENT, (_match, prefix, value) => `${prefix}${maskValue(value)}`);
|
|
141
|
+
|
|
142
|
+
// CLI arguments and sensitive query parameters.
|
|
143
|
+
output = output.replace(CLI_CREDENTIAL_QUOTED, (_match, prefix, quote, value) => `${prefix}${quote}${maskValue(value)}${quote}`);
|
|
144
|
+
output = output.replace(CLI_CREDENTIAL, (_match, prefix, value) => `${prefix}${maskValue(value)}`);
|
|
145
|
+
output = output.replace(QUERY_CREDENTIAL, (_match, prefix, value) => `${prefix}${maskValue(value)}`);
|
|
146
|
+
|
|
147
|
+
// Vendor webhook URLs encode credentials in their path rather than a named
|
|
148
|
+
// field. Preserve the service endpoint and sanitize only the credential.
|
|
149
|
+
output = output.replace(/(https:\/\/hooks\.slack\.com\/services\/)([0-9A-Za-z/_-]+)/gi, (_match, prefix, value) => `${prefix}${maskValue(value)}`);
|
|
150
|
+
output = output.replace(/(https:\/\/(?:canary\.)?discord(?:app)?\.com\/api\/webhooks\/)([0-9A-Za-z/_-]+)/gi, (_match, prefix, value) => `${prefix}${maskValue(value)}`);
|
|
151
|
+
|
|
152
|
+
return output;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Return a non-sensitive indication that another sanitizer pass would change
|
|
157
|
+
* the text. Callers never receive the matching value.
|
|
158
|
+
*/
|
|
159
|
+
export const findCredentialResiduals = input => {
|
|
160
|
+
const text = String(input ?? '');
|
|
161
|
+
return sanitizeCredentialText(text) === text ? [] : [{ ruleId: 'credential-pattern' }];
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Record-oriented sanitizer for child-process stdout/stderr chunks. An
|
|
166
|
+
* incomplete record is retained until a newline arrives (or flush is called),
|
|
167
|
+
* so credentials split at arbitrary chunk boundaries are never emitted raw.
|
|
168
|
+
*/
|
|
169
|
+
export const createCredentialStreamSanitizer = options => {
|
|
170
|
+
let pending = '';
|
|
171
|
+
let decoder = new StringDecoder('utf8');
|
|
172
|
+
|
|
173
|
+
const appendChunk = chunk => {
|
|
174
|
+
if (Buffer.isBuffer(chunk) || ArrayBuffer.isView(chunk)) {
|
|
175
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
176
|
+
pending += decoder.write(bytes);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
pending += decoder.end();
|
|
180
|
+
decoder = new StringDecoder('utf8');
|
|
181
|
+
pending += String(chunk ?? '');
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const drain = () => {
|
|
185
|
+
let output = '';
|
|
186
|
+
const beginPattern = /-----BEGIN ([A-Z0-9 ]*PRIVATE KEY)-----/;
|
|
187
|
+
|
|
188
|
+
while (pending) {
|
|
189
|
+
const begin = beginPattern.exec(pending);
|
|
190
|
+
if (begin) {
|
|
191
|
+
const endMarker = `-----END ${begin[1]}-----`;
|
|
192
|
+
const endIndex = pending.indexOf(endMarker, begin.index + begin[0].length);
|
|
193
|
+
if (endIndex < 0) {
|
|
194
|
+
// Text before the marker can be released, but the marker and every
|
|
195
|
+
// following byte stay private until the matching end or flush.
|
|
196
|
+
if (begin.index > 0) {
|
|
197
|
+
output += sanitizeCredentialText(pending.slice(0, begin.index), options);
|
|
198
|
+
pending = pending.slice(begin.index);
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const keyEnd = endIndex + endMarker.length;
|
|
204
|
+
output += sanitizeCredentialText(pending.slice(0, keyEnd), options);
|
|
205
|
+
pending = pending.slice(keyEnd);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const boundary = Math.max(pending.lastIndexOf('\n'), pending.lastIndexOf('\r'));
|
|
210
|
+
if (boundary < 0) break;
|
|
211
|
+
output += sanitizeCredentialText(pending.slice(0, boundary + 1), options);
|
|
212
|
+
pending = pending.slice(boundary + 1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return output;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
write(chunk) {
|
|
220
|
+
appendChunk(chunk);
|
|
221
|
+
return drain();
|
|
222
|
+
},
|
|
223
|
+
flush() {
|
|
224
|
+
pending += decoder.end();
|
|
225
|
+
decoder = new StringDecoder('utf8');
|
|
226
|
+
const complete = pending;
|
|
227
|
+
pending = '';
|
|
228
|
+
return sanitizeCredentialText(complete, options);
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
};
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
-
import { createReadStream, createWriteStream } from 'node:fs';
|
|
3
2
|
import os from 'node:os';
|
|
4
3
|
import path from 'node:path';
|
|
5
|
-
import {
|
|
4
|
+
import { sanitizeForPublication } from './token-sanitization.lib.mjs';
|
|
6
5
|
|
|
7
6
|
const sanitizePathSegment = (value, fallback) => {
|
|
8
7
|
const raw = value === null || value === undefined || value === '' ? fallback : String(value);
|
|
@@ -87,9 +86,21 @@ const fileExists = async filePath => {
|
|
|
87
86
|
}
|
|
88
87
|
};
|
|
89
88
|
|
|
89
|
+
const writePrivatePublicationFile = async (destinationPath, content) => {
|
|
90
|
+
const sanitized = await sanitizeForPublication(content);
|
|
91
|
+
await fs.writeFile(destinationPath, sanitized, { encoding: 'utf8', mode: 0o600 });
|
|
92
|
+
// writeFile preserves the mode of an existing file, so enforce it after
|
|
93
|
+
// every write as well.
|
|
94
|
+
await fs.chmod(destinationPath, 0o600);
|
|
95
|
+
};
|
|
96
|
+
|
|
90
97
|
const copyIfExists = async ({ sourcePath, destinationPath }) => {
|
|
91
98
|
if (!(await fileExists(sourcePath))) return false;
|
|
92
|
-
|
|
99
|
+
// Raw local audit sources remain available to the operator but must not be
|
|
100
|
+
// group/world-readable. Only the sanitized copy enters the repository.
|
|
101
|
+
await fs.chmod(sourcePath, 0o600);
|
|
102
|
+
const content = await fs.readFile(sourcePath, 'utf8');
|
|
103
|
+
await writePrivatePublicationFile(destinationPath, content);
|
|
93
104
|
return true;
|
|
94
105
|
};
|
|
95
106
|
|
|
@@ -122,13 +133,15 @@ const findCodexSessionFile = async ({ sessionId, homeDir }) => {
|
|
|
122
133
|
// prefix. Returns the byte offset right after the copied slice.
|
|
123
134
|
const copyLogSlice = async ({ logFile, destinationPath, logStartByte = 0 }) => {
|
|
124
135
|
const stat = await fs.stat(logFile);
|
|
136
|
+
await fs.chmod(logFile, 0o600);
|
|
125
137
|
// The log was rotated/truncated since the previous collection: copy it whole.
|
|
126
138
|
const start = Number.isFinite(logStartByte) && logStartByte > 0 && logStartByte <= stat.size ? logStartByte : 0;
|
|
127
139
|
if (stat.size === 0 || start >= stat.size) {
|
|
128
|
-
await
|
|
140
|
+
await writePrivatePublicationFile(destinationPath, '');
|
|
129
141
|
return { logStartByte: start, logEndByte: stat.size };
|
|
130
142
|
}
|
|
131
|
-
|
|
143
|
+
const bytes = await fs.readFile(logFile);
|
|
144
|
+
await writePrivatePublicationFile(destinationPath, bytes.subarray(start, stat.size).toString('utf8'));
|
|
132
145
|
return { logStartByte: start, logEndByte: stat.size };
|
|
133
146
|
};
|
|
134
147
|
|
|
@@ -245,7 +258,7 @@ export const writeDevelopmentLogArtifacts = async ({ repositoryPath, logFile, is
|
|
|
245
258
|
},
|
|
246
259
|
};
|
|
247
260
|
|
|
248
|
-
await
|
|
261
|
+
await writePrivatePublicationFile(path.join(repositoryPath, metadataRelativePath), `${JSON.stringify(metadata, null, 2)}\n`);
|
|
249
262
|
|
|
250
263
|
return {
|
|
251
264
|
developmentLogDirectory,
|
|
@@ -260,6 +273,21 @@ export const writeDevelopmentLogArtifacts = async ({ repositoryPath, logFile, is
|
|
|
260
273
|
};
|
|
261
274
|
};
|
|
262
275
|
|
|
276
|
+
const verifyDevelopmentLogDirectory = async directoryPath => {
|
|
277
|
+
const entries = await fs.readdir(directoryPath, { recursive: true, withFileTypes: true });
|
|
278
|
+
for (const entry of entries) {
|
|
279
|
+
if (!entry.isFile()) continue;
|
|
280
|
+
const parentPath = entry.parentPath || entry.path;
|
|
281
|
+
const filePath = path.join(parentPath, entry.name);
|
|
282
|
+
const exactBytes = await fs.readFile(filePath, 'utf8');
|
|
283
|
+
const rescanned = await sanitizeForPublication(exactBytes);
|
|
284
|
+
if (rescanned !== exactBytes) {
|
|
285
|
+
throw new Error('Development-log publication rescan found residual credential material.');
|
|
286
|
+
}
|
|
287
|
+
await fs.chmod(filePath, 0o600);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
263
291
|
const getCommandOutput = result => (result?.stderr?.toString?.() || result?.stdout?.toString?.() || '').trim();
|
|
264
292
|
|
|
265
293
|
export const collectAndCommitDevelopmentLogArtifacts = async ({ enabled, repositoryPath, logFile, issueNumber, prNumber, tool, sessionId, branchName, rawCommand, logStartByte = 0, $, log }) => {
|
|
@@ -295,6 +323,11 @@ export const collectAndCommitDevelopmentLogArtifacts = async ({ enabled, reposit
|
|
|
295
323
|
return { ...artifacts, committed: false, pushed: false };
|
|
296
324
|
}
|
|
297
325
|
|
|
326
|
+
// Scan the exact directory bytes immediately before staging. This catches
|
|
327
|
+
// future artifacts added by this workflow even if their writer forgot to
|
|
328
|
+
// call the publication helper.
|
|
329
|
+
await verifyDevelopmentLogDirectory(path.join(repositoryPath, artifacts.relativeDirectory));
|
|
330
|
+
|
|
298
331
|
const addResult = await $({ cwd: repositoryPath })`git add -f -- ${artifacts.relativeDirectory}`;
|
|
299
332
|
if (addResult.code !== 0) {
|
|
300
333
|
await log?.(`⚠️ Could not stage development log: ${getCommandOutput(addResult)}`, { level: 'warning' });
|
package/src/fix.mjs
CHANGED
|
@@ -17,6 +17,9 @@ import path from 'path';
|
|
|
17
17
|
import { spawn } from 'child_process';
|
|
18
18
|
import { fileURLToPath } from 'url';
|
|
19
19
|
import { CI_CD_ISSUE_LABELS, CI_CD_ISSUE_TYPE, buildCiCdIssueBody, buildCiCdIssueTitle, buildSolveArgs, partitionFixArgs, summarizeRunFailures } from './fix.ci-cd.lib.mjs';
|
|
20
|
+
import { setupStdioLogInterceptor } from './lib.mjs';
|
|
21
|
+
|
|
22
|
+
setupStdioLogInterceptor();
|
|
20
23
|
|
|
21
24
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
25
|
|
|
@@ -8,6 +8,7 @@ import { ensureUseM } from './use-m-bootstrap.lib.mjs';
|
|
|
8
8
|
import { createInterface } from 'readline';
|
|
9
9
|
import { log, cleanErrorMessage, getAbsoluteLogPath } from './lib.mjs';
|
|
10
10
|
import { reportError, isSentryEnabled } from './sentry.lib.mjs';
|
|
11
|
+
import { sanitizeForPublication, writeSanitizedPublicationFile } from './token-sanitization.lib.mjs';
|
|
11
12
|
|
|
12
13
|
if (typeof globalThis.use === 'undefined') {
|
|
13
14
|
await ensureUseM();
|
|
@@ -85,14 +86,13 @@ const getCurrentGitHubUser = async () => {
|
|
|
85
86
|
* @returns {Promise<string|null>} Gist URL or null on failure
|
|
86
87
|
*/
|
|
87
88
|
const createSecretGist = async (logContent, filename) => {
|
|
89
|
+
const tempFile = `/tmp/${filename}`;
|
|
88
90
|
try {
|
|
89
|
-
|
|
90
|
-
await fs.writeFile(tempFile, logContent);
|
|
91
|
+
await writeSanitizedPublicationFile(tempFile, logContent);
|
|
91
92
|
|
|
92
93
|
const result = await $`gh gist create ${tempFile} --secret --desc "Error log for hive-mind"`;
|
|
93
94
|
if (result.exitCode === 0) {
|
|
94
95
|
const gistUrl = result.stdout.toString().trim();
|
|
95
|
-
await fs.unlink(tempFile).catch(() => {});
|
|
96
96
|
return gistUrl;
|
|
97
97
|
}
|
|
98
98
|
} catch (error) {
|
|
@@ -100,6 +100,8 @@ const createSecretGist = async (logContent, filename) => {
|
|
|
100
100
|
context: 'create_secret_gist',
|
|
101
101
|
operation: 'gh_gist_create',
|
|
102
102
|
});
|
|
103
|
+
} finally {
|
|
104
|
+
await fs.unlink(tempFile).catch(() => {});
|
|
103
105
|
}
|
|
104
106
|
return null;
|
|
105
107
|
};
|
|
@@ -183,7 +185,7 @@ export const createIssueForError = async options => {
|
|
|
183
185
|
|
|
184
186
|
await log('\n🔄 Creating GitHub issue...');
|
|
185
187
|
|
|
186
|
-
const issueTitle = error.message || errorMessage || `${errorType} in hive-mind
|
|
188
|
+
const issueTitle = await sanitizeForPublication(error.message || errorMessage || `${errorType} in hive-mind`);
|
|
187
189
|
|
|
188
190
|
let issueBody = '## Error Details\n\n';
|
|
189
191
|
issueBody += `**Type**: ${errorType}\n`;
|
|
@@ -232,11 +234,14 @@ export const createIssueForError = async options => {
|
|
|
232
234
|
issueBody += `*This issue was automatically created by @${currentUser} using hive-mind error reporting*\n`;
|
|
233
235
|
|
|
234
236
|
const tempBodyFile = `/tmp/hive-mind-issue-body-${Date.now()}.md`;
|
|
235
|
-
await
|
|
237
|
+
await writeSanitizedPublicationFile(tempBodyFile, issueBody);
|
|
236
238
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
239
|
+
let result;
|
|
240
|
+
try {
|
|
241
|
+
result = await $`gh issue create --repo link-assistant/hive-mind --title ${issueTitle} --body-file ${tempBodyFile} --label bug`;
|
|
242
|
+
} finally {
|
|
243
|
+
await fs.unlink(tempBodyFile).catch(() => {});
|
|
244
|
+
}
|
|
240
245
|
|
|
241
246
|
if (result.exitCode === 0) {
|
|
242
247
|
const issueUrl = result.stdout.toString().trim();
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
import { prClosesIssue, extractLinkedIssueNumber } from './github-linking.lib.mjs';
|
|
29
29
|
import { wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs';
|
|
30
|
+
import { sanitizeForPublication } from './token-sanitization.lib.mjs';
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* Determine whether GitHub will automatically close a PR's linked issues when
|
|
@@ -216,7 +217,7 @@ export async function ensureLinkedIssueClosedAfterMerge({ $: rawDollar, log = nu
|
|
|
216
217
|
}
|
|
217
218
|
|
|
218
219
|
// Close the issue explicitly, leaving an explanatory trail.
|
|
219
|
-
const comment = [`Closed by #${prNumber}, which targeted the non-default branch \`${baseBranch}\` (repository default: \`${defaultBranch}\`).`, '', 'GitHub only auto-closes linked issues for pull requests merged into the default branch,', 'so hive-mind closed this issue explicitly after the merge.', '', '_Automated by hive-mind ([#1895](https://github.com/link-assistant/hive-mind/issues/1895))._'].join('\n');
|
|
220
|
+
const comment = await sanitizeForPublication([`Closed by #${prNumber}, which targeted the non-default branch \`${baseBranch}\` (repository default: \`${defaultBranch}\`).`, '', 'GitHub only auto-closes linked issues for pull requests merged into the default branch,', 'so hive-mind closed this issue explicitly after the merge.', '', '_Automated by hive-mind ([#1895](https://github.com/link-assistant/hive-mind/issues/1895))._'].join('\n'));
|
|
220
221
|
|
|
221
222
|
const closeResult = await $`gh issue close ${issueNumber} --repo ${owner}/${repo} --reason completed --comment ${comment}`;
|
|
222
223
|
if (closeResult.code === 0) {
|
|
@@ -22,6 +22,7 @@ import { ghWithRateLimitRetry } from './github-rate-limit.lib.mjs';
|
|
|
22
22
|
import { extractLinkedIssueNumber } from './github-linking.lib.mjs';
|
|
23
23
|
import { classifyIssueLinkStatus } from './github-issue-auto-close.lib.mjs';
|
|
24
24
|
import { getDefaultBranch } from './github-merge.lib.mjs';
|
|
25
|
+
import { sanitizeForPublication } from './token-sanitization.lib.mjs';
|
|
25
26
|
|
|
26
27
|
const execRaw = promisify(execCallback);
|
|
27
28
|
|
|
@@ -76,7 +77,7 @@ export async function closeLinkedIssueIfNotAutoClosed(owner, repo, prNumber, ver
|
|
|
76
77
|
// If state lookup fails, fall through and attempt the close.
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
const comment = `Closed by #${prNumber}, which targeted the non-default branch \`${baseBranch}\` (repository default: \`${defaultBranch}\`).\n\nGitHub only auto-closes linked issues for pull requests merged into the default branch, so hive-mind closed this issue explicitly after the merge.\n\n_Automated by hive-mind ([#1895](https://github.com/link-assistant/hive-mind/issues/1895))._
|
|
80
|
+
const comment = await sanitizeForPublication(`Closed by #${prNumber}, which targeted the non-default branch \`${baseBranch}\` (repository default: \`${defaultBranch}\`).\n\nGitHub only auto-closes linked issues for pull requests merged into the default branch, so hive-mind closed this issue explicitly after the merge.\n\n_Automated by hive-mind ([#1895](https://github.com/link-assistant/hive-mind/issues/1895))._`);
|
|
80
81
|
await exec(`gh issue close ${issueNumber} --repo ${owner}/${repo} --reason completed --comment ${JSON.stringify(comment)}`);
|
|
81
82
|
if (verbose) console.log(`[VERBOSE] /merge: Closed issue #${issueNumber} explicitly (PR #${prNumber} merged into non-default branch '${baseBranch}')`);
|
|
82
83
|
return { closed: true, skipped: false, reason: 'closed-explicitly', issueNumber: Number(issueNumber) };
|