@cocorograph/hub-agent 0.8.109 → 0.8.110

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/hub-bundle.mjs +70 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocorograph/hub-agent",
3
- "version": "0.8.109",
3
+ "version": "0.8.110",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの Claude / Codex チャットと tmux セッションを中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -330,22 +330,48 @@ export function hasInheritedOutputChild() {
330
330
  return inheritedOutputChildren > 0
331
331
  }
332
332
 
333
- function runOnce(argv, { logger } = {}) {
333
+ export function runOnce(argv, { logger, redact = [] } = {}) {
334
+ const secrets = redact.filter((value) => value.length > 0).sort((a, b) => b.length - a.length)
335
+ const pattern = secrets.length
336
+ ? new RegExp(secrets.map(_escapeRegExp).join("|"), "g")
337
+ : null
338
+ const sanitize = (value) => pattern ? value.replace(pattern, "***") : value
339
+ const safeError = (err) => pattern ? new Error(sanitize(err.message)) : err
334
340
  return new Promise((resolve, reject) => {
335
341
  const [bin, ...args] = argv
336
- inheritedOutputChildren++
342
+ const cmd = sanitize(argv.join(" "))
343
+ if (!pattern) inheritedOutputChildren++
337
344
  let released = false
338
345
  const release = () => {
339
346
  if (released) return
340
347
  released = true
341
- inheritedOutputChildren--
348
+ if (!pattern) inheritedOutputChildren--
342
349
  }
343
350
  let child
344
351
  try {
345
- child = spawn(bin, args, { stdio: "inherit" })
352
+ child = spawn(bin, args, { stdio: pattern ? ["inherit", "pipe", "pipe"] : "inherit" })
346
353
  } catch (err) {
347
354
  release()
348
- throw err
355
+ throw safeError(err)
356
+ }
357
+ // close を待って全バイトを結合し、チャンク境界(UTF-8 の途中を含む)での漏出を防ぐ。
358
+ // 上限を超えたら収集済みも捨てて読み捨てに切り替える(途中で切ると資格の断片が残りうるため、
359
+ // 部分表示はしない)。pipe は close まで drain し続け、子を書き込み待ちで止めない。
360
+ const captured = { stdout: { chunks: [], bytes: 0, overflow: false }, stderr: { chunks: [], bytes: 0, overflow: false } }
361
+ if (pattern) {
362
+ for (const name of ["stdout", "stderr"]) {
363
+ const buf = captured[name]
364
+ child[name].on("data", (chunk) => {
365
+ if (buf.overflow) return
366
+ buf.bytes += chunk.length
367
+ if (buf.bytes > REDACTED_OUTPUT_MAX_BYTES) {
368
+ buf.overflow = true
369
+ buf.chunks = []
370
+ return
371
+ }
372
+ buf.chunks.push(chunk)
373
+ })
374
+ }
349
375
  }
350
376
  // spawn 失敗 (ENOENT 等) は exit が来ないことがある。error でも必ず settle させ、
351
377
  // bundle 同期を未完了のまま残さない (resolve/reject は最初の 1 回だけ有効)。
@@ -355,22 +381,54 @@ function runOnce(argv, { logger } = {}) {
355
381
  }
356
382
  child.once("error", (err) => {
357
383
  release()
358
- reject(err)
359
- safeLog("warn", { cmd: argv.join(" "), error: err.message }, "hub-bundle post_install failed")
384
+ reject(safeError(err))
385
+ safeLog("warn", { cmd, error: sanitize(err.message) }, "hub-bundle post_install failed")
360
386
  })
361
- child.on("exit", (code) => {
387
+ child.on(pattern ? "close" : "exit", (code) => {
362
388
  release()
389
+ // 先に settle する。転送先の障害(ENOSPC / EPIPE 等)でコマンドの成否を変えない。
363
390
  if (code === 0) {
364
391
  resolve(code)
365
- safeLog("info", { cmd: argv.join(" ") }, "hub-bundle post_install ok")
366
392
  } else {
367
- reject(new Error(`post_install command failed (exit ${code}): ${argv.join(" ")}`))
368
- safeLog("warn", { cmd: argv.join(" "), code }, "hub-bundle post_install failed")
393
+ reject(new Error(`post_install command failed (exit ${code}): ${cmd}`))
394
+ }
395
+ if (pattern) {
396
+ for (const name of ["stdout", "stderr"]) {
397
+ const buf = captured[name]
398
+ const text = buf.overflow
399
+ ? `[hub-agent] 出力が ${REDACTED_OUTPUT_MAX_BYTES} バイトを超えたため省略しました\n`
400
+ : sanitize(Buffer.concat(buf.chunks).toString("utf8"))
401
+ if (text) forwardRedactedOutput(process[name], text)
402
+ }
403
+ }
404
+ if (code === 0) {
405
+ safeLog("info", { cmd }, "hub-bundle post_install ok")
406
+ } else {
407
+ safeLog("warn", { cmd, code }, "hub-bundle post_install failed")
369
408
  }
370
409
  })
371
410
  })
372
411
  }
373
412
 
413
+ // 伏せ字済み出力の 1 ストリームあたりの収集上限。claude mcp add の通常出力は数百バイト。
414
+ const REDACTED_OUTPUT_MAX_BYTES = 1024 * 1024
415
+
416
+ // 伏せ字済み出力を親の stdout / stderr へ書く。書き込み失敗は診断出力の欠落として扱い、
417
+ // 同期例外も非同期の error も外へ出さない。error listener はこの書き込みの間だけ付け、
418
+ // 共有ストリームの他のエラーを恒久的に握り潰さない。
419
+ function forwardRedactedOutput(stream, text) {
420
+ const onError = () => {}
421
+ stream.on("error", onError)
422
+ try {
423
+ stream.write(text, () => {
424
+ // callback 後に error が emit される実装があるため、1 tick 置いてから外す。
425
+ setImmediate(() => stream.removeListener("error", onError))
426
+ })
427
+ } catch {
428
+ stream.removeListener("error", onError)
429
+ }
430
+ }
431
+
374
432
  /**
375
433
  * Bundle response の `mcp_servers` を Claude Code CLI に登録する。
376
434
  *
@@ -474,7 +532,7 @@ export async function applyMcpServers(
474
532
  // 未登録 → remove は失敗するが正常系。
475
533
  }
476
534
  }
477
- await run(argv, { logger })
535
+ await run(argv, wantHeader ? { logger, redact: [agentToken] } : { logger })
478
536
  added.push(s.name)
479
537
  } catch (err) {
480
538
  // ヘッダ不要の外部 MCP は既存なら "already exists" で失敗する。これは「既に正しく