Skip to content

feat(web-shell): show and answer Hosted tool approvals in the Managed panel - #13107

Merged
yiliang114 merged 33 commits into
mainfrom
feat/12867-webshell-approvals
Oct 1, 2026
Merged

yiliang114 merged 33 commits into
mainfrom
feat/12867-webshell-approvals

Conversation

@yiliang114

@yiliang114 yiliang114 commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

#13101 (D6b) has merged. The current diff against main contains only the 16 WebShell frontend files owned by this PR.

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.

  • Reads requested permission Actions only when the Session advertises the Actions capability. Answers retain the original input and policy revisions and use an idempotency key per Action and option.
  • Re-reads on approval updates, stream reconciliation, an expiry, and an answer. Failed reads have bounded background retries and a direct retry button. An unchanged expiry does not re-arm a one-second query loop.
  • Reuses the shared approval card and localized allow/deny labels. Immediate answer failures restore the card and allow the same option to be retried. An operation already marked failed, cancelled or recovery_blocked restores the card and reports an unconfirmed answer even when the HTTP status is 202. Creator-only rejection and an unconfirmed answer have distinct messages.
  • Matches a transcript tool row when one exists and renders its arguments in the shared approval card, including file paths and write/edit content. The current Hosted producer does not emit those tool Items, and the Java projector strips their argument fields if a call is injected upstream, so the card explicitly says that arguments are unavailable. Custom providers can opt into the same Actions capability.

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

  1. Run the D6a/D6b stack with approval mode 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.
  2. As the Session creator, allow a request and confirm the Turn continues; on a separate request, deny it and confirm the model receives a refusal. A reader who receives action_forbidden should see that only the creator can answer.
  3. Fail the first approval-list read and recover the service. The panel should offer a direct retry, and retrying should restore the card without submitting a Turn or an answer. A transport failure should restore an error and the card for a same-key retry. A replayed definitive failure also restores the card, but the same option replays its persisted failure; its retry wording and recovery are follow-up work. A still-settling accepted answer should stay hidden even if a read still lists it as requested.
  4. Move the browser clock ahead of the Action expiry while the service still lists it as requested. Expect one expiry re-read, with no repeated one-second queries. Approval-update events should continue to trigger reads.

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

OS Status
🍏 macOS ✅ Repository build and typecheck; changed-file lint and formatting; 94 Managed tests; 103 browser assertions; 25 supplemental observations +5 hook controls
🪟 Windows ⚠️ Not run locally
🐧 Linux ⚠️ Not run locally

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

  • Main risk or tradeoff: tool arguments are visible only when a matching transcript Item carries data.input. Production rollout requires both Hosted tool-call publication and a bounded, redacted Java argument projection under the field the page consumes. The injected Item probe proves rendering of that wire shape; the current server cannot produce it.
  • Not validated / out of scope: shell/auto-edit approvals, question Actions, votes, stream-gap recovery, production identity/model integration and following an admitted answer operation through later terminal settlement. This PR detects failure already present in the response, but does not add operation polling or tracking. Replaying a definitively failed answer uses the same key and cannot recover that option; the current same-option retry wording is misleading in that case and is tracked with terminal-operation recovery. A prompt-capable bound Session composer remains follow-up work. A historical creator-only answer alert may remain after its Action ends; during a failed Refresh snapshot, Actions Retry is temporarily inert until the general Refresh restores the reader. Both are recorded as non-blocking follow-ups. Automatic-review attempts 1 and 2 failed on explicit runner shutdown before publishing; attempt 3 is now running on the same head. Functional CI, the earlier exact-head triage approval and maintainer review retain separate attribution.
  • Breaking changes / migration notes: none; the provider capability is optional. feat(managed-agent): Serve durable permission Actions (D6b) #13101 has already landed.

Linked Issues

Follows #13101 (merged)
Relates to #12867

中文说明

#13101(D6b)已合入,main 已 merge 到本分支。 现在相对 main 的 diff 只包含本 PR 的 16 个 WebShell 前端文件。

这个 PR 做了什么

在 Managed 面板展示待处理的 Hosted 工具审批,让 Session 创建者允许或拒绝。此前,等待权限审批的 Hosted Turn 在面板中没有可用的审批控件。

  • 仅在 Session 声明 Actions 能力时读取待处理的 permission Action。回答保留原始输入版本和策略版本,幂等键按 Action 与选项生成。
  • 在审批更新、事件流重新同步、过期和回答后重新读取。读取失败有次数受限的后台重试和直接重试按钮;未变化的过期时间不会反复触发每秒查询。
  • 复用共享审批卡片和本地化的允许、拒绝文案。回答立即失败时恢复卡片,并允许重试同一选项。即使 HTTP 状态为 202,响应中的 operation 已为 failed、cancelled 或 recovery_blocked 时,也会恢复卡片并提示回答结果未确认。创建者权限不足与回答结果未确认分别显示对应提示。
  • 存在对应的 transcript 工具行时,取得参数并在共享审批卡片展示,包括文件路径与写入或编辑内容。当前 Hosted 生产端没有输出这些工具 Item,即使向上游注入工具调用,Java 投影层也会丢弃其参数字段,因此卡片会明确提示参数暂不可见。自定义 provider 可以选择支持同样的 Actions 能力。

为什么需要

D6a(#13071)会暂停需要权限审批的工具,D6b(#13101)公开这些 Action。本 PR 补上 #12867 中认领的 WebShell 控件,并处理临时读取失败和回答失败后的恢复。

评审验证方式

如何验证

  1. 运行 D6a/D6b 链路,审批模式设为 default。通过公开的 Session 创建 API 创建绑定 Workspace、带首次写文件提示的 Session,再在 Managed 面板打开。当前面板的 Workspace 创建入口只创建空 Session,无法提供这条初始提示。预期出现审批卡片;缺少工具 Item 时,预期出现参数暂不可见的提示。存在匹配工具 Item 时,卡片应展示文件路径与写入或编辑内容。
  2. 以 Session 创建者允许一次请求,确认 Turn 继续;在另一请求中拒绝,确认模型收到拒绝结果。只读用户收到 action_forbidden 时,应看到只有创建者可以回答的提示。
  3. 让首次审批列表读取失败,然后恢复服务。面板应提供直接重试,重试后恢复卡片,不提交 Turn 或审批回答。传输失败后应显示错误、恢复卡片,并允许使用相同幂等键重试。重放确定已失败的 operation 也会恢复卡片,但同一选项只会重放持久化的失败;相应重试文案与恢复属于后续工作。已接受、仍在结算的回答,即使重新读取仍列为待审批,也应保持隐藏。
  4. 将浏览器时钟拨到 Action 过期时间之后,而服务仍列出待审批项。预期只进行一次过期重读,不再每秒重复查询;审批更新事件仍应触发重读。

证据(前后对比)

最新提交验收报告基于 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 缺失。

测试平台

OS 状态
🍏 macOS ✅ 仓库构建和类型检查;改动文件 lint 与格式检查;94 个 Managed 测试;103 项浏览器断言;25 项补充观察 +5 项 hook 控制
🪟 Windows ⚠️ 未在本地运行
🐧 Linux ⚠️ 未在本地运行

环境(可选)

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 验证。

风险与范围

  • 主要风险或取舍:只有匹配的 transcript Item 携带 data.input 时才能显示工具参数。生产开放需要同时补齐 Hosted 工具调用发布,以及使用页面所读字段、限制大小并进行脱敏的 Java 参数投影。注入 Item 探针证明了该 wire 形状的渲染,当前服务还产不出这种形状。
  • 未验证 / 不在范围内:Shell/auto-edit 审批、问题类 Action、投票、stream-gap 恢复、生产身份/模型接入,以及持续跟踪已接收的回答 operation 到后续终态。本 PR 检查响应中已经存在的失败,不新增 operation 轮询或跟踪。确定失败的回答使用相同键重放,无法恢复该选项;此时当前引导重试同一选项的文案有误导性,与终态 operation 恢复一起跟踪。支持初始提示的绑定 Session 输入入口仍是后续工作。历史 creator-only 回答提示可能在对应 Action 结束后残留;Refresh snapshot 失败期间,Actions Retry 暂时无动作,general Refresh 恢复 reader 后正常。这两项记录为非阻断后续。自动 review 第 1、2 次因明确 runner shutdown 失败,正式结果未发布;同一提交的第 3 次已开始运行。功能 CI、既有当前提交 triage approval 和维护者 review 保持独立归属。
  • 破坏性变更 / 迁移说明:无;provider 能力为可选。feat(managed-agent): Serve durable permission Actions (D6b) #13101 已合入。

关联 Issue

Follows #13101 (merged)
Relates to #12867

wenshao and others added 5 commits September 30, 2026 18:30
… 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.
yiliang114 and others added 7 commits September 30, 2026 21:52
…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.
…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.
@wenshao

wenshao commented Sep 30, 2026

Copy link
Copy Markdown
Collaborator

Real-stack verification — b784842050

Verdict. On a real stack the approval card does what this PR says: it appears, the owner answers it, "Yes, allow once" lets the Turn continue and "Reject" reaches the model as a refusal. That holds with a scripted model and with qwen3.8-max. I found nothing in this PR's own code that should block merging it after #13101.

One thing to decide before it is marked ready (F1): the card never shows what is being approved. The description says the card attaches to the tool row, which supplies the call's arguments. On the real stack that row does not exist, so the owner sees only WriteFile / write_file. The cause is outside this PR, but the description promises otherwise.

what was run

Setup

MySQL 8.4.7, the Spring server jar with the embedded Runtime Broker (JDK 21), the packaged Hosted Harness (dist/cli.js), approval mode default, Workspace files on. The Managed panel is the real ManagedAgentWebShell served by vite from the PR sources and driven by headless Chromium. The base arm is the same page from #13101's head 358212a8b2, against the same server.

The head moved twice while I was working (badf94c4ae → cd7d6c61ab → b784842050). Every number below is from b784842050. Java and the Harness were built from cd7d6c61ab; the two heads differ only in two files under packages/web-shell/client (0 diff lines elsewhere).

Before / after

One Session waiting on a write_file approval, opened by both pages:

before and after

What holds

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

real model

  • The rig database holds 30 Sessions and 55 approvals from these runs (scripted and real model). Its 58 Items are all of type message: 0 tool_call Items and 0 item.tool_call.updated events, while an approval is pending and after the Turn finishes.
  • Why: the Hosted Harness only emits agent_message_chunk updates (hosted-harness-session.ts:348), and Java creates tool Items only from tool_call / tool_call_update Harness events (HarnessEventProjector.java:55). The D6 design says "Arguments come from Items", but Hosted Turns put no function call into Items.
  • Effect here: findManagedApprovalTool never finds a row and rawInput is never set. The "main risk" in the description (the Action's functionCallId equals the tool Item's toolCallId; "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_file calls in one model message produce three identical cards, so the owner cannot tell which file each answer is for.

three identical cards

Ways forward, for the maintainers to pick:

  1. 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-edit is offered to users.
  2. 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.
  3. 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 past expiresAt, the page calls actions/query about 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.patch adds five tests (+127 lines, tests only; eslint, prettier and tsc clean, 91/91) and brings that to 28. The three left are the stream_gap trigger, a constant inputRevision (it is always 1 today), and passing pendingApproval to the mocked MessageList.
  • 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 through sessions/create with input and workspace. Those Sessions also show "Message execution is not available in this service yet" while a Turn is executing. Both predate this PR.

edges

Gates

  • At b784842050, in a clean worktree: eslint --max-warnings 0, prettier, tsc --noEmit and the web-shell build all exit 0; unit tests 86/86 in client/components/managed and 10,443/10,443 in packages/web-shell.
  • The earlier red CI is resolved. On badf94c4ae I reproduced the Lint & Static failure (react-hooks/exhaustive-deps at use-managed-actions.ts:85) and found a Prettier failure in ManagedSessionsPage.test.tsx that the failed ESLint step was hiding; 89a14fd38c and b784842050 fix 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 Smoke was still pending when I posted this.

Not covered

  • Shell approvals. The rig's Harness offered read_file, write_file and edit only, so the exec-style card was not exercised.
  • auto-edit mode, 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 项)。

zh-CN

F1 — owner 看不到调用内容就要作答

  • 装置数据库里有这些运行留下的 30 个 Session、55 次审批(脚本模型和真实模型都有)。58 个 Item 的类型全部是 message:tool_call Item 为 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 分不清每次作答对应哪个文件(上方第四张图)。

可选做法,由维护者决定:

  1. 把本 PR 当作"作答通路"合入,在向用户开放 default / auto-edit 之前,在 feat(managed-agent): Stage D follow-ups for durable lifecycle, Turns, Actions, durable admission and AgentDefinition #12867 上跟进参数展示。
  2. 先不合,等 Harness 在提交 assistant 消息时(询问之前)发布函数调用,让 Java 投影出工具 Item,本 PR 的匹配逻辑才有对象。
  3. 无论选哪种,都应更正 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/managed 86/86,packages/web-shell 10,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 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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.

Comment thread packages/web-shell/client/components/managed/use-managed-actions.ts Outdated
@yiliang114
yiliang114 marked this pull request as ready for review September 30, 2026 14:29
@yiliang114

yiliang114 commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator Author

Review follow-up at 4dc1472d2b (built on the concurrent 4fbe5025 read-recovery fix).

  • Fixed: unchanged expiries now cause one re-read rather than recurring one-second queries; immediately rejected answers re-arm the existing approval button; HTTP 202 responses with public status: failed propagate their failureCode as an error and restore the card. Creator-only 403 and uncertain answer outcomes have accurate messages.
  • Tests strengthened: a post-answer read still returning the requested Action keeps the card hidden, so the answered filter is now exercised. The page covers a failed initial read, its direct retry, creator-only rejection and retrying the same option. The bounded retry test advances each interval across a React commit.
  • F1 / N5 narrowed: the card says when arguments are unavailable, and the description no longer claims Hosted tool Items exist. The reviewer plan creates a bound Session with initial input through the public API. Producing tool Items and adding a prompt-capable bound composer remain follow-ups.
  • N2 follow-up: detecting an already-failed response is covered; following a previously admitted operation to a later terminal result remains outside this PR. A requested Action is not automatically unhidden during settlement, since that would invite a second answer before the first settles.
  • N4 / visual evidence follow-up: focused regressions were added. The broader mutation matrix and extra screenshot scenarios remain follow-ups. The existing real-stack report supplies the backend/browser evidence; this round adds frontend integration checks with mocked HTTP responses.

Validation on macOS: repository npm run build and npm run typecheck, changed-file ESLint/Prettier, and all 92 Managed tests in 11 files passed. Two full diff audit passes were clean. Independent integration checks confirmed: clock-ahead queries 5 → 2 over about 4.4 seconds; initial-503 retry changes only Actions queries 1 → 2 while Session/transcript/stream remain 1 and submissions/answers 0; an already-failed 202 now rejects, restores the card and sets the answer error. Regression cases were red before their fixes.

Scope ledger: two substantive fix rounds (4fbe5025, then this commit). Against the inherited D6b base, implementation remains 10 files, 319 → 394 changed lines; tests remain 6 files, 441 → 630 changed lines; the full PR-owned delta still has 16 WebShell files. Test growth triggered a scope audit: this round retains distinct approval regressions in existing test files and keeps operation tracking, producer changes, questions/votes and broader test matrices as follow-ups. Landing order remains #13101, then #13107 after merging main.

Current bounded pass at e572cd72: partial review5375002256 reports one Critical and12 Suggestions. Independent actual-page/Java HTTP observation reproduced R1-1: Refresh removes the live card, and a15-second summary-only outage leaves it absent although the real Actions endpoint still returns the requested Action. Initial unknown, explicit no-capability and selected-Session isolation controls were checked. This pass fixes the selected-session capability lifetime and its direct regressions; the12 Suggestions are classified and recorded separately under the repository’s roughly-five-round scope rule. No producer, API or operation-tracking work is included.

本轮仅处理已由独立真实页面/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 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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/respond returns the durable operation, and a 202 body already carries state: failed plus errorCode for an operation that ended that way; the client drops it. With answered cleared 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 listPending resolves [], so the answered filter 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 expiresAt drives roughly one actions/query per 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.

Comment thread packages/web-shell/client/components/managed/use-managed-actions.ts
Comment thread packages/web-shell/client/components/managed/use-managed-actions.ts
yiliang114 and others added 2 commits September 30, 2026 23:02
…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
@yiliang114

Copy link
Copy Markdown
Collaborator Author

@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,验证核心行为及能证明测试有效的反例,并在报告中注明实际验证的提交。

@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /verify

@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /verify

@yiliang114

Copy link
Copy Markdown
Collaborator Author

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.

Scenario Result Evidence boundary
Allow / deny 19 + 19 passed Actual API answers and backend settlement: allow executes the file write once; deny executes zero times and returns a refusal to the model.
Read / answer recovery 20 passed First-read 503 and an already-failed HTTP 202 are browser-response fixtures against a real pending Action. Retry restores the card; the successful answer reaches Java and executes once.
Clock ahead 5 passed Real pending Action, two browser contexts: normal clock has one Actions query, ahead clock has two over 4.3 seconds; the server controls pending state.
Creator ownership 8 passed A real non-creator answer receives 403/action_forbidden; the creator's actual denial removes the reader's card via Action updates.
Tool arguments 8 passed A schema-valid matching transcript Item is supplied at the browser boundary; the card shows its path and exact write bytes.

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:

Approval card hides available write arguments

After, at 105b272:

Approval card shows the file path and write content

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 和独立验证仍在运行。

@yiliang114
yiliang114 requested a review from wenshao September 30, 2026 18:12
@yiliang114
yiliang114 dismissed a stale review October 1, 2026 09:19

fixed

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

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.

yiliang114 and others added 4 commits October 1, 2026 17:24
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
@yiliang114
yiliang114 requested review from doudouOUC and a balanced review from Copilot October 1, 2026 11:59
yiliang114 and others added 2 commits October 1, 2026 20:05
…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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

yiliang114 and others added 2 commits October 1, 2026 20:29
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.

@qwen-code-review-bot qwen-code-review-bot left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

@wenshao

wenshao commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

Real-stack verification, round 5 — 72608f2c03

Since round 4, this branch has gained:

The head moved from 9fedb263a0 to 72608f2c03 while I was verifying. I re-ran everything below on 72608f2c03; the 9fedb263a0 logs are kept in the evidence. I rebuilt the Java server and the Hosted Harness from the head. "Before" in the comparisons is the round-4 head 24b26f107d, served side by side against the same server.

Verdict.

  • The build and the merges are sound. The main merge needed no hand resolution. The one hand-resolved conflict, in 72608f2c03 (late replies vs ended Actions), keeps both sides. eslint, prettier, tsc and the build all exit 0. Unit tests pass 198/198 in managed and 10,602/10,602 in packages/web-shell. The PR's test files now type-check (R1-9); the one remaining error is on a main line and exists on main too.
  • Nothing regressed on the real stack. 166 of 169 checks pass; the 3 misses are the known R2-1 probe. This covers allow/deny, a real hosted model, expiry, viewers, faults, sequences, clock skew, R1-1 and R4-1.
  • R1-2, R1-3, R1-4, R1-5, R1-7, R1-8 and R4-1 are fixed on the real page. So is late-reply isolation. Each was shown against the round-4 head (figure 3):
    • A stale tab's Reject gets a real 409 action_already_resolved, and the card now leaves without a warning.
    • A lagging tab answers just after expiresAt and gets a real 409 action_expired. No warning is left on an empty panel.
    • After a Session switch or a replaced approval, a late reply (503, success or 409) no longer warns beside the wrong card. None of the 4 variants warns at the head; 3 of them did before.
    • A 404 is read once instead of four times.
    • A failed refresh now says "could not be refreshed".
    • The Harness's call title reaches the card.
    • Chromium's computed accessible description of the dialog now includes the arguments caveat, with no dangling IDREF.
  • The "a wrong report cannot hide a live Action" property holds. I replayed Java's real 409 body for an Action that was still live. The card left, came back after the next read, and the real Allow was then applied once.
  • R1-6 is only half fixed. This is the one open behaviour item; details and a candidate are below. It is Suggestion-level: the alert's Retry button works throughout.
  • There are three test gaps. Mutation testing kills 64 of 77 mutants. Deleting action_already_resolved from the ended codes keeps every test green, and that is the code Java actually returns in R1-2. A +55-line test-only candidate closes this gap and two others (below).

round 5

R1-6 — Refresh after the retry ladder is spent still gets no retry

The R1-6 finding named the page's Refresh as the path that strands the ladder. It also said the bug survives the R1-1 fix. 95fbc3ca96 resets loadFailures only in the !reader || !sessionId branch, but since R1-1, Refresh never reaches that branch:

  1. Refresh drops the summary, so enabled becomes undefined.
  2. The reader is kept, and the effect returns at if (enabled === undefined) return undefined; without resetting the budget.
  3. When the summary comes back, one read runs with the counter still at 4. If that read fails, LOAD_RETRY_DELAYS_MS[4] is undefined, so no retry is scheduled.

The new test restores the retry budget when the reader is withdrawn and back drives enabled: false → true, which the page does not do on Refresh.

On the real page: actions/query returned 503 four times, then the user clicked Refresh, the next read failed once more, and the service was healthy after that.

  • At the head, the page made 5 reads and showed no card for 15 s.
  • The round-4 head behaves the same.
  • With the candidate, the page made 6 reads and the card came back after 2 s.

R1-6

candidate-r1-6-reload-budget.patch (+36/−1):

  • It resets the budget in the enabled === undefined branch as well.
  • It adds the same test as the author's, driven through undefined. That test fails on the head (expected "spy" to be called 6 times, but got 5 times) and passes with the patch.
  • managed passes 199/199, and eslint, prettier and tsc are clean.
  • The ladder stays bounded: stops retrying after a bound still passes, because only a reload — a user action — resets the budget.

Test gaps the mutants found

Mutation testing kills 64 of 77 mutants at the head. Three of the survivors are real gaps:

  • N1. Deleting 'action_already_resolved' from ENDED_ACTION_CODES keeps every test green. The tests cover action_expired and action_cancelled, but R1-2 on the real stack returns action_already_resolved.
  • A3 and A8. The Action-Turn guard in findManagedApprovalTool is unpinned for both row kinds. The tests always put the Action's Turn last, so "match any Turn's call with that ID" gives the same answer.

candidate-r5-tests.patch adds +55 lines of tests only. It applies to the head and passes 200/200, and it kills all three mutants.

Other survivors are not gaps. The late-reply commits added a render-time check, answerError shown only for the displayed Action, which overlaps the existing state clearing. Each survives alone, and the pairs L3+N11 and L3+H10 are killed together. The rest change nothing a test or the page observes, or are old (H15, P5, N7).

Before and after

fixed since round 4

Not covered

CI on this head: 8 checks pass; Capture web-shell visuals and web-shell E2E Smoke were still pending when I posted this.

Evidence (figures, raw screenshots, scenario logs for both heads, probes, mutation runs, both candidate patches): assets-pr13107 @ b2276304 / pr13107/r5

中文版本

真实环境验证,第 5 轮 — 72608f2c03

第 4 轮之后,本分支新增了:

验证途中 head 从 9fedb263a0 变成了 72608f2c03。下文全部在 72608f2c03 上重跑,9fedb263a0 的日志保留在证据里。Java 服务和 Hosted Harness 都从 head 重新构建。对照中的"之前"是第 4 轮的 head 24b26f107d,与当前 head 并排连同一个服务。

结论。

  • 构建与合并没有问题。 合并 main 不需要手工解决冲突。72608f2c03 里唯一一处手工解决的冲突(迟到回复与已结束审批)两边都保留了。eslint、prettier、tsc 和构建全部 exit 0。单测 managed 198/198,packages/web-shell 10,602/10,602。PR 的测试文件现在能通过类型检查(R1-9);剩下的一个错误在 main 自己的代码行上,main 上同样存在。
  • 真实环境没有回归。 169 项检查通过 166 项,未通过的 3 项是已知的 R2-1 探针。覆盖了允许/拒绝、真实托管模型、过期、多查看者、故障、连续审批、时钟偏差、R1-1 和 R4-1。
  • R1-2、R1-3、R1-4、R1-5、R1-7、R1-8 和 R4-1 已在真实页面上修复,迟到回复隔离也成立。每一项都与第 4 轮 head 做了对照(图 3):
    • 陈旧标签页点"拒绝"会拿到真实的 409 action_already_resolved,现在卡片直接离开,没有警告。
    • 落后的标签页在 expiresAt 之后作答会拿到真实的 409 action_expired,空面板上不再残留警告。
    • 换 Session 或审批被替换后,迟到的回复(503、成功或 409)不再在别的卡片旁报警:4 种变体在当前 head 上都没有报警,之前有 3 种会报警。
    • 404 只读一次,不再读四次。
    • 刷新失败时提示"刷新失败"。
    • Harness 的调用标题上了卡片。
    • Chromium 计算出的对话框无障碍描述包含参数提示,且没有悬空的 IDREF。
  • "误报不能藏起仍待审批的 Action"这一性质成立。 我对一个仍待审批的 Action 回放了 Java 真实的 409 响应体:卡片先离开,下一次读取后回来,随后真实的"允许"只生效一次。
  • R1-6 只修了一半。 这是唯一仍未解决的行为问题,细节和候选修复见下文。它属于 Suggestion 级:提示里的重试按钮全程可用。
  • 有三处测试缺口。 变异测试 77 个变异体杀掉 64 个。把 action_already_resolved 从"已结束"码表删掉,测试仍然全绿,而这正是 Java 在 R1-2 场景里实际返回的码。一个只加测试的 +55 行候选补丁能补上它和另外两处缺口(见下文)。

R1-6 — 重试次数用尽后点"刷新",仍然不会再重试

R1-6 原评论点名的就是页面"刷新"这条路径,并指出 R1-1 修复后问题依旧存在。95fbc3ca96 只在 !reader || !sessionId 分支里重置 loadFailures,但 R1-1 之后,"刷新"根本走不到这个分支:

  1. 刷新会丢弃摘要,enabled 变成 undefined。
  2. 读取器被保留,effect 在 if (enabled === undefined) return undefined; 处直接返回,不重置计数。
  3. 摘要回来后只发一次读取,此时计数仍是 4。如果这次读取失败,LOAD_RETRY_DELAYS_MS[4] 为 undefined,不会安排重试。

新测试 restores the retry budget when the reader is withdrawn and back 走的是 enabled: false → true,而页面在"刷新"时并不会这样变化。

真实页面上: actions/query 先连续 4 次返回 503,用户随后点"刷新",下一次读取再失败一次,此后服务恢复正常。

  • 当前 head:页面共读取 5 次,15 秒内没有卡片。
  • 第 4 轮 head:与当前 head 相同。
  • 候选修复:页面读取 6 次,2 秒后卡片回来。

candidate-r1-6-reload-budget.patch(+36/−1):

  • 在 enabled === undefined 分支也重置计数。
  • 新增一个与作者测试相同、但改走 undefined 的测试。它在当前 head 上失败(expected "spy" to be called 6 times, but got 5 times),打补丁后通过。
  • managed 199/199,eslint、prettier、tsc 都干净。
  • 重试仍然有上限:stops retrying after a bound 照常通过,因为只有重新加载这一用户操作才会重置计数。

变异测试发现的测试缺口

变异测试在当前 head 上杀掉 77 个中的 64 个。幸存者里有三处是真实的缺口:

  • N1。 把 'action_already_resolved' 从 ENDED_ACTION_CODES 删掉,测试仍然全绿。测试只覆盖了 action_expired 和 action_cancelled,而 R1-2 在真实环境里返回的是 action_already_resolved。
  • A3 和 A8。 findManagedApprovalTool 的"限定在 Action 所在 Turn"这一条件,两种工具行都没有测试钉住。测试里 Action 的 Turn 总排在最后,所以"匹配任意 Turn 中同 ID 的调用"也能得到同样结果。

candidate-r5-tests.patch 只加测试,共 +55 行。它能直接应用到当前 head,200/200 通过,并杀掉这三个变异体。

其余幸存者不是缺口。迟到回复相关的提交加了一层渲染时判断(answerError 只给当前显示的 Action),和原有的状态清理重叠:两者各自删掉都能存活,L3+N11、L3+H10 成对删除才会被测试发现。剩下的变异对测试和页面都没有可观察的影响,或是以前就存在的幸存者(H15、P5、N7)。

未覆盖

本 head 的 CI:发帖时 8 项通过,Capture web-shell visuals 与 web-shell E2E Smoke 仍在进行。

证据(图、原始截图、两个 head 的场景日志、探针、变异运行、两个候选补丁):assets-pr13107 @ b2276304 / pr13107/r5

@wenshao

wenshao commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao

wenshao commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qqqys qqqys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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:59 now takes enabled: 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.
  • :62 withdraws the reader only on an affirmative false: const reader = enabled === false ? undefined : provider.actions.
  • The load effect at :98-106 keeps the two cases apart: the !reader || !sessionId branch is the one that calls setPending({ actions: [] }), and if (enabled === undefined) return undefined; at :106 skips the read without touching pending. enabled is in the dependency array at :159, so the read resumes on the next summary.
  • ManagedSessionsPage.tsx:169 passes 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, reader is undefined, so no /actions/query is 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 — respond throws on failed, cancelled and recovery_blocked, so an answer the service did not apply leaves the card up instead of hiding it as answered. toPendingAction returns [] for anything that is not kind === 'permission' with state === 'requested', and toSessionSummary sets actions: true only 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 is isNonRetryableClientError moved verbatim out of ManagedSessionsPage.tsx, not new logic; the page's local copy is deleted in the same diff. A non-retryable 4xx still calls setLoadError, so the page still renders the alert and a Retry that resets the budget, and nothing is swallowed silently.
  • managed-approval.ts — toManagedPermissionRequest binds id: 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 — extraDescriptionId is 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.

@yiliang114
yiliang114 added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit f73f6c3 Oct 1, 2026
51 checks passed
@chiga0

chiga0 commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

Post-merge review — reviewed at head 9fedb263a0d9a95d766ae92a2238f8a081b69bd6 (the last pre-merge commit fully read), base e083d6a6b8cc728d12133cbd098e8a9f8176c546. The PR merged to 72608f2c while the review was running; findings are against 9fedb263.


Summary

No 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

packages/web-shell/client/components/managed/ManagedSessionsPage.tsx:586 (managed.approval.forbidden branch)

When respond returns action_forbidden (HTTP 403), the ToolApproval card is restored to the screen with no disabled prop and no visual indication that the current user cannot answer. The "You don't have permission" message (managed.approval.forbidden) is rendered as a sibling <p> below the card, but both buttons remain clickable. A non-creator viewer who sees this message can keep clicking Allow or Deny, generating one 403 per click indefinitely until the Action expires.

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.

action_forbidden is intentionally absent from ENDED_ACTION_CODES (use-managed-actions.ts:17-21), so it does not take the silent-hide path the new commit added for action_expired / action_cancelled / action_already_resolved. That distinction is correct (a forbidden viewer is different from an ended action), but the card still needs either a disabled prop or a product-defined hide policy.


Verified claims

Class 10 — stated intent: "An unchanged expiry does not re-arm a one-second query loop" (use-managed-actions.ts:161-171)

Verified correct. earliestExpiry is computed as Math.min(...actions.map(a => a.expiresAt)) — a stable number whenever the action set is unchanged. React's useEffect compares dependencies by ===; a numeric value that does not change means the expiry effect does not re-run and no new setTimeout is set. The timer fires exactly once per distinct earliestExpiry value.

Class 4 — authorization: action_forbidden detection (ManagedSessionsPage.tsx:180-184)

Verified correctly wired end-to-end. JavaManagedAgentClient.toHttpError reads payload.error.code into JavaManagedAgentHttpError.code (java-managed-agent-client.ts:461-479). The page checks 'code' in approvalCause && approvalCause.code === 'action_forbidden', which matches that property. Creator-only enforcement is server-side (HTTP 403 on respondWebShellAction); the frontend correctly shows a distinct message rather than leaking any privilege.

Class 8 — replay divergence: still-settling answered actions (use-managed-actions.ts:179)

Verified correct. answered is a ReadonlySet<string> populated optimistically on click and cleared only on success, failure, or session change. action = actions.find(entry => !answered.has(entry.actionId)) skips any action in the set, so a re-read that returns a just-answered action as requested does not flash the card back.

R1-2 (definitively-ended actions) — addressed by commit 9fedb263

The new commit adds ENDED_ACTION_CODES = { action_expired, action_cancelled, action_already_resolved }, an endedAction(failure) helper, and endedAnswers ref. When respond throws one of these codes the card is kept in answered (hidden), endedAnswers records it, no error is shown, and a re-read is triggered. On the subsequent read, if the server still lists the action (race / stale report), stillListed removes it from answered so the card reappears — a clean safety net. endedAnswers.current.clear() in the session-reset effect (line 95) prevents the ref from leaking across sessions.


Scope and unreviewed dimensions

Reviewed: use-managed-actions.ts, managed-approval.ts, managed-request-error.ts, ManagedSessionsPage.tsx, java-managed-agent-provider.ts, managed-agent-provider.ts, java-managed-agent-client.ts, ToolApproval.tsx, generated schema types. Examined patches for all 19 changed files.

Not covered: local execution unavailable — rungs 1-3 not run; daemon provider (no actions property, no delta); test files reviewed for relevance but not executed.

Reviewed with AI assistance.

yiliang114 added a commit to yiliang114/qwen-code that referenced this pull request Oct 4, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants