feat(managed-agent): add pinned extension package management (#15490)

* feat(managed-agent): support pinned managed extension packages

* fix(managed-agent): align extension package CI contracts

* test(managed-agent): pin package audit ambiguities

* fix(managed-agent): report unavailable settings and satisfy broker style

* fix(managed-agent): retain queue inventory in extension integration

* test(managed-agent): preserve pinned context across Todo refresh
This commit is contained in:
jinye authored and GitHub committed 2026-10-26 01:19:27 +08:00
1 parent 9c0414ab42
commit 41ce46af24
212 files changed
+25916 -573

No files matched your search

@@ -87,11 +87,13 @@ matches its own route lines.
is mounted with no legacy tag and no exclusion appears in the column, which
is the signal #15075 asked for. The remaining labels are pinned in
`MANAGED_ONLY_REPORTED`, so an addition fails the suite until someone
records it. The column currently reports three labels, each a public and a
WebShell route pair: `SESSION_CONTEXT_COMPRESSION` compresses a Session's
context, `SESSION_RECOVERY_QUERY` reads its recovery state, and
`SESSION_RECOVERY_RECHECK` re-runs its recovery checks. The daemon
advertises no tag for any of the three.
records it. The column currently reports twelve labels: nine `EXTENSION_*`
labels describe package publication, archive access, settings, operation
queries and Session configuration. Their Session-pinned contract differs
from the daemon's extension APIs; recording them does not establish legacy
API compatibility or platform acceptance. `SESSION_CONTEXT_COMPRESSION`
compresses a Session's context, `SESSION_RECOVERY_QUERY` reads its recovery
state, and `SESSION_RECOVERY_RECHECK` re-runs its recovery checks.
## Acceptance
@@ -54,7 +54,7 @@ mvn -Dtest=ManagedSurfaceCaptureTest -Dcapability.parity.capture=true test
- 矩阵不判断某条路径是否应复制另一条路径。
- internal writer 路由不在 OpenAPI 文档中。只有注册表列出它们。
- `managed only` 列由推导得出:列出副本中没有任何 `managed` 行引用的路由标签。排除集合记录某个标签为何不算缺口,且其中每一项都必须指向副本真实记录的标签。新挂载且没有旧标签、也没有排除理由的路由会出现在该列,这正是 #15075 要求的信号。剩余标签由 `MANAGED_ONLY_REPORTED` 固定,因此新增项会使测试失败,直到有人记录其记账方式。该列目前报告三个标签,每个都对应 public 与 WebShell 一对路由:`SESSION_CONTEXT_COMPRESSION` 压缩会话上下文,`SESSION_RECOVERY_QUERY` 读取会话恢复状态,`SESSION_RECOVERY_RECHECK` 重跑会话恢复检查。守护进程对这三者都没有标签。
- `managed only` 列由推导得出:列出副本中没有任何 `managed` 行引用的路由标签。排除集合记录某个标签为何不算缺口,且其中每一项都必须指向副本真实记录的标签。新挂载且没有旧标签、也没有排除理由的路由会出现在该列,这正是 #15075 要求的信号。剩余标签由 `MANAGED_ONLY_REPORTED` 固定,因此新增项会使测试失败,直到有人记录其记账方式。该列目前报告十二个标签:九个 `EXTENSION_*` 标签描述包发布、archive 访问、设置、操作查询及 Session 配置。它们按 Session 固定版本的合同与守护进程的扩展 API 不同;登记这些标签不代表旧 API 兼容或平台验收。`SESSION_CONTEXT_COMPRESSION` 压缩会话上下文,`SESSION_RECOVERY_QUERY` 读取会话恢复状态,`SESSION_RECOVERY_RECHECK` 重跑会话恢复检查。
## 验收
@@ -0,0 +1,211 @@
# Managed extension packages
[English](2026-10-24-managed-extension-packages.md) | [简体中文](2026-10-24-managed-extension-packages.zh-CN.md)
## Status and baseline
Implementation based on `101b3d416de4b7a7dca094afd475549013d8b7d5`.
The code is under verification. This document describes the accepted design,
not completed runtime qualification.
## Problem and scope
Managed Sessions can load published static Skills, MCP pins and Hook catalogs,
but cannot install, distribute and explicitly apply a complete extension package.
An ordinary installed directory is not portable: loaders expand host paths and
environment variables, converters can rewrite plugin roots, and release selection
uses the installer's platform. Copying it would move the wrong bytes or secrets.
The first profile supports static Skills and assets, commands, declarative agents,
context, MCP, Hooks and versioned settings. It rejects dynamic Skill permissions,
Hooks, model overrides and path activation, agent-private MCP/Hooks, executors,
LSP, channels and workflows before lossy conversion. It does not build dependencies.
The execution targets are local Linux, SSH Linux and opt-in native macOS developer
process-group supervision. This is not full ordinary-CLI semantic compatibility.
## Package and settings ownership
Prepare original Git/npm/directory/archive inputs with existing acquisition,
conversion and parsing primitives, explicit target platform and explicit roots.
Never initialize Config or mutate process environment/cwd during preparation.
Keep placeholders until compilation on the target Runtime. Imported installed
extensions must be prepared again from their original source.
Canonical archives are tar.gz with a root manifest. The package digest is SHA-256
of archive bytes; metadata records the source revision, target and compiler version.
Enforce the existing 100 MiB compressed, 1 GiB expanded and 100,000-entry ceilings
for every source. Reject escaping resources and unsupported archive links.
External interpreters remain target dependencies, not package resources.
The control plane owns immutable archives in a configured durable artifact
directory and SQL metadata, operations and references scoped to tenant/Workspace.
Use bounded binary streams, not base64 archives in JSON or Session resources.
Production replicas need shared durable storage. Runtime private-state directories
are verified caches, never the only recovery source. Retain installed versions,
Session/child pins, pending recovery and history references before collecting data.
Settings are immutable encrypted revisions. Reuse SecretProtector/key rotation;
bind encryption context to tenant, Workspace, extension and settings revision.
Do not copy source Keychain selectors, .env or credential sidecars. Deliver exact
settings only to the selected Runtime and extension subprocess. Public responses,
journals and browser persistence contain references/configured keys, never values.
Publishing and Runtime execution are separate disabled-by-default settings. The
control plane requires a configured durable artifact directory and permitted actor;
the Workspace OPERATOR grant remains mandatory. Runtime execution additionally
selects the platform supervision profile and private cache directory. Published
versions and settings are retained; this increment does not run GC.
See the [deployment configuration](../../packages/managed-agent-server/distribution/config/application.example.yml).
## Session configuration and recovery
Keep AgentDefinition identity and the base tool profile immutable. An effective
configuration combines the base definition, independent MCP selections, mandatory
base Hooks and selected package contributions. Create/load/resume/recovery use the
same binding; package pins cannot masquerade as definition pins or upgrade a profile.
Session creation accepts optional `extensionPins`. Omission or null preserves the
legacy path; an explicit empty array selects an empty package configuration.
Include the exact pins in the creation idempotency digest and authorize package
membership and deployment policy before creating the Session.
Initial pins, including an explicit empty selection, require the supported Shell/1
or MCP/1 profile with negotiated lifecycle v2 before a Session or creation receipt is persisted. Version 2 of the
existing creation receipt keeps immutable `initialSelection` beside the optional
static Skills snapshot; version 1 retains its meaning. No installation ledger or
schema column is added. Private create/load carries that same expected-revision-0
command. Bootstrap through normal config_install before the first model submission,
and reject a different command or any bootstrap after the initial Turn was submitted.
An incomplete bootstrap keeps inference blocked and recovery targets its original owner.
Before that first submission, the exact initial command may also recover its previous
content as revision 2 under the normal successor checks. Child Sessions accept only
continuation or explicit recovery of their original bootstrap operation; an operator
cannot replace their package selection. Cold management loads stay passive so a failed
bootstrap can be repaired without admitting inference.
SSH child execution requires a newly persisted public-child contract and the enabled
extension execution profile. Deployment changes do not promote an older Session.
Parent and child retain their own Runtime owners and credentials. A package launch
checks the parent's original READY binding, generation and active slot under lock;
remote worktree and snapshot children remain unavailable.
Evolve the existing config_install domain with a closed tagged body, retaining
managed-config_install/1 and old MCP records. Sessions without packages keep the
old path. The first apply imports independent MCP/base Hooks; subsequent MCP writes
use the unified adapter, even after all packages are removed. Journal phase,
effective configuration and MCP component revisions are distinct counters.
Apply is deliberately stop-and-reconfigure: prepare, stage immutable inputs and
verify both stage ACKs; atomically commit config_install/config.bound and an admission
barrier; drain changed Session-owned components; enable replacements; verify both
enable ACKs; publish active. Staging does not connect MCP or execute Hooks. A stage
failure leaves the old configuration usable. After commit, reconcile the original
operation/owner; unknown results retain the barrier and cannot be replayed elsewhere.
Explicit recovery to previous content first proves replacement owners stopped,
then compiles that content into new effective catalog/configuration revisions.
Runtime reconciliation accepts the exact original prepared, pending or active operation,
including after a restart, and refuses to stop a newer configuration.
Never decrement Hook/MCP revisions, erase history, reset base once consumption or
re-run SessionStart. A Session update must not close/seal/retire the whole Runtime.
## Component and platform wiring
Keep source provenance and native naming/conflict rules for Skills. Legacy static
Skills with ambiguous origin conflict explicitly instead of being guessed/replaced.
Expose pinned package assets through a read-only source namespace, not widened
Workspace paths or file history. Shell extension context supplies target package
roots while retaining Session cwd. Cache drift refuses use and permits reconstruction.
Product read-only access does not establish a new same-UID Shell sandbox.
The model-facing tool schema enumerates selected public package pins. Read and
Glob accept a package-relative path or `extension://<extensionId>/<path>` with the
matching explicit source; instruction roots use that logical namespace. Shell
receives `QWEN_EXTENSION_ROOT` through its explicit extension context. No private
cache path or settings value is added to instruction text. Upgrading a selected
package preserves its exact settings revision; users may explicitly reuse the
latest saved settings revision without reading its values.
Commands reuse parsers and injection/escaping rules. File/Shell expansion uses
stable Runtime invocations with Hooks, approval and durable receipts. Save expansion
before inference; recovery must not execute it twice. User order is
UserPromptSubmit, expansion, UserPromptExpansion, inference. Model command calls
dispatch through skill in their own batch; static Skills retain instruction-only
behavior. Child launch snapshots pin the parent binding, agent descriptor, tool
restrictions and configured model route; Java validates membership before creation.
Keep depth/quota/cancellation/relay contracts and never widen parent permissions.
Target dependency preflight checks directly resolvable commands and interpreters
without running Hooks or connecting MCP. Authored complex Shell expressions remain
subject to the real approved invocation. Argument placeholders in nested Shell
substitutions, backticks and heredocs are explicitly refused rather than inserted
with an unproven escaping context.
Linux owner shutdown uses process-tree evidence; SSH needs actual startup, scoped
configuration, drain, retirement and recovery implementations, not a boolean flip.
Native macOS developer mode is off by default and only proves registered process
groups stopped. Persist the supervision mode in registration and receipts. Old
proofs cannot certify new owners, and production verifiers reject developer proofs.
Only an original Runtime negotiating the new contract may apply packages.
## Interfaces, rollout and verification
Add Workspace package publication/catalog/settings/operation APIs and Session
selection/query/apply/recovery APIs to public/WebShell/SDK surfaces. Publication
requires Workspace OPERATOR and deployment source/actor/execution policy. Commands
use stable idempotency keys and expected configuration revisions. Status is read-only:
it never creates Sessions, cold-loads Harnesses or resubmits work. Reuse the manager
UI with a distinct Managed provider and abort stale target requests.
Catalogs contain at most 256 packages; a Session may select at most 32.
Only the explicit pre-admission `extension_config_revision_conflict` response
clears a pending apply and refreshes its configuration revision. Generic conflicts
and unknown outcomes retain the original selection and idempotency key.
Implement protocol/storage, local execution, SSH/macOS profiles, then fault gates.
Test cross-platform source preparation, secrets/root isolation, cache reconstruction,
all seven components, apply/disable/remove/recovery, independent Sessions, preserved
base MCP/Hook state, commit/drain/enable/ACK loss, permissions and old logs/profiles.
Use real MySQL/process/SSH evidence and distinguish Linux cleanup from macOS scope.
Follow affected-package checks and the Java module's shared ten-minute local budget;
CI owns complete platform/fault matrices. Evidence lives in
`.qwen/e2e-tests/managed-extension-packages.md`; incomplete gates stay explicit.
## Implementation and qualification
The implementation includes the raw compiler, durable archive/settings APIs,
unified configuration ledger and barriers, Runtime component/asset wiring,
public child snapshots, SDKs and Managed WebShell provider. Both publication and
execution remain opt-in. A missing or drifted private cache is refused by ordinary
tools; explicit apply can verify the authoritative archive and reconstruct the
same private path without widening Workspace access.
Focused TypeScript tests include two extensions whose real MCP/Hook subprocesses
consume identically named settings without seeing the other extension's values.
The native macOS worker HTTP slice exercises attestation, upload, activation,
read-only assets, file-history isolation and Shell. These tests do not establish
Linux process-tree or SSH cleanup.
A real MySQL/control-plane/Hosted Harness/native macOS run reached initial active
revision 1, passed context and static Skill delivery, a real MCP effect, Hook
invocation and public approval. Its allotted Java window ended during command
expansion; command completion, child invocation, update, compensation and restart
non-replay remain unqualified by that run. It also exposed missing Hook setting
injection, subsequently fixed and verified by the isolated real-process test above.
Selected MySQL configuration/creation cases passed; a new child-admission case
reached an invalid test policy, which was corrected but not rerun locally.
The module's shared local Java verification used approximately 596 seconds of its
600-second budget. Complete Java/SpotBugs/MySQL, seven-component execution,
Linux/SSH/macOS cleanup, permission revocation and commit/drain/start/ACK/response
fault matrices remain required CI gates. A Harness/control-plane restart may
reconnect a surviving original Runtime; this increment does not claim transparent
replacement of a lost Runtime process or recovery after host reboot. Lost original
owners keep their existing fail-closed recovery rules and must never replay work.
The existing CI collects the new unit/contract/MySQL test classes, but does not yet
automate the complete package platform/fault matrix. The existing static
`HostedExtensionsIT` cannot substitute for that remaining qualification.
## Open decisions
None for the accepted first profile. Unsupported dynamic semantics require a
separate activation contract rather than silently expanding this profile.
@@ -0,0 +1,159 @@
# Managed 扩展包
[English](2026-10-24-managed-extension-packages.md) | [简体中文](2026-10-24-managed-extension-packages.zh-CN.md)
## 状态与基线
基于 `101b3d416de4b7a7dca094afd475549013d8b7d5` 实现。
代码正在验证中。本文记录已采纳设计,不代表运行验收完成。
## 问题与范围
Managed Session 已能加载发布的静态 Skills、MCP pins 和 Hook catalog,
但不能安装、分发和显式应用完整扩展包。普通已安装目录不可直接移植:
加载器展开本机路径与环境变量,转换器会改写 plugin root,Release 选择依据安装机平台。
直接复制会带入错误字节或凭据。
首期支持静态 Skills 与资产、commands、声明式 agents、context、MCP、Hooks 和版本化 settings。
在有损转换前拒绝动态 Skill 权限、Hooks、模型覆盖、路径激活,agent 私有 MCP/Hook,
executor、LSP、channels 和 workflows。不构建依赖。
执行目标为本地 Linux、SSH Linux,以及显式开启的原生 macOS 开发者进程组监管。
这不表示兼容普通 CLI 的所有语义。
## 包与设置归属
对原始 Git/npm/目录/archive 输入复用现有获取、转换和解析原语,显式传入目标平台和根。
准备时不初始化 Config,不修改共享进程环境或 cwd。
保留占位符,直到目标 Runtime 编译。导入已安装扩展须重新获取原始来源。
规范 archive 为根目录包含 manifest 的 tar.gz;包摘要为 archive 字节的 SHA-256,
元数据记录来源修订、目标平台和编译器版本。所有来源统一执行既有压缩 100 MiB、
展开 1 GiB、100,000 个条目的上限。拒绝逃逸资源和不支持的 archive 链接。
外部解释器是目标依赖,不冒充包内资源。
控制面在配置的耐久 artifact 目录保存不可变 archive,SQL 按租户与 Workspace
保存元数据、操作和引用。使用有界二进制流,不把 base64 大包放进 JSON 或 Session 资源。
生产多副本要求共享持久存储。Runtime 私有 state 目录仅作校验缓存,不是唯一恢复来源。
安装版本、Session/child pin、未决恢复和历史引用未释放前,不回收字节。
Settings 使用不可变加密修订,复用 SecretProtector 与密钥轮换;加密上下文绑定租户、
Workspace、extension 和 settings revision。不复制源 Keychain selector、.env 或凭据 sidecar。
只向选定 Runtime 及所属扩展子进程交付精确设置。公开响应、日志和浏览器持久化仅含引用、
已配置键,不含值。
发布和 Runtime 执行分别默认关闭。控制面要求配置耐久 artifact 目录及允许的 actor,
Workspace OPERATOR 授权仍为必要条件。Runtime 执行还需选择平台监管 profile 与私有缓存目录。
此增量保留已发布版本和 settings,不执行 GC。
参见[部署配置](../../packages/managed-agent-server/distribution/config/application.example.yml)。
## 会话配置与恢复
保持 AgentDefinition 身份与基础工具 profile 不变。effective configuration 组合基础定义、
独立 MCP 选择、不可删除的基础 Hooks,以及所选包贡献项。
创建、加载、resume、恢复读取同一 binding;包 pin 不冒充 definition pin,也不提升 profile。
创建 Session 接受可选的 `extensionPins`:省略或 null 保持旧流程,显式空数组选择空的新格式包配置。
创建幂等摘要包含精确 pins,并在创建 Session 前核验包所属 Workspace 及部署权限。
初始 pins(包括显式空选择)要求受支持且已协商 lifecycle v2 的 Shell/1 或 MCP/1 profile,在持久化 Session 或创建 receipt 前拒绝不兼容 profile。
现有创建 receipt 的 v2 在可选静态 Skills 快照旁保存不可变的 `initialSelection`,v1 保留原语义,
不新增安装账本或 schema 列。私有 create/load 传递同一 expectedRevision 为 0 的命令,
首次模型提交前通过正常 config_install 完成 bootstrap;其他命令或首次 Turn 已提交后的 bootstrap 均拒绝。
初始化未完成时阻止推理,恢复仍只针对原操作及原执行者。
在首次提交前,精确初始命令也可通过正常 successor 校验,以第 2 版恢复之前内容。
子 Session 只接受原 bootstrap 操作的继续或显式恢复,operator 不能替换其包选择。
冷启动管理加载保持被动,让失败初始化可修复,同时继续阻止推理。
SSH 子执行要求新创建时持久化的 public-child 合同及已开启的 extension 执行 profile,
修改部署不会提升旧 Session。父子保留各自 Runtime owner 和凭据;包 agent 启动在锁内核验
父配置的原 READY binding、generation 和 active slot。远程 worktree 与 snapshot 子任务仍不可用。
在既有 config_install domain 增加闭合 tagged 正文,保留 managed-config_install/1 和旧 MCP 记录。
不使用包的会话保持旧路径。首次 apply 导入独立 MCP/基础 Hooks;进入新格式后,
即使移除全部包,MCP 写入也继续统一适配。日志 phase、实际配置和 MCP component revision 分开。
应用采用有停顿的排空重配:prepare,stage 不可变输入并核验双方 ACK;同事务提交
config_install/config.bound 与准入屏障;排空变更的 Session 组件;启用替代项;
核验双方 enable ACK;发布 active。Stage 不连接 MCP、不执行 Hook。
Stage 失败时旧配置继续可用;commit 后只对账原 operation/owner。
结果未知时保持屏障,不在另一 Runtime 重放。
显式恢复上一版先证明替代 owner 已停止,再将旧内容编译为新的 effective catalog/configuration
修订。不降低 Hook/MCP 版本、不擦历史、不重置基础 once、不补跑 SessionStart。
Runtime 对账接受精确的原 prepared、pending 或 active 操作,包括重启后的状态;不能停止较新的配置。
更新一个 Session 不能关闭、封闭或退役整个 Runtime。
## 组件与平台接线
Skills 保留真实来源及原生命名、冲突规则;旧静态 Skill 来源不明时明确冲突,不猜测替换。
固定包资产使用只读 source 命名空间,不放宽 Workspace 路径、不进入 file history。
Shell 的扩展执行上下文提供目标包根,cwd 仍是 Session 目录。
缓存漂移时拒绝使用并可重建。产品只读访问不意味着新增同 UID Shell 沙箱。
模型工具 schema 列出已选的公开 package pins。Read/Glob 在匹配的显式 source 下接受
包内相对路径或 `extension://<extensionId>/<path>`,指令根使用该逻辑命名空间。
Shell 通过显式扩展上下文取得 `QWEN_EXTENSION_ROOT`。指令正文不添加私有缓存路径或设置值。
升级所选包保留精确 settings revision;用户可以显式复用最新保存修订,无须读取值。
Commands 复用 parser、injection 与转义规则;文件/Shell 展开使用稳定 Runtime invocation,
遵守 Hook、审批和持久回执。先保存展开结果再推理,恢复不重复执行。
用户顺序为 UserPromptSubmit、展开、UserPromptExpansion、推理。
模型命令通过 skill 分派并使用独立批次;静态 Skills 保持仅加载指令。
Child launch snapshot 固定父 binding、agent descriptor、工具限制和已配置模型路由,
Java 核对来源成员关系后创建。保留层数、限额、取消、中继,不扩大父权限。
目标依赖预检只检查可直接解析的命令和解释器,不运行 Hook、不连接 MCP。
作者提供的复杂 Shell 表达式仍由真实审批调用检查。嵌套 Shell substitution、
backtick 和 heredoc 中的参数占位符明确拒绝,不在未经证明的转义上下文中插入参数。
Linux owner 停止使用进程树证据。SSH 必须实现启动、按范围配置、排空、退役与恢复,
不能仅翻转布尔开关。原生 macOS 开发者模式默认关闭,只证明登记的进程组已停止。
注册和回执持久保存监管模式。旧证明不能认证新 owner,生产验证器拒绝开发者证明。
只有原 Runtime 协商新合同时才允许 apply。
## 接口、发布与验证
公开、WebShell、SDK 增加 Workspace 包发布/目录/settings/operation 接口,
以及 Session 选择、查询、apply、恢复接口。发布要求 Workspace OPERATOR 加部署来源、
actor 和执行策略。命令使用稳定幂等键与预期配置版本。
状态查询只读,不创建 Session、不冷加载 Harness、不重提操作。
管理 UI 复用界面,使用独立 Managed provider,并取消旧目标请求。
目录最多包含 256 个包,每个 Session 最多选择 32 个。
只有明确在受理前返回的 `extension_config_revision_conflict` 才清除 pending apply 并刷新配置版本;
通用冲突和未知结果保留原选择及幂等键。
按协议/存储、本地执行、SSH/macOS profile、故障门禁顺序实施。
覆盖跨平台准备、凭据与根隔离、缓存重建、七类组件、应用/停用/移除/恢复、
同 Workspace 会话隔离、基础 MCP/Hook 保留、commit/drain/enable/ACK 丢失、权限和旧记录/profile。
使用真实 MySQL、进程和 SSH 证据,区分 Linux 清理与 macOS 范围。
遵守受影响包检查和 Java 模块共享十分钟本地验证预算;CI 承担完整平台/故障矩阵。
证据记录在 `.qwen/e2e-tests/managed-extension-packages.md`,缺失门禁保持明确。
## 实现与验收
实现包括原始包编译器、耐久 archive/settings API、统一配置账本与屏障、Runtime 组件及
资产接线、公开 child snapshot、SDK 和 Managed WebShell provider。发布与执行仍需显式开启。
普通工具拒绝缺失或漂移的私有缓存;显式 apply 可以校验权威 archive 并重建原私有路径,
不放宽 Workspace 访问。
定向 TypeScript 测试包含两个扩展的真实 MCP/Hook 子进程:读取同名设置,同时看不到
另一扩展的值。原生 macOS worker HTTP 切片覆盖身份核验、上传、激活、只读资产、
file-history 隔离和 Shell。这些测试不证明 Linux 进程树或 SSH 清理。
真实 MySQL/控制面/Hosted Harness/原生 macOS 运行达到初始 active revision 1,
通过 context、静态 Skill 交付、真实 MCP 副作用、Hook 调用和公开审批。
分配的 Java 窗口在 command 展开期间结束;此运行尚未验收 command 完成、child 调用、
更新、补偿及重启不重放。它也暴露了 Hook settings 注入缺失,随后已修复,并通过上述
独立真实进程测试。所选 MySQL 配置及创建用例通过;新增 child 准入用例触发了无效测试策略,
fixture 已修正,但未在本地重跑。
Java 模块共享本地验证预算已用约 596/600 秒。完整 Java/SpotBugs/MySQL、七类组件执行、
Linux/SSH/macOS 清理、权限撤销以及 commit/drain/start/ACK/response 故障矩阵,
仍是必要 CI 门禁。Harness/控制面重启可以重连仍存活的原 Runtime;此增量不宣称透明替换
丢失的 Runtime 进程或主机重启恢复。原 owner 丢失沿用既有 fail-closed 恢复规则,不能重放工作。
现有 CI 会收集新增单元、合同及 MySQL 测试类,但尚未自动化完整包的平台/故障矩阵。
既有静态 `HostedExtensionsIT` 不能替代这部分剩余验收。
## 未决决策
首期已采纳范围没有未决产品选择。不支持的动态语义另行实现激活合同,不能静默扩大本 profile。
@@ -39,6 +39,7 @@ const MANAGED_SURFACE_FILE = resolve(
const ROUTE_METHODS: ReadonlySet<string> = new Set([
'GET',
'POST',
'PUT',
'PATCH',
'DELETE',
]);
+20 -11
View File
@@ -330,10 +330,10 @@ export const CAPABILITY_PARITY: Record<ServeFeature, ParityRow> = {
'no Managed session refresh route',
),
extension_batch_activation_v2: NO_MANAGED_EQUIVALENT(
'no Managed extension management route',
'Managed packages use a separate Session-pinned lifecycle contract',
),
extension_activation_explicit_refresh: NO_MANAGED_EQUIVALENT(
'no Managed extension management route',
'Managed packages use a separate Session-pinned lifecycle contract',
),
workspace_skill_manage: NO_MANAGED_EQUIVALENT(
'no Managed skill management route',
@@ -410,7 +410,7 @@ export const CAPABILITY_PARITY: Record<ServeFeature, ParityRow> = {
workspace_hooks: { standing: 'managed', evidence: 'surface:HOOK_CATALOG' },
session_hooks: { standing: 'managed', evidence: 'surface:HOOK_CATALOG' },
workspace_extensions: NO_MANAGED_EQUIVALENT(
'no Managed extension management route',
'Managed packages use a separate Session-pinned lifecycle contract',
),
session_branch: NO_MANAGED_EQUIVALENT('no Managed branch route'),
session_branch_worktree: NO_MANAGED_EQUIVALENT('no Managed branch route'),
@@ -487,19 +487,19 @@ export const CAPABILITY_PARITY: Record<ServeFeature, ParityRow> = {
'issue #15075: no Managed memory CRUD',
),
extension_management_v2: NO_MANAGED_EQUIVALENT(
'no Managed extension management route',
'Managed packages use a separate Session-pinned lifecycle contract',
),
extension_list_details: NO_MANAGED_EQUIVALENT(
'no Managed extension management route',
'Managed packages use a separate Session-pinned lifecycle contract',
),
extension_state: NO_MANAGED_EQUIVALENT(
'no Managed extension management route',
'Managed packages use a separate Session-pinned lifecycle contract',
),
extension_git_credentials: NO_MANAGED_EQUIVALENT(
'no Managed extension management route',
'Managed packages use a separate Session-pinned lifecycle contract',
),
extension_local_path_install: NO_MANAGED_EQUIVALENT(
'no Managed extension management route',
'Managed packages use a separate Session-pinned lifecycle contract',
),
workspace_persisted_transcript: {
standing: 'managed',
@@ -620,13 +620,22 @@ export const MANAGED_ONLY_NOT_A_GAP = new Set([
* column checked — a route added or removed without a decision fails the
* suite — while the signal stays visible to whoever reads the matrix.
*
* Each label is a route pair on the public and the WebShell surface:
* The extension labels describe the Managed package lifecycle, whose
* Session-pinned contract differs from the daemon's extension APIs.
* `SESSION_CONTEXT_COMPRESSION` compresses a Session's context,
* `SESSION_RECOVERY_QUERY` reads a Session's recovery state and
* `SESSION_RECOVERY_RECHECK` re-runs its recovery checks. The daemon
* advertises no tag for any of the three.
* `SESSION_RECOVERY_RECHECK` re-runs its recovery checks.
*/
export const MANAGED_ONLY_REPORTED: readonly string[] = [
'EXTENSION_ARCHIVE',
'EXTENSION_CONFIG_APPLY',
'EXTENSION_CONFIG_GET',
'EXTENSION_GET',
'EXTENSION_LIST',
'EXTENSION_OPERATION',
'EXTENSION_PUBLISH',
'EXTENSION_SETTINGS_GET',
'EXTENSION_SETTINGS_SET',
'SESSION_CONTEXT_COMPRESSION',
'SESSION_RECOVERY_QUERY',
'SESSION_RECOVERY_RECHECK',
@@ -30,6 +30,7 @@ import {
import { findHostedApprovalContinuation } from './hosted-approval-continuation.js';
import { HostedChildAgentSession } from './hosted-child-agent-session.js';
import { hostedPublicPlanActive } from './hosted-plan-mode.js';
import { HostedSkillCatalog } from './hosted-skill-catalog.js';
import { slowHostBudgetMs } from '../test-utils/slow-test-host.js';
import type { HttpToolPublicationOwner } from '@qwen-code/qwen-code-core/managed-runtime/http-managed-session-store.js';
@@ -80,6 +81,16 @@ it.each(
),
),
)
.concat(
['allow', 'deny'].map((decision) => ({
decision,
promptId: randomUUID(),
round: 1,
planMode: false,
profile: 'hosted-workspace-shell/1',
publicTool: 'skill',
})),
)
.concat(
['allow', 'deny', 'cancelled', 'expired'].flatMap((decision) =>
['write_file', 'web_fetch', 'agent', 'run_shell_command'].map(
@@ -211,6 +222,34 @@ it.each(
const prepare = vi.spyOn(HostedWorkspaceBroker.prototype, 'prepare');
const execute = vi.spyOn(HostedWorkspaceBroker.prototype, 'execute');
const publicShell = profile === 'hosted-workspace-shell/1' && !planMode;
const commandSkills =
publicTool === 'skill'
? new HostedSkillCatalog(
[],
[
{
name: 'sample:run',
description: 'Run once',
prompt: '!{printf %s {{args}}}',
extensionId: 'sample',
modelInvocable: true,
},
],
)
: undefined;
const extensionConfig = {
revision: 1,
packages: [
{
pin: {
extensionId: 'sample',
packageRevision: 1,
packageDigest: 'a'.repeat(64),
},
assetRoot: 'extension://sample/',
},
],
};
const publication = publicShell
? {
owner: {
@@ -251,6 +290,7 @@ it.each(
},
{
profile,
...(commandSkills ? { skills: commandSkills, extensionConfig } : {}),
publicPlanMode: planMode,
...(profile === 'hosted-workspace-shell/1' && !planMode
? {
@@ -276,17 +316,22 @@ it.each(
callId: 'original-call',
args: planMode
? { plan: '# Plan\nRead the files, then update original.txt.' }
: publicTool === 'agent'
? {
description: 'audit',
prompt: 'review the file',
run_in_background: true,
}
: publicTool === 'run_shell_command'
? { command: 'printf once' }
: publicTool === 'web_fetch'
? { url: 'https://example.com/docs', prompt: 'Summarize' }
: { file_path: 'original.txt', content: 'one original batch' },
: publicTool === 'skill'
? { skill: 'sample:run', args: 'argument' }
: publicTool === 'agent'
? {
description: 'audit',
prompt: 'review the file',
run_in_background: true,
}
: publicTool === 'run_shell_command'
? { command: 'printf once' }
: publicTool === 'web_fetch'
? { url: 'https://example.com/docs', prompt: 'Summarize' }
: {
file_path: 'original.txt',
content: 'one original batch',
},
isClientInitiated: false,
prompt_id: promptId,
};
@@ -411,6 +456,45 @@ it.each(
continuation: { phase: 'model_output_committed' },
},
});
if (publicTool === 'skill') {
expect(saved!.plan.commandStep).toMatchObject({
toolName: 'shell',
configRevision: 1,
});
if (decision === 'allow') {
const commandExecute = vi
.spyOn(HostedWorkspaceBroker.prototype, 'extensionCommand')
.mockResolvedValue({
state: 'settled',
result: {
executionStatus: 'success',
responseParts: [{ text: 'ran once' }],
},
});
const resumed = new HostedWorkspaceToolTurn(
{ baseUrl: 'http://127.0.0.1:1', token: 'fixture-no-traffic' },
replacement,
harness,
promptId,
vi.fn().mockResolvedValue('unused'),
() => true,
publication,
undefined,
{
settings: { mode: 'default', timeoutMs: 60_000 },
waiters: new HostedApprovalWaiters(),
},
{ profile, skills: commandSkills, extensionConfig },
);
const result = await resumed.resumeSavedNativeBatch(
saved!,
1,
new AbortController().signal,
);
expect(JSON.stringify(result)).toContain('ran once');
expect(commandExecute).toHaveBeenCalledTimes(1);
}
}
if (publicShell && publicTool === 'run_shell_command') {
expect(saved!.plan.calls[0].publicationId).toMatch(
/^[a-f0-9-]{36}$/u,
@@ -21,6 +21,7 @@ import {
assertManagedSessionStableId,
type ManagedSessionDurableRef,
type ManagedSessionKey,
type ManagedSessionJsonValue,
} from '@qwen-code/qwen-code-core/managed-runtime/managed-session-records.js';
import {
hostedActionAllowed,
@@ -87,6 +88,12 @@ export interface HostedApprovalContinuation {
readonly stage: 'approval' | 'final';
readonly approvalOrdinal: number;
readonly actionId: string | null;
readonly commandStep?: {
readonly toolStageId: string;
readonly toolName: 'read_file' | 'shell';
readonly inputRef: ManagedSessionDurableRef;
readonly configRevision: number;
};
readonly calls: readonly HostedNativePlannedCall[];
}
@@ -114,7 +121,8 @@ export async function readHostedApprovalContinuation(
const plan = raw as unknown as HostedApprovalContinuation;
const header = session.authority.sessionHeader;
if (
Object.keys(raw).length !== 15 + (plan.publicChildModel ? 1 : 0) ||
Object.keys(raw).length !==
15 + (plan.publicChildModel ? 1 : 0) + (plan.commandStep ? 1 : 0) ||
plan.promptId !== promptId ||
!['approval', 'final'].includes(plan.stage) ||
!isDeepStrictEqual(plan.definitionRef, header.definitionRef) ||
@@ -128,7 +136,8 @@ export async function readHostedApprovalContinuation(
plan.approvalOrdinal >= plan.calls.length ||
!plan.runtime ||
Object.keys(plan.runtime).length !== 4 ||
plan.runtime.runtimeSessionId !== hostedRuntimeSessionId(promptId) ||
(!plan.commandStep &&
plan.runtime.runtimeSessionId !== hostedRuntimeSessionId(promptId)) ||
!/^[1-9][0-9]{0,18}$/u.test(plan.runtime.generation) ||
!/^[1-9][0-9]{0,18}$/u.test(plan.runtime.workspaceGeneration) ||
BigInt(plan.runtime.generation) > 2n ** 63n - 1n ||
@@ -141,6 +150,27 @@ export async function readHostedApprovalContinuation(
throw new Error(
'Hosted approval continuation conflicts with the original Turn.',
);
if (plan.commandStep) {
if (
Object.keys(plan.commandStep).sort().join(',') !==
'configRevision,inputRef,toolName,toolStageId' ||
!Number.isSafeInteger(plan.commandStep.configRevision) ||
plan.commandStep.configRevision < 1 ||
!['read_file', 'shell'].includes(plan.commandStep.toolName) ||
plan.stage !== 'approval' ||
plan.calls.some((item) => item.call.name !== 'skill')
)
throw new Error('Invalid command step continuation.');
assertManagedSessionStableId(plan.commandStep.toolStageId, 'command step');
assertManagedSessionStableId(
plan.runtime.runtimeSessionId,
'command owner',
);
assertManagedSessionDurableRef(
plan.commandStep.inputRef as unknown as ManagedSessionJsonValue,
'command approval input',
);
}
for (const id of [
plan.batchId,
plan.runtime.bindingId,
@@ -217,6 +247,7 @@ export async function readHostedApprovalContinuation(
!item.call ||
!(
NATIVE_TOOLS.has(item.call.name) ||
(plan.commandStep && item.call.name === 'skill') ||
publicPlanExit ||
(publicShell && ['agent', 'run_shell_command'].includes(item.call.name))
) ||
@@ -233,8 +264,8 @@ export async function readHostedApprovalContinuation(
parts[item.partIndex]?.functionCall?.id !== item.call.callId ||
parts[item.partIndex]?.functionCall?.name !== item.call.name ||
item.prepareKey !==
`${hostedRuntimeSessionId(promptId)}:${item.runtimeCallId}` ||
item.prepareReference?.sessionId !== hostedRuntimeSessionId(promptId) ||
`${plan.runtime.runtimeSessionId}:${item.runtimeCallId}` ||
item.prepareReference?.sessionId !== plan.runtime.runtimeSessionId ||
item.prepareReference.promptId !== promptId ||
item.prepareReference.callId !== item.runtimeCallId ||
item.prepareReference.argsDigest !== item.requestDigest ||
@@ -266,7 +297,7 @@ export async function readHostedApprovalContinuation(
};
if (
saved.harnessSessionId !== header.sessionKey.sessionId ||
saved.runtimeSessionId !== hostedRuntimeSessionId(promptId) ||
saved.runtimeSessionId !== plan.runtime.runtimeSessionId ||
typeof saved.payloadJson !== 'string' ||
item.requestDigest !==
`sha256:${createHash('sha256').update(saved.payloadJson).digest('hex')}`
@@ -306,9 +337,19 @@ export async function readHostedApprovalContinuation(
options.v !== 3 ||
options.functionCallId !== item.call.callId ||
options.turnId !== promptId ||
options.toolName !== item.call.name ||
options.toolName !==
(plan.commandStep && ordinal === plan.approvalOrdinal
? plan.commandStep.toolName === 'shell'
? 'run_shell_command'
: 'read_file'
: item.call.name) ||
(options.inputRef &&
!isDeepStrictEqual(options.inputRef, item.inputRef)) ||
!isDeepStrictEqual(
options.inputRef,
plan.commandStep && ordinal === plan.approvalOrdinal
? plan.commandStep.inputRef
: item.inputRef,
)) ||
(action.state === 'decided' &&
!hostedActionAllowed(action, options.policyRevision) &&
!hostedActionDenied(action, options.policyRevision)) ||
@@ -0,0 +1,84 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { createHash } from 'node:crypto';
import type {
ManagedExtensionApplyCommand,
ManagedExtensionPin,
} from '@qwen-code/qwen-code-core/managed-runtime/managed-extension-package.js';
import {
parsePublicChildExtensionAgent,
type PublicChildExtensionAgent,
} from '@qwen-code/qwen-code-core/managed-runtime/managed-child-operations.js';
import {
assertManagedSessionDurableRef,
type ManagedSessionDurableRef,
type ManagedSessionJsonValue,
} from '@qwen-code/qwen-code-core/managed-runtime/managed-session-records.js';
export interface HostedChildExtensionAgent {
readonly extensionAgent: PublicChildExtensionAgent;
readonly effectiveConfigRef: ManagedSessionDurableRef;
}
export function parseHostedChildExtensionAgent(
value: unknown,
): HostedChildExtensionAgent | undefined {
if (value === undefined) return undefined;
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
Object.keys(value).sort().join(',') !== 'effectiveConfigRef,extensionAgent'
)
throw new Error('Invalid child extension binding.');
const fields = value as Record<string, ManagedSessionJsonValue>;
const effectiveConfigRef = assertManagedSessionDurableRef(
fields['effectiveConfigRef'],
'child extension source',
);
const extensionAgent = parsePublicChildExtensionAgent(
fields['extensionAgent'],
);
if (
effectiveConfigRef.kind !== 'managed-extension-config-bundle' ||
effectiveConfigRef.schemaVersion !== 1 ||
effectiveConfigRef.digest !== extensionAgent.bundleRef.digest
)
throw new Error(
'Child extension source does not match its parent binding.',
);
return { extensionAgent, effectiveConfigRef };
}
export function hostedChildExtensionCommandId(
sessionId: string,
sourceDigest: string,
): string {
const bytes = createHash('sha256')
.update(`managed-extension-child:${sessionId}:${sourceDigest}`)
.digest()
.subarray(0, 16);
bytes[6] = (bytes[6]! & 0x0f) | 0x50;
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
const hex = bytes.toString('hex');
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
export function hostedChildExtensionApplyCommand(
sessionId: string,
sourceDigest: string,
extensions: readonly ManagedExtensionPin[],
): ManagedExtensionApplyCommand {
return {
operationId: hostedChildExtensionCommandId(sessionId, sourceDigest),
actorKey: createHash('sha256')
.update(`managed-extension-child:${sessionId}`)
.digest('hex'),
expectedRevision: 0,
extensions,
};
}
@@ -0,0 +1,175 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { createHash } from 'node:crypto';
import { describe, expect, it, vi } from 'vitest';
import type { ManagedSession } from '@qwen-code/qwen-code-core/managed-runtime/managed-session-assembly.js';
import type { ManagedSessionDurableRef } from '@qwen-code/qwen-code-core/managed-runtime/managed-session-records.js';
import type { ManagedToolStage } from '@qwen-code/qwen-code-core/managed-runtime/managed-tool-stage-record.js';
import type { HostedWorkspaceBroker } from './hosted-workspace-broker.js';
import { expandHostedExtensionCommand } from './hosted-extension-command.js';
function fixture(prompt: string) {
const bytes = new Map<string, Buffer>();
const records = new Map<
string,
{
record: ManagedToolStage;
revision: number;
recordRef: ManagedSessionDurableRef;
operationId: string;
}
>();
const publish = async (kind: string, content: Buffer) => {
const digest = createHash('sha256').update(content).digest('hex');
const resourceId = `resource-${bytes.size}`;
bytes.set(resourceId, content);
return {
kind,
resourceId,
schemaVersion: 1,
byteLength: content.length,
digest,
};
};
const session = {
resources: {
publish,
read: async (ref: ManagedSessionDurableRef) => bytes.get(ref.resourceId)!,
},
authority: {
sessionHeader: {
sessionKey: {
tenantId: 'tenant',
workspaceId: 'workspace',
sessionId: 'session',
},
},
extensionRecord: (_domain: string, id: string) => records.get(id),
commitExtensionRecord: async (
_command: unknown,
request: { record: ManagedToolStage },
) => {
const previous = records.get(request.record.toolStageId);
const recordRef = await publish(
'managed-tool_stage',
Buffer.from(JSON.stringify(request.record)),
);
records.set(request.record.toolStageId, {
record: request.record,
recordRef,
revision: (previous?.revision ?? 0) + 1,
operationId: request.record.toolStageId,
});
},
issueOperationGrant: vi.fn((request: Record<string, unknown>) => request),
},
} as unknown as ManagedSession;
const control = vi.fn(async () => ({
state: 'settled',
result: { executionStatus: 'success', responseParts: [{ text: 'RESULT' }] },
}));
const broker = {
runtimeSessionId: 'owner',
runtime: {
bindingId: 'binding',
generation: '1',
workspaceGeneration: '1',
},
extensionCommand: control,
} as unknown as HostedWorkspaceBroker;
const decide = vi.fn(
async (
_id: string,
toolName: 'read_file' | 'shell',
input: Record<string, unknown>,
) => ({
allowed: true,
toolName,
input,
approvalMode: 'default' as const,
preHookOccurrence: null,
permissionHookOccurrence: null,
actionRequestId: null,
}),
);
const completed = vi.fn(async () => undefined);
const options = {
session,
broker,
promptId: 'prompt',
callId: 'call',
revision: 1,
command: {
name: 'sample:run',
description: 'Run sample',
prompt,
extensionId: 'sample',
modelInvocable: true,
},
args: 'hello; $(touch /tmp/never)',
signal: new AbortController().signal,
definitionDigest: () => 'a'.repeat(64),
decide,
completed,
};
return { options, records, control, decide, completed };
}
describe('Hosted durable command expansion', () => {
it('expands pure prompt arguments without starting Runtime work', async () => {
const context = fixture('Review {{args}}');
const prompt = await expandHostedExtensionCommand(context.options);
expect(prompt).toContain('Review hello; $(touch /tmp/never)');
expect(context.decide).not.toHaveBeenCalled();
expect(context.control).not.toHaveBeenCalled();
});
it('escapes arguments inside Shell and reuses the committed result', async () => {
const context = fixture('Before !{printf %s {{args}}} after');
expect(await expandHostedExtensionCommand(context.options)).toContain(
'Before RESULT after',
);
expect(context.decide.mock.calls[0]?.[2]).toMatchObject({
command: "printf %s 'hello; $(touch /tmp/never)'",
});
const stage = [...context.records.values()][0]!.record;
expect(stage.run.state).toBe('settled');
expect(stage.permissionRef).not.toBeNull();
expect(await expandHostedExtensionCommand(context.options)).toContain(
'Before RESULT after',
);
expect(context.control).toHaveBeenCalledTimes(1);
expect(context.decide).toHaveBeenCalledTimes(1);
});
it('queries an unknown original step without dispatching it again', async () => {
const context = fixture('!{pwd}');
context.control.mockResolvedValueOnce({
state: 'outcome_unknown',
} as never);
await expect(expandHostedExtensionCommand(context.options)).rejects.toThrow(
'outcome is unknown',
);
expect(await expandHostedExtensionCommand(context.options)).toContain(
'RESULT',
);
expect(
context.control.mock.calls.map(
(call) => (call as unknown as [{ action: string }])[0].action,
),
).toEqual(['execute', 'status']);
expect(context.decide).toHaveBeenCalledTimes(1);
});
it('keeps quoted Shell placeholders literal', async () => {
const context = fixture('!{printf "%s" "{{args}}"}');
await expandHostedExtensionCommand(context.options);
expect(context.decide.mock.calls[0]?.[2]).toMatchObject({
command: 'printf "%s" "hello; \\$(touch /tmp/never)"',
});
});
});
@@ -0,0 +1,370 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { createHash } from 'node:crypto';
import { isDeepStrictEqual } from 'node:util';
import type { ManagedSession } from '@qwen-code/qwen-code-core/managed-runtime/managed-session-assembly.js';
import {
parseManagedToolStage,
type ManagedToolStage,
} from '@qwen-code/qwen-code-core/managed-runtime/managed-tool-stage-record.js';
import type { ManagedSessionDurableRef } from '@qwen-code/qwen-code-core/managed-runtime/managed-session-records.js';
import type { HostedCommand } from './hosted-skill-catalog.js';
import type { HostedWorkspaceBroker } from './hosted-workspace-broker.js';
import { extractInjections } from '../services/prompt-processors/injectionParser.js';
export class HostedCommandExpansionRefusedError extends Error {}
export interface HostedCommandPermission {
readonly allowed: boolean;
readonly toolName: 'read_file' | 'shell';
readonly input: Record<string, unknown>;
readonly inputRef?: ManagedSessionDurableRef;
readonly approvalMode: 'default' | 'auto-edit' | 'auto' | 'yolo';
readonly preHookOccurrence: string | null;
readonly permissionHookOccurrence: string | null;
readonly actionRequestId: string | null;
}
export async function expandHostedExtensionCommand(options: {
session: ManagedSession;
broker: HostedWorkspaceBroker;
promptId: string;
callId: string;
revision: number;
command: HostedCommand;
args: string;
signal: AbortSignal;
definitionDigest(toolName: 'read_file' | 'shell'): string;
decide(
toolStageId: string,
toolName: 'read_file' | 'shell',
input: Record<string, unknown>,
): Promise<HostedCommandPermission>;
completed(
toolStageId: string,
toolName: 'read_file' | 'shell',
input: Record<string, unknown>,
result: Record<string, unknown>,
): Promise<void>;
}): Promise<string> {
const { session, broker, command, signal } = options;
const expansionId = `command-${hash([session.authority.sessionHeader.sessionKey, options.promptId, options.callId, options.revision, command.name])}`;
const original = command.prompt;
const injections = [
...injectionsFor('!{').map((entry) => ({
...entry,
toolName: 'shell' as const,
})),
...injectionsFor('@{').map((entry) => ({
...entry,
toolName: 'read_file' as const,
})),
].sort((a, b) => a.startIndex - b.startIndex);
if (
injections.some(
(entry, index) =>
index > 0 && entry.startIndex < injections[index - 1]!.endIndex,
)
)
throw new HostedCommandExpansionRefusedError(
'Nested file and shell command preprocessing is unsupported.',
);
let prompt = '';
let offset = 0;
for (const [stepIndex, injection] of injections.entries()) {
signal.throwIfAborted();
const toolStageId = `command-step-${hash([expansionId, stepIndex])}`;
prompt += raw(original.slice(offset, injection.startIndex));
offset = injection.endIndex;
if (!injection.content) continue;
const input: Record<string, unknown> =
injection.toolName === 'shell'
? {
command: renderShellArguments(
injection.content,
options.args,
).replaceAll('${extensionPath}', '${QWEN_EXTENSION_ROOT}'),
timeout: 120000,
}
: { file_path: raw(injection.content) };
const saved = session.authority.extensionRecord('tool_stage', toolStageId);
let stage: ManagedToolStage;
if (saved) {
stage = parseManagedToolStage(saved.record);
if (
stage.expansionId !== expansionId ||
stage.stepIndex !== stepIndex ||
stage.toolName !== injection.toolName
)
throw new Error('Command preprocessing identity conflicts.');
} else {
const decision = await options.decide(
toolStageId,
injection.toolName,
input,
);
const inputRef =
decision.inputRef ??
(await publish('managed-extension-command-input', decision.input));
const { input: _input, inputRef: _inputRef, ...permission } = decision;
const permissionRef = await publish(
'managed-extension-command-permission',
{ ...permission, inputRef },
);
stage = {
kind: 'tool_stage',
toolStageId,
expansionId,
stepIndex,
toolName: injection.toolName,
inputRef,
permissionRef,
resultRef: null,
run: {
state: 'admitted',
reason: null,
definition: {
definitionId: injection.toolName,
definitionRevision: 1,
definitionDigest: options.definitionDigest(injection.toolName),
},
executionCallId: toolStageId,
effectId: null,
dispatchId: null,
deliveryId: null,
execution: 'intent',
runtime: null,
delivery: null,
},
};
await commit(stage);
if (!decision.allowed) {
const result = {
executionStatus: 'not_started',
responseParts: [
{ text: 'The command preprocessing step was denied.' },
],
};
stage = {
...stage,
resultRef: await publish('managed-extension-command-result', result),
run: {
...stage.run,
state: 'cancelled',
execution: 'not_started_proven',
},
};
await commit(stage);
}
}
const actualInput = await read<Record<string, unknown>>(stage.inputRef);
let result: Record<string, unknown>;
if (stage.resultRef) result = await read(stage.resultRef);
else {
if (!broker.runtime)
throw new Error('Command Runtime owner is unavailable.');
const runtime = {
runtimeBindingId: broker.runtime.bindingId,
generation: broker.runtime.generation,
};
if (stage.run.runtime && !isDeepStrictEqual(stage.run.runtime, runtime))
throw new Error(
'Command preprocessing belongs to its original Runtime.',
);
const resume = stage.run.execution !== 'intent';
if (!resume) {
stage = {
...stage,
run: {
...stage.run,
state: 'running',
execution: 'dispatch_started',
runtime,
},
};
await commit(stage);
}
const operation = {
kind: 'extension-command' as const,
operationId: toolStageId,
toolName: injection.toolName,
input: actualInput,
revision: options.revision,
extensionId: command.extensionId,
};
const send = (action: 'execute' | 'status' | 'cancel') =>
broker.extensionCommand({
...operation,
action,
grant: session.authority.issueOperationGrant({
domain: 'tool_stage',
recordId: toolStageId,
ownerId: broker.runtimeSessionId,
workspaceGeneration: broker.runtime!.workspaceGeneration,
phases: ['execute', 'status', 'cancel'],
leaseDurationMs: 60_000,
}),
});
let response = await send(resume ? 'status' : 'execute');
while (response['state'] === 'running') {
if (signal.aborted) {
response = await send('cancel');
break;
}
await new Promise<void>((resolve) => setTimeout(resolve, 250));
response = await send('status');
}
if (
response['state'] !== 'settled' ||
!response['result'] ||
typeof response['result'] !== 'object'
)
throw new Error(
'Command preprocessing outcome is unknown; query its original operation.',
);
result = response['result'] as Record<string, unknown>;
stage = {
...stage,
resultRef: await publish('managed-extension-command-result', result),
run: {
...stage.run,
state: result['executionStatus'] === 'success' ? 'settled' : 'failed',
execution: 'settled',
},
};
await commit(stage);
}
await options.completed(
toolStageId,
injection.toolName,
actualInput,
result,
);
if (result['executionStatus'] !== 'success')
throw new HostedCommandExpansionRefusedError(
'Command preprocessing did not complete successfully.',
);
if (!Array.isArray(result['responseParts']))
throw new Error('Command preprocessing returned invalid text.');
const output = result['responseParts']
.map((part: unknown) =>
part &&
typeof part === 'object' &&
'text' in part &&
typeof part.text === 'string'
? part.text
: '',
)
.join('\n');
if (Buffer.byteLength(output) > 48 * 1024)
throw new HostedCommandExpansionRefusedError(
'Command preprocessing output exceeds the inline limit.',
);
prompt += output;
}
prompt += raw(original.slice(offset));
if (
!original.includes('{{args}}') &&
!original.includes('$ARGUMENTS') &&
options.args.trim()
)
prompt += `\n\n/${command.name} ${options.args}`;
if (Buffer.byteLength(prompt) > 48 * 1024)
throw new HostedCommandExpansionRefusedError(
'Expanded command exceeds the inline limit.',
);
return `<extension_command name="${command.name}" source="extension://${command.extensionId}/" revision="${options.revision}">\n${prompt}\n</extension_command>`;
function raw(text: string): string {
return text
.replaceAll('{{args}}', options.args)
.replaceAll('$ARGUMENTS', options.args)
.replaceAll('${extensionPath}', `extension://${command.extensionId}`);
}
function injectionsFor(trigger: string) {
try {
return extractInjections(original, trigger, command.name);
} catch (cause) {
throw new HostedCommandExpansionRefusedError(
cause instanceof Error
? cause.message
: 'Invalid command preprocessing.',
);
}
}
function publish(kind: string, value: unknown) {
return session.resources.publish(kind, Buffer.from(JSON.stringify(value)));
}
function read<T>(ref: ManagedSessionDurableRef): Promise<T> {
return session.resources
.read(ref)
.then((bytes) => JSON.parse(bytes.toString('utf8')) as T);
}
async function commit(stage: ManagedToolStage): Promise<void> {
const previous = session.authority.extensionRecord(
'tool_stage',
stage.toolStageId,
);
await session.authority.commitExtensionRecord(
{
operation: 'commitCommandToolStage',
commandId: previous
? `${stage.toolStageId}:${previous.revision + 1}`
: stage.toolStageId,
sessionKey: session.authority.sessionHeader.sessionKey,
contentDigest: hash(stage),
},
{ domain: 'tool_stage', record: stage },
{ class: 'trusted_entry' },
);
}
}
function hash(value: unknown): string {
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
}
function quote(value: string): string {
return `'${value.replaceAll("'", "'\\''")}'`;
}
function renderShellArguments(template: string, args: string): string {
if (
(template.includes('{{args}}') || template.includes('$ARGUMENTS')) &&
/`|\$\(|<</u.test(template)
)
throw new HostedCommandExpansionRefusedError(
'Argument placeholders in Shell substitutions and here-documents are unsupported.',
);
let result = '';
let quoted: "'" | '"' | undefined;
for (let index = 0; index < template.length; index++) {
const placeholder = template.startsWith('{{args}}', index)
? '{{args}}'
: template.startsWith('$ARGUMENTS', index)
? '$ARGUMENTS'
: undefined;
if (placeholder) {
result +=
quoted === "'"
? args.replaceAll("'", "'\\''")
: quoted === '"'
? args.replace(/[\\$`"]/gu, '\\$&')
: quote(args);
index += placeholder.length - 1;
continue;
}
const character = template[index]!;
result += character;
if (character === '\\' && quoted !== "'" && index + 1 < template.length) {
result += template[++index];
continue;
}
if (character === quoted) quoted = undefined;
else if (!quoted && (character === "'" || character === '"'))
quoted = character;
}
return result;
}
@@ -0,0 +1,334 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { randomUUID } from 'node:crypto';
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { openManagedSession } from '@qwen-code/qwen-code-core/managed-runtime/managed-session-assembly.js';
import { LocalManagedSessionResourceStore } from '@qwen-code/qwen-code-core/managed-runtime/managed-session-resources.js';
import type { ManagedExtensionRuntimeControl } from '@qwen-code/qwen-code-core/managed-runtime/managed-extension-package.js';
import { HostedExtensionSession } from './hosted-extension-session.js';
import { HostedWorkspaceBroker } from './hosted-workspace-broker.js';
import { MANAGED_EXTENSION_DEVELOPER_CAPABILITY_DIGEST } from './managed-extension-profile.js';
const cleanups: Array<() => Promise<void>> = [];
afterEach(async () => {
vi.restoreAllMocks();
for (const cleanup of cleanups.splice(0)) await cleanup();
});
async function fixture() {
const root = await mkdtemp(path.join(tmpdir(), 'managed-extension-apply-'));
const sessionKey = {
tenantId: 'tenant',
workspaceId: 'workspace',
sessionId: randomUUID(),
};
const resources = LocalManagedSessionResourceStore.create({
runtimeBaseDir: root,
sessionKey,
});
const session = await openManagedSession({
runtimeBaseDir: root,
cwd: root,
transcriptPath: path.join(root, 'transcript.jsonl'),
sessionId: sessionKey.sessionId,
sessionKey,
version: 'test',
workerId: 'test',
activationLeaseDurationMs: 60_000,
create: {
definitionRef: await resources.publish(
'managed-definition',
Buffer.from('{}'),
),
rootSnapshotRef: await resources.publish(
'managed-root',
Buffer.from('{}'),
),
createdBy: 'test',
},
});
cleanups.push(async () => {
await session.close();
await rm(root, { recursive: true, force: true });
});
const owners = {
bindingId: 'original-binding',
generation: '1',
workspaceGeneration: '1',
};
const warm = vi
.spyOn(HostedWorkspaceBroker.prototype, 'warm')
.mockImplementation(async function (this: HostedWorkspaceBroker) {
this.runtime = owners;
});
vi.spyOn(
HostedWorkspaceBroker.prototype,
'capabilityDigest',
'get',
).mockReturnValue(MANAGED_EXTENSION_DEVELOPER_CAPABILITY_DIGEST);
const states = new Map<string, { state: string; revision: number }>();
let rejectPrepare = false;
let rejectActivation = false;
const control = vi
.spyOn(HostedWorkspaceBroker.prototype, 'extensionControl')
.mockImplementation(async function (
this: HostedWorkspaceBroker,
operation: ManagedExtensionRuntimeControl,
) {
this.runtime = owners;
const common = {
operationId: operation.operationId,
executionMode: 'developer-process-group',
};
if (operation.kind === 'extension-prepare') {
if (rejectPrepare) throw new Error('invalid package');
states.set(operation.operationId, {
state: 'prepared',
revision: operation.expectedRevision + 1,
});
return {
...common,
state: 'prepared',
expectedRevision: operation.expectedRevision,
revision: operation.expectedRevision + 1,
packages: operation.extensions.map((pin) => ({
pin,
assetRoot: `extension://${pin.extensionId}/`,
contributions: {
skills: [],
commands: [],
agents: [],
context: [],
mcpServers: {},
hooks: {},
settings: [],
},
})),
mcpServers: operation.baseMcpServers,
hookCatalog: null,
};
}
if (operation.kind === 'extension-activate') {
if (rejectActivation) {
states.set(operation.operationId, {
state: 'outcome_unknown',
revision: operation.revision,
});
throw new Error('lost activation response');
}
states.set(operation.operationId, {
state: 'active',
revision: operation.revision,
});
}
if (operation.kind === 'extension-reconcile') {
states.set(operation.operationId, {
state: 'failed',
revision: operation.revision,
});
return {
...common,
state: 'stopped',
revision: operation.revision,
receipts: [{ ownerId: 'old-owner', state: 'stopped' }],
};
}
return {
...common,
...(states.get(operation.operationId) ?? {
state: 'not_found',
revision: 0,
}),
};
});
const callbacks = {
validate: vi.fn(),
drain: vi.fn(async () => undefined),
enable: vi.fn(async () => undefined),
};
const manager = new HostedExtensionSession(
session,
{ baseUrl: 'http://127.0.0.1:1', token: 'fixture-no-traffic' },
{ mcpRevision: 0, mcpServers: [], hookCatalog: null },
callbacks,
);
await manager.open();
const command = (expectedRevision = 0, extensionId = 'sample') => ({
operationId: randomUUID(),
actorKey: 'a'.repeat(64),
expectedRevision,
extensions: [
{ extensionId, packageRevision: 1, packageDigest: 'b'.repeat(64) },
],
});
return {
session,
manager,
warm,
control,
callbacks,
command,
failPrepare: (fail = true) => {
rejectPrepare = fail;
},
failActivation: (fail = true) => {
rejectActivation = fail;
},
};
}
describe('Hosted extension Session configuration', () => {
it('blocks a failed initial selection on reload until an explicit repair is active', async () => {
const context = await fixture();
const initial = context.command();
context.failPrepare();
await expect(context.manager.bootstrap(initial)).rejects.toThrow(
'invalid package',
);
await expect(context.manager.bootstrap(initial)).rejects.toThrow(
'requires recovery',
);
expect(context.manager.activeBundleRef).toBeUndefined();
context.failPrepare(false);
await context.manager.submit(context.command(0, 'repaired'));
await expect(context.manager.bootstrap(initial)).resolves.toBeUndefined();
expect(await context.manager.active()).toMatchObject({
configRevision: 1,
packages: [{ pin: { extensionId: 'repaired' } }],
});
});
it('activates an explicit empty initial selection', async () => {
const context = await fixture();
await context.manager.bootstrap({ ...context.command(), extensions: [] });
expect(context.manager.activeBundleRef).toBeDefined();
expect(await context.manager.active()).toMatchObject({
configRevision: 1,
packages: [],
});
});
it('keeps the previous view usable when preparation fails', async () => {
const context = await fixture();
context.failPrepare();
await expect(context.manager.submit(context.command())).rejects.toThrow(
'invalid package',
);
expect(context.manager.pending).toBe(false);
expect(await context.manager.snapshot()).toMatchObject({
revision: 0,
extensions: [],
operation: { state: 'failed' },
});
expect(context.callbacks.drain).not.toHaveBeenCalled();
expect(
context.session.authority
.eventsInSequenceRange(1, context.session.authority.committedSequence)
.filter((event) => event.kind === 'config.bound'),
).toHaveLength(0);
});
it('commits the binding before drain and only queries an unknown original operation', async () => {
const context = await fixture();
context.callbacks.drain.mockImplementation(async () => {
expect(
context.session.authority
.eventsInSequenceRange(1, context.session.authority.committedSequence)
.filter((event) => event.kind === 'config.bound'),
).toHaveLength(1);
});
context.failActivation();
const command = context.command();
await expect(context.manager.submit(command)).rejects.toThrow(
'lost activation response',
);
expect(context.manager.pending).toBe(true);
const activations = () =>
context.control.mock.calls.filter(
([operation]) => operation.kind === 'extension-activate',
);
expect(activations()).toHaveLength(1);
await expect(context.manager.continue(command.operationId)).rejects.toThrow(
'requires recovery',
);
expect(activations()).toHaveLength(1);
expect(context.warm).toHaveBeenCalledTimes(1);
});
it('restores saved pins as a newer revision after proving the failed owner stopped', async () => {
const context = await fixture();
await context.manager.submit(context.command());
context.failActivation();
const failed = context.command(1, 'replacement');
await expect(context.manager.submit(failed)).rejects.toThrow();
context.failActivation(false);
await context.manager.recoverPrevious(failed.operationId);
expect(await context.manager.snapshot()).toMatchObject({
revision: 3,
extensions: [{ extensionId: 'sample' }],
operation: { mode: 'recoverPrevious', state: 'active' },
});
const prepares = context.control.mock.calls
.map(([operation]) => operation)
.filter((operation) => operation.kind === 'extension-prepare');
expect(prepares.at(-1)).toMatchObject({
expectedRevision: 2,
extensions: [{ extensionId: 'sample' }],
});
expect(prepares.at(-1)!.operationId).not.toBe(failed.operationId);
});
it('retries a compensation preparation on its original operation after a transient failure', async () => {
const context = await fixture();
await context.manager.submit(context.command());
context.failActivation();
const failed = context.command(1, 'replacement');
await expect(context.manager.submit(failed)).rejects.toThrow();
context.failActivation(false);
context.failPrepare();
await expect(
context.manager.recoverPrevious(failed.operationId),
).rejects.toThrow('invalid package');
expect(context.manager.pending).toBe(true);
expect(await context.manager.snapshot()).toMatchObject({
operation: { mode: 'recoverPrevious', state: 'preparing' },
});
const prepareOperation = context.control.mock.calls
.map(([operation]) => operation)
.filter((operation) => operation.kind === 'extension-prepare')
.at(-1)!.operationId;
context.failPrepare(false);
await context.manager.continue(failed.operationId);
expect(context.manager.pending).toBe(false);
expect(await context.manager.snapshot()).toMatchObject({
revision: 3,
extensions: [{ extensionId: 'sample' }],
operation: { mode: 'recoverPrevious', state: 'active' },
});
const compensationPrepares = context.control.mock.calls
.map(([operation]) => operation)
.filter(
(operation) =>
operation.kind === 'extension-prepare' &&
operation.expectedRevision === 2,
);
expect(compensationPrepares).toHaveLength(3);
expect(
compensationPrepares.every(
(operation) => operation.operationId === prepareOperation,
),
).toBe(true);
expect(
context.control.mock.calls.filter(
([operation]) => operation.kind === 'extension-reconcile',
),
).toHaveLength(1);
});
});
@@ -0,0 +1,800 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { createHash } from 'node:crypto';
import type { ManagedSession } from '@qwen-code/qwen-code-core/managed-runtime/managed-session-assembly.js';
import {
publishManagedExtensionContributions,
readManagedExtensionContributions,
} from '@qwen-code/qwen-code-core/managed-runtime/managed-extension-contributions.js';
import {
isManagedExtensionConfig,
managedConfigBinding,
managedExtensionCompensationOperationId,
parseManagedConfigInstallRecord,
type ManagedConfigInstallContent,
type ManagedConfigInstallRecord,
} from '@qwen-code/qwen-code-core/managed-runtime/managed-config-install.js';
import {
parseManagedExtensionApplyCommand,
parseManagedExtensionEffectiveBundle,
type ManagedExtensionEffectiveBundle,
type ManagedExtensionMcpSelection,
type ManagedExtensionPrepareResult,
} from '@qwen-code/qwen-code-core/managed-runtime/managed-extension-package.js';
import type { ManagedPackageContributions } from '@qwen-code/qwen-code-core/extension/managed-package.js';
import type { ManagedHookCatalogPin } from '@qwen-code/qwen-code-core/managed-runtime/managed-hook-protocol.js';
import type { ManagedSessionDurableRef } from '@qwen-code/qwen-code-core/managed-runtime/managed-session-records.js';
import {
HostedWorkspaceBroker,
type HostedWorkspaceBrokerOptions,
} from './hosted-workspace-broker.js';
import {
MANAGED_EXTENSION_LINUX_CAPABILITY_DIGEST,
MANAGED_EXTENSION_DEVELOPER_CAPABILITY_DIGEST,
} from './managed-extension-profile.js';
import {
selectedMcpServers,
type HostedMcpCommand,
} from './hosted-mcp-settings.js';
import { HostedMcpConflictError } from './hosted-mcp-session.js';
export interface HostedExtensionPackage {
readonly extensionId: string;
readonly contributions: ManagedPackageContributions;
}
export interface HostedExtensionCallbacks {
validate(packages: readonly HostedExtensionPackage[]): void;
drain(
bundle: ManagedExtensionEffectiveBundle,
forceMcpServerId?: string,
): Promise<void>;
enable(
bundle: ManagedExtensionEffectiveBundle,
packages: readonly HostedExtensionPackage[],
operationId: string,
): Promise<void>;
}
interface RuntimeOwner {
readonly rootSnapshotRef: ManagedSessionDurableRef;
readonly runtimeSessionId: string;
readonly executionMode: ManagedExtensionEffectiveBundle['executionMode'];
readonly runtime: {
readonly bindingId: string;
readonly generation: string;
readonly workspaceGeneration: string;
};
}
export class HostedExtensionRevisionConflict extends HostedMcpConflictError {}
export class HostedExtensionRecoveryRequired extends Error {
constructor() {
super('The original extension configuration requires recovery.');
}
}
/** All durable facts use the Session's existing config_install chain. */
export class HostedExtensionSession {
private history: ManagedConfigInstallRecord[] = [];
constructor(
private readonly session: ManagedSession,
private readonly brokerOptions: HostedWorkspaceBrokerOptions,
private readonly baseline: {
readonly mcpRevision: number;
readonly mcpServers: readonly ManagedExtensionMcpSelection[];
readonly hookCatalog: ManagedHookCatalogPin | null;
},
private readonly callbacks: HostedExtensionCallbacks,
) {}
async open(): Promise<void> {
await this.session.authority.settlePendingAppend();
let current =
this.session.authority.domainRecord('config_install')?.recordRef;
const history: ManagedConfigInstallRecord[] = [];
const seen = new Set<string>();
while (current) {
if (seen.has(current.resourceId))
throw new HostedExtensionRecoveryRequired();
seen.add(current.resourceId);
const value = await this.read<unknown>(current);
if (!isManagedExtensionConfig(value)) break;
const record = parseManagedConfigInstallRecord(value);
history.push(record);
current = record.previousRecordRef ?? undefined;
}
this.history = history.reverse();
}
get enabled(): boolean {
return this.history.length > 0;
}
get activeBundleRef(): ManagedSessionDurableRef | undefined {
return (
this.history.findLast((entry) => entry.phase === 'active')?.bundleRef ??
undefined
);
}
get pending(): boolean {
const latest = this.history.at(-1);
return (
!!latest &&
latest.phase !== 'active' &&
!(latest.phase === 'failed' && latest.mode === 'apply')
);
}
async active(): Promise<ManagedExtensionEffectiveBundle | undefined> {
const active = this.history.findLast((entry) => entry.phase === 'active');
const previous = this.history[0]?.previousBundleRef;
const ref = active?.bundleRef ?? previous;
return ref ? this.bundle(ref) : undefined;
}
async packages(
bundle: ManagedExtensionEffectiveBundle,
): Promise<readonly HostedExtensionPackage[]> {
return Promise.all(
bundle.packages.map(async (entry) => ({
extensionId: entry.pin.extensionId,
contributions: await readManagedExtensionContributions(
this.session.resources,
entry.contributionsRef,
),
})),
);
}
async snapshot(operationId?: string) {
const latest = this.history.at(-1);
const selected = operationId
? this.history.findLast(
(entry) => entry.command.operationId === operationId,
)
: latest;
if (operationId && !selected)
throw new Error('Extension operation was not found.');
const active = await this.active();
const requested = latest?.bundleRef
? await this.bundle(latest.bundleRef)
: latest?.restoresBundleRef
? await this.bundle(latest.restoresBundleRef)
: undefined;
return {
sessionId: this.session.authority.sessionHeader.sessionKey.sessionId,
supported: true,
revision:
latest &&
['committed', 'active', 'recovery_required'].includes(latest.phase)
? latest.configRevision
: (latest?.previousConfigRevision ?? 0),
extensions: active?.packages.map((entry) => entry.pin) ?? [],
requestedExtensions:
requested?.packages.map((entry) => entry.pin) ??
latest?.command.extensions ??
[],
...(selected
? {
operation: {
operationId: selected.command.operationId,
state: selected.phase,
mode: selected.mode,
revision: selected.configRevision,
...(selected.error ? { error: selected.error } : {}),
},
}
: {}),
};
}
async submit(value: unknown): Promise<void> {
const command = parseManagedExtensionApplyCommand(value);
const prior = this.history.findLast(
(entry) => entry.command.operationId === command.operationId,
);
if (prior) {
if (JSON.stringify(prior.command) !== JSON.stringify(command))
throw new HostedMcpConflictError(
'Extension operation identity conflicts.',
);
if (['active', 'failed'].includes(prior.phase)) return;
if (this.history.at(-1) !== prior)
throw new HostedExtensionRecoveryRequired();
await this.continue(command.operationId);
return;
}
if (this.pending) throw new HostedExtensionRecoveryRequired();
const active = await this.active();
if (command.expectedRevision !== (active?.configRevision ?? 0))
throw new HostedExtensionRevisionConflict(
'Extension configuration revision conflicts.',
);
const broker = await this.runtime(command.operationId);
const executionMode = this.executionMode(broker);
const baseline =
active ??
parseManagedExtensionEffectiveBundle({
kind: 'managed-extension-config-bundle',
formatVersion: 1,
configRevision: 0,
executionMode,
mcpRevision: this.baseline.mcpRevision,
independentMcpSelections: this.baseline.mcpServers,
baseMcpServers: this.baseline.mcpServers
.filter((pin) => pin.state === 'enabled')
.map(({ state: _state, ...pin }) => pin),
baseHookCatalog: this.baseline.hookCatalog,
packages: [],
mcpServers: this.baseline.mcpServers
.filter((pin) => pin.state === 'enabled')
.map(({ state: _state, ...pin }) => pin),
hookCatalog: this.baseline.hookCatalog,
});
if (baseline.executionMode !== executionMode)
throw new HostedExtensionRecoveryRequired();
const previousBundleRef = await this.publish(
'managed-extension-config-bundle',
baseline,
);
const owner: RuntimeOwner = {
rootSnapshotRef: this.session.authority.sessionHeader.rootSnapshotRef,
runtimeSessionId: broker.runtimeSessionId,
executionMode,
runtime: {
bindingId: broker.runtime!.bindingId,
generation: broker.runtime!.generation,
workspaceGeneration: broker.runtime!.workspaceGeneration,
},
};
const rootSnapshotRef = await this.publish('managed-extension-root', owner);
await this.commit({
kind: 'hosted-extension-config',
formatVersion: 1,
command,
mode: 'apply',
phaseOperationId: command.operationId,
configRevision: command.expectedRevision + 1,
previousConfigRevision: command.expectedRevision,
phase: 'preparing',
previousBundleRef,
restoresBundleRef: null,
bundleRef: null,
rootSnapshotRef,
stageAckRefs: [],
enableAckRefs: [],
error: null,
});
await this.continue(command.operationId);
}
async bootstrap(value: unknown): Promise<void> {
const command = parseManagedExtensionApplyCommand(value);
if (command.expectedRevision !== 0 || command.mcpChange)
throw new Error('Invalid initial extension selection.');
await this.submit(command);
const active = await this.active();
if (
!this.activeBundleRef ||
!active ||
active.configRevision < 1 ||
this.pending
)
throw new HostedExtensionRecoveryRequired();
}
async continue(operationId: string): Promise<void> {
let latest = this.history.at(-1);
if (!latest || latest.command.operationId !== operationId)
throw new HostedExtensionRecoveryRequired();
if (
latest.phase === 'active' ||
(latest.phase === 'failed' && latest.mode === 'apply')
)
return;
if (latest.phase === 'failed') throw new HostedExtensionRecoveryRequired();
const broker = await this.runtime(operationId, latest.rootSnapshotRef);
if (latest.phase === 'preparing') {
try {
latest = await this.prepare(latest, broker);
} catch (cause) {
if (latest.mode === 'apply')
await this.commit({
...this.content(latest),
phase: 'failed',
error: { code: 'extension_prepare_failed' },
});
throw cause;
}
}
if (latest.phase === 'prepared')
latest = await this.commit({
...this.content(latest),
phase: 'committed',
});
if (latest.phase !== 'committed' && latest.phase !== 'recovery_required')
throw new HostedExtensionRecoveryRequired();
const current = latest;
try {
const phaseOperationId = current.phaseOperationId;
const status = await broker.extensionControl({
kind: 'extension-status',
operationId: phaseOperationId,
});
if (status['state'] === 'prepared') {
await this.prepareRuntime(current, broker);
await this.callbacks.drain(
await this.bundle(current.bundleRef!),
current.mode === 'apply' &&
['reload', 'restart'].includes(
current.command.mcpChange?.action ?? '',
)
? current.command.mcpChange?.serverId
: undefined,
);
await broker.extensionControl({
kind: 'extension-activate',
operationId: phaseOperationId,
expectedRevision: current.previousConfigRevision,
revision: current.configRevision,
});
} else if (
status['state'] !== 'active' ||
status['revision'] !== current.configRevision
) {
throw new HostedExtensionRecoveryRequired();
}
const enabled = await broker.extensionControl({
kind: 'extension-status',
operationId: phaseOperationId,
});
if (
enabled['state'] !== 'active' ||
enabled['revision'] !== current.configRevision ||
enabled['executionMode'] !== this.executionMode(broker)
)
throw new HostedExtensionRecoveryRequired();
const bundle = await this.bundle(current.bundleRef!);
await this.callbacks.enable(
bundle,
await this.packages(bundle),
current.phaseOperationId,
);
const runtimeAck = await this.publish('managed-extension-enable-ack', {
operationId,
revision: current.configRevision,
executionMode: bundle.executionMode,
side: 'runtime',
});
const harnessAck = await this.publish('managed-extension-enable-ack', {
operationId,
revision: current.configRevision,
bundleRef: current.bundleRef,
side: 'harness',
});
await this.commit({
...this.content(current),
phase: 'active',
enableAckRefs: [runtimeAck, harnessAck],
error: null,
});
} catch (cause) {
if (current.phase !== 'recovery_required')
await this.commit({
...this.content(current),
phase: 'recovery_required',
error: { code: 'extension_recovery_required' },
});
throw cause;
}
}
async recoverPrevious(operationId: string): Promise<void> {
const latest = this.history.at(-1);
if (!latest || latest.command.operationId !== operationId)
throw new HostedExtensionRecoveryRequired();
if (latest.mode === 'recoverPrevious') {
await this.continue(operationId);
return;
}
if (
latest.phase !== 'recovery_required' ||
!latest.bundleRef ||
!latest.previousBundleRef
)
throw new HostedExtensionRecoveryRequired();
const broker = await this.runtime(operationId, latest.rootSnapshotRef);
const proof = await broker.extensionControl({
kind: 'extension-reconcile',
operationId,
expectedRevision: latest.previousConfigRevision,
revision: latest.configRevision,
});
if (
proof['state'] !== 'stopped' ||
proof['revision'] !== latest.configRevision ||
proof['executionMode'] !== this.executionMode(broker) ||
!Array.isArray(proof['receipts']) ||
!proof['receipts'].length
)
throw new HostedExtensionRecoveryRequired();
await this.callbacks.drain(await this.bundle(latest.previousBundleRef));
await this.commit({
...this.content(latest),
mode: 'recoverPrevious',
phaseOperationId: managedExtensionCompensationOperationId(operationId),
configRevision: latest.configRevision + 1,
previousConfigRevision: latest.configRevision,
previousBundleRef: latest.bundleRef,
restoresBundleRef: latest.previousBundleRef,
bundleRef: null,
phase: 'preparing',
stageAckRefs: [],
enableAckRefs: [],
error: null,
});
await this.continue(operationId);
}
async runtimeOwner(): Promise<RuntimeOwner | undefined> {
const latest = this.history.findLast((entry) => entry.phase === 'active');
return latest ? this.read<RuntimeOwner>(latest.rootSnapshotRef) : undefined;
}
async submitMcp(command: HostedMcpCommand): Promise<void> {
const replay = this.history.findLast(
(entry) => entry.command.operationId === command.operationId,
);
if (replay) {
const change = replay.command.mcpChange;
const server = change?.servers.find(
(entry) => entry.serverId === command.serverId,
);
if (
!change ||
replay.command.actorKey !== command.actorKey ||
change.action !== command.action ||
change.serverId !== command.serverId ||
change.expectedRevision !== command.expectedRevision ||
(command.server &&
(!server ||
command.server.serverRevision !== server.serverRevision ||
command.server.definitionDigest !== server.definitionDigest))
)
throw new HostedMcpConflictError('MCP command identity conflicts.');
await this.submit(replay.command);
return;
}
const active = await this.active();
if (!active || this.pending) throw new HostedExtensionRecoveryRequired();
if (command.expectedRevision !== active.mcpRevision)
throw new HostedMcpConflictError('MCP settings revision conflicts.');
if (
active.mcpServers.some((pin) => pin.serverId === command.serverId) &&
!active.independentMcpSelections.some(
(pin) => pin.serverId === command.serverId,
)
)
throw new HostedMcpConflictError(
'Manage this MCP server through its extension.',
);
const servers = selectedMcpServers(
active.independentMcpSelections,
command,
);
const broker = await this.runtime(command.operationId);
const available = await broker.mcpDefinitions();
const selected = servers.find((pin) => pin.serverId === command.serverId)!;
if (
(selected.state === 'enabled' || command.action === 'edit') &&
!available.some(
(pin) =>
pin.serverId === selected.serverId &&
pin.serverRevision === selected.serverRevision &&
pin.definitionDigest === selected.definitionDigest,
)
)
throw new HostedMcpConflictError(
'MCP definition is not published for this Workspace.',
);
await this.submit({
operationId: command.operationId,
actorKey: command.actorKey,
expectedRevision: active.configRevision,
extensions: active.packages.map((entry) => entry.pin),
mcpChange: {
action: command.action,
serverId: command.serverId,
expectedRevision: command.expectedRevision,
servers,
},
});
}
async mcpSnapshot(operationId?: string, inventory = true) {
const active = await this.active();
if (!active) throw new HostedExtensionRecoveryRequired();
const record = operationId
? this.history.findLast(
(entry) => entry.command.operationId === operationId,
)
: this.history.findLast((entry) => entry.command.mcpChange !== undefined);
if (operationId && !record?.command.mcpChange)
throw new Error('MCP operation was not found.');
const broker = new HostedWorkspaceBroker(
this.brokerOptions,
this.session.authority.sessionHeader.sessionKey,
'mcp-management-inventory',
);
const change = record?.command.mcpChange;
return {
sessionId: this.session.authority.sessionHeader.sessionKey.sessionId,
revision: active.mcpRevision,
supported: true,
availableServers: inventory ? await broker.mcpDefinitions() : [],
servers: active.independentMcpSelections,
...(record && change
? {
operation: {
operationId: record.command.operationId,
action: change.action,
serverId: change.serverId,
revision: change.expectedRevision + 1,
state:
record.phase === 'active'
? 'settled'
: record.phase === 'failed'
? 'failed'
: record.phase === 'recovery_required'
? 'recovery_required'
: 'pending',
...(record.error ? { error: record.error } : {}),
},
}
: {}),
};
}
private async prepareRuntime(
record: ManagedConfigInstallRecord,
broker: HostedWorkspaceBroker,
) {
const baseline = await this.bundle(
record.mode === 'recoverPrevious'
? record.restoresBundleRef!
: record.previousBundleRef!,
);
const change =
record.mode === 'apply' ? record.command.mcpChange : undefined;
if (change && change.expectedRevision !== baseline.mcpRevision)
throw new HostedMcpConflictError('MCP component revision conflicts.');
const selected = change?.servers ?? baseline.independentMcpSelections;
const baseMcpServers = selected
.filter((entry) => entry.state === 'enabled')
.map(({ state: _state, ...pin }) => pin);
const command =
record.mode === 'apply'
? record.command
: {
operationId: record.phaseOperationId,
actorKey: record.command.actorKey,
expectedRevision: record.previousConfigRevision,
extensions: baseline.packages.map((entry) => entry.pin),
};
const result = await broker.extensionControl({
kind: 'extension-prepare',
operationId: record.phaseOperationId,
command,
expectedRevision: record.previousConfigRevision,
extensions: command.extensions,
baseMcpServers,
baseHookCatalog: baseline.baseHookCatalog,
});
if (
result['state'] !== 'prepared' ||
result['revision'] !== record.configRevision ||
result['executionMode'] !== baseline.executionMode ||
!Array.isArray(result['packages'])
)
throw new HostedExtensionRecoveryRequired();
const prepared = result as unknown as ManagedExtensionPrepareResult;
if (
JSON.stringify(prepared.packages.map((entry) => entry.pin)) !==
JSON.stringify(command.extensions)
)
throw new HostedExtensionRecoveryRequired();
return { prepared, baseline, selected, baseMcpServers };
}
private async prepare(
record: ManagedConfigInstallRecord,
broker: HostedWorkspaceBroker,
) {
const { prepared, baseline, selected, baseMcpServers } =
await this.prepareRuntime(record, broker);
const packages = await Promise.all(
prepared.packages.map(async (entry) => ({
pin: entry.pin,
assetRoot: `extension://${entry.pin.extensionId}/`,
contributionsRef: await publishManagedExtensionContributions(
this.session.resources,
entry.contributions,
),
})),
);
this.callbacks.validate(
prepared.packages.map((entry) => ({
extensionId: entry.pin.extensionId,
contributions: entry.contributions,
})),
);
const previous =
record.mode === 'recoverPrevious'
? await this.bundle(record.previousBundleRef!)
: undefined;
const bundle = parseManagedExtensionEffectiveBundle({
kind: 'managed-extension-config-bundle',
formatVersion: 1,
configRevision: record.configRevision,
executionMode: prepared.executionMode,
mcpRevision: previous
? previous.mcpRevision + 1
: baseline.mcpRevision + (record.command.mcpChange ? 1 : 0),
independentMcpSelections: selected,
...(record.mode === 'apply' && record.command.mcpChange
? { mcpChange: record.command.mcpChange }
: {}),
baseMcpServers,
baseHookCatalog: baseline.baseHookCatalog,
packages,
mcpServers: prepared.mcpServers,
hookCatalog: prepared.hookCatalog,
});
const bundleRef = await this.publish(
'managed-extension-config-bundle',
bundle,
);
const runtimeAck = await this.publish('managed-extension-stage-ack', {
operationId: record.command.operationId,
revision: record.configRevision,
bundleRef,
side: 'runtime',
executionMode: prepared.executionMode,
});
const harnessAck = await this.publish('managed-extension-stage-ack', {
operationId: record.command.operationId,
revision: record.configRevision,
bundleRef,
side: 'harness',
});
return this.commit({
...this.content(record),
phase: 'prepared',
bundleRef,
stageAckRefs: [runtimeAck, harnessAck],
});
}
private async runtime(
operationId: string,
rootSnapshotRef?: ManagedSessionDurableRef,
) {
const owner = rootSnapshotRef
? await this.read<RuntimeOwner>(rootSnapshotRef)
: undefined;
if (
owner &&
JSON.stringify(owner.rootSnapshotRef) !==
JSON.stringify(this.session.authority.sessionHeader.rootSnapshotRef)
)
throw new HostedExtensionRecoveryRequired();
const id =
owner?.runtimeSessionId ??
`extensions-${hash([this.session.authority.sessionHeader.sessionKey, operationId])}`;
const broker = new HostedWorkspaceBroker(
this.brokerOptions,
this.session.authority.sessionHeader.sessionKey,
id,
);
if (owner)
broker.expectExtensionRuntime({
runtimeBindingId: owner.runtime.bindingId,
generation: owner.runtime.generation,
});
else {
await broker.warm();
if (!broker.runtime) throw new HostedExtensionRecoveryRequired();
broker.expectExtensionRuntime({
runtimeBindingId: broker.runtime.bindingId,
generation: broker.runtime.generation,
});
}
await broker.extensionControl({ kind: 'extension-status', operationId });
if (owner && owner.executionMode !== this.executionMode(broker))
throw new HostedExtensionRecoveryRequired();
return broker;
}
private executionMode(
broker: HostedWorkspaceBroker,
): ManagedExtensionEffectiveBundle['executionMode'] {
if (broker.capabilityDigest === MANAGED_EXTENSION_LINUX_CAPABILITY_DIGEST)
return 'linux-cgroup';
if (
broker.capabilityDigest === MANAGED_EXTENSION_DEVELOPER_CAPABILITY_DIGEST
)
return 'developer-process-group';
throw new Error(
'The selected Runtime does not support extension packages.',
);
}
private content(
record: ManagedConfigInstallRecord,
): ManagedConfigInstallContent {
const {
operationId: _operationId,
revision: _revision,
previousRecordRef: _previous,
...content
} = record;
return content;
}
private async commit(content: ManagedConfigInstallContent) {
const receipt = await this.session.authority.commitDomainRecord(
{
operation: `applyExtensions.${content.mode}.${content.phase}`,
commandId: content.command.operationId,
sessionKey: this.session.authority.sessionHeader.sessionKey,
contentDigest: hash(content),
},
{
domain: 'config_install',
content: { ...content },
...(content.phase === 'committed'
? { configBinding: managedConfigBinding(content) }
: {}),
},
{ class: 'trusted_entry' },
);
const record = parseManagedConfigInstallRecord(
await this.read(receipt.recordRef),
);
this.history.push(record);
return record;
}
private async bundle(ref: ManagedSessionDurableRef) {
if (
ref.kind !== 'managed-extension-config-bundle' ||
ref.schemaVersion !== 1
)
throw new HostedExtensionRecoveryRequired();
return parseManagedExtensionEffectiveBundle(await this.read(ref));
}
private read<T>(ref: ManagedSessionDurableRef): Promise<T> {
return this.session.resources
.read(ref)
.then((bytes) => JSON.parse(bytes.toString('utf8')) as T);
}
private publish(kind: string, value: unknown) {
return this.session.resources.publish(
kind,
Buffer.from(JSON.stringify(value)),
);
}
}
function hash(value: unknown): string {
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
}
@@ -0,0 +1,136 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect, it } from 'vitest';
import {
extensionInstructionText,
extensionToolDeclaration,
extensionToolInput,
} from './hosted-extension-tool-context.js';
const source = {
extensionId: 'one',
packageRevision: 2,
packageDigest: 'a'.repeat(64),
};
const pin = { ...source, settingsRevision: 3 };
const config = {
revision: 7,
packages: [{ pin, assetRoot: 'extension://one/' }],
};
describe('Hosted extension native tool context', () => {
it('binds a model-selected exact package to the current Harness revision and private settings pin', () => {
expect(
extensionToolInput('root', config, 'read_file', {
file_path: 'asset.txt',
extension_source: source,
}),
).toEqual({
file_path: 'asset.txt',
managedExtension: { harnessSessionId: 'root', configRevision: 7, pin },
});
expect(
extensionToolInput('root', config, 'run_shell_command', {
command: 'pwd',
extension_context: source,
}),
).toMatchObject({ command: 'pwd', managedExtension: { pin } });
expect(
extensionToolInput('root', config, 'read_file', {
file_path: 'workspace.txt',
}),
).toEqual({ file_path: 'workspace.txt' });
});
it('rejects fabricated private metadata, stale pins, writes, and background execution', () => {
for (const [tool, input] of [
['read_file', { managedExtension: { pin } }],
['read_file', { extension_source: { ...source, packageRevision: 1 } }],
['read_file', { extension_source: pin }],
['write_file', { extension_source: source }],
['run_shell_command', { extension_source: source }],
['read_file', { extension_context: source }],
['run_shell_command', { extension_context: source, is_background: true }],
['run_shell_command', { extension_context: source, is_monitor: true }],
] as const)
expect(() => extensionToolInput('root', config, tool, input)).toThrow();
});
it('publishes only public pin selectors without mutating the original native schema', () => {
const declaration = {
name: 'read_file',
description: 'Read',
parametersJsonSchema: {
type: 'object',
properties: { file_path: { type: 'string' } },
},
};
const augmented = extensionToolDeclaration(declaration, config);
expect(augmented.parametersJsonSchema).toMatchObject({
properties: {
extension_source: {
additionalProperties: false,
required: ['extensionId', 'packageRevision', 'packageDigest'],
oneOf: [
{
properties: {
extensionId: { const: 'one' },
packageRevision: { const: 2 },
packageDigest: { const: 'a'.repeat(64) },
},
},
],
},
},
});
expect(declaration.parametersJsonSchema.properties).not.toHaveProperty(
'extension_source',
);
expect(JSON.stringify(augmented)).not.toContain('settingsRevision');
expect(
extensionToolDeclaration({ ...declaration, name: 'write_file' }, config)
.parametersJsonSchema,
).toBe(declaration.parametersJsonSchema);
});
it('maps instruction assets to the selected logical package without exposing a Runtime path', () => {
expect(
extensionInstructionText(
'Read ${extensionPath}/references/guide.md and ${CLAUDE_PLUGIN_ROOT}/data.txt',
'one',
),
).toBe(
'Read extension://one/references/guide.md and extension://one/data.txt',
);
expect(
extensionToolInput('root', config, 'read_file', {
extension_source: source,
file_path: 'extension://one/references/guide.md',
}),
).toMatchObject({
file_path: 'references/guide.md',
managedExtension: { pin },
});
expect(
extensionToolInput('root', config, 'glob', {
extension_source: source,
path: 'extension://one/references',
pattern: '**/*.md',
}),
).toMatchObject({ path: 'references', pattern: '**/*.md' });
for (const file_path of [
'extension://other/data.txt',
'extension://one/../private.txt',
'extension://one//../../private.txt',
])
expect(() =>
extensionToolInput('root', config, 'read_file', {
extension_source: source,
file_path,
}),
).toThrow();
});
});
@@ -0,0 +1,139 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type { FunctionDeclaration } from '@google/genai';
import {
parseManagedExtensionPin,
type ManagedExtensionPin,
} from '@qwen-code/qwen-code-core/managed-runtime/managed-extension-package.js';
import { normalizeWorkspaceRelativePath } from './managed-workspace-binding.js';
export interface HostedExtensionToolConfig {
readonly revision: number;
readonly packages: ReadonlyArray<{
readonly pin: ManagedExtensionPin;
readonly assetRoot: string;
}>;
}
const sourceSchema = {
type: 'object',
properties: {
extensionId: { type: 'string' },
packageRevision: { type: 'integer', minimum: 1 },
packageDigest: { type: 'string', pattern: '^[a-f0-9]{64}$' },
},
required: ['extensionId', 'packageRevision', 'packageDigest'],
additionalProperties: false,
};
export function extensionToolDeclaration(
tool: FunctionDeclaration,
config?: HostedExtensionToolConfig,
): FunctionDeclaration {
if (
!config?.packages.length ||
!['read_file', 'glob', 'run_shell_command'].includes(tool.name ?? '')
)
return tool;
const schema = structuredClone(tool.parametersJsonSchema) as {
properties: Record<string, unknown>;
};
schema.properties[
tool.name === 'run_shell_command' ? 'extension_context' : 'extension_source'
] = {
...sourceSchema,
oneOf: config.packages.map(({ pin }) => ({
properties: {
extensionId: { const: pin.extensionId },
packageRevision: { const: pin.packageRevision },
packageDigest: { const: pin.packageDigest },
},
})),
};
return {
...tool,
parametersJsonSchema: schema,
description: `${tool.description ?? ''} Select one of the listed exact extension pins to read its read-only assets using a package-relative path or extension://<extensionId>/<path>. For Shell, select extension_context and use $QWEN_EXTENSION_ROOT for package files; cwd stays the Session working directory.`,
};
}
export function extensionToolInput(
harnessSessionId: string,
config: HostedExtensionToolConfig | undefined,
toolName: string,
input: Readonly<Record<string, unknown>>,
): Record<string, unknown> {
const { extension_source: source, extension_context: context } = input;
const ordinary: Record<string, unknown> = { ...input };
delete ordinary['extension_source'];
delete ordinary['extension_context'];
if (Object.hasOwn(input, 'managedExtension'))
throw new Error('Private extension tool metadata is not a model argument.');
if (source === undefined && context === undefined) return ordinary;
const shell = toolName === 'run_shell_command';
if (
(!shell && !['read_file', 'glob'].includes(toolName)) ||
(shell ? source !== undefined : context !== undefined) ||
!config
)
throw new Error('This tool does not admit the selected extension context.');
const requested = shell ? context : source;
if (
!requested ||
typeof requested !== 'object' ||
Array.isArray(requested) ||
Object.keys(requested).sort().join(',') !==
'extensionId,packageDigest,packageRevision'
)
throw new Error('Select an exact extension package pin.');
const pin = parseManagedExtensionPin(requested);
const selected = config.packages.find(
(entry) =>
entry.pin.extensionId === pin.extensionId &&
entry.pin.packageRevision === pin.packageRevision &&
entry.pin.packageDigest === pin.packageDigest,
);
if (!selected)
throw new Error(
'The extension package is not selected in this Session configuration.',
);
if (!shell) {
for (const field of ['file_path', 'path', 'pattern']) {
const value = ordinary[field];
if (typeof value !== 'string' || !value.startsWith('extension://'))
continue;
const prefix = `extension://${pin.extensionId}/`;
if (!value.startsWith(prefix))
throw new Error('Asset path names another extension.');
ordinary[field] = normalizeWorkspaceRelativePath(
value.slice(prefix.length),
);
}
}
if (
shell &&
(ordinary['is_background'] === true || ordinary['is_monitor'] === true)
)
throw new Error('Extension Shell requires a foreground invocation.');
return {
...ordinary,
managedExtension: {
harnessSessionId,
configRevision: config.revision,
pin: selected.pin,
},
};
}
export function extensionInstructionText(
body: string,
extensionId: string,
): string {
return body
.replaceAll('${extensionPath}', `extension://${extensionId}`)
.replaceAll('${CLAUDE_PLUGIN_ROOT}', `extension://${extensionId}`);
}
@@ -68,6 +68,7 @@ describe('Hosted Harness private contract', () => {
'session-skills/2',
'workspace-shell-mcp/1',
'session-mcp-management/1',
'session-extension-packages/1',
'recovery-cleanup/1',
'targeted-cancel/1',
'event-keepalive/1',
@@ -97,6 +97,7 @@ export function createHostedHarnessContract(
'session-skills/2',
'workspace-shell-mcp/1',
'session-mcp-management/1',
'session-extension-packages/1',
'recovery-cleanup/1',
'targeted-cancel/1',
// GET /session/:id/events writes keepalive comments while no event
@@ -2963,6 +2963,83 @@ it('keeps agent instructions ahead of refreshed project context', async () => {
);
});
it('preserves pinned instruction sources when Todo changes after a tool batch', async () => {
const mocks = config([]);
const observed: string[] = [];
const skills = new HostedSkillCatalog([
{ name: 'published', description: 'SKILL_SECTION', body: 'body' },
]);
let todo = 'TODO_FIRST';
let round = 0;
const call = {
callId: 'context-call',
name: 'read_file',
args: {},
isClientInitiated: false,
prompt_id: input.promptId,
};
mocks.getHistory.mockImplementation(() => [
{
role: 'model',
parts:
round === 1
? [{ functionCall: { name: call.name } }]
: [{ text: 'done' }],
},
]);
mocks.sendMessageStream.mockImplementation(async function* () {
observed.push(mocks.setUserMemory.mock.calls.at(-1)?.[0] ?? '');
round++;
if (round === 1) yield { type: LlmEventType.ToolCallRequest, value: call };
else yield { type: LlmEventType.Content, value: 'done' };
yield { type: LlmEventType.Finished };
});
const execute = vi.fn(async () => {
todo = 'TODO_UPDATED';
return [
{
functionResponse: {
id: call.callId,
name: call.name,
response: { output: 'contents' },
},
},
];
});
await runHostedHarnessTextTurn({
...input,
agentInstructions: 'AGENT_SECTION',
extensionInstructions: 'EXTENSION_SECTION',
skills,
workspaceContext: { read: () => 'WORKSPACE_SECTION' },
toolTurn: {
declarations: async () => [{ name: call.name }],
consumeResults: async () => undefined,
execute,
planModeInstructions: () => 'PLAN_SECTION',
todoInstructions: async () => todo,
},
});
expect(execute).toHaveBeenCalledOnce();
expect(observed).toHaveLength(2);
for (const [index, prompt] of observed.entries()) {
let position = -1;
for (const marker of [
'AGENT_SECTION',
'EXTENSION_SECTION',
'SKILL_SECTION',
'PLAN_SECTION',
index === 0 ? 'TODO_FIRST' : 'TODO_UPDATED',
'WORKSPACE_SECTION',
]) {
const next = prompt.indexOf(marker);
expect(next).toBeGreaterThan(position);
position = next;
}
}
expect(observed[1]).not.toContain('TODO_FIRST');
});
it('advertises only the published Skill catalog while keeping host Skill discovery disabled', async () => {
const mocks = config([{ type: LlmEventType.Finished }]);
await runHostedHarnessTextTurn({
+36 -4
View File
@@ -303,6 +303,7 @@ export async function runHostedHarnessTextTurn(input: {
| 'setToolOutputBudget'
| 'capturesChildModelRoute'
| 'setChildModelRoute'
| 'setChildModelResolver'
| 'planModeInstructions'
| 'todoInstructions'
>
@@ -312,6 +313,7 @@ export async function runHostedHarnessTextTurn(input: {
modelSelection?: HostedModelSelection;
publicChildModelRoute?: PublicChildModelRoute;
agentInstructions?: string;
extensionInstructions?: string;
skills?: HostedSkillCatalog;
textDeltas?: HostedHarnessTextDeltas;
completeFinalOutput?: (
@@ -381,6 +383,7 @@ async function runPreparedHostedHarnessTextTurn(
input.agentInstructions
? `# Agent instructions\n\n${input.agentInstructions}`
: undefined,
input.extensionInstructions,
input.skills?.instructions(),
input.toolTurn?.planModeInstructions?.(),
todoContext,
@@ -500,7 +503,7 @@ async function runPreparedHostedHarnessTextTurn(
await input.hooks?.ensureReady(input.signal);
// Seed instructions when memory reload is skipped; restore them after
// initialize() when its hierarchical memory refresh replaces user memory.
if (input.agentInstructions || input.skills)
if (input.agentInstructions || input.extensionInstructions || input.skills)
config.setUserMemory(
contextWithInstructions(input.workspaceContext?.read()),
);
@@ -517,7 +520,11 @@ async function runPreparedHostedHarnessTextTurn(
const authType = config.getModelsConfig().getCurrentAuthType();
if (!authType)
throw new Error('Hosted Harness model authentication is unavailable.');
if (input.agentInstructions || input.skills) {
if (
input.agentInstructions ||
input.extensionInstructions ||
input.skills
) {
config.setUserMemory(
contextWithInstructions(input.workspaceContext?.read()),
);
@@ -625,6 +632,31 @@ async function runPreparedHostedHarnessTextTurn(
}
: {}),
});
input.toolTurn.setChildModelResolver?.(async (selector) => {
const resolved = await config
.getBaseLlmClient()
.resolveForModel(selector, { failClosed: true });
const selected = resolved.contentGeneratorConfig;
if (!selected.authType) throw new HostedModelUnavailableError();
return {
modelId: resolved.model,
route: {
authType: selected.authType,
endpointDigest: createHash('sha256')
.update(selected.baseUrl ?? '')
.digest('hex'),
providerPin: llmPreparedProviderPin(selected),
origin: {
modelId: config.getModel(),
authType: active.authType!,
endpointDigest: createHash('sha256')
.update(active.baseUrl ?? '')
.digest('hex'),
providerPin: llmPreparedProviderPin(active),
},
},
};
});
};
if (
[...initialMessages, ...(input.resumeModel?.prepared.history ?? [])].some(
@@ -636,7 +668,7 @@ async function runPreparedHostedHarnessTextTurn(
config.getEffectiveInputModalities().image !== true
)
throw new HostedImageInputUnsupportedError();
if (input.agentInstructions || input.skills)
if (input.agentInstructions || input.extensionInstructions || input.skills)
await config.getLlmClient().refreshSystemInstruction();
await input.modelScope?.bindBudget(config.getTurnBudget(), input.prompt);
runPromptHook = input.modelScope
@@ -871,7 +903,7 @@ async function runPreparedHostedHarnessTextTurn(
const calls: ToolCallRequestInfo[] = selectedSkills.map((skill) => ({
callId: randomUUID(),
name: 'skill',
args: { skill },
args: { skill, ...input.skills?.userArguments(skill, effectivePrompt) },
isClientInitiated: true,
origin: { kind: 'client' },
prompt_id: input.promptId,
@@ -102,6 +102,7 @@ import {
HostedWorkspaceBrokerRejection,
} from './hosted-workspace-broker.js';
import { WORKSPACE_CAPABILITY_DIGEST } from './managed-workspace-activation.js';
import { MANAGED_EXTENSION_DEVELOPER_CAPABILITY_DIGEST } from './managed-extension-profile.js';
import { HostedShellPublisher } from './hosted-shell-publisher.js';
import type { ShellPublisherDescriptor } from './managed-shell-publisher.js';
import {
@@ -545,16 +546,28 @@ afterEach(async () => {
listeners.clear();
});
async function app(withBroker = false, instanceId?: string) {
async function app(
withBroker = false,
instanceId?: string,
extensionPackages = true,
) {
const result = express();
result.use(express.json());
const contract = createHostedHarnessContract(
const advertised = createHostedHarnessContract(
`sha256:${'a'.repeat(64)}`,
BOOT_ID,
['hosted-workspace-shell/1', 'hosted-workspace-mcp/1'],
instanceId,
);
const contract = extensionPackages
? advertised
: {
...advertised,
features: advertised.features?.filter(
(feature) => feature !== 'session-extension-packages/1',
),
};
installHostedHarnessContractMiddleware(result, contract);
registerHostedHarnessSessionRoutes(
result,
@@ -23345,6 +23358,165 @@ describe('Hosted Harness tool approvals', () => {
const files = 'hosted-workspace-files/1';
it('loads a failed initial extension selection for management while keeping the first Prompt blocked', async () => {
vi.spyOn(
HostedWorkspaceBroker.prototype,
'capabilityDigest',
'get',
).mockReturnValue(MANAGED_EXTENSION_DEVELOPER_CAPABILITY_DIGEST);
vi.mocked(HostedWorkspaceBroker.prototype.warm).mockImplementation(
async function (this: HostedWorkspaceBroker) {
this.runtime = {
bindingId: 'original-binding',
generation: '1',
workspaceGeneration: '1',
};
},
);
let failPrepare = true;
const runtimeStates = new Map<string, string>();
const control = vi
.spyOn(HostedWorkspaceBroker.prototype, 'extensionControl')
.mockImplementation(async (operation) => {
const common = {
operationId: operation.operationId,
executionMode: 'developer-process-group',
};
if (operation.kind === 'extension-prepare') {
if (failPrepare) throw new Error('invalid initial package');
runtimeStates.set(operation.operationId, 'prepared');
return {
...common,
state: 'prepared',
expectedRevision: operation.expectedRevision,
revision: operation.expectedRevision + 1,
packages: operation.extensions.map((pin) => ({
pin,
assetRoot: `extension://${pin.extensionId}/`,
contributions: {
skills: [],
commands: [],
agents: [],
context: [],
mcpServers: {},
hooks: {},
settings: [],
},
})),
mcpServers: [],
hookCatalog: null,
};
}
if (operation.kind === 'extension-activate')
runtimeStates.set(operation.operationId, 'active');
return {
...common,
state: runtimeStates.get(operation.operationId),
revision: 1,
};
});
const initialSelection = {
operationId: randomUUID(),
actorKey: 'a'.repeat(64),
expectedRevision: 0,
extensions: [
{
extensionId: 'sample',
packageRevision: 1,
packageDigest: 'b'.repeat(64),
},
],
};
const definition = {
managedSessionStore: store(),
toolProfile: 'hosted-workspace-shell/1',
publicChildAgentsVersion: 1,
agentDefinition: {
agentId: `agent_${'c'.repeat(32)}`,
revision: '1',
digest: 'd'.repeat(64),
},
initialSelection,
};
const original = await app(true);
await headers(supertest(original).post('/session'))
.send({ ...definition, sessionId: SESSION_ID, sessionScope: 'thread' })
.expect(503);
await headers(supertest(original).post(`/session/${SESSION_ID}/load`))
.send(definition)
.expect(503);
expect(control.mock.calls.map(([operation]) => operation.kind)).toEqual([
'extension-status',
'extension-status',
'extension-prepare',
]);
const replacement = await app(true);
const attachment = await headers(
supertest(replacement).post(`/session/${SESSION_ID}/load`),
)
.send({ ...definition, passiveManagedRuntimeRecovery: true })
.expect(200);
expect(control).toHaveBeenCalledTimes(3);
expect(HostedWorkspaceBroker.prototype.warm).toHaveBeenCalledOnce();
expect(state.model).not.toHaveBeenCalled();
const authorize = (request: supertest.Test) =>
headers(request).set('X-Qwen-Client-Id', attachment.body.clientId);
const prompt = [{ type: 'text', text: 'start after configuration' }];
const promptBody = {
prompt,
promptId: PROMPT_ID,
payloadDigest: `sha256:${createHash('sha256').update(JSON.stringify(prompt)).digest('hex')}`,
};
await authorize(
supertest(replacement).post(`/session/${SESSION_ID}/prompt`),
)
.send(promptBody)
.expect(409);
failPrepare = false;
const repair = await authorize(
supertest(replacement).post(
`/session/${SESSION_ID}/extensions/management`,
),
)
.send({ ...initialSelection, operationId: randomUUID() })
.expect(202);
expect(repair.body).toMatchObject({
revision: 1,
operation: { state: 'active' },
});
const callsBeforeConflict = control.mock.calls.length;
const rejected = await authorize(
supertest(replacement).post(
`/session/${SESSION_ID}/extensions/management`,
),
)
.send({ ...initialSelection, operationId: randomUUID() })
.expect(409);
expect(rejected.body).toMatchObject({
error: 'extension_config_revision_conflict',
});
expect(control).toHaveBeenCalledTimes(callsBeforeConflict);
state.model.mockResolvedValue({ text: 'ready', model: 'test-model' });
await authorize(
supertest(replacement).post(`/session/${SESSION_ID}/prompt`),
)
.send(promptBody)
.expect(202);
await waitFor(async () => {
const status = await authorize(
supertest(replacement).get(`/session/${SESSION_ID}/status`),
);
expect(status.body).toMatchObject({
hasActivePrompt: false,
recoveryBlocked: false,
});
});
expect(state.model).toHaveBeenCalledOnce();
await authorize(
supertest(replacement).post(`/session/${SESSION_ID}/detach`),
).expect(204);
});
it.each(['write_file', 'exit_plan_mode'] as const)(
'rechecks live approval after a cold restore while preserving %s approval semantics',
async (toolName) => {
@@ -38512,6 +38684,72 @@ describe('Hosted Harness stored definitions', () => {
});
});
it('creates and cold-loads a custom public Shell definition with base Hooks and no extension selection', async () => {
vi.spyOn(HostedWorkspaceBroker.prototype, 'release').mockResolvedValue();
vi.spyOn(
HostedWorkspaceBroker.prototype,
'activeHoldRuntimeSessionIds',
).mockResolvedValue([]);
const server = await app(true);
const request = {
managedSessionStore: store(),
toolProfile: 'hosted-workspace-shell/1',
publicChildAgentsVersion: 1,
lifecycleProtocolVersion: 2,
agentDefinition: {
agentId: `agent_${'a'.repeat(32)}`,
revision: '1',
digest: 'b'.repeat(64),
},
hookCatalog: {
catalogId: 'deployment',
catalogRevision: 1,
definitionDigest: 'c'.repeat(64),
},
};
const created = await headers(supertest(server).post('/session')).send({
...request,
sessionId: SESSION_ID,
sessionScope: 'thread',
});
expect(created.status).toBe(200);
await headers(supertest(server).post(`/session/${SESSION_ID}/detach`))
.set('X-Qwen-Client-Id', created.body.clientId)
.expect(204);
const replacement = await app(true);
const loaded = await headers(
supertest(replacement).post(`/session/${SESSION_ID}/load`),
)
.send({ ...request, passiveManagedRuntimeRecovery: true })
.expect(200);
expect(state.model).not.toHaveBeenCalled();
await headers(supertest(replacement).post(`/session/${SESSION_ID}/detach`))
.set('X-Qwen-Client-Id', loaded.body.clientId)
.expect(204);
const refused = await headers(
supertest(replacement).post(`/session/${SESSION_ID}/load`),
).send({ ...request, publicChildAgentsVersion: 0 });
expect(refused.status).toBe(409);
});
it('keeps the public custom Shell gate closed on the legacy extension contract', async () => {
const server = await app(true, undefined, false);
await headers(supertest(server).post('/session'))
.send({
sessionId: SESSION_ID,
sessionScope: 'thread',
managedSessionStore: store(),
toolProfile: 'hosted-workspace-shell/1',
publicChildAgentsVersion: 1,
agentDefinition: {
agentId: `agent_${'a'.repeat(32)}`,
revision: '1',
digest: 'b'.repeat(64),
},
})
.expect(400);
});
it('pins creation-time Skills for the built-in agent across resident and cold loads', async () => {
const server = await app(true);
const resources = LocalManagedSessionResourceStore.create({
+658 -16
View File
@@ -6,6 +6,7 @@
import {
parsePublicChildModelRoute,
type PublicChildExtensionAgent,
type PublicChildModelRoute,
} from '@qwen-code/qwen-code-core/managed-runtime/managed-child-operations.js';
@@ -13,6 +14,7 @@ import {
HostedContextCompression,
HostedCompressionRefusal,
} from './hosted-context-compression.js';
import { extensionInstructionText } from './hosted-extension-tool-context.js';
import {
parseHostedApprovalSelection,
@@ -92,6 +94,25 @@ import {
readHostedMcpSettings,
validateHostedMcpHistory,
} from './hosted-mcp-settings.js';
import { isManagedExtensionConfig } from '@qwen-code/qwen-code-core/managed-runtime/managed-config-install.js';
import {
parseManagedExtensionApplyCommand,
type ManagedExtensionApplyCommand,
parseManagedExtensionEffectiveBundle,
type ManagedExtensionEffectiveBundle,
} from '@qwen-code/qwen-code-core/managed-runtime/managed-extension-package.js';
import { readManagedExtensionContributions } from '@qwen-code/qwen-code-core/managed-runtime/managed-extension-contributions.js';
import {
parseHostedChildExtensionAgent,
hostedChildExtensionApplyCommand,
type HostedChildExtensionAgent,
} from './hosted-extension-agent.js';
import {
HostedExtensionSession,
HostedExtensionRecoveryRequired,
HostedExtensionRevisionConflict,
type HostedExtensionPackage,
} from './hosted-extension-session.js';
import { createHash, randomUUID } from 'node:crypto';
import { isDeepStrictEqual } from 'node:util';
import type { Part } from '@google/genai';
@@ -377,6 +398,9 @@ const RESTORE_CONTAINER_KINDS = new Set([
'managed-definition',
'managed-session_metadata',
'managed-config_install',
'managed-extension-config-bundle',
'managed-extension-contributions',
'managed-extension-root',
'managed-file_history',
'managed-tool-outcome',
'managed-tool-result-manifest',
@@ -490,6 +514,102 @@ function describeManagedError(cause: unknown): string {
* Lifts a Session's recovery block. The named reason belongs to the block,
* so it is cleared with it here rather than by hand at each lift site.
*/
function selectExtensionCatalog(
session: HostedSession,
bundle: ManagedExtensionEffectiveBundle,
packages: readonly HostedExtensionPackage[],
): void {
session.baseSkills ??= session.skills ?? new HostedSkillCatalog([]);
session.skills = extensionSkillCatalog(session.baseSkills, packages);
session.extensionBundle = bundle;
session.extensionPackages = packages;
session.extensionContext = packages
.flatMap((entry) =>
entry.contributions.context.map(
(context) =>
`<extension_context source="extension://${entry.extensionId}/${context.path}">\n${extensionInstructionText(context.body, entry.extensionId)}\n</extension_context>`,
),
)
.join('\n\n');
}
function extensionSkillCatalog(
base: HostedSkillCatalog | undefined,
packages: readonly HostedExtensionPackage[],
): HostedSkillCatalog {
const agents = packages.flatMap((entry) =>
entry.contributions.agents.map((agent) => agent.name),
);
if (new Set(agents).size !== agents.length)
throw new Error('Extension agent names conflict.');
return new HostedSkillCatalog(
[
...(base?.entries() ?? []),
...packages.flatMap((entry) =>
entry.contributions.skills.map((skill) => ({
name: skill.name,
description: skill.description,
body: extensionInstructionText(skill.body, entry.extensionId),
model_invocable: skill.model_invocable,
user_invocable: skill.user_invocable,
asset_directory: `extension://${entry.extensionId}/${skill.assetDirectory.replace(/\/$/u, '')}/`,
})),
),
],
packages.flatMap((entry) =>
entry.contributions.commands.map((command) => ({
...command,
extensionId: entry.extensionId,
})),
),
);
}
function extensionAgentsOf(session: HostedSession) {
const bundleRef = session.extensions?.activeBundleRef;
if (!bundleRef || !session.extensionBundle) return [];
return (session.extensionPackages ?? []).flatMap((entry) => {
const member = session.extensionBundle!.packages.find(
(member) => member.pin.extensionId === entry.extensionId,
)!;
return entry.contributions.agents.map((agent) => ({
name: agent.name,
...(agent.model ? { model: agent.model } : {}),
...(agent.tools ? { tools: agent.tools } : {}),
...(agent.disallowedTools
? { disallowedTools: agent.disallowedTools }
: {}),
descriptor: {
configRevision: session.extensionBundle!.configRevision,
bundleRef,
pin: member.pin,
contributionsRef: member.contributionsRef,
agentName: agent.name,
toolNames: [],
},
}));
});
}
async function extensionBootstrapIncomplete(
session: HostedSession,
manager: HostedExtensionSession,
): Promise<boolean> {
if (!session.initialSelection && !session.publicChildExtensionAgent)
return false;
const active = await manager.active();
return (
!manager.activeBundleRef ||
!active ||
active.configRevision < 1 ||
(!!session.publicChildExtensionAgent &&
!isDeepStrictEqual(
active.packages.map((entry) => entry.pin),
session.extensionAgentPins,
))
);
}
function liftRecoveryBlock(session: HostedSession): void {
session.blocked = false;
session.recoveryReason = undefined;
@@ -533,6 +653,17 @@ interface HostedSession {
agentDefinition?: HostedAgentDefinition;
sessionSkillsRef?: ManagedSessionDurableRef;
skills?: HostedSkillCatalog;
baseSkills?: HostedSkillCatalog;
extensions?: HostedExtensionSession;
extensionBundle?: ManagedExtensionEffectiveBundle;
extensionPackages?: readonly HostedExtensionPackage[];
extensionContext?: string;
publicChildExtensionAgent?: HostedChildExtensionAgent;
initialSelection?: ManagedExtensionApplyCommand;
extensionBootstrapPending?: boolean;
extensionAgentPins?: ManagedExtensionApplyCommand['extensions'];
extensionAgentTools?: readonly string[];
extensionAgentPrompt?: string;
clientId: string;
cwd: string;
streams: Set<() => void>;
@@ -1127,6 +1258,7 @@ function childAgentsOf(session: HostedSession) {
promptId: string,
model: string,
route: PublicChildModelRoute,
extensionAgent?: PublicChildExtensionAgent,
) => {
const selection = parseHostedApprovalSelection(
await session.stores!.approvalSelection(),
@@ -1150,7 +1282,8 @@ function childAgentsOf(session: HostedSession) {
promptId,
);
return {
version: 1 as const,
version: extensionAgent ? (2 as const) : (1 as const),
...(extensionAgent ? { extensionAgent } : {}),
sourcePromptId: promptId,
approvalMode:
selection.mode === 'auto' ? ('default' as const) : selection.mode,
@@ -3688,6 +3821,14 @@ async function executeHostedTurn(
mcp: session.mcp,
hooks: session.hooks,
skills: session.skills,
extensionConfig: session.extensionBundle
? {
revision: session.extensionBundle.configRevision,
packages: session.extensionBundle.packages,
}
: undefined,
extensionAgents: extensionAgentsOf(session),
allowedToolNames: session.extensionAgentTools,
profile: session.toolProfile,
foregroundOnly: session.foregroundOnly,
context: workspaceContext,
@@ -3817,6 +3958,7 @@ async function executeHostedTurn(
session.managed,
promptId,
),
extensionInstructions: session.extensionContext,
agentInstructions: await readHostedAgentInstructions(
session.agentDefinition,
session.managed.resources,
@@ -4453,7 +4595,24 @@ export function registerHostedHarnessSessionRoutes(
let captureBytes = body?.['captureBytes'];
const publicChildAgentsVersion = body?.['publicChildAgentsVersion'] ?? 0;
let publicChildModelRoute: PublicChildModelRoute | undefined;
let publicChildExtensionAgent: HostedChildExtensionAgent | undefined;
let initialSelection: ManagedExtensionApplyCommand | undefined;
try {
if (body?.['initialSelection'] !== undefined) {
initialSelection = parseManagedExtensionApplyCommand(
body['initialSelection'],
);
if (
initialSelection.expectedRevision !== 0 ||
initialSelection.mcpChange
)
throw new Error('Invalid initial extension selection.');
}
publicChildExtensionAgent = parseHostedChildExtensionAgent(
body?.['publicChildExtensionAgent'],
);
if (publicChildExtensionAgent && initialSelection)
throw new Error('Child and creation extension bindings conflict.');
publicChildModelRoute =
body?.['publicChildModelRoute'] === undefined
? undefined
@@ -4463,6 +4622,11 @@ export function registerHostedHarnessSessionRoutes(
(publicChildAgentsVersion !== 1 || (!body?.['lineage'] && create))
)
throw new Error('A model route belongs only to a public child.');
if (
publicChildExtensionAgent &&
(!publicChildModelRoute || (create && !body?.['lineage']))
)
throw new Error('An extension agent belongs only to a public child.');
} catch {
return error(res, 400, 'invalid_public_child_model_route');
}
@@ -4470,13 +4634,17 @@ export function registerHostedHarnessSessionRoutes(
error(res, 400, 'invalid_public_child_agents_version');
return;
}
const extensionChildProfile =
contract.features?.includes('session-extension-packages/1') === true &&
contract.lifecycleV2Profiles?.includes('hosted-workspace-shell/1');
if (
publicChildAgentsVersion === 1 &&
(toolProfile !== 'hosted-workspace-shell/1' ||
body?.['foregroundOnly'] === true ||
body?.['mcpServers'] !== undefined ||
body?.['hookCatalog'] !== undefined ||
agentDefinition !== undefined ||
(!extensionChildProfile &&
(body?.['mcpServers'] !== undefined ||
body?.['hookCatalog'] !== undefined ||
agentDefinition !== undefined)) ||
body?.['childWorkspaces'] === true ||
body?.['childSnapshots'] === true)
) {
@@ -5013,6 +5181,19 @@ export function registerHostedHarnessSessionRoutes(
error(res, 409, 'hosted_tool_profile_conflict');
return;
}
if (
!isDeepStrictEqual(
resident.publicChildExtensionAgent,
publicChildExtensionAgent,
)
) {
error(res, 409, 'hosted_tool_profile_conflict');
return;
}
if (!isDeepStrictEqual(resident.initialSelection, initialSelection)) {
error(res, 409, 'hosted_tool_profile_conflict');
return;
}
if (resident.childAgents) {
await resident.childAgents.serialized(async () => {
resident.publicChildLaunchEnabled =
@@ -5565,6 +5746,10 @@ export function registerHostedHarnessSessionRoutes(
? { publicChildAgentsVersion }
: {}),
...(publicChildModelRoute ? { publicChildModelRoute } : {}),
...(publicChildExtensionAgent
? { publicChildExtensionAgent }
: {}),
...(initialSelection ? { initialSelection } : {}),
...(sessionSkillsRef ? { sessionSkillsRef } : {}),
...(toolProfile ? { toolProfile } : {}),
...(foregroundOnly ? { foregroundOnly: true } : {}),
@@ -5684,6 +5869,17 @@ export function registerHostedHarnessSessionRoutes(
)
)
throw new Error('Public child model route changed.');
if (
!isDeepStrictEqual(
definition?.['publicChildExtensionAgent'],
publicChildExtensionAgent,
)
)
throw new Error('Public child extension binding changed.');
if (
!isDeepStrictEqual(definition?.['initialSelection'], initialSelection)
)
throw new Error('Initial extension selection changed.');
if (
(definition?.['publicChildAgentsVersion'] ?? 0) !==
publicChildAgentsVersion
@@ -5776,6 +5972,8 @@ export function registerHostedHarnessSessionRoutes(
? { publicChildAgentsVersion: 1 as const }
: {}),
...(publicChildModelRoute ? { publicChildModelRoute } : {}),
...(publicChildExtensionAgent ? { publicChildExtensionAgent } : {}),
...(initialSelection ? { initialSelection } : {}),
publicChildLaunchEnabled: body?.['suppressChildAgents'] !== true,
...(savedSessionSkillsRef
? { sessionSkillsRef: savedSessionSkillsRef }
@@ -5851,7 +6049,20 @@ export function registerHostedHarnessSessionRoutes(
error(res, 409, 'hosted_tool_profile_conflict');
return;
}
const mcpSettings = await readHostedMcpSettings(managed);
const configurationHead =
managed.authority.domainRecord('config_install');
const hasExtensionConfiguration =
configurationHead &&
isManagedExtensionConfig(
JSON.parse(
(
await managed.resources.read(configurationHead.recordRef)
).toString('utf8'),
),
);
const mcpSettings = hasExtensionConfiguration
? []
: await readHostedMcpSettings(managed);
validateHostedMcpHistory(mcpServers ?? [], mcpSettings);
if (
mcpSettings.length &&
@@ -5900,21 +6111,109 @@ export function registerHostedHarnessSessionRoutes(
sendAttachment(res, sessionId, session);
return;
}
if ((mcpServers || mcpSettings.length) && brokerOptions)
const extensionManagement =
hasExtensionConfiguration ||
publicChildExtensionAgent ||
initialSelection
? await extensionsManager(session, !!publicChildExtensionAgent)
: undefined;
if (initialSelection && extensionManagement && !passiveRecovery)
await extensionManagement.bootstrap(initialSelection);
if (publicChildExtensionAgent && extensionManagement) {
const source = parseManagedExtensionEffectiveBundle(
JSON.parse(
(
await managed.resources.read(
publicChildExtensionAgent.effectiveConfigRef,
)
).toString('utf8'),
),
);
const descriptor = publicChildExtensionAgent.extensionAgent;
const member = source.packages.find(
(entry) =>
isDeepStrictEqual(entry.pin, descriptor.pin) &&
isDeepStrictEqual(
entry.contributionsRef,
descriptor.contributionsRef,
),
);
if (!member || source.configRevision !== descriptor.configRevision)
throw new Error('Child extension source membership changed.');
const contributions = await readManagedExtensionContributions(
managed.resources,
member.contributionsRef,
);
const agent = contributions.agents.find(
(entry) => entry.name === descriptor.agentName,
);
if (
!agent ||
descriptor.toolNames.some(
(name) =>
(agent.tools && !agent.tools.includes(name)) ||
agent.disallowedTools?.includes(name),
)
)
throw new Error('Child extension descriptor changed.');
session.extensionAgentTools = descriptor.toolNames;
session.extensionAgentPrompt = extensionInstructionText(
agent.systemPrompt,
descriptor.pin.extensionId,
);
session.extensionAgentPins = source.packages.map((entry) => entry.pin);
if (!passiveRecovery)
await extensionManagement.bootstrap(
hostedChildExtensionApplyCommand(
sessionId,
publicChildExtensionAgent.effectiveConfigRef.digest,
session.extensionAgentPins,
),
);
}
const extensionBundle = await extensionManagement?.active();
if (
extensionManagement &&
(await extensionBootstrapIncomplete(session, extensionManagement))
) {
if (!passiveRecovery) throw new HostedExtensionRecoveryRequired();
session.extensionBootstrapPending = true;
session.blocked = true;
}
const effectiveMcpServers = extensionBundle?.mcpServers ?? mcpServers;
const effectiveHookCatalog = extensionBundle
? extensionBundle.hookCatalog
: hookCatalog;
if (
(effectiveMcpServers?.length || mcpSettings.length) &&
brokerOptions &&
!session.mcp
)
session.mcp = new HostedMcpSession(
{ ...brokerOptions, lifecycleAuthority: () => session.lifecycle },
managed,
mcpServers ?? [],
effectiveMcpServers ?? [],
mcpSettings.at(-1),
undefined,
extensionBundle !== undefined,
);
if (hookCatalog && brokerOptions)
if (effectiveHookCatalog && brokerOptions && !session.hooks)
session.hooks = new HostedHookSession(
{ ...brokerOptions, lifecycleAuthority: () => session.lifecycle },
managed,
hookCatalog,
effectiveHookCatalog,
session.mcp?.broker,
() => session.lifecycleProtocolVersion === 2,
);
if (extensionBundle && extensionManagement)
selectExtensionCatalog(
session,
extensionBundle,
await extensionManagement.packages(extensionBundle),
);
if (session.extensionAgentPrompt)
session.extensionContext = `${session.extensionContext ?? ''}\n\n${session.extensionAgentPrompt}`;
if (extensionManagement?.pending) session.blocked = true;
if (!foregroundOnly && session.toolProfile && brokerOptions) {
session.childRuns = new HostedChildRunSession(
{
@@ -6539,6 +6838,11 @@ export function registerHostedHarnessSessionRoutes(
session.monitorWake?.kick();
}
if (pinned) session.approval = pinned;
if (session.extensionBootstrapPending) {
sessions.set(sessionId, session);
sendAttachment(res, sessionId, session);
return;
}
// A takeover recovers exactly the parked Turn, including the file
// history it left pending; only refuse a stranger's pending state.
// A bare load of a parked Session keeps refusing with 409 so it never
@@ -6773,8 +7077,7 @@ export function registerHostedHarnessSessionRoutes(
takeover &&
unsettled &&
session.toolProfile &&
!session.hooks &&
!session.mcp &&
((!session.hooks && !session.mcp) || session.extensions?.enabled) &&
brokerOptions
) {
const verdict = await managed.authority.harnessRunAuthorization();
@@ -7466,8 +7769,26 @@ export function registerHostedHarnessSessionRoutes(
if (!identity(req, sessions, legacyClose))
return error(res, 404, 'hosted_session_not_found');
try {
const extensionBody = object(req.body);
const extensionCommand =
object(extensionBody?.['command']) ?? extensionBody;
const extensionOperation =
req.method === 'POST' &&
(req.path === '/extensions/management' ||
(req.path === '/mcp/management' && session.extensions?.enabled))
? extensionCommand?.['operationId']
: req.method === 'POST'
? /^\/extensions\/management\/operations\/([^/]+)\/(?:continue|recover-previous)$/u.exec(
req.path,
)?.[1]
: undefined;
await session.stores!.authorizeOrdinary(
legacyClose ? 'legacy-close' : undefined,
legacyClose
? 'legacy-close'
: typeof extensionOperation === 'string' &&
HOSTED_UUID.test(extensionOperation)
? `configuration:${extensionOperation}`
: undefined,
);
if (session.lifecycle)
return error(res, 409, 'hosted_lifecycle_operation_active');
@@ -8516,6 +8837,8 @@ export function registerHostedHarnessSessionRoutes(
const session = identity(req, sessions);
if (!session) return error(res, 404, 'hosted_session_not_found');
if (!session.hooks) return error(res, 409, 'hosted_hooks_unavailable');
if (session.extensions?.enabled)
return error(res, 409, 'hosted_extension_configuration_required');
if (
session.active ||
session.hooksBusy ||
@@ -8574,12 +8897,295 @@ export function registerHostedHarnessSessionRoutes(
);
};
async function extensionsManager(
session: HostedSession,
childBootstrap = false,
): Promise<HostedExtensionSession> {
if (
!brokerOptions ||
session.foregroundOnly ||
((session.childDepth ?? 0) !== 0 &&
!(childBootstrap && session.publicChildExtensionAgent)) ||
!['hosted-workspace-shell/1', HOSTED_MCP_PROFILE].includes(
session.toolProfile ?? '',
) ||
!contract.lifecycleV2Profiles?.includes(session.toolProfile!)
)
throw new Error('Hosted extension management is unavailable.');
if (session.extensions) return session.extensions;
const head = session.managed.authority.domainRecord('config_install');
const unified =
head &&
isManagedExtensionConfig(
JSON.parse(
(await session.managed.resources.read(head.recordRef)).toString(
'utf8',
),
),
);
const legacy = unified ? [] : await readHostedMcpSettings(session.managed);
const baseMcp = session.definition?.['mcpServers'];
const options = {
...brokerOptions,
lifecycleAuthority: () => session.lifecycle,
};
const manager = new HostedExtensionSession(
session.managed,
options,
{
mcpRevision: legacy.at(-1)?.revision ?? 0,
mcpServers:
legacy.at(-1)?.servers ??
(baseMcp === undefined
? []
: parseHostedMcpServers(baseMcp).map((pin) => ({
...pin,
state: 'enabled' as const,
}))),
hookCatalog:
session.hooks?.selectedPin ??
(session.definition?.['hookCatalog'] === undefined
? null
: parseHostedHookPin(session.definition['hookCatalog'])),
},
{
validate: (packages) => {
extensionSkillCatalog(session.baseSkills ?? session.skills, packages);
if (
(session.childDepth ?? 0) === 0 &&
session.publicChildAgentsVersion !== 1 &&
packages.some((entry) => entry.contributions.agents.length > 0)
)
throw new Error(
'Extension agents require the public child Session profile.',
);
},
drain: async (bundle, forceMcpServerId) => {
if (
session.hooks &&
!isDeepStrictEqual(session.hooks.selectedPin, bundle.hookCatalog)
) {
await session.hooks.close();
session.hooks = undefined;
}
if (bundle.mcpServers.length || session.hooks)
await session.mcp?.drainForExtensionConfiguration(
bundle.mcpServers,
forceMcpServerId,
);
else if (session.mcp) {
await session.mcp.close();
session.mcp = undefined;
}
},
enable: async (bundle, packages, operationId) => {
const ownerId = `extensions-components-${createHash('sha256')
.update(
JSON.stringify([
session.managed.authority.sessionHeader.sessionKey,
bundle.configRevision,
]),
)
.digest('hex')}`;
if (bundle.mcpServers.length && !session.mcp) {
const broker =
session.hooks?.broker ??
new HostedWorkspaceBroker(
options,
session.managed.authority.sessionHeader.sessionKey,
ownerId,
);
session.mcp = new HostedMcpSession(
options,
session.managed,
bundle.mcpServers,
undefined,
broker,
true,
);
if (session.hooks?.broker === broker)
session.hooks.delegateBrokerToMcp();
}
if (bundle.hookCatalog && !session.hooks) {
session.hooks = new HostedHookSession(
options,
session.managed,
bundle.hookCatalog,
session.mcp?.broker,
() => session.lifecycleProtocolVersion === 2,
ownerId,
);
}
if (
bundle.hookCatalog &&
session.hooks &&
(!isDeepStrictEqual(
session.hooks.selectedPin,
bundle.hookCatalog,
) ||
session.managed.authority.extensionRecordsInDomain(
'hook_registration',
).length === 0)
)
await session.hooks.configure(
`extension-catalog-${operationId}-${bundle.configRevision}`,
bundle.hookCatalog,
session.managed.authority.extensionRecordsInDomain(
'hook_registration',
).length,
);
await session.mcp?.enableExtensionConfiguration(
bundle.mcpServers,
operationId,
);
await session.hooks?.ensureReady();
selectExtensionCatalog(session, bundle, packages);
},
},
);
await manager.open();
session.extensions = manager;
return manager;
}
app.get('/session/:id/extensions/management', (req, res) => {
const session = identity(req, sessions);
if (!session) return error(res, 404, 'hosted_session_not_found');
void extensionsManager(session, !!session.publicChildExtensionAgent)
.then(async (manager) => res.json(await manager.snapshot()))
.catch(() => error(res, 409, 'hosted_extension_management_unavailable'));
});
app.get(
'/session/:id/extensions/management/operations/:operationId',
(req, res) => {
const session = identity(req, sessions);
if (!session) return error(res, 404, 'hosted_session_not_found');
void extensionsManager(session, !!session.publicChildExtensionAgent)
.then(async (manager) =>
res.json(await manager.snapshot(req.params['operationId'])),
)
.catch(() => error(res, 404, 'hosted_extension_operation_not_found'));
},
);
for (const action of ['apply', 'continue', 'recover-previous'] as const) {
const path =
action === 'apply'
? '/session/:id/extensions/management'
: `/session/:id/extensions/management/operations/:operationId/${action}`;
app.post(path, (req, res) => {
const session = identity(req, sessions);
if (!session) return error(res, 404, 'hosted_session_not_found');
if (
session.active ||
session.hooksBusy ||
session.mcpBusy ||
session.mcpRecovering ||
session.mcpClosing ||
session.lifecycle ||
session.recoveryCleanupOnly ||
(action === 'apply' &&
session.blocked &&
!session.extensionBootstrapPending)
)
return error(res, 409, 'hosted_turn_active');
let operationId: string;
let command:
| ReturnType<typeof parseManagedExtensionApplyCommand>
| undefined;
try {
if (action === 'apply') {
command = parseManagedExtensionApplyCommand(
object(req.body)?.['command'] ?? req.body,
);
operationId = command.operationId;
} else {
operationId = req.params['operationId'];
if (!HOSTED_UUID.test(operationId))
throw new Error('Invalid operation.');
}
} catch {
return error(res, 400, 'invalid_extension_command');
}
session.mcpBusy = true;
void (async () => {
const manager = await extensionsManager(
session,
!!session.publicChildExtensionAgent,
);
try {
if (command && session.publicChildExtensionAgent) {
const binding = session.publicChildExtensionAgent;
const source = parseManagedExtensionEffectiveBundle(
JSON.parse(
(
await session.managed.resources.read(
binding.effectiveConfigRef,
)
).toString('utf8'),
),
);
if (
!isDeepStrictEqual(
command,
hostedChildExtensionApplyCommand(
session.managed.authority.sessionHeader.sessionKey.sessionId,
binding.effectiveConfigRef.digest,
source.packages.map((entry) => entry.pin),
),
)
)
throw new HostedMcpConflictError(
'Child extension configuration is immutable.',
);
}
if (command) await manager.submit(command);
else if (action === 'continue') await manager.continue(operationId);
else await manager.recoverPrevious(operationId);
session.extensionBootstrapPending =
await extensionBootstrapIncomplete(session, manager);
session.blocked =
manager.pending || session.extensionBootstrapPending;
res.status(202).json(await manager.snapshot(operationId));
} catch (cause) {
session.extensionBootstrapPending =
await extensionBootstrapIncomplete(session, manager);
session.blocked =
manager.pending || session.extensionBootstrapPending;
if (cause instanceof HostedExtensionRevisionConflict)
error(res, 409, 'extension_config_revision_conflict');
else if (
cause instanceof HostedMcpConflictError ||
cause instanceof ManagedSessionConflictError ||
cause instanceof ManagedSessionCommitRejectedError
)
error(res, 409, 'hosted_extension_configuration_conflict');
else if (manager.enabled)
res.status(202).json(await manager.snapshot(operationId));
else error(res, 503, 'hosted_extension_recovery_required');
}
})()
.catch(() => {
if (!res.headersSent)
error(res, 503, 'hosted_extension_recovery_required');
})
.finally(() => {
session.mcpBusy = false;
});
});
}
app.get('/session/:id/mcp/management', (req, res) => {
const session = identity(req, sessions);
if (!session) return error(res, 404, 'hosted_session_not_found');
if (session.recoveryCleanupOnly)
return error(res, 409, 'hosted_mcp_session_load_required');
void (async () => {
if (session.extensions?.enabled) {
res.json(await session.extensions.mcpSnapshot());
return;
}
const manager = mcpManager(session);
await manager.open();
res.json(await manager.snapshot());
@@ -8592,6 +9198,15 @@ export function registerHostedHarnessSessionRoutes(
if (session.recoveryCleanupOnly)
return error(res, 409, 'hosted_mcp_session_load_required');
void (async () => {
if (session.extensions?.enabled) {
res.json(
await session.extensions.mcpSnapshot(
req.params['operationId'],
false,
),
);
return;
}
const manager = mcpManager(session);
await manager.open();
if (!manager.status(req.params['operationId']))
@@ -8617,16 +9232,26 @@ export function registerHostedHarnessSessionRoutes(
)
return error(res, 409, 'hosted_turn_active');
let command;
let manager: HostedMcpManager;
let manager: HostedMcpManager | undefined;
try {
command = parseHostedMcpCommand(req.body);
manager = mcpManager(session);
if (!session.extensions?.enabled) manager = mcpManager(session);
} catch {
return error(res, 400, 'invalid_mcp_command');
}
session.mcpBusy = true;
void (async () => {
try {
if (session.extensions?.enabled) {
await session.extensions.submitMcp(command);
res
.status(202)
.json(
await session.extensions.mcpSnapshot(command.operationId, false),
);
return;
}
if (!manager) throw new Error('MCP manager is unavailable.');
await manager.open();
await manager.submit(command);
res
@@ -8639,13 +9264,21 @@ export function registerHostedHarnessSessionRoutes(
cause instanceof ManagedSessionCommitRejectedError
)
error(res, 409, 'hosted_mcp_configuration_conflict');
else if (manager.status(command.operationId))
else if (session.extensions?.enabled)
res
.status(202)
.json(
await session.extensions.mcpSnapshot(command.operationId, false),
);
else if (manager?.status(command.operationId))
res
.status(202)
.json(await manager.snapshot(false, command.operationId));
else error(res, 503, 'hosted_mcp_recovery_required');
} finally {
session.mcp = manager.mcp;
if (manager) session.mcp = manager.mcp;
if (session.extensions?.enabled)
session.blocked = session.extensions.pending;
if (session.mcp && session.hooks?.broker === session.mcp.broker)
session.hooks.delegateBrokerToMcp();
session.mcpBusy = false;
@@ -10245,6 +10878,14 @@ export function registerHostedHarnessSessionRoutes(
{
mcp: session.mcp,
skills: session.skills,
extensionConfig: session.extensionBundle
? {
revision: session.extensionBundle.configRevision,
packages: session.extensionBundle.packages,
}
: undefined,
extensionAgents: extensionAgentsOf(session),
allowedToolNames: session.extensionAgentTools,
profile: session.toolProfile,
foregroundOnly: session.foregroundOnly,
context: workspaceContext,
@@ -10525,6 +11166,7 @@ export function registerHostedHarnessSessionRoutes(
session.managed,
promptId,
),
extensionInstructions: session.extensionContext,
agentInstructions: await readHostedAgentInstructions(
session.agentDefinition,
session.managed.resources,
+19 -3
View File
@@ -395,6 +395,7 @@ export class HostedHookSession {
private readonly initialPin: ManagedHookCatalogPin,
broker?: HostedWorkspaceBroker,
private readonly lifecycleV2: () => boolean = () => false,
runtimeSessionId?: string,
) {
this.ownsBroker = broker === undefined;
const previous = lifecycleV2()
@@ -405,9 +406,10 @@ export class HostedHookSession {
new HostedWorkspaceBroker(
options,
this.key,
previous
? parseHookExecution(previous.record).runtimeSessionId
: `hooks-activation-${digest([session.activation.activationId, session.activation.epoch])}`,
runtimeSessionId ??
(previous
? parseHookExecution(previous.record).runtimeSessionId
: `hooks-activation-${digest([session.activation.activationId, session.activation.epoch])}`),
);
}
@@ -442,6 +444,20 @@ export class HostedHookSession {
);
}
get selectedPin(): ManagedHookCatalogPin {
const latest = this.session.authority
.extensionRecordsInDomain('hook_registration')
.filter((entry) => entry.run.state === 'settled')
.at(-1);
if (!latest) return this.initialPin;
const registration = parseHookRegistration(latest.record);
return {
catalogId: registration.catalogId,
catalogRevision: registration.catalogRevision,
definitionDigest: registration.run.definition!.definitionDigest,
};
}
async acquire(): Promise<void> {
if (this.acquired) {
// A release absorbed on an earlier turn is retried here, so a fenced
@@ -340,6 +340,63 @@ it('queries a lost configure reply by its original identity instead of reconnect
});
});
it('keeps an unchanged independent MCP connection through package configuration changes', async () => {
await mcp.ensureReady();
const before = mcp.tools();
const configuration =
session.authority.extensionRecordsInDomain('mcp_configuration')[0]!;
const token = parseMcpConfiguration(configuration.record);
const requestCount = requests.length;
await mcp.drainForExtensionConfiguration([pin]);
await mcp.enableExtensionConfiguration([pin], randomUUID());
expect(requests).toHaveLength(requestCount);
expect(mcp.tools()).toEqual(before);
expect(
session.authority.extensionRecordsInDomain('mcp_configuration'),
).toHaveLength(1);
expect(
parseMcpConfiguration(
session.authority.extensionRecord(
'mcp_configuration',
token.configurationId,
)!.record,
),
).toMatchObject({
configurationId: token.configurationId,
configRevision: token.configRevision,
connectionGeneration: token.connectionGeneration,
releaseState: 'active',
});
});
it('drains only the MCP server changed by an extension configuration', async () => {
const other = { ...pin, serverId: 'other' };
mcp = new HostedMcpSession(
{ baseUrl: 'http://127.0.0.1:1', token: 'test' },
session,
[pin, other],
);
await mcp.ensureReady();
const preserved = session.authority
.extensionRecordsInDomain('mcp_configuration')
.map((entry) => parseMcpConfiguration(entry.record))
.find((entry) => entry.serverId === 'other')!;
const next = { ...pin, serverRevision: 2, definitionDigest: 'b'.repeat(64) };
await mcp.drainForExtensionConfiguration([next, other]);
await mcp.enableExtensionConfiguration([next, other], randomUUID());
expect(
requests
.filter((request) => request.kind === 'mcp-release')
.map((request) => ('serverId' in request ? request.serverId : undefined)),
).toEqual(['demo']);
expect(
session.authority
.extensionRecordsInDomain('mcp_configuration')
.map((entry) => parseMcpConfiguration(entry.record))
.filter((entry) => entry.serverId === 'other'),
).toEqual([preserved]);
});
async function interruptBeforeDispatch(operationId: string) {
await mcp.ensureReady();
const original = session.authority.commitExtensionRecord.bind(
+106 -2
View File
@@ -121,6 +121,7 @@ export class HostedMcpSession {
string,
{ configuration: McpConfiguration; catalog: ManagedMcpCatalog }
>();
private extensionServers?: readonly HostedMcpServerPin[];
constructor(
private readonly options: HostedWorkspaceBrokerOptions,
@@ -128,8 +129,10 @@ export class HostedMcpSession {
private readonly admissionServers: readonly HostedMcpServerPin[],
settings?: HostedMcpSettings,
sharedBroker?: HostedWorkspaceBroker,
extensionView = false,
) {
this.settings = settings;
if (extensionView) this.extensionServers = admissionServers;
const configurations = this.configurations();
const owners = new Set(
configurations
@@ -163,6 +166,7 @@ export class HostedMcpSession {
}
get servers(): readonly HostedMcpServerPin[] {
if (this.extensionServers) return this.extensionServers;
return [
...new Map(
[...this.admissionServers, ...(this.settings?.servers ?? [])].map(
@@ -173,6 +177,11 @@ export class HostedMcpSession {
}
get selectedServers(): readonly HostedMcpSelection[] {
if (this.extensionServers)
return this.extensionServers.map((pin) => ({
...pin,
state: 'enabled' as const,
}));
return (
this.settings?.servers ??
this.admissionServers.map((pin) => ({
@@ -182,6 +191,10 @@ export class HostedMcpSession {
);
}
get managementRevision(): number {
return this.settings?.revision ?? 0;
}
private enabled(serverId: string): boolean {
return this.selectedServers.some(
(pin) => pin.serverId === serverId && pin.state === 'enabled',
@@ -260,13 +273,104 @@ export class HostedMcpSession {
signal?.throwIfAborted();
if (this.configuring.size)
throw new HostedMcpConflictError('MCP configuration is in progress.');
this.ready ??= this.initialize(signal).catch((cause: unknown) => {
this.ready ??= (
this.extensionServers
? this.installExtensionServers(
this.extensionServers,
`extension-load-${digest(this.extensionServers).slice(0, 24)}`,
signal,
)
: this.initialize(signal)
).catch((cause: unknown) => {
this.ready = undefined;
throw cause;
});
await waitForTurn(this.ready, signal);
}
async drainForExtensionConfiguration(
servers: readonly HostedMcpServerPin[],
forceServerId?: string,
): Promise<void> {
await this.reconcileEffects();
const changed = this.configurations().filter(
(entry) =>
entry.releaseState !== 'released' &&
(entry.serverId === forceServerId ||
!servers.some(
(pin) =>
pin.serverId === entry.serverId &&
pin.serverRevision === entry.serverRevision &&
pin.definitionDigest === entry.run.definition?.definitionDigest,
)),
);
await this.drainConfigurations(changed);
for (const entry of changed) {
const latest = this.configurations().find(
(value) => value.configurationId === entry.configurationId,
)!;
if (latest.releaseState !== 'released')
await this.commitConfiguration({ ...latest, releaseState: 'released' });
this.catalogs.delete(entry.serverId);
for (const [key, binding] of this.bindings)
if (binding.configuration.serverId === entry.serverId)
this.bindings.delete(key);
}
this.extensionServers = servers;
this.settings = undefined;
}
async enableExtensionConfiguration(
servers: readonly HostedMcpServerPin[],
operationId: string,
): Promise<void> {
this.extensionServers = servers;
this.settings = undefined;
await this.installExtensionServers(servers, operationId);
this.ready = Promise.resolve();
}
private async installExtensionServers(
servers: readonly HostedMcpServerPin[],
operationId: string,
signal?: AbortSignal,
): Promise<void> {
for (const pin of servers) {
const matching = this.configurations()
.filter(
(entry) =>
entry.serverId === pin.serverId &&
entry.serverRevision === pin.serverRevision &&
entry.run.definition?.definitionDigest === pin.definitionDigest &&
entry.releaseState === 'active' &&
entry.run.state === 'settled',
)
.at(-1);
if (matching) {
if (
this.catalogs.get(pin.serverId)?.configuration.configurationId !==
matching.configurationId
)
await this.install(pin, matching, undefined, undefined, signal);
continue;
}
const id = `extension-mcp-${digest([operationId, pin.serverId]).slice(0, 48)}`;
const previous = this.session.authority.extensionRecord(
'mcp_configuration',
id,
);
const revision = previous
? parseMcpConfiguration(previous.record).configRevision - 1
: Math.max(
0,
...this.configurations()
.filter((entry) => entry.serverId === pin.serverId)
.map((entry) => entry.configRevision),
);
await this.configure(id, pin, revision, signal);
}
}
async refresh(signal?: AbortSignal): Promise<void> {
this.refreshing++;
try {
@@ -664,7 +768,7 @@ export class HostedMcpSession {
for (const { catalog, configuration } of this.bindings.values()) {
if (
!this.enabled(catalog.serverId) ||
(this.settings &&
((this.settings || this.extensionServers !== undefined) &&
parseMcpConfiguration(
this.session.authority.extensionRecord(
'mcp_configuration',
@@ -5,6 +5,8 @@
*/
import { randomUUID } from 'node:crypto';
import { parseManagedConfigInstallRecord } from '@qwen-code/qwen-code-core/managed-runtime/managed-config-install.js';
import { expandHostedExtensionCommand } from './hosted-extension-command.js';
import { isDeepStrictEqual } from 'node:util';
import type { Part } from '@google/genai';
import type { ManagedSession } from '@qwen-code/qwen-code-core/managed-runtime/managed-session-assembly.js';
@@ -1196,6 +1198,50 @@ async function hostedSkillResponse(
args = updated;
}
const problem = catalog.validationError(args, origin);
const command = catalog.command(args);
if (!problem && command) {
const head = session.authority.domainRecord('config_install');
if (!head) throw new HostedHookRecoveryRequiredError();
const record = parseManagedConfigInstallRecord(
JSON.parse(
(await session.resources.read(head.recordRef)).toString('utf8'),
),
);
if (record.phase !== 'active') throw new HostedHookRecoveryRequiredError();
try {
const text = await expandHostedExtensionCommand({
session,
broker: {} as HostedWorkspaceBroker,
promptId,
callId,
revision: record.configRevision,
command,
args: String((args as Record<string, unknown>)['args'] ?? ''),
signal: new AbortController().signal,
definitionDigest: () => {
throw new HostedHookRecoveryRequiredError();
},
decide: async () => {
throw new HostedHookRecoveryRequiredError();
},
completed: async (id, _name, _input, result) => {
if (
hooks &&
!hooks.hasCompletedOccurrence(
result['executionStatus'] === 'success'
? HookEventName.PostToolUse
: HookEventName.PostToolUseFailure,
id,
)
)
throw new HostedHookRecoveryRequiredError();
},
});
return convertToFunctionResponse('skill', callId, [{ text }]);
} catch {
throw new HostedHookRecoveryRequiredError();
}
}
return problem
? convertToFunctionErrorResponse('skill', callId, [], problem)
: convertToFunctionResponse('skill', callId, [
+96 -12
View File
@@ -28,6 +28,16 @@ export interface HostedSkill {
readonly base_directory?: string;
readonly model_invocable?: boolean;
readonly user_invocable?: boolean;
readonly asset_directory?: string;
}
export interface HostedCommand {
readonly name: string;
readonly description: string;
readonly prompt: string;
readonly extensionId: string;
readonly modelInvocable: boolean;
readonly argumentHint?: string;
}
export function parseSessionSkillsRef(
@@ -96,6 +106,7 @@ export function parseHostedSkills(value: unknown): readonly HostedSkill[] {
'base_directory',
'model_invocable',
'user_invocable',
'asset_directory',
].includes(key),
) ||
!boundedText(skill['name'], 128) ||
@@ -111,6 +122,14 @@ export function parseHostedSkills(value: unknown): readonly HostedSkill[] {
if (names.has(skill['name'])) invalid();
names.add(skill['name']);
const base = skill['base_directory'];
const asset = skill['asset_directory'];
if (
asset !== undefined &&
(typeof asset !== 'string' ||
!/^extension:\/\/[A-Za-z0-9._:-]+\/(?:[^\\\s]*\/?)$/u.test(asset) ||
asset.split('/').some((part) => part === '..' || part === '.'))
)
invalid();
if (base !== undefined) {
if (
!boundedText(base, 4096) ||
@@ -132,6 +151,7 @@ export function parseHostedSkills(value: unknown): readonly HostedSkill[] {
name: skill['name'],
description: skill['description'],
body: skill['body'],
...(asset === undefined ? {} : { asset_directory: asset as string }),
...(base === undefined ? {} : { base_directory: base as string }),
...(skill['model_invocable'] === undefined
? {}
@@ -169,14 +189,56 @@ export async function readHostedSkills(
export class HostedSkillCatalog {
private readonly skills: readonly HostedSkill[];
constructor(skills: readonly HostedSkill[]) {
constructor(
skills: readonly HostedSkill[],
private readonly commands: readonly HostedCommand[] = [],
) {
this.skills = parseHostedSkills(skills);
const names = new Set(this.skills.map((skill) => skill.name));
for (const command of commands) {
validateSkillName(command.name);
if (
names.has(command.name) ||
!boundedText(command.description, 4096) ||
!boundedText(command.prompt, 512 * 1024)
)
invalid();
names.add(command.name);
}
}
entries(): readonly HostedSkill[] {
return this.skills;
}
command(args: unknown): HostedCommand | undefined {
if (!args || typeof args !== 'object') return undefined;
return this.commands.find(
(command) => command.name === (args as { skill?: unknown }).skill,
);
}
userArguments(name: string, prompt: string): { args?: string } {
return this.commands.some((command) => command.name === name)
? {
args: prompt
.trimStart()
.slice(name.length + 1)
.trimStart(),
}
: {};
}
instructions(): string | undefined {
const available = this.skills.filter(
(skill) => skill.model_invocable !== false,
);
const available = [
...this.skills.filter((skill) => skill.model_invocable !== false),
...this.commands
.filter((command) => command.modelInvocable)
.map((command) => ({
name: command.name,
description: command.description,
})),
];
if (!available.length) return undefined;
return `The following published skills are available. Use the skill tool to load their instructions when relevant.\n${renderAvailableSkillsBlock(available)}`;
}
@@ -188,21 +250,25 @@ export class HostedSkillCatalog {
const match = /^\s*\/([^\s/]+)(?=\s|$)/u.exec(remaining);
if (
!match ||
!this.skills.some(
(!this.skills.some(
(skill) => skill.name === match[1] && skill.user_invocable !== false,
)
) &&
!this.commands.some((command) => command.name === match[1]))
)
return [...names];
names.add(match[1]);
if (this.commands.some((command) => command.name === match[1]))
return [...names];
remaining = remaining.slice(match[0].length);
}
}
declaration(): FunctionDeclaration | undefined {
const available = this.skills.filter(
(skill) => skill.model_invocable !== false,
);
if (!available.length) return undefined;
const available = [
...this.skills.filter((skill) => skill.model_invocable !== false),
...this.commands.filter((command) => command.modelInvocable),
];
if (!available.length && !this.commands.length) return undefined;
return {
name: 'skill',
description:
@@ -214,6 +280,7 @@ export class HostedSkillCatalog {
type: 'string',
enum: available.map((skill) => skill.name),
},
...(this.commands.length ? { args: { type: 'string' } } : {}),
},
required: ['skill'],
additionalProperties: false,
@@ -228,8 +295,19 @@ export class HostedSkillCatalog {
if (!args || typeof args !== 'object' || Array.isArray(args))
return 'Hosted Skill requires only a skill name.';
const values = args as Record<string, unknown>;
if (Object.keys(values).length !== 1 || typeof values['skill'] !== 'string')
const command = this.command(args);
if (
(command
? Object.keys(values).some((key) => !['skill', 'args'].includes(key)) ||
(values['args'] !== undefined && typeof values['args'] !== 'string')
: Object.keys(values).length !== 1) ||
typeof values['skill'] !== 'string'
)
return 'Hosted Skill requires only a skill name; arguments and activation options are unsupported.';
if (command)
return origin === 'model' && !command.modelInvocable
? 'This command may only be invoked by the user.'
: undefined;
if (!this.skills.some((skill) => skill.name === values['skill']))
return `Skill ${JSON.stringify(values['skill'])} is not published for this Session.`;
const skill = this.skills.find((entry) => entry.name === values['skill'])!;
@@ -244,7 +322,13 @@ export class HostedSkillCatalog {
const error = this.validationError(args);
if (error) throw new Error(error);
const name = (args as { skill: string }).skill;
if (this.command(args))
throw new Error(
'Extension commands require the durable command expansion pipeline.',
);
const skill = this.skills.find((entry) => entry.name === name)!;
return `Base directory for this skill relative to the Session working directory: ${skill.base_directory ?? '.'}\nResolve asset paths relative to this directory with the Workspace tools.\n\n${skill.body}\n`;
return skill.asset_directory
? `Base directory for this skill: ${skill.asset_directory}\nResolve asset paths relative to this directory with the Workspace tools.\n\n${skill.body}\n`
: `Base directory for this skill relative to the Session working directory: ${skill.base_directory ?? '.'}\nResolve asset paths relative to this directory with the Workspace tools.\n\n${skill.body}\n`;
}
}
@@ -251,7 +251,15 @@ export function parseHostedActionOptions(bytes: Buffer): HostedActionOptions {
]) {
if (!Object.hasOwn(options, field)) continue;
const ref = assertManagedSessionDurableRef(options[field], field);
if (ref.kind !== kind || ref.schemaVersion !== 1)
if (
(ref.kind !== kind &&
!(
field === 'inputRef' &&
options['v'] === 3 &&
ref.kind === 'managed-extension-command-input'
)) ||
ref.schemaVersion !== 1
)
throw new Error('Invalid Hosted Action reference.');
}
return options as unknown as HostedActionOptions;
@@ -48,6 +48,11 @@ import type {
ManagedMonitorControl,
ManagedMonitorOperationView,
} from '@qwen-code/qwen-code-core/managed-runtime/managed-monitor-protocol.js';
import type { ManagedExtensionRuntimeControl } from '@qwen-code/qwen-code-core/managed-runtime/managed-extension-package.js';
import {
MANAGED_EXTENSION_LINUX_CAPABILITY_DIGEST,
MANAGED_EXTENSION_DEVELOPER_CAPABILITY_DIGEST,
} from './managed-extension-profile.js';
import type { ManagedSessionLifecycleAuthority } from '@qwen-code/qwen-code-core/managed-runtime/http-managed-session-store.js';
export interface HostedWorkspaceBrokerOptions {
@@ -126,6 +131,7 @@ export class HostedWorkspaceBrokerExecutionUnresolved extends Error {
}
export class HostedWorkspaceBroker {
private extensionBinding?: { runtimeBindingId: string; generation: string };
private readonly baseUrl: URL;
private leaseAcquired = false;
private capability?: string;
@@ -295,6 +301,10 @@ export class HostedWorkspaceBroker {
scope['tenantId'] !== this.key.tenantId ||
scope['workspaceId'] !== this.key.workspaceId ||
(scope['capabilityDigest'] !== WORKSPACE_CAPABILITY_DIGEST &&
scope['capabilityDigest'] !==
MANAGED_EXTENSION_LINUX_CAPABILITY_DIGEST &&
scope['capabilityDigest'] !==
MANAGED_EXTENSION_DEVELOPER_CAPABILITY_DIGEST &&
!(
scope['capabilityDigest'] === RESIDENT_WORKSPACE_CAPABILITY_DIGEST &&
scope['isolationClass'] === 'workspace'
@@ -746,6 +756,59 @@ export class HostedWorkspaceBroker {
return result as unknown as ManagedHookOperationView;
}
async extensionControl(
operation: ManagedExtensionRuntimeControl,
): Promise<Record<string, unknown>> {
const envelope = await this.request(
`/harness-sessions/${encodeURIComponent(this.key.sessionId)}/extensions/control`,
{
operation,
...(this.extensionBinding
? {
expectedRuntimeBindingId: this.extensionBinding.runtimeBindingId,
expectedRuntimeGeneration: this.extensionBinding.generation,
}
: {}),
},
true,
8 * 1024 * 1024,
);
this.readRuntime(envelope);
this.extensionBinding = {
runtimeBindingId: this.runtime!.bindingId,
generation: this.runtime!.generation,
};
const result = object(envelope['result']);
if (result['operationId'] !== operation.operationId)
throw new Error('Runtime extension response identity is invalid.');
return result;
}
async extensionCommand(
operation: import('@qwen-code/qwen-code-core/managed-runtime/managed-extension-package.js').ManagedExtensionCommandControl,
): Promise<Record<string, unknown>> {
const envelope = await this.request(
`/tool-sessions/${encodeURIComponent(this.identity.runtimeSessionId)}/control`,
{ operation },
);
const result = object(envelope['result']);
if (
result['operationId'] !== operation.operationId ||
!['running', 'settled', 'outcome_unknown'].includes(
String(result['state']),
)
)
throw new Error('Runtime extension command response is invalid.');
return result;
}
expectExtensionRuntime(expected: {
runtimeBindingId: string;
generation: string;
}): void {
this.extensionBinding = expected;
}
/**
* #13896: one watch's maintenance through the client route. A watch
* answers running, exited or unknown — never the Hook/MCP settlement
@@ -1050,6 +1113,7 @@ export class HostedWorkspaceBroker {
path: string,
body?: Record<string, unknown>,
harnessScoped = false,
maximumResponseBytes = 2 * 1024 * 1024,
): Promise<Record<string, unknown>> {
const url = new URL(`/internal/runtime-broker/v1${path}`, this.baseUrl);
const fields = {
@@ -1091,7 +1155,7 @@ export class HostedWorkspaceBroker {
const chunk = await reader.read();
if (chunk.done) break;
length += chunk.value.length;
if (length > 2 * 1024 * 1024) {
if (length > maximumResponseBytes) {
await reader.cancel();
throw new Error('Runtime Broker response exceeds its limit.');
}
@@ -45,12 +45,17 @@ import {
hostedShellLaneAttachesChildAgents,
type HostedWorkspaceToolProfile,
} from './hosted-workspace-profiles.js';
import {
MANAGED_EXTENSION_LINUX_CAPABILITY_DIGEST,
MANAGED_EXTENSION_DEVELOPER_CAPABILITY_DIGEST,
} from './managed-extension-profile.js';
// H4b: the kind gate admits `child_agent` for real, so this suite runs
// without an enablement mock. The Broker is mocked as in the sibling
// suite: the agent path never dispatches through it, but the turn's
// constructor warms it.
const broker = vi.hoisted(() => ({
capabilityDigest: '',
fileHistory: vi.fn(),
warm: vi.fn().mockResolvedValue(undefined),
acquire: vi.fn().mockResolvedValue(undefined),
@@ -71,6 +76,9 @@ vi.mock('./hosted-workspace-broker.js', async (importOriginal) => ({
HostedWorkspaceBroker: class {
resolveCapabilities = async () => {};
readonly runtimeSessionId = 'prompt';
get capabilityDigest() {
return broker.capabilityDigest;
}
acquired = false;
runtime = {
bindingId: 'binding',
@@ -135,6 +143,10 @@ function createTurn(
childSnapshots = false,
publicChildren = false,
launchEnabled: () => boolean = () => true,
extensionExtras: Pick<
NonNullable<ConstructorParameters<typeof HostedWorkspaceToolTurn>[9]>,
'extensionAgents' | 'extensionConfig' | 'mcp' | 'hooks'
> = {},
): HostedWorkspaceToolTurn {
return new HostedWorkspaceToolTurn(
{ baseUrl: 'http://127.0.0.1:1', token: 'test' },
@@ -175,8 +187,13 @@ function createTurn(
funnel: children,
publicChildren,
launchEnabled,
snapshot: async (sourcePromptId, modelId, modelRoute) => ({
version: 1,
snapshot: async (
sourcePromptId,
modelId,
modelRoute,
extensionAgent,
) => ({
version: extensionAgent ? 2 : 1,
sourcePromptId,
modelId,
modelRoute,
@@ -184,6 +201,7 @@ function createTurn(
approvalRevision: 2,
modelRevision: 1,
skillsRef: null,
...(extensionAgent ? { extensionAgent } : {}),
}),
depth,
childWorkspaces,
@@ -217,6 +235,7 @@ function createTurn(
}
: {}),
...(hooksSession ? { hooks: hooksSession } : {}),
...extensionExtras,
},
);
}
@@ -254,6 +273,7 @@ function cancelWhenChildWaits(abort: AbortController): void {
beforeEach(async () => {
vi.resetAllMocks();
broker.capabilityDigest = '';
for (const method of [
broker.warm,
broker.acquire,
@@ -1581,6 +1601,155 @@ it('refuses a background agent while the Session owner holds the mount', async (
);
});
it.each([
{ owner: 'mcp', capability: MANAGED_EXTENSION_LINUX_CAPABILITY_DIGEST },
{ owner: 'hooks', capability: MANAGED_EXTENSION_DEVELOPER_CAPABILITY_DIGEST },
] as const)(
'keeps the parent $owner owner while a pinned v2 extension child returns its result',
async ({ owner, capability }) => {
broker.capabilityDigest = capability;
const pin = {
extensionId: 'review',
packageRevision: 1,
packageDigest: 'a'.repeat(64),
};
const descriptor = {
configRevision: 1,
bundleRef: await session.resources.publish(
'managed-extension-config-bundle',
Buffer.from('{}'),
),
pin,
contributionsRef: await session.resources.publish(
'managed-extension-contributions',
Buffer.from('{}'),
),
agentName: 'review:auditor',
toolNames: [],
};
const heldOwner = {
broker: new HostedWorkspaceBroker(
{ baseUrl: 'http://127.0.0.1:1', token: 'test' },
sessionKey,
'parent-mcp',
),
mountHeld: true,
ensureReady: vi.fn(async () => undefined),
acquire: vi.fn(async () => undefined),
refresh: vi.fn(async () => undefined),
tools: () => [],
toolInput: () => undefined,
fire: vi.fn(async () => []),
close: vi.fn(async () => undefined),
};
const extensionExtras = {
extensionAgents: [{ name: descriptor.agentName, descriptor }],
extensionConfig: {
revision: 1,
packages: [{ pin, assetRoot: 'extension://review/' }],
},
...(owner === 'mcp'
? {
mcp: heldOwner as unknown as import('./hosted-mcp-session.js').HostedMcpSession,
}
: {
hooks: heldOwner as unknown as HostedHookSession,
}),
};
const turn = createTurn(
0,
undefined,
'prompt',
true,
false,
true,
undefined,
'hosted-workspace-shell/1',
false,
true,
() => true,
extensionExtras,
);
turn.setChildModelRoute('model', {
authType: 'openai',
endpointDigest: 'a'.repeat(64),
providerPin: 'b'.repeat(64),
});
const ordinary = await executeAgent(
turn,
call({ description: 'ordinary', prompt: 'review' }, 'legacy'),
);
expect(JSON.stringify(ordinary)).toContain(mountRefusal);
broker.capabilityDigest = 'legacy-runtime';
const legacyRuntime = await executeAgent(
turn,
call(
{
description: 'extension audit',
prompt: 'review',
subagent_type: descriptor.agentName,
},
'legacy-runtime',
),
);
expect(JSON.stringify(legacyRuntime)).toContain(mountRefusal);
broker.capabilityDigest = capability;
extensionExtras.extensionConfig.revision = 2;
const stale = await executeAgent(
turn,
call(
{
description: 'extension audit',
prompt: 'review',
subagent_type: descriptor.agentName,
},
'stale-config',
),
);
expect(JSON.stringify(stale)).toContain(mountRefusal);
extensionExtras.extensionConfig.revision = 1;
const completed = executeAgent(
turn,
call({
description: 'extension audit',
prompt: 'review the package',
subagent_type: descriptor.agentName,
run_in_background: false,
}),
);
await vi.waitFor(
() => expect(children.record('prompt:call-1')).toBeDefined(),
{ timeout: 10_000 },
);
expect(await children.publicLaunchSnapshot('prompt:call-1')).toMatchObject({
version: 2,
extensionAgent: { ...descriptor, toolNames: expect.any(Array) },
});
await children.dispatchStarted('prompt:call-1', {
dispatchId: 'child-dispatch',
runtime: { runtimeBindingId: 'child-binding', generation: '1' },
});
await children.attach('prompt:call-1', randomUUID());
await children.settleCompleted('prompt:call-1', {
result: Buffer.from('extension child result'),
receipt: Buffer.from('{"outcome":"settled"}'),
});
await children.accept('prompt:call-1');
expect(JSON.stringify(await completed)).toContain('extension child result');
expect(heldOwner.mountHeld).toBe(true);
expect(heldOwner.close).not.toHaveBeenCalled();
expect(broker.release).not.toHaveBeenCalled();
expect(consumption).toEqual(['prompt:call-1']);
const launch = await children.publicLaunchSnapshot('prompt:call-1');
expect(launch?.version).toBe(2);
if (launch?.version === 2 && launch.extensionAgent) {
expect(launch.extensionAgent.toolNames).toContain('read_file');
expect(launch.extensionAgent.toolNames).not.toContain('agent');
expect(launch.extensionAgent.toolNames).not.toContain('send_message');
}
},
);
// The wake-turn id already embeds its commissioning run id
// (`<childRunId>:accept:notify`); using it verbatim as the launch base
// grows one suffix per chained hop into the 128-char lineage bound. The
@@ -36,6 +36,7 @@ import {
type HostedFileHistorySettleBlocker,
} from './hosted-file-history.js';
import { HostedShellPublisher } from './hosted-shell-publisher.js';
import type { HostedExtensionToolConfig } from './hosted-extension-tool-context.js';
import {
HOSTED_TOOL_INPUT_ORDER,
HostedHookSession,
@@ -9809,6 +9810,7 @@ function backgroundTurnRig(
fitsInline?: (type: string) => boolean;
publicationMissing?: boolean;
foregroundOnly?: boolean;
extensionConfig?: HostedExtensionToolConfig;
closeResults?: ReadonlyArray<'NOT_STARTED' | 'MISSING'>;
executionCallIds?: readonly string[];
hooks?: HostedHookSession;
@@ -9966,6 +9968,7 @@ function backgroundTurnRig(
childRuns: orchestrator as never,
backgroundLane: options.lane,
foregroundOnly: options.foregroundOnly,
extensionConfig: options.extensionConfig,
},
);
return {
@@ -10726,6 +10729,59 @@ it.each([true, false])(
},
);
it.each([true, false])(
'preserves extension Shell pins with background admission enabled (%s)',
async (enabled) => {
const pin = {
extensionId: 'ext_11111111-1111-4111-8111-111111111111',
packageRevision: 1,
packageDigest: 'a'.repeat(64),
};
const rig = backgroundTurnRig(
{ executionStatus: 'not_started', responseParts: [], capture: null },
{
extensionConfig: {
revision: 1,
packages: [{ pin, assetRoot: path.join(root, 'extension-assets') }],
},
},
);
turn = rig.turn;
enablement.childRun = enabled;
const declarations = await turn.declarations(new AbortController().signal);
const shell = declarations.find(
(tool) => tool.name === 'run_shell_command',
);
expect(shell?.parametersJsonSchema).toHaveProperty(
'properties.extension_context.oneOf',
[
{
properties: Object.fromEntries(
Object.entries(pin).map(([key, value]) => [key, { const: value }]),
),
},
],
);
expect(shell?.description).toContain('$QWEN_EXTENSION_ROOT');
expect(shell?.description).toContain(
'cwd stays the Session working directory',
);
if (enabled)
expect(shell?.parametersJsonSchema).toHaveProperty(
'properties.is_background',
);
else {
expect(shell?.parametersJsonSchema).not.toHaveProperty(
'properties.is_background',
);
expect(shell?.description).toContain(
'Shell file mutations are not backed up',
);
}
expect(declarations.map((tool) => tool.name)).not.toContain('monitor');
},
);
it.each([true, false])(
'advertises Monitor only while its domain is enabled (%s)',
async (enabled) => {
@@ -12557,3 +12613,63 @@ it('refuses a malformed permission rewrite on an admitted background Shell', asy
expect(broker.executeV3).not.toHaveBeenCalled();
expect(broker.acquire).toHaveBeenCalledOnce();
});
it.each(['auto', 'yolo'] as const)(
'enforces Session deny rules on extension command steps in %s mode',
async (mode) => {
turn = createTurn(true, {
mode,
permissionRules: { allow: [], ask: [], deny: ['Bash'] },
});
const signal = new AbortController().signal;
await turn.declarations(signal);
const permission = await (
turn as unknown as {
commandPermission(
id: string,
toolName: 'shell',
input: Record<string, unknown>,
signal: AbortSignal,
outerCallId: string,
): Promise<{ allowed: boolean; approvalMode: string }>;
}
).commandPermission(
'command-step',
'shell',
{ command: 'printf denied' },
signal,
'outer-command',
);
expect(permission).toMatchObject({ allowed: false, approvalMode: mode });
expect(actionIds()).toEqual([]);
expect(broker.execute).not.toHaveBeenCalled();
},
);
it('honors an explicit Session allow rule for an extension command step', async () => {
turn = createTurn(true, {
mode: 'default',
permissionRules: { allow: ['Bash'], ask: [], deny: [] },
});
const signal = new AbortController().signal;
await turn.declarations(signal);
const permission = await (
turn as unknown as {
commandPermission(
id: string,
toolName: 'shell',
input: Record<string, unknown>,
signal: AbortSignal,
outerCallId: string,
): Promise<{ allowed: boolean }>;
}
).commandPermission(
'command-step',
'shell',
{ command: 'printf allowed' },
signal,
'outer-command',
);
expect(permission.allowed).toBe(true);
expect(actionIds()).toEqual([]);
});
@@ -1,3 +1,8 @@
import {
extensionToolDeclaration,
extensionToolInput,
type HostedExtensionToolConfig,
} from './hosted-extension-tool-context.js';
/**
* @license
* Copyright 2026 Qwen Team
@@ -11,6 +16,10 @@ import {
} from './hosted-turn-cleanup.js';
import { probeNeverStarted } from './hosted-never-started-reconcile.js';
import { RESIDENT_WORKSPACE_CAPABILITY_DIGEST } from './managed-workspace-activation.js';
import {
MANAGED_EXTENSION_LINUX_CAPABILITY_DIGEST,
MANAGED_EXTENSION_DEVELOPER_CAPABILITY_DIGEST,
} from './managed-extension-profile.js';
import {
readHostedFileHistory,
@@ -136,6 +145,7 @@ import type { HostedChildRunSession } from './hosted-child-run-session.js';
import type {
PublicChildLaunchSnapshot,
PublicChildModelRoute,
PublicChildExtensionAgent,
} from '@qwen-code/qwen-code-core/managed-runtime/managed-child-operations.js';
import type { HostedChildAgentSession } from './hosted-child-agent-session.js';
import {
@@ -230,6 +240,11 @@ import {
HOSTED_SKILL_BATCH_REFUSAL,
type HostedSkillCatalog,
} from './hosted-skill-catalog.js';
import {
expandHostedExtensionCommand,
HostedCommandExpansionRefusedError,
type HostedCommandPermission,
} from './hosted-extension-command.js';
import {
endHostedAction,
HOSTED_APPROVAL_OPTIONS,
@@ -237,6 +252,7 @@ import {
hostedActionAllowed,
hostedApprovalAsks,
hostedLiveApprovalPoll,
readHostedActionOptions,
type HostedActionOptions,
type HostedApprovalSettings,
type HostedApprovalWaiters,
@@ -1852,7 +1868,19 @@ export class HostedWorkspaceToolTurn {
promptId: string,
model: string,
route: PublicChildModelRoute,
extensionAgent?: PublicChildExtensionAgent,
) => Promise<PublicChildLaunchSnapshot>;
private childModelResolver?: (
model: string,
) => Promise<{ modelId: string; route: PublicChildModelRoute }>;
private readonly extensionAgents: ReadonlyArray<{
name: string;
model?: string;
tools?: readonly string[];
disallowedTools?: readonly string[];
descriptor: PublicChildExtensionAgent;
}>;
private readonly allowedToolNames?: readonly string[];
private readonly childDepth: number;
private readonly childWorkspaces: boolean;
private readonly childSnapshots: boolean;
@@ -1862,6 +1890,7 @@ export class HostedWorkspaceToolTurn {
/** H4e-b3: the member's command funnel to its parent, beside its messages. */
private readonly commands?: HostedSessionCommandSession;
private readonly skills?: HostedSkillCatalog;
private readonly extensionConfig?: HostedExtensionToolConfig;
private toolOutputBudget = DEFAULT_TOOL_OUTPUT_BATCH_BUDGET;
/** b3b: whether this Session's run carries a member preamble (once read). */
private memberPreamble?: boolean;
@@ -1922,6 +1951,7 @@ export class HostedWorkspaceToolTurn {
promptId: string,
model: string,
route: PublicChildModelRoute,
extensionAgent?: PublicChildExtensionAgent,
) => Promise<PublicChildLaunchSnapshot>;
readonly depth: number;
/** #13753 I2: the host serves child Workspaces. */
@@ -1934,6 +1964,15 @@ export class HostedWorkspaceToolTurn {
teams?: HostedTeamSession;
commands?: HostedSessionCommandSession;
skills?: HostedSkillCatalog;
extensionConfig?: HostedExtensionToolConfig;
extensionAgents?: ReadonlyArray<{
name: string;
model?: string;
tools?: readonly string[];
disallowedTools?: readonly string[];
descriptor: PublicChildExtensionAgent;
}>;
allowedToolNames?: readonly string[];
/**
* H4e-b3 decisions 5/9: this run's first input carried an unescaped
* `plan-mode="required"` preamble; its calls admit reads only.
@@ -1942,6 +1981,7 @@ export class HostedWorkspaceToolTurn {
publicPlanMode?: boolean;
},
) {
this.extensionConfig = extras?.extensionConfig;
this.foregroundOnly = extras?.foregroundOnly === true;
this.mcp = this.foregroundOnly ? undefined : extras?.mcp;
this.hooks = this.foregroundOnly ? undefined : extras?.hooks;
@@ -1972,6 +2012,8 @@ export class HostedWorkspaceToolTurn {
this.teams = this.foregroundOnly ? undefined : extras?.teams;
this.commands = this.foregroundOnly ? undefined : extras?.commands;
this.skills = extras?.skills;
this.extensionAgents = extras?.extensionAgents ?? [];
this.allowedToolNames = extras?.allowedToolNames;
this.memberPlanMode = extras?.memberPlanMode === true;
this.publicPlanMode = extras?.publicPlanMode === true;
this.planning = this.publicPlanMode;
@@ -2037,6 +2079,13 @@ export class HostedWorkspaceToolTurn {
setChildModelRoute(modelId: string, route: PublicChildModelRoute): void {
this.childModelRoute = { modelId, route };
}
setChildModelResolver(
resolve: (
model: string,
) => Promise<{ modelId: string; route: PublicChildModelRoute }>,
): void {
this.childModelResolver = resolve;
}
private agentsAdmitted(): boolean {
return (
@@ -2138,7 +2187,8 @@ export class HostedWorkspaceToolTurn {
...(todoTool ? [HOSTED_TODO_WRITE_TOOL] : []),
]
.filter((tool) => tool.name !== 'monitor' || this.monitorsAdmitted())
.map((tool) => {
.map((declaration) => {
let tool = declaration;
if (
tool.name === 'run_shell_command' &&
!this.backgroundShellAdmitted()
@@ -2147,17 +2197,17 @@ export class HostedWorkspaceToolTurn {
properties?: Record<string, unknown>;
};
delete schema.properties?.['is_background'];
return {
tool = {
...tool,
description:
'Run one foreground shell command in the saved Session working directory. Timeout is 1..600000 ms. Shell file mutations are not backed up. Changing tracked file content or permissions can cause subsequent Write/Edit and undo conflicts.',
parametersJsonSchema: schema,
};
}
return hostedWorkspaceDeclarations(
[tool],
!this.fileBackupsEnabled,
)[0];
return extensionToolDeclaration(
hostedWorkspaceDeclarations([tool], !this.fileBackupsEnabled)[0],
this.extensionConfig,
);
}),
...(this.mcp?.tools() ?? []),
...(skill ? [skill] : []),
@@ -2250,6 +2300,31 @@ export class HostedWorkspaceToolTurn {
);
}
if (this.publicPlanMode) this.advertised.push(HOSTED_PUBLIC_PLAN_TOOL);
if (this.publicChildren && this.extensionAgents.length)
this.advertised = this.advertised.map((tool) =>
tool.name !== 'agent'
? tool
: {
...tool,
parametersJsonSchema: {
...(tool.parametersJsonSchema as Record<string, unknown>),
properties: {
...((tool.parametersJsonSchema as Record<string, unknown>)[
'properties'
] as Record<string, unknown>),
subagent_type: {
type: 'string',
enum: this.extensionAgents.map((agent) => agent.name),
description: 'Run a pinned extension agent.',
},
},
},
},
);
if (this.allowedToolNames)
this.advertised = this.advertised.filter((tool) =>
this.allowedToolNames!.includes(tool.name ?? ''),
);
return this.planning
? this.advertised.filter(
(tool) =>
@@ -2415,6 +2490,25 @@ export class HostedWorkspaceToolTurn {
);
}
private extensionChildCanShareWorkspace(call: ToolCallRequestInfo): boolean {
const selected = this.extensionAgents.find(
(agent) => agent.name === call.args['subagent_type'],
);
return (
this.publicChildren &&
selected !== undefined &&
this.extensionConfig !== undefined &&
selected.descriptor.configRevision === this.extensionConfig.revision &&
this.extensionConfig.packages.some((entry) =>
isDeepStrictEqual(entry.pin, selected.descriptor.pin),
) &&
[
MANAGED_EXTENSION_LINUX_CAPABILITY_DIGEST,
MANAGED_EXTENSION_DEVELOPER_CAPABILITY_DIGEST,
].includes(this.broker.capabilityDigest ?? '')
);
}
async resumeCommittedResults(signal?: AbortSignal): Promise<void> {
if (this.acquired) return;
if (this.skills && !this.hooks && !this.mcp) {
@@ -3403,8 +3497,9 @@ export class HostedWorkspaceToolTurn {
this.session.authority.action(item.actionId)?.state === 'expired',
);
if (
this.hooks ||
this.mcp ||
(!plan.commandStep && (this.hooks || this.mcp)) ||
(plan.commandStep &&
plan.commandStep.configRevision !== this.extensionConfig?.revision) ||
this.promptId !== plan.promptId ||
this.broker.runtimeSessionId !== plan.runtime.runtimeSessionId
)
@@ -3423,8 +3518,10 @@ export class HostedWorkspaceToolTurn {
(item) => item.call.name !== 'agent',
);
if (needsRuntime) {
await oweHostedTurnCleanup(this.session, this.promptId, this.broker);
this.cleanupOwed = true;
if (!this.hooks && !this.mcp) {
await oweHostedTurnCleanup(this.session, this.promptId, this.broker);
this.cleanupOwed = true;
}
await this.broker.acquire({
runtimeBindingId: plan.runtime.bindingId,
generation: plan.runtime.generation,
@@ -3515,6 +3612,7 @@ export class HostedWorkspaceToolTurn {
stage: 'approval' | 'final',
actionId: string | null,
approvalOrdinal: number,
commandStep?: HostedApprovalContinuation['commandStep'],
): Promise<ManagedSessionDurableRef | undefined> {
const batch = this.nativeBatch;
const runtime = this.broker.runtime;
@@ -3527,15 +3625,16 @@ export class HostedWorkspaceToolTurn {
batch.uncaptured ||
batch.requests.some((request) => request.todo) ||
!runtime ||
(isHostedWorkspaceShellProfile(this.profile) &&
(!commandStep &&
isHostedWorkspaceShellProfile(this.profile) &&
!singlePlanExit &&
!this.publicChildren) ||
this.hooks ||
this.mcp ||
(!commandStep && (this.hooks || this.mcp)) ||
batch.requests.some(
(request) =>
!(
singlePlanExit ||
(commandStep && request.call.name === 'skill') ||
(this.publicChildren &&
['agent', 'run_shell_command'].includes(request.call.name)) ||
[
@@ -3632,6 +3731,7 @@ export class HostedWorkspaceToolTurn {
stage,
approvalOrdinal,
actionId,
...(commandStep ? { commandStep } : {}),
calls,
};
const planBytes = Buffer.from(JSON.stringify(plan));
@@ -3693,7 +3793,13 @@ export class HostedWorkspaceToolTurn {
},
): Promise<Part[]> {
signal.throwIfAborted();
if (this.mcp) await waitForTurn(this.warmed, signal);
if (
this.mcp ||
(this.publicChildren &&
this.extensionAgents.length > 0 &&
calls.some((call) => call.name === 'agent'))
)
await waitForTurn(this.warmed, signal);
const declarations = this.advertised ?? (await this.declarations(signal));
this.hookAllowed.clear();
this.agentDispatched.clear();
@@ -3717,6 +3823,8 @@ export class HostedWorkspaceToolTurn {
call.name === 'skill' &&
this.skills !== undefined;
if (
(this.allowedToolNames !== undefined &&
!this.allowedToolNames.includes(call.name)) ||
(!declarations.some((tool) => tool.name === call.name) &&
!(skill && call.isClientInitiated)) ||
ids.has(call.callId) ||
@@ -3807,9 +3915,13 @@ export class HostedWorkspaceToolTurn {
const args = call.args;
const unsupportedKey = Object.keys(args).find(
(key) =>
!['command', 'timeout', 'description', 'is_background'].includes(
key,
),
![
'command',
'timeout',
'description',
'is_background',
'extension_context',
].includes(key),
);
// H3: a background request is admitted exactly when this Session
// owns its child_run orchestrator and the domain is enabled — the
@@ -3938,9 +4050,11 @@ export class HostedWorkspaceToolTurn {
validationError = globError;
} else {
try {
input['path'] = normalizeWorkspaceRelativePath(
directory.trim(),
);
input['path'] =
directory.trim().startsWith('extension://') &&
call.args['extension_source'] !== undefined
? directory.trim()
: normalizeWorkspaceRelativePath(directory.trim());
} catch (cause) {
if (!(cause instanceof InvalidWorkspaceRelativePathError))
throw cause;
@@ -4000,6 +4114,8 @@ export class HostedWorkspaceToolTurn {
input = { ...args, is_monitor: true };
} else if (call.name === 'agent') {
const args = call.args;
const sharedExtensionWorkspace =
this.extensionChildCanShareWorkspace(call);
// The deliberate refusals below keep their texts whenever the
// Agent call is not admitted.
agentAdmitted = this.agentsAdmitted();
@@ -4016,6 +4132,9 @@ export class HostedWorkspaceToolTurn {
? ['plan_mode_required']
: []),
'isolation',
...(this.publicChildren && this.extensionAgents.length
? ['subagent_type']
: []),
].includes(key),
);
const backgroundValue = args['run_in_background'];
@@ -4040,6 +4159,14 @@ export class HostedWorkspaceToolTurn {
'Public child agents share this Workspace; isolation is unavailable.';
} else if (unsupportedKey !== undefined) {
validationError = `Hosted child agent received unsupported argument ${JSON.stringify(unsupportedKey)}. This profile runs only the Session's own definition, without nesting: fork_*, working_dir, ${teamsOn ? '' : 'name, '}model and subagent_type belong to the legacy Agent tool.`;
} else if (
args['subagent_type'] !== undefined &&
!this.extensionAgents.some(
(agent) => agent.name === args['subagent_type'],
)
) {
validationError =
'Hosted extension agent is not in the active pinned configuration.';
} else if (
args['isolation'] !== undefined &&
args['isolation'] !== 'worktree' &&
@@ -4068,9 +4195,16 @@ export class HostedWorkspaceToolTurn {
} else if (named && !agentBackground) {
validationError =
'Hosted team member always runs in the background; omit run_in_background or set it to true.';
} else if (this.sessionOwnerMayHoldMount()) {
} else if (
!sharedExtensionWorkspace &&
this.sessionOwnerMayHoldMount()
) {
validationError = `Hosted child agent is unavailable while ${HOSTED_CHILD_MOUNT_REFUSALS.sessionOwner}; a Session-owned hold releases only when the Session closes, so read the work through ordinary tools instead.`;
} else if (!agentBackground && this.acquired) {
} else if (
!sharedExtensionWorkspace &&
!agentBackground &&
this.acquired
) {
// A foreground child cannot borrow its waiting parent's mount.
// Ordered after the Session-owner arm: a Session-scoped mount
// outlives this Turn, so the background and fresh-turn advice
@@ -4078,6 +4212,7 @@ export class HostedWorkspaceToolTurn {
// Session-owned hold advice that cannot act on it.
validationError = `Hosted child agent run_in_background=false is unavailable while ${HOSTED_CHILD_MOUNT_REFUSALS.turn}; run it in the background or let the current tool work finish first in a fresh turn.`;
} else if (
!sharedExtensionWorkspace &&
!agentBackground &&
calls.some(
(other) => other.name !== 'agent' && other.name !== 'workflow',
@@ -4352,7 +4487,11 @@ export class HostedWorkspaceToolTurn {
validationError = filePathError;
} else {
try {
input['file_path'] = normalizeWorkspaceRelativePath(file.trim());
input['file_path'] =
file.trim().startsWith('extension://') &&
call.args['extension_source'] !== undefined
? file.trim()
: normalizeWorkspaceRelativePath(file.trim());
} catch (cause) {
if (!(cause instanceof InvalidWorkspaceRelativePathError))
throw cause;
@@ -4360,6 +4499,19 @@ export class HostedWorkspaceToolTurn {
}
}
}
try {
input = extensionToolInput(
this.session.authority.sessionHeader.sessionKey.sessionId,
this.extensionConfig,
call.name,
input,
);
} catch (cause) {
validationError ??=
cause instanceof Error
? cause.message
: 'Extension tool context was refused.';
}
// The inline Session Store limit keeps rejecting exactly as main
// does for the ordinary tools. A call that already carries its own
// refusal never reads its encoding. Workflow launches and messages
@@ -5205,21 +5357,18 @@ export class HostedWorkspaceToolTurn {
]
: [],
);
const skillBatch = requests.every((request) => request.skill)
? enforceFunctionResponseBudget(
requests.map((request, index) => ({
callId: request.call.callId,
toolName: request.call.name,
responseParts:
refusal(index) ??
convertToFunctionResponse(
request.call.name,
request.call.callId,
[{ text: this.skills!.response(request.call.args) }],
),
})),
this.toolOutputBudget,
)
const skillResponses = [];
if (requests.every((request) => request.skill))
for (const [index, request] of requests.entries())
skillResponses.push({
callId: request.call.callId,
toolName: request.call.name,
responseParts:
refusal(index) ??
(await this.skillResponse(request.call, signal)),
});
const skillBatch = skillResponses.length
? enforceFunctionResponseBudget(skillResponses, this.toolOutputBudget)
: undefined;
executing = true;
for (const [index, request] of requests.entries()) {
@@ -6545,14 +6694,49 @@ export class HostedWorkspaceToolTurn {
// can ever answer. Translate the throw into the same refusal: an
// over-size prompt is a model-correctable argument error, never a
// recovery-blocked Turn.
const extensionAgent =
request.call.args['subagent_type'] === undefined
? undefined
: this.extensionAgents.find(
(agent) => agent.name === request.call.args['subagent_type'],
);
const childModel = extensionAgent?.model
? await this.childModelResolver?.(extensionAgent.model)
: this.childModelRoute;
if (extensionAgent?.model && !childModel)
throw new Error('Extension agent model is unavailable.');
const allowed = (this.advertised ?? [])
.map((tool) => tool.name!)
.filter(
(name) =>
(HOSTED_WORKSPACE_SHELL_TOOLS.some((tool) => tool.name === name) ||
name === 'glob' ||
name === 'skill' ||
name === 'todo_write' ||
this.mcp?.tools().some((tool) => tool.name === name)) &&
(!extensionAgent?.tools || extensionAgent.tools.includes(name)) &&
!extensionAgent?.disallowedTools?.includes(name),
);
const descriptor = extensionAgent
? { ...extensionAgent.descriptor, toolNames: allowed }
: undefined;
const publicLaunch = this.publicChildren
? ((await children.publicLaunchSnapshot(childRunId)) ??
(await this.childSnapshot!(
this.promptId,
this.childModelRoute?.modelId ?? model,
this.childModelRoute!.route,
childModel?.modelId ?? model,
childModel!.route,
descriptor,
)))
: undefined;
if (
extensionAgent &&
(publicLaunch?.version !== 2 ||
!isDeepStrictEqual(publicLaunch.extensionAgent, descriptor))
)
throw new Error(
'Extension child requires its pinned v2 launch snapshot.',
);
let envelopeBytes: number;
try {
envelopeBytes = encodeChildLaunchEnvelope({
@@ -8316,6 +8500,336 @@ export class HostedWorkspaceToolTurn {
: refusals;
}
private async skillResponse(
call: ToolCallRequestInfo,
signal: AbortSignal,
): Promise<Part[]> {
const command = this.skills!.command(call.args);
if (!command)
return convertToFunctionResponse(call.name, call.callId, [
{ text: this.skills!.response(call.args) },
]);
if (!this.extensionConfig)
throw new Error('The command has no pinned extension configuration.');
try {
const expanded = await expandHostedExtensionCommand({
session: this.session,
broker: this.broker,
promptId: this.promptId,
callId: call.callId,
revision: this.extensionConfig.revision,
command,
args: String(call.args['args'] ?? ''),
signal,
definitionDigest: (toolName) =>
createHash('sha256')
.update(
JSON.stringify(
(this.advertised ?? []).find(
(tool) =>
tool.name ===
(toolName === 'shell' ? 'run_shell_command' : toolName),
) ??
HOSTED_WORKSPACE_SHELL_TOOLS.find(
(tool) =>
tool.name ===
(toolName === 'shell' ? 'run_shell_command' : toolName),
),
),
)
.digest('hex'),
decide: async (id, name, input) => {
await waitForTurn(this.warmed, signal);
if (!this.acquired) await this.acquire(false, signal);
return this.commandPermission(id, name, input, signal, call.callId);
},
completed: async (id, name, input, result) => {
const failed = result['executionStatus'] !== 'success';
const event = failed
? HookEventName.PostToolUseFailure
: HookEventName.PostToolUse;
const fields = {
tool_name: name === 'shell' ? 'run_shell_command' : name,
tool_input: input,
tool_use_id: id,
prompt_id: this.promptId,
permission_mode: this.approval?.settings.mode ?? 'yolo',
...(failed
? {
error: String(
result['error'] ?? 'Command preprocessing failed',
),
is_interrupt: result['executionStatus'] === 'cancelled',
}
: { tool_response: result }),
};
const output = await this.hooks?.fire(
event,
id,
await this.toolHookFields(event, id, fields),
signal,
this.promptHookRunner,
);
if (
output &&
createHookOutput(
failed
? HookEventName.PostToolUseFailure
: HookEventName.PostToolUse,
output,
).shouldStopExecution()
) {
this.hookStopReason ??=
'Command preprocessing was stopped by a Hook.';
throw new HostedCommandExpansionRefusedError(
'Command preprocessing was stopped by a Hook.',
);
}
},
});
return convertToFunctionResponse(call.name, call.callId, [
{ text: expanded },
]);
} catch (cause) {
if (cause instanceof HostedCommandExpansionRefusedError)
return convertToFunctionErrorResponse(
call.name,
call.callId,
[],
cause.message,
);
throw new HostedToolRecoveryRequiredError(cause);
}
}
private async commandPermission(
id: string,
toolName: 'read_file' | 'shell',
initial: Record<string, unknown>,
signal: AbortSignal,
outerCallId: string,
): Promise<HostedCommandPermission> {
const name = toolName === 'shell' ? 'run_shell_command' : toolName;
const settings = await this.readApprovalSettings();
const result: HostedCommandPermission = {
allowed: false,
toolName,
input: initial,
approvalMode: settings.mode,
preHookOccurrence: this.hooks ? id : null,
permissionHookOccurrence: null,
actionRequestId: null,
};
if (
!this.advertised?.some((tool) => tool.name === name) ||
((this.memberPlanMode || this.planning) && toolName === 'shell')
)
return result;
let call: ToolCallRequestInfo = {
name,
callId: id,
args: initial,
prompt_id: this.promptId,
isClientInitiated: false,
};
const pre = await this.hooks?.fire(
HookEventName.PreToolUse,
id,
await this.toolHookFields(
HookEventName.PreToolUse,
id,
this.preToolUseFields(call),
),
signal,
this.promptHookRunner,
undefined,
HOSTED_TOOL_INPUT_ORDER,
);
if (preToolUseRefusal(pre, false)) return result;
call = { ...call, args: preToolUseReplacement(pre, false) ?? call.args };
if (hostedShellInputError(name, call.args))
return { ...result, input: call.args };
const mustAsk = pre !== undefined && new PreToolUseHookOutput(pre).isAsk();
let policy = await evaluateHostedPermissionRules(
settings.permissionRules,
call,
);
if (policy === 'deny') return { ...result, input: call.args };
let hookAllowed =
pre !== undefined &&
new PreToolUseHookOutput(pre).getPermissionDecision() === 'allow';
let permissionHookOccurrence: string | null = null;
if (
this.hooks &&
policy === 'default' &&
hostedApprovalAsks(settings, name, this.globProfile)
) {
permissionHookOccurrence = id;
const permission = await this.hooks.fire(
HookEventName.PermissionRequest,
id,
this.permissionRequestFields(call),
signal,
this.promptHookRunner,
undefined,
HOSTED_TOOL_INPUT_ORDER,
);
const decision = permissionRequestDecision(permission);
if (decision.refusal)
return { ...result, input: call.args, permissionHookOccurrence };
hookAllowed ||= decision.allowed === true;
call = { ...call, args: decision.input ?? call.args };
}
if (hostedShellInputError(name, call.args))
return { ...result, input: call.args, permissionHookOccurrence };
policy = await evaluateHostedPermissionRules(
settings.permissionRules,
call,
);
if (policy === 'deny')
return { ...result, input: call.args, permissionHookOccurrence };
let actionRequestId: string | null = null;
let approvedInputRef: ManagedSessionDurableRef | undefined;
let allowed = true;
if (
mustAsk ||
policy === 'ask' ||
(policy !== 'allow' &&
!hookAllowed &&
hostedApprovalAsks(settings, name, this.globProfile))
) {
if (!this.approval) allowed = false;
else {
actionRequestId = `command_approval_${createHash('sha256').update(id).digest('hex').slice(0, 48)}`;
const authority = this.session.authority;
const existing = authority.action(actionRequestId);
let options: HostedActionOptions;
if (existing) {
options = await readHostedActionOptions(this.session, existing);
if (
options.toolName !== name ||
!('inputRef' in options) ||
!options.inputRef ||
!isDeepStrictEqual(
JSON.parse(
(await this.session.resources.read(options.inputRef)).toString(
'utf8',
),
),
call.args,
)
)
throw new HostedToolRecoveryRequiredError(
'Command step approval input changed.',
);
} else {
const inputRef = await this.session.resources.publish(
'managed-extension-command-input',
Buffer.from(JSON.stringify(call.args)),
);
const createdAt = Date.now();
const ordinal =
this.nativeBatch?.requests.findIndex(
(request) => request.call.callId === outerCallId,
) ?? -1;
if (ordinal < 0)
throw new HostedToolRecoveryRequiredError(
'Command outer batch is unavailable.',
);
this.nativeBatch!.actionIds.set(ordinal, actionRequestId);
const continuationRef = await this.captureNativeBatch(
'approval',
actionRequestId,
ordinal,
{
toolStageId: id,
toolName,
inputRef,
configRevision: this.extensionConfig!.revision,
},
);
if (!continuationRef)
throw new HostedToolRecoveryRequiredError(
'Command approval continuation is unavailable.',
);
options = {
v: 3,
continuationRef,
inputRef,
requestId: actionRequestId,
turnId: this.promptId,
functionCallId: outerCallId,
toolName: name,
policyRevision: HOSTED_TOOL_APPROVAL_POLICY,
inputRevision: 1,
createdAt,
expiresAt: createdAt + settings.timeoutMs,
options: HOSTED_APPROVAL_OPTIONS,
...(mustAsk || policy === 'ask'
? { requiresHumanDecision: true as const }
: {}),
};
const optionsRef = await this.session.resources.publish(
'managed-action-options',
Buffer.from(JSON.stringify(options)),
);
await this.harness.commitDurableWait(
{
requestId: actionRequestId,
kind: 'permission',
source: 'tool_call',
optionsRef,
inputRevision: '1',
invocationRef: inputRef,
attemptId: id,
routeRef: inputRef,
},
{ turnId: this.promptId, promptId: this.promptId },
);
}
await this.approval.waiters.wait(
actionRequestId,
options.expiresAt,
signal,
() =>
authority.action(actionRequestId!)?.state !== 'requested' ||
authority.writesStopped,
this.approval.readSettings
? hostedLiveApprovalPoll(
this.session,
this.approval.waiters,
options,
() => this.readApprovalSettings(),
signal,
this.globProfile,
this.approval.isBlocked,
)
: undefined,
);
if (authority.action(actionRequestId)?.state === 'requested')
await endHostedAction(
this.session,
actionRequestId,
signal.aborted ? 'cancelled' : 'expired',
);
await this.harness.resolveDurableWait();
approvedInputRef = 'inputRef' in options ? options.inputRef : undefined;
allowed = hostedActionAllowed(
authority.action(actionRequestId)!,
options.policyRevision,
);
}
}
return {
...result,
allowed: allowed && !signal.aborted,
input: call.args,
permissionHookOccurrence,
actionRequestId,
...(approvedInputRef ? { inputRef: approvedInputRef } : {}),
};
}
private async ask(
approval: HostedApprovalTurnOptions,
call: ToolCallRequestInfo,
@@ -29,6 +29,10 @@ import {
type ManagedContextReceipt,
} from './managed-context-envelope.js';
import { createHash } from 'node:crypto';
import {
MANAGED_EXTENSION_LINUX_CAPABILITY_DIGEST,
MANAGED_EXTENSION_DEVELOPER_CAPABILITY_DIGEST,
} from './managed-extension-profile.js';
import {
CONTEXT_BINDING_DOMAIN_TAG,
computeManagedContextDigest,
@@ -424,6 +428,95 @@ describe('Managed context envelope contract', () => {
expect(Object.isFrozen((outcome as { body: object }).body)).toBe(true);
});
it.each([
{
capabilityDigest: MANAGED_EXTENSION_LINUX_CAPABILITY_DIGEST,
managedExtensions: {
enabled: true,
mode: 'linux-cgroup',
cacheDirectory: '/private/extension-cache',
cgroupRoot: '/sys/fs/cgroup/qwen',
},
},
{
capabilityDigest: MANAGED_EXTENSION_DEVELOPER_CAPABILITY_DIGEST,
managedExtensions: {
enabled: true,
mode: 'developer-process-group',
cacheDirectory: '/private/extension-cache',
},
},
])(
'attests the exact extension boot configuration $managedExtensions.mode',
(profile) => {
const configured = parseManagedContextBoot({ ...boot, ...profile });
// Java ManagedContextProtocol derives both bodies by removing these boot fields.
const {
type: _type,
version: _version,
token: _token,
...responseFields
} = configured;
const {
runtimeInstanceId: _runtimeInstanceId,
runtimeIncarnation: _runtimeIncarnation,
leaseId: _leaseId,
epoch: _epoch,
...requestFields
} = responseFields;
const request = { ...requestFields, protocolVersion: 3 };
expect(checkManagedContextAttestation(request, configured)).toStrictEqual(
{
status: 200,
body: { ...responseFields, protocolVersion: 3 },
},
);
expect(createManagedContextAttestationResponse(configured)).toStrictEqual(
{
...responseFields,
protocolVersion: 3,
},
);
const { managedExtensions: _extensions, ...missing } = request;
expect(checkManagedContextAttestation(missing, configured)).toStrictEqual(
INVALID,
);
expect(
checkManagedContextAttestation(
{
...request,
managedExtensions: { ...profile.managedExtensions, unknown: true },
},
configured,
),
).toStrictEqual(INVALID);
expect(
checkManagedContextAttestation(
{
...request,
managedExtensions: {
...profile.managedExtensions,
cacheDirectory: '/another/cache',
},
},
configured,
),
).toStrictEqual({
status: 409,
code: 'managed_runtime_identity_conflict',
});
expect(
checkManagedContextAttestation(
{
...(fixtures.attestationCases[0].body as object),
managedExtensions: profile.managedExtensions,
},
boot,
),
).toStrictEqual(INVALID);
},
);
it.each(fixtures.attestationCases)(
'answers the $id attestation case',
(fixture) => {
@@ -4,6 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {
MANAGED_EXTENSION_LINUX_CAPABILITY_DIGEST,
MANAGED_EXTENSION_DEVELOPER_CAPABILITY_DIGEST,
parseManagedExtensionRuntimeConfiguration,
} from './managed-extension-profile.js';
import {
computeManagedContextDigest,
isCanonicalDecimalText,
@@ -139,6 +144,7 @@ export type ManagedContextIsolationClass = 'session' | 'workspace';
export interface ManagedContextBoot {
readonly type: 'boot';
readonly managedExtensions?: import('./managed-extension-profile.js').ManagedExtensionRuntimeConfiguration;
readonly version: typeof MANAGED_CONTEXT_BOOT_VERSION;
readonly managedContext: typeof MANAGED_CONTEXT_PROTOCOL;
readonly runtimeInstanceId: string;
@@ -170,6 +176,7 @@ export interface ManagedContextReady {
}
export interface ManagedContextAttestationResponse {
readonly managedExtensions?: import('./managed-extension-profile.js').ManagedExtensionRuntimeConfiguration;
readonly protocolVersion: typeof PROTOCOL_VERSION;
readonly managedContext: typeof MANAGED_CONTEXT_PROTOCOL;
readonly runtimeInstanceId: string;
@@ -237,22 +244,42 @@ const CONTEXT_UNAVAILABLE: ManagedContextRefusal = Object.freeze({
* without echoing the input, which carries the bearer token.
*/
export function parseManagedContextBoot(value: unknown): ManagedContextBoot {
const boot = readClosed(value, BOOT_KEYS);
const hasExtensions =
!!value &&
typeof value === 'object' &&
Object.hasOwn(value, 'managedExtensions');
const boot = readClosed(
value,
hasExtensions ? [...BOOT_KEYS, 'managedExtensions'].sort() : BOOT_KEYS,
);
if (
!boot ||
boot.type !== 'boot' ||
boot.version !== MANAGED_CONTEXT_BOOT_VERSION ||
boot.managedContext !== MANAGED_CONTEXT_PROTOCOL ||
!isBearerToken(boot.token) ||
!isManagedIdentifier(boot.runtimeInstanceId) ||
!isManagedIdentifier(boot.runtimeIncarnation) ||
!isManagedIdentifier(boot.leaseId) ||
!isEpoch(boot.epoch) ||
boot['type'] !== 'boot' ||
boot['version'] !== MANAGED_CONTEXT_BOOT_VERSION ||
boot['managedContext'] !== MANAGED_CONTEXT_PROTOCOL ||
!isBearerToken(boot['token']) ||
!isManagedIdentifier(boot['runtimeInstanceId']) ||
!isManagedIdentifier(boot['runtimeIncarnation']) ||
!isManagedIdentifier(boot['leaseId']) ||
!isEpoch(boot['epoch']) ||
!hasValidAttestedFields(boot)
) {
throw new Error(INVALID_BOOT_MESSAGE);
}
return Object.freeze(boot) as ManagedContextBoot;
if (
[
MANAGED_EXTENSION_LINUX_CAPABILITY_DIGEST,
MANAGED_EXTENSION_DEVELOPER_CAPABILITY_DIGEST,
].includes(boot['capabilityDigest'] as string) !== hasExtensions
)
throw new Error(INVALID_BOOT_MESSAGE);
if (hasExtensions) {
boot['managedExtensions'] = parseManagedExtensionRuntimeConfiguration(
boot['managedExtensions'],
boot['capabilityDigest'] as string,
);
}
return Object.freeze(boot) as unknown as ManagedContextBoot;
}
/** The ready v2 record for a worker listening on the loopback `port`. */
@@ -316,16 +343,36 @@ export function checkManagedContextAttestation(
boot: ManagedContextBoot,
): ManagedContextOutcome<ManagedContextAttestationResponse> {
const identity = parseManagedContextBoot(boot);
const request = readClosed(body, ATTESTATION_KEYS);
const request = readClosed(
body,
identity.managedExtensions
? [...ATTESTATION_KEYS, 'managedExtensions'].sort()
: ATTESTATION_KEYS,
);
if (
!request ||
request.protocolVersion !== PROTOCOL_VERSION ||
request.managedContext !== MANAGED_CONTEXT_PROTOCOL ||
request['protocolVersion'] !== PROTOCOL_VERSION ||
request['managedContext'] !== MANAGED_CONTEXT_PROTOCOL ||
!hasValidAttestedFields(request)
) {
return INVALID;
}
if (ATTESTED_KEYS.some((key) => request[key] !== identity[key])) {
let managedExtensions;
if (identity.managedExtensions) {
try {
managedExtensions = parseManagedExtensionRuntimeConfiguration(
request['managedExtensions'],
request['capabilityDigest'] as string,
);
} catch {
return INVALID;
}
}
if (
ATTESTED_KEYS.some((key) => request[key] !== identity[key]) ||
JSON.stringify(managedExtensions) !==
JSON.stringify(identity.managedExtensions)
) {
return IDENTITY_CONFLICT;
}
return { status: 200, body: attestationResponse(identity) };
@@ -509,6 +556,9 @@ function attestationResponse(
mountRoot: identity.mountRoot,
capabilityDigest: identity.capabilityDigest,
isolationClass: identity.isolationClass,
...(identity.managedExtensions
? { managedExtensions: identity.managedExtensions }
: {}),
});
}
@@ -4,7 +4,14 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { createHash } from 'node:crypto';
import { createHash, randomUUID } from 'node:crypto';
import * as tar from 'tar';
import { compileManagedExtensionPackage } from '@qwen-code/qwen-code-core/extension/managed-package.js';
import {
MANAGED_EXTENSION_DEVELOPER_CAPABILITY_DIGEST,
MANAGED_EXTENSION_LINUX_CAPABILITY_DIGEST,
} from './managed-extension-profile.js';
import { extensionToolInput } from './hosted-extension-tool-context.js';
import express from 'express';
import fs from 'node:fs';
import os from 'node:os';
@@ -374,6 +381,268 @@ describe('Managed context worker boot', () => {
).rejects.toThrow('Managed Runtime worker boot payload is invalid.');
});
it.skipIf(process.platform !== 'darwin')(
'routes exact extension assets read-only and native Shell with private settings through the selected worker',
async () => {
const root = workspace(['services/api', 'services/web']);
const cache = fs.mkdtempSync(
path.join(os.tmpdir(), 'extension-worker-cache-'),
);
const source = fs.mkdtempSync(
path.join(os.tmpdir(), 'extension-worker-source-'),
);
onTestFinished(() => {
fs.rmSync(cache, { recursive: true, force: true });
fs.rmSync(source, { recursive: true, force: true });
vi.unstubAllEnvs();
});
fs.writeFileSync(
path.join(source, 'qwen-extension.json'),
JSON.stringify({
name: 'native-test',
version: '1.0.0',
settings: [
{ name: 'Token', envVar: 'SELECTED_SECRET', sensitive: true },
],
}),
);
fs.writeFileSync(
path.join(source, 'resource.txt'),
'private package resource',
);
const target = {
platform: 'darwin' as const,
arch: process.arch as 'arm64' | 'x64',
};
const compiled = await compileManagedExtensionPackage(source, {
target,
resolveRecipes: false,
});
const archive = path.join(cache, 'upload.tgz');
await tar.c({ cwd: source, file: archive, gzip: true }, [
'qwen-extension.json',
'resource.txt',
]);
const bytes = fs.readFileSync(archive);
const pin = {
extensionId: 'one',
packageRevision: 1,
packageDigest: createHash('sha256').update(bytes).digest('hex'),
settingsRevision: 1,
};
vi.stubEnv('OTHER_EXTENSION_SECRET', 'must-not-inherit');
const origin = await startWorker({
...BOOT,
mountRoot: root,
capabilityDigest: MANAGED_EXTENSION_DEVELOPER_CAPABILITY_DIGEST,
managedExtensions: {
enabled: true,
mode: 'developer-process-group',
cacheDirectory: cache,
},
});
const attested = await post(origin, ATTEST, {
...(CANONICAL_ATTESTATION as Record<string, unknown>),
mountRoot: root,
capabilityDigest: MANAGED_EXTENSION_DEVELOPER_CAPABILITY_DIGEST,
managedExtensions: {
enabled: true,
mode: 'developer-process-group',
cacheDirectory: cache,
},
});
expect(attested.status).toBe(200);
expect((await attested.json()).managedExtensions).toStrictEqual({
enabled: true,
mode: 'developer-process-group',
cacheDirectory: cache,
});
const uploaded = await fetch(
`${origin}/internal/managed-runtime/v3/extensions/packages/${pin.packageDigest}`,
{
method: 'PUT',
headers: { ...HEADERS, 'content-type': 'application/gzip' },
body: bytes,
},
);
expect(uploaded.status).toBe(200);
const installed = workspaceInstallation(
'550e8400-e29b-41d4-a716-446655440101',
'services/api',
);
const operationId = randomUUID();
const command = {
operationId,
actorKey: 'a'.repeat(64),
expectedRevision: 0,
extensions: [pin],
};
const control = (operation: Record<string, unknown>) =>
post(origin, '/internal/managed-runtime/v3/extensions', {
protocolVersion: 1,
sessionKey: {
tenantId: BOOT.tenantId,
workspaceId: BOOT.workspaceId,
sessionId: '550e8400-e29b-41d4-a716-446655440201',
},
contextBinding: installed.binding,
operation,
});
const prepared = await control({
kind: 'extension-prepare',
operationId,
command,
expectedRevision: 0,
extensions: [pin],
baseMcpServers: [],
baseHookCatalog: null,
settings: { one: { SELECTED_SECRET: 'owned-secret' } },
toolProfile: 'hosted-workspace-shell/1',
packageManifests: { one: compiled.manifest },
});
expect(prepared.status).toBe(200);
expect(JSON.stringify(await prepared.json())).not.toContain(
'owned-secret',
);
expect(
(
await control({
kind: 'extension-activate',
operationId,
expectedRevision: 0,
revision: 1,
})
).status,
).toBe(200);
expect((await post(origin, CONTEXT, installed)).status).toBe(200);
expect(
(await post(origin, ACTIVATION, workspaceActivation(installed))).status,
).toBe(200);
const globalHistory = vi
.spyOn(Storage, 'getGlobalQwenDir')
.mockReturnValue(path.join(cache, 'global-history'));
onTestFinished(() => globalHistory.mockRestore());
const historyBound = await post(
origin,
MANAGED_RUNTIME_PROVIDER_ROUTE.path,
{
protocolVersion: 1,
providerProtocol: MANAGED_RUNTIME_PROVIDER_PROTOCOL,
session: {
harnessSessionId: '550e8400-e29b-41d4-a716-446655440201',
runtimeSessionId: '550e8400-e29b-41d4-a716-446655440101',
turnKind: 'continuation',
},
operation: { kind: 'raw-file-history', action: 'bind', state: null },
},
);
expect(historyBound.status).toBe(200);
const config = {
revision: 1,
packages: [{ pin, assetRoot: 'extension://one/' }],
};
const call = async (
id: string,
toolName: string,
input: Record<string, unknown>,
) =>
post(origin, EXECUTE, {
...shell('550e8400-e29b-41d4-a716-446655440101', id, ''),
toolName,
input: extensionToolInput(
'550e8400-e29b-41d4-a716-446655440201',
config,
toolName,
input,
),
});
const selector = {
extensionId: pin.extensionId,
packageRevision: pin.packageRevision,
packageDigest: pin.packageDigest,
};
const read = await call('extension-read', 'read_file', {
file_path: 'resource.txt',
extension_source: selector,
});
expect(read.status).toBe(200);
expect(JSON.stringify(await read.json())).toContain(
'private package resource',
);
expect(fs.existsSync(path.join(root, 'services/api/resource.txt'))).toBe(
false,
);
fs.writeFileSync(
path.join(root, 'services/api/workspace-only.txt'),
'workspace',
);
const globbed = await call('extension-glob', 'glob', {
pattern: '*.txt',
extension_source: selector,
});
expect(globbed.status).toBe(200);
const globBody = JSON.stringify(await globbed.json());
expect(globBody).toContain('resource.txt');
expect(globBody).not.toContain('workspace-only.txt');
const writeAttempt = await post(origin, EXECUTE, {
...shell('550e8400-e29b-41d4-a716-446655440101', 'extension-write', ''),
toolName: 'write_file',
input: {
file_path: 'resource.txt',
content: 'must not write',
managedExtension: {
harnessSessionId: '550e8400-e29b-41d4-a716-446655440201',
configRevision: 1,
pin,
},
},
});
expect(writeAttempt.status).toBe(500);
const readAgain = await call('extension-read-again', 'read_file', {
file_path: 'resource.txt',
extension_source: selector,
});
expect(JSON.stringify(await readAgain.json())).toContain(
'private package resource',
);
const escaped = await call('extension-escape', 'read_file', {
file_path: '../../../etc/passwd',
extension_source: selector,
});
expect(JSON.stringify(await escaped.json())).not.toContain('root:');
const shellResponse = await call('extension-shell', 'run_shell_command', {
command:
'printf "%s|%s|%s" "$SELECTED_SECRET" "${OTHER_EXTENSION_SECRET-unset}" "$PWD"',
extension_context: selector,
});
expect(shellResponse.status).toBe(200);
const shellBody = JSON.stringify(await shellResponse.json());
expect(shellBody).toContain('owned-secret|unset|');
expect(shellBody).toContain(realDirectory(root, 'services/api'));
const sibling = workspaceInstallation(
'550e8400-e29b-41d4-a716-446655440102',
'services/web',
);
await post(origin, CONTEXT, sibling);
await post(origin, ACTIVATION, workspaceActivation(sibling));
const crossScope = await post(origin, EXECUTE, {
...shell('550e8400-e29b-41d4-a716-446655440102', 'cross-extension', ''),
toolName: 'read_file',
input: extensionToolInput(
'550e8400-e29b-41d4-a716-446655440201',
config,
'read_file',
{
file_path: 'resource.txt',
extension_source: selector,
},
),
});
expect(crossScope.status).toBe(500);
},
);
it('answers ready v2 and serves exactly the boot v2 routes', async () => {
const worker = await startManagedRuntimeAttestationWorker(BOOT);
openWorkers.add(worker);
@@ -3251,3 +3520,68 @@ describe('selectShellCapturePublisher', () => {
expect(prepared.publisher).toBe(remotePublishers);
});
});
it.skipIf(!['darwin', 'linux'].includes(process.platform))(
'admits extension command control through the negotiated strict worker gate',
async () => {
const mountRoot = workspace();
const cacheDirectory = fs.mkdtempSync(
path.join(os.tmpdir(), 'review-extension-route-'),
);
onTestFinished(() =>
fs.rmSync(cacheDirectory, { recursive: true, force: true }),
);
const origin = await startWorker({
...BOOT,
mountRoot,
capabilityDigest:
process.platform === 'linux'
? MANAGED_EXTENSION_LINUX_CAPABILITY_DIGEST
: MANAGED_EXTENSION_DEVELOPER_CAPABILITY_DIGEST,
managedExtensions: {
enabled: true,
mode:
process.platform === 'linux'
? 'linux-cgroup'
: 'developer-process-group',
cacheDirectory,
...(process.platform === 'linux'
? { cgroupRoot: path.join(cacheDirectory, 'cgroup') }
: {}),
},
});
const observations = [];
for (const action of ['execute', 'status', 'cancel']) {
const response = await post(
origin,
'/internal/managed-runtime/v3/extensions/commands',
{
protocolVersion: 1,
runtimeSessionId: 'test-runtime-session',
operation: { kind: 'extension-command', action },
},
);
observations.push({
action,
status: response.status,
body: await response.text(),
});
}
for (const entry of observations) {
expect(entry.status).toBe(409);
expect(JSON.parse(entry.body)).toEqual({
code: 'managed_extension_command_refused',
});
}
const legacy = await startWorker({ ...BOOT, mountRoot });
expect(
(
await post(
legacy,
'/internal/managed-runtime/v3/extensions/commands',
{},
)
).status,
).toBe(404);
},
);
+507 -17
View File
@@ -5,10 +5,50 @@
*/
import { createHash } from 'node:crypto';
import { isDeepStrictEqual } from 'node:util';
import { preflightManagedExtensionPackage } from './managed-extension-preflight.js';
import { managedExtensionEnvironment } from './managed-extension-environment.js';
import { CSI_FILES_RETIREMENT_CAPABILITY_DIGEST } from './managed-csi-file-profile.js';
import { constants, promises as fs, type BigIntStats } from 'node:fs';
import {
compileManagedExtensionPackage,
type ManagedPackageContributions,
} from '@qwen-code/qwen-code-core/extension/managed-package.js';
import {
MANAGED_EXTENSION_RETIRE_ROUTE,
ManagedExtensionRuntime,
registerManagedExtensionRuntimeRoutes,
} from './managed-extension-runtime.js';
import { runManagedExtensionShell } from './managed-extension-shell.js';
import {
ManagedExtensionCommandRuntime,
registerManagedExtensionCommandRoute,
} from './managed-extension-command-runtime.js';
import { parseManagedExtensionPin } from '@qwen-code/qwen-code-core/managed-runtime/managed-extension-package.js';
import {
type ManagedToolSet,
type ManagedToolSetResolver,
createManagedToolSet,
ManagedToolExecutor,
realpathDeepestExisting,
type ManagedShellCapturePublisher,
} from './managed-runtime-tool-executor.js';
import { ManagedExtensionProcessOwners } from './managed-extension-process.js';
import { stageManagedExtensionManifests } from './managed-extension-manifests.js';
import {
constants,
promises as fs,
mkdirSync,
type BigIntStats,
} from 'node:fs';
import { execFileSync } from 'node:child_process';
import path from 'node:path';
import { sessionIdContext } from '@qwen-code/qwen-code-core/utils/sessionIdContext.js';
import {
computeManagedContextDigest,
normalizeWorkspaceRelativePath,
type ManagedContextBinding,
} from './managed-workspace-binding.js';
import type express from 'express';
import type { Application, Response } from 'express';
import {
@@ -26,16 +66,10 @@ import {
type ManagedContextBoot,
type ManagedContextOutcome,
} from './managed-context-envelope.js';
import {
createManagedToolSet,
ManagedToolExecutor,
realpathDeepestExisting,
type ManagedShellCapturePublisher,
} from './managed-runtime-tool-executor.js';
import { ManagedChildRunSupervisor } from '@qwen-code/qwen-code-core/managed-runtime/managed-child-run-supervisor.js';
import { RemoteShellResultPublisher } from './remote-shell-result-publication.js';
import type { ManagedShellPublisherRegistry } from './managed-shell-publisher.js';
import type { ManagedRuntimeLedger } from './managed-runtime-ledger.js';
import { ManagedRuntimeLedger } from './managed-runtime-ledger.js';
import { registerManagedRuntimeToolRoutes } from './managed-runtime-tool-routes.js';
import { registerManagedRuntimeToolV3Routes } from './managed-runtime-tool-v3-routes.js';
import {
@@ -296,13 +330,64 @@ export function registerManagedContextRoutes(
if (boot.capabilityDigest === CSI_FILES_RETIREMENT_CAPABILITY_DIGEST)
throw new Error('Private CSI file profile requires its dedicated worker.');
const installations = new ManagedContextInstallations(boot);
let extensionRuntime: ManagedExtensionRuntime | undefined;
let extensionToolSet:
| ((
reference: Parameters<ManagedToolSetResolver>[0],
invocation: NonNullable<Parameters<ManagedToolSetResolver>[1]>,
tools: ManagedToolSet,
) => Promise<ManagedToolSet>)
| undefined;
const extensionConfiguration = boot.managedExtensions;
const processDirectory = extensionConfiguration
? path.join(
extensionConfiguration.cacheDirectory,
'owners',
createHash('sha256')
.update(
`${boot.tenantId}\0${boot.workspaceId}\0${boot.runtimeInstanceId}\0${boot.epoch}`,
)
.digest('hex'),
)
: undefined;
if (extensionConfiguration && !ledger && processDirectory) {
mkdirSync(processDirectory, { recursive: true, mode: 0o700 });
const pgid = Number(
execFileSync('/bin/ps', ['-o', 'pgid=', '-p', String(process.pid)], {
encoding: 'utf8',
}).trim(),
);
if (!Number.isSafeInteger(pgid) || pgid < 2)
throw new Error('Worker process group is unavailable.');
ledger = ManagedRuntimeLedger.create({
workFile: path.join(processDirectory, 'ledger.json'),
worker: {
pid: process.pid,
pgid,
groupOwned: false,
incarnation: boot.runtimeIncarnation,
startedAt: Date.now(),
},
});
}
const extensionProcesses = extensionConfiguration
? new ManagedExtensionProcessOwners(extensionConfiguration.mode, {
ledger,
cgroupRoot: extensionConfiguration.cgroupRoot,
directory: processDirectory,
})
: undefined;
const foregroundOnly = sshForegroundOnly && !extensionConfiguration;
const activations = new WorkspaceActivations();
const resident =
boot.capabilityDigest === RESIDENT_WORKSPACE_CAPABILITY_DIGEST;
if (resident && boot.isolationClass !== 'workspace')
throw new Error('Resident Workspace requires Workspace isolation.');
const requiresActivation =
boot.capabilityDigest === WORKSPACE_CAPABILITY_DIGEST || resident;
boot.capabilityDigest === WORKSPACE_CAPABILITY_DIGEST ||
resident ||
extensionConfiguration !== undefined;
const admissionOpen = (): boolean => executor.isAdmissionOpen;
const remotePublisher =
!capturePublisher && requiresActivation && !resident
@@ -345,7 +430,7 @@ export function registerManagedContextRoutes(
handleManagedRuntimeJsonError,
);
const mcp = sshForegroundOnly
const mcp = foregroundOnly
? undefined
: new ManagedMcpRuntime(
boot,
@@ -355,7 +440,8 @@ export function registerManagedContextRoutes(
!mount.isAvailable ||
resident ||
!requiresActivation ||
!activations.isActive(runtimeSessionId)
!activations.isActive(runtimeSessionId) ||
extensionRuntime?.isChanging(runtimeSessionId) === true
)
return undefined;
const binding = installations.installed(runtimeSessionId);
@@ -370,9 +456,10 @@ export function registerManagedContextRoutes(
loadManagedMcpManifest(
resident ? undefined : process.env['QWEN_MANAGED_MCP_CONFIG'],
),
extensionProcesses,
);
if (mcp) registerManagedMcpRoutes(app, boot, mcp);
const hooks = sshForegroundOnly
const hooks = foregroundOnly
? undefined
: new ManagedHookRuntime(
boot,
@@ -382,7 +469,8 @@ export function registerManagedContextRoutes(
!mount.isAvailable ||
resident ||
!requiresActivation ||
!activations.isActive(runtimeSessionId)
!activations.isActive(runtimeSessionId) ||
extensionRuntime?.isChanging(runtimeSessionId) === true
)
return undefined;
const binding = installations.installed(runtimeSessionId);
@@ -397,6 +485,7 @@ export function registerManagedContextRoutes(
loadManagedHookManifest(
resident ? undefined : process.env['QWEN_MANAGED_HOOK_CONFIG'],
),
extensionProcesses,
);
if (hooks) registerManagedHookRoutes(app, boot, hooks);
// H3 background Shells share the delegation the Hook commands already use;
@@ -406,7 +495,7 @@ export function registerManagedContextRoutes(
? undefined
: process.env['QWEN_MANAGED_HOOK_CGROUP_ROOT'];
const backgroundSupervisor =
!sshForegroundOnly && cgroupRoot
!foregroundOnly && cgroupRoot
? ManagedChildRunSupervisor.create({ cgroupRoot })
: undefined;
const backgroundRegistry = new ManagedBackgroundShellRegistry();
@@ -414,8 +503,9 @@ export function registerManagedContextRoutes(
// private second instance could only ever answer unknown.
const monitorRegistry = new ManagedMonitorRegistry();
const executor: ManagedToolExecutor = new ManagedToolExecutor(
async (reference) => {
async (reference, invocation) => {
const isActive = () =>
extensionRuntime?.isChanging(reference.sessionId) !== true &&
admissionOpen() &&
mount.isAvailable &&
(!requiresActivation || activations.isActive(reference.sessionId));
@@ -434,13 +524,21 @@ export function registerManagedContextRoutes(
boot.runtimeInstanceId,
reference.sessionId,
);
const toolSet = sessionIdContext.run(sessionId, () =>
let toolSet = sessionIdContext.run(sessionId, () =>
createManagedToolSet(
directory,
sessionId,
requiresActivation ? boot.mountRoot : directory,
),
);
if (invocation) {
if (!extensionToolSet)
throw new Error('Extension tool context is unavailable.');
toolSet = await extensionToolSet(reference, invocation, {
...toolSet,
isActive,
});
}
return {
...toolSet,
tools: resident
@@ -450,7 +548,7 @@ export function registerManagedContextRoutes(
),
)
: toolSet.tools,
isActive,
isActive: toolSet.isActive ?? isActive,
};
},
publisher,
@@ -562,6 +660,7 @@ export function registerManagedContextRoutes(
);
}
const isActive = () =>
extensionRuntime?.isChanging(sessionId) !== true &&
!executor.isAdmissionSealed &&
mount.isAvailable &&
activations.isActive(sessionId);
@@ -579,6 +678,397 @@ export function registerManagedContextRoutes(
},
sshForegroundOnly,
);
if (extensionConfiguration && mcp && hooks && extensionProcesses) {
const configurationDirectory = path.join(
extensionConfiguration.cacheDirectory,
createHash('sha256')
.update(
`${boot.tenantId}\0${boot.workspaceId}\0${boot.workspaceGeneration}`,
)
.digest('hex'),
);
const relativeCache = path.relative(
path.resolve(boot.mountRoot),
configurationDirectory,
);
if (!relativeCache.startsWith('..') && !path.isAbsolute(relativeCache))
throw new Error('Extension cache must stay outside the Workspace.');
const contexts = new Map<string, ManagedContextBinding>();
const hydrated = new Map<
string,
{
contributions: ManagedPackageContributions;
environment: Readonly<Record<string, string>>;
}
>();
const staged = new Map<
string,
ReturnType<typeof stageManagedExtensionManifests>
>();
extensionRuntime = new ManagedExtensionRuntime({
directory: configurationDirectory,
executionMode: extensionConfiguration.mode,
ownerJournalId: extensionProcesses.journalId,
bindContext: async (sessionId, value) => {
const binding = value as ManagedContextBinding;
computeManagedContextDigest(binding);
if (
binding.tenantId !== boot.tenantId ||
binding.workspaceId !== boot.workspaceId ||
binding.workspaceGeneration !== boot.workspaceGeneration ||
binding.storageId !== boot.storageId ||
!(await mount.resolve(binding.cwdRelative))
)
throw new Error('Extension context scope conflicts.');
const existing = contexts.get(sessionId);
if (
existing &&
computeManagedContextDigest(existing) !==
computeManagedContextDigest(binding)
)
throw new Error('Extension context changed.');
contexts.set(sessionId, { ...binding });
},
admitted: async (sessionId) => {
const binding = contexts.get(sessionId);
return (
admissionOpen() &&
binding !== undefined &&
(await mount.resolve(binding.cwdRelative)) !== undefined
);
},
idle: (sessionId) =>
!mcp.hasExtensionPendingOperations(sessionId) &&
!hooks.hasExtensionHolds(sessionId) &&
executor.isSessionConfigurationIdle(
extensionRuntime?.runtimeSessionId(sessionId) ?? sessionId,
),
compile: async (
directory,
_pin,
environment,
sessionId,
publishedManifest,
) => {
const binding = contexts.get(sessionId);
const workspaceDirectory =
binding && (await mount.resolve(binding.cwdRelative));
if (
!workspaceDirectory ||
!['linux', 'darwin'].includes(process.platform) ||
!['x64', 'arm64'].includes(process.arch)
)
throw new Error('Extension target is unavailable.');
const target = {
platform: process.platform as 'linux' | 'darwin',
arch: process.arch as 'x64' | 'arm64',
};
const raw = await compileManagedExtensionPackage(directory, {
target,
workspaceDirectory,
resolveRecipes: false,
});
if (
Buffer.byteLength(JSON.stringify(raw.contributions)) >
2 * 1024 * 1024
)
throw new Error('Extension contributions exceed their size limit.');
if (
publishedManifest === undefined ||
!isDeepStrictEqual(raw.manifest, publishedManifest)
)
throw new Error('Extension published manifest conflicts.');
const resolved = await compileManagedExtensionPackage(directory, {
target,
workspaceDirectory,
environment,
});
await preflightManagedExtensionPackage(
resolved.contributions,
directory,
workspaceDirectory,
managedExtensionEnvironment(
process.env,
workspaceDirectory,
environment,
),
);
hydrated.set(directory, {
contributions: resolved.contributions,
environment,
});
return raw.contributions;
},
stage: async (sessionId, command, packages, base) => {
await extensionProcesses.coverSession(
sessionId,
command.expectedRevision + 1,
);
const manifests = stageManagedExtensionManifests({
tenantId: boot.tenantId,
workspaceId: boot.workspaceId,
runtimeSessionId: sessionId,
command,
packages,
...base,
mcp,
hooks,
hydrated: (directory) => {
const value = hydrated.get(directory);
if (!value) throw new Error('Extension recipes are unavailable.');
return value;
},
});
staged.set(`${sessionId}\0${command.operationId}`, manifests);
return manifests;
},
drain: async (sessionId, command) => {
const next = staged.get(`${sessionId}\0${command.operationId}`);
if (!next) throw new Error('Extension configuration is not staged.');
const retained = mcp.retainedExtensionOwnerIds(
sessionId,
next.mcpManifest,
);
await mcp.drainExtensionSession(sessionId, next.mcpManifest);
if (hooks.hasExtensionHolds(sessionId))
throw new Error('Original extension Hook outcome is unknown.');
const receipts = await extensionProcesses.stopSession(
sessionId,
retained,
);
if (receipts.some((receipt) => receipt.state !== 'stopped'))
throw new Error('Original extension process stop is unknown.');
},
reconcile: async (sessionId) => {
await mcp.drainExtensionSession(sessionId);
if (hooks.hasExtensionHolds(sessionId))
throw new Error('Original Hook execution outcome is unknown.');
return extensionProcesses.stopSession(sessionId);
},
install: async (sessionId, _packages, command) => {
const manifests = staged.get(`${sessionId}\0${command.operationId}`);
if (!manifests)
throw new Error('Extension configuration is not prepared.');
const retained = mcp.retainedExtensionOwnerIds(
sessionId,
manifests.mcpManifest,
);
if (await extensionProcesses.hasLiveSessionOwners(sessionId, retained))
throw new Error('Original extension owners require reconciliation.');
mcp.installSessionDefinitions(sessionId, manifests.mcpManifest);
hooks.installSessionManifest(sessionId, manifests.hookManifest);
},
});
executor.attachExtensionLifecycle({
mode: extensionConfiguration.mode,
drain: async () => {
if (
mcp.hasUnsettledExtensionOperations() ||
hooks.hasUnsettledExtensionOperations()
)
throw new Error('Original extension operations are unsettled.');
const receipts = await extensionProcesses.stopAll();
if (receipts.some((entry) => entry.state !== 'stopped'))
throw new Error('Original extension owners are not stopped.');
},
});
app.post(
MANAGED_EXTENSION_RETIRE_ROUTE.path,
managedRuntimeNoStore,
authorizeManagedRuntime(boot),
managedRuntimeJsonBody(4096),
async (req, res) => {
try {
const body = req.body as Record<string, unknown>;
if (
!body ||
Object.keys(body).sort().join(',') !==
'protocolVersion,retirementId' ||
body['protocolVersion'] !== 1 ||
typeof body['retirementId'] !== 'string'
)
throw new Error('Invalid extension retirement.');
await executor.retireSsh(body['retirementId']);
res.json({
protocolVersion: 1,
retirementId: body['retirementId'],
executionMode: extensionConfiguration.mode,
scope:
extensionConfiguration.mode === 'linux-cgroup'
? 'process_tree'
: 'process_group',
state: 'stopped',
runtimeInstanceId: boot.runtimeInstanceId,
runtimeIncarnation: boot.runtimeIncarnation,
leaseId: boot.leaseId,
epoch: boot.epoch,
});
} catch {
res.status(409).json({ code: 'managed_extension_stop_unverified' });
}
},
);
extensionToolSet = async (reference, invocation, original) => {
const value = invocation.input['managedExtension'];
if (!value || typeof value !== 'object' || Array.isArray(value))
throw new Error('Invalid extension tool context.');
const context = value as Record<string, unknown>;
if (
Object.keys(context).sort().join(',') !==
'configRevision,harnessSessionId,pin' ||
typeof context['harnessSessionId'] !== 'string' ||
!Number.isSafeInteger(context['configRevision']) ||
Number(context['configRevision']) < 1
)
throw new Error('Invalid extension tool context.');
const logicalId = context['harnessSessionId'];
const installed = installations.installed(reference.sessionId);
const logical = contexts.get(logicalId);
if (
!installed ||
!logical ||
computeManagedContextDigest(installed) !==
computeManagedContextDigest(logical)
)
throw new Error('Extension tool Session binding conflicts.');
const runtime = extensionRuntime!;
const selected = await runtime.getToolContext(
logicalId,
Number(context['configRevision']),
parseManagedExtensionPin(context['pin']),
);
const active = () =>
original.isActive?.() !== false && !runtime.isChanging(logicalId);
if (
invocation.toolName === 'read_file' ||
invocation.toolName === 'glob'
) {
const assetTools = createManagedToolSet(
selected.assetRoot,
runtimeSessionKey(
boot.runtimeInstanceId,
logicalId +
'/' +
parseManagedExtensionPin(context['pin']).extensionId,
),
selected.assetRoot,
);
return {
...assetTools,
extensionAssets: true,
tools: new Map(
[...assetTools.tools].filter(
([name]) => name === invocation.toolName,
),
),
isActive: active,
};
}
if (invocation.toolName !== 'run_shell_command')
throw new Error('Extension assets are read-only.');
return {
...original,
isActive: active,
runShell: (input, signal, capture) =>
runManagedExtensionShell(extensionProcesses, {
sessionId: logicalId,
configRevision: Number(context['configRevision']),
command: String(input['command']),
cwd: original.directory!,
environment: managedExtensionEnvironment(
process.env,
original.directory!,
selected.environment,
),
signal,
capture,
timeoutMs:
typeof input['timeout'] === 'number'
? input['timeout']
: undefined,
}),
};
};
registerManagedExtensionRuntimeRoutes(app, boot, extensionRuntime);
registerManagedExtensionCommandRoute(
app,
boot,
new ManagedExtensionCommandRuntime({
directory: path.join(configurationDirectory, 'command-receipts'),
tenantId: boot.tenantId,
workspaceId: boot.workspaceId,
workspaceGeneration: boot.workspaceGeneration,
prepareInput: async (runtimeSessionId, operation) => {
const logicalId = operation.grant.sessionKey.sessionId;
const installed = installations.installed(runtimeSessionId);
const logical = contexts.get(logicalId);
if (
!admissionOpen() ||
!installed ||
!logical ||
(requiresActivation && !activations.isActive(runtimeSessionId)) ||
computeManagedContextDigest(installed) !==
computeManagedContextDigest(logical)
)
throw new Error('Command Session context conflicts.');
const runtime = extensionRuntime!;
const active = await runtime.status(logicalId);
const pin = active.extensions.find(
(entry) => entry.extensionId === operation.extensionId,
);
if (
active.state !== 'active' ||
active.pendingOperationId ||
active.revision !== operation.revision ||
!pin
)
throw new Error('Command configuration is unavailable.');
await runtime.getToolContext(logicalId, operation.revision, pin);
const input = structuredClone(operation.input);
const context = {
harnessSessionId: logicalId,
configRevision: operation.revision,
pin,
};
if (operation.toolName === 'shell') {
if (
Object.keys(input).some(
(key) => !['command', 'timeout'].includes(key),
) ||
typeof input['command'] !== 'string'
)
throw new Error('Invalid command Shell input.');
return { ...input, managedExtension: context };
}
if (
Object.keys(input).some(
(key) => !['file_path', 'offset', 'limit', 'pages'].includes(key),
) ||
typeof input['file_path'] !== 'string'
)
throw new Error('Invalid command file input.');
const prefix = `extension://${operation.extensionId}/`;
if (input['file_path'].startsWith(prefix)) {
input['file_path'] = normalizeWorkspaceRelativePath(
input['file_path'].slice(prefix.length),
);
return { ...input, managedExtension: context };
}
if (input['file_path'].startsWith('extension://'))
throw new Error('Command asset belongs to another extension.');
input['file_path'] = normalizeWorkspaceRelativePath(
input['file_path'],
);
return input;
},
execute: (reference, toolName, input) =>
executor.execute(reference, toolName, input),
cancel: (reference) => executor.cancel(reference),
}),
);
}
activations.register(app, boot, installations, executor);
registerManagedRuntimeToolRoutes(app, boot, executor);
if (publisher) {
@@ -0,0 +1,136 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { createHash } from 'node:crypto';
import { promises as fs } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { Readable } from 'node:stream';
import * as tar from 'tar';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { ManagedExtensionCache } from './managed-extension-cache.js';
let temporary: string;
beforeEach(async () => {
temporary = await fs.mkdtemp(
path.join(os.tmpdir(), 'managed-package-cache-'),
);
});
afterEach(async () => {
await fs.rm(temporary, { force: true, recursive: true });
});
async function archive(link = false) {
const source = path.join(temporary, 'source');
await fs.mkdir(source);
await fs.writeFile(
path.join(source, 'qwen-extension.json'),
'{"name":"test"}',
);
await fs.mkdir(path.join(source, 'skills'));
await fs.writeFile(path.join(source, 'skills', 'script.sh'), 'echo test');
if (link) await fs.symlink('/etc/passwd', path.join(source, 'outside'));
const file = path.join(temporary, 'package.tar.gz');
await tar.c({ cwd: source, file, gzip: true }, ['.']);
const bytes = await fs.readFile(file);
return { bytes, digest: createHash('sha256').update(bytes).digest('hex') };
}
describe('ManagedExtensionCache', () => {
it('retains full package resources and never replaces another materialization', async () => {
const { bytes, digest } = await archive();
const cache = new ManagedExtensionCache(path.join(temporary, 'cache'));
await cache.upload(digest, Readable.from(bytes));
const first = await cache.materialize(digest);
expect(
await fs.readFile(path.join(first, 'skills/script.sh'), 'utf8'),
).toBe('echo test');
const [concurrentA, concurrentB] = await Promise.all([
cache.materialize(digest),
cache.materialize(digest),
]);
expect(concurrentA).not.toBe(concurrentB);
const second = await cache.materialize(digest);
expect(second).not.toBe(first);
expect(
await fs.readFile(path.join(first, 'skills/script.sh'), 'utf8'),
).toBe('echo test');
});
it('repairs missing or drifted assets at the identical path only from a verified archive', async () => {
const { bytes, digest } = await archive();
const cache = new ManagedExtensionCache(path.join(temporary, 'cache'));
await cache.upload(digest, Readable.from(bytes));
const pinned = await cache.materialize(digest);
await fs.rm(pinned, { recursive: true });
await cache.restoreMaterialized(digest, pinned);
await expect(
cache.verifyMaterialized(digest, pinned),
).resolves.toBeUndefined();
await fs.writeFile(path.join(pinned, 'skills/script.sh'), 'changed');
await expect(cache.verifyMaterialized(digest, pinned)).rejects.toThrow(
'drifted',
);
await cache.restoreMaterialized(digest, pinned);
expect(
await fs.readFile(path.join(pinned, 'skills/script.sh'), 'utf8'),
).toBe('echo test');
const sibling = await cache.materialize(digest);
await fs.writeFile(path.join(pinned, 'skills/script.sh'), 'changed again');
await fs.writeFile(
path.join(cache.directory, `${digest}.tar.gz`),
'corrupt',
);
await expect(cache.restoreMaterialized(digest, pinned)).rejects.toThrow(
'digest',
);
expect(
await fs.readFile(path.join(pinned, 'skills/script.sh'), 'utf8'),
).toBe('changed again');
await expect(
cache.verifyMaterialized(digest, sibling),
).resolves.toBeUndefined();
const outside = path.join(temporary, `${digest}-${'a'.repeat(36)}`);
await fs.mkdir(outside);
await expect(cache.restoreMaterialized(digest, outside)).rejects.toThrow(
'scope',
);
expect(await fs.stat(outside)).toBeDefined();
});
it('rejects corrupt uploads without leaving a cache artifact', async () => {
const { bytes } = await archive();
const cachePath = path.join(temporary, 'cache');
const cache = new ManagedExtensionCache(cachePath);
await expect(
cache.upload('0'.repeat(64), Readable.from(bytes)),
).rejects.toThrow('digest');
expect(await fs.readdir(cachePath)).toEqual([]);
});
it('rejects links before materializing a directory', async () => {
const { bytes, digest } = await archive(true);
const cachePath = path.join(temporary, 'cache');
const cache = new ManagedExtensionCache(cachePath);
await expect(cache.upload(digest, Readable.from(bytes))).rejects.toThrow();
expect(await fs.readdir(cachePath)).toEqual([]);
});
it('cleans up an interrupted stream', async () => {
const cachePath = path.join(temporary, 'cache');
const cache = new ManagedExtensionCache(cachePath);
const stream = Readable.from(
(async function* () {
yield Buffer.from('partial');
throw new Error('disconnected');
})(),
);
await expect(cache.upload('0'.repeat(64), stream)).rejects.toThrow(
'disconnected',
);
expect(await fs.readdir(cachePath)).toEqual([]);
});
});
@@ -0,0 +1,224 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { createHash, randomUUID } from 'node:crypto';
import { createReadStream, createWriteStream, promises as fs } from 'node:fs';
import path from 'node:path';
import type { Readable } from 'node:stream';
import { Transform } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import * as tar from 'tar';
import {
MAX_ARCHIVE_ENTRIES,
MAX_ARCHIVE_EXPANDED_BYTES,
MAX_ARCHIVE_PATH_BYTES,
} from '@qwen-code/qwen-code-core/extension/archive-safety.js';
export const MANAGED_EXTENSION_ARCHIVE_MAX_BYTES = 100 * 1024 * 1024;
const DIGEST = /^[0-9a-f]{64}$/u;
export class ManagedExtensionCache {
constructor(readonly directory: string) {
if (!path.isAbsolute(directory)) throw new Error('Invalid package cache.');
}
async upload(digest: string, source: Readable): Promise<void> {
this.assertDigest(digest);
await fs.mkdir(this.directory, { recursive: true, mode: 0o700 });
const temporary = path.join(this.directory, `.upload-${randomUUID()}`);
const hash = createHash('sha256');
let bytes = 0;
const limit = new Transform({
transform(chunk: Buffer, _encoding, callback) {
bytes += chunk.byteLength;
if (bytes > MANAGED_EXTENSION_ARCHIVE_MAX_BYTES) {
callback(new Error('Package archive exceeds its size limit.'));
return;
}
hash.update(chunk);
callback(null, chunk);
},
});
try {
await pipeline(
source,
limit,
createWriteStream(temporary, { flags: 'wx', mode: 0o600 }),
);
if (hash.digest('hex') !== digest)
throw new Error('Package archive digest mismatch.');
await this.validateArchive(temporary);
await fs.rename(temporary, this.archivePath(digest));
} finally {
await fs.rm(temporary, { force: true });
}
}
materialize(digest: string): Promise<string> {
this.assertDigest(digest);
return this.extract(digest);
}
private async extract(digest: string): Promise<string> {
const archive = this.archivePath(digest);
const hash = createHash('sha256');
for await (const chunk of createReadStream(archive)) hash.update(chunk);
if (hash.digest('hex') !== digest)
throw new Error('Cached package archive digest mismatch.');
await this.validateArchive(archive);
// Each materialization has its own directory: a caller never replaces a
// directory another Session has pinned.
const destination = path.join(this.directory, `${digest}-${randomUUID()}`);
await fs.mkdir(destination, { mode: 0o700 });
try {
await tar.x({
file: archive,
cwd: destination,
strict: true,
noChmod: true,
});
await fs.writeFile(
`${destination}.integrity`,
await this.treeDigest(destination),
{ mode: 0o600, flag: 'wx' },
);
return destination;
} catch (error) {
await fs.rm(destination, { recursive: true, force: true });
throw error;
}
}
async restoreMaterialized(digest: string, directory: string): Promise<void> {
this.assertDigest(digest);
if (
path.dirname(directory) !== this.directory ||
!new RegExp(`^${digest}-[0-9a-f-]{36}$`, 'u').test(
path.basename(directory),
) ||
!(await fs.lstat(this.directory)).isDirectory()
)
throw new Error('Extension cache scope conflicts.');
try {
await this.verifyMaterialized(digest, directory);
return;
} catch {
// Only explicit preparation repairs assets; ordinary tool reads only verify.
}
const fresh = await this.materialize(digest);
const quarantine = path.join(this.directory, `.quarantine-${randomUUID()}`);
const moveIfPresent = async (source: string, target: string) => {
try {
await fs.rename(source, target);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
}
};
try {
await moveIfPresent(directory, quarantine);
await moveIfPresent(`${directory}.integrity`, `${quarantine}.integrity`);
await fs.rename(fresh, directory);
await fs.rename(`${fresh}.integrity`, `${directory}.integrity`);
await this.verifyMaterialized(digest, directory);
await fs.rm(quarantine, { recursive: true, force: true });
await fs.rm(`${quarantine}.integrity`, { force: true });
} finally {
await fs.rm(fresh, { recursive: true, force: true });
await fs.rm(`${fresh}.integrity`, { force: true });
}
}
async verifyMaterialized(digest: string, directory: string): Promise<void> {
this.assertDigest(digest);
if (
path.dirname(directory) !== this.directory ||
!path.basename(directory).startsWith(`${digest}-`)
)
throw new Error('Extension cache scope conflicts.');
if (!(await fs.lstat(directory)).isDirectory())
throw new Error('Extension cache root is unsafe.');
const expected = await fs.readFile(`${directory}.integrity`, 'utf8');
if ((await this.treeDigest(directory)) !== expected)
throw new Error('Extension package assets drifted.');
}
private async treeDigest(directory: string): Promise<string> {
const hash = createHash('sha256');
const visit = async (relative: string): Promise<void> => {
const entries = await fs.readdir(path.join(directory, relative), {
withFileTypes: true,
});
for (const entry of entries.sort((a, b) =>
a.name.localeCompare(b.name, 'en'),
)) {
const child = path.posix.join(relative, entry.name);
if (!entry.isDirectory() && !entry.isFile())
throw new Error('Extension assets contain an unsafe entry.');
hash.update(
JSON.stringify([child, entry.isDirectory() ? 'directory' : 'file']),
);
if (entry.isDirectory()) await visit(child);
else {
const content = createHash('sha256');
for await (const chunk of createReadStream(
path.join(directory, child),
))
content.update(chunk);
hash.update(content.digest());
}
}
};
await visit('');
return hash.digest('hex');
}
private async validateArchive(file: string): Promise<void> {
let count = 0;
let bytes = 0;
let pathBytes = 0;
let invalid = false;
const seen = new Set<string>();
const source = createReadStream(file);
const parser = tar.t({
strict: true,
onReadEntry(entry) {
const normalized = path.posix.normalize(entry.path).replace(/\/$/u, '');
count++;
bytes += entry.size;
pathBytes += Buffer.byteLength(entry.path);
if (
!['File', 'Directory', 'OldFile', 'ContiguousFile'].includes(
entry.type,
) ||
entry.path.includes('\\') ||
entry.path.includes('\0') ||
path.posix.isAbsolute(entry.path) ||
/^[a-zA-Z]:/u.test(entry.path) ||
normalized === '..' ||
normalized.startsWith('../') ||
seen.has(normalized) ||
count > MAX_ARCHIVE_ENTRIES ||
bytes > MAX_ARCHIVE_EXPANDED_BYTES ||
pathBytes > MAX_ARCHIVE_PATH_BYTES
) {
invalid = true;
source.destroy(new Error('Unsafe package archive.'));
}
seen.add(normalized);
},
});
await pipeline(source, parser);
if (invalid) throw new Error('Unsafe package archive.');
}
private archivePath(digest: string): string {
return path.join(this.directory, `${digest}.tar.gz`);
}
private assertDigest(digest: string): void {
if (!DIGEST.test(digest)) throw new Error('Invalid package digest.');
}
}
@@ -0,0 +1,196 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { promises as fs } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { ManagedToolResultPayload } from './managed-runtime-tool-executor.js';
import {
ManagedExtensionCommandRuntime,
type ManagedExtensionCommand,
type ManagedExtensionCommandDependencies,
} from './managed-extension-command-runtime.js';
let directory: string;
beforeEach(async () => {
directory = await fs.mkdtemp(path.join(os.tmpdir(), 'managed-command-'));
});
afterEach(async () => {
await fs.rm(directory, { recursive: true, force: true });
});
function operation(): ManagedExtensionCommand {
const operationId = `command-step-${'a'.repeat(64)}`;
return {
kind: 'extension-command',
operationId,
action: 'execute',
toolName: 'shell',
input: { command: 'echo hello' },
revision: 1,
extensionId: 'ext_00000000-0000-4000-8000-000000000001',
grant: {
sessionKey: {
tenantId: 'tenant',
workspaceId: 'workspace',
sessionId: 'harness',
},
operationId,
domain: 'tool_stage',
operationRevision: 2,
ownerId: 'runtime-session',
workspaceGeneration: '1',
resourceScope: {
recordRef: {
resourceId: 'record',
kind: 'managed-tool_stage',
schemaVersion: 1,
byteLength: 2,
digest: 'b'.repeat(64),
},
phases: ['execute', 'status', 'cancel'],
},
leaseDurationMs: 60_000,
expiresAt: Date.now() + 60_000,
},
};
}
function dependencies(): ManagedExtensionCommandDependencies {
return {
directory,
tenantId: 'tenant',
workspaceId: 'workspace',
workspaceGeneration: '1',
prepareInput: vi.fn(async (_id, command) => command.input),
execute: vi.fn(
async (): Promise<ManagedToolResultPayload> => ({
executionStatus: 'success',
responseParts: [{ text: 'hello' }],
}),
),
cancel: vi.fn(),
};
}
describe('Managed extension command durable execution', () => {
it('persists a result before exposing settlement and never repeats a lost response', async () => {
const deps = dependencies();
const runtime = new ManagedExtensionCommandRuntime(deps);
const command = operation();
expect(await runtime.control('runtime-session', command)).toMatchObject({
state: 'running',
});
await vi.waitFor(async () => {
expect(
await runtime.control('runtime-session', {
...command,
action: 'status',
}),
).toMatchObject({
state: 'settled',
result: { executionStatus: 'success' },
});
});
const restarted = new ManagedExtensionCommandRuntime(deps);
expect(await restarted.control('runtime-session', command)).toMatchObject({
state: 'settled',
});
expect(deps.execute).toHaveBeenCalledTimes(1);
await expect(
restarted.control('runtime-session', {
...command,
input: { command: 'touch other' },
}),
).rejects.toThrow('identity');
});
it('holds a surviving intent on restart and refuses to redispatch it', async () => {
const deps = dependencies();
let finish!: (value: ManagedToolResultPayload) => void;
vi.mocked(deps.execute).mockImplementation(
async () =>
new Promise((resolve) => {
finish = resolve;
}),
);
const original = new ManagedExtensionCommandRuntime(deps);
const command = operation();
await original.control('runtime-session', command);
await vi.waitFor(() => expect(deps.execute).toHaveBeenCalledTimes(1));
const replacement = new ManagedExtensionCommandRuntime(deps);
expect(await replacement.control('runtime-session', command)).toMatchObject(
{ state: 'outcome_unknown' },
);
expect(
await replacement.control('runtime-session', {
...command,
action: 'cancel',
}),
).toMatchObject({ state: 'outcome_unknown' });
expect(deps.execute).toHaveBeenCalledTimes(1);
finish({ executionStatus: 'success', responseParts: [] });
await vi.waitFor(async () =>
expect(
await original.control('runtime-session', {
...command,
action: 'status',
}),
).toMatchObject({ state: 'settled' }),
);
});
it('never creates an execution during status or under another owner or expired grant', async () => {
const deps = dependencies();
const runtime = new ManagedExtensionCommandRuntime(deps);
const command = operation();
expect(
await runtime.control('runtime-session', {
...command,
action: 'status',
}),
).toMatchObject({ state: 'outcome_unknown' });
await expect(runtime.control('other', command)).rejects.toThrow('owner');
await expect(
runtime.control('runtime-session', {
...command,
grant: { ...command.grant, expiresAt: Date.now() - 1 },
}),
).rejects.toThrow();
expect(deps.execute).not.toHaveBeenCalled();
expect(await fs.readdir(directory)).toEqual([]);
});
it('cancels preparation before it can start a tool', async () => {
const deps = dependencies();
let finish!: (input: Record<string, unknown>) => void;
vi.mocked(deps.prepareInput).mockImplementation(
async () =>
new Promise((resolve) => {
finish = resolve;
}),
);
const runtime = new ManagedExtensionCommandRuntime(deps);
const command = operation();
await runtime.control('runtime-session', command);
await vi.waitFor(() => expect(deps.prepareInput).toHaveBeenCalledTimes(1));
await runtime.control('runtime-session', { ...command, action: 'cancel' });
finish(command.input);
await vi.waitFor(async () =>
expect(
await runtime.control('runtime-session', {
...command,
action: 'status',
}),
).toMatchObject({
state: 'settled',
result: { executionStatus: 'not_started' },
}),
);
expect(deps.execute).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,358 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { createHash } from 'node:crypto';
import { promises as fs } from 'node:fs';
import path from 'node:path';
import type { Application } from 'express';
import { atomicWriteJSON } from '@qwen-code/qwen-code-core/utils/atomicFileWrite.js';
import {
parseOperationGrant,
type OperationGrant,
} from '@qwen-code/qwen-code-core/managed-runtime/managed-extension-record.js';
import { ManagedOperationGrantGate } from '@qwen-code/qwen-code-core/managed-runtime/managed-operation-grant-gate.js';
import {
authorizeManagedRuntime,
managedRuntimeJsonBody,
managedRuntimeNoStore,
nameManagedRuntimeIncarnation,
type ManagedRuntimeRequestIdentity,
} from './managed-runtime-attestation-contract.js';
import {
ManagedMcpToolUnknownError,
type ManagedToolReference,
type ManagedToolResultPayload,
} from './managed-runtime-tool-executor.js';
export const MANAGED_EXTENSION_COMMAND_ROUTE = Object.freeze({
key: 'extension-command',
method: 'POST',
path: '/internal/managed-runtime/v3/extensions/commands',
});
export interface ManagedExtensionCommand {
readonly kind: 'extension-command';
readonly operationId: string;
readonly action: 'execute' | 'status' | 'cancel';
readonly grant: OperationGrant;
readonly toolName: 'read_file' | 'shell';
readonly input: Record<string, unknown>;
readonly revision: number;
readonly extensionId: string;
}
interface View {
readonly operationId: string;
readonly state: 'running' | 'settled' | 'outcome_unknown';
readonly result?: ManagedToolResultPayload;
}
interface Receipt {
readonly operationId: string;
readonly fingerprint: string;
readonly state: 'running' | 'settled';
readonly result?: ManagedToolResultPayload;
}
export interface ManagedExtensionCommandDependencies {
readonly directory: string;
readonly tenantId: string;
readonly workspaceId: string;
readonly workspaceGeneration: string;
readonly prepareInput: (
runtimeSessionId: string,
operation: ManagedExtensionCommand,
) => Promise<Record<string, unknown>>;
readonly execute: (
reference: ManagedToolReference,
toolName: string,
input: Record<string, unknown>,
) => Promise<ManagedToolResultPayload>;
readonly cancel: (reference: ManagedToolReference) => unknown;
}
/** An intent surviving its process is unknown. It can never dispatch twice. */
export class ManagedExtensionCommandRuntime {
private readonly gates = new ManagedOperationGrantGate();
private readonly locks = new Map<string, Promise<unknown>>();
private readonly running = new Map<
string,
{
reference: ManagedToolReference;
controller: AbortController;
done: Promise<void>;
}
>();
constructor(
private readonly dependencies: ManagedExtensionCommandDependencies,
) {
if (!path.isAbsolute(dependencies.directory))
throw new Error('Invalid command receipt directory.');
}
async control(runtimeSessionId: string, value: unknown): Promise<View> {
const operation = this.parse(runtimeSessionId, value);
const key = digest([operation.grant.sessionKey, operation.operationId]);
const previous = this.locks.get(key) ?? Promise.resolve();
const pending = previous
.catch(() => undefined)
.then(() => this.controlLocked(key, runtimeSessionId, operation));
this.locks.set(key, pending);
try {
return await pending;
} finally {
if (this.locks.get(key) === pending) this.locks.delete(key);
}
}
private async controlLocked(
key: string,
runtimeSessionId: string,
operation: ManagedExtensionCommand,
): Promise<View> {
const filename = path.join(this.dependencies.directory, `${key}.json`);
const fingerprint = digest({
runtimeSessionId,
harnessSessionId: operation.grant.sessionKey.sessionId,
operationId: operation.operationId,
toolName: operation.toolName,
input: operation.input,
revision: operation.revision,
extensionId: operation.extensionId,
});
const stored = await this.read(filename);
if (
stored &&
(stored.operationId !== operation.operationId ||
stored.fingerprint !== fingerprint)
)
throw new Error('Original extension command identity conflicts.');
if (stored?.state === 'settled')
return {
operationId: operation.operationId,
state: 'settled',
result: stored.result!,
};
const active = this.running.get(key);
if (operation.action === 'cancel') {
if (active) {
active.controller.abort();
if (active.reference.argsDigest)
await this.dependencies.cancel(active.reference);
}
return {
operationId: operation.operationId,
state: active ? 'running' : 'outcome_unknown',
};
}
if (stored || operation.action === 'status')
return {
operationId: operation.operationId,
state: active ? 'running' : 'outcome_unknown',
};
await fs.mkdir(this.dependencies.directory, {
recursive: true,
mode: 0o700,
});
await atomicWriteJSON(
filename,
{ operationId: operation.operationId, fingerprint, state: 'running' },
{ mode: 0o600, forceMode: true, noFollow: true },
);
const reference = {
sessionId: runtimeSessionId,
promptId: `extension-command:${operation.operationId}`,
callId: operation.operationId,
argsDigest: '',
};
const controller = new AbortController();
const work = Promise.resolve()
.then(async () => {
let started = false;
try {
const input = await this.dependencies.prepareInput(
runtimeSessionId,
operation,
);
controller.signal.throwIfAborted();
reference.argsDigest = createHash('sha256')
.update(JSON.stringify(input))
.digest('hex');
started = true;
const result = await this.dependencies.execute(
reference,
operation.toolName === 'shell' ? 'run_shell_command' : 'read_file',
input,
);
// Command expansion has the same bounded inline result contract as its persisted resource.
const bounded: ManagedToolResultPayload =
Buffer.byteLength(JSON.stringify(result)) <= 48 * 1024
? result
: {
executionStatus: 'error',
responseParts: [
{
text: 'Command preprocessing output exceeds the inline limit.',
},
],
};
await atomicWriteJSON(
filename,
{
operationId: operation.operationId,
fingerprint,
state: 'settled',
result: bounded,
},
{ mode: 0o600, forceMode: true, noFollow: true },
);
} catch (error) {
if (started || error instanceof ManagedMcpToolUnknownError) return;
await atomicWriteJSON(
filename,
{
operationId: operation.operationId,
fingerprint,
state: 'settled',
result: {
executionStatus: 'not_started',
responseParts: [
{ text: 'Extension command admission failed.' },
],
},
},
{ mode: 0o600, forceMode: true, noFollow: true },
);
}
})
.finally(() => this.running.delete(key));
this.running.set(key, { reference, controller, done: work });
void work.catch(() => undefined);
return { operationId: operation.operationId, state: 'running' };
}
private parse(
runtimeSessionId: string,
value: unknown,
): ManagedExtensionCommand {
if (!value || typeof value !== 'object' || Array.isArray(value))
throw new Error('Invalid command control.');
const op = value as Record<string, unknown>;
if (
Object.keys(op).sort().join(',') !==
'action,extensionId,grant,input,kind,operationId,revision,toolName' ||
op['kind'] !== 'extension-command' ||
!['execute', 'status', 'cancel'].includes(String(op['action'])) ||
typeof op['operationId'] !== 'string' ||
!/^command-step-[a-f0-9]{64}$/u.test(op['operationId']) ||
!['read_file', 'shell'].includes(String(op['toolName'])) ||
!op['input'] ||
typeof op['input'] !== 'object' ||
Array.isArray(op['input']) ||
!Number.isSafeInteger(op['revision']) ||
Number(op['revision']) < 1 ||
typeof op['extensionId'] !== 'string' ||
!/^ext_[a-f0-9-]{36}$/u.test(op['extensionId'])
)
throw new Error('Invalid command control.');
const grant = parseOperationGrant(op['grant']);
if (
grant.domain !== 'tool_stage' ||
grant.operationId !== op['operationId'] ||
grant.ownerId !== runtimeSessionId ||
grant.sessionKey.tenantId !== this.dependencies.tenantId ||
grant.sessionKey.workspaceId !== this.dependencies.workspaceId ||
grant.workspaceGeneration !== this.dependencies.workspaceGeneration
)
throw new Error('Command owner conflicts.');
this.gates.install(grant);
if (
!this.gates.admits(
grant.sessionKey,
grant.operationId,
String(op['action']),
Date.now(),
)
)
throw new Error('Command grant does not admit this action.');
return { ...op, grant } as unknown as ManagedExtensionCommand;
}
private async read(filename: string): Promise<Receipt | undefined> {
try {
const stat = await fs.lstat(filename);
if (!stat.isFile() || stat.size > 64 * 1024)
throw new Error('Invalid command receipt.');
const value = JSON.parse(await fs.readFile(filename, 'utf8')) as Receipt;
if (
!['running', 'settled'].includes(value.state) ||
typeof value.fingerprint !== 'string' ||
(value.state === 'settled' &&
(!value.result || !Array.isArray(value.result.responseParts)))
)
throw new Error('Invalid command receipt.');
return value;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
throw error;
}
}
}
export function registerManagedExtensionCommandRoute(
app: Application,
identity: ManagedRuntimeRequestIdentity & { runtimeIncarnation: string },
runtime: ManagedExtensionCommandRuntime,
): void {
app.post(
MANAGED_EXTENSION_COMMAND_ROUTE.path,
managedRuntimeNoStore,
authorizeManagedRuntime(identity),
nameManagedRuntimeIncarnation(identity),
managedRuntimeJsonBody(64 * 1024),
async (req, res) => {
try {
const body = req.body as Record<string, unknown>;
if (
!body ||
Object.keys(body).sort().join(',') !==
'operation,protocolVersion,runtimeSessionId' ||
body['protocolVersion'] !== 1 ||
typeof body['runtimeSessionId'] !== 'string'
)
throw new Error('Invalid command envelope.');
const result = await runtime.control(
body['runtimeSessionId'],
body['operation'],
);
res.json({
protocolVersion: 1,
runtimeSessionId: body['runtimeSessionId'],
result,
});
} catch {
res.status(409).json({ code: 'managed_extension_command_refused' });
}
},
);
}
function digest(value: unknown): string {
const canonical = (item: unknown): unknown =>
Array.isArray(item)
? item.map(canonical)
: item && typeof item === 'object'
? Object.fromEntries(
Object.entries(item)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, child]) => [key, canonical(child)]),
)
: item;
return createHash('sha256')
.update(JSON.stringify(canonical(value)))
.digest('hex');
}
@@ -0,0 +1,32 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { expect, it } from 'vitest';
import { managedExtensionEnvironment } from './managed-extension-environment.js';
it('inherits only execution essentials and the selected extension settings', () => {
const inherited = {
PATH: '/bin',
HOME: '/private/operator',
LANG: 'en_US.UTF-8',
QWEN_MANAGED_TOKEN: 'runtime-token',
OTHER_EXTENSION_SECRET: 'other-secret',
NODE_OPTIONS: '--require /private/preload.js',
};
expect(
managedExtensionEnvironment(inherited, '/workspace/session', {
SELECTED_SECRET: 'selected',
QWEN_EXTENSION_ROOT: '/private/package',
}),
).toEqual({
PATH: '/bin',
LANG: 'en_US.UTF-8',
HOME: '/workspace/session',
USERPROFILE: '/workspace/session',
SELECTED_SECRET: 'selected',
QWEN_EXTENSION_ROOT: '/private/package',
});
expect(inherited.HOME).toBe('/private/operator');
});
@@ -0,0 +1,32 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
export function managedExtensionEnvironment(
inherited: NodeJS.ProcessEnv,
directory: string,
extension: Readonly<Record<string, string>> = {},
): Record<string, string> {
const environment: Record<string, string> = {};
for (const key of [
'PATH',
'LANG',
'LC_ALL',
'LC_CTYPE',
'TZ',
'TMPDIR',
'TMP',
'TEMP',
]) {
const value = inherited[key];
if (value !== undefined) environment[key] = value;
}
return {
...environment,
HOME: directory,
USERPROFILE: directory,
...extension,
};
}
@@ -0,0 +1,7 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
export class ManagedExtensionExecutionUnknownError extends Error {}
@@ -0,0 +1,121 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type { HookRunner } from '@qwen-code/qwen-code-core/hooks/hookRunner.js';
import { resolveCommandHookTimeoutMs } from '@qwen-code/qwen-code-core/hooks/hook-timeout.js';
import type {
CommandHookConfig,
HookExecutionResult,
HookInput,
HookEventName,
} from '@qwen-code/qwen-code-core/hooks/types.js';
import type { ManagedExtensionProcessOwners } from './managed-extension-process.js';
export async function executeManagedExtensionCommandHook(
runner: HookRunner,
owners: ManagedExtensionProcessOwners,
sessionId: string,
configRevision: number,
config: CommandHookConfig,
input: HookInput,
signal: AbortSignal,
environment: NodeJS.ProcessEnv,
): Promise<HookExecutionResult> {
const startedAt = Date.now();
signal.throwIfAborted();
const { shellConfig, command, env } = runner.commandInvocation(
config,
input,
environment,
);
const { child, ownerId } = await owners.start({
sessionId,
configRevision,
executable: shellConfig.executable,
args: [...shellConfig.argsPrefix, command],
env,
cwd: input.cwd,
});
let stdout = '';
let stderr = '';
let overflow = false;
const capture = (chunk: Buffer, stream: 'stdout' | 'stderr') => {
if (stream === 'stdout') stdout += chunk.toString();
else stderr += chunk.toString();
if (
Buffer.byteLength(stdout) > 1024 * 1024 ||
Buffer.byteLength(stderr) > 1024 * 1024
) {
overflow = true;
stdout = stdout.slice(0, 1024 * 1024);
stderr = stderr.slice(0, 1024 * 1024);
void owners.stop(ownerId);
}
};
child.stdout?.on('data', (chunk: Buffer) => capture(chunk, 'stdout'));
child.stderr?.on('data', (chunk: Buffer) => capture(chunk, 'stderr'));
child.stdin?.on('error', () => undefined);
child.stdin?.end(JSON.stringify(input));
let timer: ReturnType<typeof setTimeout> | undefined;
let cancel: (() => void) | undefined;
try {
const exit = new Promise<'exited'>((resolve) => {
if (child.exitCode !== null || child.signalCode !== null)
resolve('exited');
else child.once('exit', () => resolve('exited'));
});
const cancelled = new Promise<'cancelled'>((resolve) => {
cancel = () => resolve('cancelled');
signal.addEventListener('abort', cancel, { once: true });
if (signal.aborted) cancel();
});
const timeout = new Promise<'timeout'>((resolve) => {
timer = setTimeout(
() => resolve('timeout'),
resolveCommandHookTimeoutMs(
config.timeout,
config.name ?? config.command,
),
);
});
const outcome = await Promise.race([exit, cancelled, timeout]);
const receipt = await owners.stop(ownerId);
const result: HookExecutionResult =
outcome === 'exited'
? runner.commandResult(
config,
input.hook_event_name as HookEventName,
stdout,
stderr,
child.exitCode,
Date.now() - startedAt,
)
: ({
hookConfig: config,
eventName: input.hook_event_name as HookEventName,
success: false,
outcome,
duration: Date.now() - startedAt,
stdout,
stderr,
} satisfies HookExecutionResult);
if (overflow) {
result.success = false;
result.outcome = 'non_blocking_error';
result.error = new Error('Hook output exceeds its size limit.');
}
return {
...result,
processReceipt: receipt,
...(receipt.scope === 'process_tree'
? { processTreeDrained: receipt.state === 'stopped' }
: {}),
};
} finally {
if (timer) clearTimeout(timer);
if (cancel) signal.removeEventListener('abort', cancel);
}
}
@@ -0,0 +1,431 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { randomUUID } from 'node:crypto';
import { promises as fs } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { expect, it, vi } from 'vitest';
import type { ManagedPackageContributions } from '@qwen-code/qwen-code-core/extension/managed-package.js';
import type { OperationGrant } from '@qwen-code/qwen-code-core/managed-runtime/managed-extension-record.js';
import { HookEventName } from '@qwen-code/qwen-code-core/hooks/types.js';
import { ManagedMcpRuntime } from './managed-mcp-runtime.js';
import { ManagedHookRuntime } from './managed-hook-runtime.js';
import { stageManagedExtensionManifests } from './managed-extension-manifests.js';
import { ManagedExtensionProcessOwners } from './managed-extension-process.js';
import { ManagedRuntimeLedger } from './managed-runtime-ledger.js';
it('keeps private settings out of public MCP and Hook digest inputs', () => {
const workspace = {
tenantId: 'tenant',
workspaceId: 'workspace',
workspaceGeneration: '1',
};
const contributions = {
skills: [],
commands: [],
agents: [],
context: [],
settings: [{ name: 'Token', envVar: 'TOKEN', sensitive: true }],
mcpServers: {
fixture: { command: process.execPath },
},
hooks: {
PreToolUse: [
{ hooks: [{ type: 'command', command: 'printf "%s" "$TOKEN"' }] },
],
},
};
const pin = {
extensionId: 'one',
packageRevision: 1,
packageDigest: 'a'.repeat(64),
settingsRevision: 1,
};
const build = (secret: string, settingsRevision = 1) =>
stageManagedExtensionManifests({
...workspace,
runtimeSessionId: 'root',
command: {
operationId: randomUUID(),
actorKey: 'a'.repeat(64),
expectedRevision: 0,
extensions: [],
},
packages: [
{
pin: { ...pin, settingsRevision },
assetRoot: '/private/package',
contributions,
},
],
baseMcpServers: [],
baseHookCatalog: null,
mcp: new ManagedMcpRuntime(workspace, async () => undefined, {
version: 1,
servers: [],
}),
hooks: new ManagedHookRuntime(workspace, async () => undefined, {
version: 1,
catalogs: [],
}),
hydrated: () => ({
environment: { TOKEN: secret, UNDECLARED: 'must-not-leak' },
contributions,
}),
});
const first = build('fixture-secret-a');
const changedPrivateInput = build('fixture-secret-b');
expect(first.mcpServers).toEqual(changedPrivateInput.mcpServers);
expect(first.hookCatalog).toEqual(changedPrivateInput.hookCatalog);
expect(first.mcpManifest.servers[0]?.env).toEqual({
TOKEN: 'fixture-secret-a',
});
expect(first.hookManifest.catalogs[0]?.hooks[0]?.config).toMatchObject({
env: { TOKEN: 'fixture-secret-a' },
});
expect(JSON.stringify(contributions)).not.toContain('fixture-secret');
expect(JSON.stringify(first)).not.toContain('must-not-leak');
const newRevision = build('fixture-secret-b', 2);
expect(newRevision.mcpServers[0]?.definitionDigest).not.toEqual(
first.mcpServers[0]?.definitionDigest,
);
expect(newRevision.hookCatalog?.definitionDigest).not.toEqual(
first.hookCatalog?.definitionDigest,
);
});
it('keeps unchanged package MCP revisions and restores their exact pins after worker reconstruction', () => {
const workspace = {
tenantId: 'tenant',
workspaceId: 'workspace',
workspaceGeneration: '1',
};
const mcp = new ManagedMcpRuntime(workspace, async () => undefined, {
version: 1,
servers: [],
});
const hooks = new ManagedHookRuntime(workspace, async () => undefined, {
version: 1,
catalogs: [],
});
const contributions = {
skills: [],
commands: [],
agents: [],
context: [],
settings: [],
hooks: {},
mcpServers: { fixture: { command: process.execPath } },
};
const input = {
...workspace,
runtimeSessionId: 'root',
command: {
operationId: randomUUID(),
actorKey: 'a'.repeat(64),
expectedRevision: 6,
extensions: [],
},
packages: [
{
pin: {
extensionId: 'one',
packageRevision: 1,
packageDigest: 'b'.repeat(64),
},
assetRoot: '/private/package',
contributions,
},
],
baseMcpServers: [],
baseHookCatalog: null,
mcp,
hooks,
hydrated: () => ({ contributions, environment: {} }),
};
const initial = stageManagedExtensionManifests(input);
mcp.installSessionDefinitions('root', initial.mcpManifest);
const updated = stageManagedExtensionManifests({
...input,
command: { ...input.command, expectedRevision: 7 },
});
expect(updated.mcpServers).toEqual(initial.mcpServers);
const unrelated = stageManagedExtensionManifests({
...input,
runtimeSessionId: 'other-root',
command: { ...input.command, expectedRevision: 7 },
});
expect(unrelated.mcpServers[0]?.serverRevision).toBe(8);
const reconstructed = new ManagedMcpRuntime(
workspace,
async () => undefined,
{ version: 1, servers: [] },
);
const restored = stageManagedExtensionManifests({
...input,
command: { ...input.command, expectedRevision: 7 },
mcp: reconstructed,
preparedMcpServers: updated.mcpServers,
});
expect(restored.mcpServers).toEqual(initial.mcpServers);
const conflicting = {
...initial.mcpManifest.servers[0]!,
serverRevision: 1,
definitionDigest: 'c'.repeat(64),
};
const conflictingRuntime = new ManagedMcpRuntime(
workspace,
async () => undefined,
{ version: 1, servers: [conflicting] },
);
expect(() =>
stageManagedExtensionManifests({
...input,
mcp: conflictingRuntime,
baseMcpServers: [
{
serverId: conflicting.serverId,
serverRevision: 1,
definitionDigest: conflicting.definitionDigest,
},
],
}),
).toThrow('duplicate');
});
it.skipIf(process.platform !== 'darwin')(
'delivers only each package declared settings to real MCP and Hook subprocesses without recipe env mappings',
async () => {
const directory = await fs.mkdtemp(
path.join(os.tmpdir(), 'extension-env-'),
);
const key = {
tenantId: 'tenant',
workspaceId: 'workspace',
sessionId: 'root',
};
const workspace = { ...key, workspaceGeneration: '1' };
const ledger = ManagedRuntimeLedger.create({
workFile: path.join(directory, 'ledger.json'),
worker: {
pid: process.pid,
pgid: process.pid,
incarnation: 'settings-test',
startedAt: Date.now(),
groupOwned: false,
},
});
const owners = new ManagedExtensionProcessOwners(
'developer-process-group',
{
ledger,
directory: path.join(directory, 'owners'),
},
);
const mcp = new ManagedMcpRuntime(
workspace,
async () => directory,
{
version: 1,
servers: [],
},
owners,
);
const hooks = new ManagedHookRuntime(
workspace,
async () => directory,
{
version: 1,
catalogs: [],
},
owners,
);
const probe =
'{TOKEN:process.env.TOKEN,ONLY_ONE:process.env.ONLY_ONE??null,ONLY_TWO:process.env.ONLY_TWO??null,UNDECLARED:process.env.UNDECLARED??null}';
const server = `
const {createInterface}=require("node:readline");
createInterface({input:process.stdin}).on("line",line=>{
const req=JSON.parse(line);
if(req.id===undefined)return;
const result=req.method==="initialize"
?{protocolVersion:"2024-11-05",capabilities:{tools:{}},serverInfo:{name:"settings",version:"1"}}
:{tools:[{name:"probe",description:JSON.stringify(${probe}),inputSchema:{type:"object"}}]};
process.stdout.write(JSON.stringify({jsonrpc:"2.0",id:req.id,result})+"\\n");
});`;
const packages = ['one', 'two'].map((name, index) => {
const contributions: ManagedPackageContributions = {
skills: [],
commands: [],
agents: [],
context: [],
settings: [
{ name: 'Token', envVar: 'TOKEN', sensitive: true },
{ name: 'Own value', envVar: `ONLY_${name.toUpperCase()}` },
],
mcpServers: {
probe: { command: process.execPath, args: ['-e', server] },
},
hooks: {
PreToolUse: [
{
hooks: [
{
type: 'command',
command: `${JSON.stringify(process.execPath)} -e 'process.stdout.write(JSON.stringify({systemMessage:JSON.stringify(${probe})}))'`,
},
],
},
],
},
};
return {
pin: {
extensionId: name,
packageRevision: 1,
packageDigest: String(index + 1).repeat(64),
settingsRevision: 1,
},
assetRoot: path.join(directory, name),
contributions,
};
});
const staged = stageManagedExtensionManifests({
...workspace,
runtimeSessionId: key.sessionId,
command: {
operationId: randomUUID(),
actorKey: 'a'.repeat(64),
expectedRevision: 0,
extensions: packages.map((entry) => entry.pin),
},
packages,
baseMcpServers: [],
baseHookCatalog: null,
mcp,
hooks,
hydrated: (root) => ({
contributions: packages.find((entry) => entry.assetRoot === root)!
.contributions,
environment: {
TOKEN: `${path.basename(root)}-secret`,
ONLY_ONE: 'one-only',
ONLY_TWO: 'two-only',
UNDECLARED: 'must-not-leak',
},
}),
});
const grant = (
operationId: string,
domain: OperationGrant['domain'],
phases: string[],
): OperationGrant => ({
sessionKey: key,
operationId,
domain,
operationRevision: 1,
ownerId: 'owner',
workspaceGeneration: '1',
resourceScope: {
recordRef: {
resourceId: `record:${operationId}`,
kind: `managed-${domain}`,
schemaVersion: 1,
byteLength: 1,
digest: 'b'.repeat(64),
},
phases,
},
leaseDurationMs: 60_000,
expiresAt: Date.now() + 60_000,
});
try {
expect(
JSON.stringify({
packages,
mcpServers: staged.mcpServers,
hookCatalog: staged.hookCatalog,
}),
).not.toContain('-secret');
mcp.installSessionDefinitions(key.sessionId, staged.mcpManifest);
hooks.installSessionManifest(key.sessionId, staged.hookManifest);
for (const [index, pkg] of packages.entries()) {
const expected = {
TOKEN: `${pkg.pin.extensionId}-secret`,
ONLY_ONE: index === 0 ? 'one-only' : null,
ONLY_TWO: index === 1 ? 'two-only' : null,
UNDECLARED: null,
};
const serverPin = staged.mcpServers[index]!;
const configureId = `configure-${index}`;
await mcp.control('owner', {
kind: 'mcp-configure',
sessionKey: key,
operationId: configureId,
...serverPin,
configRevision: 1,
grant: grant(configureId, 'mcp_configuration', [
'configure',
'discover',
'release',
]),
});
await vi.waitFor(
async () => {
const result = await mcp.control('owner', {
kind: 'mcp-status',
sessionKey: key,
operationId: `query-${index}`,
targetOperationId: configureId,
});
expect(result.state).toBe('settled');
expect(result.error).toBeUndefined();
expect(JSON.parse(result.catalog!.tools[0]!.description!)).toEqual(
expected,
);
},
{ timeout: 10_000 },
);
const executeId = `hook-${index}`;
await hooks.control('owner', {
kind: 'hook-execute',
sessionKey: key,
operationId: executeId,
pin: staged.hookCatalog!,
hookId: staged.hookManifest.catalogs[0]!.hooks[index]!.hookId,
input: {
session_id: key.sessionId,
cwd: directory,
transcript_path: '',
hook_event_name: HookEventName.PreToolUse,
timestamp: new Date().toISOString(),
},
grant: grant(executeId, 'hook_execution', ['execute']),
});
await vi.waitFor(
async () => {
const result = await hooks.control('owner', {
kind: 'hook-status',
sessionKey: key,
operationId: `hook-query-${index}`,
targetOperationId: executeId,
});
expect(result.state).toBe('settled');
expect(result.error).toBeUndefined();
expect(result.result?.success).toBe(true);
expect(JSON.parse(result.result!.output!.systemMessage!)).toEqual(
expected,
);
},
{ timeout: 10_000 },
);
}
} finally {
await Promise.all([mcp.close(), hooks.close()]);
await owners.stopAll();
await fs.rm(directory, { recursive: true, force: true });
}
},
);
@@ -0,0 +1,250 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { createHash } from 'node:crypto';
import type { ManagedPackageContributions } from '@qwen-code/qwen-code-core/extension/managed-package.js';
import {
HookEventName,
HooksConfigSource,
} from '@qwen-code/qwen-code-core/hooks/types.js';
import type {
ManagedExtensionApplyCommand,
ManagedExtensionMcpPin,
} from '@qwen-code/qwen-code-core/managed-runtime/managed-extension-package.js';
import type { ManagedHookCatalogPin } from '@qwen-code/qwen-code-core/managed-runtime/managed-hook-protocol.js';
import {
ManagedHookRuntime,
type ManagedHookDefinition,
type ManagedHookManifest,
} from './managed-hook-runtime.js';
import {
ManagedMcpRuntime,
type ManagedMcpDefinition,
type ManagedMcpManifest,
} from './managed-mcp-runtime.js';
import type { ManagedRuntimeExtensionPackage } from './managed-extension-runtime.js';
function digest(value: unknown) {
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
}
function object(value: unknown): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value))
throw new Error('Invalid extension recipe.');
return value as Record<string, unknown>;
}
export function stageManagedExtensionManifests(input: {
tenantId: string;
workspaceId: string;
runtimeSessionId: string;
command: ManagedExtensionApplyCommand;
packages: readonly ManagedRuntimeExtensionPackage[];
baseMcpServers: readonly ManagedExtensionMcpPin[];
preparedMcpServers?: readonly ManagedExtensionMcpPin[];
baseHookCatalog: ManagedHookCatalogPin | null;
mcp: ManagedMcpRuntime;
hooks: ManagedHookRuntime;
hydrated: (assetRoot: string) => {
contributions: ManagedPackageContributions;
environment: Readonly<Record<string, string>>;
};
}) {
const servers = [...input.mcp.resolvePinnedDefinitions(input.baseMcpServers)];
const hooks = input.baseHookCatalog
? [...input.hooks.resolvePinnedCatalog(input.baseHookCatalog).hooks]
: [];
const revision = input.command.expectedRevision + 1;
for (const pkg of input.packages) {
const { contributions, environment } = input.hydrated(pkg.assetRoot);
const settings = Object.fromEntries(
pkg.contributions.settings
.filter((setting) => Object.hasOwn(environment, setting.envVar))
.map((setting) => [setting.envVar, environment[setting.envVar]!]),
);
for (const [name, raw] of Object.entries(contributions.mcpServers)) {
const recipe = object(raw);
if (
Object.keys(recipe).some(
(key) =>
![
'command',
'args',
'env',
'timeout',
'url',
'httpUrl',
'headers',
'type',
].includes(key),
)
)
throw new Error('Unsupported extension MCP recipe field.');
const transport =
recipe['command'] !== undefined
? 'stdio'
: recipe['type'] === 'sse'
? 'sse'
: 'streamable-http';
const definition: ManagedMcpDefinition = {
tenantId: input.tenantId,
workspaceId: input.workspaceId,
serverId: `${pkg.pin.extensionId}:${name}`,
serverRevision: revision,
definitionDigest: digest({
pin: pkg.pin,
recipe: pkg.contributions.mcpServers[name],
}),
transport,
...(transport === 'stdio'
? {
command: recipe['command'] as string,
args: recipe['args'] as string[] | undefined,
env: {
...settings,
...(recipe['env'] === undefined
? {}
: (object(recipe['env']) as Record<string, string>)),
},
}
: {
url: (recipe['httpUrl'] ?? recipe['url']) as string,
headers: recipe['headers'] as Record<string, string> | undefined,
}),
...(recipe['timeout'] === undefined
? {}
: { timeoutMs: Number(recipe['timeout']) }),
};
const unchanged = input.mcp.matchingSessionDefinition(
input.runtimeSessionId,
definition.serverId,
definition.definitionDigest,
);
const restored = input.preparedMcpServers?.find(
(pin) =>
pin.serverId === definition.serverId &&
pin.definitionDigest === definition.definitionDigest,
);
servers.push(
restored
? { ...definition, serverRevision: restored.serverRevision }
: unchanged
? { ...definition, serverRevision: unchanged.serverRevision }
: definition,
);
}
for (const [event, rawGroups] of Object.entries(contributions.hooks)) {
if (
!Object.values(HookEventName).includes(event as HookEventName) ||
!Array.isArray(rawGroups)
)
throw new Error('Unsupported extension hook event.');
for (const [groupIndex, rawGroup] of rawGroups.entries()) {
const group = object(rawGroup);
if (
Object.keys(group).some(
(key) => !['hooks', 'matcher', 'sequential'].includes(key),
)
)
throw new Error('Unsupported extension Hook group field.');
if (!Array.isArray(group['hooks']))
throw new Error('Invalid extension Hook group.');
for (const [index, rawHook] of group['hooks'].entries()) {
const config = object(rawHook);
if (!['command', 'http', 'prompt'].includes(String(config['type'])))
throw new Error('Unsupported extension Hook type.');
hooks.push({
hookId: `${pkg.pin.extensionId}:${event}:${groupIndex}:${index}`,
eventName: event as HookEventName,
...(typeof group['matcher'] === 'string'
? { matcher: group['matcher'] }
: {}),
sequential: group['sequential'] === true,
onceKey:
config['once'] === true
? `${pkg.pin.extensionId}:${event}:${groupIndex}:${index}`
: null,
failClosed: true,
async: config['async'] === true,
source: HooksConfigSource.Extensions,
sourceTrusted: true,
enabled: true,
config: {
...Object.fromEntries(
Object.entries(config).filter(
([key]) => !['once', 'async'].includes(key),
),
),
...(config['type'] === 'command'
? {
env: {
...settings,
...(config['env'] === undefined
? {}
: object(config['env'])),
},
}
: {}),
} as unknown as ManagedHookDefinition['config'],
});
}
}
}
}
if (
servers.length > 32 ||
new Set(servers.map((server) => server.serverId)).size !== servers.length
)
throw new Error(
'Extension MCP selection has duplicate identities or exceeds its limit.',
);
const mcpManifest: ManagedMcpManifest = { version: 1, servers };
const hookCatalog = hooks.length
? {
catalogId: `extensions:${digest(input.runtimeSessionId).slice(0, 32)}`,
catalogRevision: revision,
definitionDigest: digest({
base: input.baseHookCatalog,
packages: input.packages
.filter((pkg) => Object.keys(pkg.contributions.hooks).length > 0)
.map((pkg) => ({ pin: pkg.pin, hooks: pkg.contributions.hooks })),
}),
}
: null;
const hookManifest: ManagedHookManifest = {
version: 1,
catalogs: hookCatalog
? [
{
...hookCatalog,
tenantId: input.tenantId,
workspaceId: input.workspaceId,
hooks,
},
]
: [],
};
// Constructors validate every recipe during staging, without opening a
// transport or dispatching a Hook.
const workspace = {
tenantId: input.tenantId,
workspaceId: input.workspaceId,
workspaceGeneration: '1',
};
new ManagedMcpRuntime(workspace, async () => undefined, mcpManifest);
new ManagedHookRuntime(workspace, async () => undefined, hookManifest);
return {
mcpManifest,
hookManifest,
mcpServers: servers.map(
({ serverId, serverRevision, definitionDigest }) => ({
serverId,
serverRevision,
definitionDigest,
}),
),
hookCatalog,
};
}
@@ -0,0 +1,69 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { promises as fs } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { expect, it } from 'vitest';
import type { ManagedPackageContributions } from '@qwen-code/qwen-code-core/extension/managed-package.js';
import { preflightManagedExtensionPackage } from './managed-extension-preflight.js';
const empty: ManagedPackageContributions = {
skills: [],
commands: [],
agents: [],
context: [],
mcpServers: {},
hooks: {},
settings: [],
};
it('checks target executables and package scripts without executing code, while reporting dynamic dependency boundaries', async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'extension-preflight-'));
try {
const script = path.join(root, 'probe.js');
await fs.writeFile(script, 'throw new Error("must not execute")');
const contribution = {
...empty,
mcpServers: { test: { command: process.execPath, args: [script] } },
};
await expect(
preflightManagedExtensionPackage(contribution, root, root, {
PATH: '/usr/bin:/bin',
}),
).resolves.toEqual({ dynamicCommands: 0 });
await fs.rm(script);
await expect(
preflightManagedExtensionPackage(contribution, root, root, {}),
).rejects.toThrow();
await expect(
preflightManagedExtensionPackage(
{
...empty,
mcpServers: { bad: { command: 'missing-extension-interpreter-123' } },
},
root,
root,
{ PATH: root },
),
).rejects.toThrow('unavailable');
await expect(
preflightManagedExtensionPackage(
{
...empty,
hooks: {
PreToolUse: [
{ hooks: [{ type: 'command', command: 'echo dynamic | cat' }] },
],
},
},
root,
root,
{ PATH: '/usr/bin:/bin' },
),
).resolves.toEqual({ dynamicCommands: 1 });
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
@@ -0,0 +1,194 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { constants, promises as fs } from 'node:fs';
import path from 'node:path';
import type { ManagedPackageContributions } from '@qwen-code/qwen-code-core/extension/managed-package.js';
async function executable(
command: string,
cwd: string,
environment: Readonly<Record<string, string>>,
): Promise<string> {
const candidates = command.includes('/')
? [path.resolve(cwd, command)]
: (environment['PATH'] ?? '/usr/bin:/bin')
.split(path.delimiter)
.map((directory) => path.resolve(cwd, directory, command));
for (const candidate of candidates) {
try {
if (!(await fs.stat(candidate)).isFile()) continue;
await fs.access(candidate, constants.X_OK);
return candidate;
} catch {
/* Try the next PATH entry. */
}
}
throw new Error(`Extension executable is unavailable: ${command}`);
}
export async function preflightManagedExtensionPackage(
contributions: ManagedPackageContributions,
root: string,
cwd: string,
environment: Readonly<Record<string, string>>,
): Promise<{ dynamicCommands: number }> {
const env: Record<string, string> = {
...environment,
QWEN_EXTENSION_ROOT: root,
CLAUDE_PLUGIN_ROOT: root,
extensionPath: root,
};
const expand = (value: string, executionEnvironment = env) =>
value.replace(
/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/gu,
(original, braced: string, plain: string) =>
executionEnvironment[braced ?? plain] ?? original,
);
let dynamicCommands = 0;
const check = async (
command: string,
args: readonly string[],
executionEnvironment = env,
) => {
const resolved = await executable(
expand(command, executionEnvironment),
cwd,
executionEnvironment,
);
const handle = await fs.open(resolved, 'r');
let head: string;
try {
const buffer = Buffer.alloc(512);
const read = await handle.read(buffer, 0, 512, 0);
head = buffer.subarray(0, read.bytesRead).toString();
} finally {
await handle.close();
}
if (head.startsWith('#!')) {
const shebang = head.split('\n')[0]!.slice(2).trim().split(/\s+/u);
await executable(shebang[0]!, cwd, executionEnvironment);
if (
path.basename(shebang[0]!) === 'env' &&
shebang[1] &&
!shebang[1].startsWith('-')
)
await executable(shebang[1], cwd, executionEnvironment);
}
if (path.basename(resolved) === 'env' && args.length) {
if (args[0]!.startsWith('-') || args[0]!.includes('=')) {
dynamicCommands++;
return;
}
await check(args[0]!, args.slice(1), executionEnvironment);
return;
}
if (args.includes('-e') || args.includes('--eval') || args.includes('-c')) {
dynamicCommands++;
return;
}
for (const argument of args.filter(
(value) =>
!value.startsWith('-') && /\.(?:[cm]?js|py|rb|sh)$/u.test(value),
)) {
const script = path.resolve(cwd, expand(argument, executionEnvironment));
if (!(await fs.stat(script)).isFile())
throw new Error('Extension script is unavailable.');
await fs.access(script, constants.R_OK);
}
};
const shell = async (command: string, executionEnvironment = env) => {
// Operators, substitutions and conditional commands need runtime resolution.
if (/[|;&><`\n]|\$\(/u.test(command)) {
dynamicCommands++;
return;
}
const tokens = [...command.matchAll(/"([^"]*)"|'([^']*)'|([^\s]+)/gu)].map(
(match) => match[1] ?? match[2] ?? match[3]!,
);
if (!tokens.length) return;
if (
tokens.some(
(token) =>
token.includes('$') && expand(token, executionEnvironment) === token,
)
) {
dynamicCommands++;
return;
}
if (/^[A-Za-z_][A-Za-z0-9_]*=/u.test(tokens[0]!)) {
dynamicCommands++;
return;
}
if (
[
':',
'[',
'alias',
'bg',
'break',
'cd',
'continue',
'exit',
'export',
'false',
'fg',
'getopts',
'hash',
'jobs',
'pwd',
'read',
'readonly',
'return',
'set',
'shift',
'test',
'times',
'trap',
'true',
'type',
'ulimit',
'umask',
'unalias',
'unset',
'wait',
].includes(tokens[0]!)
)
return;
if (tokens[0] === 'eval') {
dynamicCommands++;
return;
}
if (['exec', 'command'].includes(tokens[0]!)) tokens.shift();
if (tokens.length)
await check(tokens[0]!, tokens.slice(1), executionEnvironment);
};
for (const value of Object.values(contributions.mcpServers)) {
const recipe = value as {
command?: string;
args?: string[];
env?: Record<string, string>;
};
if (recipe.command)
await check(recipe.command, recipe.args ?? [], { ...env, ...recipe.env });
}
for (const groups of Object.values(contributions.hooks)) {
for (const group of groups as Array<{
hooks?: Array<{
type?: string;
command?: string;
env?: Record<string, string>;
}>;
}>) {
for (const hook of group.hooks ?? [])
if (hook.type === 'command' && hook.command)
await shell(hook.command, { ...env, ...hook.env });
}
}
for (const command of contributions.commands)
for (const match of command.prompt.matchAll(/!\{([^}]*)\}/gu))
await shell(match[1]!);
return { dynamicCommands };
}
@@ -0,0 +1,194 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { promises as fs } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import { ManagedRuntimeLedger } from './managed-runtime-ledger.js';
import { ManagedExtensionProcessOwners } from './managed-extension-process.js';
const directories: string[] = [];
afterEach(async () => {
await Promise.all(
directories
.splice(0)
.map((directory) => fs.rm(directory, { recursive: true, force: true })),
);
});
describe('ManagedExtensionProcessOwners', () => {
it('does not silently substitute developer mode on unsupported hosts', () => {
if (process.platform !== 'linux')
expect(
() => new ManagedExtensionProcessOwners('linux-cgroup', {}),
).toThrow('unavailable');
expect(
() => new ManagedExtensionProcessOwners('developer-process-group', {}),
).toThrow('unavailable');
});
it.skipIf(process.platform !== 'darwin')(
'stops changed Session owners while preserving retained MCP owners and sibling Sessions',
async () => {
const directory = await fs.mkdtemp(
path.join(os.tmpdir(), 'extension-retained-'),
);
directories.push(directory);
const ledger = ManagedRuntimeLedger.create({
workFile: path.join(directory, 'ledger.json'),
worker: {
pid: process.pid,
pgid: process.pid,
incarnation: 'test',
startedAt: Date.now(),
groupOwned: false,
},
});
const owners = new ManagedExtensionProcessOwners(
'developer-process-group',
{ ledger, directory: path.join(directory, 'owners') },
);
const start = (sessionId: string) =>
owners.start({
sessionId,
configRevision: 1,
executable: process.execPath,
args: ['-e', 'setInterval(()=>{},1000)'],
env: {},
cwd: directory,
});
try {
const retained = await start('a');
const changed = await start('a');
const sibling = await start('b');
const receipts = await owners.stopSession(
'a',
new Set([retained.ownerId]),
);
expect(receipts).toEqual([
expect.objectContaining({
ownerId: changed.ownerId,
state: 'stopped',
}),
]);
expect(receipts.some((receipt) => receipt.coverage !== undefined)).toBe(
false,
);
expect(await owners.hasLiveSessionOwners('a')).toBe(true);
expect(
await owners.hasLiveSessionOwners('a', new Set([retained.ownerId])),
).toBe(false);
expect(await owners.hasLiveSessionOwners('b')).toBe(true);
expect(retained.child.exitCode).toBeNull();
expect(sibling.child.exitCode).toBeNull();
} finally {
await owners.stopAll();
}
},
);
it.skipIf(process.platform !== 'darwin')(
'persists zero-owner coverage and rejects a missing indexed owner after restart',
async () => {
const directory = await fs.mkdtemp(
path.join(os.tmpdir(), 'extension-journal-'),
);
directories.push(directory);
const ledger = ManagedRuntimeLedger.create({
workFile: path.join(directory, 'ledger.json'),
worker: {
pid: process.pid,
pgid: process.pid,
incarnation: 'test',
startedAt: Date.now(),
groupOwned: false,
},
});
const options = { ledger, directory: path.join(directory, 'owners') };
const original = new ManagedExtensionProcessOwners(
'developer-process-group',
options,
);
await original.coverSession('root', 1);
const resumed = new ManagedExtensionProcessOwners(
'developer-process-group',
options,
);
expect(await resumed.stopSession('root')).toEqual([
expect.objectContaining({
state: 'stopped',
coverage: 'registered-owners',
configRevision: 1,
journalId: original.journalId,
}),
]);
await fs.writeFile(
path.join(options.directory, '.owners.json'),
JSON.stringify(['extension-aaaa']),
);
const damaged = new ManagedExtensionProcessOwners(
'developer-process-group',
options,
);
await expect(damaged.stopSession('root')).rejects.toThrow('missing');
},
);
it.skipIf(process.platform !== 'darwin')(
'records the process group before target code and reports only group scope',
async () => {
const directory = await fs.mkdtemp(
path.join(os.tmpdir(), 'extension-process-'),
);
directories.push(directory);
const ledger = ManagedRuntimeLedger.create({
workFile: path.join(directory, 'ledger.json'),
worker: {
pid: process.pid,
pgid: process.pid,
incarnation: 'test',
startedAt: Date.now(),
},
});
const owners = new ManagedExtensionProcessOwners(
'developer-process-group',
{ ledger },
);
const owner = await owners.start({
sessionId: 'session-a',
configRevision: 1,
executable: process.execPath,
args: [
'-e',
'process.stdout.write("ready\\n");setInterval(()=>{},1000)',
],
env: {},
cwd: directory,
});
try {
expect(ledger.outstandingGroups()).toEqual([
expect.objectContaining({
callId: owner.ownerId,
pgid: owner.child.pid,
}),
]);
const receipt = await owners.stop(owner.ownerId);
expect(receipt).toMatchObject({
state: 'stopped',
scope: 'process_group',
mode: 'developer-process-group',
sessionId: 'session-a',
});
expect(await owners.stopSession('session-b')).toEqual([]);
expect(await owners.stop(owner.ownerId)).toEqual(receipt);
} finally {
owner.child.kill('SIGKILL');
await owners.stop(owner.ownerId);
}
},
);
});
@@ -0,0 +1,479 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { ManagedExtensionExecutionUnknownError } from './managed-extension-error.js';
import { execFileSync, spawn, type ChildProcess } from 'node:child_process';
import { randomUUID, createHash } from 'node:crypto';
import {
promises as fs,
readFileSync,
mkdirSync,
writeFileSync,
} from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { atomicWriteJSON } from '@qwen-code/qwen-code-core/utils/atomicFileWrite.js';
import type { Writable } from 'node:stream';
import { HookCommandCgroup } from '@qwen-code/qwen-code-core/hooks/hook-command-cgroup.js';
import type { ManagedRuntimeLedger } from './managed-runtime-ledger.js';
export type ManagedExtensionProcessMode =
| 'linux-cgroup'
| 'developer-process-group';
export interface ManagedExtensionProcessReceipt {
readonly ownerId: string;
readonly sessionId: string;
readonly configRevision: number;
readonly mode: ManagedExtensionProcessMode;
readonly scope: 'process_tree' | 'process_group';
readonly state: 'stopped' | 'outcome_unknown';
readonly coverage?: 'registered-owners';
readonly journalId?: string;
}
interface Owner {
readonly ownerId: string;
readonly sessionId: string;
readonly configRevision: number;
readonly child?: ChildProcess;
readonly pgid?: number;
readonly startedAt: number;
readonly uptimeMs: number;
readonly unit?: HookCommandCgroup;
}
// The target command is not even read until its group is durably registered.
const DEVELOPER_LAUNCHER = String.raw`
const fs = require('node:fs');
const { spawn } = require('node:child_process');
let message = '';
const byte = Buffer.alloc(1);
while (fs.readSync(3, byte, 0, 1, null)) {
if (byte[0] === 10) break;
message += byte.toString();
}
if (!message) process.exit(1);
const input = JSON.parse(message);
const child = spawn(input.executable, input.args, { env: input.env, stdio: 'inherit' });
child.on('error', () => process.exit(1));
child.on('exit', code => process.exit(code ?? 1));
`;
export class ManagedExtensionProcessOwners {
private readonly owners = new Map<string, Owner>();
private readonly receipts = new Map<string, ManagedExtensionProcessReceipt>();
readonly journalId: string;
private readonly recovering: Promise<void>;
private journalUpdates: Promise<void> = Promise.resolve();
private readonly journalOwners = new Set<string>();
private readonly bootIdentity =
process.platform === 'darwin'
? execFileSync('/usr/sbin/sysctl', ['-n', 'kern.bootsessionuuid'], {
encoding: 'utf8',
}).trim()
: process.platform === 'linux'
? readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim()
: '';
private readonly stopping = new Map<
string,
Promise<ManagedExtensionProcessReceipt>
>();
constructor(
readonly mode: ManagedExtensionProcessMode,
private readonly options: {
readonly cgroupRoot?: string;
readonly ledger?: ManagedRuntimeLedger;
readonly directory?: string;
},
) {
if (
(mode === 'linux-cgroup' && process.platform !== 'linux') ||
(mode === 'developer-process-group' &&
(process.platform !== 'darwin' || !options.ledger))
)
throw new Error('Extension process mode is unavailable on this Runtime.');
if (options.directory) {
mkdirSync(options.directory, { recursive: true, mode: 0o700 });
const marker = path.join(options.directory, '.journal-id');
try {
this.journalId = readFileSync(marker, 'utf8');
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
this.journalId = randomUUID();
writeFileSync(marker, this.journalId, { flag: 'wx', mode: 0o600 });
writeFileSync(path.join(options.directory, '.owners.json'), '[]', {
flag: 'wx',
mode: 0o600,
});
}
} else this.journalId = randomUUID();
this.recovering = this.loadOwners();
void this.recovering.catch(() => undefined);
}
async start(input: {
readonly sessionId: string;
readonly configRevision: number;
readonly executable: string;
readonly args: readonly string[];
readonly env: NodeJS.ProcessEnv;
readonly cwd: string;
}): Promise<{ ownerId: string; child: ChildProcess }> {
await this.recovering;
if (
!input.sessionId ||
!Number.isSafeInteger(input.configRevision) ||
input.configRevision < 1
)
throw new Error('Invalid extension process owner.');
const ownerId = `extension-${randomUUID()}`;
const startedAt = Date.now();
const uptimeMs = os.uptime() * 1000;
await this.saveOwner({
ownerId,
sessionId: input.sessionId,
configRevision: input.configRevision,
startedAt,
uptimeMs,
});
const unit =
this.mode === 'linux-cgroup'
? HookCommandCgroup.create(this.options.cgroupRoot, ownerId)
: undefined;
const launch = unit?.launch(input.executable, [...input.args], input.env);
const child = spawn(
launch?.executable ?? process.execPath,
launch?.args ?? ['--input-type=commonjs', '--eval', DEVELOPER_LAUNCHER],
{
cwd: input.cwd,
env: launch?.env ?? { PATH: '/usr/bin:/bin' },
detached: !unit,
stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
},
);
child.on('error', () => undefined);
child.stdio[3]?.on('error', () => undefined);
const owner = {
ownerId,
sessionId: input.sessionId,
configRevision: input.configRevision,
child,
unit,
startedAt,
uptimeMs,
pgid: child.pid,
};
this.owners.set(ownerId, owner);
try {
await this.saveOwner(owner);
if (unit) {
let joined = '';
child.stdio[3]?.on('data', (chunk: Buffer) => {
joined += chunk.toString();
});
for (
let attempt = 0;
attempt < 40 && !joined.includes('joined\n');
attempt++
) {
if (
joined.includes('unavailable\n') ||
child.exitCode !== null ||
child.signalCode !== null
)
throw new Error('Extension process membership is unproven.');
await new Promise((resolve) => setTimeout(resolve, 50));
}
if (!joined.includes('joined\n'))
throw new Error('Extension process membership is unproven.');
} else {
if (!child.pid) throw new Error('Extension process failed to start.');
this.options.ledger!.addGroup({
pgid: child.pid,
callId: ownerId,
startedAt,
});
(child.stdio[3] as Writable).end(
`${JSON.stringify({ executable: input.executable, args: input.args, env: input.env })}\n`,
);
}
return { ownerId, child };
} catch (error) {
// Unknown cleanup keeps the owner: nobody can reinterpret a failed
// startup as permission to discard its process identity.
child.stdio[3]?.destroy();
child.kill('SIGKILL');
const receipt = await this.stop(ownerId).catch(() => undefined);
if (receipt?.state !== 'stopped')
throw new ManagedExtensionExecutionUnknownError(
'Extension process startup cleanup is unknown.',
);
throw error;
}
}
async hasLiveSessionOwners(
sessionId: string,
except: ReadonlySet<string> = new Set(),
): Promise<boolean> {
await this.recovering;
return [...this.owners.values()].some(
(owner) =>
owner.sessionId === sessionId &&
!except.has(owner.ownerId) &&
this.receipts.get(owner.ownerId)?.state !== 'stopped',
);
}
async coverSession(sessionId: string, configRevision: number): Promise<void> {
await this.recovering;
if (!this.options.directory)
throw new Error('Extension owner scope requires a durable journal.');
const file = path.join(
this.options.directory,
`.scope-${createHash('sha256').update(sessionId).digest('hex')}.json`,
);
await atomicWriteJSON(
file,
{
sessionId,
configRevision,
journalId: this.journalId,
ownerId: `coverage-${randomUUID()}`,
},
{ mode: 0o600 },
);
}
async stopSession(
sessionId: string,
except: ReadonlySet<string> = new Set(),
): Promise<readonly ManagedExtensionProcessReceipt[]> {
await this.recovering;
const stopped = await Promise.all(
[...this.owners.values()]
.filter(
(owner) =>
owner.sessionId === sessionId && !except.has(owner.ownerId),
)
.map((owner) => this.stop(owner.ownerId)),
);
if (
except.size > 0 ||
!this.options.directory ||
stopped.some((receipt) => receipt.state !== 'stopped')
)
return stopped;
const coverage = JSON.parse(
await fs.readFile(
path.join(
this.options.directory,
`.scope-${createHash('sha256').update(sessionId).digest('hex')}.json`,
),
'utf8',
),
) as Record<string, unknown>;
if (
coverage['sessionId'] !== sessionId ||
coverage['journalId'] !== this.journalId ||
typeof coverage['ownerId'] !== 'string' ||
!Number.isSafeInteger(coverage['configRevision'])
)
throw new Error('Extension owner coverage is unavailable.');
return [
...stopped,
{
ownerId: coverage['ownerId'],
sessionId,
configRevision: Number(coverage['configRevision']),
mode: this.mode,
scope: this.mode === 'linux-cgroup' ? 'process_tree' : 'process_group',
state: 'stopped',
coverage: 'registered-owners',
journalId: this.journalId,
},
];
}
stop(ownerId: string): Promise<ManagedExtensionProcessReceipt> {
let pending = this.stopping.get(ownerId);
if (!pending) {
pending = this.stopOwner(ownerId).finally(() =>
this.stopping.delete(ownerId),
);
this.stopping.set(ownerId, pending);
}
return pending;
}
async stopAll(): Promise<readonly ManagedExtensionProcessReceipt[]> {
await this.recovering;
return Promise.all([...this.owners.keys()].map((id) => this.stop(id)));
}
private async stopOwner(
ownerId: string,
): Promise<ManagedExtensionProcessReceipt> {
await this.recovering;
const existing = this.receipts.get(ownerId);
if (existing) return existing;
const owner = this.owners.get(ownerId);
if (!owner) throw new Error('Extension process owner is unknown.');
let stopped = false;
try {
if (owner.unit) {
await owner.unit.terminate(1_000);
stopped = await owner.unit.waitForEmpty(5_000);
if (stopped) owner.unit.remove();
} else if (owner.pgid && !this.options.ledger!.hasIntegrityFailure) {
stopped =
(await this.options.ledger!.stopWitnessedGroup(owner.pgid, 5000)) ===
'gone';
stopped &&= !this.options.ledger!.hasIntegrityFailure;
}
} catch {
stopped = false;
}
const receipt: ManagedExtensionProcessReceipt = {
ownerId,
sessionId: owner.sessionId,
configRevision: owner.configRevision,
mode: this.mode,
scope: this.mode === 'linux-cgroup' ? 'process_tree' : 'process_group',
state: stopped ? 'stopped' : 'outcome_unknown',
};
if (stopped) {
if (this.options.directory)
await atomicWriteJSON(
path.join(this.options.directory, `${ownerId}.receipt.json`),
receipt,
{ mode: 0o600 },
);
this.receipts.set(ownerId, receipt);
}
return receipt;
}
private async saveOwner(owner: Omit<Owner, 'child' | 'unit'>): Promise<void> {
if (!this.options.directory) return;
await fs.mkdir(this.options.directory, { recursive: true, mode: 0o700 });
this.journalOwners.add(owner.ownerId);
await atomicWriteJSON(
path.join(this.options.directory, `${owner.ownerId}.json`),
{
version: 1,
bootIdentity: this.bootIdentity,
mode: this.mode,
ownerId: owner.ownerId,
sessionId: owner.sessionId,
configRevision: owner.configRevision,
startedAt: owner.startedAt,
uptimeMs: owner.uptimeMs,
pgid: owner.pgid ?? null,
},
{ mode: 0o600 },
);
this.journalUpdates = this.journalUpdates.then(() =>
atomicWriteJSON(
path.join(this.options.directory!, '.owners.json'),
[...this.journalOwners].sort(),
{ mode: 0o600 },
),
);
await this.journalUpdates;
}
private async loadOwners(): Promise<void> {
if (!this.options.directory) return;
await fs.mkdir(this.options.directory, { recursive: true, mode: 0o700 });
const index = JSON.parse(
await fs.readFile(
path.join(this.options.directory, '.owners.json'),
'utf8',
),
) as unknown;
if (
!Array.isArray(index) ||
!index.every(
(id) => typeof id === 'string' && /^extension-[0-9a-f-]+$/u.test(id),
)
)
throw new Error('Extension owner index is invalid.');
const files = await fs.readdir(this.options.directory);
for (const id of index) {
if (!files.includes(`${id}.json`))
throw new Error('Original extension owner record is missing.');
this.journalOwners.add(id);
}
for (const file of files) {
if (!/^extension-[0-9a-f-]+\.json$/u.test(file)) continue;
const value = JSON.parse(
await fs.readFile(path.join(this.options.directory, file), 'utf8'),
) as Record<string, unknown>;
if (
value['version'] !== 1 ||
value['bootIdentity'] !== this.bootIdentity ||
value['mode'] !== this.mode ||
typeof value['ownerId'] !== 'string' ||
`${value['ownerId']}.json` !== file ||
typeof value['sessionId'] !== 'string' ||
!Number.isSafeInteger(value['configRevision']) ||
!Number.isFinite(value['startedAt']) ||
!Number.isFinite(value['uptimeMs'])
)
throw new Error('Extension owner journal is invalid.');
// Never infer ownership after a reboot or a clock-domain inconsistency.
if (Number(value['uptimeMs']) > os.uptime() * 1000)
throw new Error('Original extension owner boot is unavailable.');
const owner: Owner = {
ownerId: value['ownerId'],
sessionId: value['sessionId'],
configRevision: Number(value['configRevision']),
startedAt: Number(value['startedAt']),
uptimeMs: Number(value['uptimeMs']),
pgid: typeof value['pgid'] === 'number' ? value['pgid'] : undefined,
...(this.mode === 'linux-cgroup'
? {
unit: HookCommandCgroup.attach(
this.options.cgroupRoot,
value['ownerId'],
),
}
: {}),
};
this.owners.set(owner.ownerId, owner);
if (this.mode === 'developer-process-group' && owner.pgid)
this.options.ledger!.addGroup({
pgid: owner.pgid,
callId: owner.ownerId,
startedAt: owner.startedAt,
uptimeMs: owner.uptimeMs,
});
try {
const receipt = JSON.parse(
await fs.readFile(
path.join(this.options.directory, `${owner.ownerId}.receipt.json`),
'utf8',
),
) as ManagedExtensionProcessReceipt;
if (
receipt.ownerId !== owner.ownerId ||
receipt.sessionId !== owner.sessionId ||
receipt.mode !== this.mode ||
receipt.configRevision !== owner.configRevision ||
receipt.state !== 'stopped' ||
receipt.scope !==
(this.mode === 'linux-cgroup' ? 'process_tree' : 'process_group')
)
throw new Error('Extension stop receipt conflicts.');
this.receipts.set(owner.ownerId, receipt);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
}
}
}
}
@@ -0,0 +1,68 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { createHash } from 'node:crypto';
import path from 'node:path';
import type { ManagedExtensionProcessMode } from './managed-extension-process.js';
export interface ManagedExtensionRuntimeConfiguration {
readonly enabled: true;
readonly mode: ManagedExtensionProcessMode;
readonly cacheDirectory: string;
readonly cgroupRoot?: string;
}
export const MANAGED_EXTENSION_LINUX_CAPABILITY_DIGEST = `sha256:${createHash('sha256').update('managed-extension-execution/1;linux-process-tree/1').digest('hex')}`;
export const MANAGED_EXTENSION_DEVELOPER_CAPABILITY_DIGEST = `sha256:${createHash('sha256').update('managed-extension-execution/1;darwin-developer-process-group/1').digest('hex')}`;
export function parseManagedExtensionRuntimeConfiguration(
value: unknown,
capabilityDigest: string,
): ManagedExtensionRuntimeConfiguration {
if (!value || typeof value !== 'object' || Array.isArray(value))
throw new Error('Invalid Managed extension profile.');
const config = value as Record<string, unknown>;
if (
![
'cacheDirectory,enabled,mode',
'cacheDirectory,cgroupRoot,enabled,mode',
].includes(Object.keys(config).sort().join(',')) ||
config['enabled'] !== true ||
typeof config['cacheDirectory'] !== 'string' ||
!path.isAbsolute(config['cacheDirectory']) ||
config['cacheDirectory'].includes('\0') ||
!(
(config['mode'] === 'linux-cgroup' &&
capabilityDigest === MANAGED_EXTENSION_LINUX_CAPABILITY_DIGEST) ||
(config['mode'] === 'developer-process-group' &&
capabilityDigest === MANAGED_EXTENSION_DEVELOPER_CAPABILITY_DIGEST)
)
)
throw new Error(
'Managed extension profile conflicts with its capability digest.',
);
if (
config['mode'] === 'linux-cgroup' &&
(typeof config['cgroupRoot'] !== 'string' ||
!path.isAbsolute(config['cgroupRoot']))
)
throw new Error(
'Managed Linux extensions require a delegated cgroup root.',
);
if (
config['mode'] === 'developer-process-group' &&
config['cgroupRoot'] !== undefined
)
throw new Error('Developer process groups do not use cgroups.');
return Object.freeze({
enabled: true,
mode: config['mode'],
cacheDirectory: config['cacheDirectory'],
...(typeof config['cgroupRoot'] === 'string'
? { cgroupRoot: config['cgroupRoot'] }
: {}),
});
}
@@ -0,0 +1,396 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { createHash, randomUUID } from 'node:crypto';
import { promises as fs } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { Readable } from 'node:stream';
import * as tar from 'tar';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
ManagedExtensionRuntime,
type ManagedExtensionRuntimeDependencies,
} from './managed-extension-runtime.js';
let temporary: string;
beforeEach(async () => {
temporary = await fs.mkdtemp(path.join(os.tmpdir(), 'extension-runtime-'));
});
afterEach(async () => {
await fs.rm(temporary, { recursive: true, force: true });
});
function dependencies(): ManagedExtensionRuntimeDependencies {
return {
executionMode: 'developer-process-group',
directory: path.join(temporary, 'runtime'),
admitted: vi.fn(async () => true),
idle: vi.fn(() => true),
compile: vi.fn(async () => ({
skills: [],
commands: [],
agents: [],
context: [],
mcpServers: {},
hooks: {},
settings: [],
})),
drain: vi.fn(async () => undefined),
install: vi.fn(async () => undefined),
};
}
function command(expectedRevision = 0) {
return {
operationId: randomUUID(),
actorKey: 'a'.repeat(64),
expectedRevision,
extensions: [],
};
}
describe('ManagedExtensionRuntime', () => {
it('uses Session revision CAS and drains only the selected Session', async () => {
const deps = dependencies();
const runtime = new ManagedExtensionRuntime(deps);
const first = command();
await runtime.prepare('a', first);
expect(await runtime.status('a')).toMatchObject({ revision: 0 });
await runtime.activate('a', first.operationId);
expect(await runtime.status('a')).toMatchObject({ revision: 1 });
expect(await runtime.status('b')).toMatchObject({ revision: 0 });
expect(deps.drain).toHaveBeenCalledExactlyOnceWith('a', first);
await expect(runtime.prepare('a', command())).rejects.toThrow('revision');
await runtime.activate('a', first.operationId);
expect(deps.install).toHaveBeenCalledTimes(1);
});
it('retains an unknown original operation across reconstruction without replaying install', async () => {
const deps = dependencies();
vi.mocked(deps.install).mockRejectedValue(new Error('reply lost'));
const runtime = new ManagedExtensionRuntime(deps);
const input = command();
await runtime.prepare('a', input);
await expect(runtime.activate('a', input.operationId)).rejects.toThrow(
'reply lost',
);
const replacement = new ManagedExtensionRuntime(deps);
expect(await replacement.status('a')).toMatchObject({
revision: 0,
pendingOperationId: input.operationId,
});
await expect(replacement.activate('a', input.operationId)).rejects.toThrow(
'unknown',
);
await expect(replacement.prepare('a', command())).rejects.toThrow(
'unknown',
);
expect(deps.install).toHaveBeenCalledTimes(1);
});
it('blocks concurrent activation while another Session stays available', async () => {
const deps = dependencies();
let release!: () => void;
vi.mocked(deps.drain).mockImplementation(
async () =>
new Promise<void>((resolve) => {
release = resolve;
}),
);
const runtime = new ManagedExtensionRuntime(deps);
const input = command();
await runtime.prepare('a', input);
const activation = runtime.activate('a', input.operationId);
await vi.waitFor(() => expect(release).toBeTypeOf('function'));
await expect(runtime.prepare('a', command())).rejects.toThrow('changing');
expect(await runtime.status('b')).toMatchObject({ revision: 0 });
release();
await activation;
});
it('rehydrates active metadata after restart only through exact original prepare', async () => {
const deps = dependencies();
const original = new ManagedExtensionRuntime(deps);
const input = command();
await original.prepare('root', input);
await original.activate('root', input.operationId);
const restarted = new ManagedExtensionRuntime(deps);
await expect(restarted.activate('root', input.operationId)).rejects.toThrow(
'reprepare',
);
await restarted.prepare('root', input);
await restarted.activate('root', input.operationId);
expect(deps.drain).toHaveBeenCalledTimes(1);
expect(deps.install).toHaveBeenCalledTimes(2);
});
it('rebuilds a missing active root through a new explicit apply without changing its definition paths', async () => {
const source = path.join(temporary, 'source');
await fs.mkdir(source);
await fs.writeFile(path.join(source, 'asset.txt'), 'authoritative');
const archive = path.join(temporary, 'archive.tgz');
await tar.c({ file: archive, cwd: source, gzip: true }, ['asset.txt']);
const bytes = await fs.readFile(archive);
const digest = createHash('sha256').update(bytes).digest('hex');
const deps = dependencies();
const runtime = new ManagedExtensionRuntime(deps);
await runtime.cache.upload(digest, Readable.from(bytes));
const pin = {
extensionId: 'one',
packageRevision: 1,
packageDigest: digest,
};
const first = { ...command(), extensions: [pin] };
const initial = await runtime.prepare('a', first);
await runtime.activate('a', first.operationId);
const root = initial.packages[0]!.assetRoot;
await fs.rm(root, { recursive: true });
await expect(runtime.getToolContext('a', 1, pin)).rejects.toThrow();
const next = { ...command(1), extensions: [pin] };
const rebuilt = await runtime.prepare('a', next);
expect(rebuilt.packages[0]!.assetRoot).toBe(root);
expect(await fs.readFile(path.join(root, 'asset.txt'), 'utf8')).toBe(
'authoritative',
);
await runtime.activate('a', next.operationId);
expect(await runtime.readAsset('a', 2, 'one', 'asset.txt')).toEqual({
path: 'asset.txt',
content: 'authoritative',
});
expect((await runtime.getToolContext('a', 2, pin)).assetRoot).toBe(root);
expect(deps.compile).toHaveBeenNthCalledWith(
2,
root,
pin,
{},
'a',
undefined,
);
await fs.writeFile(path.join(root, 'asset.txt'), 'drift');
await expect(runtime.getToolContext('a', 2, pin)).rejects.toThrow(
'drifted',
);
const repair = { ...command(2), extensions: [pin] };
expect((await runtime.prepare('a', repair)).packages[0]!.assetRoot).toBe(
root,
);
await runtime.activate('a', repair.operationId);
expect(await runtime.readAsset('a', 3, 'one', 'asset.txt')).toEqual({
path: 'asset.txt',
content: 'authoritative',
});
});
it('contains private assets to the active package and exact Session revision', async () => {
const source = path.join(temporary, 'source');
await fs.mkdir(source);
await fs.writeFile(path.join(source, 'asset.txt'), 'inside');
const archive = path.join(temporary, 'archive.tgz');
await tar.c({ file: archive, cwd: source, gzip: true }, ['asset.txt']);
const bytes = await fs.readFile(archive);
const digest = createHash('sha256').update(bytes).digest('hex');
const runtime = new ManagedExtensionRuntime(dependencies());
await runtime.cache.upload(digest, Readable.from(bytes));
const input = {
...command(),
extensions: [
{ extensionId: 'one', packageRevision: 1, packageDigest: digest },
],
};
const prepared = await runtime.prepare('a', input, {
baseMcpServers: [],
baseHookCatalog: null,
settings: { one: { TOKEN: 'private-secret' } },
});
expect(JSON.stringify(prepared)).not.toContain('private-secret');
await runtime.activate('a', input.operationId);
expect(
(await runtime.getToolContext('a', 1, input.extensions[0])).environment,
).toMatchObject({ TOKEN: 'private-secret' });
await expect(
runtime.getToolContext('b', 1, input.extensions[0]),
).rejects.toThrow('unavailable');
expect(await runtime.readAsset('a', 1, 'one', 'asset.txt')).toEqual({
path: 'asset.txt',
content: 'inside',
});
expect(await runtime.globAssets('a', 1, 'one', '**/*.txt')).toEqual([
'asset.txt',
]);
await expect(runtime.readAsset('a', 0, 'one', 'asset.txt')).rejects.toThrow(
'unavailable',
);
await expect(runtime.readAsset('b', 1, 'one', 'asset.txt')).rejects.toThrow(
'unavailable',
);
await expect(
runtime.readAsset('a', 1, 'one', '../../archive.tgz'),
).rejects.toThrow();
const unchanged = await runtime.prepare(
'a',
{ ...command(1), extensions: input.extensions },
{
baseMcpServers: [],
baseHookCatalog: null,
settings: { one: { TOKEN: 'private-secret' } },
},
);
expect(unchanged.packages[0]!.assetRoot).toBe(
prepared.packages[0]!.assetRoot,
);
await fs.writeFile(
path.join(prepared.packages[0]!.assetRoot, 'asset.txt'),
'drift',
);
await expect(
runtime.getToolContext('a', 1, input.extensions[0]),
).rejects.toThrow('drifted');
await expect(runtime.readAsset('a', 1, 'one', 'asset.txt')).rejects.toThrow(
'drifted',
);
});
});
it('reconciles the exact active operation after restart and does not replay its stop', async () => {
const deps: ManagedExtensionRuntimeDependencies = {
...dependencies(),
reconcile: vi.fn(async () => [
{
ownerId: 'original-owner',
sessionId: 'a',
configRevision: 1,
mode: 'developer-process-group' as const,
scope: 'process_group' as const,
state: 'stopped' as const,
},
]),
};
const runtime = new ManagedExtensionRuntime(deps);
const input = command();
await runtime.prepare('a', input);
await runtime.activate('a', input.operationId);
expect(await runtime.operationStatus('a', input.operationId)).toMatchObject({
state: 'active',
revision: 1,
});
const restarted = new ManagedExtensionRuntime(deps);
await expect(
restarted.reconcile('a', input.operationId, 0, 1),
).resolves.toMatchObject({
state: 'stopped',
operationId: input.operationId,
});
await expect(
restarted.reconcile('a', input.operationId, 0, 1),
).resolves.toMatchObject({
state: 'stopped',
operationId: input.operationId,
});
expect(deps.reconcile).toHaveBeenCalledExactlyOnceWith('a');
expect(deps.install).toHaveBeenCalledOnce();
});
it('refuses compensation when a newer active operation owns the Session', async () => {
const deps = { ...dependencies(), reconcile: vi.fn() };
const runtime = new ManagedExtensionRuntime(deps);
const first = command();
await runtime.prepare('a', first);
await runtime.activate('a', first.operationId);
const next = command(1);
await runtime.prepare('a', next);
await runtime.activate('a', next.operationId);
await expect(runtime.reconcile('a', first.operationId, 0, 1)).rejects.toThrow(
'conflicts',
);
expect(deps.reconcile).not.toHaveBeenCalled();
expect(await runtime.status('a')).toMatchObject({
operationId: next.operationId,
revision: 2,
});
});
it.each([false, true])(
'reconciles a committed prepared operation before activation with prior configuration: %s',
async (previous) => {
const deps: ManagedExtensionRuntimeDependencies = {
...dependencies(),
reconcile: vi.fn(async () => [
{
ownerId: 'original-owner',
sessionId: 'a',
configRevision: previous ? 2 : 1,
mode: 'developer-process-group' as const,
scope: 'process_group' as const,
state: 'stopped' as const,
},
]),
};
const runtime = new ManagedExtensionRuntime(deps);
if (previous) {
const initial = command();
await runtime.prepare('a', initial);
await runtime.activate('a', initial.operationId);
}
const input = command(previous ? 1 : 0);
await runtime.prepare('a', input);
const restarted = new ManagedExtensionRuntime(deps);
await expect(
restarted.reconcile(
'a',
input.operationId,
input.expectedRevision,
input.expectedRevision + 1,
),
).resolves.toMatchObject({
state: 'stopped',
operationId: input.operationId,
});
expect(deps.install).toHaveBeenCalledTimes(previous ? 1 : 0);
const recovery = command(input.expectedRevision + 1);
await restarted.prepare('a', recovery);
await restarted.activate('a', recovery.operationId);
expect(await restarted.status('a')).toMatchObject({
revision: input.expectedRevision + 2,
operationId: recovery.operationId,
});
},
);
it('retains an unknown stop for the active original owner until evidence arrives', async () => {
const reconcile = vi.fn(async () => [
{
ownerId: 'original-owner',
sessionId: 'a',
configRevision: 1,
mode: 'developer-process-group' as const,
scope: 'process_group' as const,
state: 'outcome_unknown' as 'outcome_unknown' | 'stopped',
},
]);
const runtime = new ManagedExtensionRuntime({ ...dependencies(), reconcile });
const input = command();
await runtime.prepare('a', input);
await runtime.activate('a', input.operationId);
await expect(
runtime.reconcile('a', input.operationId, 0, 1),
).resolves.toMatchObject({ state: 'outcome_unknown' });
expect(await runtime.operationStatus('a', input.operationId)).toMatchObject({
state: 'active',
});
reconcile.mockResolvedValueOnce([
{
ownerId: 'original-owner',
sessionId: 'a',
configRevision: 1,
mode: 'developer-process-group',
scope: 'process_group',
state: 'stopped',
},
]);
await expect(
runtime.reconcile('a', input.operationId, 0, 1),
).resolves.toMatchObject({ state: 'stopped' });
});
@@ -0,0 +1,998 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { createHash } from 'node:crypto';
import { promises as fs } from 'node:fs';
import path from 'node:path';
import type { Application } from 'express';
import { minimatch } from 'minimatch';
import {
parseManagedExtensionApplyCommand,
parseManagedExtensionPins,
parseManagedExtensionPin,
type ManagedExtensionApplyCommand,
type ManagedExtensionPin,
type ManagedExtensionMcpPin,
type ManagedExtensionPrepareResult,
type ManagedExtensionRuntimeControl,
type ManagedExtensionPreparedPackage,
} from '@qwen-code/qwen-code-core/managed-runtime/managed-extension-package.js';
import type { ManagedHookCatalogPin } from '@qwen-code/qwen-code-core/managed-runtime/managed-hook-protocol.js';
import type { ManagedPackageContributions } from '@qwen-code/qwen-code-core/extension/managed-package.js';
import { atomicWriteJSON } from '@qwen-code/qwen-code-core/utils/atomicFileWrite.js';
import {
authorizeManagedRuntime,
managedRuntimeJsonBody,
managedRuntimeNoStore,
type ManagedRuntimeRequestIdentity,
} from './managed-runtime-attestation-contract.js';
import { ManagedExtensionCache } from './managed-extension-cache.js';
export const MANAGED_EXTENSION_RUNTIME_ROUTE = Object.freeze({
key: 'extensions',
method: 'POST',
path: '/internal/managed-runtime/v3/extensions',
});
export const MANAGED_EXTENSION_RETIRE_ROUTE = Object.freeze({
key: 'extension-retire',
method: 'POST',
path: '/internal/managed-runtime/v3/extensions/retire',
});
export const MANAGED_EXTENSION_UPLOAD_ROUTE = Object.freeze({
key: 'extension-package-upload',
method: 'PUT',
path: '/internal/managed-runtime/v3/extensions/packages/:digest',
});
export type ManagedRuntimeExtensionPackage = ManagedExtensionPreparedPackage;
export interface ManagedExtensionRuntimeState {
readonly state: 'active' | 'failed' | 'outcome_unknown';
readonly executionMode: 'linux-cgroup' | 'developer-process-group';
readonly revision: number;
readonly extensions: readonly ManagedExtensionPin[];
readonly operationId: string | null;
readonly pendingOperationId?: string;
}
interface Prepared {
readonly ownerJournalId: string;
readonly base: {
readonly baseMcpServers: readonly ManagedExtensionMcpPin[];
readonly baseHookCatalog: ManagedHookCatalogPin | null;
};
readonly mcpServers: readonly ManagedExtensionMcpPin[];
readonly hookCatalog: ManagedHookCatalogPin | null;
readonly command: ManagedExtensionApplyCommand;
readonly packages: readonly ManagedRuntimeExtensionPackage[];
}
export interface ManagedExtensionRuntimeDependencies {
readonly ownerJournalId?: string;
readonly executionMode: 'linux-cgroup' | 'developer-process-group';
readonly directory: string;
readonly bindContext?: (
sessionId: string,
contextBinding: unknown,
) => Promise<void>;
readonly admitted: (sessionId: string) => Promise<boolean>;
readonly idle: (sessionId: string) => boolean;
readonly compile: (
directory: string,
pin: ManagedExtensionPin,
environment: Readonly<Record<string, string>>,
sessionId: string,
publishedManifest?: unknown,
) => Promise<ManagedPackageContributions>;
readonly stage?: (
sessionId: string,
command: ManagedExtensionApplyCommand,
packages: readonly ManagedRuntimeExtensionPackage[],
base: {
baseMcpServers: readonly ManagedExtensionMcpPin[];
baseHookCatalog: ManagedHookCatalogPin | null;
preparedMcpServers?: readonly ManagedExtensionMcpPin[];
},
) => Promise<{
mcpServers: readonly ManagedExtensionMcpPin[];
hookCatalog: ManagedHookCatalogPin | null;
}>;
// This is Session scoped: closing the Runtime would also stop its siblings.
readonly drain: (
sessionId: string,
command: ManagedExtensionApplyCommand,
) => Promise<void>;
readonly reconcile?: (
sessionId: string,
) => Promise<
ReadonlyArray<
import('./managed-extension-process.js').ManagedExtensionProcessReceipt
>
>;
readonly install: (
sessionId: string,
packages: readonly ManagedRuntimeExtensionPackage[],
command: ManagedExtensionApplyCommand,
) => Promise<void>;
}
export class ManagedExtensionRuntime {
readonly cache: ManagedExtensionCache;
private readonly prepared = new Map<string, Prepared>();
private readonly active = new Map<string, Prepared>();
private readonly busy = new Set<string>();
private readonly environments = new Map<
string,
Readonly<Record<string, Readonly<Record<string, string>>>>
>();
private readonly sessionOwners = new Map<string, string>();
private readonly ownerSessions = new Map<string, string>();
constructor(
private readonly dependencies: ManagedExtensionRuntimeDependencies,
) {
if (!path.isAbsolute(dependencies.directory))
throw new Error('Invalid extension Runtime directory.');
this.cache = new ManagedExtensionCache(
path.join(dependencies.directory, 'packages'),
);
}
async status(sessionId: string): Promise<ManagedExtensionRuntimeState> {
await this.requireSession(sessionId);
let pendingOperationId: string | undefined;
try {
pendingOperationId = parseManagedExtensionApplyCommand(
JSON.parse(
await fs.readFile(this.pendingPath(sessionId), 'utf8'),
) as unknown,
).operationId;
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
}
const pending = pendingOperationId ? { pendingOperationId } : {};
const installed = this.active.get(sessionId);
if (installed) return { ...this.state(installed.command), ...pending };
try {
const stored = JSON.parse(
await fs.readFile(this.statePath(sessionId), 'utf8'),
) as unknown;
const command = parseManagedExtensionApplyCommand(stored);
const stopped = await fs
.stat(`${this.operationPath(sessionId, command.operationId)}.stopped`)
.then(
() => true,
(error: NodeJS.ErrnoException) => {
if (error.code === 'ENOENT') return false;
throw error;
},
);
return {
...this.state(command),
...(stopped ? { state: 'failed' as const } : {}),
...pending,
};
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT')
return {
state: pendingOperationId ? 'outcome_unknown' : 'active',
executionMode: this.dependencies.executionMode,
revision: 0,
extensions: [],
operationId: null,
...pending,
};
throw error;
}
}
async operationStatus(sessionId: string, operationId: string) {
const current = await this.status(sessionId);
try {
const proof = JSON.parse(
await fs.readFile(
`${this.operationPath(sessionId, operationId)}.stopped`,
'utf8',
),
) as Record<string, unknown>;
if (
proof['operationId'] !== operationId ||
proof['executionMode'] !== this.dependencies.executionMode ||
proof['state'] !== 'stopped'
)
throw new Error('Original stop proof conflicts.');
return proof;
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
}
if (current.operationId === operationId && !current.pendingOperationId)
return current;
const prepared =
this.prepared.get(this.preparedKey(sessionId, operationId)) ??
(await this.loadPrepared(sessionId, operationId));
if (prepared && !current.pendingOperationId)
return this.preparedView(prepared);
return { ...current, state: 'outcome_unknown' as const, operationId };
}
async prepare(
sessionId: string,
value: unknown,
base: {
baseMcpServers: readonly ManagedExtensionMcpPin[];
baseHookCatalog: ManagedHookCatalogPin | null;
settings?: Readonly<Record<string, Readonly<Record<string, string>>>>;
toolProfile?: string;
packageManifests?: Readonly<Record<string, unknown>>;
} = { baseMcpServers: [], baseHookCatalog: null },
): Promise<ManagedExtensionPrepareResult> {
const command = parseManagedExtensionApplyCommand(value);
return this.lock(sessionId, async () => {
const status = await this.status(sessionId);
const key = this.preparedKey(sessionId, command.operationId);
const existing =
this.prepared.get(key) ??
(await this.loadPrepared(sessionId, command.operationId));
const publicBase = {
baseMcpServers: base.baseMcpServers,
baseHookCatalog: base.baseHookCatalog,
};
if (existing) {
if (
JSON.stringify(existing.command) !== JSON.stringify(command) ||
JSON.stringify(existing.base) !== JSON.stringify(publicBase)
)
throw new Error('Extension operation identity conflicts.');
for (const pkg of existing.packages) {
await this.cache.restoreMaterialized(
pkg.pin.packageDigest,
pkg.assetRoot,
);
const contributions = await this.dependencies.compile(
pkg.assetRoot,
pkg.pin,
base.settings?.[pkg.pin.extensionId] ?? {},
sessionId,
base.packageManifests?.[pkg.pin.extensionId],
);
validateExtensionToolProfile(contributions, base.toolProfile);
if (
JSON.stringify(contributions) !== JSON.stringify(pkg.contributions)
)
throw new Error('Prepared extension package drifted.');
}
const catalogs = this.dependencies.stage
? await this.dependencies.stage(
sessionId,
command,
existing.packages,
{ ...publicBase, preparedMcpServers: existing.mcpServers },
)
: { mcpServers: [], hookCatalog: null };
if (
JSON.stringify(catalogs.mcpServers) !==
JSON.stringify(existing.mcpServers) ||
JSON.stringify(catalogs.hookCatalog) !==
JSON.stringify(existing.hookCatalog)
)
throw new Error('Prepared extension catalogs drifted.');
this.environments.set(key, base.settings ?? {});
this.prepared.set(key, existing);
return this.preparedView(existing);
}
if (status.pendingOperationId)
throw new Error('Original extension activation outcome is unknown.');
if (status.revision !== command.expectedRevision)
throw new Error('Extension configuration revision conflicts.');
if (!this.dependencies.idle(sessionId))
throw new Error('Extension configuration requires an idle Session.');
const packages: ManagedRuntimeExtensionPackage[] = [];
try {
for (const pin of command.extensions) {
const unchanged = this.active
.get(sessionId)
?.packages.find(
(entry) => JSON.stringify(entry.pin) === JSON.stringify(pin),
);
const assetRoot =
unchanged?.assetRoot ??
(await this.cache.materialize(pin.packageDigest));
await this.cache.restoreMaterialized(pin.packageDigest, assetRoot);
packages.push({
pin,
assetRoot,
contributions: {
skills: [],
commands: [],
agents: [],
context: [],
mcpServers: {},
hooks: {},
settings: [],
},
});
const contributions = await this.dependencies.compile(
assetRoot,
pin,
base.settings?.[pin.extensionId] ?? {},
sessionId,
base.packageManifests?.[pin.extensionId],
);
validateExtensionToolProfile(contributions, base.toolProfile);
packages[packages.length - 1] = { pin, assetRoot, contributions };
}
} catch (error) {
await Promise.all(
packages.map((entry) =>
this.active
.get(sessionId)
?.packages.some((active) => active.assetRoot === entry.assetRoot)
? Promise.resolve()
: fs.rm(entry.assetRoot, { recursive: true, force: true }),
),
);
throw error;
}
const catalogs = this.dependencies.stage
? await this.dependencies.stage(sessionId, command, packages, base)
: { mcpServers: [], hookCatalog: null };
if (
!this.dependencies.stage &&
packages.some(
(entry) =>
Object.keys(entry.contributions.mcpServers).length ||
Object.keys(entry.contributions.hooks).length,
)
)
throw new Error('Extension process components are unsupported.');
const prepared: Prepared = {
ownerJournalId: this.dependencies.ownerJournalId ?? 'in-memory',
command,
packages,
base: publicBase,
mcpServers: catalogs.mcpServers,
hookCatalog: catalogs.hookCatalog,
};
const view = this.preparedView(prepared);
await fs.mkdir(
path.dirname(this.operationPath(sessionId, command.operationId)),
{ recursive: true, mode: 0o700 },
);
await atomicWriteJSON(
this.operationPath(sessionId, command.operationId),
prepared,
{ mode: 0o600 },
);
this.environments.set(key, base.settings ?? {});
this.prepared.set(key, prepared);
return view;
});
}
async reconcile(
sessionId: string,
operationId: string,
expectedRevision: number,
revision: number,
) {
return this.lock(sessionId, async () => {
const prepared = await this.loadPrepared(sessionId, operationId);
if (!prepared)
throw new Error('Original extension owner journal is unavailable.');
try {
const proof = JSON.parse(
await fs.readFile(
`${this.operationPath(sessionId, operationId)}.stopped`,
'utf8',
),
) as Record<string, unknown>;
if (
proof['operationId'] !== operationId ||
proof['revision'] !== revision ||
expectedRevision + 1 !== revision ||
proof['executionMode'] !== this.dependencies.executionMode ||
proof['state'] !== 'stopped'
)
throw new Error('Original stop proof conflicts.');
return proof;
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
}
const command = prepared.command;
const current = await this.status(sessionId);
if (
command.operationId !== operationId ||
command.expectedRevision !== expectedRevision ||
revision !== expectedRevision + 1 ||
(current.pendingOperationId !== undefined &&
current.pendingOperationId !== operationId) ||
(current.revision !== expectedRevision &&
(current.operationId !== operationId ||
current.revision !== revision ||
JSON.stringify(current.extensions) !==
JSON.stringify(command.extensions)))
)
throw new Error('Original extension operation conflicts.');
const receipts = this.dependencies.reconcile
? await this.dependencies.reconcile(sessionId)
: undefined;
if (
!receipts ||
receipts.length === 0 ||
receipts.some((entry) => entry.state !== 'stopped')
)
return {
operationId,
revision,
state: 'outcome_unknown',
executionMode: this.dependencies.executionMode,
};
const proof = {
operationId,
revision,
state: 'stopped' as const,
executionMode: this.dependencies.executionMode,
receipts,
};
await atomicWriteJSON(
`${this.operationPath(sessionId, operationId)}.stopped`,
proof,
{ mode: 0o600 },
);
await atomicWriteJSON(this.statePath(sessionId), command, {
mode: 0o600,
});
this.active.delete(sessionId);
await fs.rm(this.pendingPath(sessionId), { force: true });
return proof;
});
}
async activate(
sessionId: string,
operationId: string,
): Promise<ManagedExtensionRuntimeState> {
return this.lock(sessionId, async () => {
const status = await this.status(sessionId);
if (status.pendingOperationId)
throw new Error('Original extension activation outcome is unknown.');
if (status.operationId === operationId) {
if (!this.active.has(sessionId)) {
const restored = this.prepared.get(
this.preparedKey(sessionId, operationId),
);
if (!restored)
throw new Error(
'Active extension configuration requires exact reprepare.',
);
await this.dependencies.install(
sessionId,
restored.packages,
restored.command,
);
this.active.set(sessionId, restored);
}
return status;
}
const prepared = this.prepared.get(
this.preparedKey(sessionId, operationId),
);
if (!prepared || status.revision !== prepared.command.expectedRevision)
throw new Error('Extension configuration was not prepared.');
if (!this.dependencies.idle(sessionId))
throw new Error('Extension configuration requires an idle Session.');
await fs.mkdir(path.dirname(this.statePath(sessionId)), {
recursive: true,
mode: 0o700,
});
await atomicWriteJSON(this.pendingPath(sessionId), prepared.command, {
mode: 0o600,
});
await this.dependencies.drain(sessionId, prepared.command);
await this.requireSession(sessionId);
if (!this.dependencies.idle(sessionId))
throw new Error('Extension Session acquired work while draining.');
await this.dependencies.install(
sessionId,
prepared.packages,
prepared.command,
);
await fs.mkdir(path.dirname(this.statePath(sessionId)), {
recursive: true,
mode: 0o700,
});
await atomicWriteJSON(this.statePath(sessionId), prepared.command, {
mode: 0o600,
});
this.active.set(sessionId, prepared);
await fs.rm(this.pendingPath(sessionId));
return this.state(prepared.command);
});
}
async getToolContext(
sessionId: string,
configRevision: number,
value: unknown,
): Promise<{
assetRoot: string;
environment: Readonly<Record<string, string>>;
}> {
await this.requireSession(sessionId);
if (this.busy.has(sessionId))
throw new Error('Extension configuration is changing.');
const status = await this.status(sessionId);
const active = this.active.get(sessionId);
const pin = parseManagedExtensionPin(value);
const pkg = active?.packages.find(
(entry) => JSON.stringify(entry.pin) === JSON.stringify(pin),
);
if (
!active ||
!pkg ||
status.pendingOperationId ||
status.revision !== configRevision
)
throw new Error('Extension tool configuration is unavailable.');
const environment = this.environments.get(
this.preparedKey(sessionId, active.command.operationId),
);
if (!environment)
throw new Error('Extension settings require exact reprepare.');
await this.cache.verifyMaterialized(pin.packageDigest, pkg.assetRoot);
return {
assetRoot: pkg.assetRoot,
environment: {
...environment[pin.extensionId],
QWEN_EXTENSION_ROOT: pkg.assetRoot,
},
};
}
async bindSession(
sessionId: string,
runtimeSessionId: string | undefined,
contextBinding: unknown,
): Promise<void> {
if (this.dependencies.bindContext)
await this.dependencies.bindContext(sessionId, contextBinding);
if (runtimeSessionId === undefined) return;
const existing = this.ownerSessions.get(runtimeSessionId);
if (existing && existing !== sessionId)
throw new Error('Extension Runtime owner conflicts.');
this.ownerSessions.set(runtimeSessionId, sessionId);
this.sessionOwners.set(sessionId, runtimeSessionId);
}
runtimeSessionId(sessionId: string): string {
return this.sessionOwners.get(sessionId) ?? sessionId;
}
isChanging(sessionId: string): boolean {
return this.busy.has(this.ownerSessions.get(sessionId) ?? sessionId);
}
async readAsset(
sessionId: string,
revision: number,
extensionId: string,
relativePath: string,
) {
const root = await this.assetRoot(sessionId, revision, extensionId);
const file = await this.contained(root, relativePath);
const handle = await fs.open(file, 'r');
try {
const stat = await handle.stat();
if (!stat.isFile() || stat.size > 256 * 1024)
throw new Error('Extension asset exceeds the read limit.');
return { path: relativePath, content: await handle.readFile('utf8') };
} finally {
await handle.close();
}
}
async globAssets(
sessionId: string,
revision: number,
extensionId: string,
pattern: string,
) {
const root = await this.assetRoot(sessionId, revision, extensionId);
if (pattern.length > 1024 || pattern.includes('\0'))
throw new Error('Invalid extension glob.');
const paths: string[] = [];
const visit = async (directory: string) => {
for (const entry of await fs.readdir(directory, {
withFileTypes: true,
})) {
const filename = path.join(directory, entry.name);
const relative = path
.relative(root, filename)
.split(path.sep)
.join('/');
if (entry.isSymbolicLink())
throw new Error('Extension assets contain a link.');
if (entry.isDirectory()) await visit(filename);
else if (
entry.isFile() &&
minimatch(relative, pattern, { dot: true })
) {
if (paths.length === 2048)
throw new Error('Extension glob exceeds its result limit.');
paths.push(relative);
}
}
};
await visit(root);
return paths.sort();
}
private async assetRoot(
sessionId: string,
revision: number,
extensionId: string,
): Promise<string> {
const status = await this.status(sessionId);
const active = this.active.get(sessionId);
if (this.isChanging(sessionId) || !active || status.revision !== revision)
throw new Error('Extension configuration is unavailable.');
const entry = active.packages.find(
(item) => item.pin.extensionId === extensionId,
);
if (!entry)
throw new Error('Extension package is not active in this Session.');
if (status.pendingOperationId)
throw new Error('Extension activation outcome is unknown.');
await this.cache.verifyMaterialized(
entry.pin.packageDigest,
entry.assetRoot,
);
return fs.realpath(entry.assetRoot);
}
private async contained(root: string, relative: string): Promise<string> {
if (
!relative ||
relative.includes('\\') ||
relative.includes('\0') ||
path.isAbsolute(relative)
)
throw new Error('Invalid extension asset path.');
const filename = await fs.realpath(path.resolve(root, relative));
const resolved = path.relative(root, filename);
if (
resolved === '..' ||
resolved.startsWith(`..${path.sep}`) ||
path.isAbsolute(resolved)
)
throw new Error('Extension asset path escapes its package.');
return filename;
}
private preparedView(value: Prepared): ManagedExtensionPrepareResult {
const result: ManagedExtensionPrepareResult = {
operationId: value.command.operationId,
state: 'prepared',
expectedRevision: value.command.expectedRevision,
revision: value.command.expectedRevision + 1,
executionMode: this.dependencies.executionMode,
packages: value.packages,
mcpServers: value.mcpServers,
hookCatalog: value.hookCatalog,
};
if (Buffer.byteLength(JSON.stringify(result)) > 8 * 1024 * 1024)
throw new Error('Extension prepare response exceeds its size limit.');
return result;
}
private state(
command: ManagedExtensionApplyCommand,
): ManagedExtensionRuntimeState {
return {
state: 'active',
executionMode: this.dependencies.executionMode,
revision: command.expectedRevision + 1,
extensions: command.extensions,
operationId: command.operationId,
};
}
private async loadPrepared(
sessionId: string,
operationId: string,
): Promise<Prepared | undefined> {
try {
const value = JSON.parse(
await fs.readFile(this.operationPath(sessionId, operationId), 'utf8'),
) as Prepared;
if (
value.ownerJournalId !==
(this.dependencies.ownerJournalId ?? 'in-memory')
)
throw new Error('Original extension owner journal is unavailable.');
const command = parseManagedExtensionApplyCommand(value.command);
if (
command.operationId !== operationId ||
!Array.isArray(value.packages) ||
value.packages.length !== command.extensions.length
)
throw new Error('Invalid prepared extension receipt.');
const pins = parseManagedExtensionPins(
value.packages.map((pkg) => pkg.pin),
);
if (JSON.stringify(pins) !== JSON.stringify(command.extensions))
throw new Error('Prepared extension pins conflict.');
for (const pkg of value.packages) {
if (
typeof pkg.assetRoot !== 'string' ||
path.dirname(pkg.assetRoot) !== this.cache.directory ||
!path.basename(pkg.assetRoot).startsWith(`${pkg.pin.packageDigest}-`)
)
throw new Error('Invalid prepared extension asset root.');
}
this.preparedView(value);
return value;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
throw error;
}
}
private operationPath(sessionId: string, operationId: string): string {
return `${this.statePath(sessionId)}.${createHash('sha256').update(operationId).digest('hex')}.prepared`;
}
private statePath(sessionId: string): string {
return path.join(
this.dependencies.directory,
'sessions',
`${createHash('sha256').update(sessionId).digest('hex')}.json`,
);
}
private pendingPath(sessionId: string): string {
return `${this.statePath(sessionId)}.pending`;
}
private preparedKey(sessionId: string, operationId: string): string {
return `${sessionId}\0${operationId}`;
}
private async requireSession(sessionId: string): Promise<void> {
if (!sessionId || !(await this.dependencies.admitted(sessionId)))
throw new Error('Extension Session is unavailable.');
}
private async lock<T>(sessionId: string, work: () => Promise<T>): Promise<T> {
if (this.busy.has(sessionId))
throw new Error('Extension Session configuration is changing.');
this.busy.add(sessionId);
try {
await this.requireSession(sessionId);
return await work();
} finally {
this.busy.delete(sessionId);
}
}
}
export function registerManagedExtensionRuntimeRoutes(
app: Application,
identity: ManagedRuntimeRequestIdentity & {
readonly tenantId: string;
readonly workspaceId: string;
},
runtime: ManagedExtensionRuntime,
): void {
app.put(
MANAGED_EXTENSION_UPLOAD_ROUTE.path,
managedRuntimeNoStore,
authorizeManagedRuntime(identity),
async (req, res) => {
try {
if (req.headers['content-type'] !== 'application/gzip')
throw new Error('Invalid package content type.');
await runtime.cache.upload(String(req.params['digest']), req);
res.json({ protocolVersion: 1, packageDigest: req.params['digest'] });
} catch {
res.status(400).json({ code: 'managed_extension_package_invalid' });
}
},
);
app.post(
MANAGED_EXTENSION_RUNTIME_ROUTE.path,
managedRuntimeNoStore,
authorizeManagedRuntime(identity),
managedRuntimeJsonBody(512 * 1024),
async (req, res) => {
try {
const body = req.body as Record<string, unknown>;
if (
!body ||
![
'contextBinding,operation,protocolVersion,sessionKey',
'contextBinding,operation,protocolVersion,runtimeSessionId,sessionKey',
].includes(Object.keys(body).sort().join(',')) ||
body['protocolVersion'] !== 1 ||
(body['runtimeSessionId'] !== undefined &&
typeof body['runtimeSessionId'] !== 'string')
)
throw new Error('Invalid extension request.');
const runtimeSessionId = body['runtimeSessionId'] as string | undefined;
const sessionKey = body['sessionKey'] as Record<string, unknown>;
if (
!sessionKey ||
Object.keys(sessionKey).sort().join(',') !==
'sessionId,tenantId,workspaceId' ||
sessionKey['tenantId'] !== identity.tenantId ||
sessionKey['workspaceId'] !== identity.workspaceId ||
typeof sessionKey['sessionId'] !== 'string' ||
!/^[A-Za-z0-9._:-]{1,128}$/u.test(sessionKey['sessionId'])
)
throw new Error('Extension Session scope conflicts.');
const sessionId = sessionKey['sessionId'];
await runtime.bindSession(
sessionId,
runtimeSessionId,
body['contextBinding'],
);
const operation = body['operation'] as Record<string, unknown>;
let result: unknown;
if (
operation['kind'] === 'prepare' ||
operation['kind'] === 'extension-prepare'
) {
const control = operation as unknown as Extract<
ManagedExtensionRuntimeControl,
{ kind: 'extension-prepare' }
>;
const parsed = parseManagedExtensionApplyCommand(control.command);
if (
control.kind === 'extension-prepare' &&
(control.operationId !== parsed.operationId ||
control.expectedRevision !== parsed.expectedRevision ||
JSON.stringify(control.extensions) !==
JSON.stringify(parsed.extensions))
)
throw new Error('Extension prepare identity conflicts.');
result = await runtime.prepare(
sessionId,
parsed,
control.kind === 'extension-prepare'
? {
toolProfile:
typeof operation['toolProfile'] === 'string'
? operation['toolProfile']
: undefined,
packageManifests: operation['packageManifests'] as
| Readonly<Record<string, unknown>>
| undefined,
baseMcpServers: control.baseMcpServers,
baseHookCatalog: control.baseHookCatalog,
settings: operation['settings'] as
| Readonly<Record<string, Readonly<Record<string, string>>>>
| undefined,
}
: undefined,
);
} else if (
(operation['kind'] === 'activate' ||
operation['kind'] === 'extension-activate') &&
typeof operation['operationId'] === 'string'
)
result = await runtime.activate(sessionId, operation['operationId']);
else if (
operation['kind'] === 'extension-reconcile' &&
typeof operation['operationId'] === 'string' &&
Number.isSafeInteger(operation['expectedRevision']) &&
Number.isSafeInteger(operation['revision'])
)
result = await runtime.reconcile(
sessionId,
operation['operationId'],
Number(operation['expectedRevision']),
Number(operation['revision']),
);
else if (
operation['kind'] === 'status' ||
operation['kind'] === 'extension-status'
)
result =
typeof operation['operationId'] === 'string'
? await runtime.operationStatus(
sessionId,
operation['operationId'],
)
: await runtime.status(sessionId);
else if (
(operation['kind'] === 'asset-read' ||
operation['kind'] === 'extension-asset-read') &&
Number.isSafeInteger(operation['revision']) &&
typeof operation['extensionId'] === 'string' &&
typeof operation['path'] === 'string'
)
result = await runtime.readAsset(
sessionId,
Number(operation['revision']),
operation['extensionId'],
operation['path'],
);
else if (
(operation['kind'] === 'asset-glob' ||
operation['kind'] === 'extension-asset-glob') &&
Number.isSafeInteger(operation['revision']) &&
typeof operation['extensionId'] === 'string' &&
typeof operation['pattern'] === 'string'
)
result = await runtime.globAssets(
sessionId,
Number(operation['revision']),
operation['extensionId'],
operation['pattern'],
);
else throw new Error('Invalid extension operation.');
if (
typeof operation['operationId'] === 'string' &&
String(operation['kind']).includes('asset-')
)
result = Array.isArray(result)
? { operationId: operation['operationId'], paths: result }
: { ...(result as object), operationId: operation['operationId'] };
res.json({
protocolVersion: 1,
sessionKey,
...(runtimeSessionId ? { runtimeSessionId } : {}),
result,
});
} catch {
res
.status(409)
.json({ code: 'managed_extension_configuration_unavailable' });
}
},
);
}
function validateExtensionToolProfile(
value: ManagedPackageContributions,
profile: string | undefined,
): void {
if (profile === undefined) return;
const shell =
profile === 'hosted-workspace-shell/1' ||
profile === 'hosted-workspace-team-lead/1';
const mcp = shell || profile === 'hosted-workspace-mcp/1';
if (!mcp && Object.keys(value.mcpServers).length)
throw new Error('Session profile does not admit extension MCP.');
if (
!shell &&
(JSON.stringify(value.hooks).includes('"type":"command"') ||
Object.values(value.mcpServers).some(
(recipe) => recipe && typeof recipe === 'object' && 'command' in recipe,
) ||
value.commands.some((command) => /!\{|!`/u.test(command.prompt)))
)
throw new Error(
'Session profile does not admit extension Shell execution.',
);
const base = ['read_file', 'glob'];
if (profile !== 'hosted-workspace-read/1')
base.push(
'write_file',
'edit',
'list_directory',
'grep_search',
'todo_write',
);
if (shell) base.push('run_shell_command');
for (const agent of value.agents)
if (
agent.tools?.some(
(tool) => !base.includes(tool) && !(mcp && tool.startsWith('mcp__')),
)
)
throw new Error('Extension agent tools exceed the Session profile.');
}
@@ -0,0 +1,66 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { promises as fs } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { expect, it, vi } from 'vitest';
import { managedToolDigest } from '@qwen-code/qwen-code-core/tools/managed-tool-protocol.js';
import { ManagedExtensionExecutionUnknownError } from './managed-extension-error.js';
import {
createManagedToolSet,
ManagedToolExecutor,
ManagedMcpToolUnknownError,
} from './managed-runtime-tool-executor.js';
it('retains an unproven extension Shell as unknown without settling or replaying the native invocation', async () => {
const directory = await fs.mkdtemp(
path.join(os.tmpdir(), 'extension-executor-'),
);
const runShell = vi.fn(async () => {
throw new ManagedExtensionExecutionUnknownError(
'Original owner stop is unknown.',
);
});
const native = createManagedToolSet(directory, 'owner');
const resolver = vi.fn(async () => ({ ...native, runShell }));
const executor = new ManagedToolExecutor(resolver);
const input = {
command: 'printf test',
managedExtension: {
harnessSessionId: 'root',
configRevision: 1,
pin: {
extensionId: 'one',
packageRevision: 1,
packageDigest: 'a'.repeat(64),
},
},
};
const reference = {
sessionId: 'owner',
promptId: 'prompt',
callId: 'original-step',
argsDigest: managedToolDigest(input),
};
try {
await expect(
executor.execute(reference, 'run_shell_command', input),
).rejects.toBeInstanceOf(ManagedMcpToolUnknownError);
expect(executor.status(reference)).toMatchObject({ state: 'unknown' });
expect(executor.isSessionConfigurationIdle('owner')).toBe(false);
expect(resolver).toHaveBeenCalledWith(reference, {
toolName: 'run_shell_command',
input,
});
await expect(
executor.execute(reference, 'run_shell_command', input),
).rejects.toBeInstanceOf(ManagedMcpToolUnknownError);
expect(runShell).toHaveBeenCalledTimes(1);
} finally {
await executor.close();
await fs.rm(directory, { recursive: true, force: true });
}
});
@@ -0,0 +1,92 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { promises as fs } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { expect, it, vi } from 'vitest';
import { ManagedRuntimeLedger } from './managed-runtime-ledger.js';
import { ManagedExtensionProcessOwners } from './managed-extension-process.js';
import { runManagedExtensionShell } from './managed-extension-shell.js';
import { ManagedExtensionExecutionUnknownError } from './managed-extension-error.js';
it.skipIf(process.platform !== 'darwin')(
'runs with exact private environment and seals capture after registered group stop',
async () => {
const directory = await fs.mkdtemp(
path.join(os.tmpdir(), 'extension-shell-'),
);
const ledger = ManagedRuntimeLedger.create({
workFile: path.join(directory, 'ledger.json'),
worker: {
pid: process.pid,
pgid: process.pid,
incarnation: 'test',
startedAt: Date.now(),
groupOwned: false,
},
});
const owners = new ManagedExtensionProcessOwners(
'developer-process-group',
{ ledger, directory: path.join(directory, 'owners') },
);
const write = vi.fn(
async (_stream: 'stdout' | 'stderr', _chunk: Buffer) => {},
);
const capture = {
write,
finish: vi.fn(async () => {}),
setStarted: vi.fn(),
setProcessResult: vi.fn(),
};
try {
const result = await runManagedExtensionShell(owners, {
sessionId: 'root',
configRevision: 1,
command: 'printf "%s" "$EXT_TEST_TOKEN"',
cwd: directory,
environment: { EXT_TEST_TOKEN: 'private-value', PATH: '/usr/bin:/bin' },
signal: new AbortController().signal,
capture,
});
expect(result.llmContent).toBe('private-value');
expect(
Buffer.concat(
write.mock.calls.map((call) => call[1] as Buffer),
).toString(),
).toBe('private-value');
expect(capture.finish).toHaveBeenCalledWith('stdout', true);
expect(await owners.hasLiveSessionOwners('root')).toBe(false);
capture.finish.mockClear();
const stop = vi.spyOn(owners, 'stop').mockResolvedValueOnce({
ownerId: 'unproven',
sessionId: 'root',
configRevision: 1,
mode: 'developer-process-group',
scope: 'process_group',
state: 'outcome_unknown',
});
try {
await expect(
runManagedExtensionShell(owners, {
sessionId: 'root',
configRevision: 1,
command: 'printf unproven',
cwd: directory,
environment: { PATH: '/usr/bin:/bin' },
signal: new AbortController().signal,
capture,
}),
).rejects.toBeInstanceOf(ManagedExtensionExecutionUnknownError);
expect(capture.finish).not.toHaveBeenCalled();
} finally {
stop.mockRestore();
}
} finally {
await owners.stopAll();
await fs.rm(directory, { force: true, recursive: true });
}
},
);
@@ -0,0 +1,150 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { ManagedExtensionExecutionUnknownError } from './managed-extension-error.js';
import { ToolErrorType } from '@qwen-code/qwen-code-core/tools/tool-error.js';
import { getShellConfiguration } from '@qwen-code/qwen-code-core/utils/shell-utils.js';
import type {
ShellRawCaptureSink,
ShellExecutionResult,
} from '@qwen-code/qwen-code-core/services/shellExecutionService.js';
import type { ToolResult } from '@qwen-code/qwen-code-core/tools/tools.js';
import type { ManagedExtensionProcessOwners } from './managed-extension-process.js';
export async function runManagedExtensionShell(
owners: ManagedExtensionProcessOwners,
input: {
sessionId: string;
configRevision: number;
command: string;
cwd: string;
environment: NodeJS.ProcessEnv;
signal: AbortSignal;
capture?: ShellRawCaptureSink;
timeoutMs?: number;
},
): Promise<ToolResult> {
input.signal.throwIfAborted();
const shell = getShellConfiguration();
const { child, ownerId } = await owners.start({
sessionId: input.sessionId,
configRevision: input.configRevision,
executable: shell.executable,
args: [...shell.argsPrefix, input.command],
cwd: input.cwd,
env: input.environment,
});
const streamsClosed = new Promise<void>((resolve) =>
child.once('close', () => resolve()),
);
if (child.pid) input.capture?.setStarted(child.pid);
child.stdin?.end();
const chunks: Buffer[] = [];
let retained = 0;
let exceeded = false;
const captures = { stdout: Promise.resolve(), stderr: Promise.resolve() };
for (const id of ['stdout', 'stderr'] as const) {
child[id]?.on('data', (chunk: Buffer) => {
if (input.capture) {
child[id]?.pause();
captures[id] = captures[id]
.then(() => input.capture!.write(id, chunk))
.finally(() => child[id]?.resume());
}
const remaining = Math.max(0, 1024 * 1024 - retained);
if (chunk.length > remaining) exceeded = true;
if (remaining) {
const prefix = chunk.subarray(0, remaining);
chunks.push(prefix);
retained += prefix.length;
}
});
}
let timedOut = false;
let timer: ReturnType<typeof setTimeout> | undefined;
let wake: (() => void) | undefined;
const stop = () => {
wake?.();
};
try {
const exited = new Promise<void>((resolve) => {
if (child.exitCode !== null || child.signalCode !== null) resolve();
else child.once('exit', () => resolve());
});
const interrupted = new Promise<void>((resolve) => {
wake = resolve;
input.signal.addEventListener('abort', stop, { once: true });
if (input.signal.aborted) resolve();
timer = setTimeout(() => {
timedOut = true;
resolve();
}, input.timeoutMs ?? 120000);
});
await Promise.race([exited, interrupted]);
const receipt = await owners.stop(ownerId).catch(() => {
throw new ManagedExtensionExecutionUnknownError(
'Extension Shell process stop is unknown.',
);
});
if (receipt.state !== 'stopped')
throw new ManagedExtensionExecutionUnknownError(
'Extension Shell process stop is unknown.',
);
let drainTimer: ReturnType<typeof setTimeout> | undefined;
try {
await Promise.race([
streamsClosed,
new Promise<never>((_resolve, reject) => {
drainTimer = setTimeout(
() =>
reject(
new ManagedExtensionExecutionUnknownError(
'Extension Shell output closure is unknown.',
),
),
5000,
);
}),
]);
} finally {
if (drainTimer) clearTimeout(drainTimer);
}
await Promise.all(Object.values(captures));
const rawOutput = Buffer.concat(chunks);
const output = rawOutput.toString('utf8');
const result: ShellExecutionResult = {
rawOutput,
output,
exitCode: child.exitCode,
signal: null,
error: null,
aborted: input.signal.aborted || timedOut,
pid: child.pid,
executionMethod: 'child_process',
outputCaptureLimitExceeded: exceeded,
};
input.capture?.setProcessResult(result);
await input.capture?.finish('stdout', true);
await input.capture?.finish('stderr', true);
return {
llmContent: `${output}${exceeded ? '\n[Output truncated]' : ''}${timedOut ? '\n[Command timed out]' : ''}`,
returnDisplay: output,
...(child.exitCode !== 0 || result.aborted
? {
error: {
message: result.aborted
? 'Shell command stopped.'
: `Shell exited with code ${child.exitCode}.`,
type: ToolErrorType.EXECUTION_FAILED,
},
}
: {}),
};
} finally {
if (timer) clearTimeout(timer);
input.signal.removeEventListener('abort', stop);
}
}
@@ -0,0 +1,90 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type { ChildProcess } from 'node:child_process';
import {
ReadBuffer,
serializeMessage,
} from '@modelcontextprotocol/sdk/shared/stdio.js';
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
import type { ManagedExtensionProcessOwners } from './managed-extension-process.js';
export class ManagedExtensionStdioTransport implements Transport {
onclose?: Transport['onclose'];
onerror?: Transport['onerror'];
onmessage?: Transport['onmessage'];
private child?: ChildProcess;
private ownerId?: string;
private closed = false;
private readonly buffer = new ReadBuffer();
constructor(
private readonly owners: ManagedExtensionProcessOwners,
private readonly spec: Parameters<
ManagedExtensionProcessOwners['start']
>[0],
) {}
get processOwnerId(): string | undefined {
return this.ownerId;
}
async start(): Promise<void> {
if (this.child)
throw new Error('Extension stdio transport already started.');
const owner = await this.owners.start(this.spec);
this.ownerId = owner.ownerId;
this.child = owner.child;
this.child.stderr?.on('data', () => undefined);
this.child.on('error', (error) => this.onerror?.(error));
this.child.stdin?.on('error', (error) => this.onerror?.(error));
this.child.stdout?.on('data', (chunk: Buffer) => {
this.buffer.append(chunk);
try {
let message: JSONRPCMessage | null;
while ((message = this.buffer.readMessage()) !== null)
this.onmessage?.(message);
} catch (error) {
this.onerror?.(
error instanceof Error ? error : new Error('Invalid MCP message.'),
);
}
});
this.child.on('exit', () => {
void this.close().catch((error: unknown) =>
this.onerror?.(
error instanceof Error
? error
: new Error('Extension process stop is unknown.'),
),
);
});
}
async send(message: JSONRPCMessage): Promise<void> {
const input = this.child?.stdin;
if (!input || this.closed)
throw new Error('Extension stdio transport is closed.');
await new Promise<void>((resolve, reject) => {
input.write(serializeMessage(message), (error) =>
error ? reject(error) : resolve(),
);
});
}
async close(): Promise<void> {
if (this.closed) return;
if (!this.ownerId)
throw new Error('Extension process owner is unavailable.');
this.child?.stdin?.end();
const receipt = await this.owners.stop(this.ownerId);
if (receipt.state !== 'stopped')
throw new Error('Extension process stop outcome is unknown.');
this.closed = true;
this.onclose?.();
}
}
@@ -149,6 +149,42 @@ async function settled(instance: ManagedHookRuntime, id = 'execution') {
return view!;
}
describe('ManagedHookRuntime', () => {
it('keeps a Session package catalog out of a sibling Session', async () => {
const instance = new ManagedHookRuntime(
{ ...key, workspaceGeneration: '1' },
async () => directory,
{ version: 1, catalogs: [] },
);
runtimes.push(instance);
instance.installSessionManifest(key.sessionId, {
version: 1,
catalogs: [
{
...pin,
tenantId: key.tenantId,
workspaceId: key.workspaceId,
hooks: [definition()],
},
],
});
const query = {
kind: 'hook-catalog',
sessionKey: key,
operationId: 'catalog-lookup',
pin,
};
expect(
(await instance.control('runtime-a', query)).catalog?.hooks,
).toHaveLength(1);
await expect(
instance.control('runtime-b', {
...query,
sessionKey: { ...key, sessionId: 'other-session' },
operationId: 'other-lookup',
}),
).rejects.toThrow('catalog_unavailable');
});
it.each([1, 2])(
'fences native hook dispatch when CSI seals during directory lookup %s',
async (gateLookup) => {
+134 -47
View File
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { managedExtensionEnvironment } from './managed-extension-environment.js';
import { createHash } from 'node:crypto';
import { createAbortController } from '@qwen-code/qwen-code-core/utils/abortController.js';
import { readFileSync } from 'node:fs';
@@ -40,6 +41,8 @@ import {
type RegisteredHandlerRef,
} from '@qwen-code/qwen-code-core/managed-runtime/managed-hook-protocol.js';
import type { ManagedSessionKey } from '@qwen-code/qwen-code-core/managed-runtime/managed-session-records.js';
import type { ManagedExtensionProcessOwners } from './managed-extension-process.js';
import { executeManagedExtensionCommandHook } from './managed-extension-hook.js';
export interface ManagedHookDefinition
extends Omit<ManagedHookDescriptor, 'config' | 'handler' | 'plannerKey'> {
@@ -513,6 +516,7 @@ interface Operation {
export class ManagedHookRuntime {
private readonly manifest: ManagedHookManifest;
private readonly sessionManifests = new Map<string, ManagedHookManifest>();
private readonly operations = new Map<string, Operation>();
private readonly gates = new ManagedOperationGrantGate();
private readonly shutdown = createAbortController();
@@ -531,10 +535,48 @@ export class ManagedHookRuntime {
runtimeSessionId: string,
) => Promise<string | undefined>,
manifest: ManagedHookManifest,
private readonly extensionProcesses?: ManagedExtensionProcessOwners,
) {
this.manifest = parseManifest(manifest);
}
resolvePinnedCatalog(
pin: ManagedHookCatalogPin,
): ManagedHookManifest['catalogs'][number] {
const available = [
...this.manifest.catalogs,
...[...this.sessionManifests.values()].flatMap(
(manifest) => manifest.catalogs,
),
];
const found = available.find(
(entry) =>
entry.catalogId === pin.catalogId &&
entry.catalogRevision === pin.catalogRevision &&
entry.definitionDigest === pin.definitionDigest,
);
if (!found) throw new ManagedHookError('managed_hook_catalog_unavailable');
return structuredClone(found);
}
installSessionManifest(
runtimeSessionId: string,
manifest: ManagedHookManifest,
): void {
const parsed = parseManifest(manifest);
if (this.hasExtensionHolds(runtimeSessionId))
throw new ManagedHookError('managed_hook_session_busy');
if (
parsed.catalogs.some(
(entry) =>
entry.tenantId !== this.workspace.tenantId ||
entry.workspaceId !== this.workspace.workspaceId,
)
)
throw new ManagedHookError('managed_hook_scope_conflict');
this.sessionManifests.set(runtimeSessionId, parsed);
}
sealAdmission(): void {
this.admissionSealed = true;
}
@@ -593,7 +635,9 @@ export class ManagedHookRuntime {
return structuredClone(existing.view);
}
const { pin: expected } = control;
const catalog = this.manifest.catalogs.find(
const catalog = (
this.sessionManifests.get(control.sessionKey.sessionId) ?? this.manifest
).catalogs.find(
(entry) =>
entry.tenantId === control.sessionKey.tenantId &&
entry.workspaceId === control.sessionKey.workspaceId &&
@@ -842,6 +886,26 @@ export class ManagedHookRuntime {
];
}
hasUnsettledExtensionOperations(): boolean {
return (
this.pendingStarts > 0 ||
[...this.operations.values()].some(
(entry) =>
entry.view.state !== 'settled' ||
entry.moduleEvaluationPending === true,
)
);
}
hasExtensionHolds(sessionId: string): boolean {
return [...this.operations.values()].some(
(entry) =>
entry.sessionKey.sessionId === sessionId &&
(entry.view.state !== 'settled' ||
entry.moduleEvaluationPending === true),
);
}
hasHolds(runtimeSessionId: string): boolean {
return [...this.operations.values()].some(
(entry) =>
@@ -981,53 +1045,71 @@ export class ManagedHookRuntime {
: undefined,
);
dispatched = true;
const result = await runner.executeHook(
config,
definition.eventName,
{
...control.input,
cwd: directory,
session_id: control.sessionKey.sessionId,
},
{
signal: entry.controller.signal,
...(Array.isArray(
(control.input as unknown as Record<string, unknown>)['messages'],
)
? {
messages: (
control.input as unknown as {
messages: Array<Record<string, unknown>>;
}
).messages,
}
: {}),
...('tool_use_id' in control.input &&
typeof control.input.tool_use_id === 'string'
? { toolUseID: control.input.tool_use_id }
: {}),
},
config.type === HookType.Command
? {
waitForProcessTree: true,
cgroupRoot: process.env['QWEN_MANAGED_HOOK_CGROUP_ROOT'],
environment: {
PATH: process.env['PATH'] ?? '',
HOME: directory,
USERPROFILE: directory,
...(process.env['SystemRoot']
? { SYSTEMROOT: process.env['SystemRoot'] }
const result =
config.type === HookType.Command && this.extensionProcesses
? await executeManagedExtensionCommandHook(
runner,
this.extensionProcesses,
control.sessionKey.sessionId,
control.pin.catalogRevision,
config,
{
...control.input,
cwd: directory,
session_id: control.sessionKey.sessionId,
},
entry.controller.signal,
managedExtensionEnvironment(process.env, directory),
)
: await runner.executeHook(
config,
definition.eventName,
{
...control.input,
cwd: directory,
session_id: control.sessionKey.sessionId,
},
{
signal: entry.controller.signal,
...(Array.isArray(
(control.input as unknown as Record<string, unknown>)[
'messages'
],
)
? {
messages: (
control.input as unknown as {
messages: Array<Record<string, unknown>>;
}
).messages,
}
: {}),
...('tool_use_id' in control.input &&
typeof control.input.tool_use_id === 'string'
? { toolUseID: control.input.tool_use_id }
: {}),
},
}
: config.type === HookType.Http
? {
trackHttpRequest: true,
// Preserve the response as completion proof after user cancel.
httpRequestSignal: this.shutdown.signal,
}
: undefined,
);
config.type === HookType.Command
? {
waitForProcessTree: true,
cgroupRoot: process.env['QWEN_MANAGED_HOOK_CGROUP_ROOT'],
environment: {
PATH: process.env['PATH'] ?? '',
HOME: directory,
USERPROFILE: directory,
...(process.env['SystemRoot']
? { SYSTEMROOT: process.env['SystemRoot'] }
: {}),
},
}
: config.type === HookType.Http
? {
trackHttpRequest: true,
// Preserve the response as completion proof after user cancel.
httpRequestSignal: this.shutdown.signal,
}
: undefined,
);
if (result.error instanceof HookCommandIsolationUnavailableError) {
entry.view = {
operationId: control.operationId,
@@ -1056,7 +1138,12 @@ export class ManagedHookRuntime {
}
const uncertain =
(config.type === HookType.Command &&
result.processTreeDrained !== true) ||
result.processTreeDrained !== true &&
!(
this.extensionProcesses?.mode === 'developer-process-group' &&
result.processReceipt?.scope === 'process_group' &&
result.processReceipt.state === 'stopped'
)) ||
(config.type === HookType.Function && !functionSettled) ||
(config.type === HookType.Http &&
(!result.httpRequestState ||
@@ -212,6 +212,142 @@ function invoke(
}
describe('Managed MCP Runtime', () => {
it('installs package definitions only for the selected Session and drains that Session', async () => {
const instance = runtime([]);
instance.installSessionDefinitions(sessionKey.sessionId, {
version: 1,
servers: [stdioDefinition()],
});
expect(
instance.publishedDefinitions(
sessionKey.tenantId,
sessionKey.workspaceId,
),
).toEqual([]);
const configured = await settled(instance, configure());
expect(configured.state).toBe('settled');
expect(configured.error).toBeUndefined();
expect(instance.hasHolds(runtimeSessionId)).toBe(true);
await instance.drainSession('unrelated-session');
expect(instance.hasHolds(runtimeSessionId)).toBe(true);
expect(() =>
instance.installSessionDefinitions(sessionKey.sessionId, {
version: 1,
servers: [],
}),
).toThrow('busy');
await instance.drainSession(runtimeSessionId);
expect(instance.hasHolds(runtimeSessionId)).toBe(false);
instance.installSessionDefinitions(sessionKey.sessionId, {
version: 1,
servers: [],
});
});
it('preserves independent MCP connection identity across another component change and leaves sibling Sessions untouched', async () => {
const independent = stdioDefinition();
const extension = { ...stdioDefinition(), serverId: 'one:fixture' };
const otherKey = { ...sessionKey, sessionId: 'session-b' };
const otherOwner = 'mcp:session-b';
const instance = new ManagedMcpRuntime(
{ ...sessionKey, workspaceGeneration: '1' },
async () => directory,
{ version: 1, servers: [independent] },
);
runtimes.push(instance);
instance.installSessionDefinitions(sessionKey.sessionId, {
version: 1,
servers: [independent, extension],
});
instance.installSessionDefinitions(otherKey.sessionId, {
version: 1,
servers: [independent],
});
const finish = async (owner: string, control: ManagedMcpControl) => {
let value =
control.kind === 'mcp-invoke' && control.request.kind === 'tool_call'
? await instance.invokeTool(owner, control)
: await instance.control(owner, control);
await vi.waitFor(
async () => {
value = await instance.control(owner, {
kind: 'mcp-status',
sessionKey: control.sessionKey,
operationId: 'lookup',
targetOperationId: control.operationId,
});
expect(value.state).not.toBe('running');
},
{ timeout: 15000 },
);
return value;
};
const original = (await finish(runtimeSessionId, configure())).catalog!;
await finish(runtimeSessionId, {
...configure(),
operationId: 'extension-configuration',
serverId: extension.serverId,
grant: grant('extension-configuration'),
});
const otherControl = {
...configure(),
operationId: 'configuration-2',
configRevision: 2,
sessionKey: otherKey,
grant: { ...grant('configuration-2'), sessionKey: otherKey },
};
const sibling = (await finish(otherOwner, otherControl)).catalog!;
const before = (
await readFile(path.join(directory, 'calls-1'), 'utf8')
).split('initialize').length;
const next = { version: 1 as const, servers: [independent] };
await instance.drainExtensionSession(sessionKey.sessionId, next);
instance.installSessionDefinitions(sessionKey.sessionId, next);
expect(instance.hasHolds(runtimeSessionId)).toBe(true);
expect(instance.hasHolds(otherOwner)).toBe(true);
const invokeA = invoke(original, 'same-a', {
kind: 'tool_call',
name: 'echo',
arguments: { preserved: 'a' },
});
const resultA = await finish(runtimeSessionId, invokeA);
expect(resultA.state).toBe('settled');
expect(resultA.error).toBeUndefined();
const invokeB = invoke(sibling, 'same-b', {
kind: 'tool_call',
name: 'echo',
arguments: { preserved: 'b' },
});
const resultB = await finish(otherOwner, {
...invokeB,
sessionKey: otherKey,
grant: { ...invokeB.grant, sessionKey: otherKey },
});
expect(resultB.state).toBe('settled');
expect(resultB.error).toBeUndefined();
expect(
(await readFile(path.join(directory, 'calls-1'), 'utf8')).split(
'initialize',
).length,
).toBe(before);
expect(
(
await finish(runtimeSessionId, {
kind: 'mcp-discover',
sessionKey,
operationId: 'unchanged-discovery',
serverId: original.serverId,
serverRevision: original.serverRevision,
connectionGeneration: original.connectionGeneration,
grant: grant('configuration-1'),
})
).catalog,
).toMatchObject({
configRevision: original.configRevision,
connectionGeneration: original.connectionGeneration,
});
});
it('isolates pending Session lookups while preserving the Workspace drain total', async () => {
vi.spyOn(Storage, 'getGlobalQwenDir').mockReturnValue(
path.join(directory, 'global'),
@@ -263,6 +399,12 @@ describe('Managed MCP Runtime', () => {
expect(instance.getDrainInspection().pendingStarts).toBe(1);
expect(instance.hasPendingWork(otherRuntime)).toBe(true);
expect(instance.hasPendingWork(runtimeSessionId)).toBe(false);
expect(instance.hasExtensionPendingOperations(sessionKey.sessionId)).toBe(
false,
);
expect(instance.hasExtensionPendingOperations(otherKey.sessionId)).toBe(
true,
);
await expect(
executor.controlFileHistory(sessionKey.sessionId, runtimeSessionId, {
kind: 'raw-file-history',
+230 -34
View File
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { managedExtensionEnvironment } from './managed-extension-environment.js';
import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
@@ -34,6 +35,8 @@ import {
} from '@qwen-code/qwen-code-core/managed-runtime/managed-mcp-protocol.js';
import type { ManagedSessionKey } from '@qwen-code/qwen-code-core/managed-runtime/managed-session-records.js';
import { launchManagedStdio } from './managed-stdio-supervisor.js';
import type { ManagedExtensionProcessOwners } from './managed-extension-process.js';
import { ManagedExtensionStdioTransport } from './managed-extension-stdio.js';
export interface ManagedMcpDefinition {
readonly tenantId: string;
@@ -333,6 +336,7 @@ function parseManifest(value: unknown): ManagedMcpManifest {
export class ManagedMcpRuntime {
private readonly definitions: ManagedMcpManifest;
private readonly sessionDefinitions = new Map<string, ManagedMcpManifest>();
private readonly connections = new Map<string, Connection>();
private readonly operations = new Map<string, Operation>();
private readonly gates = new ManagedOperationGrantGate();
@@ -342,6 +346,7 @@ export class ManagedMcpRuntime {
private admissionSealed = false;
private grantInstalled = false;
private readonly pendingStarts = new Map<string, number>();
private readonly extensionPendingStarts = new Map<string, number>();
constructor(
private readonly workspace: {
@@ -353,10 +358,170 @@ export class ManagedMcpRuntime {
runtimeSessionId: string,
) => Promise<string | undefined>,
manifest: ManagedMcpManifest,
private readonly extensionProcesses?: ManagedExtensionProcessOwners,
) {
this.definitions = parseManifest(manifest);
}
resolvePinnedDefinitions(
pins: ReadonlyArray<{
serverId: string;
serverRevision: number;
definitionDigest: string;
}>,
): readonly ManagedMcpDefinition[] {
const available = [
...this.definitions.servers,
...[...this.sessionDefinitions.values()].flatMap(
(manifest) => manifest.servers,
),
];
return pins.map((pin) => {
const found = available.find(
(entry) =>
entry.serverId === pin.serverId &&
entry.serverRevision === pin.serverRevision &&
entry.definitionDigest === pin.definitionDigest,
);
if (!found)
throw new ManagedMcpError('managed_mcp_definition_unavailable');
return structuredClone(found);
});
}
installSessionDefinitions(
runtimeSessionId: string,
manifest: ManagedMcpManifest,
): void {
const parsed = parseManifest(manifest);
if (
[...this.connections.values()].some(
(entry) =>
entry.sessionKey.sessionId === runtimeSessionId &&
(entry.pending.size > 0 ||
(!entry.closed && !this.matchesDefinition(entry, parsed))),
) ||
this.hasExtensionPendingOperations(runtimeSessionId)
)
throw new ManagedMcpError('managed_mcp_session_busy');
if (
parsed.servers.some(
(entry) =>
entry.tenantId !== this.workspace.tenantId ||
entry.workspaceId !== this.workspace.workspaceId,
)
)
throw new ManagedMcpError('managed_mcp_scope_conflict');
this.sessionDefinitions.set(runtimeSessionId, parsed);
}
hasExtensionPendingOperations(sessionId: string): boolean {
return (
this.extensionPendingStarts.has(sessionId) ||
[...this.operations.values()].some(
(entry) =>
entry.control.sessionKey.sessionId === sessionId &&
entry.view.state !== 'settled',
)
);
}
hasUnsettledExtensionOperations(): boolean {
return (
this.pendingStarts.size > 0 ||
[...this.operations.values()].some(
(entry) => entry.view.state !== 'settled',
)
);
}
matchingSessionDefinition(
sessionId: string,
serverId: string,
digest: string,
): ManagedMcpDefinition | undefined {
return this.sessionDefinitions
.get(sessionId)
?.servers.find(
(entry) =>
entry.serverId === serverId && entry.definitionDigest === digest,
);
}
private matchesDefinition(
connection: Connection,
manifest: ManagedMcpManifest,
): boolean {
return (
!connection.retiring &&
manifest.servers.some(
(entry) =>
entry.serverId === connection.definition.serverId &&
entry.serverRevision === connection.definition.serverRevision &&
entry.definitionDigest === connection.definition.definitionDigest,
)
);
}
retainedExtensionOwnerIds(
sessionId: string,
manifest: ManagedMcpManifest,
): ReadonlySet<string> {
return new Set(
[...this.connections.values()]
.filter(
(entry) =>
entry.sessionKey.sessionId === sessionId &&
!entry.closed &&
this.matchesDefinition(entry, manifest),
)
.flatMap((entry) =>
entry.transport instanceof ManagedExtensionStdioTransport &&
entry.transport.processOwnerId
? [entry.transport.processOwnerId]
: [],
),
);
}
async drainExtensionSession(
sessionId: string,
next?: ManagedMcpManifest,
): Promise<void> {
if (this.hasExtensionPendingOperations(sessionId))
throw new ManagedMcpError('managed_mcp_drain_unknown');
await Promise.all(
[...this.connections.values()]
.filter(
(entry) =>
entry.sessionKey.sessionId === sessionId &&
(!next || !this.matchesDefinition(entry, next)),
)
.map((entry) => this.closeConnection(entry)),
);
if (
[...this.connections.values()].some(
(entry) =>
entry.sessionKey.sessionId === sessionId &&
(!next || !this.matchesDefinition(entry, next)) &&
(!entry.closed || entry.pending.size),
)
)
throw new ManagedMcpError('managed_mcp_drain_unknown');
}
async drainSession(runtimeSessionId: string): Promise<void> {
if (this.hasPendingWork(runtimeSessionId))
throw new ManagedMcpError('managed_mcp_drain_unknown');
await Promise.all(
[...this.connections.values()]
.filter((entry) => entry.runtimeSessionId === runtimeSessionId)
.map((entry) => this.closeConnection(entry)),
);
if (this.hasHolds(runtimeSessionId))
throw new ManagedMcpError('managed_mcp_drain_unknown');
}
sealAdmission(): void {
this.admissionSealed = true;
}
@@ -493,6 +658,11 @@ export class ManagedMcpRuntime {
throw new ManagedMcpError('managed_mcp_grant_invalid');
let directory: string | undefined;
if (control.kind !== 'mcp-release') {
this.extensionPendingStarts.set(
control.sessionKey.sessionId,
(this.extensionPendingStarts.get(control.sessionKey.sessionId) ?? 0) +
1,
);
this.pendingStarts.set(
runtimeSessionId,
(this.pendingStarts.get(runtimeSessionId) ?? 0) + 1,
@@ -500,6 +670,14 @@ export class ManagedMcpRuntime {
try {
directory = await this.sessionDirectory(runtimeSessionId);
} finally {
const scopeRemaining =
this.extensionPendingStarts.get(control.sessionKey.sessionId)! - 1;
if (scopeRemaining)
this.extensionPendingStarts.set(
control.sessionKey.sessionId,
scopeRemaining,
);
else this.extensionPendingStarts.delete(control.sessionKey.sessionId);
const remaining = this.pendingStarts.get(runtimeSessionId)! - 1;
if (remaining) this.pendingStarts.set(runtimeSessionId, remaining);
else this.pendingStarts.delete(runtimeSessionId);
@@ -674,7 +852,10 @@ export class ManagedMcpRuntime {
): Promise<void> {
const control = operation.control;
if (control.kind === 'mcp-configure') {
const definition = this.definitions.servers.find(
const definition = (
this.sessionDefinitions.get(control.sessionKey.sessionId) ??
this.definitions
).servers.find(
(item) =>
item.tenantId === control.sessionKey.tenantId &&
item.workspaceId === control.sessionKey.workspaceId &&
@@ -706,43 +887,56 @@ export class ManagedMcpRuntime {
throw new ManagedMcpError('managed_mcp_connection_quota');
const transport =
definition.transport === 'stdio'
? (() => {
const env = {
...Object.fromEntries(
DEFAULT_INHERITED_ENV_VARS.map((key) => [key, '']),
? this.extensionProcesses
? new ManagedExtensionStdioTransport(this.extensionProcesses, {
sessionId: control.sessionKey.sessionId,
configRevision: control.configRevision,
executable: definition.command!,
args: definition.args ?? [],
env: managedExtensionEnvironment(
process.env,
directory!,
definition.env,
),
PATH: process.env['PATH'] ?? '',
HOME: directory!,
USERPROFILE: directory!,
...(process.env['SystemRoot']
? { SYSTEMROOT: process.env['SystemRoot'] }
: {}),
...definition.env,
};
// Windows keeps the SDK transport: its cross-spawn launch still
// resolves .cmd/.bat command shims, which the native launcher
// does not. Containment on Windows is unchanged.
if (process.platform === 'win32')
cwd: directory!,
})
: (() => {
const env = {
...Object.fromEntries(
DEFAULT_INHERITED_ENV_VARS.map((key) => [key, '']),
),
PATH: process.env['PATH'] ?? '',
HOME: directory!,
USERPROFILE: directory!,
...(process.env['SystemRoot']
? { SYSTEMROOT: process.env['SystemRoot'] }
: {}),
...definition.env,
};
// Windows keeps the SDK transport: its cross-spawn launch still
// resolves .cmd/.bat command shims, which the native launcher
// does not. Containment on Windows is unchanged.
if (process.platform === 'win32')
return new StdioClientTransport({
command: definition.command!,
args: definition.args,
cwd: directory!,
stderr: 'pipe',
env,
});
const launch = launchManagedStdio(
definition.command!,
definition.args ?? [],
env,
);
return new StdioClientTransport({
command: definition.command!,
args: definition.args,
command: launch.executable,
args: launch.args,
cwd: directory!,
stderr: 'pipe',
env,
env: launch.env,
});
const launch = launchManagedStdio(
definition.command!,
definition.args ?? [],
env,
);
return new StdioClientTransport({
command: launch.executable,
args: launch.args,
cwd: directory!,
stderr: 'pipe',
env: launch.env,
});
})()
})()
: definition.transport === 'streamable-http'
? new StreamableHTTPClientTransport(new URL(definition.url!), {
requestInit: { headers: definition.headers, redirect: 'error' },
@@ -807,7 +1001,9 @@ export class ManagedMcpRuntime {
try {
await client.connect(transport, { timeout: TIMEOUT_MS });
const onmessage = transport.onmessage!;
transport.onmessage = (message) => {
transport.onmessage = (
message: import('@modelcontextprotocol/sdk/types.js').JSONRPCMessage,
) => {
if ('method' in message) {
const changed = {
'notifications/tools/list_changed': 'tools',
@@ -363,7 +363,15 @@ export function isOwnedManagedRuntimeRoute(
url: string | undefined,
routes: readonly DeclaredManagedRuntimeRoute[],
): boolean {
return routes.some((route) => method === route.method && url === route.path);
return routes.some((route) => {
if (method !== route.method || !url) return false;
if (url === route.path) return true;
if (!route.path.endsWith('/:digest')) return false;
const prefix = route.path.slice(0, -':digest'.length);
return (
url.startsWith(prefix) && /^[0-9a-f]{64}$/u.test(url.slice(prefix.length))
);
});
}
/** Admits exactly the declared routes, by default those of boot v1. */
@@ -34,6 +34,12 @@ import {
} from './managed-runtime-tool-executor.js';
import { managedRuntimeLedgerFromEnvironment } from './managed-runtime-ledger.js';
import type { ManagedSshRuntime } from './managed-runtime-ssh-worker.js';
import {
MANAGED_EXTENSION_RETIRE_ROUTE,
MANAGED_EXTENSION_RUNTIME_ROUTE,
MANAGED_EXTENSION_UPLOAD_ROUTE,
} from './managed-extension-runtime.js';
import { MANAGED_EXTENSION_COMMAND_ROUTE } from './managed-extension-command-runtime.js';
import { PUBLICATION_INSTALL_ROUTE } from './remote-shell-result-publication.js';
import { WORKSPACE_CAPABILITY_DIGEST } from './managed-workspace-activation.js';
import {
@@ -316,9 +322,18 @@ export async function startManagedRuntimeAttestationWorker(
boot.version === 2
? capturePublisher ||
remotePublishers ||
boot.capabilityDigest === WORKSPACE_CAPABILITY_DIGEST
boot.capabilityDigest === WORKSPACE_CAPABILITY_DIGEST ||
boot.managedExtensions !== undefined
? [
...MANAGED_CONTEXT_WORKER_ROUTES,
...(boot.managedExtensions
? [
MANAGED_EXTENSION_RUNTIME_ROUTE,
MANAGED_EXTENSION_UPLOAD_ROUTE,
MANAGED_EXTENSION_RETIRE_ROUTE,
MANAGED_EXTENSION_COMMAND_ROUTE,
]
: []),
...(csiBoot
? [
MANAGED_CSI_ATTEST_ROUTE,
@@ -330,12 +345,21 @@ export async function startManagedRuntimeAttestationWorker(
...(sshRuntime ? [sshRuntime.route] : []),
...(remotePublishers ? [MANAGED_SHELL_PUBLISHER_ROUTE] : []),
...(!capturePublisher &&
boot.capabilityDigest === WORKSPACE_CAPABILITY_DIGEST
(boot.capabilityDigest === WORKSPACE_CAPABILITY_DIGEST ||
boot.managedExtensions !== undefined)
? [PUBLICATION_INSTALL_ROUTE]
: []),
]
: [
...MANAGED_CONTEXT_WORKER_ROUTES,
...(boot.managedExtensions
? [
MANAGED_EXTENSION_RUNTIME_ROUTE,
MANAGED_EXTENSION_UPLOAD_ROUTE,
MANAGED_EXTENSION_RETIRE_ROUTE,
MANAGED_EXTENSION_COMMAND_ROUTE,
]
: []),
...(csiBoot
? [
MANAGED_CSI_ATTEST_ROUTE,
@@ -59,7 +59,9 @@ export interface SshRuntimeRecord {
}
export interface SshRuntimeCleanProof {
readonly version: 1;
readonly version: 1 | 2;
readonly executionMode?: 'linux-cgroup' | 'developer-process-group';
readonly scope?: 'process_tree' | 'process_group';
readonly resourceId: string;
readonly bindingId: string;
readonly bindingGeneration: number;
@@ -364,7 +366,16 @@ export function sshCleanProof(
retiredAt: string,
): SshRuntimeCleanProof {
return {
version: 1,
version: record.boot.managedExtensions ? 2 : 1,
...(record.boot.managedExtensions
? {
executionMode: record.boot.managedExtensions.mode,
scope:
record.boot.managedExtensions.mode === 'linux-cgroup'
? ('process_tree' as const)
: ('process_group' as const),
}
: {}),
resourceId: record.resourceId,
bindingId: record.bindingId,
bindingGeneration: record.bindingGeneration,
@@ -59,6 +59,7 @@ const POSIX_PS = '/bin/ps';
const WINDOWS_TASKKILL = `${process.env['SystemRoot'] || 'C:\\Windows'}\\System32\\taskkill.exe`;
export interface ManagedRuntimeLedgerWorkerRecord {
readonly groupOwned?: boolean;
readonly pid: number;
/** The worker's own process group; its pid on Windows, which has none. */
readonly pgid: number;
@@ -697,6 +698,33 @@ export class ManagedRuntimeLedger {
return state;
}
async stopWitnessedGroup(
pgid: number,
budgetMs: number,
): Promise<ProcessLiveness> {
const record = this.groups.get(pgid);
if (record === undefined) return 'gone';
const identity = judgeGroupIdentity(
record,
queryTableQuietly(),
Date.now(),
);
if (identity === 'gone' || identity === 'recycled')
return this.waitForGroupExit(pgid, 0);
if (identity !== 'ours') return 'denied';
signalProcessGroup(pgid, 'SIGTERM');
if ((await this.waitForGroupExit(pgid, 1000)) === 'gone') return 'gone';
const remainingIdentity = judgeGroupIdentity(
record,
queryTableQuietly(),
Date.now(),
);
if (remainingIdentity === 'gone' || remainingIdentity === 'recycled')
return this.waitForGroupExit(pgid, 0);
if (remainingIdentity !== 'ours') return 'denied';
return this.stopGroup(pgid, budgetMs);
}
async stopGroup(pgid: number, budgetMs: number): Promise<ProcessLiveness> {
const record = this.groups.get(pgid);
if (record === undefined) return 'gone';
@@ -910,6 +938,13 @@ export async function sweepWorkerLedger(
);
}
const document = parseLedgerDocument(raw);
if (document?.worker.groupOwned === false) {
throw new LedgerSweepUnprovenError(
workFile,
[document.worker.pgid],
'Worker process group belongs to its host; only explicit extension-owner reconciliation may stop its registered groups.',
);
}
if (!document) {
// A ledger nobody can read is an operator's evidence, not a retryable
// stop: a reaper would re-read the same immutable bytes forever and
@@ -4,9 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { ManagedExtensionExecutionUnknownError } from './managed-extension-error.js';
import path from 'node:path';
import type { RetainedFileHistoryStorage } from '@qwen-code/qwen-code-core/services/fileHistoryService.js';
import { promises as fs } from 'node:fs';
import type { ShellRawCaptureSink } from '@qwen-code/qwen-code-core/services/shellExecutionService.js';
import { lstat, readlink, realpath } from 'node:fs/promises';
import { isDeepStrictEqual } from 'node:util';
import {
@@ -209,6 +211,12 @@ export class ManagedToolUnavailableError extends Error {
/** The admitted tools, keyed by name, over one configuration. */
export interface ManagedToolSet {
readonly extensionAssets?: true;
readonly runShell?: (
input: Readonly<Record<string, unknown>>,
signal: AbortSignal,
capture?: ShellRawCaptureSink,
) => Promise<ToolResult>;
readonly retainedFileHistory?: RetainedFileHistoryStorage;
readonly fileHistory?: ManagedRuntimeFileHistory;
/**
@@ -238,6 +246,10 @@ export interface ManagedToolSet {
*/
export type ManagedToolSetResolver = (
reference: ManagedToolReference,
invocation?: {
readonly toolName: string;
readonly input: Readonly<Record<string, unknown>>;
},
) => Promise<ManagedToolSet | undefined>;
const ADMITTED_TOOL_NAMES: ReadonlySet<string> = new Set([
@@ -373,6 +385,18 @@ export class ManagedToolExecutor {
private csiClosing: Promise<void> | undefined;
private retirementId?: string;
private providerWasUsed = false;
private extensionLifecycle?: {
readonly mode: 'linux-cgroup' | 'developer-process-group';
readonly drain: () => Promise<void>;
};
attachExtensionLifecycle(
value: NonNullable<ManagedToolExecutor['extensionLifecycle']>,
): void {
if (this.extensionLifecycle)
throw new Error('Extension lifecycle is already attached.');
this.extensionLifecycle = value;
}
private capturePreparationStarted = false;
private historyControlOutcomeUnknown = false;
private ledgerIntegrityFailed = false;
@@ -1017,7 +1041,10 @@ export class ManagedToolExecutor {
if (existing?.refusal) return existing.refusal;
if (!existing) {
this.assertAdmissionOpen();
const tools = await this.toolsFor(reference);
const tools =
input['managedExtension'] === undefined
? await this.toolsFor(reference)
: await this.toolsFor(reference, { toolName, input });
this.assertLegacySession(reference.sessionId);
if (!this.isAdmissionOpen || !tools || tools.isActive?.() === false)
throw new ManagedToolUnavailableError(
@@ -1079,7 +1106,10 @@ export class ManagedToolExecutor {
return join(existing, reference, toolName, inputJson);
}
this.assertAdmissionOpen();
const tools = await this.toolsFor(reference);
const tools =
input['managedExtension'] === undefined
? await this.toolsFor(reference)
: await this.toolsFor(reference, { toolName, input });
this.assertLegacySession(reference.sessionId);
// A concurrent execute of the same call may have journaled it meanwhile.
const joined = this.entries.get(reference.callId);
@@ -1212,7 +1242,10 @@ export class ManagedToolExecutor {
'Tool v3 capture is not available.',
);
}
const tools = await this.toolsFor(reference);
const tools =
input['managedExtension'] === undefined
? await this.toolsFor(reference)
: await this.toolsFor(reference, { toolName, input });
this.assertLegacySession(reference.sessionId);
const joined = this.entries.get(reference.callId);
if (joined) return this.executeV3(request);
@@ -1227,6 +1260,15 @@ export class ManagedToolExecutor {
return this.executeV3Monitor(request, tools, structuredClone(input));
}
const normalized = structuredClone(input);
delete normalized['managedExtension'];
if (
input['managedExtension'] !== undefined &&
(normalized['is_background'] === true ||
normalized['is_monitor'] === true)
)
throw new ManagedToolConflictError(
'Extension Shell requires a foreground invocation.',
);
// The background and monitor families reject long waits' shell-side
// policies such as the foreground sleep guard — they exist exactly so
// that a wait is durable rather than blocked. Validate with their own
@@ -1909,6 +1951,25 @@ export class ManagedToolExecutor {
}
/** Read-only lookup; never creates or advances an invocation. */
isSessionConfigurationIdle(sessionId: string): boolean {
return (
!this.closing &&
!this.retirementId &&
this.pendingStarts.size === 0 &&
!this.historyControls.has(sessionId) &&
this.hooks?.hasHolds(sessionId) !== true &&
this.mcp?.hasPendingWork(sessionId) !== true &&
this.provider?.hasActiveSession(sessionId) !== true &&
!this.backgroundRegistry.hasHolds(sessionId) &&
!this.monitorRegistry.hasHolds(sessionId) &&
![...this.entries.values()].some(
(entry) =>
entry.reference.sessionId === sessionId &&
!['settled', 'acknowledged'].includes(entry.state),
)
);
}
hasActiveSession(sessionId: string): boolean {
return (
this.hooks?.hasHolds(sessionId) === true ||
@@ -2085,9 +2146,13 @@ export class ManagedToolExecutor {
this.pendingStarts.size > 0 ||
this.historyControls.size > 0 ||
this.providerWasUsed ||
this.mcpCalls.size > 0 ||
this.mcp?.getDrainInspection().hasActivity ||
this.hooks?.getDrainInspection().hasActivity ||
(!this.extensionLifecycle &&
(this.mcpCalls.size > 0 ||
this.mcp?.getDrainInspection().hasActivity ||
this.hooks?.getDrainInspection().hasActivity)) ||
(this.extensionLifecycle &&
(this.mcp?.hasUnsettledExtensionOperations() ||
this.hooks?.hasUnsettledExtensionOperations())) ||
[...this.entries.values()].some((entry) =>
entry.version === 3
? entry.state !== 'settled' ||
@@ -2106,6 +2171,7 @@ export class ManagedToolExecutor {
this.backgroundRegistry.stopAll(5_000),
this.monitorRegistry.stopAll(5_000),
]);
await this.extensionLifecycle?.drain();
const remaining = await ledger.killOutstanding();
if (
ledger.hasIntegrityFailure ||
@@ -2180,6 +2246,7 @@ export class ManagedToolExecutor {
// Runs start only from a fresh journal entry, which still carries its
// input; only an acknowledged entry loses it, and nothing runs that.
const params = structuredClone(entry.input!);
delete params['managedExtension'];
let fileInvocation: AnyToolInvocation | undefined;
if (
directory &&
@@ -2350,6 +2417,12 @@ export class ManagedToolExecutor {
return sessionIdContext.run(sessionId, () => {
const invocation = fileInvocation ?? tool.build(params);
invocationStarted = true;
if (entry.toolName === ShellTool.Name && tools.runShell)
return tools.runShell(
params,
entry.controller.signal,
entry.captureSink,
);
const ledger = this.options.ledger;
const trackGroup = (pid: number) => {
if (!ledger) return;
@@ -2402,9 +2475,11 @@ export class ManagedToolExecutor {
return invocation.execute(entry.controller.signal);
});
};
const registeredHistory = this.fileHistories.get(
entry.reference.sessionId,
);
const registeredHistory =
tools.extensionAssets === true &&
[ReadFileTool.Name, GlobTool.Name].includes(entry.toolName)
? undefined
: this.fileHistories.get(entry.reference.sessionId);
const history = tools.fileHistory ?? registeredHistory;
const mutating = [WriteFileTool.Name, EditTool.Name].includes(
entry.toolName,
@@ -2502,6 +2577,11 @@ export class ManagedToolExecutor {
}
payload = toPayload(result, ManagedToolExecutor.isCancelRequested(entry));
} catch (error) {
if (error instanceof ManagedExtensionExecutionUnknownError) {
entry.state = 'unknown';
entry.lastSequence++;
return;
}
const message = error instanceof Error ? error.message : String(error);
payload = {
executionStatus:
@@ -15,6 +15,10 @@ import type {
ManagedContextBoot,
ManagedContextInstallations,
} from './managed-context-envelope.js';
import {
MANAGED_EXTENSION_LINUX_CAPABILITY_DIGEST,
MANAGED_EXTENSION_DEVELOPER_CAPABILITY_DIGEST,
} from './managed-extension-profile.js';
import { computeManagedContextDigest } from './managed-workspace-binding.js';
import type { ManagedToolExecutor } from './managed-runtime-tool-executor.js';
@@ -86,7 +90,15 @@ export class WorkspaceActivations {
(boot.capabilityDigest === RESIDENT_WORKSPACE_CAPABILITY_DIGEST
? boot.isolationClass !== 'workspace' ||
request['profile'] !== RESIDENT_WORKSPACE_PROFILE
: boot.capabilityDigest !== WORKSPACE_CAPABILITY_DIGEST ||
: ![
WORKSPACE_CAPABILITY_DIGEST,
...(boot.managedExtensions
? [
MANAGED_EXTENSION_LINUX_CAPABILITY_DIGEST,
MANAGED_EXTENSION_DEVELOPER_CAPABILITY_DIGEST,
]
: []),
].includes(boot.capabilityDigest) ||
request['profile'] !== WORKSPACE_EXECUTION_PROFILE) ||
binding === undefined ||
binding.contextConfigRef !== WORKSPACE_CONTEXT_CONFIG_REF ||
@@ -221,8 +221,9 @@ const allowedProcessEnvAccesses = normalizeAllowances([
'packages/cli/src/serve/managed-context-worker.ts',
{
reason:
'Managed Runtime startup selects its deployment-owned MCP and Hook manifests and the delegated cgroup root for process isolation from the process environment; definitions are then scoped by tenant and workspace.',
'Managed Runtime startup selects its deployment-owned MCP and Hook manifests and the delegated cgroup root for process isolation from the process environment; definitions are then scoped by tenant and workspace. Extension preflight and Shell children copy only the target Runtime environment allowlist through managedExtensionEnvironment, with settings from the selected extension.',
accesses: {
whole: 2,
'key:QWEN_MANAGED_HOOK_CGROUP_ROOT': 1,
'key:QWEN_MANAGED_HOOK_CONFIG': 1,
'key:QWEN_MANAGED_MCP_CONFIG': 1,
@@ -257,8 +258,9 @@ const allowedProcessEnvAccesses = normalizeAllowances([
'packages/cli/src/serve/managed-hook-runtime.ts',
{
reason:
'Hook commands use the Runtime host PATH and Windows SystemRoot for executable lookup and OS startup, and the deployment-owned cgroup root for process-tree isolation; HOME and USERPROFILE come from the verified Session directory.',
'Hook commands use the Runtime host PATH and Windows SystemRoot for executable lookup and OS startup, and the deployment-owned cgroup root for process-tree isolation; HOME and USERPROFILE come from the verified Session directory. Extension Hooks pass the target Runtime environment through managedExtensionEnvironment to copy only its allowlist.',
accesses: {
whole: 1,
'key:PATH': 1,
'key:QWEN_MANAGED_HOOK_CGROUP_ROOT': 1,
'key:SystemRoot': 2,
@@ -289,8 +291,8 @@ const allowedProcessEnvAccesses = normalizeAllowances([
'packages/cli/src/serve/managed-mcp-runtime.ts',
{
reason:
'MCP stdio children use the Runtime host PATH and Windows SystemRoot for executable lookup and OS startup; their remaining environment comes from the verified workspace directory and deployment-owned definition.',
accesses: { 'key:PATH': 1, 'key:SystemRoot': 2 },
'MCP stdio children use the Runtime host PATH and Windows SystemRoot for executable lookup and OS startup; their remaining environment comes from the verified workspace directory and deployment-owned definition. Extension MCP children pass the target Runtime environment through managedExtensionEnvironment to copy only its allowlist and the selected extension settings.',
accesses: { 'key:PATH': 1, 'key:SystemRoot': 2, whole: 1 },
},
],
[
@@ -0,0 +1,123 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import express from 'express';
import { promises as fs } from 'node:fs';
import type { Server } from 'node:http';
import path from 'node:path';
import os from 'node:os';
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import { registerManagedPackagePreparation } from './managed-package-preparation.js';
import type { ExtensionsController } from './workspace-extensions-controller.js';
const prepare = vi.hoisted(() => vi.fn());
vi.mock('@qwen-code/qwen-code-core/extension/managed-package.js', () => ({
prepareManagedExtensionPackage: prepare,
ManagedPackageCompatibilityError: class extends Error {},
}));
let server: Server;
let base: string;
let selected: ExtensionsController;
let originalSource: string;
const directories: string[] = [];
const owner = () =>
({
validateExtensionMutationClient: () => true,
}) as unknown as ExtensionsController;
beforeEach(async () => {
selected = owner();
originalSource = await fs.mkdtemp(
path.join(os.tmpdir(), 'managed-prepare-test-'),
);
directories.push(originalSource);
await fs.writeFile(
path.join(originalSource, 'qwen-extension.json'),
'{"name":"fixture","version":"1"}',
);
prepare.mockReset().mockImplementation(async (_source, options) => {
directories.push(options.destination);
await fs.writeFile(
path.join(options.destination, 'package.tar.gz'),
'archive',
);
return {
packageDigest: 'a'.repeat(64),
manifest: {},
sourceRevision: 'original',
};
});
const app = express();
app.use(express.json());
registerManagedPackagePreparation(
app,
'/workspace/extensions',
() => selected,
() => (_req, _res, next) => next(),
);
server = app.listen(0, '127.0.0.1');
await new Promise<void>((resolve) => server.once('listening', resolve));
const address = server.address();
if (!address || typeof address === 'string')
throw new Error('Missing test port');
base = `http://127.0.0.1:${address.port}`;
});
afterEach(async () => {
server.closeAllConnections();
await new Promise<void>((resolve) => server.close(() => resolve()));
await Promise.all(
directories
.splice(0)
.map((directory) => fs.rm(directory, { recursive: true, force: true })),
);
});
const publish = () =>
fetch(`${base}/workspace/extensions/managed-packages/prepare`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
source: originalSource,
consent: true,
target: { platform: 'linux', arch: 'x64' },
}),
});
it('streams the prepared archive only to its original Workspace controller', async () => {
const original = selected;
const response = await publish();
expect(response.status).toBe(201);
const result = (await response.json()) as { archiveUrl: string };
expect(await (await fetch(base + result.archiveUrl)).text()).toBe('archive');
selected = owner();
expect((await fetch(base + result.archiveUrl)).status).toBe(404);
selected = original;
expect((await fetch(base + result.archiveUrl)).status).toBe(200);
});
it('discards a preparation whose Workspace controller changed before completion', async () => {
prepare.mockImplementationOnce(async (_source, options) => {
directories.push(options.destination);
selected = owner();
return {
packageDigest: 'a'.repeat(64),
manifest: {},
sourceRevision: 'original',
};
});
const response = await publish();
expect(response.status).toBe(409);
expect(await response.json()).toEqual({
code: 'managed_package_target_changed',
});
await vi.waitFor(async () => {
await expect(fs.stat(directories.at(-1)!)).rejects.toMatchObject({
code: 'ENOENT',
});
});
});
@@ -0,0 +1,176 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import * as fs from 'node:fs/promises';
import { createReadStream } from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { randomUUID } from 'node:crypto';
import type { Application, Request, Response, RequestHandler } from 'express';
import {
prepareManagedExtensionPackage,
ManagedPackageCompatibilityError,
} from '@qwen-code/qwen-code-core/extension/managed-package.js';
import { parseInstallSource } from '@qwen-code/qwen-code-core/extension/marketplace.js';
import type { ExtensionsController } from './workspace-extensions-controller.js';
export function registerManagedPackagePreparation(
app: Application,
base: string,
resolve: (
req: Request,
res: Response,
trust: boolean,
) => ExtensionsController | null,
mutate: (options: { strict: boolean }) => RequestHandler,
): void {
let preparing = 0;
const prepared = new Map<
string,
{ directory: string; expiresAt: number; owner: ExtensionsController }
>();
const sweep = setInterval(() => {
for (const [id, entry] of prepared) {
if (entry.expiresAt <= Date.now()) {
prepared.delete(id);
void fs.rm(entry.directory, { recursive: true, force: true });
}
}
}, 60_000);
sweep.unref();
app.post(
`${base}/managed-packages/prepare`,
mutate({ strict: true }),
async (req, res) => {
const controller = resolve(req, res, true);
if (
!controller ||
!controller.validateExtensionMutationClient(req, res, {
requireClientId: false,
})
)
return;
const body: unknown = req.body;
if (!body || typeof body !== 'object' || Array.isArray(body))
return res.status(400).json({ code: 'invalid_managed_package_source' });
const input = body as Record<string, unknown>;
const target = input['target'] as
| { platform?: string; arch?: string }
| undefined;
if (
typeof input['source'] !== 'string' ||
input['source'].length > 4096 ||
input['consent'] !== true ||
!target ||
!['linux', 'darwin'].includes(target.platform ?? '') ||
!['arm64', 'x64', 'any'].includes(target.arch ?? '') ||
(input['ref'] !== undefined &&
(typeof input['ref'] !== 'string' ||
input['ref'].startsWith('-') ||
input['ref'].length > 256))
) {
return res.status(400).json({ code: 'invalid_managed_package_source' });
}
if (prepared.size + preparing >= 16)
return res
.status(429)
.json({ code: 'managed_package_preparation_capacity' });
const abort = new AbortController();
const timer = setTimeout(() => abort.abort(), 10 * 60_000);
timer.unref();
res.once('close', () => {
if (!res.writableEnded) abort.abort();
});
let directory: string | undefined;
preparing++;
try {
const source = await parseInstallSource(input['source']);
if (
(source.type === 'local' && !path.isAbsolute(source.source)) ||
source.pluginName ||
source.marketplaceConfig
) {
return res
.status(400)
.json({ code: 'managed_package_original_source_required' });
}
source.ref = input['ref'] as string | undefined;
directory = await fs.mkdtemp(
path.join(os.tmpdir(), 'qwen-managed-publish-'),
);
const result = await prepareManagedExtensionPackage(source, {
target: target as {
platform: 'linux' | 'darwin';
arch: 'arm64' | 'x64' | 'any';
},
destination: directory,
signal: abort.signal,
});
if (resolve(req, res, true) !== controller) {
if (!res.headersSent)
res.status(409).json({ code: 'managed_package_target_changed' });
return;
}
const id = randomUUID();
prepared.set(id, {
directory,
expiresAt: Date.now() + 15 * 60_000,
owner: controller,
});
directory = undefined;
return res.status(201).json({
packageDigest: result.packageDigest,
manifest: result.manifest,
sourceRevision: result.sourceRevision,
archiveUrl: `${req.originalUrl.split('?')[0].replace(/\/managed-packages\/prepare$/, '')}/managed-packages/${id}/archive`,
});
} catch (error) {
if (!res.headersSent)
res.status(400).json({
code: 'managed_package_preparation_failed',
error:
error instanceof ManagedPackageCompatibilityError
? error.message
: 'The original package could not be prepared for the selected target.',
});
} finally {
clearTimeout(timer);
preparing--;
if (directory) await fs.rm(directory, { recursive: true, force: true });
}
return;
},
);
app.get(
`${base}/managed-packages/:preparationId/archive`,
async (req, res) => {
const owner = resolve(req, res, true);
if (!owner) return;
const entry = prepared.get(String(req.params['preparationId']));
if (!entry || entry.expiresAt <= Date.now() || entry.owner !== owner)
return res
.status(404)
.json({ code: 'managed_package_preparation_not_found' });
const archive = path.join(entry.directory, 'package.tar.gz');
const stat = await fs.stat(archive).catch(() => undefined);
if (!stat)
return res
.status(404)
.json({ code: 'managed_package_preparation_not_found' });
res.setHeader('Cache-Control', 'no-store');
res
.type('application/octet-stream')
.setHeader('Content-Length', stat.size);
const stream = createReadStream(archive, { flags: 'r' });
stream.on('error', () => res.destroy());
res.once('close', () => stream.destroy());
stream.pipe(res);
return;
},
);
}
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { registerManagedPackagePreparation } from './managed-package-preparation.js';
import * as crypto from 'node:crypto';
import * as fs from 'node:fs/promises';
import * as os from 'node:os';
@@ -861,6 +862,7 @@ export function registerWorkspaceExtensionRoutes(
}
const registerFor = (base: string, resolve: ResolveController): void => {
registerManagedPackagePreparation(app, base, resolve, mutate);
// GET {base} — read-only installed extension status.
app.get(base, async (req, res) => {
const assertGenerationOpen = deps.captureGenerationAssertion?.();
@@ -4,70 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { z } from 'zod';
import {
parse as parseYaml,
normalizeContent,
} from '@qwen-code/qwen-code-core';
/**
* Defines the Zod schema for a Markdown command definition file.
* The frontmatter contains optional metadata, and the body is the prompt.
*/
export const MarkdownCommandDefSchema = z.object({
frontmatter: z
.object({
description: z.string().optional(),
'argument-hint': z.string().optional(),
when_to_use: z.string().optional(),
'disable-model-invocation': z.boolean().optional(),
})
.passthrough()
.optional(),
prompt: z.string({
required_error: 'The prompt content is required.',
invalid_type_error: 'The prompt content must be a string.',
}),
});
export type MarkdownCommandDef = z.infer<typeof MarkdownCommandDefSchema>;
/**
* Parses a Markdown command file with optional YAML frontmatter.
* @param content The file content
* @returns Parsed command definition with frontmatter and prompt
*/
export function parseMarkdownCommand(content: string): MarkdownCommandDef {
const normalizedContent = normalizeContent(content);
// Match YAML frontmatter pattern: ---\n...\n---\n
// Allow empty frontmatter: ---\n---\n
const frontmatterRegex = /^---\n(?:([\s\S]*?)\n)?---(?:\n|$)([\s\S]*)$/;
const match = normalizedContent.match(frontmatterRegex);
if (!match) {
// No frontmatter, entire content is the prompt
return {
prompt: normalizedContent.trim(),
};
}
const [, frontmatterYaml = '', body] = match;
// Parse YAML frontmatter if not empty
let frontmatter: Record<string, unknown> | undefined;
if (frontmatterYaml.trim()) {
try {
frontmatter = parseYaml(frontmatterYaml) as Record<string, unknown>;
} catch (error) {
throw new Error(
`Failed to parse YAML frontmatter: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
return {
frontmatter,
prompt: body.trim(),
};
}
export {
MarkdownCommandDefSchema,
parseMarkdownCommand,
type MarkdownCommandDef,
} from '@qwen-code/qwen-code-core/utils/markdown-command.js';
+29 -8
View File
@@ -844,6 +844,8 @@ export async function downloadFromGitHubRelease(
installMetadata: ExtensionInstallMetadata,
destination: string,
signal?: AbortSignal,
target?: { platform: string; arch: string },
archiveSafety: TarArchiveSafetyOptions = {},
): Promise<GitHubDownloadResult> {
const { source, ref } = installMetadata;
const { owner, repo } = parseGitHubRepoForReleases(source);
@@ -859,7 +861,7 @@ export async function downloadFromGitHubRelease(
throw new Error(`No release data found for ${owner}/${repo} at tag ${ref}`);
}
const asset = findReleaseAsset(releaseData.assets);
const asset = findReleaseAsset(releaseData.assets, target);
let archiveUrl: string | undefined;
let isTar = false;
let isZip = false;
@@ -907,7 +909,12 @@ export async function downloadFromGitHubRelease(
}
signal?.throwIfAborted();
await extractArchiveFile(downloadedAssetPath, destination, signal);
await extractArchiveFile(
downloadedAssetPath,
destination,
signal,
archiveSafety,
);
signal?.throwIfAborted();
await fs.promises.unlink(downloadedAssetPath);
@@ -921,6 +928,7 @@ export async function downloadFromArchiveUrl(
installMetadata: ExtensionInstallMetadata,
destination: string,
signal?: AbortSignal,
archiveSafety: TarArchiveSafetyOptions = {},
): Promise<void> {
const archiveExtension = getSupportedArchiveExtension(installMetadata.source);
if (!archiveExtension) {
@@ -953,7 +961,12 @@ export async function downloadFromArchiveUrl(
}
signal?.throwIfAborted();
await extractArchiveFile(downloadedAssetPath, destination, signal);
await extractArchiveFile(
downloadedAssetPath,
destination,
signal,
archiveSafety,
);
signal?.throwIfAborted();
await fs.promises.unlink(downloadedAssetPath);
}
@@ -995,9 +1008,12 @@ export async function extractArchiveFile(
assertExtractedArchiveContainsExtensionSource(destination);
}
export function findReleaseAsset(assets: Asset[]): Asset | undefined {
const platform = os.platform();
const arch = os.arch();
export function findReleaseAsset(
assets: Asset[],
target?: { platform: string; arch: string },
): Asset | undefined {
const platform = target?.platform ?? os.platform();
const arch = target?.arch ?? os.arch();
const platformArchPrefix = `${platform}.${arch}.`;
const platformPrefix = `${platform}.`;
@@ -1011,8 +1027,13 @@ export function findReleaseAsset(assets: Asset[]): Asset | undefined {
}
// Check for platform specific asset
const platformAsset = assets.find((asset) =>
asset.name.toLowerCase().startsWith(platformPrefix),
const platformAsset = assets.find(
(asset) =>
asset.name.toLowerCase().startsWith(platformPrefix) &&
(!target ||
!/^(?:x64|arm64|arm|ia32|ppc64|s390x|riscv64)\./.test(
asset.name.toLowerCase().slice(platformPrefix.length),
)),
);
if (platformAsset) {
return platformAsset;
@@ -0,0 +1,260 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as os from 'node:os';
import * as tar from 'tar';
import {
compileManagedExtensionPackage,
prepareManagedExtensionPackage,
managedPackageRelativePath,
} from './managed-package.js';
import { findReleaseAsset } from './github.js';
const target = { platform: 'linux', arch: 'x64' } as const;
let temporary: string;
let root: string;
async function file(name: string, content: string): Promise<void> {
const destination = path.join(root, name);
await fs.mkdir(path.dirname(destination), { recursive: true });
await fs.writeFile(destination, content);
}
beforeEach(async () => {
temporary = await fs.mkdtemp(path.join(os.tmpdir(), 'managed-package-test-'));
root = path.join(temporary, 'source');
await fs.mkdir(root);
await file(
'qwen-extension.json',
JSON.stringify({ name: 'fixture', version: '1.0.0' }),
);
});
afterEach(async () => {
await fs.rm(temporary, { recursive: true, force: true });
});
describe('Managed raw package compiler', () => {
it('preserves an architecture-independent publication when compiled on either target architecture', async () => {
const prepared = await prepareManagedExtensionPackage(
{ type: 'local', source: root },
{
target: { platform: 'linux', arch: 'any' },
destination: path.join(temporary, 'prepared'),
},
);
const extracted = path.join(temporary, 'target');
await fs.mkdir(extracted);
await tar.x({ file: prepared.archivePath, cwd: extracted });
for (const arch of ['x64', 'arm64'] as const) {
const compiled = await compileManagedExtensionPackage(extracted, {
target: { platform: 'linux', arch },
resolveRecipes: false,
});
expect(compiled.manifest).toEqual(prepared.manifest);
}
await expect(
compileManagedExtensionPackage(extracted, {
target: { platform: 'darwin', arch: 'arm64' },
}),
).rejects.toThrow('package target does not match this Runtime');
});
it('compiles all seven categories using only target roots and supplied settings', async () => {
await file(
'qwen-extension.json',
JSON.stringify({
name: 'fixture',
version: '1.0.0',
mcpServers: {
echo: {
command: 'node',
args: ['${extensionPath}/server.js'],
env: { TOKEN: '${TOKEN}' },
},
},
hooks: {
PreToolUse: [
{
hooks: [
{
type: 'command',
command: 'node ${CLAUDE_PLUGIN_ROOT}/hook.js',
},
],
},
],
},
settings: [{ name: 'Token', envVar: 'TOKEN', sensitive: true }],
}),
);
await file(
'skills/inspect/SKILL.md',
'---\nname: inspect\ndescription: inspect assets\ndisable-model-invocation: true\n---\nRead assets/data.txt.',
);
await file('skills/inspect/assets/data.txt', 'durable asset');
await file(
'commands/review.md',
'---\ndescription: Review\nargument-hint: <file>\n---\nReview @{README.md} and !{git status}.',
);
await file(
'agents/reviewer.md',
'---\nname: reviewer\ndescription: Reviews changes\ntools: Read, Glob\nmodel: inherit\n---\nReview precisely.',
);
await file('QWEN.md', 'Use the fixed context.');
const before = { cwd: process.cwd(), token: process.env['TOKEN'] };
const compiled = await compileManagedExtensionPackage(root, {
target,
workspaceDirectory: '/target/workspace',
environment: { TOKEN: 'explicit-value' },
});
expect(compiled.manifest.components).toEqual([
'skills',
'commands',
'agents',
'context',
'mcp',
'hooks',
'settings',
]);
expect(compiled.contributions.skills[0]).toMatchObject({
name: 'fixture:inspect',
assetDirectory: 'skills/inspect',
model_invocable: false,
user_invocable: true,
});
expect(compiled.contributions.commands[0].prompt).toContain(
'!{git status}',
);
expect(compiled.contributions.mcpServers).toMatchObject({
echo: {
args: [path.join(root, 'server.js')],
env: { TOKEN: 'explicit-value' },
},
});
expect({ cwd: process.cwd(), token: process.env['TOKEN'] }).toEqual(before);
});
it.each([
'allowed-tools: Read',
'hooks: {PreToolUse: []}',
'model: large',
'paths: [src/**]',
])(
'rejects dynamic Skill declarations before static conversion: %s',
async (declaration) => {
await file(
'skills/unsafe/SKILL.md',
`---\nname: unsafe\ndescription: fixture\n${declaration}\n---\nBody`,
);
await expect(
compileManagedExtensionPackage(root, { target }),
).rejects.toThrow('unsupported');
},
);
it('checks unsupported fields in source format before a converter can discard them', async () => {
await fs.rm(path.join(root, 'qwen-extension.json'));
await file(
'gemini-extension.json',
JSON.stringify({ name: 'source', version: '1', lspServers: { x: {} } }),
);
await expect(
compileManagedExtensionPackage(root, { target }),
).rejects.toThrow('lspServers');
});
it('never silently reads a publisher environment variable', async () => {
await file(
'qwen-extension.json',
JSON.stringify({
name: 'fixture',
mcpServers: { x: { command: '${HOME}/bin/server' } },
}),
);
await expect(
compileManagedExtensionPackage(root, { target }),
).rejects.toThrow('required');
const raw = await compileManagedExtensionPackage(root, {
target,
resolveRecipes: false,
});
expect(raw.contributions.mcpServers).toMatchObject({
x: { command: '${HOME}/bin/server' },
});
});
it('prepares raw recipes, strips private sidecars and preserves assets and executable modes', async () => {
await file(
'qwen-extension.json',
JSON.stringify({
name: 'fixture',
mcpServers: {
x: { command: 'node', args: ['${extensionPath}/run.js'] },
},
}),
);
await file('run.js', 'console.log("ok")');
await fs.chmod(path.join(root, 'run.js'), 0o755);
await file('.env', 'DO_NOT_DISTRIBUTE=value');
await file('.qwen-extension-settings.json', 'DO_NOT_DISTRIBUTE');
const result = await prepareManagedExtensionPackage(
{ source: root, type: 'local' },
{ target, destination: path.join(temporary, 'output') },
);
const restored = path.join(temporary, 'restored');
await fs.mkdir(restored);
await tar.x({ file: result.archivePath, cwd: restored });
expect(
await fs.readFile(path.join(restored, 'qwen-extension.json'), 'utf8'),
).toContain('${extensionPath}');
expect(await fs.readdir(restored)).not.toContain('.env');
expect(await fs.readdir(restored)).not.toContain(
'.qwen-extension-settings.json',
);
expect((await fs.stat(path.join(restored, 'run.js'))).mode & 0o111).toBe(
0o111,
);
expect(result.packageDigest).toMatch(/^[a-f0-9]{64}$/);
});
it('refuses an installed copy without a separately supplied original source', async () => {
await file('.qwen-extension-install.json', '{}');
await expect(
prepareManagedExtensionPackage(
{ source: root, type: 'local' },
{ target, destination: path.join(temporary, 'output') },
),
).rejects.toThrow('original source');
});
it('rejects symbolic links and package root escapes', async () => {
await fs.symlink('/etc/passwd', path.join(root, 'outside'));
await expect(
compileManagedExtensionPackage(root, { target }),
).rejects.toThrow('unsupported resource');
for (const unsafe of ['../x', '/x', 'C:/x', 'a/../x', 'a\\x', 'a//x'])
expect(() => managedPackageRelativePath(unsafe)).toThrow();
});
it('chooses the target release asset when publication happens on another OS', () => {
const asset = (name: string) => ({
name,
browser_download_url: `https://example.test/${name}`,
});
const assets = [
asset('darwin.arm64.package.tar.gz'),
asset('linux.x64.package.tar.gz'),
];
expect(findReleaseAsset(assets, target)?.name).toBe(
'linux.x64.package.tar.gz',
);
expect(
findReleaseAsset(assets, { platform: 'linux', arch: 'arm64' }),
).toBeUndefined();
});
});
@@ -0,0 +1,792 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { createHash } from 'node:crypto';
import * as fs from 'node:fs/promises';
import { createReadStream } from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { parse as parseToml } from '@iarna/toml';
import * as tar from 'tar';
import {
parseMarkdownCommand,
MarkdownCommandDefSchema,
} from '../utils/markdown-command.js';
import { parseSkillContent } from '../skills/skill-load.js';
import { qualifySkillName } from '../skills/types.js';
import { resolveBuiltinToolName } from '../tools/tool-names.js';
import {
convertClaudeToQwenConfig,
type ClaudePluginConfig,
} from './claude-converter.js';
import { convertGeminiToQwenConfig } from './gemini-converter.js';
import {
cloneFromGit,
downloadFromArchiveUrl,
downloadFromGitHubRelease,
extractArchiveFile,
} from './github.js';
import { downloadFromNpmRegistry } from './npm.js';
import {
MAX_ARCHIVE_ENTRIES,
MAX_ARCHIVE_EXPANDED_BYTES,
} from './archive-safety.js';
import type { ExtensionInstallMetadata } from '../config/config.js';
export const MANAGED_PACKAGE_COMPILER_VERSION = 1;
export const MANAGED_PACKAGE_MAX_ARCHIVE_BYTES = 100 * 1024 * 1024;
export type ManagedPackageComponent =
| 'skills'
| 'commands'
| 'agents'
| 'context'
| 'mcp'
| 'hooks'
| 'settings';
export interface ManagedPackageTarget {
platform: 'linux' | 'darwin';
arch: 'arm64' | 'x64' | 'any';
}
export interface ManagedPackageManifest {
schemaVersion: 1;
compilerVersion: 1;
name: string;
version: string;
target: ManagedPackageTarget;
components: ManagedPackageComponent[];
sourceType?: string;
sourceRevision?: string;
}
export interface ManagedPackageSkill {
name: string;
description: string;
body: string;
assetDirectory: string;
model_invocable: boolean;
user_invocable: boolean;
}
export interface ManagedPackageCommand {
name: string;
description: string;
prompt: string;
argumentHint?: string;
modelInvocable: boolean;
}
export interface ManagedPackageAgent {
name: string;
description: string;
systemPrompt: string;
tools?: string[];
disallowedTools?: string[];
model?: string;
}
export interface ManagedPackageContributions {
skills: ManagedPackageSkill[];
commands: ManagedPackageCommand[];
agents: ManagedPackageAgent[];
context: Array<{ path: string; body: string }>;
mcpServers: Record<string, unknown>;
hooks: Record<string, unknown>;
settings: Array<{ name: string; envVar: string; sensitive?: boolean }>;
}
export interface CompiledManagedPackage {
manifest: ManagedPackageManifest;
contributions: ManagedPackageContributions;
}
export class ManagedPackageCompatibilityError extends Error {
constructor(readonly reasons: readonly string[]) {
super(`Managed extension package is incompatible: ${reasons.join(', ')}`);
this.name = 'ManagedPackageCompatibilityError';
}
}
function object(value: unknown, field: string): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value))
throw new ManagedPackageCompatibilityError([`${field}: invalid object`]);
return value as Record<string, unknown>;
}
function text(value: unknown, field: string, maxBytes = 512 * 1024): string {
if (
typeof value !== 'string' ||
!value.trim() ||
Buffer.byteLength(value) > maxBytes
)
throw new ManagedPackageCompatibilityError([`${field}: invalid text`]);
return value;
}
export function managedPackageRelativePath(value: string): string {
const normalized = value.replace(/^\.\//, '');
if (
!normalized ||
normalized.includes('\\') ||
normalized.includes(':') ||
normalized.includes('\0') ||
normalized.startsWith('/') ||
normalized.split('/').some((part) => !part || part === '.' || part === '..')
)
throw new ManagedPackageCompatibilityError([
'package resource path escapes its root',
]);
return normalized;
}
function frontmatter(content: string): {
metadata: Record<string, unknown>;
body: string;
} {
const parsed = parseMarkdownCommand(content);
return { metadata: parsed.frontmatter ?? {}, body: parsed.prompt };
}
function hasValue(value: unknown): boolean {
return (
value !== undefined &&
value !== null &&
value !== false &&
value !== '' &&
(!Array.isArray(value) || value.length > 0) &&
(typeof value !== 'object' ||
Array.isArray(value) ||
Object.keys(value).length > 0)
);
}
function rejectFields(
value: Record<string, unknown>,
fields: readonly string[],
source: string,
): void {
const reasons = fields
.filter((key) => hasValue(value[key]))
.map((key) => `${source}.${key}: unsupported`);
if (reasons.length) throw new ManagedPackageCompatibilityError(reasons);
}
const PRIVATE_FILES = new Set([
'.git',
'.env',
'.qwen-extension-settings.json',
'.qwen-extension-git-credentials.json',
'.qwen-extension-install.json',
]);
async function filesUnder(
root: string,
excludePrivate = false,
): Promise<string[]> {
const files: string[] = [];
let total = 0;
const visit = async (relative: string): Promise<void> => {
const entries = await fs.readdir(path.join(root, relative), {
withFileTypes: true,
});
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
if (
excludePrivate &&
(PRIVATE_FILES.has(entry.name) || entry.name.startsWith('.env.'))
)
continue;
const name = relative ? `${relative}/${entry.name}` : entry.name;
managedPackageRelativePath(name);
if (entry.isSymbolicLink() || (!entry.isDirectory() && !entry.isFile()))
throw new ManagedPackageCompatibilityError([
`${name}: unsupported resource type`,
]);
if (entry.isDirectory()) await visit(name);
else {
total += (await fs.lstat(path.join(root, name))).size;
files.push(name);
if (
files.length > MAX_ARCHIVE_ENTRIES ||
total > MAX_ARCHIVE_EXPANDED_BYTES
)
throw new ManagedPackageCompatibilityError([
'package resource limit exceeded',
]);
}
}
};
await visit('');
return files;
}
async function readText(root: string, relative: string): Promise<string> {
const file = managedPackageRelativePath(relative);
const info = await fs.lstat(path.join(root, file));
if (!info.isFile() || info.isSymbolicLink() || info.size > 1024 * 1024)
throw new ManagedPackageCompatibilityError([
`${file}: invalid text resource`,
]);
return new TextDecoder('utf-8', { fatal: true }).decode(
await fs.readFile(path.join(root, file)),
);
}
async function readObject(
root: string,
relative: string,
): Promise<Record<string, unknown>> {
return object(JSON.parse(await readText(root, relative)), relative);
}
async function rawConfig(
root: string,
files: readonly string[],
): Promise<Record<string, unknown>> {
if (files.includes('qwen-extension.json'))
return readObject(root, 'qwen-extension.json');
if (files.includes('gemini-extension.json')) {
const raw = await readObject(root, 'gemini-extension.json');
rejectFields(
raw,
['lspServers', 'channels', 'workflows', 'executor', 'outputStyles'],
'gemini-extension.json',
);
return { ...raw, ...convertGeminiToQwenConfig(root) };
}
const pluginFile = files.includes('.claude-plugin/plugin.json')
? '.claude-plugin/plugin.json'
: files.includes('.qoder-plugin/plugin.json')
? '.qoder-plugin/plugin.json'
: undefined;
if (pluginFile) {
const raw = await readObject(root, pluginFile);
rejectFields(
raw,
['lspServers', 'channels', 'workflows', 'outputStyles', 'executor'],
pluginFile,
);
const config: Record<string, unknown> = {
...convertClaudeToQwenConfig(raw as unknown as ClaudePluginConfig),
name: raw['name'],
version: raw['version'] ?? '1.0.0',
};
for (const key of [
'commands',
'skills',
'agents',
'contextFileName',
'settings',
'mcpServers',
'hooks',
])
if (raw[key] !== undefined) config[key] = raw[key];
return config;
}
if (files.includes('plugin.json')) {
const raw = await readObject(root, 'plugin.json');
if (
raw['$schema'] !==
'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json'
)
throw new ManagedPackageCompatibilityError([
'plugin.json: unsupported schema',
]);
rejectFields(
raw,
['lspServers', 'channels', 'workflows', 'executor', 'outputStyles'],
'plugin.json',
);
return { ...raw, name: raw['name'], version: raw['version'] ?? '1.0.0' };
}
throw new ManagedPackageCompatibilityError(['extension manifest missing']);
}
function resourceRoots(
config: Record<string, unknown>,
key: string,
fallback: string,
): string[] {
const value = config[key] ?? fallback;
const roots = typeof value === 'string' ? [value] : value;
if (!Array.isArray(roots) || roots.some((item) => typeof item !== 'string'))
throw new ManagedPackageCompatibilityError([
`${key}: invalid resource roots`,
]);
return (roots as string[]).map(managedPackageRelativePath);
}
function selectedFiles(
files: readonly string[],
roots: readonly string[],
): string[] {
return files.filter((file) =>
roots.some((root) => file === root || file.startsWith(`${root}/`)),
);
}
function strings(value: unknown, field: string): string[] | undefined {
if (value === undefined) return undefined;
const entries =
typeof value === 'string'
? value
.split(',')
.map((item) => item.trim())
.filter(Boolean)
: value;
if (
!Array.isArray(entries) ||
entries.some((entry) => typeof entry !== 'string' || !entry)
)
throw new ManagedPackageCompatibilityError([`${field}: invalid list`]);
return (entries as string[]).map(
(entry) => resolveBuiltinToolName(entry) ?? entry,
);
}
export async function compileManagedExtensionPackage(
root: string,
options: {
target: ManagedPackageTarget;
workspaceDirectory?: string;
environment?: Readonly<Record<string, string>>;
resolveRecipes?: boolean;
},
): Promise<CompiledManagedPackage> {
const files = await filesUnder(root);
if (
files.some((file) =>
file
.split('/')
.some((part) => PRIVATE_FILES.has(part) || part.startsWith('.env.')),
)
)
throw new ManagedPackageCompatibilityError([
'package contains private settings or credentials',
]);
const config = await rawConfig(root, files);
let packageTarget = options.target;
if (config['managedTarget'] !== undefined) {
const target = object(config['managedTarget'], 'managedTarget');
if (
target['platform'] !== options.target.platform ||
(target['arch'] !== 'any' && target['arch'] !== options.target.arch)
) {
throw new ManagedPackageCompatibilityError([
'package target does not match this Runtime',
]);
}
packageTarget = {
platform: options.target.platform,
arch: target['arch'] as ManagedPackageTarget['arch'],
};
}
rejectFields(
config,
['lspServers', 'channels', 'workflows', 'executor', 'outputStyles'],
'manifest',
);
if (files.some((file) => /^(?:workflows|channels|lsp)\//.test(file)))
throw new ManagedPackageCompatibilityError([
'package contains unsupported executable components',
]);
const name = text(config['name'], 'name', 128);
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(name))
throw new ManagedPackageCompatibilityError(['name: invalid identity']);
const contributions: ManagedPackageContributions = {
skills: [],
commands: [],
agents: [],
context: [],
mcpServers: {},
hooks: {},
settings: [],
};
for (const file of selectedFiles(
files,
resourceRoots(config, 'skills', 'skills'),
).filter((file) => file.endsWith('/SKILL.md'))) {
const content = await readText(root, file);
const { metadata } = frontmatter(content);
rejectFields(
metadata,
[
'allowedTools',
'allowed-tools',
'hooks',
'paths',
'arguments',
'args-file',
],
file,
);
if (metadata['model'] !== undefined && metadata['model'] !== 'inherit')
throw new ManagedPackageCompatibilityError([
`${file}.model: unsupported`,
]);
const skill = parseSkillContent(content, file);
contributions.skills.push({
name: qualifySkillName(name, skill.name),
description: skill.description,
body: skill.body,
assetDirectory: path.posix.dirname(file),
model_invocable: skill.disableModelInvocation !== true,
user_invocable: skill.userInvocable !== false,
});
}
const commandRoots = resourceRoots(config, 'commands', 'commands');
for (const file of selectedFiles(files, commandRoots).filter((file) =>
/\.(?:md|toml)$/.test(file),
)) {
const content = await readText(root, file);
const parsed = file.endsWith('.toml')
? { metadata: {}, body: '', definition: object(parseToml(content), file) }
: { ...frontmatter(content), definition: undefined };
if (!parsed.definition)
MarkdownCommandDefSchema.parse({
frontmatter: parsed.metadata,
prompt: parsed.body,
});
const metadata = parsed.definition ?? parsed.metadata;
rejectFields(
metadata,
['allowed-tools', 'allowedTools', 'hooks', 'model', 'context', 'agent'],
file,
);
const relative = commandRoots.find((entry) => file.startsWith(`${entry}/`));
const authored = (
relative ? file.slice(relative.length + 1) : path.posix.basename(file)
)
.replace(/\.(?:md|toml)$/, '')
.split('/')
.map((part) => part.replaceAll(':', '_'))
.join(':');
contributions.commands.push({
name: `${name}:${authored}`,
description:
typeof metadata['description'] === 'string'
? metadata['description']
: authored,
prompt: text(parsed.definition?.['prompt'] ?? parsed.body, file),
...(typeof metadata['argument-hint'] === 'string'
? { argumentHint: metadata['argument-hint'] }
: {}),
modelInvocable:
metadata['disable-model-invocation'] !== true &&
metadata['disable-model-invocation'] !== 'true',
});
}
for (const file of selectedFiles(
files,
resourceRoots(config, 'agents', 'agents'),
).filter((file) => file.endsWith('.md'))) {
const { metadata, body } = frontmatter(await readText(root, file));
rejectFields(
metadata,
[
'mcpServers',
'mcp_servers',
'hooks',
'executor',
'container',
'isolation',
'skills',
'permissionMode',
'approvalMode',
'background',
'memory',
],
file,
);
const agent: ManagedPackageAgent = {
name: `${name}:${text(metadata['name'] ?? path.posix.basename(file, '.md'), file, 128)}`,
description: text(metadata['description'], `${file}.description`, 4096),
systemPrompt: text(body, file),
...(metadata['tools'] !== undefined
? { tools: strings(metadata['tools'], file) }
: {}),
...(metadata['disallowedTools'] !== undefined
? { disallowedTools: strings(metadata['disallowedTools'], file) }
: {}),
...(metadata['model'] !== undefined
? { model: text(metadata['model'], `${file}.model`, 512) }
: {}),
};
contributions.agents.push(agent);
}
const contextRoots =
config['contextFileName'] === undefined
? files.includes('QWEN.md')
? ['QWEN.md']
: files.includes('system-prompt.md')
? ['system-prompt.md']
: []
: resourceRoots(config, 'contextFileName', 'QWEN.md');
for (const file of contextRoots)
contributions.context.push({
path: file,
body: await readText(root, file),
});
let mcp = config['mcpServers'];
if (typeof mcp === 'string')
mcp = await readObject(root, managedPackageRelativePath(mcp));
if (mcp === undefined && files.includes('.mcp.json')) {
const loaded = await readObject(root, '.mcp.json');
mcp = loaded['mcpServers'] ?? loaded;
}
if (mcp === undefined && files.includes('mcp.json')) {
const loaded = await readObject(root, 'mcp.json');
mcp = loaded['mcpServers'] ?? loaded['servers'] ?? loaded;
}
if (mcp !== undefined) contributions.mcpServers = object(mcp, 'mcpServers');
let hooks = config['hooks'];
if (typeof hooks === 'string')
hooks = await readObject(root, managedPackageRelativePath(hooks));
if (hooks === undefined && files.includes('hooks/hooks.json'))
hooks = await readObject(root, 'hooks/hooks.json');
if (hooks !== undefined) {
const loaded = object(hooks, 'hooks');
contributions.hooks = object(loaded['hooks'] ?? loaded, 'hooks');
}
if (config['settings'] !== undefined) {
if (!Array.isArray(config['settings']))
throw new ManagedPackageCompatibilityError(['settings: invalid list']);
contributions.settings = config['settings'].map((entry: unknown) => {
const setting = object(entry, 'setting');
const envVar = text(setting['envVar'], 'setting.envVar', 128);
if (
!/^[A-Za-z_][A-Za-z0-9_]*$/.test(envVar) ||
envVar.startsWith('QWEN_MANAGED_') ||
[
'QWEN_EXTENSION_ROOT',
'CLAUDE_PLUGIN_ROOT',
'extensionPath',
'workspacePath',
].includes(envVar)
)
throw new ManagedPackageCompatibilityError([
'setting.envVar: reserved or invalid',
]);
return {
name: text(setting['name'], 'setting.name', 512),
envVar,
sensitive: setting['sensitive'] === true,
};
});
}
if (options.resolveRecipes !== false) {
const environment: Readonly<Record<string, string>> = {
...options.environment,
extensionPath: root,
CLAUDE_PLUGIN_ROOT: root,
QWEN_EXTENSION_ROOT: root,
workspacePath: options.workspaceDirectory ?? '',
workspaceFolder: options.workspaceDirectory ?? '',
pathSeparator: '/',
'/': '/',
};
const hydrate = (value: unknown): unknown => {
if (typeof value === 'string')
return value.replace(/\$\{([^}]+)\}/g, (_, variable: string) => {
if (!Object.hasOwn(environment, variable))
throw new ManagedPackageCompatibilityError([
`setting ${variable}: required`,
]);
return environment[variable];
});
if (Array.isArray(value)) return value.map(hydrate);
if (value && typeof value === 'object')
return Object.fromEntries(
Object.entries(value).map(([key, item]) => [key, hydrate(item)]),
);
return value;
};
contributions.mcpServers = hydrate(contributions.mcpServers) as Record<
string,
unknown
>;
contributions.hooks = hydrate(contributions.hooks) as Record<
string,
unknown
>;
}
for (const entries of [
contributions.skills,
contributions.commands,
contributions.agents,
]) {
if (new Set(entries.map((entry) => entry.name)).size !== entries.length)
throw new ManagedPackageCompatibilityError([
'duplicate component identity',
]);
}
if (
contributions.skills.length > 256 ||
Object.keys(contributions.mcpServers).length > 16
)
throw new ManagedPackageCompatibilityError([
'component catalog limit exceeded',
]);
const components: ManagedPackageComponent[] = [];
if (contributions.skills.length) components.push('skills');
if (contributions.commands.length) components.push('commands');
if (contributions.agents.length) components.push('agents');
if (contributions.context.length) components.push('context');
if (Object.keys(contributions.mcpServers).length) components.push('mcp');
if (Object.keys(contributions.hooks).length) components.push('hooks');
if (contributions.settings.length) components.push('settings');
return {
manifest: {
schemaVersion: 1,
compilerVersion: MANAGED_PACKAGE_COMPILER_VERSION,
name,
version: text(config['version'] ?? '1.0.0', 'version', 128),
target: packageTarget,
components,
...(config['managedSource']
? {
sourceType: text(
object(config['managedSource'], 'managedSource')['type'],
'sourceType',
64,
),
sourceRevision: text(
object(config['managedSource'], 'managedSource')['revision'],
'sourceRevision',
256,
),
}
: {}),
},
contributions,
};
}
export async function prepareManagedExtensionPackage(
source: ExtensionInstallMetadata,
options: {
target: ManagedPackageTarget;
destination: string;
signal?: AbortSignal;
},
): Promise<{
archivePath: string;
packageDigest: string;
manifest: ManagedPackageManifest;
sourceRevision: string;
}> {
if (['local', 'link', 'snapshot'].includes(source.type)) {
const isDirectory = (await fs.stat(source.source)).isDirectory();
const installed =
isDirectory &&
(await fs
.lstat(path.join(source.source, '.qwen-extension-install.json'))
.catch((error: NodeJS.ErrnoException) => {
if (error.code !== 'ENOENT') throw error;
return undefined;
}));
if (installed)
throw new ManagedPackageCompatibilityError([
'installed package requires its original source',
]);
}
const temporary = await fs.mkdtemp(
path.join(os.tmpdir(), 'qwen-managed-package-'),
);
const root = path.join(temporary, 'package');
await fs.mkdir(root);
let sourceRevision = source.ref ?? '';
try {
if (source.type === 'git')
sourceRevision = await cloneFromGit(source, root, options.signal);
else if (source.type === 'npm') {
const result = await downloadFromNpmRegistry(
{ ...source },
root,
options.signal,
{ enforceResourceLimits: true },
);
sourceRevision = result.version;
} else if (source.type === 'github-release') {
const result = await downloadFromGitHubRelease(
source,
root,
options.signal,
options.target,
{ enforceResourceLimits: true },
);
sourceRevision = result.tagName;
} else if (source.type === 'archive-url')
await downloadFromArchiveUrl(source, root, options.signal, {
enforceResourceLimits: true,
});
else if (/\.(?:zip|tar\.gz)$/i.test(source.source))
await extractArchiveFile(source.source, root, options.signal, {
enforceResourceLimits: true,
});
else {
const files = await filesUnder(source.source, true);
for (const file of files) {
const destination = path.join(root, file);
await fs.mkdir(path.dirname(destination), { recursive: true });
await fs.copyFile(path.join(source.source, file), destination);
}
}
options.signal?.throwIfAborted();
const scrub = async (directory: string): Promise<void> => {
for (const entry of await fs.readdir(directory, {
withFileTypes: true,
})) {
const file = path.join(directory, entry.name);
if (PRIVATE_FILES.has(entry.name) || entry.name.startsWith('.env.')) {
await fs.rm(file, { recursive: true, force: true });
} else if (entry.isDirectory()) await scrub(file);
}
};
await scrub(root);
let files = await filesUnder(root);
const config = await rawConfig(root, files);
config['managedTarget'] = options.target;
config['managedSource'] = {
type:
source.type === 'local'
? /\.(?:zip|tar\.gz)$/i.test(source.source)
? 'archive'
: 'directory'
: source.type,
revision:
sourceRevision || text(config['version'] ?? 'snapshot', 'version', 128),
};
await fs.writeFile(
path.join(root, 'qwen-extension.json'),
JSON.stringify(config),
);
files = await filesUnder(root);
const compiled = await compileManagedExtensionPackage(root, {
target: options.target,
resolveRecipes: false,
});
await fs.mkdir(options.destination, { recursive: true });
const archivePath = path.join(options.destination, 'package.tar.gz');
await tar.create(
{
cwd: root,
file: archivePath,
gzip: true,
portable: true,
noMtime: true,
},
files.sort(),
);
if ((await fs.stat(archivePath)).size > MANAGED_PACKAGE_MAX_ARCHIVE_BYTES)
throw new ManagedPackageCompatibilityError([
'compressed package limit exceeded',
]);
const hash = createHash('sha256');
for await (const chunk of createReadStream(archivePath)) hash.update(chunk);
const packageDigest = hash.digest('hex');
return {
archivePath,
packageDigest,
manifest: compiled.manifest,
sourceRevision: sourceRevision || packageDigest,
};
} finally {
await fs.rm(temporary, { recursive: true, force: true });
}
}
+6 -2
View File
@@ -12,7 +12,10 @@ import { ExtensionUpdateState } from './extensionManager.js';
import { createDebugLogger } from '../utils/debugLogger.js';
import { redactUrlCredentials } from './redaction.js';
import { clientForUrl } from './http-client.js';
import { assertTarArchiveLinksAreSafe } from './archive-safety.js';
import {
assertTarArchiveLinksAreSafe,
type TarArchiveSafetyOptions,
} from './archive-safety.js';
import { resolveNetworkTarget } from './network-policy.js';
const debugLogger = createDebugLogger('EXT_NPM');
@@ -524,6 +527,7 @@ export async function downloadFromNpmRegistry(
installMetadata: ExtensionInstallMetadata,
destination: string,
signal?: AbortSignal,
archiveSafety: TarArchiveSafetyOptions = {},
): Promise<NpmDownloadResult> {
const { name, version: requestedVersion } = parseNpmPackageSource(
installMetadata.source,
@@ -602,7 +606,7 @@ export async function downloadFromNpmRegistry(
signal?.throwIfAborted();
// Extract tarball
await assertTarArchiveLinksAreSafe(tarballPath);
await assertTarArchiveLinksAreSafe(tarballPath, signal, archiveSafety);
signal?.throwIfAborted();
await tar.x({
file: tarballPath,
+139 -111
View File
@@ -1204,25 +1204,12 @@ export class HookRunner {
}
};
// Use hook-specific shell configuration if specified
const shellConfig = this.getShellConfigForHook(hookConfig);
const command = this.expandCommand(
hookConfig.command,
const { shellConfig, command, env } = this.commandInvocation(
hookConfig,
input,
shellConfig.shell,
options?.environment,
);
const env: NodeJS.ProcessEnv = {
// Hook commands are child processes launched on the agent's behalf,
// so they must not inherit Qwen-internal daemon secrets.
...sanitizeChildEnv(options?.environment ?? process.env),
GEMINI_PROJECT_DIR: input.cwd,
CLAUDE_PROJECT_DIR: input.cwd, // For compatibility
QWEN_PROJECT_DIR: input.cwd, // For Qwen Code compatibility
...getShellContextEnvVars(),
...hookConfig.env,
};
let processUnit: HookCommandCgroup | undefined;
if (options?.waitForProcessTree) {
try {
@@ -1591,101 +1578,16 @@ export class HookRunner {
return;
}
// Parse output
// Exit code 2 is a blocking error - ignore stdout, use stderr only
let output: HookOutput | undefined;
const isBlockingError = exitCode === 2;
// For exit code 2, only use stderr (ignore stdout)
const stdoutText = stdout.trim();
const textToParse = isBlockingError
? stderr.trim()
: stdoutText || stderr.trim();
// Only stdout is promoted as plain-text context; the stderr fallback
// stays a system message. JSON on stderr is still parsed as structured
// output when stdout is empty, as it was before.
const parsedFromStdout = !isBlockingError && stdoutText !== '';
if (textToParse) {
// Structured output is a JSON object, possibly double-encoded as a
// JSON string (applies to both exit 0 and exit 2). Anything else,
// including bare JSON values such as `42` or `[1, 2]`, is plain text.
let parsed: unknown;
let parseFailed = false;
try {
parsed = JSON.parse(textToParse);
if (typeof parsed === 'string') {
parsed = JSON.parse(parsed);
}
} catch {
parseFailed = true;
}
if (
!parseFailed &&
parsed !== null &&
typeof parsed === 'object' &&
!Array.isArray(parsed)
) {
output = parsed as HookOutput;
} else if (
parseFailed &&
!isBlockingError &&
textToParse.startsWith('{')
) {
// Output that starts like a JSON object but does not parse is a
// broken structured payload, not context or a message: as in
// Claude Code, it is a non-blocking error and nothing of it reaches
// the model. Exit code 2 still blocks on its stderr text below.
debugLogger.warn(
`Hook "${hookConfig.name || hookConfig.command}" printed output that starts like a JSON object but is not valid JSON; it is ignored`,
);
finish({
hookConfig,
eventName,
success: false,
outcome: 'non_blocking_error',
error: new Error('Hook output is not valid JSON'),
stdout,
stderr,
exitCode: exitCode ?? -1,
duration,
});
return;
} else {
output = this.convertPlainTextToHookOutput(
textToParse,
isBlockingError
? exitCode
: exitCode === EXIT_CODE_SUCCESS
? EXIT_CODE_SUCCESS
: EXIT_CODE_NON_BLOCKING_ERROR,
parsedFromStdout ? eventName : undefined,
);
}
}
const killedBySignal = exitCode === null;
finish({
hookConfig,
eventName,
success: exitCode === EXIT_CODE_SUCCESS,
// A signal this runner did not send (it returned above for its own
// abort and timeout) leaves exitCode null: a failure, not a cancel.
outcome:
exitCode === EXIT_CODE_SUCCESS
? 'success'
: exitCode === 2
? 'blocking'
: 'non_blocking_error',
output,
stdout,
stderr,
exitCode: exitCode ?? -1,
duration,
...(killedBySignal && {
error: new Error('Hook killed by signal'),
}),
});
finish(
this.commandResult(
hookConfig,
eventName,
stdout,
stderr,
exitCode,
duration,
),
);
});
// Handle process errors
@@ -1711,6 +1613,132 @@ export class HookRunner {
});
}
commandInvocation(
hookConfig: CommandHookConfig,
input: HookInput,
environment?: NodeJS.ProcessEnv,
) {
const shellConfig = this.getShellConfigForHook(hookConfig);
return {
shellConfig,
command: this.expandCommand(hookConfig.command, input, shellConfig.shell),
env: {
// Hook commands are child processes launched on the agent's behalf,
// so they must not inherit Qwen-internal daemon secrets.
...sanitizeChildEnv(environment ?? process.env),
GEMINI_PROJECT_DIR: input.cwd,
CLAUDE_PROJECT_DIR: input.cwd,
QWEN_PROJECT_DIR: input.cwd,
...getShellContextEnvVars(),
...hookConfig.env,
} as NodeJS.ProcessEnv,
};
}
commandResult(
hookConfig: CommandHookConfig,
eventName: HookEventName,
stdout: string,
stderr: string,
exitCode: number | null,
duration: number,
): HookExecutionResult {
// Parse output
// Exit code 2 is a blocking error - ignore stdout, use stderr only
let output: HookOutput | undefined;
const isBlockingError = exitCode === 2;
// For exit code 2, only use stderr (ignore stdout)
const stdoutText = stdout.trim();
const textToParse = isBlockingError
? stderr.trim()
: stdoutText || stderr.trim();
// Only stdout is promoted as plain-text context; the stderr fallback
// stays a system message. JSON on stderr is still parsed as structured
// output when stdout is empty, as it was before.
const parsedFromStdout = !isBlockingError && stdoutText !== '';
if (textToParse) {
// Structured output is a JSON object, possibly double-encoded as a
// JSON string (applies to both exit 0 and exit 2). Anything else,
// including bare JSON values such as `42` or `[1, 2]`, is plain text.
let parsed: unknown;
let parseFailed = false;
try {
parsed = JSON.parse(textToParse);
if (typeof parsed === 'string') {
parsed = JSON.parse(parsed);
}
} catch {
parseFailed = true;
}
if (
!parseFailed &&
parsed !== null &&
typeof parsed === 'object' &&
!Array.isArray(parsed)
) {
output = parsed as HookOutput;
} else if (
parseFailed &&
!isBlockingError &&
textToParse.startsWith('{')
) {
// Output that starts like a JSON object but does not parse is a
// broken structured payload, not context or a message: as in
// Claude Code, it is a non-blocking error and nothing of it reaches
// the model. Exit code 2 still blocks on its stderr text below.
debugLogger.warn(
`Hook "${hookConfig.name || hookConfig.command}" printed output that starts like a JSON object but is not valid JSON; it is ignored`,
);
return {
hookConfig,
eventName,
success: false,
outcome: 'non_blocking_error',
error: new Error('Hook output is not valid JSON'),
stdout,
stderr,
exitCode: exitCode ?? -1,
duration,
};
} else {
output = this.convertPlainTextToHookOutput(
textToParse,
isBlockingError
? exitCode
: exitCode === EXIT_CODE_SUCCESS
? EXIT_CODE_SUCCESS
: EXIT_CODE_NON_BLOCKING_ERROR,
parsedFromStdout ? eventName : undefined,
);
}
}
const killedBySignal = exitCode === null;
return {
hookConfig,
eventName,
success: exitCode === EXIT_CODE_SUCCESS,
// A signal this runner did not send (it returned above for its own
// abort and timeout) leaves exitCode null: a failure, not a cancel.
outcome:
exitCode === EXIT_CODE_SUCCESS
? 'success'
: exitCode === 2
? 'blocking'
: 'non_blocking_error',
output,
stdout,
stderr,
exitCode: exitCode ?? -1,
duration,
...(killedBySignal && {
error: new Error('Hook killed by signal'),
}),
};
}
/**
* Resolve project directory variables in a command string before launch.
*
+5
View File
@@ -1395,6 +1395,11 @@ export function detectTodoChanges(
* Hook execution result
*/
export interface HookExecutionResult {
processReceipt?: {
readonly ownerId: string;
readonly scope: 'process_tree' | 'process_group';
readonly state: 'stopped' | 'outcome_unknown';
};
/** True only if a managed command never starts or its owned cgroup is empty. */
processTreeDrained?: boolean;
httpRequestState?: 'not_started' | 'response_received' | 'outcome_unknown';
@@ -0,0 +1,526 @@
{
"schemaVersion": 1,
"valid": [
{
"name": "nullable_preparing",
"content": {
"kind": "hosted-extension-config",
"formatVersion": 1,
"command": {
"operationId": "11111111-1111-4111-8111-111111111111",
"actorKey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"expectedRevision": 0,
"extensions": []
},
"mode": "apply",
"phaseOperationId": "11111111-1111-4111-8111-111111111111",
"configRevision": 1,
"previousConfigRevision": 0,
"phase": "preparing",
"previousBundleRef": null,
"restoresBundleRef": null,
"bundleRef": null,
"rootSnapshotRef": {
"resourceId": "root",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"stageAckRefs": [],
"enableAckRefs": [],
"error": null
}
},
{
"name": "prepared",
"content": {
"kind": "hosted-extension-config",
"formatVersion": 1,
"command": {
"operationId": "11111111-1111-4111-8111-111111111111",
"actorKey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"expectedRevision": 0,
"extensions": []
},
"mode": "apply",
"phaseOperationId": "11111111-1111-4111-8111-111111111111",
"configRevision": 1,
"previousConfigRevision": 0,
"phase": "prepared",
"previousBundleRef": null,
"restoresBundleRef": null,
"bundleRef": {
"resourceId": "bundle",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"rootSnapshotRef": {
"resourceId": "root",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"stageAckRefs": [
{
"resourceId": "hstage",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
{
"resourceId": "rstage",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
],
"enableAckRefs": [],
"error": null
}
},
{
"name": "active",
"content": {
"kind": "hosted-extension-config",
"formatVersion": 1,
"command": {
"operationId": "11111111-1111-4111-8111-111111111111",
"actorKey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"expectedRevision": 0,
"extensions": []
},
"mode": "apply",
"phaseOperationId": "11111111-1111-4111-8111-111111111111",
"configRevision": 1,
"previousConfigRevision": 0,
"phase": "active",
"previousBundleRef": null,
"restoresBundleRef": null,
"bundleRef": {
"resourceId": "bundle",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"rootSnapshotRef": {
"resourceId": "root",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"stageAckRefs": [
{
"resourceId": "hstage",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
{
"resourceId": "rstage",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
],
"enableAckRefs": [
{
"resourceId": "henable",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
{
"resourceId": "renable",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
],
"error": null
}
},
{
"name": "separate_mcp_token",
"content": {
"kind": "hosted-extension-config",
"formatVersion": 1,
"command": {
"operationId": "11111111-1111-4111-8111-111111111111",
"actorKey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"expectedRevision": 0,
"extensions": [],
"mcpChange": {
"action": "disable",
"serverId": "base",
"expectedRevision": 7,
"servers": [
{
"serverId": "base",
"serverRevision": 2,
"definitionDigest": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"state": "disabled"
}
]
}
},
"mode": "apply",
"phaseOperationId": "11111111-1111-4111-8111-111111111111",
"configRevision": 1,
"previousConfigRevision": 0,
"phase": "preparing",
"previousBundleRef": null,
"restoresBundleRef": null,
"bundleRef": null,
"rootSnapshotRef": {
"resourceId": "root",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"stageAckRefs": [],
"enableAckRefs": [],
"error": null
}
},
{
"name": "committed",
"content": {
"kind": "hosted-extension-config",
"formatVersion": 1,
"command": {
"operationId": "11111111-1111-4111-8111-111111111111",
"actorKey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"expectedRevision": 0,
"extensions": []
},
"mode": "apply",
"phaseOperationId": "11111111-1111-4111-8111-111111111111",
"configRevision": 1,
"previousConfigRevision": 0,
"phase": "committed",
"previousBundleRef": null,
"restoresBundleRef": null,
"bundleRef": {
"resourceId": "bundle",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"rootSnapshotRef": {
"resourceId": "root",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"stageAckRefs": [
{
"resourceId": "hstage",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
{
"resourceId": "rstage",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
],
"enableAckRefs": [],
"error": null
}
}
],
"invalid": [
{
"name": "committed_missing_stage",
"content": {
"kind": "hosted-extension-config",
"formatVersion": 1,
"command": {
"operationId": "11111111-1111-4111-8111-111111111111",
"actorKey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"expectedRevision": 0,
"extensions": []
},
"mode": "apply",
"phaseOperationId": "11111111-1111-4111-8111-111111111111",
"configRevision": 1,
"previousConfigRevision": 0,
"phase": "committed",
"previousBundleRef": null,
"restoresBundleRef": null,
"bundleRef": {
"resourceId": "bundle",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"rootSnapshotRef": {
"resourceId": "root",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"stageAckRefs": [],
"enableAckRefs": [],
"error": null
}
},
{
"name": "prepared_null_bundle",
"content": {
"kind": "hosted-extension-config",
"formatVersion": 1,
"command": {
"operationId": "11111111-1111-4111-8111-111111111111",
"actorKey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"expectedRevision": 0,
"extensions": []
},
"mode": "apply",
"phaseOperationId": "11111111-1111-4111-8111-111111111111",
"configRevision": 1,
"previousConfigRevision": 0,
"phase": "prepared",
"previousBundleRef": null,
"restoresBundleRef": null,
"bundleRef": null,
"rootSnapshotRef": {
"resourceId": "root",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"stageAckRefs": [
{
"resourceId": "hstage",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
{
"resourceId": "rstage",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
],
"enableAckRefs": [],
"error": null
}
},
{
"name": "active_missing_enable",
"content": {
"kind": "hosted-extension-config",
"formatVersion": 1,
"command": {
"operationId": "11111111-1111-4111-8111-111111111111",
"actorKey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"expectedRevision": 0,
"extensions": []
},
"mode": "apply",
"phaseOperationId": "11111111-1111-4111-8111-111111111111",
"configRevision": 1,
"previousConfigRevision": 0,
"phase": "active",
"previousBundleRef": null,
"restoresBundleRef": null,
"bundleRef": {
"resourceId": "bundle",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"rootSnapshotRef": {
"resourceId": "root",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"stageAckRefs": [
{
"resourceId": "hstage",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
{
"resourceId": "rstage",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
],
"enableAckRefs": [],
"error": null
}
},
{
"name": "secret_field",
"content": {
"kind": "hosted-extension-config",
"formatVersion": 1,
"command": {
"operationId": "11111111-1111-4111-8111-111111111111",
"actorKey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"expectedRevision": 0,
"extensions": []
},
"mode": "apply",
"phaseOperationId": "11111111-1111-4111-8111-111111111111",
"configRevision": 1,
"previousConfigRevision": 0,
"phase": "preparing",
"previousBundleRef": null,
"restoresBundleRef": null,
"bundleRef": null,
"rootSnapshotRef": {
"resourceId": "root",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"stageAckRefs": [],
"enableAckRefs": [],
"error": null,
"settings": {
"token": "must_not_persist"
}
}
},
{
"name": "pin_missing_digest",
"content": {
"kind": "hosted-extension-config",
"formatVersion": 1,
"command": {
"operationId": "11111111-1111-4111-8111-111111111111",
"actorKey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"expectedRevision": 0,
"extensions": [
{
"extensionId": "sample",
"packageRevision": 1
}
]
},
"mode": "apply",
"phaseOperationId": "11111111-1111-4111-8111-111111111111",
"configRevision": 1,
"previousConfigRevision": 0,
"phase": "preparing",
"previousBundleRef": null,
"restoresBundleRef": null,
"bundleRef": null,
"rootSnapshotRef": {
"resourceId": "root",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"stageAckRefs": [],
"enableAckRefs": [],
"error": null
}
}
],
"legacy": [
{
"name": "mcp_v1_unchanged",
"content": {
"kind": "hosted-mcp-settings",
"formatVersion": 1,
"command": {
"operationId": "11111111-1111-4111-8111-111111111111",
"actorKey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"action": "disable",
"serverId": "base",
"expectedRevision": 7
},
"servers": [
{
"serverId": "base",
"serverRevision": 2,
"definitionDigest": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"state": "disabled"
}
]
}
}
],
"successors": [
{
"name": "initial_prepare",
"previous": null,
"next": "nullable_preparing",
"valid": true
},
{
"name": "stage",
"previous": "nullable_preparing",
"next": "prepared",
"valid": true
},
{
"name": "commit",
"previous": "prepared",
"next": "committed",
"valid": true
},
{
"name": "enable",
"previous": "committed",
"next": "active",
"valid": true
},
{
"name": "cannot_skip_commit",
"previous": "prepared",
"next": "active",
"valid": false
},
{
"name": "cannot_repeat_active",
"previous": "active",
"next": "active",
"valid": false
}
],
"phaseOperationIds": [
{
"operationId": "11111111-1111-4111-8111-111111111111",
"expected": "a6204ca0-0013-58fe-8c73-2e92f96c3a7a"
}
]
}
@@ -16,6 +16,7 @@
"channel_delivery": "channel_delivery"
},
"additionalRecordBodies": {
"tool_stage": null,
"history_maintenance": null,
"schedule": null,
"automation_run": "automation_run",
@@ -0,0 +1,632 @@
{
"schemaVersion": 1,
"valid": [
{
"name": "admitted",
"record": {
"kind": "tool_stage",
"toolStageId": "stage-1",
"expansionId": "expansion-1",
"stepIndex": 0,
"toolName": "shell",
"inputRef": {
"resourceId": "input",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"permissionRef": null,
"resultRef": null,
"run": {
"state": "admitted",
"reason": null,
"definition": {
"definitionId": "shell",
"definitionRevision": 1,
"definitionDigest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"executionCallId": "stage-1",
"effectId": null,
"dispatchId": null,
"deliveryId": null,
"execution": "intent",
"runtime": null,
"delivery": null
}
}
},
{
"name": "dispatched",
"record": {
"kind": "tool_stage",
"toolStageId": "stage-1",
"expansionId": "expansion-1",
"stepIndex": 0,
"toolName": "shell",
"inputRef": {
"resourceId": "input",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"permissionRef": {
"resourceId": "permission",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"resultRef": null,
"run": {
"state": "running",
"reason": null,
"definition": {
"definitionId": "shell",
"definitionRevision": 1,
"definitionDigest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"executionCallId": "stage-1",
"effectId": null,
"dispatchId": null,
"deliveryId": null,
"execution": "dispatch_started",
"runtime": {
"runtimeBindingId": "binding-1",
"generation": "1"
},
"delivery": null
}
}
},
{
"name": "settled",
"record": {
"kind": "tool_stage",
"toolStageId": "stage-1",
"expansionId": "expansion-1",
"stepIndex": 0,
"toolName": "shell",
"inputRef": {
"resourceId": "input",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"permissionRef": {
"resourceId": "permission",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"resultRef": {
"resourceId": "result",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"run": {
"state": "settled",
"reason": null,
"definition": {
"definitionId": "shell",
"definitionRevision": 1,
"definitionDigest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"executionCallId": "stage-1",
"effectId": null,
"dispatchId": null,
"deliveryId": null,
"execution": "settled",
"runtime": {
"runtimeBindingId": "binding-1",
"generation": "1"
},
"delivery": null
}
}
}
],
"invalid": [
{
"name": "wrong_definition",
"record": {
"kind": "tool_stage",
"toolStageId": "stage-1",
"expansionId": "expansion-1",
"stepIndex": 0,
"toolName": "read_file",
"inputRef": {
"resourceId": "input",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"permissionRef": null,
"resultRef": null,
"run": {
"state": "admitted",
"reason": null,
"definition": {
"definitionId": "shell",
"definitionRevision": 1,
"definitionDigest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"executionCallId": "stage-1",
"effectId": null,
"dispatchId": null,
"deliveryId": null,
"execution": "intent",
"runtime": null,
"delivery": null
}
}
},
{
"name": "wrong_execution_identity",
"record": {
"kind": "tool_stage",
"toolStageId": "stage-1",
"expansionId": "expansion-1",
"stepIndex": 0,
"toolName": "shell",
"inputRef": {
"resourceId": "input",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"permissionRef": null,
"resultRef": null,
"run": {
"state": "admitted",
"reason": null,
"definition": {
"definitionId": "shell",
"definitionRevision": 1,
"definitionDigest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"executionCallId": "different",
"effectId": null,
"dispatchId": null,
"deliveryId": null,
"execution": "intent",
"runtime": null,
"delivery": null
}
}
},
{
"name": "unknown_field",
"record": {
"kind": "tool_stage",
"toolStageId": "stage-1",
"expansionId": "expansion-1",
"stepIndex": 0,
"toolName": "shell",
"inputRef": {
"resourceId": "input",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"permissionRef": null,
"resultRef": null,
"run": {
"state": "admitted",
"reason": null,
"definition": {
"definitionId": "shell",
"definitionRevision": 1,
"definitionDigest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"executionCallId": "stage-1",
"effectId": null,
"dispatchId": null,
"deliveryId": null,
"execution": "intent",
"runtime": null,
"delivery": null
},
"secret": true
}
},
{
"name": "negative_step",
"record": {
"kind": "tool_stage",
"toolStageId": "stage-1",
"expansionId": "expansion-1",
"stepIndex": -1,
"toolName": "shell",
"inputRef": {
"resourceId": "input",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"permissionRef": null,
"resultRef": null,
"run": {
"state": "admitted",
"reason": null,
"definition": {
"definitionId": "shell",
"definitionRevision": 1,
"definitionDigest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"executionCallId": "stage-1",
"effectId": null,
"dispatchId": null,
"deliveryId": null,
"execution": "intent",
"runtime": null,
"delivery": null
}
}
}
],
"successors": [
{
"name": "dispatch",
"previous": {
"kind": "tool_stage",
"toolStageId": "stage-1",
"expansionId": "expansion-1",
"stepIndex": 0,
"toolName": "shell",
"inputRef": {
"resourceId": "input",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"permissionRef": null,
"resultRef": null,
"run": {
"state": "admitted",
"reason": null,
"definition": {
"definitionId": "shell",
"definitionRevision": 1,
"definitionDigest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"executionCallId": "stage-1",
"effectId": null,
"dispatchId": null,
"deliveryId": null,
"execution": "intent",
"runtime": null,
"delivery": null
}
},
"next": {
"kind": "tool_stage",
"toolStageId": "stage-1",
"expansionId": "expansion-1",
"stepIndex": 0,
"toolName": "shell",
"inputRef": {
"resourceId": "input",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"permissionRef": {
"resourceId": "permission",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"resultRef": null,
"run": {
"state": "running",
"reason": null,
"definition": {
"definitionId": "shell",
"definitionRevision": 1,
"definitionDigest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"executionCallId": "stage-1",
"effectId": null,
"dispatchId": null,
"deliveryId": null,
"execution": "dispatch_started",
"runtime": {
"runtimeBindingId": "binding-1",
"generation": "1"
},
"delivery": null
}
},
"valid": true
},
{
"name": "result",
"previous": {
"kind": "tool_stage",
"toolStageId": "stage-1",
"expansionId": "expansion-1",
"stepIndex": 0,
"toolName": "shell",
"inputRef": {
"resourceId": "input",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"permissionRef": {
"resourceId": "permission",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"resultRef": null,
"run": {
"state": "running",
"reason": null,
"definition": {
"definitionId": "shell",
"definitionRevision": 1,
"definitionDigest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"executionCallId": "stage-1",
"effectId": null,
"dispatchId": null,
"deliveryId": null,
"execution": "dispatch_started",
"runtime": {
"runtimeBindingId": "binding-1",
"generation": "1"
},
"delivery": null
}
},
"next": {
"kind": "tool_stage",
"toolStageId": "stage-1",
"expansionId": "expansion-1",
"stepIndex": 0,
"toolName": "shell",
"inputRef": {
"resourceId": "input",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"permissionRef": {
"resourceId": "permission",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"resultRef": {
"resourceId": "result",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"run": {
"state": "settled",
"reason": null,
"definition": {
"definitionId": "shell",
"definitionRevision": 1,
"definitionDigest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"executionCallId": "stage-1",
"effectId": null,
"dispatchId": null,
"deliveryId": null,
"execution": "settled",
"runtime": {
"runtimeBindingId": "binding-1",
"generation": "1"
},
"delivery": null
}
},
"valid": true
},
{
"name": "substitute_input",
"previous": {
"kind": "tool_stage",
"toolStageId": "stage-1",
"expansionId": "expansion-1",
"stepIndex": 0,
"toolName": "shell",
"inputRef": {
"resourceId": "input",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"permissionRef": {
"resourceId": "permission",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"resultRef": null,
"run": {
"state": "running",
"reason": null,
"definition": {
"definitionId": "shell",
"definitionRevision": 1,
"definitionDigest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"executionCallId": "stage-1",
"effectId": null,
"dispatchId": null,
"deliveryId": null,
"execution": "dispatch_started",
"runtime": {
"runtimeBindingId": "binding-1",
"generation": "1"
},
"delivery": null
}
},
"next": {
"kind": "tool_stage",
"toolStageId": "stage-1",
"expansionId": "expansion-1",
"stepIndex": 0,
"toolName": "shell",
"inputRef": {
"resourceId": "another",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"permissionRef": {
"resourceId": "permission",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"resultRef": {
"resourceId": "result",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"run": {
"state": "settled",
"reason": null,
"definition": {
"definitionId": "shell",
"definitionRevision": 1,
"definitionDigest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"executionCallId": "stage-1",
"effectId": null,
"dispatchId": null,
"deliveryId": null,
"execution": "settled",
"runtime": {
"runtimeBindingId": "binding-1",
"generation": "1"
},
"delivery": null
}
},
"valid": false
},
{
"name": "replace_permission",
"previous": {
"kind": "tool_stage",
"toolStageId": "stage-1",
"expansionId": "expansion-1",
"stepIndex": 0,
"toolName": "shell",
"inputRef": {
"resourceId": "input",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"permissionRef": {
"resourceId": "permission",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"resultRef": null,
"run": {
"state": "running",
"reason": null,
"definition": {
"definitionId": "shell",
"definitionRevision": 1,
"definitionDigest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"executionCallId": "stage-1",
"effectId": null,
"dispatchId": null,
"deliveryId": null,
"execution": "dispatch_started",
"runtime": {
"runtimeBindingId": "binding-1",
"generation": "1"
},
"delivery": null
}
},
"next": {
"kind": "tool_stage",
"toolStageId": "stage-1",
"expansionId": "expansion-1",
"stepIndex": 0,
"toolName": "shell",
"inputRef": {
"resourceId": "input",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"permissionRef": {
"resourceId": "another",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"resultRef": {
"resourceId": "result",
"kind": "managed-test",
"schemaVersion": 1,
"byteLength": 2,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"run": {
"state": "settled",
"reason": null,
"definition": {
"definitionId": "shell",
"definitionRevision": 1,
"definitionDigest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"executionCallId": "stage-1",
"effectId": null,
"dispatchId": null,
"deliveryId": null,
"execution": "settled",
"runtime": {
"runtimeBindingId": "binding-1",
"generation": "1"
},
"delivery": null
}
},
"valid": false
}
]
}
@@ -176,7 +176,9 @@ export interface HttpManagedSessionStores {
close(): Promise<void>;
setLifecycleAuthority(authority?: ManagedSessionLifecycleAuthority): void;
authorizeLifecycle(kind?: 'close' | 'delete'): Promise<void>;
authorizeOrdinary(kind?: 'legacy-close'): Promise<void>;
authorizeOrdinary(
kind?: 'legacy-close' | `configuration:${string}`,
): Promise<void>;
approvalSelection(): Promise<ManagedSessionApprovalSelection | undefined>;
claimUserMessages(
promptId: string,
@@ -808,7 +810,9 @@ class ManagedSessionStoreHttpClient {
this.lifecycleAuthority = authority;
}
async authorizeOrdinary(kind?: 'legacy-close'): Promise<void> {
async authorizeOrdinary(
kind?: 'legacy-close' | `configuration:${string}`,
): Promise<void> {
await this.assertWritable();
await this.json('/execution:authorize', 'POST', {
...(kind ? { kind } : {}),
@@ -164,6 +164,9 @@ describe('managed child operations (H4b)', () => {
}
for (const patch of [
{ extra: true },
{ version: '1' },
{ version: true },
{ version: ['1'] },
{ approvalMode: ['yolo'] },
{ approvalRevision: -1 },
{ modelRevision: Number.MAX_SAFE_INTEGER + 1 },
@@ -17,6 +17,10 @@ import {
parseDefinitionPin,
type DefinitionPin,
} from './managed-extension-record.js';
import {
parseManagedExtensionPin,
type ManagedExtensionPin,
} from './managed-extension-package.js';
import {
assertManagedSessionDurableRef,
ManagedSessionRecordError,
@@ -127,7 +131,7 @@ export function parsePublicChildModelRoute(
}
export interface PublicChildLaunchSnapshot {
readonly version: 1;
readonly version: 1 | 2;
readonly sourcePromptId: string;
readonly approvalMode: 'default' | 'auto-edit' | 'yolo';
readonly approvalRevision: number;
@@ -135,6 +139,63 @@ export interface PublicChildLaunchSnapshot {
readonly modelRevision: number;
readonly modelRoute: PublicChildModelRoute;
readonly skillsRef: ManagedSessionDurableRef | null;
readonly extensionAgent?: PublicChildExtensionAgent;
}
export interface PublicChildExtensionAgent {
readonly configRevision: number;
readonly bundleRef: ManagedSessionDurableRef;
readonly pin: ManagedExtensionPin;
readonly contributionsRef: ManagedSessionDurableRef;
readonly agentName: string;
readonly toolNames: readonly string[];
}
export function parsePublicChildExtensionAgent(
value: unknown,
): PublicChildExtensionAgent {
if (!value || typeof value !== 'object' || Array.isArray(value))
throw new ManagedSessionRecordError('Invalid extension agent descriptor.');
const fields = value as Record<string, ManagedSessionJsonValue>;
if (
Object.keys(fields).sort().join(',') !==
'agentName,bundleRef,configRevision,contributionsRef,pin,toolNames' ||
!Number.isSafeInteger(fields['configRevision']) ||
Number(fields['configRevision']) < 1 ||
typeof fields['agentName'] !== 'string' ||
!/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/u.test(fields['agentName']) ||
!Array.isArray(fields['toolNames']) ||
fields['toolNames'].length > 256 ||
fields['toolNames'].some(
(name) =>
typeof name !== 'string' || !/^[A-Za-z0-9_.:-]{1,128}$/u.test(name),
) ||
new Set(fields['toolNames']).size !== fields['toolNames'].length
)
throw new ManagedSessionRecordError('Invalid extension agent descriptor.');
const bundleRef = assertManagedSessionDurableRef(
fields['bundleRef'],
'extension agent bundle',
);
const contributionsRef = assertManagedSessionDurableRef(
fields['contributionsRef'],
'extension agent contributions',
);
if (
bundleRef.kind !== 'managed-extension-config-bundle' ||
bundleRef.schemaVersion !== 1 ||
contributionsRef.kind !== 'managed-extension-contributions' ||
contributionsRef.schemaVersion !== 1
)
throw new ManagedSessionRecordError('Invalid extension agent references.');
return Object.freeze({
configRevision: fields['configRevision'] as number,
bundleRef,
pin: parseManagedExtensionPin(fields['pin']),
contributionsRef,
agentName: fields['agentName'],
toolNames: Object.freeze([...(fields['toolNames'] as string[])]),
});
}
export interface ChildLaunchEnvelope {
@@ -153,8 +214,10 @@ function parsePublicChildLaunch(value: unknown): PublicChildLaunchSnapshot {
const fields = value as Record<string, unknown>;
if (
Object.keys(fields).sort().join(',') !==
'approvalMode,approvalRevision,modelId,modelRevision,modelRoute,skillsRef,sourcePromptId,version' ||
fields['version'] !== 1 ||
(fields['version'] === 2
? 'approvalMode,approvalRevision,extensionAgent,modelId,modelRevision,modelRoute,skillsRef,sourcePromptId,version'
: 'approvalMode,approvalRevision,modelId,modelRevision,modelRoute,skillsRef,sourcePromptId,version') ||
(fields['version'] !== 1 && fields['version'] !== 2) ||
typeof fields['sourcePromptId'] !== 'string' ||
!fields['sourcePromptId'].trim() ||
fields['sourcePromptId'].length > 144 ||
@@ -173,6 +236,8 @@ function parsePublicChildLaunch(value: unknown): PublicChildLaunchSnapshot {
);
}
parsePublicChildModelRoute(fields['modelRoute']);
if (fields['version'] === 2)
parsePublicChildExtensionAgent(fields['extensionAgent']);
if (fields['skillsRef'] !== null) {
const ref = assertManagedSessionDurableRef(
fields['skillsRef'] as ManagedSessionJsonValue,
@@ -0,0 +1,316 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { mkdtemp, mkdir, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { readFileSync } from 'node:fs';
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
assertManagedConfigInstallSuccessor,
managedConfigBinding,
managedExtensionCompensationOperationId,
parseManagedConfigInstallContent,
parseManagedConfigInstallRecord,
type ManagedConfigInstallContent,
type ManagedConfigInstallRecord,
} from './managed-config-install.js';
import { LocalManagedSessionAuthority } from './managed-session-authority.js';
import { LocalJsonlManagedSessionJournalStore } from './local-jsonl-managed-session-journal-store.js';
import { LocalManagedSessionResourceStore } from './managed-session-resources.js';
import type { ManagedSessionDurableRef } from './managed-session-records.js';
const directories: string[] = [];
const sharedFixtures = JSON.parse(
readFileSync(
new URL(
'./contracts/managed-config-install-v1.fixtures.json',
import.meta.url,
),
'utf8',
),
) as {
valid: Array<{ name: string; content: unknown }>;
invalid: Array<{ name: string; content: unknown }>;
successors: Array<{
name: string;
previous: string | null;
next: string;
valid: boolean;
}>;
phaseOperationIds: Array<{ operationId: string; expected: string }>;
};
describe('shared Managed configuration fixtures', () => {
for (const fixture of sharedFixtures.successors)
it(`${fixture.name}`, () => {
const find = (name: string) =>
parseManagedConfigInstallContent(
sharedFixtures.valid.find((value) => value.name === name)!.content,
);
const check = () =>
assertManagedConfigInstallSuccessor(
fixture.previous === null
? undefined
: record(find(fixture.previous)),
find(fixture.next),
);
if (fixture.valid) expect(check).not.toThrow();
else expect(check).toThrow();
});
for (const fixture of sharedFixtures.phaseOperationIds)
it(`derives phase identity ${fixture.operationId}`, () => {
expect(managedExtensionCompensationOperationId(fixture.operationId)).toBe(
fixture.expected,
);
});
for (const fixture of sharedFixtures.valid)
it(`${fixture.name}`, () => {
expect(() =>
parseManagedConfigInstallContent(fixture.content),
).not.toThrow();
});
for (const fixture of sharedFixtures.invalid)
it(`${fixture.name}`, () => {
expect(() => parseManagedConfigInstallContent(fixture.content)).toThrow();
});
});
afterEach(async () => {
await Promise.all(
directories
.splice(0)
.map((directory) => rm(directory, { recursive: true, force: true })),
);
});
function ref(resourceId: string): ManagedSessionDurableRef {
return {
resourceId,
kind: 'managed-test',
schemaVersion: 1,
byteLength: 2,
digest: 'a'.repeat(64),
};
}
function content(
phase: ManagedConfigInstallContent['phase'] = 'preparing',
): ManagedConfigInstallContent {
return {
kind: 'hosted-extension-config',
formatVersion: 1,
command: {
operationId: '11111111-1111-4111-8111-111111111111',
actorKey: 'b'.repeat(64),
expectedRevision: 0,
extensions: [],
},
mode: 'apply',
phaseOperationId: '11111111-1111-4111-8111-111111111111',
configRevision: 1,
previousConfigRevision: 0,
phase,
previousBundleRef: null,
restoresBundleRef: null,
bundleRef: ref('bundle'),
rootSnapshotRef: ref('root'),
stageAckRefs: phase === 'preparing' ? [] : [ref('h-stage'), ref('r-stage')],
enableAckRefs: phase === 'active' ? [ref('h-enable'), ref('r-enable')] : [],
error:
phase === 'recovery_required' || phase === 'failed'
? { code: 'runtime_unavailable' }
: null,
};
}
function record(
value: ManagedConfigInstallContent,
): ManagedConfigInstallRecord {
return {
...value,
revision: 1,
operationId: value.command.operationId,
previousRecordRef: null,
};
}
describe('Managed configuration install records', () => {
it('requires both stage and enable evidence before active', () => {
expect(parseManagedConfigInstallContent(content('active')).phase).toBe(
'active',
);
expect(() =>
parseManagedConfigInstallContent({
...content('active'),
enableAckRefs: [ref('h-enable')],
}),
).toThrow();
expect(() =>
parseManagedConfigInstallContent({
...content('committed'),
stageAckRefs: [],
}),
).toThrow();
});
it('does not confuse the phase envelope revision with configuration revision', () => {
const value = {
...record(content('active')),
revision: 8,
previousRecordRef: {
...ref('previous-phase'),
kind: 'managed-config_install',
},
};
expect(parseManagedConfigInstallRecord(value).configRevision).toBe(1);
expect(() =>
parseManagedConfigInstallRecord({ ...value, settings: {} }),
).toThrow();
});
it('refuses a new operation until the current binding settles', () => {
const previous = record(content('committed'));
expect(() =>
assertManagedConfigInstallSuccessor(previous, {
...content(),
command: {
...content().command,
operationId: '22222222-2222-4222-8222-222222222222',
},
}),
).toThrow();
});
it('does not allow a resumed phase to substitute a different package view', () => {
expect(() =>
assertManagedConfigInstallSuccessor(record(content()), {
...content('prepared'),
bundleRef: ref('other-package-content'),
}),
).toThrow();
});
it('records compensation as a newer binding of saved content', () => {
const old = ref('old-bundle');
const previous = record({
...content('recovery_required'),
previousBundleRef: old,
});
const restored = {
...content(),
mode: 'recoverPrevious' as const,
phaseOperationId: managedExtensionCompensationOperationId(
previous.command.operationId,
),
configRevision: 2,
previousConfigRevision: 1,
bundleRef: ref('old-content-new-hook-revision'),
previousBundleRef: previous.bundleRef,
restoresBundleRef: old,
};
expect(parseManagedConfigInstallContent(restored).configRevision).toBe(2);
expect(() =>
assertManagedConfigInstallSuccessor(previous, restored),
).not.toThrow();
expect(() =>
assertManagedConfigInstallSuccessor(previous, {
...restored,
restoresBundleRef: ref('unrelated-bundle'),
}),
).toThrow();
});
});
async function open() {
const directory = await mkdtemp(path.join(tmpdir(), 'qwen-config-install-'));
directories.push(directory);
const runtimeBaseDir = path.join(directory, 'runtime');
await mkdir(runtimeBaseDir);
const sessionKey = {
tenantId: 'tenant',
workspaceId: 'workspace',
sessionId: 'session',
};
const lease = await LocalManagedSessionAuthority.acquireWriter({
runtimeBaseDir,
sessionId: sessionKey.sessionId,
transcriptPath: path.join(runtimeBaseDir, 'session.jsonl'),
});
const resources = LocalManagedSessionResourceStore.create({
runtimeBaseDir,
sessionKey,
});
const authority = await LocalManagedSessionAuthority.open({
journal: LocalJsonlManagedSessionJournalStore.fromLease(lease, sessionKey),
sessionKey,
cwd: '/workspace',
version: 'test',
resources,
create: {
definitionRef: ref('definition'),
rootSnapshotRef: ref('root'),
createdBy: 'daemon',
},
});
const commit = (body: ManagedConfigInstallContent, binding = false) =>
authority.commitDomainRecord(
{
operation: `extensionConfig.${body.mode}.${body.phase}`,
commandId: body.command.operationId,
sessionKey,
contentDigest: 'c'.repeat(64),
},
{
domain: 'config_install',
content: { ...body },
...(binding ? { configBinding: managedConfigBinding(body) } : {}),
},
{ class: 'trusted_entry' },
);
return { authority, resources, commit };
}
describe('Managed configuration binding commits', () => {
it('commits intent and binding in one transaction and replays it once', async () => {
const { authority, commit } = await open();
try {
await commit(content());
await commit(content('prepared'));
const receipt = await commit(content('committed'), true);
const events = authority.eventsInSequenceRange(
receipt.receipt.firstSequence,
receipt.receipt.lastSequence,
);
expect(events.map((event) => event.kind)).toEqual([
'domain.committed',
'config.bound',
]);
expect(events[1].payload['revision']).toBe(1);
expect(receipt.revision).toBe(3);
expect((await commit(content('committed'), true)).receipt.replayed).toBe(
true,
);
expect(authority.domainRecord('config_install')?.revision).toBe(3);
} finally {
await authority.close();
}
});
it('refuses a commit without its atomic binding before publishing it', async () => {
const { authority, resources, commit } = await open();
try {
await commit(content());
await commit(content('prepared'));
const publish = vi.spyOn(resources, 'publish');
await expect(commit(content('committed'))).rejects.toThrow(
/binding conflicts/u,
);
expect(publish).not.toHaveBeenCalled();
expect(authority.domainRecord('config_install')?.revision).toBe(2);
} finally {
await authority.close();
}
});
});
@@ -0,0 +1,333 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { createHash } from 'node:crypto';
import {
assertManagedSessionDurableRef,
ManagedSessionRecordError,
type ManagedSessionDurableRef,
type ManagedSessionJsonValue,
} from './managed-session-records.js';
import {
parseManagedExtensionApplyCommand,
type ManagedExtensionApplyCommand,
} from './managed-extension-package.js';
export const MANAGED_EXTENSION_CONFIG_KIND = 'hosted-extension-config';
export const MANAGED_CONFIG_INSTALL_PHASES = [
'preparing',
'prepared',
'committed',
'active',
'recovery_required',
'failed',
] as const;
export type ManagedConfigInstallPhase =
(typeof MANAGED_CONFIG_INSTALL_PHASES)[number];
export interface ManagedConfigInstallContent {
readonly kind: typeof MANAGED_EXTENSION_CONFIG_KIND;
readonly formatVersion: 1;
readonly command: ManagedExtensionApplyCommand;
readonly mode: 'apply' | 'recoverPrevious';
readonly phaseOperationId: string;
readonly configRevision: number;
readonly previousConfigRevision: number;
readonly phase: ManagedConfigInstallPhase;
readonly previousBundleRef: ManagedSessionDurableRef | null;
readonly restoresBundleRef: ManagedSessionDurableRef | null;
readonly bundleRef: ManagedSessionDurableRef | null;
readonly rootSnapshotRef: ManagedSessionDurableRef;
readonly stageAckRefs: readonly ManagedSessionDurableRef[];
readonly enableAckRefs: readonly ManagedSessionDurableRef[];
readonly error: { readonly code: string } | null;
}
export interface ManagedConfigInstallRecord
extends ManagedConfigInstallContent {
readonly operationId: string;
readonly revision: number;
readonly previousRecordRef: ManagedSessionDurableRef | null;
}
export interface ManagedConfigBinding {
readonly revision: number;
readonly previousRevision: number | null;
readonly bundleRef: ManagedSessionDurableRef;
readonly rootSnapshotRef: ManagedSessionDurableRef;
}
const CONTENT_KEYS = [
'kind',
'formatVersion',
'command',
'mode',
'phaseOperationId',
'configRevision',
'previousConfigRevision',
'phase',
'previousBundleRef',
'restoresBundleRef',
'bundleRef',
'rootSnapshotRef',
'stageAckRefs',
'enableAckRefs',
'error',
] as const;
export function isManagedExtensionConfig(value: unknown): boolean {
return (
value !== null &&
typeof value === 'object' &&
'kind' in value &&
value.kind === MANAGED_EXTENSION_CONFIG_KIND
);
}
export function parseManagedConfigInstallContent(
value: unknown,
): ManagedConfigInstallContent {
return parse(value, false) as ManagedConfigInstallContent;
}
export function parseManagedConfigInstallRecord(
value: unknown,
): ManagedConfigInstallRecord {
return parse(value, true) as ManagedConfigInstallRecord;
}
function parse(
value: unknown,
envelope: boolean,
): ManagedConfigInstallContent | ManagedConfigInstallRecord {
const body = object(value);
const expected = envelope
? [...CONTENT_KEYS, 'operationId', 'revision', 'previousRecordRef']
: CONTENT_KEYS;
if (
Object.keys(body).length !== expected.length ||
Object.keys(body).some(
(key) => !(expected as readonly string[]).includes(key),
) ||
body['kind'] !== MANAGED_EXTENSION_CONFIG_KIND ||
body['formatVersion'] !== 1 ||
!['apply', 'recoverPrevious'].includes(String(body['mode'])) ||
!revision(body['configRevision'], 1) ||
!revision(body['previousConfigRevision'], 0) ||
body['configRevision'] !== Number(body['previousConfigRevision']) + 1 ||
!(MANAGED_CONFIG_INSTALL_PHASES as readonly unknown[]).includes(
body['phase'],
)
)
invalid();
const command = parseManagedExtensionApplyCommand(body['command']);
const phase = body['phase'] as ManagedConfigInstallPhase;
const compensating = body['mode'] === 'recoverPrevious';
if (
body['phaseOperationId'] !==
(compensating
? managedExtensionCompensationOperationId(command.operationId)
: command.operationId)
)
invalid();
if (
Number(body['previousConfigRevision']) !==
command.expectedRevision + (compensating ? 1 : 0)
)
invalid();
const stageAckRefs = refs(body['stageAckRefs']);
const enableAckRefs = refs(body['enableAckRefs']);
if (
(['prepared', 'committed', 'active'].includes(phase) &&
stageAckRefs.length < 2) ||
(phase === 'active' && enableAckRefs.length < 2) ||
(['preparing', 'prepared', 'failed'].includes(phase) &&
enableAckRefs.length !== 0)
)
invalid();
const error = body['error'] === null ? null : object(body['error']);
if (
error &&
(Object.keys(error).length !== 1 ||
typeof error['code'] !== 'string' ||
!/^[a-z][a-z0-9_]{0,127}$/u.test(error['code']))
)
invalid();
if (
(phase === 'failed' || phase === 'recovery_required') !==
(error !== null)
)
invalid();
const parsed: ManagedConfigInstallContent = {
kind: MANAGED_EXTENSION_CONFIG_KIND,
formatVersion: 1,
command,
mode: body['mode'] as 'apply' | 'recoverPrevious',
phaseOperationId: body['phaseOperationId'] as string,
configRevision: body['configRevision'] as number,
previousConfigRevision: body['previousConfigRevision'] as number,
phase,
previousBundleRef:
body['previousBundleRef'] === null
? null
: ref(body['previousBundleRef']),
restoresBundleRef:
body['restoresBundleRef'] === null
? null
: ref(body['restoresBundleRef']),
bundleRef: body['bundleRef'] === null ? null : ref(body['bundleRef']),
rootSnapshotRef: ref(body['rootSnapshotRef']),
stageAckRefs,
enableAckRefs,
error: error ? { code: error['code'] as string } : null,
};
if ((parsed.restoresBundleRef !== null) !== compensating) invalid();
if (parsed.bundleRef === null && !['preparing', 'failed'].includes(phase))
invalid();
if (!envelope) return parsed;
if (
body['operationId'] !== command.operationId ||
!revision(body['revision'], 1)
)
invalid();
const previousRecordRef =
body['previousRecordRef'] === null ? null : ref(body['previousRecordRef']);
if (
(body['revision'] === 1) !== (previousRecordRef === null) ||
(previousRecordRef &&
(previousRecordRef.kind !== 'managed-config_install' ||
previousRecordRef.schemaVersion !== 1))
)
invalid();
return {
...parsed,
operationId: command.operationId,
revision: body['revision'] as number,
previousRecordRef,
};
}
export function managedConfigBinding(
content: ManagedConfigInstallContent,
): ManagedConfigBinding {
if (!content.bundleRef) invalid();
return {
revision: content.configRevision,
previousRevision: content.previousConfigRevision || null,
bundleRef: content.bundleRef,
rootSnapshotRef: content.rootSnapshotRef,
};
}
export function assertManagedConfigInstallSuccessor(
previous: ManagedConfigInstallRecord | undefined,
next: ManagedConfigInstallContent,
): void {
if (!previous) {
if (
next.phase !== 'preparing' ||
next.mode !== 'apply' ||
next.previousConfigRevision !== 0
)
invalid();
return;
}
if (previous.command.operationId !== next.command.operationId) {
if (
!['active', 'failed'].includes(previous.phase) ||
(previous.phase === 'failed' && previous.mode === 'recoverPrevious') ||
next.phase !== 'preparing' ||
next.mode !== 'apply' ||
next.previousConfigRevision !==
(previous.phase === 'failed'
? previous.previousConfigRevision
: previous.configRevision)
)
invalid();
return;
}
const transitions: Record<ManagedConfigInstallPhase, readonly string[]> = {
preparing: ['prepared', 'failed'],
prepared: ['committed', 'failed'],
committed: ['active', 'recovery_required'],
active: [],
recovery_required: ['active', 'recovery_required'],
failed: [],
};
const compensation =
previous.phase === 'recovery_required' &&
previous.mode === 'apply' &&
next.mode === 'recoverPrevious' &&
next.phase === 'preparing';
if (
(!compensation && !transitions[previous.phase].includes(next.phase)) ||
JSON.stringify(previous.command) !== JSON.stringify(next.command) ||
(!compensation &&
(previous.mode !== next.mode ||
previous.phaseOperationId !== next.phaseOperationId ||
previous.configRevision !== next.configRevision ||
(!(previous.phase === 'preparing' && previous.bundleRef === null) &&
JSON.stringify(previous.bundleRef) !==
JSON.stringify(next.bundleRef)) ||
JSON.stringify(previous.rootSnapshotRef) !==
JSON.stringify(next.rootSnapshotRef) ||
JSON.stringify(previous.previousBundleRef) !==
JSON.stringify(next.previousBundleRef) ||
JSON.stringify(previous.restoresBundleRef) !==
JSON.stringify(next.restoresBundleRef))) ||
(compensation &&
(next.configRevision !== previous.configRevision + 1 ||
previous.previousBundleRef === null ||
JSON.stringify(next.previousBundleRef) !==
JSON.stringify(previous.bundleRef) ||
JSON.stringify(next.restoresBundleRef) !==
JSON.stringify(previous.previousBundleRef)))
)
invalid();
}
export function managedExtensionCompensationOperationId(
operationId: string,
): string {
const bytes = createHash('sha256')
.update(`managed-extension-recoverPrevious:${operationId.toLowerCase()}`)
.digest()
.subarray(0, 16);
bytes[6] = (bytes[6]! & 0x0f) | 0x50;
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
const hex = bytes.toString('hex');
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
function object(value: unknown): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) invalid();
return value as Record<string, unknown>;
}
function ref(value: unknown): ManagedSessionDurableRef {
return assertManagedSessionDurableRef(
value as ManagedSessionJsonValue,
'extension config reference',
);
}
function refs(value: unknown): readonly ManagedSessionDurableRef[] {
if (!Array.isArray(value) || value.length > 8) invalid();
const parsed = value.map(ref);
if (new Set(parsed.map((entry) => entry.resourceId)).size !== parsed.length)
invalid();
return parsed;
}
function revision(value: unknown, minimum: number): value is number {
return Number.isSafeInteger(value) && Number(value) >= minimum;
}
function invalid(): never {
throw new ManagedSessionRecordError('Invalid Managed configuration install.');
}
@@ -0,0 +1,79 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { createHash } from 'node:crypto';
import { describe, expect, it } from 'vitest';
import type { ManagedSessionDurableRef } from './managed-session-records.js';
import {
publishManagedExtensionContributions,
readManagedExtensionContributions,
} from './managed-extension-contributions.js';
function store() {
const data = new Map<string, Buffer>();
return {
data,
async publish(
kind: string,
bytes: Buffer,
): Promise<ManagedSessionDurableRef> {
const resourceId = `resource-${data.size}`;
data.set(resourceId, bytes);
return {
resourceId,
kind,
schemaVersion: 1,
byteLength: bytes.length,
digest: createHash('sha256').update(bytes).digest('hex'),
};
},
async read(ref: ManagedSessionDurableRef) {
return data.get(ref.resourceId)!;
},
};
}
describe('Managed extension contribution resource closure', () => {
it('preserves UTF-8 bodies across 64 KiB parts', async () => {
const resources = store();
const contributions = {
skills: [],
commands: [],
agents: [],
context: [{ path: 'QWEN.md', body: '文'.repeat(60_000) }],
mcpServers: {},
hooks: {},
settings: [],
};
const ref = await publishManagedExtensionContributions(
resources,
contributions,
);
expect(
[...resources.data.values()].every((bytes) => bytes.length <= 64 * 1024),
).toBe(true);
expect(await readManagedExtensionContributions(resources, ref)).toEqual(
contributions,
);
});
it('rejects a part whose stored bytes changed', async () => {
const resources = store();
const ref = await publishManagedExtensionContributions(resources, {
skills: [],
commands: [],
agents: [],
context: [],
mcpServers: {},
hooks: {},
settings: [],
});
resources.data.set('resource-0', Buffer.from('changed'));
await expect(
readManagedExtensionContributions(resources, ref),
).rejects.toThrow('part does not match');
});
});
@@ -0,0 +1,120 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { createHash } from 'node:crypto';
import type { ManagedPackageContributions } from '../extension/managed-package.js';
import type { ManagedSessionResourceStore } from './managed-session-storage.js';
import {
assertManagedSessionDurableRef,
type ManagedSessionDurableRef,
type ManagedSessionJsonValue,
} from './managed-session-records.js';
const PART_BYTES = 64 * 1024;
const MAX_BYTES = 2 * 1024 * 1024;
export async function publishManagedExtensionContributions(
resources: Pick<ManagedSessionResourceStore, 'publish'>,
contributions: ManagedPackageContributions,
): Promise<ManagedSessionDurableRef> {
const bytes = Buffer.from(JSON.stringify(contributions));
if (bytes.length > MAX_BYTES)
throw new Error('Extension contributions exceed 2 MiB.');
const parts: ManagedSessionDurableRef[] = [];
for (let offset = 0; offset < bytes.length; offset += PART_BYTES)
parts.push(
await resources.publish(
'managed-extension-contributions-part',
bytes.subarray(offset, offset + PART_BYTES),
),
);
return resources.publish(
'managed-extension-contributions',
Buffer.from(
JSON.stringify({
kind: 'managed-extension-contributions',
formatVersion: 1,
byteLength: bytes.length,
digest: digest(bytes),
parts,
}),
),
);
}
export async function readManagedExtensionContributions(
resources: Pick<ManagedSessionResourceStore, 'read'>,
ref: ManagedSessionDurableRef,
): Promise<ManagedPackageContributions> {
if (
ref.kind !== 'managed-extension-contributions' ||
ref.schemaVersion !== 1 ||
ref.byteLength > PART_BYTES
)
throw new Error('Invalid extension contribution reference.');
const manifestBytes = await resources.read(ref);
if (
manifestBytes.length !== ref.byteLength ||
digest(manifestBytes) !== ref.digest
)
throw new Error(
'Extension contribution manifest does not match its reference.',
);
const value: unknown = JSON.parse(
new TextDecoder('utf-8', { fatal: true }).decode(manifestBytes),
);
if (!value || typeof value !== 'object' || Array.isArray(value))
throw new Error('Invalid extension contribution manifest.');
const manifest = value as Record<string, unknown>;
if (
Object.keys(manifest).sort().join(',') !==
'byteLength,digest,formatVersion,kind,parts' ||
manifest['kind'] !== 'managed-extension-contributions' ||
manifest['formatVersion'] !== 1 ||
!Number.isSafeInteger(manifest['byteLength']) ||
Number(manifest['byteLength']) < 1 ||
Number(manifest['byteLength']) > MAX_BYTES ||
typeof manifest['digest'] !== 'string' ||
!/^[a-f0-9]{64}$/u.test(manifest['digest']) ||
!Array.isArray(manifest['parts']) ||
manifest['parts'].length < 1 ||
manifest['parts'].length > 32
)
throw new Error('Invalid extension contribution manifest.');
const parts: Buffer[] = [];
for (const value of manifest['parts']) {
const part = assertManagedSessionDurableRef(
value as ManagedSessionJsonValue,
'extension contribution part',
);
if (
part.kind !== 'managed-extension-contributions-part' ||
part.schemaVersion !== 1 ||
part.byteLength < 1 ||
part.byteLength > PART_BYTES
)
throw new Error('Invalid extension contribution part.');
const bytes = await resources.read(part);
if (bytes.length !== part.byteLength || digest(bytes) !== part.digest)
throw new Error(
'Extension contribution part does not match its reference.',
);
parts.push(bytes);
}
const bytes = Buffer.concat(parts);
if (
bytes.length !== manifest['byteLength'] ||
digest(bytes) !== manifest['digest']
)
throw new Error('Extension contributions do not match their manifest.');
return JSON.parse(
new TextDecoder('utf-8', { fatal: true }).decode(bytes),
) as ManagedPackageContributions;
}
function digest(bytes: Buffer): string {
return createHash('sha256').update(bytes).digest('hex');
}
@@ -0,0 +1,72 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect, it } from 'vitest';
import {
parseManagedExtensionApplyCommand,
parseManagedExtensionPin,
parseManagedExtensionPins,
} from './managed-extension-package.js';
const pin = {
extensionId: 'reviewer',
packageRevision: 2,
packageDigest: 'a'.repeat(64),
};
describe('Managed extension pins', () => {
it('copies and freezes the complete package and settings identity', () => {
const input = { ...pin, settingsRevision: 3 };
const parsed = parseManagedExtensionPin(input);
input.packageRevision = 4;
expect(parsed).toEqual({ ...pin, settingsRevision: 3 });
expect(Object.isFrozen(parsed)).toBe(true);
});
it.each([
{ ...pin, packageRevision: 0 },
{ ...pin, packageRevision: Number.MAX_SAFE_INTEGER + 1 },
{ ...pin, packageDigest: 'A'.repeat(64) },
{ ...pin, extensionId: '../other-workspace' },
{ ...pin, settingsRevision: 0 },
{ ...pin, settings: { secret: 'must-not-be-in-a-pin' } },
])('rejects an invalid or non-public pin %j', (input) => {
expect(() => parseManagedExtensionPin(input)).toThrow();
});
it('rejects duplicate identities instead of selecting the last version', () => {
expect(() =>
parseManagedExtensionPins([pin, { ...pin, packageRevision: 3 }]),
).toThrow(/duplicate/u);
expect(parseManagedExtensionPins([])).toEqual([]);
});
});
describe('Managed extension apply commands', () => {
const command = {
operationId: '11111111-1111-4111-8111-111111111111',
actorKey: 'b'.repeat(64),
expectedRevision: 0,
extensions: [pin],
};
it('retains a complete replacement selection, including an empty one', () => {
expect(parseManagedExtensionApplyCommand(command)).toEqual(command);
expect(
parseManagedExtensionApplyCommand({ ...command, extensions: [] })
.extensions,
).toEqual([]);
});
it.each([
{ ...command, expectedRevision: -1 },
{ ...command, operationId: 'retry-with-new-operation' },
{ ...command, actorKey: '' },
{ ...command, force: true },
])('refuses invalid operation identity or revision %j', (input) => {
expect(() => parseManagedExtensionApplyCommand(input)).toThrow();
});
});
@@ -0,0 +1,424 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import {
assertManagedSessionDurableRef,
ManagedSessionRecordError,
type ManagedSessionDurableRef,
type ManagedSessionJsonValue,
} from './managed-session-records.js';
import type { ManagedHookCatalogPin } from './managed-hook-protocol.js';
import type { ManagedPackageContributions } from '../extension/managed-package.js';
import type { OperationGrant } from './managed-extension-record.js';
export interface ManagedExtensionCommandControl {
readonly kind: 'extension-command';
readonly operationId: string;
readonly action: 'execute' | 'status' | 'cancel';
readonly grant: OperationGrant;
readonly toolName: 'read_file' | 'shell';
readonly input: Readonly<Record<string, unknown>>;
readonly revision: number;
readonly extensionId: string;
}
export interface ManagedExtensionPin {
readonly extensionId: string;
readonly packageRevision: number;
readonly packageDigest: string;
readonly settingsRevision?: number;
}
export interface ManagedExtensionApplyCommand {
readonly operationId: string;
readonly actorKey: string;
readonly expectedRevision: number;
readonly extensions: readonly ManagedExtensionPin[];
readonly mcpChange?: ManagedExtensionMcpChange;
}
export interface ManagedExtensionMcpPin {
readonly serverId: string;
readonly serverRevision: number;
readonly definitionDigest: string;
}
export interface ManagedExtensionMcpSelection extends ManagedExtensionMcpPin {
readonly state: 'enabled' | 'disabled' | 'removed';
}
export interface ManagedExtensionMcpChange {
readonly action:
| 'add'
| 'edit'
| 'remove'
| 'enable'
| 'disable'
| 'reload'
| 'restart';
readonly serverId: string;
readonly expectedRevision: number;
readonly servers: readonly ManagedExtensionMcpSelection[];
}
export interface ManagedExtensionPackageView {
readonly pin: ManagedExtensionPin;
readonly assetRoot: string;
readonly contributionsRef: ManagedSessionDurableRef;
}
export interface ManagedExtensionEffectiveBundle {
readonly kind: 'managed-extension-config-bundle';
readonly formatVersion: 1;
readonly configRevision: number;
readonly executionMode: 'linux-cgroup' | 'developer-process-group';
readonly mcpRevision: number;
readonly independentMcpSelections: readonly ManagedExtensionMcpSelection[];
readonly mcpChange?: ManagedExtensionMcpChange;
readonly baseMcpServers: readonly ManagedExtensionMcpPin[];
readonly baseHookCatalog: ManagedHookCatalogPin | null;
readonly packages: readonly ManagedExtensionPackageView[];
readonly mcpServers: readonly ManagedExtensionMcpPin[];
readonly hookCatalog: ManagedHookCatalogPin | null;
}
export interface ManagedExtensionPreparedPackage {
readonly pin: ManagedExtensionPin;
readonly assetRoot: string;
readonly contributions: ManagedPackageContributions;
}
export interface ManagedExtensionPrepareResult {
readonly operationId: string;
readonly state: 'prepared';
readonly expectedRevision: number;
readonly revision: number;
readonly executionMode: 'linux-cgroup' | 'developer-process-group';
readonly packages: readonly ManagedExtensionPreparedPackage[];
readonly mcpServers: readonly ManagedExtensionMcpPin[];
readonly hookCatalog: ManagedHookCatalogPin | null;
}
export type ManagedExtensionRuntimeControl =
| {
readonly kind: 'extension-prepare';
readonly operationId: string;
readonly command: ManagedExtensionApplyCommand;
readonly expectedRevision: number;
readonly extensions: readonly ManagedExtensionPin[];
readonly baseMcpServers: readonly ManagedExtensionMcpPin[];
readonly baseHookCatalog: ManagedHookCatalogPin | null;
}
| {
readonly kind: 'extension-activate';
readonly operationId: string;
readonly expectedRevision: number;
readonly revision: number;
}
| { readonly kind: 'extension-status'; readonly operationId: string }
| {
readonly kind: 'extension-reconcile';
readonly operationId: string;
readonly expectedRevision: number;
readonly revision: number;
}
| {
readonly kind: 'extension-asset-read';
readonly operationId: string;
readonly revision: number;
readonly extensionId: string;
readonly path: string;
}
| {
readonly kind: 'extension-asset-glob';
readonly operationId: string;
readonly revision: number;
readonly extensionId: string;
readonly pattern: string;
};
export const MANAGED_EXTENSION_MAX_PACKAGES = 32;
export function parseManagedExtensionEffectiveBundle(
value: unknown,
): ManagedExtensionEffectiveBundle {
const bundle = object(value, 'effective extension configuration');
keys(
bundle,
[
'kind',
'formatVersion',
'configRevision',
'executionMode',
'mcpRevision',
'independentMcpSelections',
'baseMcpServers',
'baseHookCatalog',
'packages',
'mcpServers',
'hookCatalog',
],
['mcpChange'],
'effective extension configuration',
);
if (
bundle['kind'] !== 'managed-extension-config-bundle' ||
bundle['formatVersion'] !== 1 ||
!['linux-cgroup', 'developer-process-group'].includes(
String(bundle['executionMode']),
) ||
!revision(bundle['configRevision'], 0) ||
!revision(bundle['mcpRevision'], 0) ||
!Array.isArray(bundle['packages']) ||
bundle['packages'].length > MANAGED_EXTENSION_MAX_PACKAGES
)
invalid('effective extension configuration');
const packages = bundle['packages'].map(
(value): ManagedExtensionPackageView => {
const entry = object(value, 'package view');
keys(entry, ['pin', 'assetRoot', 'contributionsRef'], [], 'package view');
const pin = parseManagedExtensionPin(entry['pin']);
if (entry['assetRoot'] !== `extension://${pin.extensionId}/`)
invalid('package asset root');
const contributionsRef = assertManagedSessionDurableRef(
entry['contributionsRef'] as ManagedSessionJsonValue,
'package contributions',
);
if (
contributionsRef.kind !== 'managed-extension-contributions' ||
contributionsRef.schemaVersion !== 1
)
invalid('package contributions');
return { pin, assetRoot: entry['assetRoot'] as string, contributionsRef };
},
);
if (
new Set(packages.map((entry) => entry.pin.extensionId)).size !==
packages.length
)
invalid('duplicate package view');
return {
kind: 'managed-extension-config-bundle',
formatVersion: 1,
configRevision: bundle['configRevision'] as number,
executionMode: bundle[
'executionMode'
] as ManagedExtensionEffectiveBundle['executionMode'],
mcpRevision: bundle['mcpRevision'] as number,
independentMcpSelections: mcpSelections(bundle['independentMcpSelections']),
...(bundle['mcpChange'] === undefined
? {}
: { mcpChange: mcpChange(bundle['mcpChange']) }),
baseMcpServers: mcpPins(bundle['baseMcpServers']),
baseHookCatalog: hookPin(bundle['baseHookCatalog']),
packages,
mcpServers: mcpPins(bundle['mcpServers']),
hookCatalog: hookPin(bundle['hookCatalog']),
};
}
function mcpPins(value: unknown): readonly ManagedExtensionMcpPin[] {
if (!Array.isArray(value) || value.length > 32) invalid('MCP selection');
const pins = value.map((value): ManagedExtensionMcpPin => {
const pin = object(value, 'MCP pin');
keys(
pin,
['serverId', 'serverRevision', 'definitionDigest'],
[],
'MCP pin',
);
const identity = parseManagedExtensionPin({
extensionId: pin['serverId'],
packageRevision: pin['serverRevision'],
packageDigest: pin['definitionDigest'],
});
return {
serverId: identity.extensionId,
serverRevision: identity.packageRevision,
definitionDigest: identity.packageDigest,
};
});
if (new Set(pins.map((pin) => pin.serverId)).size !== pins.length)
invalid('duplicate MCP pin');
return pins;
}
function hookPin(value: unknown): ManagedHookCatalogPin | null {
if (value === null) return null;
const pin = object(value, 'Hook catalog pin');
keys(
pin,
['catalogId', 'catalogRevision', 'definitionDigest'],
[],
'Hook catalog pin',
);
const identity = parseManagedExtensionPin({
extensionId: pin['catalogId'],
packageRevision: pin['catalogRevision'],
packageDigest: pin['definitionDigest'],
});
return {
catalogId: identity.extensionId,
catalogRevision: identity.packageRevision,
definitionDigest: identity.packageDigest,
};
}
export function parseManagedExtensionPin(value: unknown): ManagedExtensionPin {
const pin = object(value, 'extension pin');
keys(
pin,
['extensionId', 'packageRevision', 'packageDigest'],
['settingsRevision'],
'extension pin',
);
if (
typeof pin['extensionId'] !== 'string' ||
!/^[A-Za-z0-9._:-]{1,128}$/u.test(pin['extensionId']) ||
!revision(pin['packageRevision'], 1) ||
typeof pin['packageDigest'] !== 'string' ||
!/^[a-f0-9]{64}$/u.test(pin['packageDigest']) ||
(pin['settingsRevision'] !== undefined &&
!revision(pin['settingsRevision'], 1))
)
invalid('extension pin');
return Object.freeze({
extensionId: pin['extensionId'],
packageRevision: pin['packageRevision'] as number,
packageDigest: pin['packageDigest'],
...(pin['settingsRevision'] === undefined
? {}
: { settingsRevision: pin['settingsRevision'] as number }),
});
}
export function parseManagedExtensionPins(
value: unknown,
): readonly ManagedExtensionPin[] {
if (!Array.isArray(value) || value.length > MANAGED_EXTENSION_MAX_PACKAGES)
invalid('extension selection');
const pins = value.map(parseManagedExtensionPin);
if (new Set(pins.map((pin) => pin.extensionId)).size !== pins.length)
invalid('duplicate extension selection');
return Object.freeze(pins);
}
export function parseManagedExtensionApplyCommand(
value: unknown,
): ManagedExtensionApplyCommand {
const command = object(value, 'extension apply command');
keys(
command,
['operationId', 'actorKey', 'expectedRevision', 'extensions'],
['mcpChange'],
'extension apply command',
);
if (
typeof command['operationId'] !== 'string' ||
!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(
command['operationId'],
) ||
typeof command['actorKey'] !== 'string' ||
!/^[a-f0-9]{64}$/u.test(command['actorKey']) ||
!revision(command['expectedRevision'], 0) ||
command['expectedRevision'] === Number.MAX_SAFE_INTEGER
)
invalid('extension apply command');
return Object.freeze({
operationId: command['operationId'],
actorKey: command['actorKey'],
expectedRevision: command['expectedRevision'] as number,
extensions: parseManagedExtensionPins(command['extensions']),
...(command['mcpChange'] === undefined
? {}
: { mcpChange: mcpChange(command['mcpChange']) }),
});
}
function mcpSelections(
value: unknown,
): readonly ManagedExtensionMcpSelection[] {
if (!Array.isArray(value) || value.length > 32) invalid('MCP selection');
const selected = value.map((value): ManagedExtensionMcpSelection => {
const candidate = object(value, 'MCP selection');
const { state, ...pin } = candidate;
if (!['enabled', 'disabled', 'removed'].includes(String(state)))
invalid('MCP state');
return {
...mcpPins([pin])[0],
state: state as ManagedExtensionMcpSelection['state'],
};
});
if (
new Set(selected.map((entry) => entry.serverId)).size !== selected.length ||
selected.filter((entry) => entry.state === 'enabled').length > 16
)
invalid('MCP selection');
return selected;
}
function mcpChange(value: unknown): ManagedExtensionMcpChange {
const change = object(value, 'MCP change');
keys(
change,
['action', 'serverId', 'expectedRevision', 'servers'],
[],
'MCP change',
);
if (
![
'add',
'edit',
'remove',
'enable',
'disable',
'reload',
'restart',
].includes(String(change['action'])) ||
typeof change['serverId'] !== 'string' ||
!/^[A-Za-z0-9._:-]{1,128}$/u.test(change['serverId']) ||
!revision(change['expectedRevision'], 0)
)
invalid('MCP change');
const servers = mcpSelections(change['servers']);
if (!servers.some((entry) => entry.serverId === change['serverId']))
invalid('MCP change');
return {
action: change['action'] as ManagedExtensionMcpChange['action'],
serverId: change['serverId'],
expectedRevision: change['expectedRevision'] as number,
servers,
};
}
function object(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value))
invalid(label);
return value as Record<string, unknown>;
}
function keys(
value: Record<string, unknown>,
required: readonly string[],
optional: readonly string[],
label: string,
): void {
if (
required.some((key) => !Object.hasOwn(value, key)) ||
Object.keys(value).some(
(key) => !required.includes(key) && !optional.includes(key),
)
)
invalid(label);
}
function revision(value: unknown, minimum: number): value is number {
return Number.isSafeInteger(value) && Number(value) >= minimum;
}
function invalid(label: string): never {
throw new ManagedSessionRecordError(`Invalid Managed ${label}.`);
}
@@ -91,6 +91,11 @@ import {
isHistoryMaintenanceStart,
isHistoryMaintenanceSuccessor,
} from './managed-history-maintenance-record.js';
import {
parseManagedToolStage,
isManagedToolStageStart,
isManagedToolStageSuccessor,
} from './managed-tool-stage-record.js';
// H0c of #12827: how the Session authority keys, chains and projects the
// Stage H records of managed-extension-record/1. The shared fixtures in
@@ -178,6 +183,15 @@ function childRunTaskKind(record: AnyChildRun): ManagedTaskKind {
export const MANAGED_EXTENSION_RECORD_BODIES: Readonly<
Partial<Record<ManagedSessionDomain, ManagedExtensionRecordBody>>
> = Object.freeze({
tool_stage: Object.freeze({
taskKindOf: () => null,
parse: (value: unknown) => {
const record = parseManagedToolStage(value);
return { record, recordId: record.toolStageId, run: record.run };
},
isStart: isManagedToolStageStart,
isSuccessor: isManagedToolStageSuccessor,
}),
history_maintenance: Object.freeze({
taskKindOf: () => null,
parse: (value: unknown) => {
@@ -5,6 +5,15 @@
*/
import { randomUUID } from 'node:crypto';
import { parseManagedToolStage } from './managed-tool-stage-record.js';
import {
assertManagedConfigInstallSuccessor,
isManagedExtensionConfig,
managedConfigBinding,
parseManagedConfigInstallContent,
parseManagedConfigInstallRecord,
type ManagedConfigBinding,
} from './managed-config-install.js';
import { SessionWriterLease } from '../services/session-writer-lease.js';
import { managedToolDigest } from '../tools/managed-tool-protocol.js';
import { LocalJsonlManagedSessionJournalStore } from './local-jsonl-managed-session-journal-store.js';
@@ -1578,6 +1587,7 @@ export class LocalManagedSessionAuthority {
request: {
domain: ManagedSessionDomain;
content: Readonly<Record<string, unknown>>;
configBinding?: ManagedConfigBinding;
},
actor: ManagedSessionActor,
): Promise<ManagedSessionDomainReceipt> {
@@ -1614,18 +1624,54 @@ export class LocalManagedSessionAuthority {
}
const previous = this.domainRecords.get(request.domain);
if (request.domain === 'config_install') {
const commandBody = request.content['command'];
if (
request.content['kind'] !== 'hosted-mcp-settings' ||
!commandBody ||
typeof commandBody !== 'object' ||
!('expectedRevision' in commandBody) ||
commandBody.expectedRevision !== (previous?.revision ?? 0)
) {
throw new ManagedSessionConflictError(
'MCP settings revision conflicts.',
if (isManagedExtensionConfig(request.content)) {
const content = parseManagedConfigInstallContent(request.content);
const saved = previous
? JSON.parse(
(await store.read(previous.recordRef)).toString('utf8'),
)
: undefined;
assertManagedConfigInstallSuccessor(
isManagedExtensionConfig(saved)
? parseManagedConfigInstallRecord(saved)
: undefined,
content,
);
if (
content.command.operationId !== command.commandId ||
(content.phase === 'committed'
? JSON.stringify(request.configBinding) !==
JSON.stringify(managedConfigBinding(content))
: request.configBinding !== undefined)
)
throw new ManagedSessionConflictError(
'Extension configuration binding conflicts.',
);
} else {
const saved = previous
? JSON.parse(
(await store.read(previous.recordRef)).toString('utf8'),
)
: undefined;
const commandBody = request.content['command'];
if (
isManagedExtensionConfig(saved) ||
request.content['kind'] !== 'hosted-mcp-settings' ||
!commandBody ||
typeof commandBody !== 'object' ||
!('expectedRevision' in commandBody) ||
commandBody.expectedRevision !== (previous?.revision ?? 0) ||
request.configBinding !== undefined
) {
throw new ManagedSessionConflictError(
'MCP settings revision conflicts.',
);
}
}
} else if (request.configBinding !== undefined) {
throw new ManagedSessionConflictError(
'Only an extension configuration install may bind configuration.',
);
}
let content = request.content;
const incomingTitleRevision = content['managedRenameRevision'];
@@ -1700,8 +1746,21 @@ export class LocalManagedSessionAuthority {
recordRef,
},
},
...(request.configBinding
? [
{
v: MANAGED_SESSION_FORMAT_VERSION,
sequence: this.committed + 2,
eventId: `config-bound:${request.configBinding.revision}`,
sessionKey: command.sessionKey,
kind: 'config.bound' as const,
occurredAt: this.now(),
payload: { ...request.configBinding },
},
]
: []),
],
[actor],
request.configBinding ? [actor, actor] : [actor],
);
return { receipt, recordRef, revision };
});
@@ -3149,7 +3208,10 @@ export class LocalManagedSessionAuthority {
replay = false,
): Promise<void> {
let refs: ReadonlyArray<ManagedSessionDurableRef | null> = [];
if (domain === 'mcp_configuration') {
if (domain === 'tool_stage') {
const stage = parseManagedToolStage(record);
refs = [stage.inputRef, stage.permissionRef, stage.resultRef];
} else if (domain === 'mcp_configuration') {
refs = [parseMcpConfiguration(record).catalogRef];
} else if (domain === 'mcp_operation') {
const operation = parseMcpOperation(record);
@@ -128,6 +128,7 @@ export type ManagedSessionDomain = (typeof MANAGED_SESSION_DOMAINS)[number];
*/
export const MANAGED_SESSION_ENABLED_DOMAINS: readonly ManagedSessionDomain[] =
[
'tool_stage',
'config_install',
'goal_state',
'todo_state',
@@ -0,0 +1,40 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
import {
parseManagedToolStage,
isManagedToolStageSuccessor,
} from './managed-tool-stage-record.js';
const fixtures = JSON.parse(
readFileSync(
new URL('./contracts/managed-tool-stage-v1.fixtures.json', import.meta.url),
'utf8',
),
) as {
valid: Array<{ name: string; record: unknown }>;
invalid: Array<{ name: string; record: unknown }>;
successors: Array<{
name: string;
previous: unknown;
next: unknown;
valid: boolean;
}>;
};
describe('shared Managed tool stage fixtures', () => {
it.each(fixtures.valid)('accepts $name', (fixture) => {
expect(() => parseManagedToolStage(fixture.record)).not.toThrow();
});
it.each(fixtures.invalid)('rejects $name', (fixture) => {
expect(() => parseManagedToolStage(fixture.record)).toThrow();
});
it.each(fixtures.successors)('checks successor $name', (fixture) => {
expect(isManagedToolStageSuccessor(fixture.previous, fixture.next)).toBe(
fixture.valid,
);
});
});
@@ -0,0 +1,125 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import {
isExtensionRunStart,
isExtensionRunSuccessor,
isTerminalRunState,
parseExtensionRun,
type ExtensionRun,
} from './managed-extension-record.js';
import { isDeepStrictEqual } from 'node:util';
import {
assertManagedSessionDurableRef,
assertManagedSessionStableId,
ManagedSessionRecordError,
type ManagedSessionDurableRef,
type ManagedSessionJsonValue,
} from './managed-session-records.js';
export interface ManagedToolStage {
readonly kind: 'tool_stage';
readonly toolStageId: string;
readonly expansionId: string;
readonly stepIndex: number;
readonly toolName: 'read_file' | 'shell';
readonly inputRef: ManagedSessionDurableRef;
readonly permissionRef: ManagedSessionDurableRef | null;
readonly resultRef: ManagedSessionDurableRef | null;
readonly run: ExtensionRun;
}
export function parseManagedToolStage(value: unknown): ManagedToolStage {
if (!value || typeof value !== 'object' || Array.isArray(value)) invalid();
const fields = value as Record<string, ManagedSessionJsonValue>;
if (
Object.keys(fields).sort().join(',') !==
'expansionId,inputRef,kind,permissionRef,resultRef,run,stepIndex,toolName,toolStageId' ||
fields['kind'] !== 'tool_stage' ||
!Number.isSafeInteger(fields['stepIndex']) ||
Number(fields['stepIndex']) < 0 ||
!['read_file', 'shell'].includes(String(fields['toolName']))
)
invalid();
const toolStageId = assertManagedSessionStableId(
fields['toolStageId'],
'toolStageId',
);
const run = parseExtensionRun(fields['run']);
if (
!run.definition ||
run.definition.definitionId !== fields['toolName'] ||
run.executionCallId !== toolStageId
)
invalid();
return {
kind: 'tool_stage',
toolStageId,
expansionId: assertManagedSessionStableId(
fields['expansionId'],
'expansionId',
),
stepIndex: fields['stepIndex'] as number,
toolName: fields['toolName'] as ManagedToolStage['toolName'],
inputRef: assertManagedSessionDurableRef(fields['inputRef'], 'inputRef'),
permissionRef:
fields['permissionRef'] === null
? null
: assertManagedSessionDurableRef(
fields['permissionRef'],
'permissionRef',
),
resultRef:
fields['resultRef'] === null
? null
: assertManagedSessionDurableRef(fields['resultRef'], 'resultRef'),
run,
};
}
export function isManagedToolStageStart(value: unknown): boolean {
try {
const record = parseManagedToolStage(value);
return record.resultRef === null && isExtensionRunStart(record.run);
} catch (cause) {
if (cause instanceof ManagedSessionRecordError) return false;
throw cause;
}
}
export function isManagedToolStageSuccessor(
previous: unknown,
next: unknown,
): boolean {
try {
const before = parseManagedToolStage(previous);
const after = parseManagedToolStage(next);
if (isTerminalRunState(before.run.state))
return isDeepStrictEqual(before, after);
for (const key of [
'kind',
'toolStageId',
'expansionId',
'stepIndex',
'toolName',
'inputRef',
] as const)
if (!isDeepStrictEqual(before[key], after[key])) return false;
for (const key of ['permissionRef', 'resultRef'] as const)
if (before[key] && !isDeepStrictEqual(before[key], after[key]))
return false;
return isExtensionRunSuccessor(before.run, after.run);
} catch (cause) {
if (cause instanceof ManagedSessionRecordError) return false;
throw cause;
}
}
function invalid(): never {
throw new ManagedSessionRecordError(
'Invalid Managed command preprocessing stage.',
);
}
@@ -0,0 +1,71 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { z } from 'zod';
import { parse as parseYaml } from './yaml-parser.js';
import { normalizeContent } from './textUtils.js';
/**
* Defines the Zod schema for a Markdown command definition file.
* The frontmatter contains optional metadata, and the body is the prompt.
*/
export const MarkdownCommandDefSchema = z.object({
frontmatter: z
.object({
description: z.string().optional(),
'argument-hint': z.string().optional(),
when_to_use: z.string().optional(),
'disable-model-invocation': z.boolean().optional(),
})
.passthrough()
.optional(),
prompt: z.string({
required_error: 'The prompt content is required.',
invalid_type_error: 'The prompt content must be a string.',
}),
});
export type MarkdownCommandDef = z.infer<typeof MarkdownCommandDefSchema>;
/**
* Parses a Markdown command file with optional YAML frontmatter.
* @param content The file content
* @returns Parsed command definition with frontmatter and prompt
*/
export function parseMarkdownCommand(content: string): MarkdownCommandDef {
const normalizedContent = normalizeContent(content);
// Match YAML frontmatter pattern: ---\n...\n---\n
// Allow empty frontmatter: ---\n---\n
const frontmatterRegex = /^---\n(?:([\s\S]*?)\n)?---(?:\n|$)([\s\S]*)$/;
const match = normalizedContent.match(frontmatterRegex);
if (!match) {
// No frontmatter, entire content is the prompt
return {
prompt: normalizedContent.trim(),
};
}
const [, frontmatterYaml = '', body] = match;
// Parse YAML frontmatter if not empty
let frontmatter: Record<string, unknown> | undefined;
if (frontmatterYaml.trim()) {
try {
frontmatter = parseYaml(frontmatterYaml) as Record<string, unknown>;
} catch (error) {
throw new Error(
`Failed to parse YAML frontmatter: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
return {
frontmatter,
prompt: body.trim(),
};
}
+49
View File
@@ -1584,6 +1584,55 @@ nested classes, and context restarts inside one class. See the
[isolation design](../../docs/design/2026-10-20-hosted-it-database-isolation.md)
([简体中文](../../docs/design/2026-10-20-hosted-it-database-isolation.zh-CN.md)).
## Managed extension packages
Complete package distribution and the seven-component Managed profile are opt-in.
Set `QWEN_MANAGED_AGENT_EXTENSION_PACKAGES_ENABLED=true`,
`QWEN_MANAGED_AGENT_EXTENSION_ARTIFACT_DIRECTORY` to a durable private directory,
and `QWEN_MANAGED_AGENT_EXTENSION_ALLOWED_ACTORS` to the permitted operator actor
keys. The configured Workspace OPERATOR grant is also required. See the
[configuration example](distribution/config/application.example.yml) for the
separate Runtime execution profile: Linux LOCAL and SSH require delegated cgroup
v2; native macOS requires the explicit `developer-process-group` mode. Existing
Runtime bindings do not acquire the new execution contract automatically.
Replicas must share the authoritative artifact directory. A Runtime cache is
reconstructible and cannot replace that directory. Published versions and encrypted
settings revisions are retained; this increment does not implement garbage
collection.
In WebShell, select the Managed Workspace and Session, open Extensions, prepare an
original Git/npm/directory/archive source for its target platform, then publish it.
An update explicitly selects the catalog extension ID. Select package versions and
an exact saved settings revision before applying to an idle Session. Saving settings
creates a complete immutable revision; values never appear in read responses.
Upgrading a package preserves its selected settings revision. An unfinished apply
has separate Continue original operation and Restore previous version controls;
unknown outcomes continue to block inference until the original owner reconciles.
Public clients use `/v1/agents/workspaces/{workspaceId}/extensions` for bounded
binary publication and catalog queries and `/v1/agents/sessions/{sessionId}/extensions`
for configuration queries and application. The OpenAPI contract documents the
settings and operation subresources. The Java SDK exposes `ManagedExtensionsClient`;
the TypeScript daemon SDK can prepare and stream an original package. Creation's
optional `extensionPins` freezes the initial selection before the first model call.
Omitting it retains the existing Session path.
Package assets use an explicit read-only extension source. Model tool schemas list
the selected public pins; `read_file` and `glob` accept package-relative paths or
`extension://<extensionId>/<path>`. Shell selects `extension_context` and receives
`QWEN_EXTENSION_ROOT`, while its working directory remains the Session directory.
Ordinary Workspace tools retain their existing containment rules. Executable
preflight checks target interpreters and directly resolvable commands without
running Hooks or connecting MCP; complex authored Shell expressions are checked
when their approved invocation executes. Argument placeholders inside nested Shell
substitution, backticks or heredocs are rejected explicitly.
See the linked [English design](../../docs/design/2026-10-24-managed-extension-packages.md)
and [Chinese design](../../docs/design/2026-10-24-managed-extension-packages.zh-CN.md).
Local focused tests, macOS developer receipts and the existing Linux static-extension
gate below establish different boundaries. Complete package Linux/SSH cleanup,
fault injection and real-provider qualification remain separate acceptance gates.
## Linux extension process acceptance
`HostedExtensionsIT` is collected by the existing `hosted-harness-mysql` profile. It uses the public AgentDefinition and Session APIs, real MySQL, the packaged Harness and durable native worker, a stdio MCP process, and command Hooks in a delegated cgroup v2 subtree. It verifies static Skill bytes, extension revision pinning across Harness replacement, Hook allow/deny, cancellation of a Hook and its descendant, safe continuation, and physical close. A separate MCP fault case writes and fsyncs an effect before disconnecting, then verifies that a replacement Harness refuses recovery without replaying the effect or reporting successful close.
@@ -30,3 +30,23 @@
# credential-key: '${QWEN_MANAGED_AGENT_RUNTIME_CREDENTIAL_KEY:<base64-encoded-32-byte-v2-key>}'
# previous-credential-keys:
# '[reference-v1]': <base64-encoded-32-byte-v1-key>
# Extension execution is separately disabled by default. Enable package publishing
# (qwen.managed-agent.extensions.enabled) as well before enabling execution.
# Linux LOCAL/SSH requires a writable delegated cgroup-v2 subtree on the Runtime
# host; cache-directory is an absolute private path outside every Workspace.
# qwen:
# managed-agent:
# extensions:
# enabled: true
# artifact-directory: /var/lib/qwen-managed-agent/extension-packages
# allowed-actors: [your-operator-actor]
# managed-extensions:
# enabled: true
# mode: linux-cgroup
# cache-directory: /var/lib/qwen-managed-agent/extension-cache
# cgroup-root: /sys/fs/cgroup/qwen-managed-agent/extensions
# Native macOS LOCAL is explicitly weaker developer mode:
# mode: developer-process-group
# cache-directory: /Users/developer/.qwen-managed-extension-cache
# cgroup-root: ''
@@ -133,3 +133,20 @@ them before restarting with
- `/etc/qwen-managed-agent` — credentials and `application.yml`.
- `/var/lib/qwen-managed-agent` — Runtime state.
- `/srv/qwen-workspace` — Workspace; adjust `ReadWritePaths` when it moves.
## Extension execution
Execution is disabled by default. Enable package publishing separately, then set
`QWEN_MANAGED_AGENT_EXTENSION_EXECUTION_ENABLED=true`,
`QWEN_MANAGED_AGENT_EXTENSION_EXECUTION_MODE=linux-cgroup`, an absolute private
`QWEN_MANAGED_AGENT_EXTENSION_CACHE_DIRECTORY` outside Workspace storage, and
`QWEN_MANAGED_AGENT_EXTENSION_CGROUP_ROOT` to a writable delegated cgroup-v2
subtree on each LOCAL or SSH Runtime host. The service must receive delegation
from its administrator; a writable ordinary directory is insufficient. Missing
cgroup support fails activation; no process-group fallback is used.
Native macOS LOCAL requires explicit `developer-process-group` mode and an empty
cgroup root. Its receipt proves registered process groups only. It does not prove
that detached descendants were reclaimed and is not a production process-tree
profile. Recovery retains holds when the durable owner journal or stop proof is
missing or uncertain.
@@ -79,3 +79,17 @@ curl --fail http://127.0.0.1:8080/actuator/health
- `/etc/qwen-managed-agent` — 凭据与 `application.yml`。
- `/var/lib/qwen-managed-agent` — Runtime 状态。
- `/srv/qwen-workspace` — Workspace;迁移位置时同步调整 `ReadWritePaths`。
## Extension 执行
执行默认关闭。先单独启用包发布,再配置
`QWEN_MANAGED_AGENT_EXTENSION_EXECUTION_ENABLED=true`、
`QWEN_MANAGED_AGENT_EXTENSION_EXECUTION_MODE=linux-cgroup`、位于 Workspace 存储之外的绝对私有
`QWEN_MANAGED_AGENT_EXTENSION_CACHE_DIRECTORY`,并将
`QWEN_MANAGED_AGENT_EXTENSION_CGROUP_ROOT` 指向每台 LOCAL 或 SSH Runtime 主机上可写、已委派的
cgroup-v2 子树。管理员必须为服务委派 cgroup;普通可写目录不足以提供此能力。
缺少 cgroup 支持时激活失败,不回退到进程组。
macOS 原生 LOCAL 必须显式选择 `developer-process-group` 且清空 cgroup root。
该模式的回执仅证明已登记进程组,不证明已脱离进程组的后代被回收,不属于生产进程树监管配置。
持久 owner journal 或停止证明缺失、不确定时,恢复继续保留 hold。
@@ -0,0 +1,76 @@
package com.alibaba.qwen.code.managedagent.api;
import com.alibaba.qwen.code.managedagent.extensions.ManagedExtensionManagementService;
import com.alibaba.qwen.code.managedagent.model.TenantContext;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import org.springframework.http.CacheControl;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ManagedExtensionManagementController {
private static final String BASE = "/v1/agents/sessions/{sessionId}/extensions";
private static final String WEB = "/api/agent/web-shell/v1/session-extensions";
private final ManagedExtensionManagementService service;
public ManagedExtensionManagementController(ManagedExtensionManagementService service) { this.service = service; }
@GetMapping(BASE)
public ResponseEntity<JsonNode> query(TenantContext tenant, @PathVariable String sessionId) {
return noStore(service.query(tenant.tenantId(), tenant.requireActorId(), sessionId, null));
}
@PostMapping(BASE)
public ResponseEntity<JsonNode> submit(TenantContext tenant, @PathVariable String sessionId,
@RequestHeader("Idempotency-Key") String key, @RequestBody JsonNode command) {
return noStore(service.submit(tenant.tenantId(), tenant.requireActorId(), sessionId, key, command));
}
@GetMapping(BASE + "/operations/{operationId}")
public ResponseEntity<JsonNode> operation(TenantContext tenant, @PathVariable String sessionId, @PathVariable String operationId) {
return noStore(service.query(tenant.tenantId(), tenant.requireActorId(), sessionId, operationId));
}
@PostMapping(BASE + "/operations/{operationId}/continue")
public ResponseEntity<JsonNode> resume(TenantContext tenant, @PathVariable String sessionId, @PathVariable String operationId) {
return noStore(service.resume(tenant.tenantId(), tenant.requireActorId(), sessionId, operationId, false));
}
@PostMapping(BASE + "/operations/{operationId}/recover-previous")
public ResponseEntity<JsonNode> recover(TenantContext tenant, @PathVariable String sessionId, @PathVariable String operationId) {
return noStore(service.resume(tenant.tenantId(), tenant.requireActorId(), sessionId, operationId, true));
}
public record WebQuery(@NotBlank String sessionId, String operationId) { }
public record WebSubmit(@NotBlank String sessionId, @NotBlank String idempotencyKey, @NotNull JsonNode command) { }
@PostMapping(WEB + "/query")
public ResponseEntity<JsonNode> webQuery(TenantContext tenant, @Valid @RequestBody WebQuery request) {
return noStore(service.query(tenant.tenantId(), tenant.requireActorId(), request.sessionId(), request.operationId()));
}
@PostMapping(WEB + "/submit")
public ResponseEntity<JsonNode> webSubmit(TenantContext tenant, @Valid @RequestBody WebSubmit request) {
return noStore(service.submit(tenant.tenantId(), tenant.requireActorId(), request.sessionId(), request.idempotencyKey(), request.command()));
}
@PostMapping(WEB + "/continue")
public ResponseEntity<JsonNode> webResume(TenantContext tenant, @Valid @RequestBody WebQuery request) {
return noStore(service.resume(tenant.tenantId(), tenant.requireActorId(), request.sessionId(), request.operationId(), false));
}
@PostMapping(WEB + "/recover-previous")
public ResponseEntity<JsonNode> webRecover(TenantContext tenant, @Valid @RequestBody WebQuery request) {
return noStore(service.resume(tenant.tenantId(), tenant.requireActorId(), request.sessionId(), request.operationId(), true));
}
private static ResponseEntity<JsonNode> noStore(JsonNode value) { return ResponseEntity.ok().cacheControl(CacheControl.noStore()).body(value); }
}
Loaded 100 of 212 files, more files were not shown because too many files have changed in this diff. Show more