mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
main
53
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7cb46d9fed |
feat(hooks): add FileChanged notifications for local sessions (#13969)
* feat(hooks): add FileChanged notifications for local sessions * fix(ci): preserve shell gate coverage and trim transcript data * fix(ci): flush debug prompts and isolate git fixtures * fix(hooks): respect ACP watcher ownership and skill registration * test: stabilize hosted recovery and failed spawn fixtures * fix(ci): budget the complete hosted integration fork * fix(runtime-broker): accept MariaDB CSI guard metadata * test: align schema baseline and hook dispatch waits * fix(ci): allow hosted shared fork to finish all classes * fix(hooks): preserve file watching across errors and cwd changes * fix(ci): isolate reference archive extraction per run * test(ci): keep reference archive checks dependency-free * fix(hooks): constrain recursive watches and repair CI * test(web-shell): synchronize verified connection return * fix(ci): correct runtime broker callback indentation * test(web-shell): match the managed workspace registration label Keep the remote workspace registration smoke checks in sync with the service label added by unified workspace navigation. Preserve exact text matching and all registration and replay assertions. * fix(ci): reduce standalone runtime fixture overhead * fix(hooks): ignore implicit atomic-write temporary files * test(managed-agent): recover initial node port conflicts * fix(hooks): observe parents before discovery anchors disappear * test(managed-agent): await stream cleanup before recovery teardown * test(managed-agent): align hosted shell fixture capabilities * test(managed-agent): let OS choose initial harness port * fix(ci): restore runner-local Java cache permissions * test(runtime-broker): use native execution profile paths * test(managed-agent): bind valid workspace upload probes * test(managed-agent): reuse the upload probe digest fixture * test(managed-agent): bind hook fault listeners to available ports * test(managed-agent): align the native profile path fixture * test(managed-agent): Wait for projection refusal retries * ci: Prepare daemon Java cache before Node preflight * test(managed-agent): check channel model calls after delivery * fix(hooks): observe explicitly registered symlink targets * test(managed-agent): separate consumption from relay snapshots * test(managed-agent): target tamper injection by resource key * test(managed-agent): exhaust attestation EOF retry in fault gates * fix(ci): bound RocketMQ release downloads * test(tui): await the restored assistant before capturing history * fix(ci): use a checksum-pinned RocketMQ mirror * fix(ci): provision missing browser test fonts * fix(ci): quote the font probe environment * fix(ci): prepare browser fonts without elevation * test(core): compare complete prune reports without ordering * fix(ci): repair only runner-local Java tool caches * test(managed-agent): qualify fixture JDBC templates * fix: isolate reconnect hooks and cancellation test fixtures Skip chat initialization so MCP reconnect cannot re-enable file hooks, and keep cancellation fixtures isolated from the background worktree scanner. * fix(ci): tolerate a missing Java cache helper on resumed releases Resuming an existing release checks out a tag that may predate the helper, which failed all five Java jobs before setup-java ran. * fix: finish the main merge for harness deadlines and workflow size Use the renamed settle-timeout accessor in the merged hosted-harness tests, and record sdk-java.yml's new size after the resumed-release cache guard. * style(managed-agent): restore import grouping in merged verification tests * test(managed-agent): require consumed child result mirrors * test(web-shell): center repository settings capture --------- Co-authored-by: yiliang114 <[email protected]> Co-authored-by: yungsen.hys <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> |
||
|
|
406258d66a |
fix(core): headless project-hook trust requires the folder-trust feature (#16264)
* fix(core): headless hook trust requires the folder-trust feature The reporter PoC runs headless (--output-format stream-json with a one-shot prompt), so the unconditional headless exemption left the reported execution path open on a fresh default environment. Require the operator to have enabled security.folderTrust.enabled before the workspace choice counts as implicit trust; with the feature off, project hooks are withheld in every mode. Embedders enable the feature in their managed home settings and keep working via their trust store. Verified locally against the reporter scenario: default fresh home -> marker absent; feature on + trust-store grant -> marker written. Refs #14746 Co-authored-by: Qwen-Coder <[email protected]> * refactor(core): unify the project-hook gate on explicit trust The headless exemption branch never changed an observable outcome: the CLI settings layer already drops workspace settings for an undecided folder once the feature is enabled, and with the feature off the explicit predicate is false anyway. Replace both branches with the single explicit-trust rule so interactive and non-interactive callers share one contract. Behavior is unchanged; verified against the reporter PoC (default fresh home withheld, trusted path executes). Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): address review findings on the unified hook trust gate R1-1: the headless feature-off test wrote folderTrust into workspace settings, where WORKSPACE_RESTRICTED_SETTINGS strips it — the rig user scope stayed enabled and the test never reached the named gate. Write the feature-off decision into the user-scope home file instead. R1-2: the docs sentence overclaimed; IDE-granted trust loads hooks with the feature off. State the predicate as shipped. R1-3: headless withholding was silent for the newly gated population. Emit one stderr line from the non-interactive entry and a debug-log warn from the registry when the trust gate is why the scope is absent (getTrustWithheldProjectHooks, diagnostics only). Verified: reporter PoC withheld with the stderr line present; core 1052 passed; tool-hook integration 14 passed. Co-authored-by: Qwen-Coder <[email protected]> * feat(web-shell): surface trust-withheld hooks in the workspace overview The hooks listing already marks why a project hook is disabled, but the ACP status serializer dropped the reason, so the web-shell overview only showed a count while trust-withheld hooks silently did not run. Carry disabledReason through ServeHookEntry/DaemonHookEntry (optional, older daemons unaffected), count untrusted hooks in the summary, flag the facet, and name the remedy in the tooltip (en + zh). Co-authored-by: Qwen-Coder <[email protected]> * test(web-shell): pin the trust-withheld hooks tooltip wording Component-level witness for the untrusted-hooks facet: the row shows the withheld count and the title names the remedy. The sidebar hooks facet is opt-in for embedding hosts, so the jsdom suite is the reachability-independent pin. Co-authored-by: Qwen-Coder <[email protected]> * test(cli): align fixtures with the surfaced hook state The workspace-hooks status test now expects the disabledReason the serializer carries, and the headless-warnings config stub provides the new getTrustWithheldProjectHooks accessor. Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): tolerate partial config stubs in the headless hook notice The resumed-transcript and stream-json housekeeping tests use minimal config stubs; optional-chain the two accessors the notice reads, and provide the accessor on the remaining isInteractive stubs. All 124 llm.test.tsx cases pass. Co-authored-by: Qwen-Coder <[email protected]> * fix(hooks): make the withheld-hooks surfaces reachable and equivalent Review findings on the status surfaces: the listing never carried disabledReason untrusted for settings project hooks (the trust gate keeps them out of the registry, and only agent/session rows were trustGated), so the web-shell hint was unreachable; buildHooksListing now appends the diagnostics block as disabled (untrusted) rows, with a real-listing test. The headless stderr notice now mirrors the TUI predicate (raw workspace hooks non-empty, exclude disableAllHooks and safe/bare), covering the feature-on undecided case the Config accessor cannot see. The facet value stays language-neutral; the localized withheld count and remedy live in the tooltip. Core 1035, llm 124, web-shell sidebar 51 tests pass. Co-authored-by: Qwen-Coder <[email protected]> * test(web-shell): browser witness for the withheld-hooks status surface Real Chromium e2e: open Daemon Status via the rail More popover, switch to Diagnostics, and assert the workspace.hooks section renders with the withheld count from a report shaped like the real daemon output. Screenshot retained as test output. Co-authored-by: Qwen-Coder <[email protected]> * fix(hooks): fold the deferred approval follow-ups into this PR Per review direction, land the #16295 items here instead of a follow-up: - Explicit projectHooksApproved state: a stored approval keeps execution authority inside a command-line --yolo session (previously the trust gate withheld it because the pending flag is undefined while exempt). The flag never doubles as nothing-to-approve; mid-session decisions update it. Tests pin yolo+approved executes, yolo+unapproved withholds. - Display-only raw workspace block: with the feature on and the folder untrusted the settings layer drops the workspace scope, so the listing could not show the withheld hooks. projectHooksDisplayOnly feeds only getTrustWithheldProjectHooks, never the registry; test pins both halves. - Variable-source wording: the dialog hint now says environment (project .env/settings.env or shell), not only the approved project env. Per-hook approval granularity stays out: it is a feature iteration, not a correctness fix. Core 1017+1053, cli config 565, approvals 50, llm 127 pass. Co-authored-by: Qwen-Coder <[email protected]> * style: prettier the reworded dialog hint Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): /hooks list shows withheld project hooks in ACP/web-shell The non-interactive /hooks list read only the registry, so an ACP session (web-shell) with unapproved project hooks answered \"No hooks configured\" — the user could not see what was withheld or why. The list now includes the approval/trust diagnostics blocks, marked disabled (not approved / rejected / folder not trusted). Verified against a real ACP session: the withheld SessionStart hook is listed with its reason. Tests pin all three reasons. Co-authored-by: Qwen-Coder <[email protected]> * feat(web-shell): explicit project-hooks approval prompt for withheld hooks Web Shell previously had no interactive surface for the project-hooks approval gate: users had to run /hooks manually. The daemon now reports the pending decision (with sanitized hook lines) on /workspace/hooks, including on a cold daemon and when folder trust withholds the workspace scope, and a dedicated POST /workspace/hooks/approve route (plus the workspace-qualified twin) records the same hash-bound decision the TUI dialog writes. The Web Shell renders an Approve / Keep-disabled banner from that state. This is deliberately not session/request_permission: automation clients auto-answer permission prompts, a trust decision must be an explicit user act, and approving never grants folder trust. Co-authored-by: Qwen-Coder <[email protected]> * refactor(web-shell): render hooks approval with the shared ToolApproval card Replace the custom full-width alert banner with the existing ToolApproval card in the standard approval slot above the composer, so the hooks trust decision looks and behaves like every other approval: same panel, option list, keyboard handling, and safe-default focus. The card supplies a project_hooks request with decision-specific option labels; ToolApproval localizes its heading and question for that tool name. Co-authored-by: Qwen-Coder <[email protected]> * fix(web-shell): hooks approval probe reads once and polls only while pending The approval card polled /workspace/hooks unconditionally, adding background metadata reads the workspace-overview quiet-period contract forbids. Read once per mount/workspace/settings change, poll only while a decision is pending, and let the E2E quiet-period test settle the initial probe before counting sidebar traffic. Co-authored-by: Qwen-Coder <[email protected]> * fix(hooks): apply the persisted decision once and to every live session Local review + ponytail over the PR diff: - The ACP child no longer re-derives and re-writes the approval hash on the approve control. The daemon persists the decision for the exact block the user reviewed; a second write could bind it to a block that changed since the review. The child only applies the decision, to every live session config, not just the bootstrap one. - The raw-workspace review subject hashes the project env files regardless of current folder trust, so a decision recorded while untrusted still matches after the folder is trusted. - The headless trust warning no longer doubles with the approval-gate warning for a pending block; the registry debug-log duplicate and its interface member are removed (the stderr line and listings already report the withheld state). - Drop dead test stubs for an accessor llm.tsx never reads, a redundant type member, and a stray blank-line deletion. Co-authored-by: Qwen-Coder <[email protected]> * fix(hooks): scope the approve decision to the admission-bound workspace The per-session fan-out matched every live config in the child. Key it on the admission-bound workspace root (the same root the settings handlers use) against the request's cwd instead: session/cd moves the live target dir, and a decision recorded for one workspace must not clear or set the gate on a session admitted under another. Regression test covers same-workspace, cd-moved, and foreign-workspace configs. Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): bind the project-hooks approval to the reviewed block The daemon's approve route re-read .qwen/settings.json at click time and persisted the hash of that fresh read, while the ACP child applied the decision to the block it rendered at startup. A file change inside that window (git pull, another session, the workspace's own agent) recorded an approval for hooks the user never saw, and left the card pending because the stored hash matched neither side. The child now persists against the same read that produced the status cell it rendered, so the reviewed block and the recorded block are the same object. The daemon only falls back to its own read when no child answered, which is the cold-daemon case where that read is what the client saw. Also unbreaks the web-shell lane: App.test.tsx's daemon-react-sdk mock had no useOptionalWorkspace export, so every App render threw and 1320 tests failed; and the workspace-overview facet counts no longer include the per-mount /hooks probe, which broke the exact facet-set assertion. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmw2kfno0dl * fix(cli): explicit folder trust alone runs project hooks The startup gate demanded a stored approval even when the folder was explicitly trusted, contradicting the documented either-or authority: core's readProjectHooks accepts approval OR explicit trust, but the pending flag short-circuited the accessor before trust was consulted. A platform that pre-trusts its managed directories (folderTrust enabled plus a TRUST_FOLDER/TRUST_PARENT entry) saw every project hook withheld headless. Clear the pending gate for a trusted folder unless the exact block was explicitly rejected — a recorded 'no' still wins over trust. Verified headless on a real fixture: fresh home withholds; trust config alone fires the hook; a rejected block stays withheld in the same trusted folder. Gate unit tests cover pending/rejected/approved under explicit trust. Co-authored-by: Qwen-Coder <[email protected]> * fix(hooks): bind the approval decision to the rendered block hash end to end The status cell now publishes pendingApprovalHash, the Web Shell card echoes it with the decision, and both the daemon route and the ACP child verify it: the daemon rejects a decision whose hash names a block other than the current file (409 project_hooks_changed, nothing persisted), and the child applies a hash-bound decision only to configs holding that exact startup subject (projectHooksApprovalSubject), leaving any config with a different block pending. Closes the cold-daemon re-read window: an approval can no longer bind commands the user never saw. Verified on a real daemon: render hash -> edit the hook file -> POST with the rendered hash returns 409 and persists nothing; re-render -> approve with the fresh hash persists and enables the hook; a real browser session then fires it. Card tests 5/5 (including hash echo), approve-control tests 2/2, gate tests 20/20, build + bundle clean. Co-authored-by: Qwen-Coder <[email protected]> * fix(hooks): close the remaining approval-gate review findings - Escape on the Web Shell trust card defers without recording a decision (TUI semantics): ToolApproval gains escapeDismisses/onDismiss, and the card hides until the block hash changes instead of persisting a rejection for a habitual keypress. - The card's options are *_once kinds so the shared safe-default logic pre-selects the reject side; a stray Enter on the security gate can never grant hook execution. Decision-specific labels stay via the option-id i18n keys. - The hooks-trust card reports into approvalOverlayActive, so the composer is dormant while the card owns the keyboard, exactly like a tool approval. - Under command-line YOLO, replacing the hooks block drops the old approval authority: an edited block cannot execute on the previous block's approval while the gate is exempt. - The trust-withheld listing surfaces the display-only block even when the empty execution block reads the gate as cleared (feature on, folder untrusted) — the state the copy exists for. Tests: core config + hooks-listing 1055/1055, hooks reload 38/38, ToolApproval + card 99/99 including Escape-defer and reject-default regressions; build + bundle clean. Co-authored-by: Qwen-Coder <[email protected]> * chore: record merge provenance The main merge ( |
||
|
|
d3ef273950 |
fix(cli): gate project hooks behind an approval dialog (#14748)
* fix(cli): gate project hooks behind an approval dialog Hooks in a workspace .qwen/settings.json ran at startup with no prompt whenever folder trust was disabled, which is the default. Gate them like project MCP servers (#4615): a hash-bound, user-local approval stored in ~/.qwen/hookApprovals.json, asked for in an interactive startup dialog (ink and OpenTUI) and re-checked on /hooks reload. Headless runs leave unapproved project hooks unloaded and print a stderr warning. * fix(cli): address review findings on the project hooks approval gate - Only a command-line --yolo skips the gate; a workspace can set tools.approvalMode itself. disableAllHooks, bare and safe leave it unarmed. - Key decisions by the project that supplied the hooks, not the live cwd, so /cd and --worktree keep approvals; /hooks reload acts only on a gated session. - Treat null and non-runnable hooks blocks as no hooks; validate stored status; hash with null prototypes; write the store 0600 with forceMode; keep QWEN_CODE_HOOK_APPROVALS_PATH out of project .env files. - Dialog: sanitize repo-supplied text to one row, show env/headers/ allowedEnvVars/prompt, cap the list to the available height, Esc postpones instead of rejecting; ink /hooks marks withheld hooks disabled. - Gate the legacy merged-hooks fallback in getUserHooks too. - Headless warning names the recovery for a rejected block. - Fix llm.test.tsx stubs, the real-settings reload test and the ACP integration tests (pre-approve via a shared helper); add render tests. * fix(cli): address second review round on the project hooks approval gate - Hook registry tags a copy of each definition, so an approved block keeps its hash in a reused settings instance (ACP daemon) - /hooks reload re-reads the approvals file; Esc adds a transcript notice - Truncated hook values are flagged; prompt text gets the full budget - Dialog keeps at least three hook rows and one row per hook - Hooks listing and both /hooks browsers show withheld project hooks as awaiting approval - Integration helper keys and hashes through the CLI's own code - Tests for getSeparatedHookSettings, the headless warning, the project .env exclusion, and gate table titles; docs record the hash scope and the subagent frontmatter follow-up * fix(cli): keep hook registry identity; copy project hooks for the gate instead The registry copy broke managed hook runtimes, which map registry entries back to their definitions by config identity. Hand Config a clone of the project hooks block so the settings instance an ACP daemon reuses keeps its hash. * fix(cli): bind hook approval to the unresolved block; show what runs - Hash and display the hooks block before ${VAR} resolution so startup and /hooks agree - Show command env as KEY=value; matcher last; rows cut to width with a marker - Derive dialog chrome from the wrapped header at the real width - Neutral "not approved" label (pending and rejected) - Docs: frontmatter hooks and statusLine are not gated; ACP status has no reason * fix(cli): read the unresolved hooks block only when the resolved one is gated * test: approve project hooks in new tests that rely on them * fix(hooks): bind approval to referenced env values and harden the approval dialog - Hash and show the values of $VAR/${VAR} a project hook references, so a .env change re-asks - Read the approvals file fresh, merge on save, and keep hooks withheld when a save fails - Count only hooks the registry would run; show a prompt hook's model; cut by code point - Keep the dialog inside the list budget in both renderers, modal over shells in OpenTUI - Esc dismisses a block for the session; label rejected hooks separately in /hooks - Re-arm the gate when a --yolo session leaves YOLO; forward the store path into sandboxes * test(serve): allow the hook approvals path in the sandbox env guard * fix(hooks): bind project hooks approval to the workspace env sources - Hash the unresolved block with the workspace's own .env files and settings.env instead of parsing $VAR names from the hook text; the dialog no longer shows env values. - Gate and list hooks from the resolved block, worded from the unresolved one. - Re-gate a --yolo session on its first switch out of YOLO. - Skip the gate under a shell execution sandbox; pass separated hook scopes from the hosted and memory-import callers. - Approval dialog strings are English-only in both renderers. - Tests for the rejected label, dialog choices, reload ordering, function hooks and the absent headless warning. * test(integration): hash project hook approvals with the workspace env sources * fix(cli): keep Ctrl+C out of focused Ink shell input Fixes the foreground-shell flake in #15814. Same change as #15859. * test(managed-agent): stop the verification suite running out of heap on hosted runners Raise the surefire fork to 40% of RAM and make the lease renewal test wait for renewals instead of counting them in a 150ms window. See #15248. * fix(cli): re-gate project hooks against the live block when leaving YOLO The YOLO-exit listener re-applied the startup approval snapshot, so a block replaced by /hooks reload while YOLO-exempt ran unreviewed after leaving YOLO. Read the live block through a provider closed over the live LoadedSettings (the disabledSkillNamesProvider pattern), and keep listening so re-entering and leaving YOLO re-gates again. Closes review finding R7-1. * fix(cli): keep hooks approval options visible on short terminals The dialog chrome never clamped to the row budget, so on a short terminal the fixed-height frame clipped Reject and left only the pre-focused Approve. Cut the description to the rows the chrome and one list row leave, in both renderers, before shrinking the hook list. Closes review finding R7-2. * test(cli): build reload approval fixtures from the real env sources The fixture pinned an empty env while the production hash reads every project env file findEnvFiles discovers, including ancestors of the temp root, so the approval tests went red on runners with an ancestor .env. Read the same sources in the fixture and plant an ancestor .env so the hash coverage stays provably sensitive on every machine. Closes review finding R7-3. * fix(core): approval is execution authority for project hooks (either-or) The interactive folder-trust gate merged from main swallowed the approval decision: with the feature off (default), Approve persisted but getProjectHooks() still returned undefined, so the dialog promised execution it could not deliver (review blocking item 1; tmux Cases 1b/7). readProjectHooks() now admits the block when the hash-bound approval is approved OR the folder is explicitly trusted; pending and rejected stay withheld, and an ungated interactive session still needs folder trust. Tests pin interactive + feature-off + approved => runs, pending/rejected => withheld, ungated untrusted => withheld. Also from the same review (suggestions): the Ink /hooks browser now mirrors the execution accessor instead of showing trust-withheld hooks as enabled; a command containing variable references says it resolves them from the approved project env; the Esc notice points at restart instead of implying /hooks can decide. Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): align hook status surfaces and docs with either-or authority The either-or execution gate left three status surfaces behind (review blocking items 1a-1c): the startup trust warning fired for approved blocks that actually load, getTrustWithheldProjectHooks() re-listed an approved block as trust-disabled (duplicate contradictory rows in the OpenTUI /hooks browser and ACP status), and the Ink browser's enabled expression showed ungated untrusted sessions as enabled. The warning now skips approved blocks, the listing accessor shares the execution predicate (hasApprovedProjectHooks), and the Ink row mirrors getProjectHooks() directly. Docs state approval-or-trust authority and the folderTrust-on-untrusted no-dialog case (blocking item 2). Tests: approved + default trust => no warning and no withheld copy; ungated untrusted => Ink row disabled; pending => trust row present. * fix(cli): warn from the execution accessor, not the approval flag The round-10 warning guard read gated && !pending as "approved", but pending === false also means "nothing to approve" (an untrusted folder with the trust feature on contributes no settings). That suppressed the one signal the user gets in exactly the case the warning exists for (review blocking item; also the Integration Tests no-AK failure). Skip the warning only when getProjectHooks() actually serves the block, and offer approve-or-trust only while a decision is genuinely pending. Rename the ambiguous core predicate to isProjectHooksApprovalCleared and document both meanings; getWithheldProjectHooks reads the raw block so the listing names a pending/rejected decision even when trust also withholds, with the approval reason outranking the trust reason and no duplicate rows. Scope the docs' YOLO execution claim to non-interactive runs. Tests: folderTrust-on untrusted warning regression (unit + the existing interactive integration case), approval-reason priority in the listing, raw-block reachability while pending. --------- Co-authored-by: 易良 <[email protected]> Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
35aa4b76ae |
fix(core): require explicit trust for settings-file project hooks (#16196)
* fix(core): require explicit trust for settings-file project hooks A repository .qwen/settings.json SessionStart hook ran at startup with no trust prompt while the folder-trust feature was off by default: getProjectHooks() consulted isTrustedFolder(), which returns trusted when the feature is disabled. Gate it on isExplicitlyTrustedFolder() instead, matching the frontmatter-hook and LSP gates from #16182. Update the hooks docs, and grant explicit trust in hook test fixtures. Refs #14746 Co-authored-by: Qwen-Coder <[email protected]> * style(core): use Array<T> for the tuple-array cast Co-authored-by: Qwen-Coder <[email protected]> * test(integration): grant explicit trust in ACP parity fixtures The ACP parity rig spawns the CLI with its own QWEN_HOME, so the trust granted by TestRig.setup() never reaches those sessions. With project hooks now requiring explicit folder trust, the UserPromptSubmit hook scenarios silently lost their hooks. Seed the scenario home with folder trust enabled and the rig directory trusted, matching the tool-hook suites. Co-authored-by: Qwen-Coder <[email protected]> * fix(core): align project-hook trust with the interactive boundary Follow the Claude Code model: interactive sessions keep the explicit folder-trust gate, while non-interactive invocations (-p, piped stdin, ACP/SDK embeddings) treat trust as implicit — the caller chose the directory and there is no prompt surface. This keeps platform embedders working without trust-file migration. Surface a startup warning when interactive project hooks are withheld so the gate is not silent, and document the headless contract. Revert the integration-fixture trust grants that the headless exemption makes unnecessary. Refs #14746 Co-authored-by: Qwen-Coder <[email protected]> * test(integration): drop ACP parity trust seeding made moot by headless exemption The parity rig spawns non-interactive ACP sessions, which now treat trust as implicit, so the seeded trustedFolders.json only perturbed the request-snapshot comparison (an extra tmp/ tree under .qwen-home). Restore the fixture to its main-branch shape; all 23 parity cases pass. Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): tolerate partial settings shapes in the project-hook warning The startup warning read settings.workspace.settings.hooks without optional chaining; AppContainer tests render with a partial LoadedSettings mock, and a missing workspace scope crashed the effect before config initialization completed. Guard the lookup; all 272 AppContainer tests pass. Co-authored-by: Qwen-Coder <[email protected]> * docs(hooks): resolve trust-gate wording contradictions (R1-5) Troubleshooting step 3 still stated the legacy trusted-folder rule and the restart list omitted security.folderTrust and persisted trust decisions. Align both with the interactive explicit-trust gate and the headless implicit-trust contract, and mention the withheld-hooks startup warning and browser state. Co-authored-by: Qwen-Coder <[email protected]> * test(integration): drop dead ACP trust seeding in the tool-hook suites The per-session trustedFolders.json written beside the QWEN_HOME override is never read: getTrustedFoldersPath() prefers the inherited QWEN_CODE_TRUSTED_FOLDERS_PATH that TestRig.setup() stubs. Both blocks sit in front of an --acp spawn, and ACP sessions are non-interactive, so the headless exemption already loads project hooks without any trust grant. Remove the writes and the now-unused realpathSync import, restoring both suites to their base shape. Refs #14746 * test(cli): pin the folderTrust derivation behind the project-hook gate The interactive gate is covered by the Config table, but nothing pinned settings.security.folderTrust.enabled -> Config.folderTrust. Assert that loadCliConfig derives it and defaults it to false, so a derivation flipping to true cannot silently arm repo-supplied hooks. Verified the case goes red when the default is mutated to true. Refs #14746 * docs(hooks): align trust-boundary descriptions with implementation Remove the unimplemented claim that withheld project hooks appear in the hook browser. Clarify that loop.md retains the legacy folder-trust predicate while executable project hooks use explicit interactive trust. Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): keep folder trust out of workspace reach The project-hook gate reads security.folderTrust.enabled from merged settings, so a repository could enable the very flag that authorizes its own hooks and then reach any pre-existing TRUST_FOLDER or TRUST_PARENT rule for the directory. Restrict the key to the operator scopes, beside allowPrivateNetworkHooks, and pin both directions: a workspace can neither enable the gate nor disarm a user's setting. Also close the smaller review notes. The startup warning no longer blames trust when disableAllHooks removed the hook system entirely, and it now has coverage for the warned and both silent branches. Every hook fixture ran pre-trusted, so add an interactive integration case that declines trust and asserts the hooks never fire, the warning names the remedy, and the tool result still reaches the model unmarked. Drop the duplicated node:fs import and correct the docs claim that IDE trust decisions apply live to settings-file hooks. Refs #14746 Co-authored-by: Qwen-Coder <[email protected]> * test(cli): align the daemon trust-grant case with the workspace restriction security.folderTrust is now stripped from workspace scope, so a grant on a workspace that only self-enables trust reports the disabled state instead of refusing an ineffective grant. Keep the alias DO_NOT_TRUST fixture: if the restriction regresses, the grant throws again and this case goes red (mutation-verified). Fixes the Test (ubuntu-latest) failure on |
||
|
|
1f09497f45 |
fix(core): require explicit trust for project frontmatter and LSP (#16182)
* fix(core): require explicit trust for repository execution * test(integration): explicitly trust skill hook fixtures * test(lsp): add isExplicitlyTrustedFolder to document-sync mock config The document-sync tests create a bare Config mock with only isTrustedFolder. The new explicit-trust gate in NativeLspService and LspServerManager calls isExplicitlyTrustedFolder(), which is undefined on the mock, producing TypeError at reload time. Add isExplicitlyTrustedFolder: () => true alongside the existing isTrustedFolder mock so the 13 affected cases pass again. Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
1dc74d5838 |
feat(core): skip re-injecting an unchanged UserPromptSubmit hook context block (#14129)
* feat(core): skip re-injecting an unchanged hook context block
A UserPromptSubmit hook's additionalContext is appended to the user
message on every turn, so a deterministic hook pays for the same block
again on each request and total request tokens grow quadratically over a
session. The bundled auto-recall hooks render a bounded, newest-first
result window, which makes a byte-stable payload easy to hit.
Core is the sole producer of those appended parts and keeps them in
history, so the injection site can compare the wrapped block it is about
to append against what is already there. When a match is found the block
is not appended again and the turn records no hook-context provenance,
keeping the stored transcript consistent with what the model received.
Any payload change fails the equality check and injects normally, so
there is no suppression state to expire.
Refs #14089
* fix(core): prove hook context provenance before skipping a re-injection
Require the shape an injection produces before an identical
UserPromptSubmit context block counts as already delivered. A block now
suppresses a fresh delivery only when a user history entry ends with
exactly that wrapped block and carries at least one part of the user's
own ahead of it.
Tagged text that merely appears — in a model entry, or as a whole user
message, which transcript handling already treats as user-authored — is
not proof the hook delivered it, so those cases inject as before.
Cover the three provenance cases as unit tests next to the positive
control, and re-run the falsification pair at both injection sites.
Refs #14089
* fix(core): key hook-context dedup to an injector-owned receipt
The shape of an ordinary user multipart message is not proof of hook
authorship: an ACP client can submit two text blocks that persist as
exactly the injection shape, byte-for-byte, while the UserPromptSubmit
hook returns nothing. A later genuine delivery of the same payload was
suppressed by that caller-authored entry.
Stamp the part the injector appends with a module-private symbol
receipt and require it, in addition to the exact trailing text, before
an entry counts as already delivered. The symbol cannot cross a JSON
transcript, an ACP prompt block, or model output, so caller-controlled
shape is no longer accepted as the receipt. Resume rebuilds re-stamp
the receipt from the recorded per-turn hookContext provenance, which
only the injector writes; when provenance cannot be established, the
block is injected as before.
Cover the receipt, its resume restore, and the forged-shape negatives
at both injection sites.
Refs #14089
* fix(core): keep hook-context provenance on the live injection path
R1-1: a suppressed turn still records the user-authored displayText, so the
transcript projection survives; only the hook-context provenance is left out.
R1-2: re-stamp the appended block after text flattening, so live history can
prove origin on the next turn the way the resumed path already does.
R1-6: read the resume payload through UserPromptRecordPayload.
R1-10: drop a comment describing a promptText assignment that does not exist.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmve1dj7jft
* docs(design): align the dedup design doc with shipped behaviour
- Problem paragraph: identical bytes only when the dialect omits per-query
scores; the shipped example copies the score (request-engine.ts:253).
- Risks: suppression is receipt-gated, so compaction and a resume without
per-turn hookContext provenance re-inject once and re-arm.
- Acceptance criterion: at-most-once holds while the stamped part stays
trailing; a later append or a provenance-less rebuild re-injects one
duplicate.
- Validation step 1: name the three shapes the predicate tests actually
cover; shape-only variants belong to isUserPromptSubmitContextPartText.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmve3ip14fy
* fix(core): keep the hook-context receipt helpers off the root barrel
R1-5: `createUserPromptSubmitContextPart` and
`restoreUserPromptSubmitContextReceipt` are module-internal provenance
primitives. The design doc allows no new public surface beyond the exported
predicate, and the only root-barrel consumer was a CLI test. Drop both from
the root barrel and point that test at the existing `userPromptSubmitContext`
subpath, which is how the ACP production path already imports them.
R1-9: document the suppression next to the reserved-tag explanation in the
user-facing hooks page, which currently states the opposite.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmve5nurig3
* fix(cli): keep the UserPromptSubmit append when the ACP prompt resolved to no parts
R2-1: on the ACP lane a prompt whose only block is dropped (a resource_link
that fails workspace revalidation, or a blank slash-command body) resolves to
an empty part array. Suppressing the byte-identical hook context there left
the model-bound request empty, and `toParts` rejects an empty array with
`partOrString cannot be an empty array`. Suppression now requires at least
one part, which restores the pre-dedup append for that case.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmve7t0jzg8
* fix(core): keep the hook context block on the turn that compacts
A suppressed UserPromptSubmit block is decided against pre-compaction
history, but automatic compaction replaces that history inside the same
send, so the block's only copy is dropped before the request is built
and the model sees it zero times on that turn. Pass the suppressed
payload down and re-append it when the send compacts.
Also restore the unconditional append in the ACP path when the prompt
resolved to no parts: `parts.length > 0` gated the append as well as the
suppression, so an empty-parts turn reached `toParts([])` and threw.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvec3c4pgi
* docs(design): draw the receipt-survival boundary at the operation
The design doc said the receipt survives "in-memory history churn" and drew
the boundary at serialization. `structuredClone` is an in-memory operation
that also drops symbol-keyed properties, and the prepared-request replay
installs history that way (`llm-chat.ts`), so the sentence taught the opposite
of the invariant it states: every in-memory path is safe.
Name the operation instead of the medium, and note that a replayed
prepared request re-injects once and re-arms, plus the same case in the Risks
list. Both language docs updated.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvegdno2gr
* fix(core,cli): keep a suppressed hook block through every compaction site
|
||
|
|
52ae53b02a |
fix(hooks): gate headless file and skill prompts before expansion (#14356)
* fix(hooks): gate headless file and skill prompts before expansion Headless ran a custom command's or skill's action before UserPromptSubmit, so a blocking hook could not stop embedded shell commands or a skill's tool and hook registration, and the hook saw the expanded template. Run UserPromptSubmit on the raw input before a file command or skill (stacked skills included) expands, once per submission, and hand the verdict to that submission's first model request instead of running the hook again on the expanded text. The verdict is a single-use token bound to the client, session, prompt id and hook owner; any other use throws. A refused command is not expanded and the request blocks through the existing UserPromptSubmit block path, so output and cleanup are unchanged. * test(core): reject a prompt admission after the session changes |
||
|
|
d13ff87407 |
feat(hooks): apply PreToolUse updatedInput with full revalidation (#13442)
* feat(hooks): apply PreToolUse updatedInput with full revalidation A PreToolUse hook's updatedInput now replaces the whole tool input in the terminal and in ACP sessions. The hook runs once per call before input validation and permission checks, so the replacement goes through the media gate, schema validation, permission rules, plan and AUTO mode, the approval prompt and the invocation guard, and is what the tool and PostToolUse receive. A PreToolUse ask joins the normal approval prompt and can no longer be auto-approved. Invalid replacements stop the call in both PreToolUse and PermissionRequest output, and a PermissionRequest replacement is rebuilt and checked again, including AUTO classification, before its one-time allow applies. Shell calls that a hook may still rewrite are no longer run in parallel on early admission paths, and managed runtime and fixed-policy calls refuse replacements instead of ignoring them. * fix(hooks): keep PermissionRequest deny under a PreToolUse ask A PreToolUse ask no longer skips the PermissionRequest hook, so the hook's deny still applies. Its allow never stands in for the user's confirmation: it can only have a replacement input revalidated, which the user then confirms. In ACP sessions, AUTO recovery state now follows the replacement's own classification before its one-time approval is consumed, so approving a replacement's fallback resets the right counters. The scheduler releases the batch abort listener in finally, so a throw while preparing a batch cannot leave it pinned to the signal. A Host confinement test from main now builds invocations from their input, as rebuilding a replacement requires. * fix(cli): release a rejected PermissionRequest replacement invocation In ACP, a replacement input built from a PermissionRequest hook was handed to cleanup only after it passed validation. A later deny, PLAN block, AUTO block or cancellation returned early and released only the original invocation, leaving the replacement's prepared execution pending. Hand the replacement to cleanup as soon as it is built, and release the original right away since it will not run. --------- Co-authored-by: qwen-code-dev-bot <[email protected]> |
||
|
|
e767e223c5 |
fix(hooks): deliver pre-tool and ACP failure context to the model (#13006)
* fix(hooks): deliver pre-tool and ACP failure context to the model PreToolUse additionalContext was dropped on the deny/ask/stop branches of the hook helper and never attached to the tool result in core or ACP, and ACP only logged PostToolUseFailure context on both the returned-error and thrown-error paths. Append the sanitized context to the owning call's functionResponse: - core keeps it per call across an ask bounce and assembles it after PostToolBatch, before the final batch budget; cancelled calls drop it - ACP appends Pre and failure context to model-facing parts only, keeping UI updates and error projections unchanged - the added text is bounded per call by truncateToolOutputThreshold * fix(hooks): keep hook context out of nested and App results - exec: a nested call's result is a value for the script, so its hook context is handed to the parent exec and delivered with that result (core via the nested scheduler, ACP via the nested run result) - ACP MCP App calls have no model consumer and never append context - core rechecks each call's abort signal at final assembly, so a turn cancelled during PostToolBatch drops the stored context - share the functionResponse text append with PostToolBatch context - run the hook-context integration test in the no-AK PR lane * fix(hooks): route nested failure context and pin review cases - core: a code-mode nested call's PostToolUseFailure context goes to the parent exec result instead of the error handed to the script, matching ACP and nested PreToolUse context - ACP: nested hook context keeps dispatch order under concurrent nested calls, so truncation does not depend on completion order - docs: scope delivery to Code Mode nested calls, the turn-cancel exception, the per-surface limits, and transcript/detail visibility - tests: sandbox-safe decision file, self-delimiting markers and a real value check in the integration test, ACP agent-exit handling, plus ACP cancel/truncation/nested-failure and core contentLength and nested-failure regressions * fix(hooks): drop hook context when the turn is cancelled before recording - core: after the final output budget (which can await persistence), redo the budget without the context of any call whose turn was aborted - ACP: queued records keep their parts without hook context; if the turn is aborted before finalizeAndRecord records them (e.g. while notifying the client or persisting), finalize and record those parts instead - tests: cancellation during the final budget (core) and while notifying a completed or denied result (ACP); the ACP integration agent gets its own QWEN_HOME and is awaited before teardown --------- Co-authored-by: qwen-code-dev-bot <[email protected]> |
||
|
|
31f6047e9d |
fix(core): isolate declarative subagent hooks by invocation (#12949)
* fix(core): scope declarative agent hooks to their owning invocation * test(core): sync agent mocks with hook execution frame * fix(core): bind clear hooks to the new session owner * fix(core): preserve caller hook identity for internal forks --------- Co-authored-by: qqqys <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> |
||
|
|
c8d30896a7 |
fix(hooks): key enabled state by name, not identity (#11965)
* fix(hooks): key enabled state by name, not identity * fix: address review feedback for hook registry reload warnings and docs |
||
|
|
a64d8decc1 |
fix(web-shell): preserve reference tags across reloads (#12404)
* fix(web-shell): preserve reference tags across reloads * fix(acp): validate daemon input annotations at record, replay, and render Address review findings on the user-message tag replay PR: - Skip non-object annotation elements when recording (new readDaemonInputAnnotations reader), replaying (per-element filter), and rendering (null guard in splitComposerTagContentByAnnotations), so one malformed element can no longer poison a persisted session on every reload. - Cap caller-supplied annotations at MAX_DAEMON_INPUT_ANNOTATIONS = 256, matching the sibling attachment-references bound. - Export DAEMON_INPUT_ANNOTATIONS_META_KEY from bridgeTypes and route the recorder, replay, and live-echo read sites through it so the wire/persist key cannot drift silently. - Restore the deferred /advisor branch's no-payload test pin and split its annotation assertions into a dedicated case. - Document the annotations payload trigger in hooks.md and the intentional queued-strip/composer styling scope in both design docs. * test(cli): make post-index-change hook cases deterministic across filesystem ticks (#12404) The ungated-status control assumed `git status` always rewrites the index on a dirty tree, firing the tree-shipped post-index-change hook. The rewrite only happens when an entry is racily clean, which held only because beforeEach's add and commit usually land in one filesystem tick; a loaded runner straddles the tick, status writes nothing, and the control fails. Force raciness by re-adding README.md with a future-dated mtime (resetting the canary the re-add's own index write fires), and share the fixture with the guarded case so its negative assertion is exercised against a hook that would genuinely fire ungated. --------- Co-authored-by: 钉萁 <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]> |
||
|
|
bfa2f38c1a |
fix(core): load system-scope hooks under their own source (#12077)
* fix(core): load system-scope hooks under their own source Hooks in the System and SystemDefaults settings files had no channel of their own. They reached the registry only through the merged `hooks` fallback, so they were labelled User (or Project), and when the user and workspace scopes both had hooks, or the user scope had hooks in an untrusted folder, the fallback was never read and the system hooks did not load at all. - Settings.getSystemHooks() merges SystemDefaults then System with the normal merge strategy (hook lists concatenate). - resolveHookSettingsForConfig passes system, user and project hooks through as read; the merged `hooks` is used only when no per-scope hooks were supplied at all. - Config gains systemHooks / getSystemHooks(); the legacy merged fallback in getUserHooks/getProjectHooks applies only when no scope was supplied, so a user-only hook is no longer registered a second time under the project source. Planner dedup already collapsed that copy, so User/Project execution counts do not change. - HookRegistry registers System, User, Project, Extensions in that order. - /hooks reloads the System and SystemDefaults files too. * test(core): keep the legacy fallback case independent of the system getter * test(core): pin that planner dedup keeps a doubly registered hook to one run * fix(core): document the real execution order of system hooks Registration order does not decide execution order: getHooksForEvent sorts an event's hooks by source priority, which puts System after Project and User and before Extensions. Correct the guide and the registry comment, pin the order on getHooksForEvent, and tighten the reload paragraph of the merged Browsing section. * fix(cli): keep user hook edits when a system settings file is unreadable Reloading the System and SystemDefaults files in the same atomic batch as the user and workspace files meant a broken administrator file made every /hooks reload fail, including for edits to the user's own hooks, and the user usually cannot repair that file. Reload the user and workspace files first, then the system files on their own: a failure there keeps the previous system snapshot, still applies the other edits, and names the file in the error. Also rename the source-priority test after what it asserts, the planned order, not an executed one. |
||
|
|
c7387d90f2 |
docs(hooks): merge the duplicated Browsing sections (#12071)
The hooks guide had two sections titled "Browsing your hooks" about 1,470 lines apart: one describing the read-only browser, one describing the reload the menu performs. Read side by side, "It does not change anything" and "opening the menu reloads hook definitions" contradict each other. Keep the first section in place, move the reload paragraphs into it and state the distinction: the browser never edits settings, but it does re-read them. |
||
|
|
3178c0c597 |
fix(core): count Stop-hook blocks across tool round trips (#12062)
A Stop hook that blocks forces a continuation. When that continuation calls a tool, the caller re-enters through a fresh top-level send, and the consecutive-block count, which rode the per-call send options, was lost. A hook that blocked on every check could therefore walk past stopHookBlockingCap indefinitely. Keep the hook-forced flag, the count and the ordered reasons in one bounded record per prompt id on the client. The record is retired when the stop is allowed, the cap is hit, the user steers, a new interaction starts on that id (user query, retry, cron, notification, teammate or goal turn), or the send exits abnormally. Concurrent side questions keep their own records, and a re-minted teammate prompt id starts fresh. In non-interactive text mode the Stop hook messages and the cap warning are written to stderr, sanitized; JSON and stream-JSON output are unchanged. ACP production code is unchanged: its loop already counted per prompt() call, and its queued-user-input reset is now pinned. The former goal Stop-hook exemption asked for in #11673 no longer applies: the built-in goal Stop hook was removed in #11900. |
||
|
|
a5ca52c9cf |
feat(cli): reload hook registry when /hooks opens (#11904)
* feat(cli): reload hook registry when /hooks opens Hooks were captured once at startup, so a hook added, changed or removed in a settings file had no effect until restart. Opening the /hooks menu now re-reads the settings files, hands the resolved hook fields to Config via a new setHooksFromSettings, and reloads the hook registry before the menu renders. The hook fields are resolved by one helper shared with loadCliConfig, so the reload applies the startup rules: bare and safe mode load no hooks, user hooks fall back to the merged hooks setting, and project hooks load only in a trusted folder. All three Config fields are replaced together so a stale legacy snapshot cannot come back through the getter fallback. Settings are re-read without reloading the environment or consuming the startup corruption markers. A failed reload is logged and the menu still opens. The non-interactive /hooks list is unchanged. * fix(cli): import the debug logger from its core module in hooksCommand * fix(cli): reload hooks safely from session settings * fix(cli): wrap hook reload notices for terminal menus * fix(cli): keep the hook reload notice as two translatable sentences The notice was one locale key whose English value carried a newline so the menu would break the line. `check-i18n` requires the English value to match its key byte for byte, so CI failed, and every translation encoded its own line breaks, which made layout part of the message data. Split it into two sentence-level keys and let the views place the break: Ink renders one Text per sentence, and the OpenTUI dialog renders one text row per line of its notice prop. --------- Co-authored-by: qqqys <[email protected]> |
||
|
|
06d77ad486 |
docs(goal): retire the Stop-hook era prose after the implementation was removed (#11922)
* docs(goal): retire the Stop-hook era prose after the implementation was removed The first-generation Goal implementation was deleted in #11900, but a design doc, an i18n key, two comments and a setting description still describe it: the input-control design for a Goal loop that ran as a blocking Stop hook, the `/goal` description string nothing reads any more, an ACP control comment naming an in-memory store and a type that no longer exist, a "judge's verdict" field comment, and the per-turn tool-call cap saying Goal iterations are Stop-hook continuations. * docs(web-shell): correct the Goal reason comment the workspace types copied from the bridge * docs(goal): address remaining review accuracy gaps --------- Co-authored-by: qqqys <[email protected]> |
||
|
|
61553c71cd |
feat(cli): full /hooks dialog in OpenTUI (#11903)
Replace the one-line OpenTUI /hooks panel with a read-only browser that moves from events to matchers to hooks to one hook's details, like the ink dialog. Events without matcher support skip the matcher step, and a banner says when safe mode, bare mode or disableAllHooks turned hooks off. The rows come from a new core buildHooksListing(config): the registry entries with their real enabled state, then the hooks registered for the current session, each flattened into display fields plus the original config. Each row records whether it came from the registry or the session, since a subagent's registry hooks also carry the source `session`. The ACP workspace hooks status is now built from the same listing, keeping only registry rows; its response is unchanged and pinned by a new test. Adds a "Browsing your hooks" section to the hooks guide and the new labels to the en, zh and zh-TW locales. |
||
|
|
f7e48d5c4b |
fix(core): keep escaped whitespace at pipe-list entry edges (#11860)
A hook matcher that is a pipe-separated list is rebuilt from its trimmed entries. Trimming removed whitespace the author had escaped and left its backslash dangling, so `\.env\ |\.pem` was rebuilt as `\.env\|\.pem`, where the pipe is literal: the matcher silently stopped matching `/x/secrets.pem`, and `write_file\ |note.*` stopped matching `notebook_edit`. The rebuilt expression still compiled, so nothing was logged. Trailing whitespace at an entry edge is now kept when an odd run of backslashes precedes it. After an even run the backslashes escape each other, so the whitespace is still padding and is trimmed. Leading whitespace is always padding. Part of #11610. |
||
|
|
6c04a19927 |
docs(hooks): complete the event catalog and correct stale claims (#11863)
Cover all 22 hook events in the event table, the matcher table and the per-event details. Add PostToolBatch, UserPromptExpansion, PostCompact, StopFailure and InstructionsLoaded to both tables, and add detail sections for PostToolBatch, UserPromptExpansion, PermissionDenied and InstructionsLoaded. Correct statements that no longer match the code: async hook output is not delivered, PostCompact output is ignored, StopFailure fires on different surfaces for API errors and loop detection, plain-text stdout becomes model context only for command hooks, and the Stop row no longer mentions Claude. Add a troubleshooting checklist for hooks that do not fire. Part of #11610. |
||
|
|
85631a3d35 |
fix(core)!: let bash expand project directory variables in command hooks (#11864)
Command hooks had $GEMINI_PROJECT_DIR and $CLAUDE_PROJECT_DIR replaced with a shell-escaped path before launch, for every shell. Inside the double-quoted form Claude Code documents, "$CLAUDE_PROJECT_DIR/.claude/hooks/check.sh", the inserted quotes became part of the path, so the hook failed whenever the project path contained a space (bash) or at all (PowerShell). All three variables are already exported in the hook environment, and bash expands them itself, so bash commands are now passed through unchanged. cmd.exe never expands $VAR and PowerShell reads a bare $VAR as undefined, so those shells keep the rewrite, which now also covers $QWEN_PROJECT_DIR and stops at a word boundary. The hooks guide quotes its example commands, explains how each shell sees the variables, and adds a migration note. BREAKING CHANGE: in bash hooks, $QWEN_PROJECT_DIR, $CLAUDE_PROJECT_DIR and $GEMINI_PROJECT_DIR are no longer replaced in the command text. An unquoted variable in a project path containing spaces now word-splits, and '$VAR' inside single quotes no longer expands; double-quote the variable instead. cmd and PowerShell hooks are unchanged except that $QWEN_PROJECT_DIR is now rewritten too and longer names such as $CLAUDE_PROJECT_DIRS are left alone. |
||
|
|
e04f2ec5d4 |
fix(core): accept Claude Code tool names in tool hook matchers (#11826)
A tool hook matcher copied from a Claude Code config, such as "Bash" or "Write|Edit", never matched the shell or write_file tools: matcher aliases came only from the tool id, display name and legacy names, while permission rules already accepted Claude Code's names. Tool matcher targets now also include every name the permission rule parser resolves to the same tool. Each name still identifies exactly one tool; the permission meta-categories are not applied, so "Read" matches read_file but not grep_search, and "Bash" does not match monitor. coreTools / excludeTools resolution is unchanged. Fixes #11823 |
||
|
|
632773e018 |
refactor(core): apply one matcher rule to every hook event (#11619)
* refactor(core): apply one matcher rule to every hook event Hook matchers meant different things per event and per registration path. Notification, PreCompact, PostCompact and StopFailure accepted only a single exact value, so `permission_prompt|idle_prompt` never matched, and session-scoped hooks, including skill frontmatter hooks, anchored their regex while settings hooks did not. One shared matchesHookPattern now serves the planner and the session hooks manager: an empty matcher, `*` or `.*` matches everything; the matcher or any entry of a `|` list matches exactly (tool events also compare aliases exactly); otherwise it is an unanchored regex on the subject. Existing tool, subagent, command, session and file-path matcher behaviour is unchanged. Part of #11610 * fix(core): decide pipe-list matchers entry by entry Review follow-up. A `|` list compared its entries exactly and then fell back to the whole pattern as a regex, so `Write|*` became an invalid regex that matched nothing, and a stray pipe such as `write_file|edit|` became a regex that matched everything. Lists are now decided entry by entry with the full rule; empty entries are ignored and never widen the list, and a group spanning the pipe such as `a(b|c)` still falls back to one regex. The planner no longer keeps its own copy of the match-all rule. The hooks and skills guides now say that skill matchers are unanchored, that an omitted skill matcher matches every tool, how to anchor runtime ids and exclusions, and that aliases only match exactly. Part of #11610 * fix(core): decide pipe-list matcher entries without compiling them A pipe-list entry now matches only as `*`, `.*`, or an exact subject or alias. Entries are no longer compiled one by one, so an escaped pipe such as `notes\|todo\.md` keeps its whole-pattern meaning instead of letting the `todo\.md` fragment match any path, and a group that spans the pipe no longer logs "Invalid regex" warnings for its unbalanced fragments. The whole-pattern fall-through is compiled from the non-empty entries, so a stray `|` is dropped rather than disabling the fall-through: `read_(file|edit)|` matches `read_file`, while a list of only `|` still matches nothing and a trailing `|` never becomes a match-all. Pin that a skill hook entry without `matcher:` matches every tool. * fix(core): trim pipe-list matcher entries and split only on unescaped pipes Trim each list entry at the split so the exact comparison and the rebuilt regular expression see the same entries: `read_.* | edit` now matches `read_file` instead of compiling alternatives that demand literal spaces. Split only on a `|` that is not preceded by a backslash, so an escaped pipe never manufactures an empty entry or a dangling escape: `notes\|` matches `x/notes|y`, `a\||b` matches `b`, and `foo\|bar` no longer matches `bar`. Scope the documented stray-pipe guarantee to lists that do not start with `^` or `(`; those matchers are compiled as written. * fix(core): split matcher lists only at top-level pipes The list reading of a matcher split on every pipe not preceded by a backslash, so it cut through character classes and groups and then trimmed spaces that belonged to the expression: `foo[ |]bar` was rebuilt as `foo[|]bar` and stopped matching `foo bar`, and `a(b | c)` lost its spaces the same way. A single preceding backslash also hid a real separator after an escaped backslash, so `C:\\temp\\|D:\\data\\|` kept its trailing empty alternative and matched everything. Split with a scan that reads backslashes in pairs and ignores pipes inside `[...]` and groups, so only top-level entries are trimmed, compared and rebuilt, and `read_.* | edit` keeps reading as `read_.*|edit`. Tests pin each shape and the `^` and `(` guards, and the hooks guide says which pipes separate entries. |
||
|
|
f49dda1b2f |
fix(core)!: read command hook timeout in seconds (#11615)
* fix(core)!: read command hook timeout in seconds with a 600s default Command hooks passed `timeout` to the timer as milliseconds, while HTTP and prompt hooks read it as seconds, the settings schema described seconds, and the hooks guide documented both. A command hook written with `"timeout": 10` was killed after 10ms and failed open. Command hook `timeout` is now read in seconds and defaults to 600s. A value of 1000 or more is still read as milliseconds so existing settings keep their meaning, and a debug-log warning names each such hook once. Timeout errors report seconds. HTTP, prompt and function hooks are unchanged. The schema description, generated JSON schema and every command hook example in the guide now use seconds. BREAKING CHANGE: a command hook `timeout` below 1000 is now seconds rather than milliseconds. Part of #11610 * fix(core): keep hook timeouts authoritative and surface legacy values Review follow-up. - Hook execution requests on the message bus no longer carry the bus's 60-second default deadline, so a hook is bounded by its own timeout instead of being abandoned while it keeps running. Explicit caller timeouts and other request types are unchanged. - Command hooks on MessageDisplay, StopFailure and SessionDelete, which outlive the CLI, default to 60 seconds instead of 600. - Settings warnings now name each command hook whose timeout is still read as milliseconds; the advice never suggests a seconds value that would itself be read as milliseconds. - The hooks guide scopes "seconds" to command, HTTP and prompt hooks and notes async slot usage; shipped external-context examples use seconds. - Tests pin the async registration and supervisor deadlines, and reset the warning deduplication between cases. Part of #11610 * docs(core): explain long command hook timeouts and blocking hold time A command hook timeout of 1000 or more is still read as legacy milliseconds, so a value meant as a long timeout in seconds, such as 1800 for 30 minutes, ends up as 1.8 seconds. The startup warning now also says how to keep the value that was meant as seconds, and the hooks guide explains that long timeouts stay in milliseconds. The guide also notes that a synchronous hook on an event that waits for its result holds the turn until it exits or times out, 600 seconds by default. * fix(core): bound signal-less hook requests and cancel tool hooks on abort The message bus now waives its default deadline only for hook execution requests that carry an abort signal. Requests without one cannot be cancelled, so they keep the 60-second deadline instead of waiting on the hook's own timeout. PreToolUse, PostToolUse and non-interrupt PostToolUseFailure hooks now receive the tool's abort signal, in the payload and as the request signal, so an abort stops the hook and releases the wait. Interrupt reports keep no signal because it is already aborted there. The UserPromptSubmit hook gets the send signal, and a send aborted during or right after that hook ends as an abort instead of recording or sending the prompt. MessageDisplay deliveries pass the turn signal to the request, warn when the bus reports a failed hook execution, and stay quiet when the turn was cancelled. A command hook timeout written as a numeric string is read like the number it holds instead of falling back to the default. The startup warning for legacy millisecond timeouts is removed from this change and tracked in #11675; the hooks guide now explains how to find such hooks. Async hook timeout messages report seconds, the remaining first-party millisecond values in integration fixtures and design docs are rewritten in seconds, and the async hooks bullet names the 60-second default on events whose hooks outlive the process. * fix(core): keep the 60 second command hook default and drop hook bus signal threading Raising the command hook default to 600 seconds forced two rounds of reconciliation with the 60 second and 15 second bus deadlines: first by waiving the deadline, then by threading abort signals through the tool and prompt hook call sites. Each round produced new regressions (payload vs request signal, already-aborted failure reports, unmigrated arity assertions, orphaned hook children, cancels recorded as hook errors). The default change and the cancellation work move to a follow-up issue, and this change keeps only the unit. - Command hook timeouts stay in seconds with a 60 second default, the same duration as before; values of 1000 or more keep their legacy millisecond meaning, and numeric strings are still coerced. The separate default for hooks that outlive the process is gone because it now equals the default. - MessageBus, tool hook triggers, the scheduler, the UserPromptSubmit request, the MessageDisplay dispatcher and their tests are back to main. - An unusable timeout such as "30s" is named in the debug log with the default it was replaced by, like a legacy millisecond value. - Timeout warnings are recorded as sent only once debug logging can write them, and are deduplicated per session. - Async hook timeout messages and a real spawned hook with timeout 1 now pin the seconds conversion. --------- Co-authored-by: qwen-code-dev-bot <[email protected]> |
||
|
|
7340de4f37 |
fix(core): report real stop_hook_active on Stop hooks (#11613)
* fix(core): report real stop_hook_active on Stop hooks Both places that fire the Stop hook, the core client and the ACP session, hard-coded stop_hook_active to true. A Stop hook that follows the usual pattern of blocking only while stop_hook_active is false therefore never blocked, and no hook could tell its own continuation apart. The field is now false on the first stop check and true only when the agent is continuing because a Stop hook blocked the previous stop, which is what SubagentStop already reported. The core client derives it from the stop hook iteration count it already threads into the continuation turn; the ACP loop uses its own iteration counter. Part of #11610 * fix(core): keep stop_hook_active across tool calls in a hook-forced chain Review follow-up. The core client derived stop_hook_active and the consecutive-block count from send options that only the internal recursion carried, so a hook-forced continuation that called a tool came back through a fresh call with the state lost: a guard-style hook re-blocked forever and the blocking cap never fired. The next-speaker and steer recursions, which spread the options, kept stale state instead. The client now keeps the chain per prompt id, written by both blocking branches and cleared when a stop is allowed, the cap is reached, the user steers, or a Goal turn starts. The ACP loop reports the field from its own flag, set only for continuations carrying a Stop hook reason and cleared for drained user input, guard continuations and superseded ones, leaving the iteration counter to the cap and continuation ids. Tests cover a tool round trip, a next-speaker continuation, the goal-bound branch, the guard reset sequence and a drained user turn. Part of #11610 * fix(core): key stop_hook_active by prompt and keep the block count per call Replace the single client-held Stop-hook chain with a set of prompt ids whose previous Stop check was blocked. stop_hook_active reads that set, so it still survives a tool round trip, while a second prompt stopping on the same client (a concurrent side question) can no longer evict or overwrite another prompt's state. An id is cleared when its stop is allowed, the cap is hit, steer input replaces the turn, the send exits abnormally, or a new interaction starts on that id (retry and goal turns reuse the prompt id). The consecutive-block count goes back to riding the per-call stopHookState, as on main, so the built-in /goal hook stays bounded by its own iteration budget. Counting blocks across tool round trips is tracked in #11673. In ACP, user input drained before or after a Stop check now also restarts the consecutive-block count, so an allow discarded by the drain no longer lets a user-driven block count as consecutive. |
||
|
|
28df8b8a78 |
feat(core): add permission_mode, agent_id and prompt_id to every hook input (#11618)
* feat(core): add permission_mode, agent_id and prompt_id to every hook input Only tool, subagent and session-start events carried permission_mode, and agent_id only appeared on SubagentStart and SubagentStop. Scripts shared across events could not branch on either. The common hook input now includes permission_mode (the session approval mode, unless the event reports its own), agent_id when the event fires inside a subagent, and prompt_id when the model turn is known. PostToolUse and PostToolUseFailure also report duration_ms, measured from the start of tool execution in both the core scheduler and ACP sessions. The three copies of the approval-mode to permission-mode mapping are replaced by one shared helper. Part of #11610 * fix(core): time hook duration_ms on the monotonic clock duration_ms on PostToolUse and PostToolUseFailure was the difference of two Date.now() readings, so a system clock step while a long tool ran could report a negative or inflated duration to audit hooks. Both the core scheduler and the ACP session now read performance.now() and round the elapsed time. The hooks guide also lists SessionStart among the events that report agent_type. |
||
|
|
c541bdffa4 |
fix(core): route plain-text hook stdout to additionalContext for prompt and session events (#11612)
* fix(core): route plain-text hook stdout to additionalContext for prompt and session events A command hook that prints plain text and exits 0 was turned into a systemMessage, which the SessionStart, UserPromptSubmit and UserPromptExpansion consumers never read and nothing displays. An `echo "context"` hook on those events was therefore a silent no-op. On those three events plain-text stdout now becomes hookSpecificOutput.additionalContext, so it reaches the model and chains into the next sequential hook like JSON context does. Other events keep the systemMessage conversion. Only stdout is promoted: the stderr fallback used when stdout is empty stays a system message, and non-zero exits keep their existing handling. Part of #11610 * fix(core): harden plain-text hook stdout promotion Review follow-up. Only a JSON object (optionally double-encoded) is structured hook output; bare JSON values such as `42` or `[1, 2]` are plain text instead of being dropped, and exit code 2 with a bare value on stderr now blocks. Output shaped like a JSON object that fails to parse is kept out of model context, as in Claude Code, with a debug-log warning. Promoted text has terminal escapes and control characters stripped per line. The comments now state the real rationale for the event set and the actual stderr behaviour, and the hooks guide row matches. Part of #11610 |
||
|
|
cbd2cbadec |
fix(acp): Preserve submitted prompt provenance for auto recall (#11455)
* fix(acp): Preserve submitted prompt provenance for auto recall Co-authored-by: Qwen-Coder <[email protected]> * fix(acp): Omit submitted_prompt on channel-classified turns (#11455) Channel turns are machine-relayed deliveries (loop jobs, webhook tasks, adapter-synthesized events), so nothing on them crossed a submission boundary. Since submitted_prompt presence alone gates Auto Recall's outbound provider search, emitting it there ran retrieval on unattended turns and could publish composed wrapper text as user provenance. Gate emission on a positive user-submission condition instead. Also reconcile the producer-set contract this PR changed: hooks.md, the Auto Recall READMEs, the in-product hook description with its locale mirrors, and the recording payload comment now state the actual rule, and the new design doc records the channel exclusion plus the strict-decoder rollout warning. * fix(acp): Require explicit submission provenance for auto recall Carry original Web Shell composer text separately through admission and queues; keep background dispatches and undeclared requests ineligible. Reconcile hook and integration contracts and add regression coverage. Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
f6ba502c9b |
fix(core): allow manual retry after auto mode blocks (#11025)
* fix(core): allow manual retry after auto mode blocks * fix(core): preserve auto mode retry guarantees --------- Co-authored-by: qqqys <[email protected]> |
||
|
|
1db18fefdb |
fix(hooks): close four trust-boundary holes in hook execution (#10427)
* fix(hooks): close four trust-boundary holes in hook execution HTTP hooks never follow redirects (a 3xx is a non-blocking failure and the target is never contacted). A workspace may narrow the HTTP-hook URL whitelist but never replace one set at User, System, or SystemDefaults scope. Qwen-internal secrets are never substituted into settings, hook URLs/headers, or channel configuration, compared case-insensitively for Windows, and the external-tool guard token joins the denylist; the CLI's duplicate settings resolver is replaced by a core subpath export so the serve fast path stays light. Project-level skill and subagent side effects (allowedTools grants and frontmatter hooks) require a trusted folder, mirroring the settings-file project-hooks gate. Replaces #8396. * fix(hooks): re-apply skill side effects when trust is granted mid-session The Skill tool's "already loaded" dedup return sat above the new trust gate, so a project skill first invoked in an untrusted folder kept its allowedTools and hooks unapplied for the whole session even after the folder was trusted. The gate is now re-evaluated on every invocation; both grants dedup, so re-applying is idempotent. Also keep an own `__proto__` variable in the sanitized child env (spread and delete instead of an assignment loop), correct the guard-token note in the denylist rationale (the ACP child never receives it), and pin the settings-layer and serve fast-path secret refusal with tests. * fix(hooks): give the project-skill trust gate a removal side The gate on a project skill's side effects was enforced only when they were applied: once a repo-supplied skill had registered its frontmatter hooks and granted its allowedTools as session allow rules, a folder trust revoked mid-session (an IDE trust notification flips it live) left both active until the session restarted. Rather than tracking which skills applied what and unregistering on a trust event, the grants are marked trust-gated and the two consumers re-read folder trust at decision time: the hook event handler skips a trust-gated session hook at fire time while the folder is untrusted, and the permission manager leaves a trust-gated session allow rule out of every decision (and the effective-rules listing) while it is. Trust granted again restores both, symmetric with the mid-session grant the gate already honoured, and nothing changes on rounds that registered no project-skill hook. The per-invocation re-application of side effects also turned two WARN lines into the normal steady state of a trusted skill — a dedup returning zero and a hookless skill — so both are debug-level now, reworded to say what the zero means. * fix(permissions): let an ungated same-raw grant clear the kept entry's trust gate The session-allow dedup kept the first entry's trustGated flag, so a user-level grant of a raw the project skill had already granted would inherit the repo grant's suspension when folder trust is revoked. The dedup now keeps the wider grant: an ungated arrival clears the flag; a gated re-arrival (skill reload) stays an idempotent skip and never re-gates a rule the user holds. Also pins the slash-command path's grant payload in the SkillCommandLoader trusted-folder test to exactly { trustGated: true }, so shipping the grants ungated reddens the suite instead of passing on call counts alone. --------- Co-authored-by: qwen-code-dev-bot <[email protected]> |
||
|
|
9c074b5651 |
fix(core): Preserve fire-and-forget hooks after exit (#10288)
* fix(core): preserve fire-and-forget hooks after exit Co-authored-by: Qwen-Coder <[email protected]> * codex: address PR review feedback (#10288) Co-authored-by: Qwen-Coder <[email protected]> * codex: address PR review feedback (#10288) Co-authored-by: Qwen-Coder <[email protected]> * codex: address PR review feedback (#10288) Co-authored-by: Qwen-Coder <[email protected]> * fix(core): make surviving hook status authoritative Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
b34a08d16f |
fix(core): separate hook context from transcript display (#7948)
* fix(core): separate hook context from transcript display * test(ci): gate desktop transcript projection * revert: keep desktop CI scope unchanged * test: cover transcript display fallbacks * fix(transcript): address review feedback * fix(transcript): reconcile post-merge provenance paths * fix(webui): preserve legacy transcript concatenation * test(transcript): cover projection consumers * fix(transcript): consolidate hook context projection * fix(transcript): support single-field display provenance * fix(transcript): strip hook context with invalid metadata * test(acp): cover empty replay display text --------- Co-authored-by: destire-mio <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]> |
||
|
|
26352fcc6a |
feat(external-context): Add optional Mem0 memory writes (#8507)
* feat(external-context): Add optional Mem0 memory writes Co-authored-by: Qwen-Coder <[email protected]> * fix(hooks): Preserve confirmation content visibility Render PreToolUse confirmation reasons literally and keep long confirmations accessible through the virtualized TUI. Add unit and interactive regression coverage for Mem0 write confirmations. Co-authored-by: Qwen-Coder <[email protected]> * fix(external-context): Address memory write review findings Align Hook and MCP argument handling, distinguish definitive Provider rejections from ambiguous outcomes, improve deployment diagnostics, and document the write-back trust boundary. Co-authored-by: Qwen-Coder <[email protected]> * fix(hooks): Refine plain-text confirmations Render URLs consistently, avoid persistent virtual viewport gaps, and document the literal-rendering and managed deployment boundaries. Co-authored-by: Qwen-Coder <[email protected]> * fix(external-context): Support Auto Edit write confirmation Co-authored-by: Qwen-Coder <[email protected]> * fix(external-context): Harden write confirmations Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): Measure virtual row height directly Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): Preserve YOLO Hook confirmation content Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]> |
||
|
|
e3479a6251 |
docs: worked example for a PreToolUse HTTP hook backed by an external judgment service (#8202)
* docs: worked example for a PreToolUse HTTP hook backed by an external judgment service The existing remote-security-check config example points at a service that has to already exist, without showing what that service actually looks like. Adds a minimal, stdlib-only, runnable adapter (invinoveritas /review as the judgment source) implementing the exact contract PreToolUse HTTP hooks expect -- verified live against the real production API, not just written to look plausible: a genuinely destructive shell command returns permissionDecision: "deny" with a real explanation, a benign one returns "allow", and the adapter fails open on any judgment-service-side error so an outage never blocks legitimate tool calls. * docs: address review feedback -- disclose affiliation, note swap point clearly * docs: fix timeout mismatch, add data-handling note, make backend URL configurable, log fail-open state --------- Co-authored-by: babyblueviper1 <[email protected]> |
||
|
|
cf547b6a3c |
feat(hooks): add SessionDelete event (#8059)
* feat(hooks): add SessionDelete event * test(hooks): cover SessionDelete failure paths * fix(hooks): standardize SessionDelete failure handling * fix(hooks): include session id in SessionDelete hook failure logs (#8059) * docs(hooks): note that transcript_path is empty over ACP for SessionDelete (#8059) * refactor(cli): deduplicate SessionDelete hook dispatch (#8059) * fix(cli): keep SessionDelete hook dispatch ACP-safe * fix(cli): align SessionDelete short description with other references (#8059) --------- Co-authored-by: 欢伯 <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]> Co-authored-by: Qwen Code Bot <[email protected]> Co-authored-by: qwen-code-ci-bot <[email protected]> |
||
|
|
953c9d8177 |
feat(core): tag UserPromptSubmit hook context and record display provenance (#7956)
* 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> --------- Co-authored-by: Cursor <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]> Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
aac663f28a |
feat(hooks): add security.allowPrivateNetworkHooks to bypass SSRF range checks for trusted scopes (#7968)
* 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: 欢伯 <[email protected]> |
||
|
|
17408f1028 |
feat(hooks): Add submitted prompt provenance (#7762)
* feat(hooks): add submitted prompt provenance Add an optional pre-expansion prompt sidecar for interactive UserQuery hooks while preserving legacy prompt behavior and fail-closed provenance handling across queues, retries, and continuations. Co-authored-by: Qwen-Coder <[email protected]> * fix(hooks): harden submitted prompt provenance Co-authored-by: Qwen-Coder <[email protected]> * fix(hooks): tighten submitted prompt provenance Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
34a3d46006 |
fix(core): fire StopFailure hook on loop detection early returns (#7592)
* 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 <[email protected]> Co-authored-by: Qwen Code Autofix <[email protected]> Co-authored-by: qwen-code-ci-bot <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> |
||
|
|
218dec6a6f |
feat(hooks): add MessageDisplay hook for mid-turn streaming (#6489)
* 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> |
||
|
|
14f02c132a |
fix(core): Match hook display-name matchers to tool ids (#6373)
* fix(core): match hook tool display names * fix(core): tighten hook matcher aliases * fix(core): align hook matcher aliases |
||
|
|
bcdb44c5d3 |
docs(hooks): document PreToolUse permissionDecision 'ask' behavior (#6411)
Co-authored-by: Claude <[email protected]> |
||
|
|
8daeb5b1f9 |
feat(core): add configurable auto-compact threshold and Stop hook context usage (#4025) (#5868)
* feat(core): add configurable auto-compact threshold and Stop hook context usage (#4025) Add two features requested in issue #4025: 1. Configurable auto-compact threshold via settings.json - Add context.autoCompactThreshold setting (0-1, default 0.7) - Extend computeThresholds(window, pct?) to accept optional pct parameter - Wire all 4 call sites (chatCompressionService, geminiChat, contextCommand, useContextualTips) - Large windows (>110K) dominated by absolute branch, custom threshold mainly affects small windows 2. Stop hook stdin payload includes context usage data - Add ContextUsageData interface and buildContextUsage helper - Extend StopInput with context_usage, context_limit, input_tokens fields - Wire 3 callers (Session.ts, client.ts, config.ts) - Enables hook scripts to observe context usage and suggest compact strategies * fix(test): add getAutoCompactThreshold mock to geminiChat.test.ts, add NaN guard to buildContextUsage * fix(review): address round 4 findings — schema constraints, Partial<ContextUsageData>, buildContextUsage validation * docs: add context.autoCompactThreshold and Stop hook context usage fields documentation * fix(review): add contextWindowSize fallback, pct clamp, doc accuracy, threshold propagation test * fix: correct warn value in contextCommand test comment * test(chatCompressionService): fix misleading pct=1 test name and assertion * fix(chatCompressionService): prevent negative warn threshold for low pct values * docs(chatCompressionService): update JSDoc warn formula to include max(0, ...) floor * test(chatCompressionService): add pct clamping tests and fix NaN handling Add tests for out-of-range pct values (-0.5, 1.5, NaN) to verify computeThresholds clamping behavior. Fix implementation to use Number.isFinite() check so NaN falls back to DEFAULT_PCT instead of propagating through Math.max(0, NaN) which yields NaN. * test(config): add MCP Stop dispatch validation tests Add tests for buildContextUsage runtime validation in MCP Stop dispatch path: - Valid numeric inputs produce correct ContextUsageData - Missing/undefined fields return undefined - String values rejected by Number.isFinite validation - Negative values return undefined Also add Number.isFinite check for contextWindowSize in buildContextUsage to properly validate MCP input types at runtime. * fix(chatCompressionService): fix TypeScript type narrowing for pct parameter Use explicit undefined check before Number.isFinite to properly narrow the number | undefined type in the ternary expression. --------- Co-authored-by: 俊良 <[email protected]> |
||
|
|
daec44a4f3 |
feat(hooks): pass original API call ID (toolCallId) to hook system (#4918)
- 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 <[email protected]> |
||
|
|
57282ebb7d |
feat(hooks): add prompt hook type with LLM evaluation support (#3388)
* implement prompt hook * resolve comment * resolve comment * resolve comment * resolve comment * fix unit test |
||
|
|
1c529e4f0a |
feat(hooks): Add TodoCreated and TodoCompleted hooks for todo lifecycle events (#3378)
* add TaskCreated and TaskCompleted * resolve comment * resolve lint * change merge logic from simple to or * resolve lint error * reslove commnent * fix i18n key mismatch and malformed imports * resolve comment |
||
|
|
07475026f6 |
fix(cli): remember "Start new chat session" until summary changes (#3308)
* 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 |
||
|
|
b5115e731e |
feat(hooks): Add HTTP Hook, Function Hook and Async Hook support (#2827)
* 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> |
||
|
|
dddb56d885 | feat: add stopFailure and postCompact (#2825) | ||
|
|
5221002831 | remove hooks experimental and refactor hook Config |