Skip to content

fix(coding-agent): keep Bun console output out of the RPC/JSON stdout stream - #284

Merged
Kushalkhemka merged 3 commits into
pi-boltfrom
fix/rpc-console-stdout
Nov 8, 2026
Merged

Kushalkhemka merged 3 commits into
pi-boltfrom
fix/rpc-console-stdout

Conversation

@Kushalkhemka

Copy link
Copy Markdown
Member

Problem: on Bun, console.log / info / debug / dir / dirxml / table / trace / count / group and Bun's console.write print to fd 1 without calling a replaced process.stdout.write (oven-sh/bun#47982, fix oven-sh/bun#47986 still open). Node's console calls the replaced write, so takeOverStdout() sends it to stderr there. On Pi-Bolt, an extension's or a dependency's console.log in RPC, JSON, ACP or MCP mode (and pi-bolt service, the Android engine) went into the JSON stream.

Repro (Bun 1.4.2 and the Pi-Bolt 1.4.3 runtime): an extension that calls console.log("extension loaded", { a: 1 }) in --mode rpc printed that line, unformatted Bun-style, before the first JSON response on stdout.

Fix: while stdout is taken over on Bun, those methods are replaced by the methods of a node:console Console over process.stdout / process.stderr, which calls the replaced write as Node's global console does, so the output goes to stderr with Node's formatting. trace is built from util.format + Error.captureStackTrace because Bun's Console.trace drops the Trace: <message> line; console.write is routed through process.stdout.write. restoreStdout() puts the originals back. Node and interactive mode are unchanged (no takeover there / not on Bun).

Tests: test/suite/regressions/rpc-console-stdout.test.ts (faux provider):

  • Node, in-process RPC session: an extension's console.log/info reach stderr, stdout is only JSON lines.
  • Bun (skipped when bun is not installed): test/fixtures/rpc-console-session.ts runs a full RPC session under Bun with an extension calling every method above; stdout must be JSON lines only and stderr must have each line. It fails without the fix.

npm run check clean.

… stream

On Bun, console.log/info/debug/dir/dirxml/table/trace/count/group and
console.write print to fd 1 without calling a replaced
process.stdout.write (oven-sh/bun#47982), so an extension's console.log
corrupted the JSON stream in RPC, JSON, ACP and MCP modes. While stdout
is taken over, route them through a node:console Console over
process.stdout, which goes to stderr as on Node; restore them after.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-08T03:59:01.689753Z 27d6744 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 32ec94a0a5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Kushalkhemka

Copy link
Copy Markdown
Member Author

@codex review

@opensec-code-review opensec-code-review 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.

OpenSec security review

✅ No security findings in this change.

The change makes takeOverStdout() replace Bun's global console stdout methods with a node:console Console over process.stdout, so console output from extensions and dependencies goes to stderr instead of the JSON protocol stream in RPC/JSON/ACP/MCP/service mode, restores the originals in restoreStdout(), and adds a Bun spawn regression test. I reviewed the console patch/restore ordering, the new sinks (stderr writes, trace stack output, console.write byte conversion), the list of methods covered, and whether any trusted-actor or untrusted-actor path gains reach, data, or availability; nothing introduced by the change survives verification.

Notes (uncertain or pre-existing, not counted)
  • Pre-existing and not introduced here: in RPC/JSON/ACP/MCP/service mode, in-process code that bypasses process.stdout.write (Bun's console.assert, Bun.stdout.write, direct fd 1 writes) can still inject lines into the JSON stream; the change leaves those paths open (extensions are an explicit SECURITY.md exclusion).
  • The Bun console rerouting is installed while stdoutTakeoverState is still unassigned (output-guard.ts:110-111), so any console output during Console construction would reach the real stdout; no such write exists in that window, so this is a latent ordering hazard, not a reachable path.
  • packages/coding-agent/test/fixtures/rpc-console-session.ts and test/suite/regressions/rpc-console-stdout.test.ts are test-only and out of review scope.
3 candidates rejected by verification
  • Bun console methods outside the replaced list still write to the protocol stream on stdout: The writer would be a user-installed extension/dependency, which SECURITY.md trusts and which can already emit arbitrary protocol JSON in-process; the remaining gaps (console.assert, Bun.stdout, fd 1 writes) are pre-existing Bun behavior the diff neither adds nor newly exposes.
  • Extension console output, including full stack traces, is now persisted in stderr logs: The bytes are produced by the trusted extension and go to the process's own stderr, whose reader is the operator; the change narrows where they go and matches long-standing Node behavior.
  • Synchronous throw from the replacement Console on a broken stderr pipe reaches extension code: Only the parent/supervisor that owns the stderr pipe can break it, a broken stderr already breaks every log write, and the same synchronous path already existed on Node before this change.

Range 72981bda4ac8..32ec94a0a55a, 245 changed lines. Review by OpenSec Code Review.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: befba935ae

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@opensec-code-review opensec-code-review 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.

OpenSec security review

✅ No security findings in this change.

The change fixes a Bun-only bug where extension or dependency console output went straight to fd 1 and corrupted the JSON/JSON-RPC stream in RPC, JSON, ACP, MCP and service modes: while stdout is taken over, the global console stdout methods are pointed at a node:console Console over the replaced process.stdout so the output lands on stderr, and restoreStdout puts them back. It was checked against the trust model (extensions and project resources are trusted once installed or approved) for new attacker reach into sinks, terminal output, buffer handling and resource limits, and no untrusted-attacker gain was found.

Notes (uncertain or pre-existing, not counted)
  • restoreStdout (output-guard.ts:152-162) writes back stored method values, so a console method absent on some Bun build would be restored as undefined; a robustness nit, not attacker reachable.
  • After restoreStdout in JSON/print mode, console writes go to real fd 1 again (main.ts:1127); behaviour unchanged by this PR and only on the shutdown path.
  • Change reviewed for other classes: no new entry points, no path/shell/query/template sinks, no dependency, CI or release changes; the test fixture and test file are test-only.
2 candidates rejected by verification
  • Terminal escape-sequence injection via stderr for extension/dependency console output: Node already routed console.log to stderr during stdout takeover (Node's global console calls the replaced process.stdout.write, output-guard.ts:96-108); this change only makes Bun match that, so nothing moves safe to unsafe and console.warn/error already reached stderr.
  • Memory contents exposure through the Bun console.write replacement (Buffer.from over chunk.byteOffset/byteLength): Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength) confines the view to exactly the chunk's own bytes (output-guard.ts:139-146); pooled-buffer neighbours are never serialized.

Range 72981bda4ac8..befba935ae9c, 246 changed lines. Review by OpenSec Code Review.

With stdout taken over in -p mode, an extension's console.log now goes to
stderr on Bun as it does on Node.
@Kushalkhemka

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 27d6744e1e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@opensec-code-review opensec-code-review 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.

OpenSec security review

✅ No security findings in this change.

The change hardens stream separation on the Bun runtime: while stdout is taken over (RPC, JSON, ACP, MCP and print modes), the global console methods that Bun writes straight to fd 1 are replaced by a node:console Console over process.stdout/process.stderr, so extension and dependency logging goes to stderr as it already does on Node, and restoreStdout puts the originals back. I read output-guard.ts at head with its callers (main.ts, rpc-mode, print-mode, ACP and MCP modes) and the project trust model, and checked the three candidate issues with an independent verifier. All were rejected: no removed check, no new entry point, no new sink reachable by an actor outside the trust model, and protocol output still flows through the pre-captured raw stdout write. No confirmed findings.

Notes (uncertain or pre-existing, not counted)
  • output-guard.ts:107 evaluates routeBunConsoleThroughStdoutWrite() inside the state object literal, so a throw there would leave the global console partly mutated with stdoutTakeoverState unset; robustness nit on a non-attacker-controlled path, not exploitable.
  • output-guard.ts:158-161 restores console methods unconditionally; a method replaced by another party between takeover and restore would be overwritten. Process-lifetime only.
  • The change reduces exposure: on Bun it stops extension console output from entering the JSON/RPC/ACP/MCP stdout stream (oven-sh/bun#47982) and keeps print-mode answers on stdout via writeRawStdout, which stays sanitized.
  • scripts/windows-selftest.ps1 now asserts the extension marker on stderr; consistent with Node, where the redirected process.stdout.write already sent console output there. No check weakened.
  • packages/coding-agent/test/fixtures/rpc-console-session.ts and the new regression test are test-only; the fixture runs under a spawned bun in tests and touches no shipped path.
3 candidates rejected by verification
  • Extension/dependency console output now reaches stderr unsanitized, allowing terminal escape injection: The same bytes already reached stderr before the change (console.error and the redirected process.stdout.write in takeOverStdout), the actor is an extension/model/MCP server the user configured (explicitly out of scope in SECURITY.md), and the old Bun path put them in the machine-parsed fd 1 stream instead.
  • routeBunConsoleThroughStdoutWrite can throw partway, leaving process.stdout.write replaced with stdoutTakeoverState unset: Only fixed node:console method names are touched, no attacker input reaches it, and the outcome is a startup crash of the user's own process with no gain.
  • Console replacement lets a caller clobber console methods or redirect protocol JSON off stdout: Protocol output goes through writeRawStdout/getRawStdoutWrite, whose rawStdoutWrite is bound before any console mutation; the only code that could call restoreStdout or pre-clobber console is an extension running with the user's permissions, which the trust model excludes.

Range 72981bda4ac8..27d6744e1e38, 249 changed lines. Review by OpenSec Code Review.

@Kushalkhemka
Kushalkhemka merged commit 93b6678 into pi-bolt Nov 8, 2026
19 checks passed
@Kushalkhemka
Kushalkhemka deleted the fix/rpc-console-stdout branch November 8, 2026 04:07
saifymatteo pushed a commit to saifymatteo/Pi-Bolt that referenced this pull request Nov 8, 2026
…ole prints

output-guard.ts imported both at the top (opensec-git#284), and main.ts imports output-guard at
every start, so every `pi -p` and TUI start evaluated them: +3.5 M instructions in
module init and ~1 MB of peak RSS. The Bun console redirect now makes its Node Console
at the first console call, through process.getBuiltinModule, so a run that never
prints through the console never loads them. Output is unchanged.
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