Skip to content

feat(desktop): authorize client capabilities in managed sessions - #4143

Merged
YayoiNanoka merged 30 commits into
apache:mainfrom
YayoiNanoka:feat/client-capability-session-grants
Sep 1, 2026
Merged

feat(desktop): authorize client capabilities in managed sessions#4143
YayoiNanoka merged 30 commits into
apache:mainfrom
YayoiNanoka:feat/client-capability-session-grants

Conversation

@YayoiNanoka

Copy link
Copy Markdown
Contributor

Summary

Allow trusted macOS Desktop Client Capabilities to cross the managed execution boundary without switching the Session to Full Access.

  • separate provider acceptance from Host admission so approval completes before T1 and execution
  • persist Session Grants and reuse them for matching Browser Origins
  • attest Browser Origins before admission and limit cross-Origin results
  • keep Desktop Settings on its existing read/update-confirmation path
  • present a narrow Client Capability approval in the existing composer interaction slot

This is the first stacked slice for #4012. Computer Use and Desktop MCP are intentionally left to follow-up PRs.

Refs #4012

Verification

  • Focused Node test suites passed for the Client Capability broker/coordinator/protocol, Interaction coordinator/projector, Session Grant storage, Desktop Browser/session/native capability paths, permission IPC, and Workbar adapter.
  • Relevant Core, Runtime Host, preload, and renderer type checks passed.
  • git diff --check main...HEAD passed.
  • Full root lint/build/typecheck was not run; the current checkout has unrelated existing Runtime/UI type errors and missing sigstore type packages in the Desktop main configuration.

Draft follow-up

  • Attach a screenshot of the Client Capability approval card.
  • Confirm the required repository CI on this exact head.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: OpenAI Codex contributed implementation, tests, commit preparation, and this PR description. Each affected commit carries Generated-by: OpenAI Codex.

This draft was prepared and submitted by OpenAI Codex on behalf of the human contributor of record, YayoiNanoka.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

@github-actions github-actions Bot added the effort/XL Over 1000 readable lines label Aug 29, 2026
@YayoiNanoka
YayoiNanoka force-pushed the feat/client-capability-session-grants branch 7 times, most recently from 35facba to b7dd148 Compare August 30, 2026 07:42
@YayoiNanoka
YayoiNanoka marked this pull request as ready for review August 30, 2026 07:49

function navigationResult(url: string, requiresApproval: boolean): string {
return (
`Navigated to ${sanitizedPageUrl(url)}.` +

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.

[P1] Redact destination details beyond the Origin before approval

requiresApproval means this navigation crossed into an Origin for which the Session has no Grant, but sanitizedPageUrl() still exposes its pathname. A destination such as https://other.example/reset/<token>?source=...#account therefore reveals <token> to the model before other.example is approved.

Please format the two cases separately: when requiresApproval is true, parse only HTTP(S) URLs and return at most url.origin (or a generic unapproved-site marker), dropping userinfo, pathname, query, and fragment; when it is false, preserve the current origin + pathname result for same-Origin navigation. Invalid or non-HTTP(S) destinations should remain generic.

Please also cover sensitive path segments for redirect and click navigation, including an A→B→A or multi-hop case, so the first violated URL recorded by the monotonic lease cannot leak destination details.

@YayoiNanoka
YayoiNanoka force-pushed the feat/client-capability-session-grants branch 3 times, most recently from 2ba3ae1 to f6d014a Compare August 30, 2026 11:34

@Astro-Han Astro-Han 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.

Thanks for moving managed Client Capability admission into the Runtime Host and keeping the approval/grant commit atomic. One current-main integration boundary still needs a manual merge rather than choosing either side of the conflict. This is a suggestion from an outside review, so please do push back if the final owner-association contract differs from what is now on main.

AI-assisted review disclosure: Codex ran independent authority, security, recovery, and test analysis lanes; Astro-Han is the contributor of record for this review.

readonly providerId: string;
readonly principalId: string;
readonly clientInstanceId: string;
readonly principalKind: ClientCapabilityConnectionIdentity['principalKind'];

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.

[P1] (category ③ — Client/provider trust boundary)

Thanks for making the local Desktop provider trusted for this slice. During rebase, this identity state also needs to preserve the Client-owner binding that current main added in #4187. This head still falls back to the sole global provider when the initiating Client has no same-ID provider, and the state does not carry credentialBoundClientInstanceId / capabilityOwner; a remote owner can therefore inherit an unrelated trusted Desktop provider whenever it is the only candidate. Keeping only the PR side loses remote isolation, while keeping only main leaves the local local_owner untrusted and breaks the intended Auto Browser path. Could the conflict resolve to one authority that preserves #4187’s credential-bound owner association and remote fail-closed behavior while explicitly admitting the authenticated local owner, with local Auto / remote unrelated / remote associated regressions? Please feel free to push back if another final identity seam supersedes this coordinator.

@Astro-Han Astro-Han 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.

Thanks for the substantial recovery and ownership work in this update. One cancellation seam still appears able to keep the current Code Mode run alive indefinitely. This is an AI-assisted review; I independently traced the broker, interaction, and drain paths. These are suggestions from an outside perspective, so please do push back if the approval lifetime is intentionally governed by a different invariant.

turnId: options.context.turnId,
runId: options.context.runId,
toolCallId: options.context.toolCallId,
providerSignal: prepared.providerSignal,

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.

Thanks for adding Host approval here. In the category ② cancellation/recovery path, could the caller signal also close this approval? After the provider sends accepted, the invocation broker clears its timer. If Code Mode’s 30s deadline or the caller then aborts, the broker settles/removes the invocation and sends cancel/release, but it does not abort prepared.providerSignal; requestClientCapabilityApproval() listens only to that provider signal. prepareTool() therefore remains blocked on this decision, and executeCodeCell() waits for the host-operation drain before returning. The same pending approval promise can also be reused by later calls in the scope. That turns a bounded caller timeout into an unbounded turn/Code Mode hang, so I believe this is P1. Could approval observe the caller cancellation too, close the durable interaction, clear the pending entry, and add an accepted → approval pending → caller abort regression? Please push back if approvals are deliberately allowed to outlive callers and another owner guarantees the drain still terminates.

@YayoiNanoka
YayoiNanoka force-pushed the feat/client-capability-session-grants branch 3 times, most recently from f32104d to 600ba0c Compare August 31, 2026 07:32
@YayoiNanoka

Copy link
Copy Markdown
Contributor Author

@Astro-Han Thanks for the detailed review. Both P1 findings have now been addressed on the current head.

Client/provider trust boundary

Resolved in 8c347f669.

The rebased coordinator now preserves the owner-association contract from #4187:

  • credentialBoundClientInstanceId and capabilityOwner remain part of the provider state.
  • Only a capability_provider may declare a capability owner.
  • Remote provider association requires an exact principalId and clientInstanceId match.
  • A remote owner no longer falls back to an unrelated sole provider.
  • An authenticated local_owner remains trusted for the local Desktop path.

Regression coverage includes the local Auto path, a remote owner spoofing the built-in names, an unrelated remote provider, and an exactly associated remote provider.

Caller cancellation while approval is pending

Resolved in 600ba0c6b.

The caller signal is now forwarded into Client Capability approval and observed independently from the provider signal:

  • A caller timeout durably closes the approval as timed_out.
  • Other caller cancellation closes it as turn_stopped.
  • Signal listeners and the shared pending-scope entry are cleared during settlement.
  • The cancelled invocation never reaches T1 or admitted.

The end-to-end regression covers accepted → approval pending → caller timeout, verifies that the durable interaction closes and the run drains, then retries the same scope with a new request ID and completes normally after approval.

中文

感谢详细审查。这两个 P1 问题都已在当前 head 中修复。

Client/Provider 信任边界

已在 8c347f669 中修复。

rebase 后保留了 #4187 引入的 Owner 绑定:

  • Provider 状态继续携带 credentialBoundClientInstanceIdcapabilityOwner
  • 只有 capability_provider 可以声明 Capability Owner。
  • 远程 Provider 必须精确匹配 principalIdclientInstanceId
  • 远程 Owner 不会再因为只有一个候选 Provider 就继承无关能力。
  • 经过认证的本地 local_owner 仍可用于 Desktop Auto 路径。

测试覆盖了本地 Auto、远程名称伪造、无关远程 Provider,以及精确关联的远程 Provider。

等待审批期间的 Caller 取消

已在 600ba0c6b 中修复。

Caller 的取消信号现在会传入 Client Capability 审批流程,并与 Provider 断开信号独立处理:

  • Caller 超时会将审批持久化关闭为 timed_out
  • 其他 Caller 取消会关闭为 turn_stopped
  • 完成后会清理监听器和相同 Scope 的共享等待项。
  • 被取消的调用不会进入 T1 或 admitted

端到端测试覆盖了 accepted → 等待审批 → Caller 超时,并验证审批被关闭、Run 可以退出;随后再次调用相同 Scope 时会创建新的审批请求,并能在批准后正常完成。

@YayoiNanoka

Copy link
Copy Markdown
Contributor Author

@me2seeks Thanks for catching this. This P1 has been resolved in d6e39c4e5 on the current head.

The two result paths are now handled separately:

  • When approval is required, HTTP(S) destinations expose only url.origin; userinfo, pathname, query, and fragment are omitted.
  • Invalid or non-HTTP(S) destinations return the generic an unapproved page marker.
  • When approval is not required, same-Origin navigation retains the existing origin + pathname result while still dropping query and fragment.

The monotonic Origin lease continues to retain the first violated URL, but that value is passed through the approval-required formatter before reaching the model. Regression coverage includes sensitive redirect and click paths, non-web destinations, snapshot and takeover A→B→A transitions, and the Provider second-check → first-page-await gap.

中文

感谢指出这个问题。该 P1 已在当前 head 的 d6e39c4e5 中修复。

现在会分别处理两种返回路径:

  • 需要审批时,HTTP(S) 目标最多只返回 url.origin,不会暴露 userinfo、pathname、query 或 fragment。
  • 无效地址或非 HTTP(S) 地址只返回通用的 an unapproved page 标记。
  • 不需要审批时,同 Origin 导航继续返回 origin + pathname,同时仍会去除 query 和 fragment。

单调 Origin lease 仍会记录首次违规 URL,但该值在返回模型前必须经过“需要审批”的脱敏格式化。回归测试覆盖了包含敏感路径的 redirect 和 click、非 Web 地址、snapshot 与 takeover 中的 A→B→A,以及 Provider 第二次检查到首次页面 await 之间的竞态窗口。

@me2seeks
me2seeks self-requested a review August 31, 2026 08:15
clientCapabilityScopeIdentity(target.scope),
].join('\0');
const existing = this.#pendingApprovals.get(key);
if (existing) return existing;

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.

[P1] Make every shared-scope waiter observe its own caller cancellation

When call A owns the pending approval and call B reaches the same key, this branch returns the Promise owned by A directly, so the callerSignal of B is never observed by the interaction coordinator. The invocation broker does react to the abort of B, but prepareTool() remains blocked on the approval of A and the cancelled call cannot drain.

I reproduced this on the current head with two real ToolRuntime.settleToolCall() calls: both reached provider accepted, they shared one pending interaction, then B was aborted. B remained unsettled until A was approved; only then did B continue and fail at the pre-T1 abort check. Adding a per-waiter abort race here made the same regression pass while leaving the approval of A intact.

Please make each joined waiter independently race its own caller signal, without closing the owner approval merely because a joiner cancels; cancel the prepared invocation of that waiter; and cover: two accepted calls → one shared approval → abort B → B settles before any decision → A can still be approved and publish the Session Grant.

@YayoiNanoka
YayoiNanoka force-pushed the feat/client-capability-session-grants branch from 7e4319a to f07de3f Compare August 31, 2026 10:16
@YayoiNanoka

Copy link
Copy Markdown
Contributor Author

@me2seeks Thanks for catching this. Fixed in f07de3f.

Each waiter that joins an existing shared-scope approval now races that approval against its own callerSignal. If joined call B is aborted, only B stops waiting and its prepared provider invocation is cancelled; the owner approval created by call A remains open and is not removed or closed.

The integration regression uses real ToolRuntime.settleToolCall() calls and verifies the requested sequence:

  • A and B both reach provider accepted and share one pending approval.
  • B is aborted and settles before any approval decision.
  • The provider receives cancellation for B.
  • A remains pending and can still be approved.
  • Approval publishes the Session Grant, followed by T1 and provider admission for A.

I also rebased the PR onto current main and preserved both this Client Capability admission ordering and #4287's durable Tool Result projection path.

Focused verification: the final ToolRuntime and shared-approval regression suite passed 13/13; the broader Client Capability, Browser, Interaction, storage, and UI suites passed 200/200.

中文

感谢指出这个问题,已在 f07de3f 修复。

现在每个加入共享 scope 审批的等待者,都会使用自己的 callerSignal 与共享审批独立竞争。B 被取消时,只会结束 B 自己的等待并取消 B 对应的 prepared provider invocation,不会关闭或移除 A 创建的审批。

集成回归测试使用真实的 ToolRuntime.settleToolCall(),覆盖了以下流程:

  • A、B 都到达 provider accepted,并共享一个待审批 Interaction。
  • 在用户尚未决定前取消 B,B 会立即结束。
  • Provider 收到 B 的取消。
  • A 的审批保持 pending。
  • 批准 A 后写入 Session Grant,然后执行 T1 并 admit A。

同时已 rebase 到最新 main,并保留 #4287 的 durable Tool Result projection 语义。

最终关键回归测试 13/13 通过;Client Capability、Browser、Interaction、storage 和 UI 的聚焦测试 200/200 通过。

@YayoiNanoka
YayoiNanoka force-pushed the feat/client-capability-session-grants branch from f07de3f to 4a36d4c Compare August 31, 2026 10:21
Generated-by: OpenAI Codex
Generated-by: OpenAI Codex
@YayoiNanoka
YayoiNanoka force-pushed the feat/client-capability-session-grants branch from 4a36d4c to 45f92e3 Compare September 1, 2026 03:48
Generated-by: OpenAI Codex
@YayoiNanoka
YayoiNanoka merged commit 409c71a into apache:main Sep 1, 2026
4 checks passed
abhinav-phi pushed a commit to abhinav-phi/maka that referenced this pull request Sep 1, 2026
…che#4143)

* refactor(runtime-host): separate capability acceptance from admission

Generated-by: OpenAI Codex

* feat(runtime-host): carry capability admission evidence

Generated-by: OpenAI Codex

* feat(storage): persist client capability session grants

Generated-by: OpenAI Codex

* refactor(core): centralize capability grant identity

Generated-by: OpenAI Codex

* feat(core): define client capability approval interactions

Generated-by: OpenAI Codex

* feat(storage): atomically commit capability approvals and grants

Generated-by: OpenAI Codex

* feat(runtime-host): host client capability approval interactions

Generated-by: OpenAI Codex

* feat(runtime): prepare client capabilities before durable dispatch

Generated-by: OpenAI Codex

* fix(storage): share capability grants by scope

Generated-by: OpenAI Codex

* feat(runtime-host): authorize managed client capabilities

Generated-by: OpenAI Codex

* feat(desktop): attest browser origins before admission

Generated-by: OpenAI Codex

* fix(desktop): limit cross-origin browser results

Generated-by: OpenAI Codex

* fix(runtime-host): close approvals when providers disconnect

Generated-by: OpenAI Codex

* feat(desktop): present client capability approvals

Generated-by: OpenAI Codex

* fix(runtime): classify client capability interaction events

Generated-by: OpenAI Codex

* fix(runtime-host): canonicalize client capability provider ids

Generated-by: OpenAI Codex

* fix(runtime): route client capability host admission

Generated-by: OpenAI Codex

* fix(desktop): enforce browser origin leases

Generated-by: OpenAI Codex

* chore(runtime-host): advance capability protocol epoch

Generated-by: OpenAI Codex

* chore(desktop): sync capability prompt inventory

Generated-by: OpenAI Codex

* test(desktop): complete side chat story port

Generated-by: OpenAI Codex

* test(runtime-host): honor capability admission handshake

Generated-by: OpenAI Codex

* test: trim duplicate capability coverage

Generated-by: OpenAI Codex

* test: simplify capability coverage

Generated-by: OpenAI Codex

* fix(desktop): redact unapproved browser destinations

Generated-by: OpenAI Codex

* fix(desktop): keep capability approval outside legacy shell

Generated-by: OpenAI Codex

* fix(runtime-host): preserve local capability trust

Generated-by: OpenAI Codex

* fix(runtime-host): close approvals with callers

Generated-by: OpenAI Codex

* fix(runtime-host): isolate shared approval waiters

Generated-by: OpenAI Codex

* docs: refresh Astryx surface inventory

Generated-by: OpenAI Codex

---------

Co-authored-by: YayoiNanoka <275864479+YayoiNanoka@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/XL Over 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants