fix(core): keep the ask_user_question dialog behind allow rules and auto-approval - #10160
fix(core): keep the ask_user_question dialog behind allow rules and auto-approval#10160qqqys wants to merge 3 commits into
Conversation
…uto-approval The confirmation dialog is this tool: answers are collected through onConfirm. Approving it without the dialog does not allow the tool, it silently answers "declined" on the user's behalf, and execute() then reports "User declined to answer the questions." as a successful result. Any bare ask_user_question allow rule does exactly that — a skill's allowedTools grant (applied session-wide via applySkillAllowedTools), a permissions.allow entry, or an "always allow" answer — and once one has loaded, every later question in the session loses its dialog too. The invocation now declares requiresUserInteraction() whenever a host can show the dialog (interactive TUI, ACP / stream-json hosts), so the permission flow forces 'ask' regardless of L4 allow rules and the scheduler never auto-approves it beside a sibling. Headless runs are unchanged: nothing can prompt there, the flag stays false, and execute() keeps returning its existing non-interactive message. The three mode checks are folded into one canCollectAnswers() helper so the permission default, the flag, and execute() cannot drift apart. Tests pin the flag per mode and, at the permissionFlow level with the real tool, the real PermissionManager and the real allowedTools grant, that the grant overrides the default to allow at L4 yet the flow still yields 'ask' (and needsConfirmation is true even in YOLO), that headless still yields 'allow', and that an explicit deny rule is preserved. Follow-up to the QwenLM#10002 review finding on the ask_user_question grant. Claude-Session: https://claude.ai/code/session_01FV7i3w7egJ2kMw4AhQC38Z
|
Thanks for the PR! Template looks good ✓ Problem: observed, not theoretical — and I verified the mechanism independently in the base code: a bare Direction: aligned. This is a correctness fix to the approval flow — the dialog being skippable means the tool silently answers on the user's behalf. Claude Code's changelog shows fixes in the same problem class ("Fixed auto mode suppressing AskUserQuestion when the user or a skill explicitly relies on it"), so the area is clearly relevant. Size: core paths touched ( Approach: minimal and idiomatic — it opts the tool into the existing Risk: no high-risk-path matches from the revert-history signal. One downstream consumer needs close attention in code review: the stream-json permission controller also branches on Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题: 已观测到的 bug,不是理论问题——我在 base 代码里独立验证了机制:裸的 方向: 对齐。这是审批流的正确性修复——对话框可被跳过意味着工具在替用户默默作答。Claude Code 的 changelog 里有同一问题域的修复("Fixed auto mode suppressing AskUserQuestion when the user or a skill explicitly relies on it"),该方向显然相关。 规模: 触及核心路径( 方案: 最小且符合惯例——复用 风险: 回滚历史信号无高风险路径命中。代码审查时需重点看一个下游消费方:stream-json 权限控制器也会按 进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewThe core change is sound and I verified each claim against the base code: But the flag has one more downstream consumer the PR doesn't account for, and it breaks there: Critical — stream-json hosts lose the user's answers. In Two reasons nothing in CI catches it: the existing controller test's mock tool call carries no Non-blocking: with the flag in place, the name-based Test evidenceUnattended CI run — I do not execute PR code; the evidence below is the PR's own CI fetched via the API at the reviewed commit, plus static review. The main unit suite is still running at review time; interactive/TUI behavior is not verified here (author's Linux test report is their claim, not evidence). The finalize workflow updates the table once CI settles. Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 Sandboxed verification would settle what unit tests cannot: 中文说明代码审查核心改动是成立的,我对照 base 代码逐条验证了 PR 的声明: 但这个标志还有一个 PR 没有考虑到的下游消费方,并且在那里出了问题: Critical — stream-json 宿主会丢失用户的回答。 CI 抓不到它的两个原因:现有控制器测试的 mock 调用不带 非阻塞:有了该标志后, 测试证据无人值守 CI 运行——不执行 PR 代码;以下证据来自评审提交点上通过 API 获取的 PR 自身 CI,以及静态审查。评审时主单测套件仍在运行;交互/TUI 行为未在此验证(作者的 Linux 测试报告是其自述,不是证据)。finalize 工作流会在 CI 落定后更新表格。 (表格见上方机器可读区块) 沙箱验证能补上单测补不了的部分: — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 2/5 — right fix, right mechanism, genuinely good tests, but it regresses the stream-json answer channel and cannot ship as-is. Stepping back: my independent proposal for this problem was exactly what this PR does — opt the tool into What sinks this round is the thing the gate exists to catch: naming every downstream consumer of a core-path flag. Requesting changes on the stream-json regression. 🙏 中文说明置信度:2/5 —— 修复方向正确、机制正确、测试确实写得好,但它让 stream-json 回答通道回归,现状不能合入。 退一步看:我对这个问题的独立方案与本 PR 完全一致——让工具接入为它而建的 让这一轮失败的是门槛检查本该抓住的事:点名核心路径标志的每一个下游消费方。 就 stream-json 回归请求修改。🙏 — Qwen Code · qwen3.8-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Needs some rethinking — the stream-json control path drops the user's answers once the flag flips on (see the Critical finding in my review comment above). The fix is small: route the confirmation payload through the requiresUserInteraction allow-branch in permissionController.handleOutgoingPermissionRequest, and add a controller-level regression test. Everything else looks ready. 🙏
|
已修复 stream-json 回答丢失: 验证:controller 29/29、相关 core 47/47、全仓 build/typecheck、目标 ESLint/Prettier 均通过;真实 |
|
@qwen-code /takeover |
|
🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): "agent 6a": did not execute the new/changed unit tests (permissionFlow.test.ts, askUserQuestion.test.ts, permissionController.test.ts) — the worktree has no node_modules /….
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):"agent 6a":did not execute the new/changed unit tests (permissionFlow.test.ts, askUserQuestion.test.ts, permissionController.test.ts) — the worktree has no node_modules /…。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| private canCollectAnswers(): boolean { | ||
| const isAcpMode = | ||
| this._config.getExperimentalZedIntegration() || | ||
| this._config.getInputFormat() === InputFormat.STREAM_JSON; | ||
| return this._config.isInteractive() || isAcpMode; | ||
| } |
There was a problem hiding this comment.
[Suggestion] canCollectAnswers() is a fifth copy of the host-capability predicate. resolveInteractionMode(config) !== 'headless' in packages/core/src/core/prompts.ts already expresses exactly this ("is there a host that can put the questions in front of the user"), and config.ts already uses it (supportsUserInteraction) to gate the registration of ask_user_question itself — so the registration gate and this runtime gate are two separately written copies of the same predicate. If a new answerable host or input format is added, config.ts and the scheduler copies (enterPlanMode.ts, coreToolScheduler.ts) can be updated while canCollectAnswers() is missed — or vice versa: the tool then registers and the system prompt encourages asking, but getDefaultPermission() returns 'allow', requiresUserInteraction() returns false, and execute() refuses with "Cannot ask user questions in non-interactive mode without ACP support" — the exact drift resolveInteractionMode's own comment warns about. Delegating keeps one source of truth (needs the resolveInteractionMode import from ../core/prompts.js):
| private canCollectAnswers(): boolean { | |
| const isAcpMode = | |
| this._config.getExperimentalZedIntegration() || | |
| this._config.getInputFormat() === InputFormat.STREAM_JSON; | |
| return this._config.isInteractive() || isAcpMode; | |
| } | |
| private canCollectAnswers(): boolean { | |
| return resolveInteractionMode(this._config) !== 'headless'; | |
| } |
If you apply this, run the requiresUserInteraction suite in src/tools/askUserQuestion.test.ts as the mutation check — it pins the mode truth table (interactive / stream-json / Zed → true, headless → false) and goes red if the delegation breaks that equivalence.
中文说明
canCollectAnswers() 是"宿主能否弹出对话框"这一谓词的第五份拷贝。packages/core/src/core/prompts.ts 里的 resolveInteractionMode(config) !== 'headless' 已经精确表达了同一语义,而且 config.ts 已经用它(supportsUserInteraction)来控制 ask_user_question 本身的注册——注册开关与这里的运行时开关现在是同一谓词的两份独立实现。将来新增一种可作答的宿主或输入格式时,config.ts 和调度器里的拷贝(enterPlanMode.ts、coreToolScheduler.ts)可能更新了而 canCollectAnswers() 被漏掉(或反过来):工具照常注册、系统提示词鼓励提问,但 getDefaultPermission() 返回 'allow'、requiresUserInteraction() 返回 false、execute() 报"Cannot ask user questions in non-interactive mode without ACP support"——正是 resolveInteractionMode 自身注释所警告的漂移。委托给它即可保持单一事实来源(需要从 ../core/prompts.js 导入 resolveInteractionMode)。
若应用该修改,请把 src/tools/askUserQuestion.test.ts 的 requiresUserInteraction 套件作为变异检查重新运行——它锁定了模式真值表(交互 / stream-json / Zed → true,headless → false),一旦委托破坏等价性就会变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| if ( | ||
| requiresUserInteraction && | ||
| toolCall.request.name === ToolNames.EXIT_PLAN_MODE | ||
| ) { |
There was a problem hiding this comment.
[Suggestion] This name check is the whole fix of the stream-json answer-drop regression — the unscoped if (requiresUserInteraction) at the base commit confirmed with no payload, silently discarding the answers the SDK host had just collected — yet nothing in the code says why exit_plan_mode alone takes the payload-less branch. The rationale currently lives only in two tests, so a future maintainer "simplifying" the seemingly redundant toolCall.request.name === ToolNames.EXIT_PLAN_MODE condition re-broadens the guard and drops stream-json answers again — the exact regression this PR fixes. Conversely, someone adding a third outcome-only interactive tool has no in-code signal about which path it must take, and host updatedInput can silently overwrite the args the user approved. A short comment pins the contract:
| if ( | |
| requiresUserInteraction && | |
| toolCall.request.name === ToolNames.EXIT_PLAN_MODE | |
| ) { | |
| // exit_plan_mode approves through the dialog alone: its onConfirm takes | |
| // no payload and the approved plan must not be replaced by the host's | |
| // updatedInput. Any other requiresUserInteraction tool (e.g. | |
| // ask_user_question) must take the updatedInput path below — that | |
| // channel carries the user's answers. | |
| if ( | |
| requiresUserInteraction && | |
| toolCall.request.name === ToolNames.EXIT_PLAN_MODE | |
| ) { |
中文说明
这个工具名判断正是 stream-json 回答丢失回归的全部修复——base 提交上未加限定的 if (requiresUserInteraction) 会不带 payload 直接确认,悄悄丢掉 SDK 宿主刚收集到的答案——但代码里没有任何地方说明为什么只有 exit_plan_mode 走这条不带 payload 的分支。理由目前只存在于两个测试里:未来某位维护者"简化"这个看似冗余的 toolCall.request.name === ToolNames.EXIT_PLAN_MODE 条件,就会重新放宽守卫、再次丢掉 stream-json 的答案——正是本 PR 修复的回归。反过来,若有人新增第三个"只要结果"的交互工具,也没有任何代码内信号告诉它该走哪条路径,宿主的 updatedInput 可能悄悄覆盖用户批准的参数。一条简短注释即可锁定该约定。
— qwen3.8-max via Qwen Code /review (v0.22.2)
…Mode (QwenLM#10160) canCollectAnswers() was a fifth copy of the host-capability predicate; delegating to resolveInteractionMode keeps one source of truth shared with the tool-registration gate. Also pin the stream-json payload contract in permissionController with a comment: only exit_plan_mode may approve through the payload-less branch.
|
🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下: Round summaryAddressed both new inline suggestions from the re-review at sha Feedback points and dispositions
No base conflict ( Verification
中文说明本轮摘要已处理 sha 反馈点及处理结论
无 base 冲突( 验证
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
— qwen3.8-max via Qwen Code /review (v0.22.2)
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix round for PR #10160 — no code changesFeedback triaged
Action taken: closed the disclosed gap by running the named suite locallyModel credentials equivalent to the merge-queue job's are available in this environment, so the exact suite the review named was run against a bundle rebuilt from the current HEAD:
The single failure is environment-specific and pre-existing (would fail identically on
|
What this PR does
ask_user_questionnow declaresrequiresUserInteraction()whenever a host can actually show its dialog (interactive TUI, or an ACP / stream-json host), so the question dialog can no longer be skipped by a permission rule or an automatic approval mode. The permission flow already forces'ask'for interaction-required invocations regardless of L4 allow rules, and the scheduler already refuses to auto-approve them alongside a sibling — this change simply opts the tool into that path. Headless runs are unchanged: nothing can prompt there, the flag staysfalse, andexecute()keeps returning its existing "cannot ask user questions in non-interactive mode" message. The three mode checks in the tool are folded into onecanCollectAnswers()helper so the permission default, the interaction flag, andexecute()cannot drift apart again.Two layers of regression tests are added: tool-level (
requiresUserInteraction()istrueinteractively and for ACP hosts,falseheadless) and flow-level with the realAskUserQuestionTool, the realPermissionManager, and the exactapplySkillAllowedToolsgrant a skill'sallowedToolsapplies — asserting that the grant does override the default toallowat L4 yetevaluatePermissionFlowstill yields'ask'andneedsConfirmationistrueeven in YOLO, that headless still yields'allow', and that an explicit deny rule is preserved.Why it's needed
The confirmation dialog is this tool: its answers are collected through
onConfirm. Approving it without the dialog does not "allow" the tool, it silently answers "declined" on the user's behalf, and the tool then reportsUser declined to answer the questions.as a successful result. Any bareask_user_questionallow rule triggers exactly that — a skill'sallowedToolsgrant (applied as a session-wide allow rule viaapplySkillAllowedTools→addSessionAllowRule), apermissions.allowentry, or an "always allow" answer. The review of #10002 caught this live: with the grant loaded the scheduler wentvalidating → scheduled → executing → successwith noawaiting_approval, and the skill drafted on a fabricated refusal; the fix there was to drop the grant from that one skill, and the reviewer flagged the root cause as a maintainer call. Two bundled skills onmainstill carry the same grant (batch,extension-creator), and once any skill with it has loaded, every laterask_user_questionin that session loses its dialog too — including the ACP and Web Shell paths. The fabricated "declined" also lands in the transcript as evidence, which the Goal verifier is designed to trust.Reviewer Test Plan
How to verify
cd packages/core && npx vitest run src/tools/askUserQuestion.test.ts src/core/permissionFlow.test.ts src/tools/exitPlanMode.test.ts src/core/coreToolScheduler.test.ts— all pass; the three newpermissionFlowcases and three newaskUserQuestioncases are the regression./batchor/extension-creator(both shipallowedTools: [ask_user_question]) and drive them to a clarifying question. Before: the question never appears and the model continues with "User declined to answer the questions." After: the question dialog renders; answering feeds the answers back.-y): same — the question dialog still appears (YOLO already special-cased this tool by name; the flag now covers rule-based allows the same way).qwen -p "Use the ask_user_question tool to ask me which framework I prefer"behaves exactly as before — in a plain-prun the tool is not even registered (the model reportsNo tools found matching 'keyword:ask_user_question'), and thefalsebranch of the flag for non-interactive, non-ACP configs is pinned by the new unit tests; no new "requires user approval but cannot execute in non-interactive mode" warning.Evidence (Before & After)
Before (from the #10002 review probe, real
PermissionManager+CoreToolScheduler, grant loaded from a SKILL.md):WITH grant: L3 'ask' -> L4 'allow'; scheduler [validating, scheduled, executing, success] (no awaiting_approval, dialog never shown) -> tool result: "User declined to answer the questions." (executionStatus: success).After — the same grant, through
evaluatePermissionFlow(new testkeeps the dialog when a skill's allowedTools grant would otherwise allow the tool):defaultPermission: 'ask',pm.evaluate(ctx): 'allow',requiresUserInteraction: true,finalPermission: 'ask',needsConfirmation(…, YOLO, …): true. Headless counterpart:requiresUserInteraction: false,finalPermission: 'allow'.Tested on
Environment (optional)
Linux, Node 22,
npm ci+npm run build+npm run bundle; coretsc --noEmitclean; 460 core tests (askUserQuestion, permissionFlow, exitPlanMode, coreToolScheduler) and 832 cli tests (permissionController, permissionUtils, Session, gemini) pass; headless-prun for step 4 against DashScopeqwen3.8-max.Risk & Scope
hideAlwaysAllowand skips them inautoApproveCompatiblePendingTools; for this tool both are the desired behavior (there is no meaningful "always allow" for a question, and approving a sibling must not answer a question). In AUTO mode the classifier fast path is skipped for this tool — it already fell back to the dialog for it.ask_user_questionentries inbatchandextension-creatorSKILL.mdare left in place — with this change they are inert rather than harmful, and removing them is a separate cleanup. No change to ACP hosts' handling of the confirmation request; they already received'ask'for this tool.permissions.allowrule forask_user_questionno longer suppresses the dialog. That rule never produced answers, only silent refusals, so nothing that worked before stops working.Linked Issues
Follow-up to #10002 (review finding on the
ask_user_questiongrant).中文说明
这个 PR 做了什么
ask_user_question现在在宿主能真正弹出对话框的场景(交互式 TUI,或 ACP / stream-json 宿主)下声明requiresUserInteraction(),因此它的提问对话框不再会被权限规则或自动审批模式跳过。权限流程本来就会对"需要用户交互"的调用强制返回'ask'(无视 L4 的 allow 规则),调度器也本来就不会把它随同级工具一起自动批准——本改动只是让这个工具走上这条路径。headless 运行不变:那里没有任何东西能弹窗,标志保持false,execute()仍返回原有的"非交互模式下无法向用户提问"消息。工具里三处模式判断合并成一个canCollectAnswers()辅助函数,避免权限默认值、交互标志和execute()再次各自漂移。新增两层回归测试:工具级(交互和 ACP 宿主下
requiresUserInteraction()为true,headless 下为false),以及 permissionFlow 级——用真实的AskUserQuestionTool、真实的PermissionManager和 skill 的allowedTools实际施加的applySkillAllowedTools授权,断言该授权确实在 L4 把默认值覆盖成了allow,但evaluatePermissionFlow仍然给出'ask'、即使在 YOLO 下needsConfirmation也为true;headless 仍为'allow';显式 deny 规则保留。为什么需要
确认对话框就是这个工具本身:答案是通过
onConfirm收集的。不弹对话框就"批准"它,并不是允许了这个工具,而是替用户默默回答了"拒绝",工具随后把User declined to answer the questions.当作成功结果返回。任何裸的ask_user_questionallow 规则都会触发这一点——skill 的allowedTools授权(通过applySkillAllowedTools→addSessionAllowRule以会话级 allow 规则施加)、permissions.allow条目、或一次"总是允许"。#10002 的评审现场抓到了这个问题:加载授权后调度器直接validating → scheduled → executing → success,没有awaiting_approval,skill 基于伪造的拒绝继续起草;当时的修法是从那一个 skill 里去掉授权,评审把根因标为维护者决策。main上还有两个内置 skill 带着同样的授权(batch、extension-creator),而且一旦任何带此授权的 skill 加载过,该会话里之后所有ask_user_question的对话框都会失效——包括 ACP 和 Web Shell 路径。伪造的"拒绝"还会作为证据留在 transcript 里,而 Goal verifier 正是被设计成信任 transcript 的。Reviewer 测试计划
如何验证
cd packages/core && npx vitest run src/tools/askUserQuestion.test.ts src/core/permissionFlow.test.ts src/tools/exitPlanMode.test.ts src/core/coreToolScheduler.test.ts——全部通过;permissionFlow新增的三个用例和askUserQuestion新增的三个用例就是回归测试。/batch或/extension-creator(两者都带allowedTools: [ask_user_question]),推进到一个澄清提问。Before:提问永远不出现,模型以"User declined to answer the questions."继续。After:提问对话框渲染出来;回答后答案回传。-y):同上——提问对话框仍然出现(YOLO 本来就按工具名特判了它;现在标志以同样方式覆盖了基于规则的 allow)。qwen -p "Use the ask_user_question tool to ask me which framework I prefer"行为与之前完全一致——普通-p运行里该工具根本没有注册(模型报告No tools found matching 'keyword:ask_user_question'),非交互、非 ACP 配置下标志为false的分支由新增单元测试锁定;没有新增"requires user approval but cannot execute in non-interactive mode"警告。证据(Before & After)
Before(来自 #10002 评审探针,真实
PermissionManager+CoreToolScheduler,授权从 SKILL.md 加载):WITH grant: L3 'ask' -> L4 'allow'; scheduler [validating, scheduled, executing, success](无 awaiting_approval,对话框从未显示)-> 工具结果:"User declined to answer the questions."(executionStatus: success)。After——同样的授权经过
evaluatePermissionFlow(新测试keeps the dialog when a skill's allowedTools grant would otherwise allow the tool):defaultPermission: 'ask'、pm.evaluate(ctx): 'allow'、requiresUserInteraction: true、finalPermission: 'ask'、needsConfirmation(…, YOLO, …): true。headless 对照:requiresUserInteraction: false、finalPermission: 'allow'。测试平台
Linux ✅;macOS、Windows⚠️ 未测试。
环境(可选)
Linux,Node 22,
npm ci+npm run build+npm run bundle;coretsc --noEmit干净;core 460 个测试(askUserQuestion、permissionFlow、exitPlanMode、coreToolScheduler)与 cli 832 个测试(permissionController、permissionUtils、Session、gemini)通过;第 4 步为一次针对 DashScopeqwen3.8-max的 headless-p运行。风险与范围
hideAlwaysAllow并在autoApproveCompatiblePendingTools中跳过它们;对这个工具而言两者都是期望行为(对提问不存在有意义的"总是允许",批准同级工具也不应替用户回答问题)。AUTO 模式下会跳过分类器快速路径——它对该工具本来就回落到对话框。batch和extension-creator的SKILL.md里的ask_user_question条目保留不动——有了本改动它们从有害变成无效,移除属于另一次清理。不改变 ACP 宿主对确认请求的处理;它们对该工具本来就收到'ask'。permissions.allow里针对ask_user_question的规则不再能抑制对话框。这条规则从来没产生过答案、只产生过静默拒绝,所以之前能工作的东西没有一样会停止工作。关联 Issue
#10002 的后续(评审对
ask_user_question授权的发现)。https://claude.ai/code/session_01FV7i3w7egJ2kMw4AhQC38Z