* feat(core): tag UserPromptSubmit hook context and record display provenance
UserPromptSubmit additionalContext was appended to the request as a bare
text part and persisted verbatim, so hook-injected text was
indistinguishable from user-authored text in the transcript, polluted
resumed sessions, telemetry, and auto-memory recall queries.
- Wrap injected context in a reserved
<qwen:user-prompt-submit-context> tag (hook output already escapes
angle brackets, so the tag cannot be forged from inside).
- Record the pre-injection user prompt as systemPayload.displayText plus
the injected string as hookContext on the user record; the model-bound
message stays verbatim for faithful resume replay.
- Use the pre-injection prompt text for telemetry prompt attributes and
managed auto-memory recall.
- Resume projection prefers displayText, strips a trailing whole-part
tagged block when no payload exists, and leaves legacy bare-injected
records unchanged.
- Apply the same tag wrapping on the ACP session injection path, which
already records the pre-injection prompt.
Closes#7940
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs: note UPS promptText TDZ ordering and sole-part resume guard
Document the conflict-resolution constraint that promptText must be
declared before the injection assignment, and the sole-part read-path
guard that keeps a user-authored whole-tag message intact.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(cli): cover at_command resume with tagged UPS context
Confirm the at_command branch still prefers payload.userText when a
paired user record carries a trailing tagged hook-context part, and
falls back to the tag-stripping projection only when userText is absent.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(core): address PR 7956 review findings and Goal recording spy
Omit the optional UserPromptRecordPayload third arg when no hook
injected, so Goal admission spies expecting two args stay exact and
CI client-goal.test.ts passes.
Project plain UserPromptSubmit-augmented records through
transcript-replay with the same displayText / trailing-tag strip
fallback as the TUI, covering ACP/export surfaces. Strengthen the
displayText preference fixture so it disagrees with the tag-strip
path, and use the named UserPromptRecordPayload type in resume.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(acp-bridge): import UPS tag helper via Node-free package export
transcript-replay is inlined into the browser daemon/transcript SDK
bundle. Importing isUserPromptSubmitContextPartText from the core
package barrel pulled the whole Node-bound core graph into that
bundle and failed CI (esbuild Could not resolve "node:*") across
Test, web-shell E2E, and Real daemon E2E.
Export the pure helper as @qwen-code/qwen-code-core/userPromptSubmitContext
and import that path instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(test): alias userPromptSubmitContext for Vitest source resolution
CLI and acp-bridge Vitest configs already map goalWire/transcriptRecords
to TypeScript sources; without the same alias the new package export
fails import analysis and breaks dozens of CLI suites.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(acp-bridge): keep images when projecting displayText user records
Preferring UserPromptSubmit displayText previously returned early and
skipped projectMessageParts, dropping multimodal inlineData. Rebuild
parts so displayText replaces text while images keep their order.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(core): drop unused hookContext and cover image-only displayText
UserPromptRecordPayload.hookContext had no read sites; keep displayText
only and recover injected text from the tagged message part. Also cover
the image-only !replaced append path and simplify the recording guard.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test: cover remaining UserPromptSubmit provenance Suggestions
Share stripTrailingUserPromptSubmitContextPart between TUI resume and
ACP replay, assert ACP Session tags additionalContext, and lock
telemetry to the pre-injection prompt text.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* feat(hooks): add security.allowPrivateNetworkHooks to bypass SSRF range checks for trusted scopes
HTTP hooks hard-block all private/link-local address ranges via ssrfGuard,
which makes them unusable in platform-managed environments where the hook
receiver is a first-party, VPC-internal endpoint (e.g. an internal API
gateway resolving to 172.16.0.0/12).
Add an opt-in setting, security.allowPrivateNetworkHooks (default false),
that skips the SSRF IP-range checks in urlValidator.isBlocked (literal IPs)
and validateResolvedHost (literal + post-DNS-resolution paths).
Security properties:
- Honored only from User/System/SystemDefaults scopes; the value is
stripped from Workspace settings during the merge (with a startup
warning), so a cloned repository can never self-grant the bypass.
- BLOCKED_HOSTS (169.254.169.254, metadata.google.internal, ...) remains
blocked even when the flag is on.
- Default false keeps every code path byte-for-byte compatible with
current behavior; bare/safe mode forces it off.
* fix(hooks): enforce metadata endpoint blocklist regardless of allowPrivateNetworkHooks
Address review findings on #7968: with the flag on, cloud metadata
endpoints were reachable through gaps in the relaxed checks.
- ssrfGuard: add METADATA_IPS (169.254.169.254, 100.100.100.200) and
isMetadataAddress(), which normalizes IPv4-mapped IPv6 forms
(::ffff:a9fe:a9fe, ::ffff:6464:64c8, ...) via the existing
extractMappedIPv4/expandIPv6Groups helpers.
- urlValidator.isBlocked: BLOCKED_HOSTS matching and the literal-IP
isMetadataAddress check now run unconditionally; only the general
range check (isBlockedAddress) is relaxed by the flag.
- httpHookRunner.validateResolvedHost: no longer returns early with the
flag on — DNS resolution still runs and resolved addresses are checked
against isMetadataAddress, so a hostname resolving to a metadata
endpoint is blocked. DNS failures still defer to fetch, as before.
- settings warning text now lists User/System/SystemDefaults, matching
the schema and docs.
- docs: precise wording — the flag relaxes only range checks; metadata
endpoints stay blocked in all serialized forms and after DNS resolution.
The flag now opens RFC1918/CGNAT/link-local ranges only; cloud metadata
endpoints (169.254.169.254, 100.100.100.200 in any form, plus the
BLOCKED_HOSTS hostnames) are unreachable in every configuration.
---------
Co-authored-by: 欢伯 <ri.xur@alibaba-inc.com>
* fix(core): fire StopFailure hook on loop detection early returns (#7588)
When loop detection (always-on safety or heuristic) terminates a turn
early via `return turn`, the Stop hook code after the streaming loop
was never reached. StopFailure hooks were only fired from the CLI
layer for API errors, not from client.ts for loop detection.
Added `loop_detected` to StopFailureErrorType and fire the
StopFailure hook via MessageBus before each loop detection early
return, so cleanup/notification hooks run regardless of how the
turn ends.
All 284 client tests and 682 hook tests pass.
* fix(core): use direct hookSystem call for loop-detection StopFailure (#7588)
The MessageBus bridge has no StopFailure case, so the hook never
executed. Switch to config.getHookSystem()?.fireStopFailureEvent()
(matching the CLI's API-error path), make it fire-and-forget per the
StopFailure contract, drop the stale last_assistant_message that
carried the previous turn's text, deduplicate via a private helper,
update docs with loop_detected, regenerate the settings schema, and
add regression tests for both loop-detection paths.
* test(core): add negative-path test for StopFailure hook disable guard (#7592)
* test(core): add negative-path tests for StopFailure hook guards (#7592)
---------
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* feat(hooks): add MessageDisplay hook for mid-turn streaming
Fires repeatedly as the assistant reply streams, before Stop (which only fires once at the end of the turn). Fire-and-forget, cumulative text payload, debounced (~200ms) except for the unconditional final firing. Fires from the single streaming loop in client.ts shared by the terminal UI and ACP paths.
Fixes#6488
* fix(hooks): address MessageDisplay review feedback
- Chain fire-and-forget MessageDisplay requests per message_id instead of
firing them fully unbounded, so a slow hook command can't pile up
concurrent processes.
- Gate the final flush on non-empty displayed_text and !signal.aborted,
matching the adjacent Stop hook's guard.
- Document why the final flush intentionally re-sends the last debounced
text (is_final itself is new information).
- Simplify the debounce constant's JSDoc to drop the competitor comparison.
- Add tests for the mid-stream debounced flush and the rejected-request
warn path.
* test(hooks): drain microtasks before asserting on chained MessageDisplay calls
fireMessageDisplayHook now chains per-message_id through a promise (see
previous commit), so the final flush's actual messageBus.request() call
lands a few microtask ticks after the generator itself finishes — the
mid-stream-flush test needs to let that chain settle before asserting.
* fix(hooks): flush MessageDisplay is_final on every for-await exit path
The three early `return turn` paths inside the streaming loop (always-on
loop-detection safety, heuristic loop detection, and the stream Error event)
exited before the final MessageDisplay flush, which only sat after the loop
ended normally. Hook scripts relying on is_final: true to know when to flush
never received it when a turn ended via loop detection or an API error.
Extracts the flush into a shared closure and calls it from all four exits
(the three early returns plus the normal fall-through), instead of only the
one at the bottom of the loop. Adds regression tests for all three previously
missed exits, plus the two guard-coverage tests requested in review (abort
suppresses the flush, a tool-call-only turn with no Content events does not
fire a vacuous empty-text event).
Addresses the outstanding critical review comment and the follow-up test
coverage suggestion on PR #6489.
* fix(hooks): fire MessageDisplay on the ACP surface, coalesce delivery, drain is_final before turn end
Addresses the three findings from the local verification report on #6489:
- ACP/qwen serve (Finding 1): the delivery logic now lives in a shared
MessageDisplayDispatcher (packages/core), and Session.ts wires it into
all four raw-stream loops (main prompt, Stop-hook continuation, cron
tick, background notification) — these surfaces consume GeminiChat's
stream directly and never enter GeminiClient.sendMessageStream, so
they need their own fire sites. The daemon no longer advertises an
event it never emits.
- Slow-hook backlog (Finding 2): the per-message promise chain is
replaced by coalescing delivery — at most one in-flight request plus
one pending payload per message; newer flushes overwrite the pending
slot, which is lossless because displayed_text is cumulative, and
is_final is sticky. A slow hook now sees fewer, newer payloads instead
of an ever-growing queue of stale ones.
- Headless is_final drop (Finding 3): finish() resolves only once every
enqueued payload has actually been delivered, and every exit out of
the streaming loops awaits it (early returns, normal fall-through,
and the enclosing finally for uncaught exceptions), so a short-lived
-p process can no longer exit with the final payload still queued.
As a consequence, is_final delivery now strictly precedes the Stop
hook rather than racing it.
Also: the failure log line carries the message_id, finish() is
idempotent, the review-requested tests are added (mid-stream and final
firings share one message_id; isFinal as the sole flush reason), and
hooks.md gains a delivery-semantics contract covering coalescing, the
drain guarantee, no is_final on cancellation, provisional
displayed_text, and multiple messages per tool-using turn.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(hooks): bound MessageDisplay drain wait, fix test gaps flagged in review
finish() now gives up waiting on drain after 5s (MESSAGE_DISPLAY_DRAIN_TIMEOUT_MS) instead of blocking turn teardown for up to the full 60s hook timeout, per the re-verification's S1 finding. Delivery keeps running in the background past the timeout; only the caller's wait is bounded.
Also: add the config.ts bridge test for MessageDisplay field extraction (S5), and add the missing MessageDisplay/InstructionsLoaded entries to acpAgent.test.ts's HookEventName mock (S6).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(hooks): dispatch MessageDisplay is_final alongside stale deliveries, share one drain budget
Round-3 review findings on #6489:
- finish() no longer queues the is_final payload behind an in-flight
mid-stream delivery: the pending slot's supersession argument applies to
the in-flight slot too, so the final payload is dispatched immediately,
alongside the stale delivery if one is still running. is_final is handed
to the hook the moment the message ends — before Stop — on every surface,
and can no longer be dropped by a short-lived process exiting with it
still queued (Finding 1).
- The bounded drain wait is memoized: every finish() call (explicit,
finally, or concurrent) shares one promise and one timer, so the teardown
ceiling is MESSAGE_DISPLAY_DRAIN_TIMEOUT_MS itself, not a multiple of it
(Finding 2).
- hooks.md delivery semantics rewritten to match the shipped behavior,
including the headless orphaned-hook caveat and the unspecified completion
order between an overlapped stale execution and the final one (Finding 3).
- The dispatcher mirrors its warnings to console.warn itself (stderr on
headless/ACP, ink patchConsole in the TUI) in addition to the injected
debug-file sink, so hitting the drain timeout is visible by default
(Finding 4).
- A superseded mid-stream delivery that fails after the final was
dispatched no longer warns; failures during streaming still do.
- New tests: finish() twice while delivery is in flight (the exact
client.ts sequence), concurrent finish() calls sharing one budget,
is_final overtaking a held mid-stream delivery, and drain resolving on
the final delivery alone.
* refactor(core): consolidate MessageDisplay finish() calls, dedupe test spy setup
client.ts: wrap the turn.run() streaming loop in try/finally so messageDisplay.finish() fires once instead of at each of the three early-return sites plus the post-loop path -- matching the pattern the four raw-stream loops in Session.ts already use for the same dispatcher.
message-display-dispatcher.test.ts: centralize the console.warn spy setup/teardown in beforeEach/afterEach instead of five repeated per-test try/finally blocks.
No behavior change: full client.test.ts (246/246) and the message-display-buffer/dispatcher suites (24/24) pass unchanged.
* docs(hooks): clarify MessageDisplay cancellation timing (round-4 nit)
* test(hooks): cover the 3 untested MessageDisplay dispatch sites, fix cancellation doc wording
Adds MessageDisplay is_final coverage for the Stop-hook continuation loop, the in-session cron fire, and the background-notification loop, each with a normal-completion and an abort case. Adds three MessageDisplayDispatcher edge-case tests: a delivery settling just before the drain timeout, an abort arriving after a drain wait has already started, and addChunk called after abort but before finish(). Rewords the cancellation-timing doc bullet to state the actual criterion (abort signal state when finish() runs) rather than an approximation of it.
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
- Add toolCallId field to PreToolUse/PostToolUse/PostToolUseFailure hook inputs
- Thread toolCallId from LLM API response (toolCall.id) through to hook subprocess stdin
- Fix message bus handler in config.ts to forward toolCallId (was silently dropped)
- toolCallId is optional — only included when the LLM provider supplies an ID
Signed-off-by: Shile Zhang <shile.zhang@linux.alibaba.com>
* fix(cli): remember "Start new chat session" until summary changes
Persist a project-scoped Welcome Back restart choice keyed to the
current PROJECT_SUMMARY fingerprint.
This suppresses the Welcome Back dialog after choosing "Start new chat
session", while still showing it again after the project summary is
updated.
* fix conflict
* add http/async/function type
* fix url error
* resolve comment
* align cc non blocking error
* fix hookRunner for async
* fix(hooks): update hook type validation to support http and function types
- Change validated hook types from ['command', 'plugin'] to ['command', 'http', 'function']
- Add validation for HTTP hooks requiring url field
- Add validation for function hooks requiring callback field
- Add comprehensive test coverage for all hook type validations
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(hooks): align SSRF protection with Claude Code behavior
- Allow 127.0.0.0/8 (loopback) for local dev hooks
- Allow localhost hostname for local dev hooks
- Allow ::1 (IPv6 loopback) for local dev hooks
- Add 100.64.0.0/10 (CGNAT) to blocked ranges (RFC 6598)
- Update tests to match Claude Code's ssrfGuard.ts behavior
This fixes HTTP hooks failing to connect to local dev servers.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* refactor(hooks): align HTTP hook security with Claude Code behavior
- Add CRLF/NUL sanitization for env var interpolation (header injection)
- Implement combined abort signal (external signal + timeout)
- Upgrade SSRF protection to DNS-level with ssrfGuard
- Allow loopback (127.0.0.0/8, ::1) for local dev hooks
- Block CGNAT (100.64.0.0/10) and IPv6 private ranges
- Increase default HTTP hook timeout to 10 minutes
- Fix VS Code hooks schema to support http type
- Add url, headers, allowedEnvVars, async, once, statusMessage, shell fields
- Note: "function" type is SDK-only (callback cannot be serialized to JSON)
* feat(hooks): enhance Function Hook with messages, skillRoot, shell, and matcher support
- Add MessagesProvider for automatic conversation history passing to function hooks
- Add FunctionHookContext with messages, toolUseID, and signal
- Add skillRoot support for skill-scoped session hooks
- Add shell parameter support for command hooks (bash/powershell)
- Add regex matcher support for hook pattern matching
- Add statusMessage to CommandHookConfig
- Change default function hook timeout from 60s to 5s
- Add comprehensive unit tests for all new features
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* add session hook for skill
* fix function hook parsing
* refactor ui for http hook/async hook/function hook
* update doc and add integration test
* change telemetryn type and refactor SSRF
* fix project level bug
---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>