Skip to content

feat: unify devnet logging with offckb logs and a quiet foreground node - #478

Merged
RetricSu merged 2 commits into
developfrom
agent/claude-bear/48973f2a
Jul 30, 2026
Merged

feat: unify devnet logging with offckb logs and a quiet foreground node#478
RetricSu merged 2 commits into
developfrom
agent/claude-bear/48973f2a

Conversation

@humble-little-bear

Copy link
Copy Markdown
Collaborator

背景

devnet 日志此前有 3 个源(节点/miner/proxy)× 3 个出口(前台转打、daemon.log、status TUI),行为不一致:前台被全量节点输出刷屏,daemon 模式没有内建查看方式,status 必须 TTY 全屏。本 PR 按讨论的「方案 A」统一:文件为单一日志源,前台默认安静,新增 offckb logs 命令。

改动

1. 新增 offckb logs [target](docker logs 心智,可 pipe、配合 --json)

  • offckb logs(默认)= 节点日志(run.log)
  • offckb logs script = 合约 debug! 输出(按 CKB 日志行 target 过滤 ckb-script,多行消息的延续行也保留)
  • offckb logs miner / offckb logs rpc
  • -f/--follow(tail -f 流式)、--tail N--grep <str>
  • 日志文件在三种运行模式(前台/daemon/status)下都存在,所以任何模式都能看;daemon 不作为概念暴露(它是实现残骸),rpc 按用户任务命名而非内部架构词 proxy

2. 前台 offckb node 默认安静

  • 不再转打节点/miner 全量 stdout(管道仍会 drain,避免子进程阻塞);--verbose 恢复旧行为
  • 合约 script debug 输出实时上屏:走节点 TCP log subscription(ckb-tui 同款通道,结构化 entry,非解析 stdout),只显示 ckb-script
  • 保留高价值单行:send_transaction: <hash>、JSON-RPC error warn
  • 就绪后提示 Follow the full node log with: offckb logs -f

3. RPC proxy 降噪 + 落盘

  • 每请求 RPC Req: 从 info 降为 debug;解析 proxyRes 的 JSON-RPC error 并 warn(此前完全看不到)
  • proxy 事件(请求方法 / tx hash / RPC 错误)写入 data/logs/proxy.log(前台/daemon 都写),即 offckb logs rpc 的数据源

status 维持现状;README 已补充 logs 用法。

验证

  • 新增 4 个测试文件(log 行解析/过滤/tail/follow、logs 命令、proxy 事件、TCP subscription、前台输出模式),全套 271 个 jest 测试通过;tsc --noEmit、eslint 干净(lint 的 4 个 warning 为存量)
  • 端到端实跑(隔离 XDG 环境,真实 CKB 0.207.0 节点):
    • 前台启动仅输出生命周期 + 提示,出块期间保持安静
    • logs / logs miner / logs rpc 输出正确;logs -f 实时流出新块日志
    • 经 proxy deposit → 前台显示 send_transaction: <hash>,logs rpc 记录 request + hash
    • 注入非法 send_transaction → 前台 warn RPC error: [-32602] ...,proxy.log 同步记录
    • --verbose 恢复 CKB:/CKB-Miner: 全量输出
    • TCP log subscription 对运行中节点实测有 entry 流
  • 已知边界:环境里没有任何调用 debug! 的合约(secp256k1 不产生 ckb-script 日志),所以「真实 script entry 上屏」这一环靠单测(entry 过滤+打印)和订阅协议实测覆盖,entry 结构与其余 target 完全一致

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added offckb logs [node|script|miner|rpc] to view and tail devnet logs.
    • Supports tail, substring filtering, and live follow mode (including script log entry grouping).
    • Added offckb node --verbose to restore full raw terminal output.
  • Improvements
    • Foreground offckb node output is quieter by default; complete logs continue to be saved to devnet log files for later review.
    • JSON-RPC proxy output is quieter, while JSON-RPC errors are surfaced more prominently and proxy logs are written to a dedicated proxy log file.

Walkthrough

The CLI adds offckb logs with filtering and tailing, quiets foreground node output with optional --verbose mode, streams script debug logs over TCP, and records structured RPC proxy events in log files.

Changes

Devnet logging

Layer / File(s) Summary
Log storage and TCP subscription foundation
src/devnet/log-file.ts, src/devnet/log-subscription.ts, src/cmd/status.ts, tests/logs.test.ts, tests/log-subscription.test.ts
Adds shared log parsing, target filtering, tailing, file-following, TCP subscription, retry handling, and shared devnet address resolution.
Logs command and filtering
src/cmd/logs.ts, src/cli.ts, README.md, .changeset/..., tests/logs-command.test.ts
Registers offckb logs for node, script, miner, and RPC logs with --follow, --grep, and --tail options.
Quiet foreground node mode
src/cmd/node.ts, tests/node-quiet-mode.test.ts, tests/node-supervisor.test.ts, tests/node-terminal-rpc.test.ts
Suppresses node and miner output by default, restores raw forwarding with --verbose, and streams ckb-script entries through the TCP subscription.
RPC proxy event logging
src/tools/proxy-events.ts, src/tools/rpc-proxy.ts, tests/proxy-events.test.ts
Moves proxy request and response handling into reusable helpers, persists transaction payloads and proxy events, and warns on JSON-RPC errors.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant nodeDevnet
  participant subscribeToNodeLogs
  participant UnifiedLogger
  User->>nodeDevnet: start devnet
  nodeDevnet->>subscribeToNodeLogs: subscribe to TCP log topic
  subscribeToNodeLogs->>UnifiedLogger: emit ckb-script debug entries
  User->>UnifiedLogger: run offckb logs
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: unified devnet logging and quieter foreground node output.
Description check ✅ Passed The description is directly related to the logging, quiet node, proxy, and test changes in this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
src/cli.ts (1)

109-113: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use commander's InvalidArgumentError instead of a plain Error in the --tail parser.

Custom option-argument parsers in commander are expected to throw InvalidArgumentError for validation failures; commander catches that type specifically to print a clean, correctly-formatted error via its own error path. A plain Error bypasses that and is instead caught generically in runCli's catch block with code: 'COMMAND_FAILED', losing commander's usual formatting/exit-code consistency for bad CLI input.

🔧 Proposed fix
-import { Command, CommanderError, Option, Argument } from 'commander';
+import { Command, CommanderError, InvalidArgumentError, Option, Argument } from 'commander';
...
   .option('--tail <lines>', 'Show the last N lines before following', (value: string) => {
     const parsed = Number(value);
-    if (!Number.isInteger(parsed) || parsed < 0) throw new Error('--tail must be a non-negative integer');
+    if (!Number.isInteger(parsed) || parsed < 0) {
+      throw new InvalidArgumentError('--tail must be a non-negative integer');
+    }
     return parsed;
   })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli.ts` around lines 109 - 113, Update the --tail option parser in the
CLI option definition to throw Commander’s InvalidArgumentError for invalid
values instead of a plain Error, while preserving the existing non-negative
integer validation and message.
src/cmd/logs.ts (1)

28-52: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

LGTM overall. Consider adding a showLogs(..., { follow: true }) test for the script target to exercise the inScriptEntry gating + grep combination in follow mode directly — currently only the underlying followLogFile and non-follow filterLinesByTarget paths are unit tested separately.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cmd/logs.ts` around lines 28 - 52, Add a focused test for showLogs with
target set to script and follow enabled, using mixed script/non-script and
continuation lines plus a grep filter to verify inScriptEntry gating and grep
behavior together in follow mode. Keep the existing followLogFile and non-follow
filtering tests unchanged.
src/tools/proxy-events.ts (1)

33-50: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Synchronous file I/O on every proxy event.

event() calls fs.appendFileSync synchronously for every RPC request and every JSON-RPC error response, blocking Node's event loop for the duration of the disk write. For a low-traffic local devnet proxy this is tolerable, but it adds unnecessary latency to every RPC round-trip that passes through the proxy (which is the tool's hottest path).

♻️ Optional: use a persistent append write stream instead of appendFileSync per call
 export function createProxyEventLog(filePath: string): ProxyEventLog {
-  let dirReady = false;
+  let stream: fs.WriteStream | null = null;
+  const ensureStream = () => {
+    if (stream) return stream;
+    fs.mkdirSync(path.dirname(filePath), { recursive: true });
+    stream = fs.createWriteStream(filePath, { flags: 'a' });
+    return stream;
+  };
   return {
     filePath,
     event(text: string) {
       try {
-        if (!dirReady) {
-          fs.mkdirSync(path.dirname(filePath), { recursive: true });
-          dirReady = true;
-        }
-        fs.appendFileSync(filePath, `${new Date().toISOString()} ${text}\n`);
+        ensureStream().write(`${new Date().toISOString()} ${text}\n`);
       } catch {
-        dirReady = false;
+        stream = null;
       }
     },
   };
 }

The path-traversal static-analysis hint on fs.writeFileSync(txFile, ...) (line 75-76) is a false positive — txHash is a computed hash string from ctx.hashTransaction, not attacker-supplied path input.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tools/proxy-events.ts` around lines 33 - 50, Update createProxyEventLog
and its event method to avoid synchronous appendFileSync on every proxy event;
use a persistent asynchronous append write stream, initialized with the required
parent directory and reused across calls. Preserve timestamped event output and
ensure logging failures remain swallowed so request forwarding is never
interrupted.
src/devnet/log-subscription.ts (1)

143-156: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Retry logic doesn't distinguish "never connected" from "dropped mid-run".

The error handler retries via setTimeout(connect, retryDelayMs) whenever attempts < maxAttempts, regardless of whether the socket had previously connected successfully. The doc comment on the close handler states a dropped subscription mid-run needs no reconnect, but that guarantee isn't actually enforced here — only close() (called by the caller's stopService) prevents further retries, and there's a narrow window where a mid-run drop could trigger a few retry attempts before the caller tears it down. This is self-recovering (bounded by maxAttempts) but doesn't match the documented intent.

♻️ Optional: track whether a connection was ever established
   let socket: net.Socket | null = null;
   let closed = false;
   let attempts = 0;
   let failedReported = false;
+  let everConnected = false;

   const connect = () => {
     if (closed || endpoint == null) return;
     attempts += 1;
     const conn = net.connect(endpoint.port, endpoint.host);
     socket = conn;
     let buffer = '';

     conn.on('connect', () => {
+      everConnected = true;
       conn.write(JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'subscribe', params: ['log'] }) + '\n');
     });
     ...
     conn.on('error', (error) => {
       if (closed) return;
-      if (attempts < maxAttempts) {
+      if (!everConnected && attempts < maxAttempts) {
         setTimeout(connect, retryDelayMs);
       } else {
         fail(new Error(`Log subscription to ${tcpAddress} failed after ${attempts} attempts: ${error.message}`));
       }
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/devnet/log-subscription.ts` around lines 143 - 156, Update the connection
state in the subscription logic around the conn error/close handlers to track
whether the socket has ever connected successfully. Only schedule retries from
the initial connection phase; once a connection has been established, treat
later errors or closes as terminal without calling connect, while preserving the
existing fail behavior for exhausted initial attempts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/cli.ts`:
- Line 80: Update the `--verbose` option description in the CLI option
definition to accurately state that the default output includes lifecycle
events, transaction hashes, and RPC errors in addition to contract script
output, while verbose mode prints the full raw node/miner output.

In `@src/devnet/log-file.ts`:
- Around line 116-148: Update followLogFile to use a stateful UTF-8 TextDecoder
with streaming enabled when converting each newly-read buffer, preserving
incomplete multi-byte sequences across onChange calls. Reset the decoder
alongside offset and partial when the file is truncated or rotated, and flush
any decoder state appropriately when monitoring ends.
- Around line 83-90: Update resolveLogPath so the rpc target resolves the proxy
log for the active network rather than always using Network.devnet, matching the
network selected by createRPCProxy; alternatively, explicitly document and
enforce that the command is devnet-only if no network-aware context is
available. Preserve the existing named log-file resolution for non-rpc targets.

---

Nitpick comments:
In `@src/cli.ts`:
- Around line 109-113: Update the --tail option parser in the CLI option
definition to throw Commander’s InvalidArgumentError for invalid values instead
of a plain Error, while preserving the existing non-negative integer validation
and message.

In `@src/cmd/logs.ts`:
- Around line 28-52: Add a focused test for showLogs with target set to script
and follow enabled, using mixed script/non-script and continuation lines plus a
grep filter to verify inScriptEntry gating and grep behavior together in follow
mode. Keep the existing followLogFile and non-follow filtering tests unchanged.

In `@src/devnet/log-subscription.ts`:
- Around line 143-156: Update the connection state in the subscription logic
around the conn error/close handlers to track whether the socket has ever
connected successfully. Only schedule retries from the initial connection phase;
once a connection has been established, treat later errors or closes as terminal
without calling connect, while preserving the existing fail behavior for
exhausted initial attempts.

In `@src/tools/proxy-events.ts`:
- Around line 33-50: Update createProxyEventLog and its event method to avoid
synchronous appendFileSync on every proxy event; use a persistent asynchronous
append write stream, initialized with the required parent directory and reused
across calls. Preserve timestamped event output and ensure logging failures
remain swallowed so request forwarding is never interrupted.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 17c50b63-9418-4f8e-b6d8-e2eeeafce0be

📥 Commits

Reviewing files that changed from the base of the PR and between 920ef4d and 600b320.

📒 Files selected for processing (16)
  • .changeset/logs-command-quiet-node.md
  • README.md
  • src/cli.ts
  • src/cmd/logs.ts
  • src/cmd/node.ts
  • src/cmd/status.ts
  • src/devnet/log-file.ts
  • src/devnet/log-subscription.ts
  • src/tools/proxy-events.ts
  • src/tools/rpc-proxy.ts
  • tests/log-subscription.test.ts
  • tests/logs-command.test.ts
  • tests/logs.test.ts
  • tests/node-quiet-mode.test.ts
  • tests/node-supervisor.test.ts
  • tests/proxy-events.test.ts

Comment thread src/cli.ts Outdated
Comment thread src/devnet/log-file.ts
Comment thread src/devnet/log-file.ts
@humble-little-bear
humble-little-bear force-pushed the agent/claude-bear/48973f2a branch 2 times, most recently from 319f9fa to 7ea55e1 Compare July 29, 2026 14:00

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (5)
src/devnet/log-subscription.ts (1)

143-170: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Pending retry timer survives close(). connect re-checks closed, so no stray connection happens, but the setTimeout handle is never tracked or unref'd and can keep the event loop alive after the subscription is torn down.

♻️ Proposed refactor
+  let retryTimer: NodeJS.Timeout | null = null;
@@
       if (attempts < maxAttempts) {
-        setTimeout(connect, retryDelayMs);
+        retryTimer = setTimeout(connect, retryDelayMs);
+        retryTimer.unref?.();
       } else {
@@
     close() {
       closed = true;
+      if (retryTimer) clearTimeout(retryTimer);
+      retryTimer = null;
       socket?.destroy();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/devnet/log-subscription.ts` around lines 143 - 170, Track the retry timer
created in the error handler of the subscription’s connect flow, and clear it in
close() while marking it inactive. Ensure the timer is reset when it fires and
does not keep the event loop alive, while preserving the existing closed check
and retry behavior.
tests/logs-command.test.ts (1)

28-40: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Temp directories created by fixture() are never removed. Track the created roots and clean them in afterEach, as the other suites in this PR do.

