Skip to content

feat(daemon): server-side shell command execution for ! (bang) prefix#4576

Merged
doudouOUC merged 3 commits into
daemon_mode_b_mainfrom
feat/daemon-shell-command
May 28, 2026
Merged

feat(daemon): server-side shell command execution for ! (bang) prefix#4576
doudouOUC merged 3 commits into
daemon_mode_b_mainfrom
feat/daemon-shell-command

Conversation

@doudouOUC

@doudouOUC doudouOUC commented May 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add POST /session/:id/shell route for direct shell command execution in daemon mode, bypassing the LLM entirely
  • Bridge executeShellCommand uses ShellExecutionService with streaming output via shell_output SSE events
  • ACP sessionShellHistory extMethod injects command+result into LLM history (matching CLI's addShellCommandToGeminiHistory format)
  • SDK shellCommand() on DaemonClient / DaemonSessionClient; new DaemonShellCommandResult type
  • Web-shell ! handler now calls sendShellCommand() instead of wrapping as LLM prompt via formatShellCommandPrompt
  • Channel adapters (Telegram/DingTalk/WeChat) detect ! prefix and route through direct execution
  • New user_shell_command / user_shell_result SSE event types with normalizer support

Before

  • Web-shell: ! wrapped command as natural-language prompt → LLM used Bash tool (slow, non-deterministic, wasted tokens)
  • Channels: No ! handling at all — raw text passed to LLM

After

  • All daemon clients: ! executes directly via ShellExecutionService on the daemon, output streams via SSE, result injected into LLM history for context

Test plan

  • Start daemon with qwen serve, open web-shell
  • Type ! echo hello — verify output appears immediately (no LLM turn)
  • Type ! exit 42 — verify non-zero exit code is displayed
  • After ! ls, ask LLM "what files do you see?" — verify LLM can reference the shell output
  • Type ! sleep 1 && echo done — verify streaming output arrives progressively
  • Disconnect client mid-command — verify command is aborted (abort wiring)
  • Verify TypeScript compilation passes: npx tsc --noEmit for all modified packages

🤖 Generated with Qwen Code


📝 描述准确性更新(2026-05-31,作者自查)

更正:仅 web-shell/HTTP/SDK 路径生效;channels(Telegram/钉钉/微信)的 ! 直执行当前未接通(ChannelBase.bridge 为 AcpBridge 无 shellCommand,且存在 this-binding 隐患)。

Add direct shell command execution in daemon mode, matching CLI semantics:
commands run immediately via ShellExecutionService without LLM involvement,
output streams to clients via SSE, and results are injected into LLM history
for context in subsequent turns.

- New POST /session/:id/shell route in daemon server
- Bridge executeShellCommand with streaming output via shell_output SSE events
- ACP extMethod sessionShellHistory for LLM history injection
- SDK client shellCommand() method and DaemonShellCommandResult type
- Web-shell ! handler calls server-side execution instead of wrapping as LLM prompt
- Channel adapters detect ! prefix and route through direct execution
- New user_shell_command / user_shell_result SSE event types

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
Copilot AI review requested due to automatic review settings May 27, 2026 15:10
@github-actions

Copy link
Copy Markdown
Contributor

📋 Review Summary

This PR implements server-side shell command execution for the daemon mode, allowing the ! (bang) prefix to execute commands directly via ShellExecutionService without LLM involvement. The implementation spans 16 files across multiple packages (acp-bridge, channels, cli, sdk-typescript, web-shell). Overall, the code is well-structured with proper error handling, streaming output, and consistent patterns. However, there are several security, reliability, and maintainability concerns that should be addressed before merging.

🔍 General Feedback

  • Positive: The implementation follows existing patterns in the codebase (e.g., mirroring sessionRecap route structure, using established event publishing patterns)
  • Positive: Good separation of concerns across packages with appropriate type sharing via DaemonShellCommandResult
  • Positive: Streaming output is properly implemented with chunked delivery via SSE
  • Positive: Abort handling is wired through from HTTP request close to shell execution
  • Concern: Security implications of direct shell execution without any sanitization or allowlist
  • Concern: Inconsistent error handling between web-shell and channels implementations
  • Pattern: The 256KB truncation for history injection is reasonable but should be documented

🎯 Specific Feedback

🔴 Critical

  • packages/acp-bridge/src/bridge.ts:3163 - Security: No command validation or sanitization. The executeShellCommand function accepts any command string without validation. This creates a significant security vulnerability where any authenticated client can execute arbitrary shell commands on the daemon host. Recommendation: Implement command allowlisting, path restrictions, or at minimum add a security review flag and documentation about the trust model.

  • packages/channels/base/src/ChannelBase.ts:273-295 - Security: Type casting bypass. The channel bridge uses unsafe type casting (this.bridge as { shellCommand: ... }) to check for shell command support. This bypasses TypeScript's type safety and could fail at runtime if the bridge interface changes. Recommendation: Add shellCommand to the proper bridge interface instead of casting.

  • packages/cli/src/serve/server.ts:1876-1917 - Security: Missing rate limiting. The new POST /session/:id/shell endpoint has no rate limiting, allowing potential DoS or resource exhaustion attacks. Recommendation: Apply the same rate limiting used for other mutation routes or add shell-specific limits.

🟡 High

  • packages/acp-bridge/src/bridge.ts:3175 - Resource management: Timeout handling. The SHELL_COMMAND_TIMEOUT_MS = 120_000 (2 minutes) is reasonable but hardcoded. Long-running commands could tie up resources. Consider making this configurable via BridgeOptions and documenting the timeout behavior.

  • packages/acp-bridge/src/bridge.ts:3230-3234 - Error handling: Silent failure. The history injection via sessionShellHistory extMethod silently catches all errors. While the comment says "best-effort," this could hide integration bugs. Recommendation: At minimum log the failure for debugging.

  • packages/channels/base/src/ChannelBase.ts:283-286 - Inconsistent output formatting. The channel formats output with triple-backtick code blocks, but the web-shell streams raw output via SSE. This inconsistency could confuse users across different clients. Consider standardizing the output format or documenting the difference.

  • packages/sdk-typescript/src/daemon/ui/normalizer.ts:165-178 - UX: Minimal status information. The normalizer converts user_shell_result to a simple status message with only the exit code. Users lose context about which command completed when multiple commands run. Recommendation: Include the command text in the status message.

🟢 Medium

  • packages/acp-bridge/src/bridgeTypes.ts:486-490 - Type consistency: The ShellCommandResult interface is defined here but duplicated as DaemonShellCommandResult in packages/sdk-typescript/src/daemon/types.ts. Consider re-exporting or sharing the type to avoid drift.

  • packages/cli/src/serve/server.ts:1880 - Input validation: The command validation checks command.trim().length === 0 but the bridge re-trims at line 3192. Single trim or document why both exist.

  • packages/web-shell/client/App.tsx:749-755 - Error message specificity: Changed error message from "Failed to send shell command" to "Failed to execute shell command" - good improvement, but consider including the actual command in the error for debugging.

  • packages/acp-bridge/src/bridge.ts:3163 - Missing documentation: The executeShellCommand method has JSDoc in bridgeTypes.ts but the implementation lacks inline documentation for the event publishing flow and truncation logic.

🔵 Low

  • packages/acp-bridge/src/bridge.ts:449-450 - Constant organization: Consider grouping the new shell constants (SHELL_COMMAND_TIMEOUT_MS, MAX_SHELL_OUTPUT_FOR_HISTORY) with a comment explaining their purpose.

  • packages/sdk-typescript/src/daemon/events.ts:99-100 - Event type ordering: The new event types are added at the end of the list. Consider grouping with other user_* events if more are planned.

  • packages/channels/base/src/DaemonChannelBridge.ts:40-44 - Optional method: The shellCommand? method is optional on the interface. Consider whether all session clients should support this or if the optionality is intentional for future extensibility.

  • Test coverage: The PR description includes a detailed test plan, but consider adding unit tests for:

    • executeShellCommand abort handling
    • Output truncation at 256KB boundary
    • Channel bridge error scenarios
    • Normalizer edge cases (missing command, missing exitCode)

✅ Highlights

  • Well-structured HTTP route: The POST /session/:id/shell implementation properly handles abort on client disconnect, validates input, and uses the established sendBridgeError pattern.

  • Proper event flow: The implementation publishes user_shell_command, session_update (for streaming output), and user_shell_result events in the correct sequence with proper metadata.

  • Good type safety: New types (DaemonShellCommandResult, ShellCommandResult) are properly defined and exported through the SDK's public API.

  • Thoughtful history injection: The 256KB truncation with '\n... (truncated)' marker prevents context pollution while preserving most output for LLM reference.

  • Clean web-shell integration: The removal of formatShellCommandPrompt and direct sendShellCommand call simplifies the code path significantly.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a daemon-side “direct shell execution” path for ! (bang) commands, exposed via a new POST /session/:id/shell route and surfaced across the SDK, web-shell UI, and channel adapters, with output streaming via SSE and best-effort history injection for LLM context.

Changes:

  • Add daemon HTTP + bridge support to execute shell commands directly (bypassing the LLM) and stream output via SSE.
  • Extend the TypeScript SDK with shellCommand() APIs and new daemon event types (user_shell_command, user_shell_result) plus UI normalizer support.
  • Update web-shell and channel adapters to route ! ... through the new direct-execution path.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/web-shell/client/hooks/useDaemonSession.ts Adds sendShellCommand() action to call the daemon session’s shell endpoint via SDK.
packages/web-shell/client/App.tsx Routes ! to sendShellCommand() and removes the prior LLM-prompt wrapper helper.
packages/sdk-typescript/src/index.ts Re-exports DaemonShellCommandResult.
packages/sdk-typescript/src/daemon/ui/normalizer.ts Normalizes new user_shell_command / user_shell_result daemon events for UI consumption.
packages/sdk-typescript/src/daemon/types.ts Introduces DaemonShellCommandResult type.
packages/sdk-typescript/src/daemon/index.ts Re-exports DaemonShellCommandResult from daemon module.
packages/sdk-typescript/src/daemon/events.ts Adds new known daemon event type strings.
packages/sdk-typescript/src/daemon/DaemonSessionClient.ts Adds shellCommand() session method delegating to DaemonClient.
packages/sdk-typescript/src/daemon/DaemonClient.ts Implements POST /session/:id/shell client call.
packages/cli/src/serve/server.ts Adds POST /session/:id/shell route with disconnect abort wiring and client-id parsing.
packages/cli/src/acp-integration/acpAgent.ts Adds ACP extMethod to inject shell command + output into Gemini history.
packages/channels/base/src/DaemonChannelBridge.ts Exposes optional shellCommand() capability for channel session clients.
packages/channels/base/src/ChannelBase.ts Detects ! prefix in inbound messages and routes to direct shell execution.
packages/acp-bridge/src/status.ts Adds sessionShellHistory extMethod name constant.
packages/acp-bridge/src/bridgeTypes.ts Extends bridge interface with executeShellCommand() and ShellCommandResult.
packages/acp-bridge/src/bridge.ts Implements executeShellCommand() using ShellExecutionService, SSE fanout, timeout, and history injection.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}
});

app.post('/session/:id/shell', mutate(), async (req, res) => {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks — won't take this one. The shell route deliberately uses non-strict mutate() to match POST /session/:id/prompt, which can also execute arbitrary commands via the LLM's Bash tool. Both routes share the same security posture by design; the prompt route is already the precedent.

Comment on lines +3188 to +3192
const outputChunks: string[] = [];
const abort = new AbortController();
const onSignalAbort = () => abort.abort();
signal?.addEventListener('abort', onSignalAbort, { once: true });

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks — won't take this one. The 120s SHELL_COMMAND_TIMEOUT_MS provides the bound — same pattern as the CLI's ShellExecutionService usage which also accumulates unboundedly until timeout. Capping mid-stream would silently truncate output the user explicitly asked to see.

Comment thread packages/channels/base/src/ChannelBase.ts Outdated
Comment on lines +164 to +172
case 'user_shell_command': {
const command = getString(event.data, 'command');
return command
? [{ ...base, type: 'user.text.delta', text: `! ${command}` }]
: [];
}
case 'user_shell_result': {
const exitCode = numberField(event.data, 'exitCode');
return [

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks — deferring. Unit tests for the normalizer event types will be added in a follow-up PR.

Replace `'shellCommand' in this.bridge` with `typeof === 'function'`
check for safer runtime capability detection.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Bot review triage summary

Comment File Verdict Action
mutate() security gate server.ts:1876 Push back Matches POST /session/:id/prompt precedent
Unbounded output accumulation bridge.ts:3192 Push back 120s timeout provides the bound; same as CLI pattern
'shellCommand' in check ChannelBase.ts:280 Agree Fixed in e3504e8typeof === 'function'
Missing normalizer tests normalizer.ts:172 Defer Follow-up PR

@doudouOUC doudouOUC requested a review from wenshao May 27, 2026 17:38
@doudouOUC doudouOUC self-assigned this May 27, 2026
Comment thread packages/acp-bridge/src/bridge.ts Outdated
}
});

app.post('/session/:id/shell', mutate(), async (req, res) => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] The PR adds ~327 lines of new code across 16 source files with 0 test files. Critical untested paths include:

  • This POST /session/:id/shell route (validation, abort-on-disconnect, error handling)
  • bridge.executeShellCommand (timeout, output truncation, event publishing, abort propagation)
  • Channel ! handler in ChannelBase.ts (fallback when bridge lacks shellCommand, output formatting)
  • sessionShellHistory ext method in acpAgent.ts (history injection)
  • SDK DaemonClient.shellCommand (URL construction, error handling)

Every comparable route/method in the codebase has dedicated test coverage. Consider adding tests for at least the server route, bridge method, and channel handler.

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agreed the test coverage gap is real. Deferring to a follow-up PR — this PR is already 16 files and adding tests for bridge/server/normalizer would double the scope.

Comment thread packages/acp-bridge/src/bridge.ts Outdated
Comment thread packages/acp-bridge/src/bridge.ts Outdated
Comment thread packages/acp-bridge/src/bridge.ts
Comment thread packages/channels/base/src/ChannelBase.ts Outdated
Comment thread packages/channels/base/src/DaemonChannelBridge.ts Outdated
Comment thread packages/sdk-typescript/src/daemon/ui/normalizer.ts Outdated
- Fix AnsiOutput serialization (AnsiToken[][] has no .text property)
- Align MAX_SHELL_OUTPUT_FOR_HISTORY with CLI's 10KB limit
- Add debug logging for failed history injection (was empty catch)
- Emit user_shell_result on ShellExecutionService.execute() failure
- Use dynamic backtick fencing in channel shell output
- Forward AbortSignal through DaemonChannelBridge.shellCommand
- Show "aborted" status instead of "code unknown" in normalizer

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

wenshao review triage — round 1

All fixes in commit 0e83458.

# Comment File Verdict Action
1 AnsiOutput.text doesn't exist bridge.ts:3202 Agree (Critical) Fixed — flatten AnsiToken[][] via line.map(t => t.text)
2 No test files server.ts:1876 Defer Follow-up PR — adding tests would double scope
3 256KB vs CLI's 10KB limit bridge.ts:451 Agree Aligned to 10KB
4 Empty catch {} bridge.ts:3260 Agree Added writeServeDebugLine logging
5 Missing user_shell_result on error bridge.ts:3193 Agree Added catch block with error event
6 Backtick escaping in channel output ChannelBase.ts:285 Agree Dynamic-length fencing
7 AbortSignal not forwarded DaemonChannelBridge.ts:328 Agree Added signal parameter
8 Aborted shows "code unknown" normalizer.ts:176 Agree "Shell command was aborted"

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

R2: All 8 R1 findings addressed in 0e83458. AnsiOutput serialization, MAX_SHELL_OUTPUT alignment, error logging, outer catch error publishing, dynamic backtick fencing, AbortSignal forwarding, and aborted-flag normalization all look correct. Build passes, 985 tests pass, deterministic analysis clean. LGTM! ✅ — qwen3.7-max via Qwen Code /review

@doudouOUC doudouOUC requested a review from chiga0 May 28, 2026 03:36

@chiga0 chiga0 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review Overview (AI Generated)

PR: #4576 feat(daemon): server-side shell command execution for ! (bang) prefix
Type: New Feature
Change size: +356/-10 across 16 files

Findings Summary

  • Critical/Major: 3 items
  • Minor: 5 items
  • Nit: 1 item

Key Observations

Well-structured feature PR that adds a clean server-side shell execution pipeline across all daemon clients (HTTP route → bridge → ShellExecutionService → SSE streaming → LLM history injection). The architecture correctly reuses ShellExecutionService and follows existing patterns. Main concerns are: (1) unbounded output buffer that could OOM on long-running commands, (2) unsafe Record<string, unknown> type cast in the channel adapter, and (3) LLM history injection template vulnerable to backtick escaping. No automated tests included despite test plan in description.


This review was generated by QoderWork AI

line.map((t) => t.text).join(''),
)
.join('\n');
outputChunks.push(chunk);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Major] Unbounded output buffer — potential OOM for long-running commands

outputChunks collects the full command output in memory without any size limit. While historyOutput is correctly capped at 10KB for LLM history injection, the raw output returned in ShellCommandResult has no bound. A command like cat on a multi-GB file or yes running for the full 120s timeout would consume unbounded memory before being returned.

Consider capping the buffer (e.g. keep first/last 500KB) or switching to a ring buffer, similar to how the CLI already handles shell output limits. The 10KB cap for history is good, but the full output buffer should also have a ceiling.


This review was generated by QoderWork AI

await entry.connection.extMethod(
SERVE_CONTROL_EXT_METHODS.sessionShellHistory,
{ sessionId, command, output: historyOutput, exitCode },
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Minor] History injection template vulnerable to backtick escaping

outputText is placed inside a triple-backtick code fence, but if the command output itself contains ```, it will break the code fence and corrupt the LLM's chat history structure. This could cause confusing LLM behavior in subsequent turns.

Apply the same dynamic backtick fencing that ChannelBase.ts already implements:

const longestRun = Math.max(0, ...Array.from(outputText.matchAll(/`+/g), m => m[0].length));
const fence = '`'.repeat(Math.max(3, longestRun + 1));
geminiClient.addHistory({
  role: 'user',
  parts: [{ text: `I ran the following shell command:\n${fence}sh\n${command}\n${fence}\n\nThis produced the following result:\n${fence}\n${outputText}\n${fence}` }],
});

This review was generated by QoderWork AI

},
abort.signal,
false,
{ terminalWidth: 120, terminalHeight: 40 },

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Minor] Timeout starts after await ShellExecutionService.execute() resolves

The timeout setTimeout is set after the await ShellExecutionService.execute() returns the handle. If the execute call itself takes significant time (e.g. process spawning overhead), the actual command runtime exceeds the intended 120s limit by that overhead. While usually negligible, consider starting the timer before the await for more accurate timeout semantics:

const timeoutId = setTimeout(() => abort.abort(), SHELL_COMMAND_TIMEOUT_MS);
timeoutId.unref();
try {
  const handle = await ShellExecutionService.execute(...);
  const result = await handle.result;
} finally {
  clearTimeout(timeoutId);
}

This review was generated by QoderWork AI

// 3.5. Bang (!) shell command — direct execution, no LLM
if (envelope.text.startsWith('!')) {
const cmd = envelope.text.slice(1).trim();
const bridgeShellCommand = (this.bridge as unknown as Record<string, unknown>)['shellCommand'];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Major] Unsafe Record<string, unknown> type cast — use typed interface or type guard

This cast to Record<string, unknown> followed by bracket access and a typeof check is a type-safety anti-pattern. It bypasses TypeScript's type checking entirely and is fragile to refactoring — renaming shellCommand on the bridge won't produce a compile error here.

Consider either:

  1. Adding a proper capability check to the bridge interface:
interface ShellCapableBridge {
  shellCommand(sessionId: string, command: string): Promise<...>;
}
function hasShellCommand(bridge: unknown): bridge is ShellCapableBridge {
  return typeof (bridge as ShellCapableBridge)?.shellCommand === 'function';
}
  1. Or declaring shellCommand as an optional method on the base bridge type and using this.bridge.shellCommand?.(...).

Also note: when the bridge doesn't support shellCommand, the ! command silently falls through to LLM processing (see related Minor finding on ChannelBase.ts line 278).


This review was generated by QoderWork AI

const cmd = envelope.text.slice(1).trim();
const bridgeShellCommand = (this.bridge as unknown as Record<string, unknown>)['shellCommand'];
if (cmd && typeof bridgeShellCommand === 'function') {
try {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Minor] Silent fall-through when ! command is not supported

When ! is detected but either cmd is empty or the bridge doesn't support shellCommand, the code falls through to normal LLM processing. This means:

  • ! alone (no command) gets sent to the LLM as a bare exclamation mark
  • !ls on a bridge without shellCommand capability gets sent as raw text to the LLM

The user typed ! expecting shell execution. They should get feedback, not a confusing LLM response. Add an else branch:

if (cmd && typeof bridgeShellCommand === 'function') {
  // ... existing logic
} else if (envelope.text.startsWith('!')) {
  await this.sendMessage(
    envelope.chatId,
    cmd ? 'Shell command execution is not supported in this mode' : 'Usage: ! <command>',
  );
  return;
}

This review was generated by QoderWork AI

@chiga0 chiga0 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Continued review findings (Part 2/2)

'\n... (truncated)'
: output;

try {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Minor] role: 'user' for shell history injection may mislead the LLM

The shell command result is injected as a user message in the LLM's chat history. This makes it appear as if the user typed the command and output themselves, which could confuse the model about the user's intent in subsequent turns (e.g. the model might think the user is showing it a command rather than reporting an execution result).

If the Gemini API supports system or model roles for addHistory, consider using those. Alternatively, prefix with a clearer attribution like [System: Shell command executed by user via daemon] to disambiguate. Check what the CLI's addShellCommandToGeminiHistory uses for consistency.


This review was generated by QoderWork AI

@@ -749,9 +749,9 @@ export function App({
const cmd = text.slice(1).trim();
if (!cmd) return false;
actions

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Nit] command.trim() removes intentional leading whitespace

The server route also applies command.trim() (server.ts line 1888), which strips leading/trailing whitespace from the user's input. For most commands this is fine, but ! echo hello becomes echo hello, and ! echo ' spaces ' would work correctly since only the outer trim applies.

This is consistent with how the CLI handles shell commands, so it may be intentional. Just noting it as a potential edge case if users expect to paste commands with intentional leading whitespace.


This review was generated by QoderWork AI

@@ -1084,11 +1084,4 @@ export function App({
);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Nit] Extra blank lines after removing formatShellCommandPrompt

After removing the formatShellCommandPrompt function, two blank lines remain (lines 1085-1086). Minor cleanup — one trailing blank line before the file end is the common convention.


This review was generated by QoderWork AI

role: 'user',
parts: [
{
text: `I ran the following shell command:\n\`\`\`sh\n${command}\n\`\`\`\n\nThis produced the following result:\n\`\`\`\n${outputText}\n\`\`\``,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Minor] History injection output not fenced against backtick content

Same concern as bridge.ts — the template literal wraps outputText in triple backticks. If outputText itself contains ``` (common in markdown-aware tools or when cat-ing README files), the code fence breaks and the LLM history gets corrupted.

Use dynamic backtick fencing here too (calculate the longest backtick run in outputText and use longestRun + 1 backticks for the fence). See ChannelBase.ts lines 287-293 for the existing implementation.


This review was generated by QoderWork AI

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

R3 Review — COMMENT

9 parallel review agents (correctness, security, code quality, performance, test coverage, attacker, 3AM oncall, maintainer) + batch verification.

High-confidence findings: 0

All R1/R2 findings verified fixed. No new Critical or Suggestion-level issues found.

Needs Human Review (low-confidence, Nice to have)

# File Issue
1 bridge.ts:3263 History injection failure logged via writeServeDebugLine() — invisible in production without QWEN_SERVE_DEBUG. Consider daemonLog.warn().
2 ChannelBase.ts:283 Bang handler doesn't forward AbortSignal to shellCommand(). 120s timeout prevents runaway, but channel disconnect can't cancel mid-command.
3 bridge.ts:3204 shell_output SSE chunks lack command correlation ID — concurrent commands on same session produce unattributable output (rare in practice).
4 server.ts:1876 120s HTTP POST with no timeout indication header — proxies with shorter timeouts may 502. SSE stream compensates with real-time output.

Rejected (false positives from initial scan)

  • AbortSignal race between check and addEventListener — impossible in single-threaded JS (no yield point)
  • AnsiOutput inline type fragility — TS structural typing catches relevant changes
  • Cross-session shell execution without ownership — same auth pattern as /prompt and /cancel
  • cwd disclosure in SSE — already exposed via GET /capabilities

Open @chiga0 comments (9 items)

The existing chiga0 inline comments cover the most actionable remaining concerns (backtick fence in acpAgent.ts history injection, timeout ordering, Record cast pattern). These are awaiting author response.

— claude-opus-4 via Qwen Code /review

@chiga0 chiga0 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-review Summary

Re-evaluated all 9 previous review findings against the current codebase (latest commit 0e83458).

Finding Disposition

# Severity Finding Verdict
1 Major Unbounded outputChunks buffer False Positive — 120s SHELL_COMMAND_TIMEOUT_MS caps runtime; history output capped at 10KB. Matches CLI's ShellExecutionService pattern. Author correctly noted this.
2 Minor Backtick escaping in LLM history injection (bridge.ts) Acceptable — CLI uses same pattern; edge case (output containing ```) is rare in practice. Worth a follow-up but non-blocking.
3 Minor Timeout starts after await execute() Acceptable — Spawn overhead is negligible (<10ms typically). Non-blocking.
4 Major Unsafe Record<string, unknown> type cast Improved — Commit e3504e8 already switched to typeof === 'function' guard. Full interface approach is nice-to-have refactor, not a blocker.
5 Minor Silent fall-through for unsupported ! commands Acceptable — Empty ! passing to LLM is debatable design; bridge without shellCommand capability is an edge case. Non-blocking.
6 Minor role: 'user' for shell history injection Acceptable — Matches CLI's addShellCommandToGeminiHistory format exactly. Consistent with existing pattern.
7 Nit command.trim() strips leading whitespace Acceptable — Intentional, consistent with CLI behavior.
8 Nit Extra blank lines in App.tsx Nit — Minor style issue, non-blocking.
9 Minor Backtick fencing in acpAgent.ts history injection Acceptable — Same as #2, non-blocking follow-up candidate.

Conclusion

No blocking issues remain. The PR is well-structured, follows existing codebase patterns (CLI shell execution, ACP extMethod), and all wenshao R1/R2 findings have been addressed. The wenshao R3 review also confirmed 0 high-confidence findings.

Approving.

This review was generated by QoderWork AI

@doudouOUC doudouOUC merged commit 50a0c26 into daemon_mode_b_main May 28, 2026
30 checks passed
@doudouOUC doudouOUC deleted the feat/daemon-shell-command branch May 28, 2026 06:07
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.

4 participants