Repository navigation
feat(web-shell): show and answer Hosted tool approvals in the Managed panel - #13107
Conversation
… panel
The Managed panel passed pendingApproval={null}, so a Hosted Session
waiting on a D6a approval showed nothing to answer. Build on the D6b
Actions contract (#13101):
- The provider gains an optional Actions reader. The Java provider lists
requested permission Actions through actions/query and answers through
actions/respond with the Action's inputRevision and policyRevision and
a per-Action, per-option idempotency key. The Session summary reports
the actions capability only when the service does.
- action.updated projects to action_updated. It carries no Turn, so the
transcript skips it instead of settling the Turn being streamed.
- useManagedActions re-reads pending approvals on action_updated, on a
stream gap, after an expiry and after an answer. It hides an answered
approval and brings it back if the answer fails.
- The page maps the pending Action onto the shared approval card, with
allow/deny as allow_once/reject_once so labels are localized, and
attaches it to the tool row `${turnId}:${functionCallId}` so the row
expands and shows its arguments.
Not built, type-checked, linted or tested locally.
…e contract to 1.25 Merging main brought in #12946's V23__managed_mcp_records.sql next to this PR's V23__managed_actions.sql; Flyway refuses two migrations with the same version, so the server would not start. The Actions migration becomes V24 with its content unchanged. The contract takes 1.25.0 on top of main's 1.24.0 (#12946), with a v1.25 note for the Actions routes this PR serves.
Picks up #13101's renumbering of the Actions migration to V24. The branch still carried V23__managed_actions.sql, which collides with main's V23__managed_mcp_records.sql in CI's merge with main.
…nto feat/12867-webshell-approvals
…stable The conditional selected a fresh empty array on every render, so the useCallback dependency changed identity each time and ESLint's react-hooks/exhaustive-deps warning failed the lint gate.
Real-stack verification —
|
| Claim | Observed |
|---|---|
| Card appears while the page is open | Arrives through the event stream, no reload; the page made 4 actions/query calls in the whole scenario |
| Card appears on page load | 15 ms after load |
| Allow | One actions/respond (202), key <actionId>:allow, revisions taken from the Action; operation completed; file written; exactly 1 tool execution |
| Reject | Same with deny; no file, 0 executions; the model's tool result is "The Session owner denied this tool call, so it was not run." |
| Expiry | Card leaves 311 ms after expiresAt; 2 actions/query calls over the 30 s wait, so no polling; the model is told nobody answered |
| Other viewers | A second tab of the owner drops the card 87 ms after the answer; an answer through the public REST API removes it in 185 ms |
| Failed answer | 503 once: card comes back with the failure text, the retry reuses the same key and succeeds, the text clears |
| Retried click replays | Reply lost after the service committed it: clicking the same option returns replayed: true; one operation, the tool ran once |
| Localized labels | zh-CN shows 写入文件 / 拒绝 / 是,允许一次 |
| Several approvals in one Turn | write → edit, and three calls in one model message (allow / reject / allow): each answer hit a different Action and the files on disk match the clicks |
| Real model | qwen3.8-max, plain-language request: allow writes the file with the requested line; reject leaves no file and the model reports the denial |
107 of 118 checks pass. The 11 misses are F1 (10) and N3 (1).
F1 — the owner answers without seeing the call
- The rig database holds 30 Sessions and 55 approvals from these runs (scripted and real model). Its 58 Items are all of type
message: 0tool_callItems and 0item.tool_call.updatedevents, while an approval is pending and after the Turn finishes. - Why: the Hosted Harness only emits
agent_message_chunkupdates (hosted-harness-session.ts:348), and Java creates tool Items only fromtool_call/tool_call_updateHarness events (HarnessEventProjector.java:55). The D6 design says "Arguments come from Items", but Hosted Turns put no function call into Items. - Effect here:
findManagedApprovalToolnever finds a row andrawInputis never set. The "main risk" in the description (the Action'sfunctionCallIdequals the tool Item'stoolCallId; "both hold on the feat(managed-agent): Serve durable permission Actions (D6b) #13101 head") cannot be confirmed or refuted by real data, because there is no tool Item to compare with. Only unit fixtures exercise that matching. - A batch makes it concrete: three
write_filecalls in one model message produce three identical cards, so the owner cannot tell which file each answer is for.
Ways forward, for the maintainers to pick:
- Merge this as the answering plumbing and track argument display on feat(managed-agent): Stage D follow-ups for durable lifecycle, Turns, Actions, durable admission and AgentDefinition #12867 before
default/auto-editis offered to users. - Hold it until the Harness publishes the function call when it commits the assistant message (before it asks), so Java projects a tool Item and this PR's matching has something to match.
- Either way, correct the description, and consider a line on the card saying the arguments are not available.
I would not block the merge on F1, but I would not call the approval flow usable until it is closed: approving a write or a command the owner cannot read defeats the purpose of asking.
Smaller notes
- N1 — a reader gets a card they cannot use. An actor who may read the Session but did not create it sees the same card. Answering returns 403
action_forbidden, which the page renders as "The approval answer could not be sent. Try again." Retrying can never work for that viewer. - N2 — lost reply, then the other option. If the reply to "allow" is lost after the service committed it and delivery to the Harness is slow (8 s injected), the card returns with the same "could not be sent" text. Clicking "Reject" is then accepted (202), fails server-side with
action_already_resolved, the tool runs, and the page shows nothing. Narrow, and the first answer was a real one; it is the wording that misleads. - N3 — browser clock ahead of the expiry. The expiry re-read uses the browser's
Date.now(). With the browser clock pastexpiresAt, the page callsactions/queryabout once a second for the whole wait (11 calls in 12 s, against 0 with the clock in step). - N4 — test gaps. 31 source mutants of the new client code: the PR's tests kill 21. Not pinned: the re-read after expiry, hiding an answered approval while it settles, state leaking across a Session switch, reporting a failed list read, matching the Action's own Turn when a later Turn reuses the call ID, and rendering the failure text.
candidate-tests.patchadds five tests (+127 lines, tests only; eslint, prettier and tsc clean, 91/91) and brings that to 28. The three left are thestream_gaptrigger, a constantinputRevision(it is always 1 today), and passingpendingApprovalto the mockedMessageList. - N5 — the test plan cannot be followed from the panel. "Create a Workspace-bound Session, and ask for a file write" is not possible in the Web Shell: the Workspace creator sends
input: []and the composer is hidden on Workspace Sessions. I created the Sessions throughsessions/createwithinputandworkspace. Those Sessions also show "Message execution is not available in this service yet" while a Turn is executing. Both predate this PR.
Gates
- At
b784842050, in a clean worktree: eslint--max-warnings 0, prettier,tsc --noEmitand the web-shell build all exit 0; unit tests 86/86 inclient/components/managedand 10,443/10,443 inpackages/web-shell. - The earlier red CI is resolved. On
badf94c4aeI reproduced theLint & Staticfailure (react-hooks/exhaustive-depsatuse-managed-actions.ts:85) and found a Prettier failure inManagedSessionsPage.test.tsxthat the failed ESLint step was hiding;89a14fd38candb784842050fix both. The two red Java jobs were the Flyway V23 collision from feat(managed-agent): Serve durable permission Actions (D6b) #13101, fixed there by V24. - CI on this head: 19 checks pass,
web-shell E2E Smokewas still pending when I posted this.
Not covered
- Shell approvals. The rig's Harness offered
read_file,write_fileandeditonly, so the exec-style card was not exercised. auto-editmode, question Actions, and the stream-gap path.- Linux and Windows. Java and the Harness were not rebuilt at
b784842050(see Setup).
Unrelated to this PR, seen on the way: one model message with 22 tool calls leaves the Turn recovery-blocked with commit.commandId exceeds 512 UTF-8 bytes. It happens in yolo mode too, so it has nothing to do with approvals; 8 calls work.
Evidence (figures, scenario logs, rig scripts, probes): assets-pr13107 @ 81aeca90
中文版本
真实环境验证 — b784842050
结论。 在真实环境里,审批卡片的行为与 PR 描述一致:卡片会出现,owner 可以作答,"是,允许一次"后 Turn 继续执行,"拒绝"会作为拒绝结果交给模型。脚本模型和 qwen3.8-max 都验证过。本 PR 自身的代码里没有发现应当阻止它在 #13101 之后合入的问题。
标记 ready 之前需要先定一件事(F1):卡片上看不到被审批的内容。 PR 描述写的是卡片挂在工具行上,由工具行提供调用参数。真实环境里这一行不存在,owner 只能看到 WriteFile / write_file。原因不在本 PR,但 PR 描述的说法与实际不符。
环境
MySQL 8.4.7、内嵌 Runtime Broker 的 Spring 服务 jar(JDK 21)、打包后的 Hosted Harness(dist/cli.js),审批模式 default,开启 Workspace 文件。Managed 面板是真实的 ManagedAgentWebShell,由 vite 直接从 PR 源码提供,用无头 Chromium 驱动。对照臂是 #13101 head 358212a8b2 的同一页面,连同一个服务。
验证期间 head 变了两次(badf94c4ae → cd7d6c61ab → b784842050)。下面所有数字都来自 b784842050。Java 和 Harness 由 cd7d6c61ab 构建;两个 head 只在 packages/web-shell/client 下的两个文件上有差异(其余 0 行差异)。
前后对比
同一个等待 write_file 审批的 Session,两个页面分别打开:见上方第二张图。对照页没有任何可作答的内容,也从不请求 actions/query;30 秒后审批过期,模型被告知无人作答。PR 页面在加载后 15 ms 显示卡片。
成立的部分
| 主张 | 实测 |
|---|---|
| 页面打开期间出现卡片 | 由事件流触发,无需刷新;整个场景页面只发了 4 次 actions/query |
| 页面加载时出现卡片 | 加载后 15 ms |
| 允许 | 一次 actions/respond(202),幂等键 <actionId>:allow,revision 取自 Action;operation 完成;文件写入;恰好 1 次工具执行 |
| 拒绝 | 同上,选项为 deny;没有文件、0 次执行;模型收到"The Session owner denied this tool call, so it was not run." |
| 过期 | expiresAt 之后 311 ms 卡片消失;30 秒等待期间只有 2 次 actions/query,没有轮询;模型被告知无人作答 |
| 其他查看者 | owner 的第二个标签页在作答后 87 ms 撤下卡片;通过公开 REST API 作答后 185 ms 撤下 |
| 作答失败 | 服务返回一次 503:卡片恢复并显示失败提示,重试使用同一个幂等键并成功,提示消失 |
| 重复点击重放 | 服务已提交但响应丢失:再点同一选项返回 replayed: true;只有一个 operation,工具只执行一次 |
| 文案本地化 | zh-CN 下显示 写入文件 / 拒绝 / 是,允许一次 |
| 一个 Turn 内多次审批 | write → edit,以及一条模型消息里的三个调用(允许 / 拒绝 / 允许):每次作答对应不同的 Action,磁盘上的文件与点击一致 |
| 真实模型 | qwen3.8-max,自然语言请求:允许后文件按要求写入;拒绝后没有文件,模型说明被拒绝 |
118 项检查通过 107 项。未通过的 11 项是 F1(10 项)和 N3(1 项)。
F1 — owner 看不到调用内容就要作答
- 装置数据库里有这些运行留下的 30 个 Session、55 次审批(脚本模型和真实模型都有)。58 个 Item 的类型全部是
message:tool_callItem 为 0,item.tool_call.updated事件为 0,审批等待期间和 Turn 结束之后都是如此。 - 原因:Hosted Harness 只发出
agent_message_chunk更新(hosted-harness-session.ts:348),而 Java 只从 Harness 的tool_call/tool_call_update事件生成工具 Item(HarnessEventProjector.java:55)。D6 设计写的是"参数从 Items 读取",但 Hosted Turn 不会把函数调用放进 Items。 - 对本 PR 的影响:
findManagedApprovalTool永远找不到工具行,rawInput永远不会被设置。PR 描述里的"主要风险"(Action 的functionCallId等于工具 Item 的toolCallId,"两者在 feat(managed-agent): Serve durable permission Actions (D6b) #13101 的 head 上都成立")在真实数据上既无法证实也无法证伪,因为没有工具 Item 可比。这段匹配逻辑只有单元测试夹具在走。 - 批量调用最直观:一条模型消息里的三个
write_file调用会依次弹出三张完全相同的卡片,owner 分不清每次作答对应哪个文件(上方第四张图)。
可选做法,由维护者决定:
- 把本 PR 当作"作答通路"合入,在向用户开放
default/auto-edit之前,在 feat(managed-agent): Stage D follow-ups for durable lifecycle, Turns, Actions, durable admission and AgentDefinition #12867 上跟进参数展示。 - 先不合,等 Harness 在提交 assistant 消息时(询问之前)发布函数调用,让 Java 投影出工具 Item,本 PR 的匹配逻辑才有对象。
- 无论选哪种,都应更正 PR 描述,并考虑在卡片上注明"参数暂不可见"。
我不会因为 F1 阻止合入,但在它解决之前,不应认为审批流程可用:owner 读不到内容就批准写文件或执行命令,询问就失去了意义。
次要问题
- N1 — 只读者看到一张用不了的卡片。 能读取 Session 但不是创建者的 actor 会看到同样的卡片。作答返回 403
action_forbidden,页面显示为"审批回答未能发送,请重试。"对这个查看者来说,重试永远不会成功。 - N2 — 响应丢失后改选另一项。 "允许"已被服务提交但响应丢失,且投递到 Harness 较慢(注入 8 秒)时,卡片带着同样的"未能发送"提示恢复。此时点"拒绝"会被受理(202),随后在服务端以
action_already_resolved失败,工具照常执行,页面没有任何提示。场景很窄,而且第一次作答确实是 owner 给的;问题在于提示文字有误导。 - N3 — 浏览器时钟早于服务端。 过期后的重读用的是浏览器的
Date.now()。浏览器时钟超过expiresAt时,页面在整个等待期间大约每秒请求一次actions/query(12 秒 11 次;时钟同步时为 0 次)。 - N4 — 测试缺口。 对新增客户端代码做了 31 个源码变异,PR 自带测试杀掉 21 个。没有被钉住的行为:过期后的重读、作答结算期间隐藏卡片、切换 Session 后状态残留、列表读取失败的上报、后续 Turn 复用 call ID 时仍匹配 Action 所在 Turn、失败提示的渲染。
candidate-tests.patch增加 5 个测试(+127 行,只改测试;eslint、prettier、tsc 均通过,91/91),杀掉数升到 28。剩下 3 个是stream_gap触发、恒定的inputRevision(目前恒为 1)、以及传给被 mock 的MessageList的pendingApproval。 - N5 — 测试计划无法在面板里完成。 "创建绑定 Workspace 的 Session 并请求写文件"在 Web Shell 里做不到:Workspace 创建器发送的是
input: [],Workspace Session 上输入框被隐藏。我是通过sessions/create同时带input和workspace创建的。这些 Session 在 Turn 执行期间还会显示"当前服务暂未开放消息执行"。两点都早于本 PR。
门禁
b784842050,干净 worktree:eslint--max-warnings 0、prettier、tsc --noEmit、web-shell 构建全部 exit 0;单元测试client/components/managed86/86,packages/web-shell10,443/10,443。- 之前的 CI 红灯已解决。我在
badf94c4ae上复现了Lint & Static失败(use-managed-actions.ts:85的react-hooks/exhaustive-deps),并发现ManagedSessionsPage.test.tsx还有一个被失败的 ESLint 步骤挡住的 Prettier 失败;89a14fd38c和b784842050已分别修复。两个 Java 红灯是 feat(managed-agent): Serve durable permission Actions (D6b) #13101 带来的 Flyway V23 撞号,已在那边改为 V24。 - 当前 head 的 CI:19 项通过,发帖时
web-shell E2E Smoke仍在运行。
未覆盖
- Shell 审批。装置里的 Harness 只提供
read_file、write_file、edit,命令类卡片没有走到。 auto-edit模式、问题类 Action、事件流断流路径。- Linux 和 Windows。Java 与 Harness 没有在
b784842050上重新构建(见"环境")。
与本 PR 无关、顺带看到的:一条模型消息带 22 个工具调用时,Turn 会以 commit.commandId exceeds 512 UTF-8 bytes 进入 recovery blocked。yolo 模式下同样出现,所以与审批无关;8 个调用正常。
证据(图、场景日志、装置脚本、探针):assets-pr13107 @ 81aeca90
yiliang114
left a comment
There was a problem hiding this comment.
Reviewed the WebShell changes at b784842. I reproduced the read-error behavior below using the actual Java provider and both React hooks with a mocked one-off 503; manual Refresh recovered the card. This is a non-blocking recovery issue.
|
Review follow-up at
Validation on macOS: repository Scope ledger: two substantive fix rounds ( Current bounded pass at 本轮仅处理已由独立真实页面/Java HTTP 探测复现的 R1-1:刷新与摘要接口故障错误撤下已确认可读的审批。保留初始未知、明确无能力与切换 Session 的隔离边界;其余12条建议依约五轮 review 后的范围规则另行记录。 |
A transient failure of actions/query on the first read left a pending approval invisible until a manual Refresh: summary polls do not re-read Actions, and without an Action there is no expiry timer. The page then showed "The approval answer could not be sent" although nothing was sent. - useManagedActions reports loadError and answerError separately, and retries a failed read after 2, 5 and 10 seconds before giving up. retry() reads again on demand and restarts the bound. - The page shows "Pending approvals could not be loaded." with a Retry button for a read failure, and keeps the answer message for a failed answer. - Tests cover the automatic recovery, the retry bound and the manual retry.
yiliang114
left a comment
There was a problem hiding this comment.
Reviewed the PR-owned delta at 4fbe5025: the 16 WebShell files above the inherited D6b head (358212a8b2), plus the Java/OpenAPI contract those files depend on. 4fbe5025 closes the failed-read retry from the earlier review comment (split loadError/answerError, bounded re-reads, manual Retry).
No merge blocker in this PR's own code. Three findings, details inline:
- [P2] The answer's outcome is never read.
actions/respondreturns the durable operation, and a 202 body already carriesstate: failedpluserrorCodefor an operation that ended that way; the client drops it. Withansweredcleared only on a Session switch, an answer whose operation failed (delivery failure,action_already_resolved,action_expired) hides the card and shows nothing — the mechanism behind N2's "the page shows nothing". - [P2, test] The test named for the hide-on-answer behaviour passes for the wrong reason: its second
listPendingresolves[], so theansweredfilter it is supposed to cover is never exercised. The two new retry tests are good. - [P3] The expiry re-read is armed off each read result and never bounded, so a server that keeps listing the Action past
expiresAtdrives roughly oneactions/queryper second. Same symptom as N3, but this path does not need a skewed browser clock.
Checked and deliberately not flagged: the turn-less last-match fallback in findManagedApprovalTool (documented, and pinned by managed-approval.test.ts), and the stream_gap arm of the re-read trigger (reachable — stream.reconciled is a persisted event, projected to stream_gap).
Still open at this head, from the earlier real-stack comment: F1 and N1/N2/N5. F1 is outside this PR's code, but the description still describes the card as attached to a tool row that supplies the call's arguments, which the real stack does not produce yet.
Verification: I did not run a local build, lint or test run (the review worktree has no install). CI on 4fbe5025 was still running when this was posted — the Java 11/17 and macOS lanes were green, Lint & Static and Test pending; the previous head b7848420 was fully green.
…pprovals # Conflicts: # docs/design/2026-09-30-managed-agent-actions.md # docs/design/2026-09-30-managed-agent-actions.zh-CN.md # packages/sdk-java/managed-agent-server/src/main/java/com/alibaba/qwen/code/managedagent/harness/QwenHostedHarnessConnector.java # packages/sdk-java/managed-agent-server/src/main/java/com/alibaba/qwen/code/managedagent/store/ManagedActionStore.java # packages/sdk-java/managed-agent-server/src/test/java/com/alibaba/qwen/code/managedagent/ManagedActionsTest.java
|
@qwen-code /verify Please verify the current head independently, including the central behavior and load-bearing negative controls. Keep the report tied to the exact tested commit. 中文请独立验收当前 head,验证核心行为及能证明测试有效的反例,并在报告中注明实际验证的提交。 |
|
@qwen-code /verify |
|
@qwen-code /verify |
|
Current-head E2E acceptance at 105b272: 79 browser assertions passed, 0 failed, using actual MySQL, Java/Broker, packaged Hosted Harness and Chromium. The model and actor identity use local test adapters. Full report, raw results, network records, hashes and probe scripts.
The parameter probe reproduced a remaining defect at 756e8d9: a valid write_file Item suppressed the unavailable notice but its write content did not appear on the approval card. Commit 105b272 renders the already-received arguments through the shared card. Two actual-card DOM regressions failed before the fix and pass afterward. Separately, 94 Managed tests, repository build/typecheck, changed-file lint/formatting and two full-diff self-audits passed. Before, at 756e8d9: After, at 105b272: The actual Hosted producer still emits no tool Items, and that real path retains the explicit arguments-unavailable notice. This report does not establish upstream Item production, production identity/model integration, or tracking an admitted operation through a later asynchronous failure. The still-settling answer/list-requested branch is covered by a focused hook test rather than a forced browser run. This macOS local-process run is separate from the CI container/VM verification; current-head CI and independent verification are still pending. 中文说明本次验收提交为 105b272,使用真实 MySQL、Java/Broker、打包后的 Hosted Harness 和 Chromium,79 项浏览器断言通过,0 项失败。模型和身份使用本地测试适配器。完整报告、原始结果、网络记录、哈希和探针脚本。 允许和拒绝分别通过 19 项:真实回答和后端结算,允许实际执行一次写文件,拒绝执行零次且模型收到拒绝。恢复场景通过 20 项:首次读取 503 和已经失败的 HTTP 202 明确为浏览器响应注入,Action 本身真实;重试恢复卡片,最终成功回答实际到达 Java 并执行一次。时钟超前通过 5 项:两个浏览器上下文和真实待审批 Action,4.3 秒内正常时钟一次查询、超前时钟两次,待审批状态由服务器决定。创建者权限通过 8 项:非创建者实际收到 403/action_forbidden,创建者真实拒绝后,Action 更新撤下 reader 卡片。参数展示通过 8 项:浏览器边界提供通过 schema 验证且身份匹配的工具 Item,卡片显示路径和确切写入内容。 参数探针在 756e8d9 复现了一个剩余缺陷:合法 write_file Item 会隐藏参数不可用提示,却没有在审批卡片显示写入内容。105b272a 通过共享卡片展示已收到的参数;新增两条实际卡片 DOM 回归在修复前失败、修复后通过。另完成 94 个 Managed 单测、仓库构建和类型检查、改动文件 lint/格式检查,以及两轮完整 diff 自查。上面两张截图分别是修复前和修复后。 实际 Hosted 生产端仍未输出工具 Item,真实无 Item 路径继续显示参数不可用提示。本报告没有证明上游 Item 产出、生产身份/真实模型接入,或已接受 operation 后续异步失败的跟踪。回答仍在结算、列表仍 requested 的分支由独立 hook 单测覆盖,没有强制运行对应浏览器场景。本次 macOS 本机进程验收与 CI 容器/VM 验证分开记录;最新提交的 CI 和独立验证仍在运行。 |
|
Qwen Code review request accepted. Review is queued for an available runner; follow the workflow run for progress. A command-triggered review is not listed under the checks of this PR; the result is posted here as a review when it finishes. |
The three approvals fixtures handed to `summary()` omitted `capabilities.canCancel`, which is a required member, so each one was a TS2741 that no gate in this package reports (both tsconfigs exclude `client/**/*.test.tsx` and vitest does not typecheck). They also modelled a summary the real mapper cannot emit, which always sets both flags. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmupe138l16
`toManagedPermissionRequest` reads `tool.title` for the approval `title`, but no producer ever set it: `managedEventsToMessages` only assigned args/status/times/rawOutput, so the left branch was dead and the card description degenerated into a restatement of the heading while the Harness`s own per-call title was dropped. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmupe138l16
Two lifecycle leaks in `useManagedActions`: - a successful re-read cleared `loadError` but never `answerError`, so "the answer could not be confirmed" stayed on screen after the Harness ended the Action, and then labelled the next, unrelated card. The warning now carries the Action it belongs to and is dropped once that Action is no longer pending. - a withdrawn reader cleared `pending` without resetting the retry budget, so a reader restored after the ladder was exhausted got a single attempt with no retry scheduled, contradicting the hook`s own comment. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmupe138l16
…ered R1-5: a 4xx other than 408/429 is the service's answer, not a hiccup, so a deleted Session no longer costs four guaranteed-failing /actions/query requests and a Retry that can never succeed. The classification the submit path already used moves to managed-request-error.ts and is reused, so both failure paths of the feature agree. R1-4: the hook reports whether the pending approvals were read for this Session, so a failed background re-read is named as a refresh instead of claiming nothing could be loaded next to the loaded, actionable card. R1-8: the "arguments are unavailable" caveat renders as a sibling of the approval dialog, so ToolApproval takes an extra description id and the panel describes the caveat too — otherwise a screen-reader user confirms a Hosted tool call hearing only the tool name. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmupe138l16
R1-10, four sites the review mutation-tested as unpinned: 1. the `stream_gap` arm of the re-read trigger — the durable transcript is the only route that reports a gap here, since use-managed-session breaks the live loop on a gap before merging it; the hook doc now says so. 2. the session-switch reset effect and the cross-session mask — the new test switches Session while the previous card is displayed and its failed answer is still flagged, so deleting the effect leaves the warning behind and reverting the mask leaves the stale card answerable. 3. the `pendingApproval` wiring into MessageList — the page test's mock now captures the prop, which is what keeps the turn owning the pending call from being folded away. 4. the `setAnswerError(undefined)` clear on a successful answer — the retry test now ends on the alert being gone, not just on the card. Every assertion was checked to fail with the production line it covers reverted (8/8 mutations RED). Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmupe138l16
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The approval flow is consistently capability-gated, failure-aware, and covered by focused regression tests.
Review effort: Balanced
Findings: None
What changed in this PR
Adds Hosted tool approval controls to the Managed panel, connecting durable Java Actions to the shared approval UI.
Changes:
- Loads, retries, expires, and answers pending permission Actions.
- Displays localized approval cards with available tool arguments.
- Adds provider/event mappings and regression coverage.
| File | Description |
|---|---|
client/index.tsx |
Exports the pending Action type. |
client/i18n.tsx |
Adds approval messages. |
use-managed-actions.ts |
Manages Action loading and responses. |
use-managed-actions.test.tsx |
Tests hook lifecycle and retries. |
ManagedSessionsPage.tsx |
Renders approval controls. |
ManagedSessionsPage.test.tsx |
Tests approval UI flows. |
managed-session-messages.ts |
Preserves approval-related tool metadata. |
managed-session-messages.test.ts |
Tests transcript projection. |
managed-approval.ts |
Adapts Actions to approval requests. |
managed-approval.test.ts |
Tests tool matching and adaptation. |
managed-agent-provider.ts |
Defines provider Action contracts. |
java-managed-agent-provider.ts |
Implements Java Action transport. |
java-managed-agent-provider.test.ts |
Tests Java Action mapping. |
java-managed-agent-event-projector.ts |
Projects Action update events. |
java-managed-agent-event-projector.test.ts |
Tests Action event projection. |
java-managed-agent-client.ts |
Adds Action API methods. |
adapters/messageTypes.ts |
Retains producer tool-call identity. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Ignore answer settlements once their Session or pending Action has changed, and clear or display warnings only for the Action they belong to. Pin late success and failure after Session switches and Action replacement.
Answering an Action that already expired, was cancelled or was answered elsewhere returns 409 with a code the contract defines as ended, and no retry can succeed. Keep the card hidden, clear the warning and read the list again instead of offering a retry. A read that still lists the Action shows it again, so a wrong report cannot hide an approvable Action. The 403 actor_scope_mismatch is not an ended Action and keeps its handling.
There was a problem hiding this comment.
COMMENT — corrected. The original version of this review was written against b7ef5909 and reported R1-2 as open with a proposed fix. 9fedb263 ("fix(web-shell): drop a Managed approval the service reports as ended") landed while I was writing it, and it fixes R1-2. The review is attached to 9fedb263, so the text below is corrected to match; the original is superseded rather than left standing.
R1-2 is fixed at this head, and the fix has a safety property worth naming
9fedb263 adds the reason-code taxonomy the thread asked for. ENDED_ACTION_CODES holds action_expired, action_cancelled and action_already_resolved with the contract stated in the comment ("The contract's codes for an Action that already ended … Retrying the answer cannot succeed"), and endedAction(failure) reads failure.code defensively rather than assuming the shape.
The part I would single out is that hiding an ended Action is self-correcting rather than trusted. endedAnswers is a ref of Actions hidden because the service reported them ended when answered, and the successful-read branch recomputes stillListed from what the service actually returned, clears the ref, and un-hides any Action still listed — with the comment "A read that still lists one shows it again, so a wrong report cannot hide an approvable Action." That is the right failure direction for an approval surface: a mis-classified code degrades to showing an approval that may already be answered, never to swallowing one that is live. The ref is also cleared on session change alongside answerFailure and loadFailures, so it cannot leak across sessions.
I had drafted a cheaper alternative — consulting the existing status-based isNonRetryableClientError in respond's catch and bumping the revision, which would cover all three codes plus future ones without a fourth copy of the reason vocabulary. The shipped version is the better trade: it distinguishes "ended" from other 4xx such as action_forbidden or a validation failure, which a status-only predicate cannot, and it buys the un-hide property above. Worth knowing that the reason strings now exist in two places (ENDED_ACTION_CODES here and the action_forbidden check in ManagedSessionsPage.tsx), so a new terminal code has to be added to one and only one of them depending on whether it should hide the card or merely stop the retry prompt.
Premise I had confirmed at b7ef5909, recorded because it is what made the fix checkable
ManagedSessionsPage.tsx derived its only special case from a reason string — approvalForbidden testing approvalCause.code === 'action_forbidden' — so the three terminal codes fell through to the generic unconfirmed-answer treatment. The blast radius was narrower than the thread stated even before the fix: the load path already cleared a stale answer warning once the failed Action disappeared from listPending, and since all three codes mean the Action is no longer pending, the misleading prompt was transient rather than stuck. What was missing was a trigger — respond's catch rolled back the optimistic answered entry and rethrew without bumping the revision, so the clear waited on an unrelated event. 9fedb263 addresses that directly by hiding the card at the point of the failure instead of waiting for a read.
Line references in this section are to b7ef5909; use-managed-actions.ts grew by 40 lines at 9fedb263, so they no longer match the current file.
What I checked and think is right
The parts of an approval UI that are easy to get wrong are correct. The idempotency key is per Action and option (${target.actionId}:${optionId}), so a retried click replays the same durable operation instead of answering twice, and keying on the option as well means a user who clicks deny after a failed allow is not silently replayed as allow. The optimistic answered set is rolled back on failure so a failed answer does not hide the card permanently — and now reconciled against the service on the next read. findManagedApprovalTool matches on the documented ${turnId}:${toolCallId} row key with a separate branch for Java itemId-keyed rows that carry the call ID apart, and toManagedPermissionRequest binds the card to action.actionId rather than to the matched tool, so a failed transcript match degrades the title and arguments, not the answer's identity. OPTION_KINDS maps the service's stable allow/deny IDs onto the shared card's option kinds so localization stays in one place.
Verdict
No blocking finding. Not approving yet for one reason only: Test (ubuntu-latest, Node 22.x), Lint & Static and Integration Tests were still pending when I posted, and 9fedb263 changed the hook this PR's behaviour depends on. I will approve when those report.
Scope: I read managed-approval.ts and managed-request-error.ts in full, use-managed-actions.ts in full at b7ef5909 plus the whole of 9fedb263's production diff, and the approval-cause derivation in ManagedSessionsPage.tsx. I have not read java-managed-agent-provider.ts (+59), managed-agent-provider.ts (+33/-1), the client and projector changes, or the ~1,040 lines of tests beyond confirming each new module has one.
Real-stack verification, round 5 —
|
|
@qwen-code /triage |
|
@qwen-code /triage |
qqqys
left a comment
There was a problem hiding this comment.
Approving at 72608f2c.
The one Critical ever filed on this PR is fixed at this head
R1-1 (filed 2026-10-01T04:31:42Z against e572cd72) reported that collapsing an unknown session summary into enabled === false tore down a live approval card on every reload, and starved /actions/query for the whole of a summary-route outage. I verified the fix in source rather than from the thread's resolution state:
use-managed-actions.ts:59now takesenabled: boolean | undefined, and the docblock states the contract — undefined while the summary is unknown, so the shown approval stays answerable and reads resume once the capability is known.:62withdraws the reader only on an affirmative false:const reader = enabled === false ? undefined : provider.actions.- The load effect at
:98-106keeps the two cases apart: the!reader || !sessionIdbranch is the one that callssetPending({ actions: [] }), andif (enabled === undefined) return undefined;at:106skips the read without touchingpending.enabledis in the dependency array at:159, so the read resumes on the next summary. ManagedSessionsPage.tsx:169passes exactly the derivation the finding asked for:detail.summary ? detail.summary.capabilities.actions === true : undefined.- The property the finding insisted any fix must keep is kept: when the capability is affirmatively absent,
readeris undefined, so no/actions/queryis issued and the card is cleared.
R1-2 is also present at this head — ENDED_ACTION_CODES (:17-21), the defensive endedAction code read (:23-29), and the endedAnswers ref whose un-hide is recomputed from what the service actually returned (:116-127), so a mis-classified terminal code degrades to showing an approval that may already be answered rather than swallowing a live one.
Critical-only scan, including the files no prior review covered
The last bot review disclosed it had not read the provider, client or projector layers, so I read those myself:
java-managed-agent-provider.ts—respondthrows onfailed,cancelledandrecovery_blocked, so an answer the service did not apply leaves the card up instead of hiding it as answered.toPendingActionreturns[]for anything that is notkind === 'permission'withstate === 'requested', andtoSessionSummarysetsactions: trueonly when the service says so, which is what the capability gate above reads.managed-agent-provider.ts— type additions only.actions?being optional means a provider without it yields an undefined reader, which lands in the clearing branch rather than throwing.managed-request-error.ts— this isisNonRetryableClientErrormoved verbatim out ofManagedSessionsPage.tsx, not new logic; the page's local copy is deleted in the same diff. A non-retryable 4xx still callssetLoadError, so the page still renders the alert and a Retry that resets the budget, and nothing is swallowed silently.managed-approval.ts—toManagedPermissionRequestbindsid: action.actionId, so a failed or ambiguous transcript match degrades the displayed title and arguments, never the identity of what is being answered. That is the right side to fail on for an approval surface.ToolApproval.tsx—extraDescriptionIdis additive and optional, and the page sets it only inside the same conditional that mounts the caveat element (ManagedSessionsPage.tsx:540-563), so no IDREF dangles.- The load alert now distinguishes
loaded(refreshFailed) from a first load (loadFailed), so it no longer claims nothing loaded beside a card that is on screen.
Two residuals I am explicitly not treating as blocking: listPending requests limit: 20 with no pagination, which the code comment states as a known bound and the UI renders one Action at a time; and findManagedApprovalTool matches on a :${functionCallId} suffix when the Turn is unresolved and keeps the last match, which can only affect displayed metadata for the reason above.
CI
9 pass, 0 fail, 2 pending (Capture web-shell visuals, web-shell E2E Smoke), 21 skipped at this head. Nothing attributable to this PR; pending checks were not waited on.
|
Post-merge review — reviewed at head SummaryNo blocking finding. One deferred Minor the author already acknowledged. Three positive verifications of the highest-risk claims. Verdict if pre-merge: Approve with the Minor noted below. Minor finding — R2-1
When The author explicitly deferred this in the R1-2 thread: "I am leaving this open rather than guessing the contract; it needs a product call." Recording it here so it travels with the shipped code and is findable in the follow-up ledger.
Verified claimsClass 10 — stated intent: "An unchanged expiry does not re-arm a one-second query loop" ( Verified correct. Class 4 — authorization: Verified correctly wired end-to-end. Class 8 — replay divergence: still-settling answered actions ( Verified correct. R1-2 (definitively-ended actions) — addressed by commit The new commit adds Scope and unreviewed dimensionsReviewed: Not covered: local execution unavailable — rungs 1-3 not run; daemon provider (no Reviewed with AI assistance. |
…wenLM#13294) * fix(web-shell): restore the Managed approval retry budget on reload Since R1-1, a page Refresh drops the Session summary, so the Actions reader sees enabled === undefined and returns early, never reaching the branch that resets loadFailures. After the retry ladder had been spent, the reload's first failed read scheduled no retry and the card stayed away until the next event, even though the service was healthy again. Reset the budget in the unknown-capability branch too. Only a reload, a user action, resets it, so the ladder stays bounded. Candidate patch from wenshao's round-5 real-stack verification of QwenLM#13107. * test(web-shell): pin Managed approval paths that mutants survived - action_already_resolved, the code Java returns when a stale tab answers an Action another client already decided, drops the card without a warning. - findManagedApprovalTool keeps to the Action's Turn when a later Turn reuses the call ID, for both callId-keyed and itemId-keyed rows. Candidate tests from wenshao's round-5 mutation run of QwenLM#13107.











What this PR does
Shows pending Hosted tool approvals in the Managed panel and lets the Session creator allow or deny them. Previously, a Hosted Turn waiting for permission had no approval control in the panel.
Why it's needed
D6a (#13071) pauses tools that require permission, and D6b (#13101) exposes those Actions. This PR supplies the WebShell controls claimed under #12867, including recovery from transient reads and failed answers.
Reviewer Test Plan
How to verify
default. Use the public session-creation API to create a Workspace-bound Session with an initial file-write prompt, then open it in the Managed panel. The current panel's Workspace creator makes an empty Session, so it cannot supply this initial prompt. Expect an approval card; if no tool Item is present, expect the arguments-unavailable notice. With a matching tool Item, expect its file path and write/edit content on the card.action_forbiddenshould see that only the creator can answer.Evidence (Before & After)
Current-head acceptance report at e572cd7: 103 browser assertions passed across actual allow, deny, creator-only rejection and clock-ahead behavior, plus explicit browser-response fixtures for read/answer recovery and a schema-valid tool Item. MySQL, Java/Broker, packaged Hosted Harness and Chromium are real processes; model replies and actor identity use local test adapters.
The parameter probe found that a valid file-write Item suppressed the unavailable notice while the shared card hid the write content. The card now displays those available arguments; two actual-card DOM regressions failed before the fix and pass afterward. The report includes before/after screenshots, raw network records, source/artifact hashes and the eight probe scenarios. The Hosted producer still emits no tool Items; its real path retains the unavailable notice.
Retrying an initial 503 restores the card with only the Actions query count changing from one to two. Immediate failed/cancelled/recovery_blocked HTTP 202 fixtures restore enabled options and an unconfirmed error. Successful retry/cleanup then reaches actual Java; the fixtures do not prove a backend cancellation/recovery fault. A clock ahead of expiry produces one extra read over 4.3 seconds without a one-second loop. An actual Action update removes the reader's card without a Session-summary reload. CI and independent container/VM verification are tracked separately on the PR.
Supplemental same-head recheck adds 25 UI observations and 5 provider/hook controls, with zero unexpected failures. Actual natural expiry removes both pages' cards 133ms after the deadline, with two Actions reads per page and no tool/file writes. Some observations intentionally confirm a residual creator-only alert after expiry/Refresh; it is a non-blocking follow-up. Failed re-read/snapshot scenarios use explicit browser 503 fixtures, and the next-approval response control uses mockHTTP. The original 103/55 results were not rerun.
A latest-main compatibility trial merged main 6b66321 without conflicts. Build/typecheck and independent native verification passed: 14 files / 1,360 tests, with zero failures/errors/skips or framework retries. The trial explicitly resolves SDK/daemon to its own freshly built SDK, and source/artifact hashes are stable. Relative to main, the same 16 WebShell files remain; 15 whole files match e572 and the i18n approval delta matches. Trial 6e89ccbe is local and was not pushed; actual PR head remains e572. The earlier main57 trial did not record SDK resolver/artifact ownership, so its passing counts are retained with that provenance limitation. This does not re-accept the Hosted MCP release-retry backend or rerun browser probes.
Wenshao's earlier real-stack report supplies additional replay and multiple-approval evidence and identifies the upstream missing tool Items.
Tested on
Environment (optional)
macOS arm64, Node.js 22.22.3, React 19.2.4, Playwright 1.61.1/Chromium, MySQL 8.4.7, JDK 21 and Maven 3.9.9. No Docker/Podman was available for this local run; it does not replace the separate container/VM verification.
Risk & Scope
Linked Issues
Follows #13101 (merged)
Relates to #12867
中文说明
这个 PR 做了什么
在 Managed 面板展示待处理的 Hosted 工具审批,让 Session 创建者允许或拒绝。此前,等待权限审批的 Hosted Turn 在面板中没有可用的审批控件。
为什么需要
D6a(#13071)会暂停需要权限审批的工具,D6b(#13101)公开这些 Action。本 PR 补上 #12867 中认领的 WebShell 控件,并处理临时读取失败和回答失败后的恢复。
评审验证方式
如何验证
default。通过公开的 Session 创建 API 创建绑定 Workspace、带首次写文件提示的 Session,再在 Managed 面板打开。当前面板的 Workspace 创建入口只创建空 Session,无法提供这条初始提示。预期出现审批卡片;缺少工具 Item 时,预期出现参数暂不可见的提示。存在匹配工具 Item 时,卡片应展示文件路径与写入或编辑内容。action_forbidden时,应看到只有创建者可以回答的提示。证据(前后对比)
最新提交验收报告基于 e572cd7:103 项浏览器断言通过,覆盖真实允许、拒绝、创建者权限不足和时钟超前行为,以及明确注入的读取/回答恢复响应、通过 schema 验证的工具 Item。MySQL、Java/Broker、打包后的 Hosted Harness 与 Chromium 都是真实进程,模型回复和身份使用本地测试适配器。
参数探针发现:收到合法写文件 Item 后,不可用提示消失,但共享卡片仍隐藏写入内容。现在卡片展示已收到的参数;两条实际卡片 DOM 回归在修复前失败、修复后通过。报告包含前后截图、原始网络记录、源码与产物哈希及八个探针场景。Hosted 生产端仍未输出工具 Item,真实无 Item 路径继续显示不可用提示。
首次 503 后直接重试恢复卡片,只有 Actions 查询从一变二。立即 failed/cancelled/recovery_blocked 的 HTTP 202 注入响应均恢复可用选项并显示结果未确认提示,随后成功重试或清理实际到达 Java;注入不证明后端真实取消或恢复故障。时钟超前于过期时间时,4.3 秒内只有一次额外重读,不会每秒循环。真实 Action 更新可撤下 reader 卡片,无需重新读取 Session 摘要。CI 和独立容器/VM 验证在 PR 中另行跟踪。
相同提交的补充复核新增25项UI观察与5项provider/hook控制,零非预期失败。真实自然到期在deadline后133ms撤下两页卡片,各页只读两次Actions,零工具/文件写入。部分观察刻意确认到期/Refresh后仍残留creator-only提示,将其记录为非阻断后续。失败重读/snapshot使用明确的浏览器503 fixture;下一审批回答控制使用mockHTTP。原103/55没有重跑。
最新 main 兼容试合并无冲突地合入 main 6b66321;build/typecheck 与独立原生验收通过:14 文件、1,360 用例,失败/error/skip 和框架 retry 均为零。本轮明确绑定试跑分支自己新构建的 SDK/daemon,源码与产物哈希稳定。相对 main 仍只有原来的16个 WebShell 文件;15个完整文件与 e572 相同,i18n 的审批改动一致。trial 6e89ccbe 仅在本机,没有推到 PR,实际 head 仍是 e572。此前 main57 试跑未记录 SDK 解析/产物来源,保留通过数并注明此证据限制。本轮不验收 Hosted MCP release retry 后端,也没有重跑浏览器探针。
Wenshao 之前的真实链路报告另提供重放、多次审批的证据,并指出上游工具 Item 缺失。
测试平台
环境(可选)
macOS arm64、Node.js 22.22.3、React 19.2.4、Playwright 1.61.1/Chromium、MySQL 8.4.7、JDK 21 与 Maven 3.9.9。本次本机没有 Docker/Podman,不替代独立容器/VM 验证。
风险与范围
关联 Issue
Follows #13101 (merged)
Relates to #12867