feat(serve): add extension management v2 - #6638
Conversation
Mirror the daemon extension-management REST surface to per-workspace routes, reusing the Phase 3 runtime resolver and trust gate. Extract a per-workspace extensions controller so the primary workspace shares one install queue, operation history, and status cache across the legacy and workspace-qualified routes. Reads resolve the target runtime only; mutations require a trusted workspace. Advertise a new baseline capability so clients can discover the surface, and add matching SDK client methods. Refs QwenLM#6378.
|
Re-triage on latest commit (a81fe71). Prior triage comments updated in place. TemplateTemplate looks good ✓ — all required sections present with bilingual body. ProblemThis is a DirectionAligned. Extension management for multi-workspace daemon is a necessary component of the RFC. The approach (global artifact ownership, per-workspace activation policy, transactional store with journal recovery, FIFO commit queue) is reasonable for the stated goal. SizeThis is a massive PR — 7,629 production logic lines across 4 packages. Far exceeds the 1000+ large PR advisory.
🔴 500+ maintainer awareness: This PR requires maintainer review given core infrastructure changes at scale. ApproachThe architecture (transactional store, generation-based state, FIFO preparation/commit queues, workspace projection) is sound. But the scope is very large — could have been split:
The legacy/V2 route duplication (~300 lines of nearly identical validation/mutation logic between Moving on to code review and test verification. 中文说明模板完整 ✓ 问题:这是一个 方向:对齐。多 workspace 的扩展管理是 RFC 的必要组件。 规模:这是一个超大型 PR —— 跨 4 个 package 的 7,629 行生产逻辑代码。远超 1000+ 大 PR 建议阈值。
方案:架构合理,但范围过大。legacy/V2 路由之间有 ~300 行几乎相同的验证/mutation 逻辑,是维护风险。 进入代码审查和测试验证。 — Qwen Code · qwen3.7-max |
|
Re-triage on latest commit (a81fe71). 5 fix commits pushed since prior review. Code ReviewIndependent proposal (before reading diff): To add multi-workspace extension management, I would build a transactional store with generation-based state, a single-writer commit queue, per-workspace activation policy, and a backward-compatible legacy adapter. The store should use file locking and journal-based recovery. Routes should be split between global artifact operations and per-workspace activation. The PR's approach closely matches this — transactional Findings:
Test Results
🔴 ExtensionManager test isolation — STILL BROKENThe 5 fix commits since the prior triage did not resolve the test isolation issue. Failure rate went from 55/84 (65%) to 70/95 (74%). The root cause: Example failure pattern from the test output: This originates from the Fix required: The test setup needs to either (a) create a fresh 中文说明代码审查架构合理。事务式 中等严重:
测试结果新增测试全部通过(ExtensionStore 49/49, V2 路由 27/27, Controller 5/5, Scheduler 8/8)。构建通过。 🔴 ExtensionManager 测试隔离问题仍未解决:5 个 fix commit 后,失败率从 55/84(65%)恶化到 70/95(74%)。ExtensionStore 状态在测试间泄漏。测试 setup 需要为每个测试创建全新的 store 目录或在 afterEach 中清理。 — Qwen Code · qwen3.7-max |
|
The architecture is genuinely well-designed — the transactional store with WAL journal recovery, the FIFO preparation/commit queues, the workspace projection with generation tracking, and the backward-compatible legacy adapter all show careful thought. The new code reads cleanly: no The new test suites pass cleanly: ExtensionStore (49/49), V2 routes (27/27), controller (5/5), scheduler (8/8). Build and typecheck pass. But the ExtensionManager tests are still broken — 70/95 failures (74%), up from 55/84 (65%) in the prior review. The 5 fix commits pushed since the last triage addressed network policy, timer guards, and commit lifecycle, but did not fix the root cause: store state leaking between test cases because the store directory isn't cleaned up. Beyond test failures, the scope (7,629 production lines, 4 packages) is a real concern. The I would thank the author for the careful design work, but I would not merge this until the test isolation is resolved. 中文说明架构设计经过深思熟虑。新测试套件全部通过(ExtensionStore 49/49, V2 路由 27/27, Controller 5/5, Scheduler 8/8)。构建和类型检查通过。 但 ExtensionManager 测试仍然失败 — 70/95(74%),比上次审查的 55/84(65%)更差。5 个 fix commit 未修复根本原因:store 目录在测试间泄漏。 范围(7,629 行生产代码,4 个 package)也是真实问题。测试隔离解决前不能合并。 — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
Align the new extensions controller file's copyright year with the other new files added in this change.
wenshao
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestion-level recommendations are in the Suggestion summary comment below.
Suggestions — commit
|
| File | Issue | Suggested fix |
|---|---|---|
extension-store.ts:1055 |
rollbackJournal performs destructive filesystem ops (rm, rename) with zero logging. Oncall cannot diagnose recovery failures. |
Add writeStderrLine at each decision point in rollbackJournal and recoverTransactionsUnlocked. |
extensionManager.ts:1917 |
Telemetry logs 'success' after commitArtifact even when loadExtension fails, masking commit-with-reload-failure. |
Check committed.extension before choosing telemetry status; log 'committed_but_reload_failed' when reload fails. |
workspace-extensions.ts:~890 |
V2 global REST endpoints (POST /extensions/install, GET /extensions, DELETE /extensions/:id, PUT/DELETE /extensions/:id/activation) have no REST-level tests. |
Add supertest cases covering install happy path, invalid extension id (400), missing consent (400), idempotent DELETE (204), and activation state changes. |
workspace-extensions.ts:206 |
reconcileExternalGeneration background poller and stopExtensionGenerationReconciler cleanup have no tests. |
Add a test that triggers the reconciler after a failed refresh and asserts generation catches up; test interval cleanup. |
workspace-extensions-controller.ts:662 |
buildLocalExtensionsStatus returns ext.path (absolute filesystem paths) in API response, disclosing internal directory structure. |
Omit ext.path or replace with a relative/opaque identifier. |
workspace-extensions.ts:1245 |
DELETE /extensions/:extensionId 204 short-circuit skips validateExtensionMutationClient, making it an extension-ID existence oracle. |
Move the client-ID validation before the existence check. |
workspace-extensions-controller.ts:228 |
pruneExtensionOperations has O(n²) complexity — terminalCount() rescans the map on every eviction iteration. |
Maintain a counter of terminal operations; evict oldest by insertion order in a single pass. |
workspace-extensions.ts:1286 |
GET /workspaces/:workspace/extensions calls getExtensionActivation per extension, each reading state.json from disk (N+1 reads). |
Read snapshot once before the loop; use synchronous getActivation(snapshot, ...) for each extension. |
npm.ts:195 |
fetchNpmJson and downloadNpmFile follow redirects recursively without a counter, unlike github.ts which caps at 10. |
Add a redirectCount parameter and reject after a reasonable limit. |
npm.ts:230 |
downloadNpmFile has no max download size, unlike github.ts which caps at ARCHIVE_DOWNLOAD_MAX_BYTES. |
Add a bytesWritten counter with a maximum size check. |
workspace-extensions.ts:201 |
appliedGenerationByWorkspaceId Map never removes entries for deregistered workspaces. |
Hook into workspace deregistration to clean stale entries. |
workspace-extensions.ts:~895 |
V2 POST /extensions/install duplicates all validation logic from the legacy install route. |
Extract shared validation helpers (parseAndValidateInstallRequest) used by both routes. |
extensionManager.ts:1340 |
installExtensionInternal has 9 positional parameters, extremely hard to read and maintain. |
Convert to an options object pattern. |
extensionManager.ts:1894 |
prepared.expectedArtifactGeneration ?? 0 is fragile coupling — silently bypasses OCC when generation was not captured. |
Pass undefined when not set; adjust store to skip check on undefined. |
workspace-extensions.ts:233 |
Generation reconciler retries all workspaces on partial failure, causing redundant refreshes for healthy ones. | Track per-workspace observed generation instead of a single global one. |
extension-file-watcher.ts:387 |
pollStoreGeneration duplicates the generation reconciler's polling; both react to the same generation bump. |
Delegate to a shared generation observer or suppress file-watcher polling when daemon is running. |
extensionManager.ts:2093 |
checkForAllExtensionUpdates routes update checks through the preparation queue, competing with install preparations. |
Use a separate queue for update checks. |
workspace-extensions-controller.ts:286 |
Fire-and-forget void (async () => {})(): no way to wait for in-flight operations on graceful shutdown. |
Track active operations in an AbortController set the shutdown path can abort/await. |
extension-store.ts:~510 |
readSnapshot bypasses withLock, skipping recoverCorruptStateUnlocked and recoverTransactionsUnlocked. |
Wrap in withLock or catch ExtensionStoreCorruptError and attempt recovery inline. |
extension-store.ts:~278 |
Legacy projection import always sets defaultActivation: 'enabled', losing disabled-default state from V1 !/* rules. |
Decode the !/* rule back to defaultActivation: 'disabled' during legacy import. |
— qwen3.7-max via Qwen Code /review
Redact credentials from the extension source on the two success-path fan-outs (session refresh and refresh-failure broadcast), matching the operation record and failure broadcast. Document the non-cancellation semantics of the extension timeout wrapper.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestion-level recommendations are in the Suggestion summary comment below.
Share the queue-full sentinel message via an exported constant so the throw site (controller) and the 429 match site (routes) cannot drift after the module split. Include the bound workspace in the extension operation log prefixes so concurrent per-workspace controllers are distinguishable in stderr.
samuelhsin
left a comment
There was a problem hiding this comment.
Requesting changes for the three correctness issues called out inline.
WebShell UI verification: I ran the PR daemon and WebShell in an isolated environment. 4/6 checks passed, 0 failed, and 2 were incomplete (full runtime-console capture and final mobile keyboard-focus evidence). The legacy /workspace/extensions dialog rendered its empty state, recovered after three Refresh clicks without a client session, survived close/reopen cycles, and had no horizontal overflow at 375x812. Because no UI failure was observed, there is no failure screenshot to attach.
The remaining blockers are backend multi-workspace state/serialization issues and are not meaningfully demonstrated by a WebShell screenshot: the current WebShell still uses the legacy primary-workspace route, while the new plural routes have no UI entry yet.
Validation already completed on 59b2f88: focused CLI tests 8/8, SDK tests 221/221, and full typecheck passed. I also reviewed the 9dd2e92 delta; it only centralizes the queue-full message and adds workspace IDs to logs, so the three issues below remain.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestion-level recommendations are in the Suggestion summary comment below.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
🔴 CI failure on
|
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
wenshao
left a comment
There was a problem hiding this comment.
Review: Extension Management V2 (high effort)
This is a thorough review of the Extension Management V2 PR. The architecture — transactional ExtensionStore, workspace-qualified activation, prepare/commit split with a daemon-wide scheduler — is sound and well-documented. Several correctness issues were found.
Critical (5, high confidence)
C1. acpAgent.ts:7545 — Refresh errors propagate unhandled
The old code wrapped each refreshCache()/refreshTools() call in .catch() blocks. The new code runs five sequential operations with zero error handling:
await Promise.all([extensionManager.refreshCache(), skillManager?.refreshCache()]);
await extensionManager.refreshTools();
await config.refreshHierarchicalMemory();
await config.getGeminiClient()?.refreshSystemInstruction();
A transient ExtensionStoreBusyError crashes the entire workspaceExtensionsRefresh RPC and skips sendAvailableCommandsUpdate(). The "reload" handler in the same file wraps these in try/catch — the two handlers are inconsistent.
C2. npm.ts (fetchNpmJson) — No response body size limit
fetchNpmJson accumulates chunks: Buffer[] with no byte cap. downloadNpmFile enforces NPM_ARCHIVE_DOWNLOAD_MAX_BYTES (100 MiB) but the JSON fetch path has no equivalent. A malicious/compromised registry can serve a multi-GB JSON response → OOM.
C3. extensionManager.ts (performWorkspaceExtensionMigration) — Committed-but-broken installs silently discarded
} catch (error) {
if (isExtensionCommittedWithWarningsError(error)) continue;
failedInstallNames.push(extension.config.name);
}isExtensionCommittedWithWarningsError means the extension was committed to disk but could not be loaded. continue skips it from failedInstallNames, so migration reports it as successful. The extension is non-functional until restart.
C4. extension-store.ts (commitArtifact) — Rollback failure masks the original error
} catch (error) {
if (!stateCommitted) {
await this.rollbackJournal(journal); // not wrapped in try/catch
await fsp.rm(journalPath, { force: true });
}
throw error;
}If rollbackJournal throws (e.g., renameWithRetry fails), the original error is permanently lost — caller receives the rollback error instead, and the journal file is never cleaned up. The same pattern in recoverTransactionsUnlocked blocks ALL subsequent store operations on rollback failure.
C5. workspace-extensions-controller.ts — buildWorkspaceCtx is dead code
buildWorkspaceCtx is created and exposed on the ExtensionsController interface but never invoked by any route handler. The old validateExtensionMutationClient called it on every mutation; the new version dropped the route parameter and the call. All workspace context setup (telemetry/tracing) is silently lost for extension HTTP requests.
Suggestions (8, high confidence)
S1. validateExtensionSourceHost drops SSH protocol support but validateExtensionSourceMetadata still allows it for non-public policies — dead code and a landmine for future callers.
S2. legacyProjectionIsNewerThanState throws ExtensionStoreCorruptError on equal timestamps — false positive on coarse-granularity filesystems (ext3, Docker volumes). Treat equal as "state is at least as new."
S3. V2 routes use synchronous (req, res) => { ... } handlers. If createExtensionManager() throws, the error bypasses sendBridgeError — inconsistent error responses vs legacy routes.
S4. deadlineMs covers preparation only — commit queue wait has no deadline. Operations can wait past their preparation deadline if the commit lane backs up.
S5. archive-safety.ts only checks SymbolicLink/Link types. No explicit ../absolute path rejection — relies on tar library silently rewriting paths.
S6. DiscoverTab.tsx committed-with-warnings handler silently swallows setExtensionScope failure — user sees "Installed with warnings" but scope preference may not be applied.
S7. uninstallUserExtension returns Promise<ExtensionMutationResponse | undefined> while all other mutation methods return Promise<ExtensionMutationResponse> — forces null checks on every consumer.
S8. install.ts — setExtensionScope throw in the committed-with-warnings catch hits the generic catch → process.exit(1), defeating the graceful degradation path.
Reviewed with Qwen Code /review (high effort, 45 chunks, 12 invariant agents, 2 whole-diff agents, reverse audit)
Re-verified on
|
| Suite | Result |
|---|---|
| REST surface, security, store durability | 46/46 ✅ |
ExtensionStore transactionality |
25/25 ✅ |
| SSRF guard (DNS-resolution layer) | 3/3 ✅ |
| Live runtime reconciliation | 17/17 ✅ |
packages/core extension unit tests |
534 pass ✅ |
Full packages/cli suite |
12,379 pass · 1 real failure 🔴 |
Both previously-failing CI tests are green: reports queued and running extension operation states and commits in preparation completion order. The queued-status fix landed as suggested (with phase: undefined cleared on re-queue — a nice touch).
⚠️ A false alarm I chased down — so you don't have to
7077c24fb removed the line I'd asked for:
- await config.refreshHierarchicalMemory();
await config.getGeminiClient()?.refreshSystemInstruction();That looks like it undoes the whole reconciliation fix: refreshSystemInstruction() rebuilds the prompt from config.getUserMemory(), and refreshHierarchicalMemory() is the only thing that recomputes it. Statically it reads like the disabled extension's context would be baked straight back into the rebuilt system instruction.
It doesn't. I instrumented the live ACP child:
00 handler-entry : userMemoryHasMarker=true ctxPaths=2 active=1
01 after refreshCache: userMemoryHasMarker=true ctxPaths=1 active=0
02 after refreshTools: userMemoryHasMarker=false ctxPaths=1 active=0 ← cleared here
03 after refreshSysIn: userMemoryHasMarker=false
extensionManager.refreshTools() → refreshExtensionRuntime() → config.refreshHierarchicalMemory() (extension-runtime-refresh.ts). The explicit call really was redundant, and the load-bearing ordering still holds: memory is refreshed before the system instruction is rebuilt. E2E agrees — the live session still drops the disabled extension's context without a restart (17/17).
Worth keeping in mind for future edits: that ordering is now an implicit invariant of refreshTools(). If refreshHierarchicalMemory() ever moves out of refreshExtensionRuntime(), this silently regresses again with no failing test.
🔴 New CI-red: /capabilities contract
7077c24fb made workspaces unconditional:
- ...(multiWorkspace
- ? { workspaces: runtimes.map((runtime) => ({ ... })) }
- : {}),
+ workspaces: runtimes.map((runtime) => ({ ... })),This is clearly deliberate and well-motivated — the SDK type comment was updated to say so ("newer daemons include the primary runtime even in single-workspace mode so workspace-qualified features can address it by ID"), and V2's /workspaces/:workspace/extensions genuinely needs an addressable id on single-workspace daemons. But two places still encode the old contract:
1. The test — CI will fail again (server.test.ts:2709):
expect(before.body.features).not.toContain('multi_workspace_sessions');
expect(before.body.workspaces).toBeUndefined(); // ← now [{ id: 'primary-id', primary: true, ... }]Fix (validated → server.test.ts 679/679):
- expect(before.body.workspaces).toBeUndefined();
+ expect(before.body.workspaces).toHaveLength(1);
+ expect(before.body.workspaces[0]).toMatchObject({
+ id: 'primary-id',
+ primary: true,
+ });2. The protocol doc now contradicts the code — docs/developers/qwen-serve-protocol.md:220:
"On single-workspace daemons,
workspaces[]is absent unlessmulti_workspace_sessionsis also advertised, so clients usecapabilities.workspaceCwdas the cwd selector."
Worth updating in the same commit, since this is a published-envelope change that clients gate on.
Two store behavior changes I noticed — please confirm they're intended
Neither is a bug, but both are reversals of behavior earlier in this PR, and my suite had to be updated to match:
1. Corrupt journal: quarantine → fail-closed. This reverses 2566f1f1d fix(core): quarantine corrupt store journals from earlier in this same PR. readRecoverableJournalUnlocked() no longer quarantines; it rethrows. There's a new test (fails closed when an active transaction journal is corrupt), so it's intentional — and for a forged journal, failing closed is the right posture (my forged-path test confirms the out-of-store directory is never touched).
The consequence worth documenting: a single corrupt journal now makes every store operation throw extension_store_corrupt — GET /extensions, every install/activation, and the CLI's extension commands — permanently, with no self-heal and no documented remediation. An operator has to know to delete the file from ~/.qwen/extension-store/transactions/. A line in the design doc (or an error message that names the file) would go a long way.
2. Corrupt-state.json recovery no longer bumps the generation (previous.generation + 1 → previous.generation).
I initially flagged this as "the generation moved backwards", but on inspection the new behavior is better and I'd keep it: the watcher compares with !== (extension-file-watcher.ts:392), so the old +1 produced the same generation as the state that was just lost — meaning the recovery would have been invisible to other daemons, which would have kept serving a policy that no longer exists. The new value differs, so they re-read. Faithful generation↔content labelling is the right call.
One small consequence: because the daemon tracks appliedGeneration with Math.max, it can now exceed desiredGeneration in the workspace projection after a recovery. The protocol doc only allows for the reverse ("appliedGeneration … can temporarily lag"). Cosmetic, but the doc sentence is now incomplete.
Everything else — crash-before-commit rollback, crash-after-commit roll-forward, optimistic-concurrency extension_conflict, V1 downgrade re-import, cross-process lock with no lost updates, the two-layer SSRF guard, trust gating, queue backpressure, restart durability — still holds.
中文版
在 31e1b4abf 上重新验证 —— CI 回归已修复 ✅,但引入了一个新的红测试 🔴
基于新的三个提交重新构建,并重跑了完整测试装置(真实 qwen serve 守护进程、3 个真实 workspace、真实磁盘存储、真实联网安装)。我之前提的问题都已解决。 但过程中引入了一个新的 CI 红。
| 测试套件 | 结果 |
|---|---|
| REST 接口、安全、存储持久性 | 46/46 ✅ |
ExtensionStore 事务性 |
25/25 ✅ |
| SSRF 防护(DNS 解析层) | 3/3 ✅ |
| 运行时实时对账 | 17/17 ✅ |
packages/core 扩展单测 |
534 通过 ✅ |
完整 packages/cli 套件 |
12,379 通过 · 1 个真实失败 🔴 |
之前失败的两个 CI 测试都绿了:reports queued and running extension operation states 和 commits in preparation completion order。queued 状态的修复按建议落地了(并且在重新入队时清掉了 phase,这一点做得很好)。
⚠️ 一个我追查过的假警报 —— 免得你们也走一遍
7077c24fb 删掉了我之前要求加的那行:
- await config.refreshHierarchicalMemory();
await config.getGeminiClient()?.refreshSystemInstruction();乍看之下这会把整个对账修复推翻:refreshSystemInstruction() 是基于 config.getUserMemory() 重建 prompt 的,而 refreshHierarchicalMemory() 是唯一会重算它的地方。从静态代码看,被禁用扩展的上下文似乎会被原样烤回重建后的 system instruction。
但事实并非如此。 我在运行中的 ACP 子进程里打了探针:
00 handler-entry : userMemoryHasMarker=true ctxPaths=2 active=1
01 after refreshCache: userMemoryHasMarker=true ctxPaths=1 active=0
02 after refreshTools: userMemoryHasMarker=false ctxPaths=1 active=0 ← 在这里被清掉
03 after refreshSysIn: userMemoryHasMarker=false
extensionManager.refreshTools() → refreshExtensionRuntime() → config.refreshHierarchicalMemory()(见 extension-runtime-refresh.ts)。所以那个显式调用确实是冗余的,而且关键的顺序依然成立:先刷新 memory,再重建 system instruction。 E2E 也印证了这一点 —— live session 依然能在不重启的情况下丢掉被禁用扩展的上下文(17/17)。
不过有一点值得记住:这个顺序现在是 refreshTools() 的一个隐式不变量。如果将来 refreshHierarchicalMemory() 被移出 refreshExtensionRuntime(),这个 bug 会悄无声息地回归,而且没有任何测试会失败。
🔴 新的 CI 红:/capabilities 契约
7077c24fb 让 workspaces 变成无条件下发:
- ...(multiWorkspace
- ? { workspaces: runtimes.map((runtime) => ({ ... })) }
- : {}),
+ workspaces: runtimes.map((runtime) => ({ ... })),这显然是有意为之且理由充分 —— SDK 的类型注释也同步更新了("较新的守护进程即使在单 workspace 模式下也会包含 primary runtime,以便 workspace 限定的功能能按 ID 寻址它"),而且 V2 的 /workspaces/:workspace/extensions 在单 workspace 守护进程上确实需要一个可寻址的 id。但有两处仍然写着旧契约:
1. 测试 —— CI 会再次失败(server.test.ts:2709):
expect(before.body.features).not.toContain('multi_workspace_sessions');
expect(before.body.workspaces).toBeUndefined(); // ← 现在是 [{ id: 'primary-id', primary: true, ... }]修复(已验证 → server.test.ts 679/679):
- expect(before.body.workspaces).toBeUndefined();
+ expect(before.body.workspaces).toHaveLength(1);
+ expect(before.body.workspaces[0]).toMatchObject({
+ id: 'primary-id',
+ primary: true,
+ });2. 协议文档现在与代码矛盾 —— docs/developers/qwen-serve-protocol.md:220:
"在单 workspace 守护进程上,除非同时广播
multi_workspace_sessions,否则workspaces[]不存在,因此客户端应使用capabilities.workspaceCwd作为 cwd 选择器。"
建议在同一个提交里一并更新,因为这是客户端会据以做能力判断的、已发布的 envelope 变更。
两处我注意到的存储行为变更 —— 请确认是有意的
这两处都不是 bug,但都是对本 PR 早前行为的反转,我的测试套件也相应做了更新:
1. 损坏的 journal:隔离 → fail-closed。 这反转了本 PR 早前的 2566f1f1d fix(core): quarantine corrupt store journals。readRecoverableJournalUnlocked() 不再隔离,而是直接抛出。新增了对应测试(fails closed when an active transaction journal is corrupt),所以是有意的 —— 而且对于伪造的 journal,fail-closed 是正确的姿态(我的伪造路径测试确认了 store 之外的目录始终没有被动过)。
值得写进文档的后果是:现在只要有一个损坏的 journal,所有存储操作都会抛 extension_store_corrupt —— 包括 GET /extensions、所有安装/激活、以及 CLI 的扩展命令 —— 而且是永久性的,没有自愈,也没有文档化的补救方式。运维必须自己知道去删掉 ~/.qwen/extension-store/transactions/ 下的那个文件。在设计文档里加一句话(或者在报错信息里带上文件路径)会很有帮助。
2. state.json 损坏后的恢复不再递增 generation(previous.generation + 1 → previous.generation)。
我一开始把这个标记为"generation 倒退了",但仔细看下来新行为其实更好,我建议保留:watcher 用的是 !== 比较(extension-file-watcher.ts:392),所以旧的 +1 会产生与刚刚丢失的那个状态相同的 generation —— 也就是说这次恢复对其他守护进程来说是不可见的,它们会继续提供一个已经不存在的策略。新的值不同,因此它们会重新读取。让 generation 忠实地对应内容,这是正确的选择。
一个小的连带影响:由于守护进程用 Math.max 追踪 appliedGeneration,恢复之后它可能会超过 workspace projection 里的 desiredGeneration。而协议文档只描述了反方向("appliedGeneration …… 可能会暂时滞后")。这只是措辞问题,但那句话现在不完整了。
其余部分 —— 提交前崩溃回滚、提交后崩溃向前滚动、乐观并发的 extension_conflict、V1 降级重新导入、跨进程锁无丢失更新、两层 SSRF 防护、信任门禁、队列背压、重启持久性 —— 全部依然成立。
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Thanks for the thorough re-verification. The stale single-workspace capabilities assertion is fixed in The two store behavior changes are intentional. Active corrupt journals fail closed so a forged or partially corrupt transaction cannot trigger unsafe recovery outside the store, and corrupt-state recovery preserves the prior generation so peer daemons observe a generation change instead of mistaking the recovered snapshot for the lost state. I have kept both behaviors unchanged in this pass. The operator-remediation and applied-versus-desired wording are useful documentation follow-ups, but they are not runtime regressions. |
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Addressed the latest five-Critical review on current head e70ab39.
Verification: focused CLI 243/243 and Core 204/204 tests passed; full build, typecheck, and lint passed. |
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
[codex] Closing this PR to restart review with a clean discussion history. The implementation evolved substantially across several review rounds, so many earlier comments now describe superseded code. Final state at
Validation completed: repository build, lint, and typecheck passed; 359 focused core/CLI/serve tests passed; two open-ended self-audit passes and an independent test-engineer verification found no remaining concrete defect. All known actionable review threads on this PR were addressed and resolved. The same final head and updated PR description will be used for a fresh replacement PR. Replacement PR: #6825 (#6825). Fresh review has been requested from @callmeYe, @samuelhsin, @qqqys. |
Pull request was closed
Re-verified on
|
| Suite | Result |
|---|---|
| REST surface, security, store durability | 46/46 ✅ |
ExtensionStore transactionality |
25/25 ✅ |
| SSRF guard (DNS-resolution layer) | 3/3 ✅ |
| Live runtime reconciliation | 17/17 ✅ |
Archive extraction (new — zip-extraction.ts) |
14/14 ✅ |
packages/core extension units |
553 pass ✅ |
Full packages/cli suite |
12,383 pass, 0 real failures ✅ |
Prior findings — both fixed
/capabilitiesCI-red → fixed. The test now asserts the single-workspace primary runtime is present (server.test.ts679/679, passes in isolation and in the full file).- Protocol doc contradiction → fixed.
qwen-serve-protocol.md:220now reads "Newer single-workspace daemons include the primary runtime inworkspaces[]even whenmulti_workspace_sessionsis absent … clients should fall back tocapabilities.workspaceCwdfor older daemons" — matches the code. - The
queued/runningfix and the live-reconciliation fix both remain green. The implicitrefreshTools() → refreshHierarchicalMemory()ordering I flagged last time is intact.
The only 4 remaining × in the full CLI suite are root-in-container artifacts (unlink/permission/glob tests in files this PR never touches, e.g. cleanup.test.ts); they fail identically on a clean tree because a test can't make a directory un-removable when running as root.
New surface I exercised: archive extraction
c215581a9's commit set adds a rewritten zip extractor (packages/core/src/extension/zip-extraction.ts) and a reworked archive-safety.ts. Since this is remote-attacker-controlled input reached by the archive-url install path, I drove the real compiled extractZipArchive() against hand-crafted malicious archives. (yazl refuses to build entries with .., so I emit raw central-directory bytes to get genuinely hostile filenames and symlink-mode entries.)
A "protected" file is planted outside the extraction root; every attack must fail to touch it:
| Attack | Result |
|---|---|
../outside/PROTECTED.txt (zip-slip) |
rejected, protected file intact ✅ |
../../../../../../tmp/… (deep traversal) |
rejected, nothing written to /tmp ✅ |
| absolute-path entry | never lands at its absolute target ✅ |
in-archive symlink entry (S_IFLNK mode) |
rejected: "unsupported symbolic link entry" ✅ |
| extraction through a pre-planted symlink | rejected: "Out of bound path …" — protected file intact ✅ |
__MACOSX/ junk |
skipped, real files kept ✅ |
pre-aborted AbortSignal |
extraction rejected ✅ |
The pre-planted-symlink case is the interesting one — it's the attack that a filename check alone can't stop, and it's caught by the extractor's own realpath + isWithinRoot guard (post-mkdir), not by the zip library. That's real defense-in-depth: yauzl's validateFileName is the outer wall for ../backslash filenames, and the extractor's canonical-path check is the inner wall for symlink-based escapes.
Everything I've been tracking across these rounds — transactional store, live reconciliation, SSRF, trust gating, queue backpressure, restart durability, and now archive safety — holds. LGTM.
中文版
在 c215581a9 上重新验证 —— 两个遗留问题都已解决 ✅,新增的归档加固也站得住 ✅
基于新的七个提交重新构建并重跑了全部验证。上一轮的两个问题都修好了,CI 现在应该是绿的。 我还为这些提交新引入的归档解压代码补充了覆盖。
| 测试套件 | 结果 |
|---|---|
| REST 接口、安全、存储持久性 | 46/46 ✅ |
ExtensionStore 事务性 |
25/25 ✅ |
| SSRF 防护(DNS 解析层) | 3/3 ✅ |
| 运行时实时对账 | 17/17 ✅ |
归档解压(新增 —— zip-extraction.ts) |
14/14 ✅ |
packages/core 扩展单测 |
553 通过 ✅ |
完整 packages/cli 套件 |
12,383 通过,0 个真实失败 ✅ |
遗留问题 —— 两个都已修复
/capabilitiesCI 红 → 已修复。测试现在断言单 workspace 的 primary runtime 存在(server.test.ts679/679,单独跑和在完整文件里都通过)。- 协议文档矛盾 → 已修复。
qwen-serve-protocol.md:220现在写的是"较新的单 workspace 守护进程即使在没有multi_workspace_sessions时也会在workspaces[]中包含 primary runtime …… 对于省略该数组的旧守护进程,客户端应回退到capabilities.workspaceCwd"—— 与代码一致了。 queued/running的修复和实时对账的修复都仍然是绿的。我上次提示的那个隐式的refreshTools() → refreshHierarchicalMemory()顺序依赖依然完好。
完整 CLI 套件里仅剩的 4 个 × 都是 root-in-container 环境产物(unlink/权限/glob 相关的测试,位于本 PR 从未改动的文件中,如 cleanup.test.ts);它们在干净的代码树上也会同样失败,因为以 root 运行时测试无法让一个目录变得"不可删除"。
我新验证的面:归档解压
c215581a9 这批提交新增了一个重写的 zip 解压器(packages/core/src/extension/zip-extraction.ts)和一个重构的 archive-safety.ts。由于这是 archive-url 安装路径会触达的、由远程攻击者控制的输入,我用手工构造的恶意归档直接驱动了真实编译产物 extractZipArchive()。(yazl 拒绝构建含 .. 的条目,所以我直接写原始的中央目录字节,才能得到真正带有恶意文件名和 symlink 模式的条目。)
我在解压根目录之外放了一个"受保护"文件,每个攻击都必须无法触碰它:
| 攻击 | 结果 |
|---|---|
../outside/PROTECTED.txt(zip-slip) |
被拒绝,受保护文件完好 ✅ |
../../../../../../tmp/…(深层穿越) |
被拒绝,/tmp 下无任何写入 ✅ |
| 绝对路径条目 | 从不落在其绝对目标位置 ✅ |
归档内 symlink 条目(S_IFLNK 模式) |
被拒绝:"unsupported symbolic link entry" ✅ |
| 穿过预置 symlink 的解压 | 被拒绝:"Out of bound path …" —— 受保护文件完好 ✅ |
__MACOSX/ 垃圾 |
被跳过,真实文件保留 ✅ |
预先 abort 的 AbortSignal |
解压被拒绝 ✅ |
预置 symlink 那一例是最有意思的 —— 这是单纯的文件名检查拦不住的攻击,而它被解压器自身的 realpath + isWithinRoot 守卫(在 mkdir 之后)拦下了,而不是靠 zip 库。这是真正的纵深防御:yauzl 的 validateFileName 是拦 ../反斜杠文件名的外墙,解压器的规范化路径检查是拦 symlink 逃逸的内墙。
我这几轮一直在跟踪的东西 —— 事务性存储、实时对账、SSRF、信任门禁、队列背压、重启持久性,现在再加上归档安全 —— 全部成立。LGTM。
* feat(cli): workspace-qualified extensions REST (daemon multi-workspace) Mirror the daemon extension-management REST surface to per-workspace routes, reusing the Phase 3 runtime resolver and trust gate. Extract a per-workspace extensions controller so the primary workspace shares one install queue, operation history, and status cache across the legacy and workspace-qualified routes. Reads resolve the target runtime only; mutations require a trusted workspace. Advertise a new baseline capability so clients can discover the surface, and add matching SDK client methods. Refs QwenLM#6378. * qwen: address PR review feedback (QwenLM#6638) Align the new extensions controller file's copyright year with the other new files added in this change. * qwen: address PR review feedback (QwenLM#6638) Redact credentials from the extension source on the two success-path fan-outs (session refresh and refresh-failure broadcast), matching the operation record and failure broadcast. Document the non-cancellation semantics of the extension timeout wrapper. * qwen: address PR review feedback (QwenLM#6638) Share the queue-full sentinel message via an exported constant so the throw site (controller) and the 429 match site (routes) cannot drift after the module split. Include the bound workspace in the extension operation log prefixes so concurrent per-workspace controllers are distinguishable in stderr. * feat(cli): add concurrent extension preparation Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): remove redundant extension context build Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): address extension review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): address final review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): address latest review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): reject links in npm extension archives Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): limit npm extension archive downloads Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): address review follow-ups Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): address latest review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): release rejected operation slots Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): address operation review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): align archive handling contracts Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): preserve watcher generation state Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(extensions): align management contracts Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk): bound extension operation polls Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(core): cover forged prepared commits Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(core): assert activation generation increment Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): close archive and polling gaps Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): retry suppressed extension generations Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(cli): cover archive URL extension updates Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk): preserve unbounded operation waits Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): share npm redirect download deadline Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): preserve extension reload diagnostics Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): preserve installed Claude plugin paths Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): return committed activation state Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): preserve extension preparation errors Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): validate extension setting env vars Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): target extension reconciliation Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(cli): cover resultless legacy commit warnings Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): retain suppressed extension generations Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): record legacy runtime reconciliation Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): validate extension clients by runtime Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): record workspace activation refresh Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(cli): stop extension reconcilers after cases Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): resolve global runtimes at reconciliation Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): reconcile newly registered runtimes Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): prevent overlapping runtime reconciliation Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): dispose late runtime apps during shutdown Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): keep projection repair best effort Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): preserve committed store results Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): quarantine corrupt store journals Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): harden npm download redirects Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): address review edge cases Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): honor cancellation between preparation stages Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): retry prepared cleanup failures Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(extensions): cover committed artifact recovery boundary Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): release extension refresh queue on timeout Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix: address extension review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): reconcile extension store compatibility state Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): bound npm redirects and isolate extension tests Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): make extension uninstall store-authoritative Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): defer prepared extension secret mutations Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): validate staged extensions before commit Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): enforce public extension network policy Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): handle extension response failures Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): surface committed refresh warnings Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): guard timer unref calls Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): release commit lane after durable writes Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(serve): update mutation callback assertions Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): refresh live extension instructions Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): address latest review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): address follow-up review findings Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): address remaining activation feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): preserve preparation queue status Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): enforce network request deadlines Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(serve): clarify single-workspace capabilities Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): guard deferred settings commit Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): cancel archive extraction Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): harden refresh recovery Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): serialize extension reconciliation Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): address post-commit review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6825) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6825) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6825) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: fix CI failure on PR QwenLM#6825 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: fix CI failure on PR QwenLM#6825 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6825) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix: address critical PR review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): bound legacy extension update checks Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(acp): deduplicate extension refresh requests Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6825) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6825) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6825) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (QwenLM#6825) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk): update browser bundle budget after main merge Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>



What this PR does
This PR introduces Extension Management V2 for
qwen serveunder the additiveextension_management_v2capability. Installed extension artifacts remain user-level and shared across all hosted workspaces, while activation becomes policy: a global default plus optional exact workspace overrides. The daemon exposes a global catalog and mutation surface for install, update checks, update, uninstall, and default activation, together with workspace-qualified projection, activation, and refresh routes. The TypeScript SDK includes matching catalog, mutation, polling, and workspace-projection helpers.Extension mutations now use a transactional store as the single writer of final extension directories and activation state. Preparation runs outside the final artifact directory, commits are serialized, state advances through a monotonic generation, and journal-based recovery handles interrupted artifact swaps. A daemon-wide scheduler allows up to two concurrent preparation tasks and serializes durable artifact and state writes in preparation-completion order. The commit lane is released immediately after those writes become durable, so runtime refresh and cleanup can continue without blocking the next durable commit. Successful global changes reconcile every hosted runtime, while workspace activation changes reconcile only their target. Runtime reconciliation refreshes extension and skill caches, extension tools, hierarchical memory, active chat system instructions, and available commands. Post-commit refresh failures are surfaced as warnings through daemon operation results, legacy CLI commands, and the TUI without rolling back committed state.
Sensitive user settings now participate in the artifact transaction without placing secrets in the artifact itself. Preparation writes one revisioned secret bundle to the selected secure-storage backend and places only a backend-and-revision selector in the staged artifact. The artifact commit atomically activates that complete bundle; cancellation discards the unselected bundle, a successful commit removes the previously selected bundle, and later legacy per-key synchronization is outside the durable lane. A crash can leave only an unreachable orphan bundle, never a new artifact paired with old or partially written credentials. Post-commit settings or cleanup failures are returned with their
codeanderrorthrough daemon operations and update callers, usingupdated with warnings; only reload or runtime-refresh failures useupdated, needs restart.Daemon shutdown now centralizes runtime-owned cleanup against the actual runtime app, including deferred apps that finish after shutdown has started. Scheduled-task keepalive, workspace Git state, sub-session launchers, extension reconciliation, device flow, ACP, rate limiting, event-loop monitoring, and metrics sampling are disposed idempotently with per-resource error isolation.
The existing
workspace_extensionscapability and/workspace/extensions/*routes remain available as a primary-workspace compatibility adapter. Legacy activation rules are migrated and projected back to the existing enablement format, while the extension watcher observes store generations so other daemon processes converge after external commits.Why it's needed
Extension artifacts are stored globally under one Qwen home even when a daemon hosts multiple workspaces. Treating extension mutations as independent per-workspace operations allows concurrent requests to race against the same artifact directory and can leave other workspaces running stale extension state. Extension Management V2 makes artifact ownership and mutation explicitly global while preserving per-workspace activation, providing a consistent transaction boundary, deterministic serialization, targeted runtime reconciliation, and backward compatibility for existing clients. The follow-up transaction and shutdown changes ensure that committed artifacts select one complete credential revision, post-commit failures remain actionable, and late-created runtime resources cannot outlive daemon shutdown.
Reviewer Test Plan
How to verify
Start a daemon with at least two registered workspaces and confirm that capabilities advertise both
extension_management_v2and the existingworkspace_extensionscompatibility surface. Install a remote extension through/extensions/installwith explicit consent and either user or workspace initial activation, poll the returned operation to completion, and confirm that the artifact appears once in the global catalog while each workspace projection reports the expected default, override, effective activation, desired generation, and applied generation.Exercise default activation, exact workspace activation, clearing a workspace override, update checks, update, uninstall, and explicit workspace refresh. Confirm that global artifact/default changes reconcile all hosted runtimes, workspace activation changes reconcile only the trusted target, reads remain available for an untrusted registered workspace, and workspace mutations reject untrusted or unknown targets. For an extension with a context file, change activation while a session is already running and confirm that the same session's next model turn uses the rebuilt system instruction without the disabled extension context. Confirm that the legacy
/workspace/extensions/*routes continue to work for the primary workspace and expose compatible operation results.Submit overlapping installs or updates and confirm that preparation admits at most two active tasks, durable commits remain single-lane FIFO by preparation completion, the lane releases after artifact and state writes are durable, runtime refresh or cleanup may continue afterward without blocking the next durable commit, queued preparation can be aborted without releasing an active non-cooperative task early, and admission rejects more than ten unfinished operations. Simulate a runtime refresh failure after commit and confirm that the operation finishes with warnings while the committed generation and artifact remain authoritative. Restart after prepared, artifact-swapped, and state-committed transaction states and confirm recovery rolls back before the state commit point and completes cleanup after it.
For an extension with sensitive settings, prepare an update and confirm that the installed extension continues reading the old settings until the artifact is committed, then reads the complete new bundle immediately after commit even before legacy per-key synchronization finishes. Cancel another prepared update and confirm that its unselected bundle is removed; commit a replacement and confirm that the previous bundle and overrides are removed. Force post-commit settings synchronization to fail and confirm that the durable lane is already available to the next commit and that callers receive the exact warning rather than a restart-only status. Start deferred runtime construction, close the daemon before it mounts, allow the late runtime app to finish, and confirm that every runtime producer and monitor is disposed even if one cleanup callback throws.
Focused unit coverage exercises store migration and recovery, activation precedence, concurrent store instances, stale-update conflicts, scheduler behavior and durable-commit lane release, revisioned settings activation and cleanup, late-runtime shutdown, route trust and reconciliation, watcher convergence, committed-warning propagation through CLI and TUI surfaces, direct extension TUI actions, SDK paths, and operation polling. Build, typecheck, targeted package tests, and a bundled two-workspace daemon smoke were run during development.
Evidence (Before & After)
Before: If an extension mutation committed successfully but runtime refresh failed, CLI and TUI callers could report only success or suppress the refresh error, leaving users without a clear indication that the persisted state and running runtime had diverged.
After: The durable mutation remains authoritative, while daemon operation results, legacy CLI commands, automatic updates, and TUI extension actions display the exact post-commit warning. Settings-only and cleanup warnings use
updated with warnings, while reload/runtime failures retain the restart guidance.Tested on
Environment (optional)
Node.js 22, Vitest package suites, repository build and typecheck, and a local bundled
qwen servetwo-workspace smoke.Risk & Scope
extension_management_v2is additive, the existing capability and primary-workspace routes remain supported, and existing enablement rules are migrated with a backward-compatible projection.Linked Issues
Refs #6378. Builds on #6567.
中文说明
这个 PR 做了什么
本 PR 在新增的
extension_management_v2capability 下,为qwen serve引入 Extension Management V2。已安装扩展的 artifact 仍然是用户级资源,并由所有托管 workspace 共享;activation 则作为策略管理:一个全局默认值,加上可选的精确 workspace override。daemon 新增全局 catalog 与 mutation 接口,用于安装、检查更新、更新、卸载和默认 activation,同时提供按 workspace 限定的 projection、activation 与 refresh 路由。TypeScript SDK 补充了对应的 catalog、mutation、轮询和 workspace projection 辅助方法。扩展 mutation 现在由事务式 store 统一写入最终扩展目录和 activation 状态。准备阶段在最终 artifact 目录之外执行,commit 串行化,状态通过单调递增的 generation 推进,并通过 journal 恢复被中断的 artifact 交换。daemon 级调度器允许最多两个并发准备任务,并按照准备完成顺序串行执行 artifact 与状态的持久化写入。这些写入持久化后会立即释放 commit lane,因此 runtime refresh 和清理可以继续执行,而不会阻塞下一次持久化 commit。成功的全局变更会 reconcile 所有托管 runtime,workspace activation 变更只 reconcile 目标 runtime。runtime reconciliation 会刷新 extension 与 skill cache、extension tool、分层 memory、运行中 chat 的 system instruction 以及可用命令;commit 后的 refresh 失败会通过 daemon operation 结果、旧 CLI 命令和 TUI 显式展示为 warning,不会回滚已提交状态。
敏感的用户设置现在也参与 artifact 事务,但 secret 本身不会写入 artifact。准备阶段会向选定的安全存储后端写入一个带 revision 的完整 secret bundle,并只在 staged artifact 中放入后端与 revision selector。artifact commit 会原子激活这个完整 bundle;取消操作会删除未选中的 bundle,成功 commit 会删除之前选中的 bundle,而旧版逐 key 同步则在 durable lane 之外执行。进程崩溃最多留下一个不可达的孤立 bundle,不会出现新 artifact 搭配旧凭据或部分写入凭据的状态。commit 后的设置或清理失败会通过 daemon operation 和更新调用方返回原始
code与error,并使用updated with warnings;只有 reload 或 runtime refresh 失败才使用updated, needs restart。daemon shutdown 现在会针对实际 runtime app 统一清理 runtime 所有资源,包括 shutdown 开始后才完成构建的 deferred app。scheduled-task keepalive、workspace Git state、sub-session launcher、extension reconciliation、device flow、ACP、rate limiting、event-loop monitor 和 metrics sampler 都会幂等释放,并且每项清理互相隔离错误。
现有的
workspace_extensionscapability 和/workspace/extensions/*路由继续作为 primary workspace 的兼容适配层保留。旧 activation 规则会被迁移,并投影回原有 enablement 格式;扩展 watcher 观察 store generation,使其他 daemon 进程能够在外部 commit 后收敛。为什么需要
即使一个 daemon 托管多个 workspace,扩展 artifact 仍然存放在同一个 Qwen home 下。若把扩展 mutation 当作相互独立的 workspace 操作,并发请求就可能竞争同一个 artifact 目录,并让其他 workspace 继续运行过期的扩展状态。Extension Management V2 明确区分全局 artifact 所有权和 mutation 与按 workspace activation,在保持现有客户端兼容性的同时,提供一致的事务边界、确定性的串行化、针对性的 runtime reconciliation。后续的事务与 shutdown 调整进一步保证已提交 artifact 只选择一个完整凭据 revision、commit 后失败保持可操作性,并阻止晚创建的 runtime 资源在 daemon shutdown 后继续存活。
Reviewer Test Plan
如何验证
启动一个注册了至少两个 workspace 的 daemon,确认 capabilities 同时声明
extension_management_v2和既有的workspace_extensions兼容接口。通过/extensions/install安装一个远程扩展,显式提供 consent,并选择 user 或 workspace 初始 activation;轮询返回的 operation 直到完成。确认 artifact 在全局 catalog 中只出现一次,同时每个 workspace projection 都返回预期的默认值、override、有效 activation、目标 generation 和已应用 generation。验证默认 activation、精确 workspace activation、清除 workspace override、检查更新、更新、卸载和显式 workspace refresh。确认全局 artifact 或默认值变更会 reconcile 所有托管 runtime,workspace activation 变更只 reconcile 受信任的目标;未受信任但已注册的 workspace 仍可读取状态,而针对未受信任或未知目标的 workspace mutation 会被拒绝。对于带 context file 的扩展,在 session 已经运行时修改 activation,确认同一个 session 的下一次模型调用会使用重建后的 system instruction,且不再包含已禁用扩展的上下文。确认旧
/workspace/extensions/*路由继续对 primary workspace 生效,并返回兼容的 operation 结果。提交重叠的安装或更新操作,确认准备阶段最多允许两个活跃任务,持久化 commit 始终保持单 lane 并按准备完成顺序执行;artifact 与状态写入持久化后 lane 会立即释放,后续 runtime refresh 或清理可以继续执行且不会阻塞下一次持久化 commit;排队中的准备任务可以被 abort,而正在运行但不响应 abort 的任务不会提前释放槽位;超过十个未完成 operation 时 admission 会拒绝新请求。模拟 commit 后的 runtime refresh 失败,确认 operation 以 warning 完成,已提交 generation 和 artifact 仍是权威状态。分别在 prepared、artifact-swapped 和 state-committed 事务状态后重启,确认恢复逻辑在状态 commit 点之前执行回滚,在 commit 点之后完成清理。
对于带敏感设置的扩展,prepare 一次更新并确认已安装扩展在 artifact commit 前仍读取旧设置;commit 后即使旧版逐 key 同步尚未完成,也会立即读取完整的新 bundle。取消另一个 prepared update 并确认其未选中 bundle 被删除;commit 替换版本并确认旧 bundle 与 override 被删除。强制 commit 后设置同步失败,确认 durable lane 已可供下一次 commit 使用,而且调用方拿到准确 warning,而不是只有 restart 状态。启动 deferred runtime 构建,在其 mount 前关闭 daemon,再允许晚到的 runtime app 完成,确认即使某个 cleanup callback 抛错,所有 runtime producer 与 monitor 仍被释放。
聚焦单测覆盖 store 迁移与恢复、activation 优先级、并发 store 实例、过期更新冲突、调度行为与持久化 commit lane 释放、带 revision 的设置激活与清理、晚到 runtime shutdown、路由 trust 与 reconciliation、watcher 收敛、committed warning 在 CLI/TUI 界面的传播、扩展 TUI action 的直接交互、SDK 路径和 operation 轮询。开发过程中已运行构建、类型检查、目标 package 测试以及打包后的双 workspace daemon smoke。
证据(Before & After)
Before: 扩展 mutation 已成功提交但 runtime refresh 失败时,CLI 和 TUI 调用方可能只报告成功或吞掉 refresh 错误,用户无法明确得知持久化状态与正在运行的 runtime 已出现偏差。
After: 持久化 mutation 仍然是权威状态,同时 daemon operation 结果、旧 CLI 命令、自动更新和 TUI 扩展操作会显示准确的 post-commit warning。仅设置或清理 warning 使用
updated with warnings,reload/runtime 失败继续保留 restart 引导。测试平台
环境(可选)
Node.js 22、Vitest package 测试、仓库构建与类型检查,以及本地打包后的
qwen serve双 workspace smoke。风险与范围
extension_management_v2是增量 capability,既有 capability 和 primary workspace 路由继续受支持,现有 enablement 规则会迁移并保留向后兼容的 projection。关联 Issue
Refs #6378。基于 #6567。