♻️ Proposed refactor
-function fixture(): { settings: Settings; transport: CapturingTransport } {
+const tempRoots: string[] = [];
+afterEach(() => {
+  while (tempRoots.length) fs.rmSync(tempRoots.pop()!, { recursive: true, force: true });
+});
+
+function fixture(): { settings: Settings; transport: CapturingTransport } {
   const root = fs.mkdtempSync(path.join(os.tmpdir(), 'offckb-logs-cmd-'));
+  tempRoots.push(root);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/logs-command.test.ts` around lines 28 - 40, Update the test fixture
setup around fixture() to track each temporary root it creates, and add
afterEach cleanup that removes those roots recursively after every test. Follow
the cleanup pattern used by the other test suites, while preserving fixture’s
existing settings and transport setup.
tests/logs.test.ts (1)

130-146: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Timing-sensitive follow test. The 500 ms sleep gives only ~2 poll intervals of margin, and fs.watchFile polling granularity varies by platform/CI load. Pass a small intervalMs and poll until the line appears (or time out) instead of a fixed sleep; also move rmSync into a finally/afterEach so a failed expectation does not leak the temp dir.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/logs.test.ts` around lines 130 - 146, The followLogFile test should
avoid fixed timing and cleanup leaks. Configure followLogFile with a small
intervalMs, poll until SCRIPT_LINE is observed or a timeout is reached, then
stop watching; move temporary-directory removal into a finally block or
afterEach so cleanup runs even when assertions fail.
tests/proxy-events.test.ts (1)

67-112: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the beforeEach/afterEach temp-dir pattern here. Each test repeats mkdtempSync/rmSync, which leaks the directory when an expectation fails, and Line 107 points the event log at os.tmpdir() directly (<tmp>/data/logs/proxy.log) rather than an isolated dir.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/proxy-events.test.ts` around lines 67 - 112, Refactor the
handleProxyResponseBody test suite to use shared beforeEach and afterEach hooks
for creating and removing an isolated temporary directory. Store the directory
and context in suite-scoped variables, reuse the context in each test, and
ensure the non-JSON response test also uses the isolated directory rather than
os.tmpdir() directly.
src/cmd/node.ts (1)

202-222: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Log subscription is only closed on the supervisor path. stopService runs when CKB/miner exit unexpectedly; a clean shutdown (SIGINT/SIGTERM handling elsewhere) leaves the socket open. Consider closing it from the signal path too so the retry timer/socket cannot hold the event loop.

Also applies to: 237-237

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cmd/node.ts` around lines 202 - 222, The logSubscription created in the
node startup flow must also be closed during clean SIGINT/SIGTERM shutdown, not
only through stopService. Update the existing signal-handling path to call the
subscription’s cleanup/unsubscribe operation when logSubscription is present,
ensuring the retry timer and socket are released while preserving the current
supervisor cleanup behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/cmd/node.ts`:
- Around line 208-212: Sanitize entry.message in the subscribeToNodeLogs
callback before passing it to logger.info, removing or escaping C0/C1 control
characters and ANSI/OSC/DCS terminal sequences while preserving normal text.
Keep the existing SCRIPT_LOG_TARGET filter and “CKB-Script:” prefix unchanged.

In `@src/devnet/log-file.ts`:
- Around line 134-144: Update the read loop in the surrounding log-file
processing function to capture the byte count returned by fs.readSync and decode
only the populated portion of buffer, preserving the existing partial-line
handling and onLine behavior.

In `@src/tools/proxy-events.ts`:
- Around line 92-95: Update handleProxyResponseBody to normalize contentType by
removing parameters such as “; charset=utf-8” before comparing it with
application/json. Preserve the existing body and JSON-shape checks, and add a
test covering a charset-bearing application/json response.
- Around line 64-66: Update the RPC event logging in the proxy event handler
around method and response code/message handling to sanitize or serialize all
fields before appending to proxy.log. Prevent embedded newlines, carriage
returns, and terminal control characters from creating forged records or
corrupting offckb logs rpc output, while preserving the existing event content
for safe values.

---

Nitpick comments:
In `@src/cmd/node.ts`:
- Around line 202-222: The logSubscription created in the node startup flow must
also be closed during clean SIGINT/SIGTERM shutdown, not only through
stopService. Update the existing signal-handling path to call the subscription’s
cleanup/unsubscribe operation when logSubscription is present, ensuring the
retry timer and socket are released while preserving the current supervisor
cleanup behavior.

In `@src/devnet/log-subscription.ts`:
- Around line 143-170: Track the retry timer created in the error handler of the
subscription’s connect flow, and clear it in close() while marking it inactive.
Ensure the timer is reset when it fires and does not keep the event loop alive,
while preserving the existing closed check and retry behavior.

In `@tests/logs-command.test.ts`:
- Around line 28-40: Update the test fixture setup around fixture() to track
each temporary root it creates, and add afterEach cleanup that removes those
roots recursively after every test. Follow the cleanup pattern used by the other
test suites, while preserving fixture’s existing settings and transport setup.

In `@tests/logs.test.ts`:
- Around line 130-146: The followLogFile test should avoid fixed timing and
cleanup leaks. Configure followLogFile with a small intervalMs, poll until
SCRIPT_LINE is observed or a timeout is reached, then stop watching; move
temporary-directory removal into a finally block or afterEach so cleanup runs
even when assertions fail.

In `@tests/proxy-events.test.ts`:
- Around line 67-112: Refactor the handleProxyResponseBody test suite to use
shared beforeEach and afterEach hooks for creating and removing an isolated
temporary directory. Store the directory and context in suite-scoped variables,
reuse the context in each test, and ensure the non-JSON response test also uses
the isolated directory rather than os.tmpdir() directly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0f7e01c1-ee00-4c64-969f-f3cf242807dd

📥 Commits

Reviewing files that changed from the base of the PR and between 600b320 and 319f9fa.

📒 Files selected for processing (17)
  • .changeset/logs-command-quiet-node.md
  • README.md
  • src/cli.ts
  • src/cmd/logs.ts
  • src/cmd/node.ts
  • src/cmd/status.ts
  • src/devnet/log-file.ts
  • src/devnet/log-subscription.ts
  • src/tools/proxy-events.ts
  • src/tools/rpc-proxy.ts
  • tests/log-subscription.test.ts
  • tests/logs-command.test.ts
  • tests/logs.test.ts
  • tests/node-quiet-mode.test.ts
  • tests/node-supervisor.test.ts
  • tests/node-terminal-rpc.test.ts
  • tests/proxy-events.test.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • tests/log-subscription.test.ts
  • .changeset/logs-command-quiet-node.md
  • src/cmd/logs.ts
  • README.md
  • tests/node-supervisor.test.ts
  • src/tools/rpc-proxy.ts
  • tests/node-quiet-mode.test.ts
  • src/cli.ts
  • src/cmd/status.ts

Comment thread src/cmd/node.ts
Comment thread src/devnet/log-file.ts
Comment thread src/tools/proxy-events.ts
Comment thread src/tools/proxy-events.ts
Add offckb logs [node|script|miner|rpc] [-f] [--grep] [--tail], reading
the log files CKB always writes (run.log/miner.log) plus a new proxy
event log, so logs are reachable in every run mode and pipe/agent
friendly.

A foreground offckb node is quiet by default: lifecycle events, live
contract script debug output (via the node's TCP log subscription, the
same channel ckb-tui uses), send_transaction hashes, and RPC errors
still print; --verbose restores the raw stdout relay.

The RPC proxy drops per-request lines to debug, warns on JSON-RPC
errors in responses, and appends everything to data/logs/proxy.log
(viewable via offckb logs rpc).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@humble-little-bear
humble-little-bear force-pushed the agent/claude-bear/48973f2a branch from 7ea55e1 to 89f3e38 Compare July 29, 2026 14:14

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (4)
src/cmd/node.ts (1)

208-212: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Injection (CWE-117)

Reachability: External
● Entry
  src/cli.ts:86
  startNode
│
▼
● Sink
  src/cmd/node.ts

Script log output is still forwarded unsanitized. entry.message arrives from the node's TCP log stream (which relays contract debug! output) and goes straight to logger.info, so embedded ANSI/OSC/C0 sequences reach the terminal. The file already has cleanChildOutput for exactly this on the stdout path; reuse it (or a control-char strip) here.

🔒 Proposed fix
-        if (entry.target === SCRIPT_LOG_TARGET) logger.info(['CKB-Script:', entry.message]);
+        if (entry.target === SCRIPT_LOG_TARGET) logger.info(['CKB-Script:', cleanChildOutput(entry.message)]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cmd/node.ts` around lines 208 - 212, Sanitize script log messages before
forwarding them in the subscribeToNodeLogs callback, reusing the existing
cleanChildOutput helper used by the stdout path. Apply it to entry.message while
preserving the SCRIPT_LOG_TARGET filter and logger.info call.
src/tools/proxy-events.ts (2)

66-66: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Injection (CWE-117)

Reachability: External
● Entry
  src/cli.ts:86
  startNode
│
▼
● Hop
  src/cmd/node.ts
│
▼
● Hop
  src/tools/rpc-proxy.ts:12
  createRPCProxy: Target RPC server
│
▼
● Sink
  src/tools/proxy-events.ts

Event fields are still written to proxy.log unescaped. method and the error code/message come from the proxied payload; an embedded \n/\r splits one event into forged records and control chars corrupt offckb logs rpc output. Strip/escape control characters in event() (single choke point) rather than at each call site.

🔒 Proposed fix
     event(text: string) {
       try {
+        // One event is one line: neutralize embedded newlines/control chars.
+        const safe = text.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ');
         if (!dirReady) {
           fs.mkdirSync(path.dirname(filePath), { recursive: true });
           dirReady = true;
         }
-        fs.appendFileSync(filePath, `${new Date().toISOString()} ${text}\n`);
+        fs.appendFileSync(filePath, `${new Date().toISOString()} ${safe}\n`);

Also applies to: 104-105

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tools/proxy-events.ts` at line 66, Update the event() helper in
proxy-events.ts to sanitize or escape control characters, including newline and
carriage return, in all event fields before writing to proxy.log. Apply the
change at this single choke point so both the request method and proxied error
code/message emitted by the existing event() calls are protected without
modifying individual call sites.

92-95: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Strict content-type comparison still drops most real responses. application/json; charset=utf-8 is a normal CKB/HTTP response value and fails contentType !== 'application/json', so JSON-RPC errors are never surfaced or written to proxy.log. Normalize the media type before comparing. (Previously flagged and marked addressed, but the exact-match check is still here.)

🔧 Proposed fix
-  if (contentType !== 'application/json') return;
+  const mediaType = (contentType ?? '').split(';')[0].trim().toLowerCase();
+  if (mediaType !== 'application/json') return;

Worth adding a test with 'application/json; charset=utf-8' in tests/proxy-events.test.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tools/proxy-events.ts` around lines 92 - 95, Update
handleProxyResponseBody to normalize contentType to its media type before
comparing it with application/json, so parameters such as charset=utf-8 are
accepted. Preserve the existing empty-body and JSON-shape checks, and add
coverage in proxy-events.test.ts for a content type containing application/json;
charset=utf-8.
src/devnet/log-file.ts (1)

134-144: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Read-block decoding is still byte-unsafe. Two previously raised issues remain: fs.readSync's return value is ignored (a short read leaves NUL bytes that get emitted inside lines), and each chunk is decoded independently, so a multi-byte UTF-8 char straddling a read boundary becomes .

🔧 Proposed fix
+  const decoder = new TextDecoder('utf-8');
...
       const length = curr.size - offset;
       const buffer = Buffer.alloc(length);
-      fs.readSync(fd, buffer, 0, length, offset);
-      offset = curr.size;
-      const text = partial + buffer.toString('utf8');
+      const bytesRead = fs.readSync(fd, buffer, 0, length, offset);
+      offset += bytesRead;
+      const text = partial + decoder.decode(buffer.subarray(0, bytesRead), { stream: true });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/devnet/log-file.ts` around lines 134 - 144, Update the read loop around
the visible fs.readSync call to use the returned byte count when decoding, so
unread Buffer bytes are excluded from text processing. Preserve UTF-8 sequences
across chunk boundaries by using a persistent StringDecoder (or equivalent
incremental decoder) for the stream, and flush it when the file-following read
ends before splitting lines and invoking onLine.
🧹 Nitpick comments (5)
tests/proxy-events.test.ts (1)

67-104: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the beforeEach/afterEach teardown from the block above. These three tests rmSync at the end of the test body, so a failing assertion leaks the temp dir; the first describe already has the right pattern.

♻️ Suggested cleanup
 describe('handleProxyResponseBody', () => {
+  let dir: string;
+  beforeEach(() => {
+    dir = fs.mkdtempSync(path.join(os.tmpdir(), 'offckb-proxy-'));
+  });
+  afterEach(() => fs.rmSync(dir, { recursive: true, force: true }));
+
   it('warns on JSON-RPC errors and records them', () => {
-    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'offckb-proxy-'));
     const ctx = makeCtx(path.join(dir, 'transactions'));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/proxy-events.test.ts` around lines 67 - 104, Update the three tests in
the handleProxyResponseBody describe block to use the existing
beforeEach/afterEach temporary-directory setup and teardown from the surrounding
test block. Remove each test body's direct fs.rmSync cleanup while preserving
the current assertions and test behavior.
src/devnet/log-subscription.ts (1)

143-150: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Track the retry timer so close() is fully synchronous. A pending setTimeout(connect, retryDelayMs) survives close(); connect() no-ops thanks to closed, but the timer can still hold the event loop open for up to retryDelayMs after shutdown.

♻️ Optional cleanup
   let attempts = 0;
   let failedReported = false;
+  let retryTimer: NodeJS.Timeout | null = null;
...
       if (attempts < maxAttempts) {
-        setTimeout(connect, retryDelayMs);
+        retryTimer = setTimeout(connect, retryDelayMs);
+        retryTimer.unref?.();
...
     close() {
       closed = true;
+      if (retryTimer) clearTimeout(retryTimer);
+      retryTimer = null;
       socket?.destroy();

Also applies to: 164-170

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/devnet/log-subscription.ts` around lines 143 - 150, Track the retry
timeout created in the connection error handler and clear it from close().
Update the surrounding subscription state and close implementation so pending
reconnect timers are cancelled during shutdown, while preserving the existing
retry behavior and closed guard.
tests/logs-command.test.ts (1)

28-40: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

fixture() leaks a temp dir per test. No afterEach removes the mkdtempSync roots, so each run leaves six directories in os.tmpdir(). Return the root and clean it up.

♻️ Suggested cleanup
-function fixture(): { settings: Settings; transport: CapturingTransport } {
+const roots: string[] = [];
+afterEach(() => {
+  while (roots.length) fs.rmSync(roots.pop() as string, { recursive: true, force: true });
+});
+
+function fixture(): { settings: Settings; transport: CapturingTransport } {
   const root = fs.mkdtempSync(path.join(os.tmpdir(), 'offckb-logs-cmd-'));
+  roots.push(root);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/logs-command.test.ts` around lines 28 - 40, Update fixture() to return
the mkdtempSync root alongside settings and transport, then add per-test cleanup
that removes this root recursively after each test. Ensure every test using
fixture() registers or performs cleanup so temporary directories do not remain
in os.tmpdir().
src/tools/proxy-events.ts (1)

33-50: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider bounding proxy.log. Every request appends a line synchronously with no size cap or rotation, so a long-running devnet with a chatty indexer grows the file indefinitely and each request pays a blocking write. A size check with a single .1 rollover (or dropping the per-request request <method> line and keeping only transactions/errors) would keep it bounded.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tools/proxy-events.ts` around lines 33 - 50, Update createProxyEventLog
so proxy.log growth is bounded: before appending each event, enforce a size
limit and perform a single .1 rollover, preserving logging failure isolation and
directory creation behavior. Keep the existing event content unless implementing
the alternative of removing only per-request request lines.
tests/logs.test.ts (1)

141-174: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a truncation/rotation case. The curr.size < prev.size reset branch in followLogFile is the trickiest part of the offset bookkeeping and is currently uncovered — a second listeners[0](smallerStat, largerStat) invocation after rewriting the file would pin it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/logs.test.ts` around lines 141 - 174, Add a test case in the
followLogFile suite that rewrites or truncates the watched log to a smaller
size, then invokes listeners[0] with the smaller current stat and larger
previous stat. Assert that followLogFile resets its offset and emits the
rewritten file content, while preserving cleanup via stop and the existing
mock/file teardown.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/tools/rpc-proxy.ts`:
- Line 45: Update the call to handleProxyResponseBody so the Content-Type value
is normalized to its base media type by removing parameters such as charset
before comparison. Preserve the existing behavior for exact application/json
responses while ensuring parameterized JSON types are recognized for RPC error
logging and persistence.

---

Duplicate comments:
In `@src/cmd/node.ts`:
- Around line 208-212: Sanitize script log messages before forwarding them in
the subscribeToNodeLogs callback, reusing the existing cleanChildOutput helper
used by the stdout path. Apply it to entry.message while preserving the
SCRIPT_LOG_TARGET filter and logger.info call.

In `@src/devnet/log-file.ts`:
- Around line 134-144: Update the read loop around the visible fs.readSync call
to use the returned byte count when decoding, so unread Buffer bytes are
excluded from text processing. Preserve UTF-8 sequences across chunk boundaries
by using a persistent StringDecoder (or equivalent incremental decoder) for the
stream, and flush it when the file-following read ends before splitting lines
and invoking onLine.

In `@src/tools/proxy-events.ts`:
- Line 66: Update the event() helper in proxy-events.ts to sanitize or escape
control characters, including newline and carriage return, in all event fields
before writing to proxy.log. Apply the change at this single choke point so both
the request method and proxied error code/message emitted by the existing
event() calls are protected without modifying individual call sites.
- Around line 92-95: Update handleProxyResponseBody to normalize contentType to
its media type before comparing it with application/json, so parameters such as
charset=utf-8 are accepted. Preserve the existing empty-body and JSON-shape
checks, and add coverage in proxy-events.test.ts for a content type containing
application/json; charset=utf-8.

---

Nitpick comments:
In `@src/devnet/log-subscription.ts`:
- Around line 143-150: Track the retry timeout created in the connection error
handler and clear it from close(). Update the surrounding subscription state and
close implementation so pending reconnect timers are cancelled during shutdown,
while preserving the existing retry behavior and closed guard.

In `@src/tools/proxy-events.ts`:
- Around line 33-50: Update createProxyEventLog so proxy.log growth is bounded:
before appending each event, enforce a size limit and perform a single .1
rollover, preserving logging failure isolation and directory creation behavior.
Keep the existing event content unless implementing the alternative of removing
only per-request request lines.

In `@tests/logs-command.test.ts`:
- Around line 28-40: Update fixture() to return the mkdtempSync root alongside
settings and transport, then add per-test cleanup that removes this root
recursively after each test. Ensure every test using fixture() registers or
performs cleanup so temporary directories do not remain in os.tmpdir().

In `@tests/logs.test.ts`:
- Around line 141-174: Add a test case in the followLogFile suite that rewrites
or truncates the watched log to a smaller size, then invokes listeners[0] with
the smaller current stat and larger previous stat. Assert that followLogFile
resets its offset and emits the rewritten file content, while preserving cleanup
via stop and the existing mock/file teardown.

In `@tests/proxy-events.test.ts`:
- Around line 67-104: Update the three tests in the handleProxyResponseBody
describe block to use the existing beforeEach/afterEach temporary-directory
setup and teardown from the surrounding test block. Remove each test body's
direct fs.rmSync cleanup while preserving the current assertions and test
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 879429b2-a5a3-48c6-ae90-ac0e6e386c1d

📥 Commits

Reviewing files that changed from the base of the PR and between 319f9fa and 89f3e38.

📒 Files selected for processing (17)
  • .changeset/logs-command-quiet-node.md
  • README.md
  • src/cli.ts
  • src/cmd/logs.ts
  • src/cmd/node.ts
  • src/cmd/status.ts
  • src/devnet/log-file.ts
  • src/devnet/log-subscription.ts
  • src/tools/proxy-events.ts
  • src/tools/rpc-proxy.ts
  • tests/log-subscription.test.ts
  • tests/logs-command.test.ts
  • tests/logs.test.ts
  • tests/node-quiet-mode.test.ts
  • tests/node-supervisor.test.ts
  • tests/node-terminal-rpc.test.ts
  • tests/proxy-events.test.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • tests/node-supervisor.test.ts
  • tests/log-subscription.test.ts
  • .changeset/logs-command-quiet-node.md
  • tests/node-quiet-mode.test.ts
  • tests/node-terminal-rpc.test.ts
  • README.md
  • src/cmd/status.ts
  • src/cmd/logs.ts

Comment thread src/tools/rpc-proxy.ts
- node: sanitize relayed script log entries (CSI/OSC/C0/C1) via
  cleanChildOutput so crafted debug! output cannot inject terminal
  control sequences
- log-file: honor readSync's byte count and decode with a streaming
  TextDecoder so multi-byte UTF-8 survives chunk boundaries
- proxy-events: sanitize event text at the single event() choke point
  (one event = one line), normalize the response media type before the
  application/json check (charset params), and bound proxy.log with a
  single .1 rollover at 10 MB
- log-subscription: retry only during the initial connect window and
  track/unref/clear the retry timer so close() is fully synchronous
- cli: throw commander's InvalidArgumentError from the --tail parser
  and align the --verbose help text with the actual quiet defaults
- tests: add follow-mode script/grep, truncation/rotation, UTF-8 split,
  event sanitization, rollover, charset content-type, and subscription
  retry cases; move temp-dir handling to afterEach cleanup

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
tests/logs.test.ts (1)

145-145: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

StatListener type duplicated across spec files.

The identical alias also appears in tests/logs-command.test.ts. Worth hoisting into a shared test-utils module alongside captureWatchListener/tempLog if more log-following tests get added.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/logs.test.ts` at line 145, Move the duplicated StatListener type alias
from the log-following test files into the shared test-utils module alongside
captureWatchListener and tempLog. Update tests/logs.test.ts and
tests/logs-command.test.ts to import and reuse the shared StatListener
definition, removing their local aliases.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/devnet/log-file.ts`:
- Around line 124-129: Update the rotation condition in the onChange callback to
also reset when the file inode changes by including curr.ino !== prev.ino
alongside the existing size checks. Preserve the current offset, partial, and
decoder reset behavior for all detected rotation cases.

In `@src/devnet/log-subscription.ts`:
- Around line 136-137: Treat the JSON-RPC subscription-id response as the
successful connection point instead of the TCP connect event: remove the
terminal/retry-suppression update from the socket connect callback and set it
after parsing the subscription response in src/devnet/log-subscription.ts (lines
136-137). Update tests/log-subscription.test.ts (lines 155-162) to emit that
subscription-id response before asserting a later socket error does not
reconnect.

In `@src/tools/proxy-events.ts`:
- Around line 123-126: Sanitize the stringified JSON-RPC error fields before
retaining or logging them: update the handling around the media-type check in
src/tools/proxy-events.ts lines 123-126 so both code and message passed to
ctx.sink.warn and ctx.events.event use sanitizeEventText. Extend the
charset-bearing JSON-RPC error case in tests/proxy-events.test.ts lines 88-98
with an escape character and assert the warning output contains no escape
character.

---

Nitpick comments:
In `@tests/logs.test.ts`:
- Line 145: Move the duplicated StatListener type alias from the log-following
test files into the shared test-utils module alongside captureWatchListener and
tempLog. Update tests/logs.test.ts and tests/logs-command.test.ts to import and
reuse the shared StatListener definition, removing their local aliases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4b50f90a-63ad-4293-ba95-7a1ba202e66c

📥 Commits

Reviewing files that changed from the base of the PR and between 89f3e38 and 44f3ef8.

📒 Files selected for processing (10)
  • src/cli.ts
  • src/cmd/node.ts
  • src/devnet/log-file.ts
  • src/devnet/log-subscription.ts
  • src/tools/proxy-events.ts
  • tests/log-subscription.test.ts
  • tests/logs-command.test.ts
  • tests/logs.test.ts
  • tests/node-quiet-mode.test.ts
  • tests/proxy-events.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/node-quiet-mode.test.ts

Comment thread src/devnet/log-file.ts
Comment on lines +124 to +129
const onChange = (curr: fs.Stats, prev: fs.Stats) => {
if (curr.size < prev.size || curr.size < offset) {
// Truncated or rotated: restart from the beginning.
offset = 0;
partial = '';
decoder = new TextDecoder('utf-8');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Detect inode changes as log rotation.

A replacement file with the same or larger size bypasses this reset; curr.size === offset can then discard the new file’s initial entries. Include curr.ino !== prev.ino in the rotation condition.

Proposed fix
-    if (curr.size < prev.size || curr.size < offset) {
+    if (curr.ino !== prev.ino || curr.size < prev.size || curr.size < offset) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const onChange = (curr: fs.Stats, prev: fs.Stats) => {
if (curr.size < prev.size || curr.size < offset) {
// Truncated or rotated: restart from the beginning.
offset = 0;
partial = '';
decoder = new TextDecoder('utf-8');
const onChange = (curr: fs.Stats, prev: fs.Stats) => {
if (curr.ino !== prev.ino || curr.size < prev.size || curr.size < offset) {
// Truncated or rotated: restart from the beginning.
offset = 0;
partial = '';
decoder = new TextDecoder('utf-8');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/devnet/log-file.ts` around lines 124 - 129, Update the rotation condition
in the onChange callback to also reset when the file inode changes by including
curr.ino !== prev.ino alongside the existing size checks. Preserve the current
offset, partial, and decoder reset behavior for all detected rotation cases.

Comment on lines +136 to +137
conn.on('connect', () => {
everConnected = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Treat JSON-RPC acknowledgement—not TCP connect—as a live subscription. A TCP connection can reset before the server processes subscribe; suppressing retries at connect permanently drops foreground script logs.

  • src/devnet/log-subscription.ts#L136-L137: set the terminal/retry-suppression state after parsing the subscription-id response, not in the socket connect callback.
  • tests/log-subscription.test.ts#L155-L162: emit the subscription-id response before asserting that a later socket error does not reconnect.
📍 Affects 2 files
  • src/devnet/log-subscription.ts#L136-L137 (this comment)
  • tests/log-subscription.test.ts#L155-L162
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/devnet/log-subscription.ts` around lines 136 - 137, Treat the JSON-RPC
subscription-id response as the successful connection point instead of the TCP
connect event: remove the terminal/retry-suppression update from the socket
connect callback and set it after parsing the subscription response in
src/devnet/log-subscription.ts (lines 136-137). Update
tests/log-subscription.test.ts (lines 155-162) to emit that subscription-id
response before asserting a later socket error does not reconnect.

Comment thread src/tools/proxy-events.ts
Comment on lines +123 to +126
// Real servers answer with parameters attached (application/json;
// charset=utf-8), so compare the bare media type, not the raw header.
const mediaType = (contentType ?? '').split(';')[0].trim().toLowerCase();
if (mediaType !== 'application/json') return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Injection (CWE-150)

Reachability: External
● Entry
  tests/proxy-events.test.ts
│
▼
● Sink
  src/tools/proxy-events.ts

Sanitize JSON-RPC error fields before calling sink.warn.

sanitizeEventText protects proxy.log, but code and message still reach ctx.sink.warn raw. A malicious proxied response can inject terminal control sequences into retained RPC-error output.

  • src/tools/proxy-events.ts#L123-L126: sanitize stringified error fields before using them in both ctx.sink.warn and ctx.events.event.
  • tests/proxy-events.test.ts#L88-L98: include an escape character in the charset-bearing JSON-RPC error and assert the warning receives no escape character.
Proposed fix
-      const code = entry.error.code ?? 'unknown';
-      const message = entry.error.message ?? 'unknown error';
+      const code = sanitizeEventText(String(entry.error.code ?? 'unknown'));
+      const message = sanitizeEventText(String(entry.error.message ?? 'unknown error'));
       ctx.sink.warn(`RPC error: [${code}] ${message}`);
       ctx.events.event(`error [${code}] ${message}`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Real servers answer with parameters attached (application/json;
// charset=utf-8), so compare the bare media type, not the raw header.
const mediaType = (contentType ?? '').split(';')[0].trim().toLowerCase();
if (mediaType !== 'application/json') return;
const code = sanitizeEventText(String(entry.error.code ?? 'unknown'));
const message = sanitizeEventText(String(entry.error.message ?? 'unknown error'));
ctx.sink.warn(`RPC error: [${code}] ${message}`);
ctx.events.event(`error [${code}] ${message}`);
📍 Affects 2 files
  • src/tools/proxy-events.ts#L123-L126 (this comment)
  • tests/proxy-events.test.ts#L88-L98
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tools/proxy-events.ts` around lines 123 - 126, Sanitize the stringified
JSON-RPC error fields before retaining or logging them: update the handling
around the media-type check in src/tools/proxy-events.ts lines 123-126 so both
code and message passed to ctx.sink.warn and ctx.events.event use
sanitizeEventText. Extend the charset-bearing JSON-RPC error case in
tests/proxy-events.test.ts lines 88-98 with an escape character and assert the
warning output contains no escape character.

@humble-little-bear

Copy link
Copy Markdown
Collaborator Author

Review findings addressed in 44f3ef8 (CI green on ubuntu/windows/macos, 301 tests passing).

Fixed

  • src/cmd/node.ts — script log entries relayed from the TCP subscription now go through cleanChildOutput, extended to strip OSC sequences and C0/C1 control chars in addition to CSI. Crafted debug! output can no longer inject terminal control sequences.
  • src/devnet/log-file.tsfollowLogFile honors readSync's byte count and decodes via a streaming TextDecoder, so multi-byte UTF-8 (e.g. non-ASCII contract debug output) survives chunk boundaries; decoder state resets on truncation/rotation along with the offset.
  • src/tools/proxy-events.tsevent() sanitizes at the single choke point (control chars incl. \n/\r → space) so one event is always one line in proxy.log; the response media type is normalized before comparison, so application/json; charset=utf-8 now reaches JSON-RPC error logging (this also covers the rpc-proxy.ts:45 call-site comment — normalization happens inside handleProxyResponseBody); proxy.log is bounded with a single .1 rollover at 10 MB.
  • src/devnet/log-subscription.ts — retries now only happen during the initial connect window (everConnected); the retry timer is tracked, unref'd, and cleared in close().
  • src/cli.ts — the --tail parser throws commander's InvalidArgumentError (clean error: option '--tail <lines>' argument ... is invalid output, exit code 1); --verbose help text now matches the actual quiet defaults.

Skipped, with reasons

  • appendFileSync → persistent write stream (proxy-events): for a local devnet proxy the per-event sync append is microseconds and keeps ordering and crash safety trivial; a stream adds flush/lifecycle complexity and can lose buffered events on exit. The unbounded-growth half of the concern is addressed by the rollover cap.
  • Close the log subscription on the SIGINT/SIGTERM path: foreground node has no custom signal handler — Ctrl+C uses the default disposition, so process exit tears the socket down, and with the retry timer now unref'd and tracked it cannot hold the event loop open.
  • offckb logs rpc only reads the devnet proxy log: by design — the whole offckb logs command is devnet-scoped (its help text says so); per-network proxy.log files are still written for testnet/mainnet proxies via proxyLogPathForNetwork.

New test coverage: script-entry sanitization, follow-mode script+grep gating, truncation/rotation re-read, multi-byte UTF-8 split reassembly, event-text sanitization, proxy.log rollover, charset-bearing content type, and the subscription retry window.

@RetricSu
RetricSu merged commit 178379f into develop Jul 30, 2026
7 checks passed
@RetricSu
RetricSu deleted the agent/claude-bear/48973f2a branch July 30, 2026 02:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants