Skip to content

feat(runtime): P6 command hook stdin/stdout JSON protocol#694

Open
Cai-Tang-www wants to merge 6 commits into
1024XEngineer:mainfrom
Cai-Tang-www:feat/command-hook-protocol-683
Open

feat(runtime): P6 command hook stdin/stdout JSON protocol#694
Cai-Tang-www wants to merge 6 commits into
1024XEngineer:mainfrom
Cai-Tang-www:feat/command-hook-protocol-683

Conversation

@Cai-Tang-www
Copy link
Copy Markdown
Collaborator

Summary

实现 kind=command hook 的完整 stdin/stdout JSON 协议(RFC #679 P6),使任意可执行脚本可作为 hook 接入 user/repo scope。

核心变更

新增 internal/runtime/hooks/command_handler.go

  • 协议类型:CommandHookPayload(stdin)、CommandHookResponse(stdout)、CommandHookSpec(执行参数)
  • BuildCommandPayload:构造 stdin JSON,包含 payload_versionhook_idpointmetadata
  • ParseCommandResponse:解析 stdout JSON,提取 status/message/update_input/annotations
  • RunCommandHook:执行外部命令,处理 stdin 管道、stdout 解析、环境隔离、超时取消

执行模式:

模式 params.command 格式 行为
argv(默认) ["python3", "hook.py"] 直接 exec,不经 shell
shell "python3 hook.py" + params.shell: true 通过 sh -c / powershell -Command 执行

单字符串 params.command 不设置 params.shell: true 触发配置校验错误,防止 shell 注入。

stdin 协议(单行 JSON):

{
  "payload_version": "1",
  "hook_id": "my-hook",
  "point": "before_tool_call",
  "metadata": {"tool_name": "bash", "workdir": "/workspace"}
}

stdout 协议(单行 JSON):

{
  "status": "pass",
  "message": "optional message",
  "update_input": {"text": "rewritten prompt"},
  "annotations": ["note1", "note2"]
}

stdout 退化模式: 非 JSON stdout 退化为 exit code 语义(0=pass, 1/2=block, other=failed),兼容简单脚本。

环境隔离: 命令进程仅注入 NEOCODE_HOOK_HOOK_IDNEOCODE_HOOK_POINTNEOCODE_HOOK_PAYLOAD_VERSION,不继承宿主环境。

update_input 接入:user_prompt_submit 点位(CanUpdateInput=true)允许,格式 {"text": "..."} 替换用户输入文本。

annotations 接入: stdout 中的 annotations 数组进入 runtime annotation buffer,与 message 一同被记录。

修改文件清单

文件 变更
internal/runtime/hooks/command_handler.go 新增:协议类型、payload 构造、response 解析、命令执行
internal/runtime/hooks/command_handler_test.go 新增:18 个测试用例,全平台覆盖
internal/runtime/hooks/result.go HookResultMetadata 新增 AnnotationsUpdateInput 字段
internal/runtime/user_hooks.go 重构 buildUserCommandHookHandler,新增 parseCommandHookParams,移除 buildCommandHookProcess
internal/runtime/repo_hooks.go 新增 validateRepoCommandParams,支持数组格式 params.command
internal/config/runtime_hooks.go 新增 validateRuntimeCommandItem,argv/shell 模式校验
internal/config/runtime_hooks_test.go 新增 3 个测试:shell 模式、字符串无 shell 拒绝、argv 模式
internal/runtime/hooks_integration.go recordUserHookAnnotations 扩展收集 Metadata.Annotations;新增 applyCommandHookUpdateInput
internal/runtime/run.go user_prompt_submit 点位接入 update_input 应用
internal/runtime/repo_hooks_test.go 更新 command hook 测试用例为数组格式
docs/runtime-hooks-design.md 新增 P6 章节:协议规范、执行模式、环境变量、示例

向后兼容

  • 现有 params.command 为字符串的配置需添加 params.shell: true,否则校验失败(符合 issue 要求:argv 为新默认模式)
  • exit code 语义(0=pass, 1/2=block, other=failed)在 stdout 非 JSON 时保留
  • CombinedOutput()Output():stderr 不再混入 message

测试覆盖

  • 正常 exit(exit 0 + JSON stdout)
  • 超时(context timeout → failed)
  • 非零 exit(exit 1 → block, exit 3 → failed)
  • 非法 JSON stdout(退化为 exit code 模式)
  • 环境隔离(仅 NEOCODE_HOOK_* 变量,无宿主环境泄漏)
  • argv 模式直接 exec
  • shell 模式 sh -c / powershell -Command
  • update_input 解析与应用
  • annotations 解析与收集
  • payload_version 字段稳定("1"

Closes #683

Implement the full command hook protocol for user/repo scopes as
specified in issue 1024XEngineer#683. External commands now communicate via structured
JSON on stdin/stdout instead of raw exit codes.

Key changes:
- New command_handler.go: protocol types (CommandHookPayload/Response),
  BuildCommandPayload, ParseCommandResponse, RunCommandHook with
  argv/shell execution modes, env isolation (NEOCODE_HOOK_* only)
- stdin: single-line JSON with payload_version, hook_id, point, metadata
- stdout: single-line JSON with status, message, update_input, annotations
- Graceful fallback: non-JSON stdout falls back to exit code semantics
  (0=pass, 1/2=block, other=failed)
- argv mode (default): params.command as string array, direct exec
- shell mode: params.command as string + params.shell=true
- update_input wired for user_prompt_submit point
- annotations collected into runtime annotation buffer
- Context timeout detection prioritized over exit code mapping
- 18 new unit tests, all cross-platform (Windows + Unix)

Closes 1024XEngineer#683

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

…ertion

Windows injects PATH at the system level even when cmd.Env is set.
Split into two focused tests: one verifies NEOCODE_HOOK_* vars are
injected via exec, the other verifies buildCommandEnv returns the
correct variable set.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 23, 2026

Codecov Report

❌ Patch coverage is 80.80000% with 48 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/runtime/hooks/command_handler.go 87.17% 16 Missing and 9 partials ⚠️
internal/runtime/user_hooks.go 0.00% 19 Missing ⚠️
internal/runtime/hooks_integration.go 87.09% 3 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@fennoai fennoai Bot left a comment

Choose a reason for hiding this comment

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

Review summary: found two command-hook protocol regressions to address before this lands. I also ran go test ./internal/runtime/hooks ./internal/runtime ./internal/config; internal/runtime/hooks currently fails on the env-isolation test.

Comment thread internal/runtime/user_hooks.go Outdated
Comment thread internal/runtime/hooks/command_handler_test.go
Cai-Tang-www and others added 4 commits May 23, 2026 17:48
input.Metadata["point"] was not populated by runHookPoint (it only
injects run_id/session_id/runtime_run_token/phase/turn), so command
hooks received an empty point in the stdin payload and
NEOCODE_HOOK_POINT. Fix by passing item.Point from config directly
into the handler closure at build time.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Self-review fixes for P6 command hook protocol:

1. Security: non-zero exit code now takes precedence over JSON status.
   A malicious script claiming {"status":"pass"} while exiting non-zero
   will be treated as block/failed based on exit code, not pass.
   buildResultFromExitCode still extracts message/annotations from JSON
   stdout when available, but status authority remains with exit code.

2. Added top-level run_id and session_id fields to CommandHookPayload,
   populated from HookContext. External scripts can now read these
   directly without digging into metadata.

3. Added tests: exit code precedence over JSON, exit code 3 with JSON
   message extraction, payload run_id/session_id verification, stdin
   payload round-trip with run_id/session_id.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
P0 security/correctness:
- Capture stderr separately from stdout; append to failed results for debug
- Add SystemRoot/SystemDrive/USERPROFILE to Windows hook env (TLS/NTLM)
- Limit stdout to 1 MiB via pipe+LimitReader to prevent OOM

P1 design/maintainability:
- Extract ValidateCommandParams/ParseCommandParams as shared exports in
  hooks package, deduplicating 3 identical validation sites
  (config/runtime_hooks.go, repo_hooks.go, user_hooks.go)
- Guard buildExecCmd against Shell+multi-args misuse (panic)
- Set result.Error for exit code 1/2 block results (consistency)

P2 test coverage:
- Add 7-subtest TestApplyCommandHookUpdateInput (caught real bug: was
  replacing ALL text parts instead of first-only)
- Add TestRunCommandHookEnvIsolationNoLeak (Unix PATH/HOME/USER check)
- Add TestRunCommandHookShellModeWindows (powershell -Command)
- Add TestValidateCommandParams (9 cases covering exported API)

P3 documentation:
- Document stderr handling strategy, run_id/session_id field precedence,
  update_input+block interaction, exit-code-over-JSON security rule,
  stdout size limit, Windows env vars

Bug fix discovered during review:
- applyCommandHookUpdateInput used update.Text="" to skip subsequent
  text parts, but still appended NewTextPart(""). Fixed with replaced flag.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add targeted tests for previously uncovered branches:
- ParseCommandParams: all 9 branches ([]string, []any, string+shell,
  nil, empty, unsupported type, empty element, shell=false)
- RunCommandHook: stdout-too-large, nonexistent binary, exit 0 empty,
  exit 2 block, exit 3 with stderr, block+message, failed default msg,
  failed custom msg, pass+annotations+update_input, stdin+metadata
- buildCommandEnv: verify Windows SystemRoot/SystemDrive/USERPROFILE
- buildResultFromResponse: failed status with default/custom message
- buildResultFromExitCode: exit 2 sets Error, exit 3 with stderr

Coverage: 90.8% -> 96.0% (command_handler.go functions all >= 75%)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.

[Hooks RFC #679] P6 核心: command kind 与 stdin/stdout JSON 协议

1 participant