@cocorograph/hub-agent 0.8.64 → 0.8.66
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/README.md +17 -0
- package/package.json +1 -1
- package/scripts/install.sh +354 -1
- package/src/claude-display-mask.mjs +89 -0
- package/src/claude-stream-bridge.mjs +36 -11
- package/src/codex-result-mask.mjs +187 -0
- package/src/main.mjs +5 -2
- package/src/permission-display-mask.mjs +142 -0
- package/src/tool-result-mask.mjs +201 -130
package/src/tool-result-mask.mjs
CHANGED
|
@@ -8,23 +8,20 @@
|
|
|
8
8
|
* jsonl へ流れていた。さらに Hub ナレッジに格納した **非公開スキルプロンプト本文**
|
|
9
9
|
* (`get_skill_prompt` MCP の戻り値) も毎回リザルトに露出していた。
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* (詳細は claude-stream-bridge.mjs の emit 経路コメント参照)。
|
|
11
|
+
* ライブ表示は SDK / app-server のイベントのコピーをマスクし、実行中のモデル入力を変更しない。
|
|
12
|
+
* Claude の終了後 JSONL 書き換えは従来の対象・正規表現を別 helper で維持する。再開時にはマスク済みの
|
|
13
|
+
* 内容が読まれる。Codex の再開用 rollout 原本には適用しない。
|
|
15
14
|
*
|
|
16
15
|
* 2 種類のマスク:
|
|
17
16
|
* - 秘匿情報: 正規表現で既知トークン形 / sensitive な KEY=VALUE 代入を伏せ字化する。
|
|
18
|
-
*
|
|
17
|
+
* 表示コピーでは JSON の秘密フィールドにも適用する。保存用の対象は拡大しない。
|
|
19
18
|
* - スキルプロンプト本文: `get_skill_prompt` (MCP 名前空間込み) の tool_result 本文を
|
|
20
19
|
* まるごとプレースホルダへ置換する。tool_use → tool_result の id 対応が必要なため、
|
|
21
20
|
* stream にはステートフルな masker (createEventMasker)、jsonl 一括には 1 パスの
|
|
22
21
|
* maskJsonlText を用意する。
|
|
23
22
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* 届くためここで捕捉でき、最終メッセージ・jsonl・hydrate も全てマスクされる。アシスタント
|
|
27
|
-
* が逐次生成する途中テキストにシークレットが現れる確率は低いと判断し v1 では対象外。
|
|
23
|
+
* 部分デルタは claude-display-mask / codex-result-mask が表示前に抑止し、
|
|
24
|
+
* 分割された秘密値を連結・表示しない。本文は完成済みメッセージから表示する。
|
|
28
25
|
*/
|
|
29
26
|
import { readFile, rename, rm, stat, utimes, writeFile } from "node:fs/promises"
|
|
30
27
|
|
|
@@ -54,12 +51,14 @@ export const DEPLOY_CRED_PLACEHOLDER =
|
|
|
54
51
|
* 既知の秘匿情報パターン (誤検知を抑えるため、形が明確なトークンに限定する)。
|
|
55
52
|
* 各要素は global フラグ付き RegExp。replace で SECRET_PLACEHOLDER に置換する。
|
|
56
53
|
*/
|
|
54
|
+
const DISPLAY_OPENAI_PATTERN = /\bsk-(?:(?:proj|svcacct)-)?[A-Za-z0-9_-]{20,}/g
|
|
55
|
+
const AT_REST_OPENAI_PATTERN = /sk-[A-Za-z0-9]{20,}/g
|
|
57
56
|
const SECRET_PATTERNS = [
|
|
58
57
|
// PEM 秘密鍵ブロック (複数行)。RSA/EC/OPENSSH/DSA/PGP の修飾子に対応。
|
|
59
58
|
/-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/g,
|
|
60
59
|
// Anthropic / OpenAI 系 API キー (sk-ant-... を先に置換するため先頭に)。
|
|
61
60
|
/sk-ant-[A-Za-z0-9_-]{20,}/g,
|
|
62
|
-
|
|
61
|
+
DISPLAY_OPENAI_PATTERN,
|
|
63
62
|
// AWS アクセスキー ID。
|
|
64
63
|
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
65
64
|
// GitHub トークン (ghp_/gho_/ghu_/ghs_/ghr_ と fine-grained PAT)。
|
|
@@ -79,10 +78,16 @@ const SECRET_PATTERNS = [
|
|
|
79
78
|
/**
|
|
80
79
|
* sensitive な名前の KEY=VALUE / KEY: VALUE 代入を伏せ字化する。キー名は残し値だけ消す
|
|
81
80
|
* (例: `DB_PASSWORD=hunter2` → `DB_PASSWORD=[REDACTED]`)。.env ダンプ / 環境変数出力対策。
|
|
82
|
-
*
|
|
81
|
+
* キーの完全な構成要素で判定し、短い値も対象にする。引用符は維持する。
|
|
83
82
|
*/
|
|
84
83
|
const ASSIGN_PATTERN =
|
|
85
|
-
|
|
84
|
+
/(["']?)\b([A-Za-z_][A-Za-z0-9_-]*)\1(\s*[=:]\s*)("(?:\\.|[^"\\])*"|'[^']*'|\[REDACTED\]|[^\s,;{}\[\]"']+|["'][^\r\n]*)/g
|
|
85
|
+
|
|
86
|
+
// Match complete key components, not substrings such as tokenUsage / tokenizer.
|
|
87
|
+
function isSecretKey(key) {
|
|
88
|
+
const normalized = key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/-/g, "_").toLowerCase()
|
|
89
|
+
return /(?:^|_)(?:password|passwd|secret|token|api_key|access_key|private_key|client_secret|authorization|cookie|set_cookie)$/.test(normalized)
|
|
90
|
+
}
|
|
86
91
|
|
|
87
92
|
/**
|
|
88
93
|
* 文字列中の秘匿情報を伏せ字化する。マスク対象が無ければ同一文字列をそのまま返す。
|
|
@@ -91,17 +96,113 @@ const ASSIGN_PATTERN =
|
|
|
91
96
|
*/
|
|
92
97
|
export function redactSecrets(text) {
|
|
93
98
|
if (typeof text !== "string" || text.length === 0) return text
|
|
99
|
+
// Decode JSON (including JSON encoded inside a JSON string) before inspecting
|
|
100
|
+
// keys. Keep the original bytes when no redaction is needed.
|
|
101
|
+
if (/^\s*[\[{\"]/.test(text)) {
|
|
102
|
+
try {
|
|
103
|
+
const parsed = JSON.parse(text)
|
|
104
|
+
const masked = redactSecretsValue(parsed)
|
|
105
|
+
return masked !== parsed ? JSON.stringify(masked) : text
|
|
106
|
+
} catch { /* Plain text and incomplete JSON use the patterns below. */ }
|
|
107
|
+
}
|
|
94
108
|
let out = text
|
|
95
109
|
for (const re of SECRET_PATTERNS) {
|
|
96
110
|
out = out.replace(re, SECRET_PLACEHOLDER)
|
|
97
111
|
}
|
|
112
|
+
out = out.replace(/\b(Authorization\s*:\s*)(?:Bearer|Basic)\s+[^\s,"'}]+/gi, `$1${SECRET_PLACEHOLDER}`)
|
|
98
113
|
out = out.replace(
|
|
99
114
|
ASSIGN_PATTERN,
|
|
100
|
-
(
|
|
115
|
+
(match, keyQuote, key, sep, value) => {
|
|
116
|
+
if (!isSecretKey(key)) return match
|
|
117
|
+
const quote = /^["']/.test(value) ? value[0] : ""
|
|
118
|
+
return `${keyQuote}${key}${keyQuote}${sep}${quote}${SECRET_PLACEHOLDER}${quote}`
|
|
119
|
+
},
|
|
101
120
|
)
|
|
121
|
+
out = out.replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+:[^\s/@]+@/gi, `$1${SECRET_PLACEHOLDER}@`)
|
|
102
122
|
return out
|
|
103
123
|
}
|
|
104
124
|
|
|
125
|
+
/** Recursively sanitize a display copy; unchanged branches retain identity. */
|
|
126
|
+
export function redactSecretsValue(value) {
|
|
127
|
+
if (typeof value === "string") return redactSecrets(value)
|
|
128
|
+
if (!value || typeof value !== "object") return value
|
|
129
|
+
let changed = false
|
|
130
|
+
const keys = new Set()
|
|
131
|
+
let collision = false
|
|
132
|
+
const entries = Object.entries(value).map(([key, original]) => {
|
|
133
|
+
const safeKey = Array.isArray(value) ? key : redactSecrets(key)
|
|
134
|
+
if (keys.has(safeKey)) collision = true
|
|
135
|
+
keys.add(safeKey)
|
|
136
|
+
const presenceFlag = typeof original === "boolean" && /^has(?:_|[A-Z])/.test(key)
|
|
137
|
+
const masked = !Array.isArray(value) && !presenceFlag && isSecretKey(key)
|
|
138
|
+
? SECRET_PLACEHOLDER : redactSecretsValue(original)
|
|
139
|
+
if (masked !== original || safeKey !== key) changed = true
|
|
140
|
+
return [safeKey, masked]
|
|
141
|
+
})
|
|
142
|
+
if (collision) return SECRET_PLACEHOLDER
|
|
143
|
+
if (!changed) return value
|
|
144
|
+
return Array.isArray(value) ? entries.map(([, item]) => item) : Object.fromEntries(entries)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Only recognize a real, explicit helper invocation, not echo/documentation. */
|
|
148
|
+
function isDeployHelperCommand(command, allowShellWrapper = true) {
|
|
149
|
+
if (typeof command !== "string") return false
|
|
150
|
+
const tokens = command.match(/"(?:\\.|[^"\\])*"|'[^']*'|&&|\|\||[;\n|]|[^\s;&|]+/g) ?? []
|
|
151
|
+
let start = 0
|
|
152
|
+
for (let end = 0; end <= tokens.length; end++) {
|
|
153
|
+
if (end < tokens.length && !/^(?:&&|\|\||[;\n|])$/.test(tokens[end])) continue
|
|
154
|
+
const words = tokens.slice(start, end).map((word) => word.replace(/^(["'])([\s\S]*)\1$/, "$2"))
|
|
155
|
+
start = end + 1
|
|
156
|
+
while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0] ?? "")) words.shift()
|
|
157
|
+
// Unwrap one explicit shell command only; do not evaluate arbitrary shell code.
|
|
158
|
+
if (allowShellWrapper && /(?:^|\/)(?:bash|sh|zsh)$/.test(words[0] ?? "")
|
|
159
|
+
&& /^-(?:c|lc|cl)$/.test(words[1] ?? "") && words.length === 3
|
|
160
|
+
&& isDeployHelperCommand(words[2], false)) return true
|
|
161
|
+
if (/(?:^|\/)python(?:3(?:\.\d+)?)?$/.test(words[0] ?? "")) {
|
|
162
|
+
words.shift()
|
|
163
|
+
while (/^-(?:u|B)$/.test(words[0] ?? "")) words.shift()
|
|
164
|
+
}
|
|
165
|
+
if (/(?:^|\/)hub_helper\.py$/.test(words[0] ?? "") && words[1] === "deploy_creds") return true
|
|
166
|
+
}
|
|
167
|
+
return false
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Shared Claude/Codex policy. Input is consulted before it is redacted. */
|
|
171
|
+
export function toolResultPlaceholder(name, input) {
|
|
172
|
+
if (isSkillPromptTool(name)) return SKILL_PROMPT_PLACEHOLDER
|
|
173
|
+
if (isDeployCredentialTool(name)) return DEPLOY_CRED_PLACEHOLDER
|
|
174
|
+
const shortName = typeof name === "string" ? name.split(/__|\./).at(-1) : ""
|
|
175
|
+
if (shortName === "exec") {
|
|
176
|
+
const source = typeof input === "string" ? input : input?.code ?? input?.source
|
|
177
|
+
// Code-mode aggregates inner results without a reliable per-result tool ID.
|
|
178
|
+
// A visible reference to a protected tool therefore makes the entire wrapper
|
|
179
|
+
// result private, including other results from that same execution. This is
|
|
180
|
+
// deliberately conservative (also covers bracket access/aliases), not a JS
|
|
181
|
+
// interpreter or a claim to detect dynamically constructed tool names.
|
|
182
|
+
if (typeof source === "string") {
|
|
183
|
+
if (/(?:\b|__)get_skill_prompt\b/.test(source)
|
|
184
|
+
|| (/(?:\b|__)(?:get_knowledge_page|get_session_log)\b/.test(source)
|
|
185
|
+
&& /(?:skill|エージェント)\//.test(source))) return SKILL_PROMPT_PLACEHOLDER
|
|
186
|
+
if (/(?:\b|__)(?:get_deploy_target|deploy_creds)\b/.test(source)) return DEPLOY_CRED_PLACEHOLDER
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (["get_knowledge_page", "get_session_log"].includes(shortName)
|
|
190
|
+
&& typeof input?.slug === "string" && /^(?:skill|エージェント)\//.test(input.slug)) {
|
|
191
|
+
return SKILL_PROMPT_PLACEHOLDER
|
|
192
|
+
}
|
|
193
|
+
if (["Bash", "exec_command"].includes(shortName)
|
|
194
|
+
&& isDeployHelperCommand(input?.command ?? input?.cmd)) return DEPLOY_CRED_PLACEHOLDER
|
|
195
|
+
return null
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function classifiedToolName(block) {
|
|
199
|
+
if (isSkillPromptTool(block.name) || isDeployCredentialTool(block.name)) return block.name
|
|
200
|
+
const placeholder = toolResultPlaceholder(block.name, block.input)
|
|
201
|
+
if (placeholder === SKILL_PROMPT_PLACEHOLDER) return "get_skill_prompt"
|
|
202
|
+
if (placeholder === DEPLOY_CRED_PLACEHOLDER) return "deploy_creds"
|
|
203
|
+
return block.name
|
|
204
|
+
}
|
|
205
|
+
|
|
105
206
|
/**
|
|
106
207
|
* tool 名が非公開スキルプロンプト取得ツールか判定する。MCP 名前空間が付くため
|
|
107
208
|
* (`mcp__hub__get_skill_prompt` / `mcp__hub_staff__get_skill_prompt`)、末尾一致でも拾う。
|
|
@@ -132,28 +233,6 @@ export function isDeployCredentialTool(name) {
|
|
|
132
233
|
)
|
|
133
234
|
}
|
|
134
235
|
|
|
135
|
-
/** tool_result の content (string | block[]) をプレースホルダ 1 本へ潰す。 */
|
|
136
|
-
function skillPlaceholderContent() {
|
|
137
|
-
return [{ type: "text", text: SKILL_PROMPT_PLACEHOLDER }]
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** tool_result の content をデプロイ接続情報プレースホルダ 1 本へ潰す。 */
|
|
141
|
-
function deployCredPlaceholderContent() {
|
|
142
|
-
return [{ type: "text", text: DEPLOY_CRED_PLACEHOLDER }]
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** content blocks 配列の text を redactSecrets で処理した新配列を返す (非破壊)。 */
|
|
146
|
-
function redactBlocks(blocks) {
|
|
147
|
-
if (!Array.isArray(blocks)) return blocks
|
|
148
|
-
return blocks.map((b) => {
|
|
149
|
-
if (b && b.type === "text" && typeof b.text === "string") {
|
|
150
|
-
const masked = redactSecrets(b.text)
|
|
151
|
-
return masked === b.text ? b : { ...b, text: masked }
|
|
152
|
-
}
|
|
153
|
-
return b
|
|
154
|
-
})
|
|
155
|
-
}
|
|
156
|
-
|
|
157
236
|
/**
|
|
158
237
|
* SDK message 形 (jsonl 1 行 / live event) のオブジェクトをマスクした **コピー** を返す。
|
|
159
238
|
* 変更が無ければ元オブジェクトをそのまま返す (参照同一で「変化なし」を呼び出し側が判定可)。
|
|
@@ -165,104 +244,33 @@ function redactBlocks(blocks) {
|
|
|
165
244
|
export function maskMessageObject(obj, toolNames) {
|
|
166
245
|
if (!MASK_ENABLED) return obj
|
|
167
246
|
if (!obj || typeof obj !== "object") return obj
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (masked !== obj[field]) {
|
|
176
|
-
next[field] = masked
|
|
177
|
-
changed = true
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return changed ? next : obj
|
|
247
|
+
collectToolUseNames(obj, toolNames)
|
|
248
|
+
obj = redactSecretsValue(obj)
|
|
249
|
+
// Some SDK/user envelopes duplicate tool output outside message.content.
|
|
250
|
+
if (obj.type === "user" && Object.hasOwn(obj, "tool_use_result") && Array.isArray(obj.message?.content)) {
|
|
251
|
+
const placeholder = obj.message.content.reduce((found, block) => found ||
|
|
252
|
+
(block?.type === "tool_result" ? toolResultPlaceholder(toolNames.get(block.tool_use_id)) : null), null)
|
|
253
|
+
if (placeholder && obj.tool_use_result !== placeholder) obj = { ...obj, tool_use_result: placeholder }
|
|
181
254
|
}
|
|
182
255
|
|
|
183
256
|
const message = obj.message
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (masked !== content && masked.some((b, i) => b !== content[i])) {
|
|
197
|
-
return { ...obj, message: { ...message, content: masked } }
|
|
198
|
-
}
|
|
199
|
-
} else if (typeof content === "string") {
|
|
200
|
-
const masked = redactSecrets(content)
|
|
201
|
-
if (masked !== content) {
|
|
202
|
-
return { ...obj, message: { ...message, content: masked } }
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
return obj
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// user: tool_result をスキル判定 or 秘匿情報マスク。素のテキスト発言も秘匿情報を伏せる。
|
|
209
|
-
if (obj.type === "user") {
|
|
210
|
-
if (Array.isArray(content)) {
|
|
211
|
-
let changed = false
|
|
212
|
-
const next = content.map((b) => {
|
|
213
|
-
if (b && b.type === "tool_result") {
|
|
214
|
-
const name = toolNames.get(b.tool_use_id)
|
|
215
|
-
if (isSkillPromptTool(name)) {
|
|
216
|
-
changed = true
|
|
217
|
-
return { ...b, content: skillPlaceholderContent() }
|
|
218
|
-
}
|
|
219
|
-
if (isDeployCredentialTool(name)) {
|
|
220
|
-
changed = true
|
|
221
|
-
return { ...b, content: deployCredPlaceholderContent() }
|
|
222
|
-
}
|
|
223
|
-
// 非スキル tool_result: 中身 (string | block[]) の秘匿情報を伏せる。
|
|
224
|
-
if (typeof b.content === "string") {
|
|
225
|
-
const masked = redactSecrets(b.content)
|
|
226
|
-
if (masked !== b.content) {
|
|
227
|
-
changed = true
|
|
228
|
-
return { ...b, content: masked }
|
|
229
|
-
}
|
|
230
|
-
return b
|
|
231
|
-
}
|
|
232
|
-
if (Array.isArray(b.content)) {
|
|
233
|
-
const mc = redactBlocks(b.content)
|
|
234
|
-
if (mc.some((x, i) => x !== b.content[i])) {
|
|
235
|
-
changed = true
|
|
236
|
-
return { ...b, content: mc }
|
|
237
|
-
}
|
|
238
|
-
return b
|
|
239
|
-
}
|
|
240
|
-
return b
|
|
241
|
-
}
|
|
242
|
-
if (b && b.type === "text" && typeof b.text === "string") {
|
|
243
|
-
const masked = redactSecrets(b.text)
|
|
244
|
-
if (masked !== b.text) {
|
|
245
|
-
changed = true
|
|
246
|
-
return { ...b, text: masked }
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
return b
|
|
250
|
-
})
|
|
251
|
-
if (changed) return { ...obj, message: { ...message, content: next } }
|
|
252
|
-
} else if (typeof content === "string") {
|
|
253
|
-
const masked = redactSecrets(content)
|
|
254
|
-
if (masked !== content) {
|
|
255
|
-
return { ...obj, message: { ...message, content: masked } }
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return obj
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
return obj
|
|
257
|
+
if (obj.type !== "user" || !Array.isArray(message?.content)) return obj
|
|
258
|
+
let changed = false
|
|
259
|
+
const content = message.content.map((block) => {
|
|
260
|
+
if (block?.type !== "tool_result") return block
|
|
261
|
+
const placeholder = toolResultPlaceholder(toolNames.get(block.tool_use_id))
|
|
262
|
+
if (!placeholder) return block
|
|
263
|
+
if (Array.isArray(block.content) && block.content.length === 1
|
|
264
|
+
&& block.content[0]?.type === "text" && block.content[0].text === placeholder) return block
|
|
265
|
+
changed = true
|
|
266
|
+
return { ...block, content: [{ type: "text", text: placeholder }] }
|
|
267
|
+
})
|
|
268
|
+
return changed ? { ...obj, message: { ...message, content } } : obj
|
|
262
269
|
}
|
|
263
270
|
|
|
264
271
|
/**
|
|
265
|
-
* assistant メッセージ obj から tool_use の id
|
|
272
|
+
* assistant メッセージ obj から tool_use の id→表示用分類名を toolNames Map へ記録する。
|
|
273
|
+
* 値は実際の tool 名とは限らず、引数から分類した get_skill_prompt / deploy_creds も含む。
|
|
266
274
|
* (マスクはしない)。履歴 hydrate で「tool_use 行が表示スライスの外へ truncate されても
|
|
267
275
|
* tool_result のスキル判定が効く」よう、全行を事前走査して id→name を集めるのに使う。
|
|
268
276
|
* @param {Record<string, unknown>} obj
|
|
@@ -272,12 +280,75 @@ export function collectToolUseNames(obj, toolNames) {
|
|
|
272
280
|
if (obj?.type === "assistant" && Array.isArray(obj.message?.content)) {
|
|
273
281
|
for (const b of obj.message.content) {
|
|
274
282
|
if (b && b.type === "tool_use" && typeof b.id === "string") {
|
|
275
|
-
toolNames.set(b.id, b
|
|
283
|
+
toolNames.set(b.id, classifiedToolName(b))
|
|
276
284
|
}
|
|
277
285
|
}
|
|
278
286
|
}
|
|
279
287
|
}
|
|
280
288
|
|
|
289
|
+
// 保存用は従来の token regex と 4 文字以上の代入だけ。表示用の JSON/key/URI
|
|
290
|
+
// 拡張を呼ばないことで、Claude resume が読む原本の変更範囲を維持する。
|
|
291
|
+
const AT_REST_ASSIGN_PATTERN =
|
|
292
|
+
/\b([A-Za-z0-9_]*(?:PASSWORD|PASSWD|SECRET|TOKEN|API[_-]?KEY|ACCESS[_-]?KEY|PRIVATE[_-]?KEY|CLIENT[_-]?SECRET|AUTH[_-]?TOKEN)[A-Za-z0-9_]*)(\s*[=:]\s*)(['"]?)([^\s'"]{4,})\3/gi
|
|
293
|
+
|
|
294
|
+
function redactAtRestText(text) {
|
|
295
|
+
let out = text
|
|
296
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
297
|
+
out = out.replace(pattern === DISPLAY_OPENAI_PATTERN ? AT_REST_OPENAI_PATTERN : pattern, SECRET_PLACEHOLDER)
|
|
298
|
+
}
|
|
299
|
+
return out.replace(AT_REST_ASSIGN_PATTERN,
|
|
300
|
+
(_match, key, sep, quote) => `${key}${sep}${quote}${SECRET_PLACEHOLDER}${quote}`)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function redactAtRestTextBlock(block) {
|
|
304
|
+
if (block?.type !== "text" || typeof block.text !== "string") return block
|
|
305
|
+
const text = redactAtRestText(block.text)
|
|
306
|
+
return text === block.text ? block : { ...block, text }
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Existing at-rest message scope; tool names are real names, never display classifications. */
|
|
310
|
+
function maskAtRestMessage(obj, toolNames) {
|
|
311
|
+
if (!obj || typeof obj !== "object") return obj
|
|
312
|
+
if (obj.type === "system" && obj.subtype === "codex_delegate") {
|
|
313
|
+
let next = obj
|
|
314
|
+
for (const field of ["prompt", "description"]) {
|
|
315
|
+
if (typeof obj[field] !== "string") continue
|
|
316
|
+
const masked = redactAtRestText(obj[field])
|
|
317
|
+
if (masked !== obj[field]) next = { ...next, [field]: masked }
|
|
318
|
+
}
|
|
319
|
+
return next
|
|
320
|
+
}
|
|
321
|
+
const message = obj.message
|
|
322
|
+
if (!message || typeof message !== "object" || !["assistant", "user"].includes(obj.type)) return obj
|
|
323
|
+
const content = message.content
|
|
324
|
+
let next = content
|
|
325
|
+
if (typeof content === "string") next = redactAtRestText(content)
|
|
326
|
+
else if (Array.isArray(content)) {
|
|
327
|
+
next = content.map((block) => {
|
|
328
|
+
if (obj.type === "assistant" && block?.type === "tool_use" && typeof block.id === "string") {
|
|
329
|
+
toolNames.set(block.id, block.name)
|
|
330
|
+
}
|
|
331
|
+
if (obj.type !== "user" || block?.type !== "tool_result") return redactAtRestTextBlock(block)
|
|
332
|
+
const name = toolNames.get(block.tool_use_id)
|
|
333
|
+
const placeholder = isSkillPromptTool(name) ? SKILL_PROMPT_PLACEHOLDER
|
|
334
|
+
: isDeployCredentialTool(name) ? DEPLOY_CRED_PLACEHOLDER : null
|
|
335
|
+
// Keep the legacy rewrite behavior even for an already masked result.
|
|
336
|
+
if (placeholder) return { ...block, content: [{ type: "text", text: placeholder }] }
|
|
337
|
+
if (typeof block.content === "string") {
|
|
338
|
+
const masked = redactAtRestText(block.content)
|
|
339
|
+
return masked === block.content ? block : { ...block, content: masked }
|
|
340
|
+
}
|
|
341
|
+
if (Array.isArray(block.content)) {
|
|
342
|
+
const masked = block.content.map(redactAtRestTextBlock)
|
|
343
|
+
if (masked.some((item, i) => item !== block.content[i])) return { ...block, content: masked }
|
|
344
|
+
}
|
|
345
|
+
return block
|
|
346
|
+
})
|
|
347
|
+
if (next.every((block, i) => block === content[i])) return obj
|
|
348
|
+
}
|
|
349
|
+
return next === content ? obj : { ...obj, message: { ...message, content: next } }
|
|
350
|
+
}
|
|
351
|
+
|
|
281
352
|
/**
|
|
282
353
|
* ライブ stream / watch 用のステートフル masker を生成する。tool_use → tool_result が
|
|
283
354
|
* 別イベントで届くため、id→name を内部 Map に蓄積しながら 1 イベントずつマスクする。
|
|
@@ -297,7 +368,7 @@ export function createEventMasker(initialToolNames) {
|
|
|
297
368
|
}
|
|
298
369
|
|
|
299
370
|
/**
|
|
300
|
-
*
|
|
371
|
+
* 保存用の従来 policy で jsonl ファイル全文をマスクする。表示は maskMessageObject を使う。
|
|
301
372
|
* tool_use を先に通すと id→name が貯まるので、行順 (assistant→user) のまま走査すれば
|
|
302
373
|
* tool_result のスキル判定が成立する。マスク対象が 1 件も無ければ null を返す
|
|
303
374
|
* (呼び出し側が「書き換え不要」を判定して mtime を温存できる)。
|
|
@@ -321,7 +392,7 @@ export function maskJsonlText(text) {
|
|
|
321
392
|
} catch {
|
|
322
393
|
return line // パース不能行はそのまま (壊さない)
|
|
323
394
|
}
|
|
324
|
-
const masked =
|
|
395
|
+
const masked = maskAtRestMessage(obj, toolNames)
|
|
325
396
|
if (masked !== obj) {
|
|
326
397
|
changed = true
|
|
327
398
|
return JSON.stringify(masked)
|