Skip to content

feat(serve): ACP/REST parity wave 2 — agents CRUD (5 methods)#4737

Closed
chiga0 wants to merge 31 commits into
feat/acp-rest-parity-wave1from
feat/acp-rest-parity-wave2
Closed

feat(serve): ACP/REST parity wave 2 — agents CRUD (5 methods)#4737
chiga0 wants to merge 31 commits into
feat/acp-rest-parity-wave1from
feat/acp-rest-parity-wave2

Conversation

@chiga0

@chiga0 chiga0 commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Add 5 `_qwen/workspace/agents/*` CRUD methods to the ACP dispatch, completing full REST parity. After wave 1 + wave 2, all REST routes are reachable via `/acp`.

Depends on: #4736 (wave 1)

Methods Added (5)

Method REST Equivalent
`_qwen/workspace/agents/list` `GET /workspace/agents`
`_qwen/workspace/agents/get` `GET /workspace/agents/:agentType`
`_qwen/workspace/agents/create` `POST /workspace/agents`
`_qwen/workspace/agents/update` `POST /workspace/agents/:agentType`
`_qwen/workspace/agents/delete` `DELETE /workspace/agents/:agentType`

Implementation

  • `SubagentManager` created in `AcpDispatcher` constructor via `createDaemonSubagentManager(boundWorkspace)`
  • `SubagentError` mapped to `INVALID_PARAMS` in `toRpcError`
  • Workspace events (`agent_changed`) broadcast on create/update/delete
  • `toSummary`/`toDetail` exported from `workspaceAgents.ts` for reuse
  • `buildInitializeResult` updated to advertise all 5 agent methods

ACP/REST Parity Status (after wave 1 + 2)

Category ACP Methods Coverage
Standard ACP (initialize, session/*, permission) 12 100%
Session extensions 6 100%
Memory 2 100%
Files 7 100%
Auth 4 100%
Workspace (MCP, tools, init, etc.) 10 100%
Agents 5 100%
Total 46 ~100%

Remaining: global permission vote (P4, architectural difference, not a gap).

Test Plan

  • Existing transport tests cover FakeBridge agent stubs
  • `npm run build` passes
  • Manual: CRUD agents via `/acp` JSON-RPC

doudouOUC added 29 commits June 3, 2026 16:58
issue #4542 路径 2 的完整实施设计,包含:
- L2 分层架构与边界规则
- callback 注入跨切依赖(不抽共享 infra)
- facade + 4 子服务内部结构
- WorkspaceRequestContext 最小集
- AcpSessionBridge 瘦身与改名
- /acp northbound ext methods(qwen/workspace/... 命名空间)
- 文件变更清单与测试策略
- Remove listWorkspaceSessions/heartbeat from migration list (they
  access bridge-internal session map, must stay in bridge)
- Add readBytes to FileService (GET /file/bytes route exists)
- Fix auth routes to match actual device-flow pattern with flowId
- Add agents.get (GET /workspace/agents/:agentType)
- Add §8 SDK compat + /acp error format specification
- Update /acp method table with corrected routes
- Change splitting principle from "child dependency" to "scope-based"
  (workspace-scoped → service, session-scoped → bridge)
- Move 8 workspace methods out of bridge (was 1):
  initWorkspace, setWorkspaceToolEnabled, 5 status getters, restartMcpServer
- Bridge exposes queryWorkspaceStatus + invokeWorkspaceCommand as
  new public methods for service to delegate child calls through
- Fix originatorClientId to optional (matches existing AuditContext)
- Add sessionId to WorkspaceRequestContext for audit correlation
- Add 8 more /acp northbound methods (tool/toggle, 5 status, mcp/restart)
- Update facade interface, deps, and file change list accordingly
…us, memory CRUD, fix callback sigs, add missing deps
…tus + invokeWorkspaceCommand

Remove 8 workspace-scoped methods (initWorkspace, setWorkspaceToolEnabled,
getWorkspaceMcpStatus, getWorkspaceSkillsStatus, getWorkspaceProvidersStatus,
getWorkspaceEnvStatus, getWorkspacePreflightStatus, restartMcpServer) that have
been migrated to DaemonWorkspaceService. Add two generic delegation methods
(queryWorkspaceStatus, invokeWorkspaceCommand) that the service uses to
forward requests through the live ACP channel.
Rename the interface and factory to better reflect their role as
session-scoped bridge handles rather than HTTP-specific constructs.

- HttpAcpBridge interface → AcpSessionBridge (+ deprecated type alias)
- createHttpAcpBridge → createAcpSessionBridge (+ deprecated re-export)
- httpAcpBridge.ts → acpSessionBridge.ts (git mv for history)
- Updated all imports and type annotations across cli/serve
- Updated error messages and JSDoc references
- Deprecated aliases preserve backward compatibility
…init routes

Rewire workspace-scoped REST routes in server.ts to delegate through
DaemonWorkspaceService instead of calling bridge methods directly.
The workspace service is constructed in runQwenServe.ts (production)
and injected into createServeApp; a default is also constructed
inside createServeApp for tests/embeds that don't inject one.

Routes rewired:
- GET /workspace/mcp
- GET /workspace/skills
- GET /workspace/providers
- GET /workspace/env
- GET /workspace/preflight
- POST /workspace/init
- POST /workspace/tools/:name/enable
- POST /workspace/mcp/:server/restart

The persistDisabledTools callback is extracted into a named binding
in runQwenServe.ts so both the bridge (backward compat) and the
workspace service share the same settings-lock implementation.
… deviceFlowRegistry

- getWorkspaceEnvStatus now uses statusProvider.getEnvStatus() directly
  (daemon-local, no ACP query) instead of routing through queryWorkspaceStatus
- getWorkspacePreflightStatus stitches daemon cells from
  statusProvider.getDaemonPreflightCells() with ACP cells from
  queryWorkspaceStatus, matching the old bridge's behavior
- Made deviceFlowRegistry and subagentManager optional in
  DaemonWorkspaceServiceDeps to eliminate the undefined-as-never crash
- Added statusProvider and isChannelLive deps to DaemonWorkspaceServiceDeps
- runQwenServe.ts now passes the real statusProvider and a channel
  liveness callback derived from bridge.sessionCount
- Rewrote daemonStatusProvider.test.ts to exercise the workspace service
  facade instead of removed bridge methods
…factoring

Add queryWorkspaceStatus and invokeWorkspaceCommand to the fake bridge
so DaemonWorkspaceService can be constructed internally by createServeApp.

- queryWorkspaceStatus dispatches to the correct impl based on method
  name (mcp, skills, providers, preflight)
- invokeWorkspaceCommand dispatches for MCP restart commands
- Tool toggle tests updated: workspace service now calls persistDisabledTools
  directly, so bridge.setToolEnabledCalls assertions are replaced with
  HTTP response body assertions
- Init workspace tests rewritten to use real temp directories since the
  workspace service now performs filesystem operations directly
- Env/preflight status tests updated to match idle fallback behavior
  (workspace service uses statusProvider, not queryWorkspaceStatus for env)
- MCP restart client-identity test simplified (originatorClientId flows
  through workspace request context, not invokeWorkspaceCommand params)
…ity, timeout, runtime guards

- bridge.ts: return response as T (not any), stop injecting cwd into invokeWorkspaceCommand
- agentsService: rename agent_created/updated/deleted → agent_changed with change/name/level data; add collision preflight before createSubagent
- memoryService: rename memory_written/deleted → memory_changed matching workspaceMemory.ts
- restartMcpServer: add 300s timeout, translate errorKind into McpServerNotFoundError/McpServerRestartFailedError, replicate pool-mode per-entry event fan-out
- initWorkspace: force-overwrite uses O_WRONLY|O_NOFOLLOW + fd-based truncate instead of fs.writeFile; add post-open parent re-verification on both create and overwrite paths
- getWorkspaceEnvStatus: hoist acpChannelLive before statusProvider check; add writeStderrLine in catch
- getDaemonPreflightCells: add writeStderrLine in catch
- preflight error cell: include errorKind via mapDomainErrorToErrorKind
- auth/agents sub-service creation: Proxy-based stub when deps are undefined, throws descriptive error at call time
- Update all test assertions to match new event names and data shapes
…and, fix collision preflight, update stale import
…paceService

The ACP dispatch handler called 8 workspace methods that were removed
from the bridge interface during the workspace service extraction. Inject
DaemonWorkspaceService into AcpDispatcher and route workspace status
queries and mutations through it, matching the REST surface in server.ts.
bridge.ts no longer reads opts.contextFilename — the consuming code
moved to DaemonWorkspaceService which receives its own copy. Remove
the dead field from BridgeOptions and the dead argument spread in
runQwenServe.ts.
…dings

chiga0 R1 flagged that the File/Auth/Agents/Memory sub-services are built
but never wired into any production route — server.ts/dispatch.ts call
them 0 times, the old workspaceMemory.ts/workspaceAgents.ts still serve
all traffic, and auth/agents are constructed with `undefined` deps behind
throwing Proxy stubs. Rather than ship a dead, partially-broken layer
alongside 1774 lines of live duplicate logic, drop it from this PR and
land it together with the route wiring + e2e tests in a follow-up (R1
option b).

Scope removed (deferred to wiring PR):
- fileService/authService/agentsService/memoryService + their tests
- validation.ts (only used by the sub-services)
- file/auth/agents/memory members on the facade + their interfaces
- fsFactory/deviceFlowRegistry/subagentManager/knownClientIds deps
  (no longer consumed by the facade once sub-services are gone)

Design doc: add an implemented-vs-deferred scope table so it no longer
claims route/ACP parity that hasn't landed (chiga0 R1 #2 / design.md).

Live-code review fixes (facade + bridge stay wired):
- bridge: add isChannelLive() wrapping liveChannelInfo(); wire the
  workspace service's acpChannelLive through it instead of
  `sessionCount > 0`, which mis-reports during the cold-spawn /
  post-kill windows (wenshao).
- bridge: remove the dead `persistDisabledTools` local + BridgeOptions
  field + the createAcpSessionBridge arg — unread after the workspace
  methods moved out (fixes TS6133 build blocker; wenshao).
- bridge: restore `MCP_RESTART_TIMEOUT_MS` (300s); still consumed by the
  runtime MCP add/remove paths the base branch added.
- bridge: drop the dangling contextFilename comment fragment.
- workspace-service: fix stale "Mirrors bridge.ts" JSDoc on
  canonicalizeExistingAncestor.
…ve, mock alignment, cleanup

Code fixes:
- initWorkspace: add !lst.isFile() guard to reject FIFOs/sockets/device
  nodes (DoS via libuv thread pool exhaustion on blocking read)
- server.ts fallback: wire isChannelLive so direct createServeApp
  consumers get accurate acpChannelLive
- dispatch.ts: fix stray space before comma (Prettier)
- bridgeTypes.ts: remove orphaned JSDoc fragment from conflict resolution
- server.test.ts: add isChannelLive() + opts param to fakeBridge mock
  (matches updated AcpSessionBridge interface)
- integration.test.ts: add isChannelLive/queryWorkspaceStatus/
  invokeWorkspaceCommand to minimalBridge fake

Test coverage migration (TOCTOU/error-translation/pool-mode/bridge
delegation) acknowledged and agreed — tracked as next step.
Migrates the test coverage wenshao flagged across 5 review rounds:

restartMcpServer (7 new tests):
- mcp_server_restart_refused event on restarted:false
- mcp_server_not_found errorKind → McpServerNotFoundError
- mcp_restart_failed errorKind → McpServerRestartFailedError
- non-errorKind errors re-thrown without translation
- pool-mode entries fan-out (per-entry events)
- malformed pool entries skipped without crash

initWorkspace (3 new tests):
- FIFO/non-regular-file guard (mkfifo + !isFile())
- EEXIST conflict guard (content exists, force not set)
- parent-symlink-outside-workspace rejection

Total: 23 → 32 tests in facade.test.ts.
@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

📋 Review Summary

This PR adds 5 ACP HTTP dispatch methods for workspace agent CRUD operations (_qwen/workspace/agents/*), completing REST parity for agent management. The implementation requires refactoring Express-coupled validation functions in workspaceAgents.ts into pure functions, then adding dispatch cases in dispatch.ts. This is a well-scoped follow-up to wave 1 (#4736) with a clear implementation plan.

🔍 General Feedback

  • Architecture approach is sound: Extracting pure validation functions is the right pattern for sharing logic between REST and ACP dispatch layers
  • Good dependency management: The PR correctly identifies wave 1 (feat(serve): ACP/REST parity wave 1 — session extensions + memory + files + auth (20 methods) #4736) as a hard dependency that must merge first
  • Comprehensive test plan: The test matrix covers happy-path, validation errors, and regression testing for existing REST routes
  • Estimated LOC is reasonable: ~660 lines total with ~300 for tests shows appropriate test coverage investment

🎯 Specific Feedback

🔴 Critical

Implementation not yet present: This PR currently contains only a placeholder commit with no code changes. The following critical items must be addressed before merge:

  1. Validation function refactoring must be completed: The three validation functions (parseAgentConfig, parseAgentUpdates, sanitizeRunConfig) at lines 812, 967, and 1225 in workspaceAgents.ts currently return undefined/null and call res.status().json() directly. These must be converted to pure functions returning result objects:

    // Current (Express-coupled):
    function parseAgentConfig(body, level, res): SubagentConfig | undefined
    
    // Required (pure):
    function parseAgentConfig(body, level): { ok: true; data: SubagentConfig } | { ok: false; error: string; code: string }
  2. REST route adapters needed: After extracting pure validators, the existing REST routes must wrap them with Express-specific response handling. Ensure no behavior changes occur during this refactoring.

  3. ACP dispatch implementation: The 5 case branches must be added to dispatch.ts handling method:

    • _qwen/workspace/agents/list
    • _qwen/workspace/agents/create
    • _qwen/workspace/agents/get
    • _qwen/workspace/agents/update
    • _qwen/workspace/agents/delete

🟡 High

Design considerations for implementation:

  1. Error code mapping strategy: When refactoring validators, ensure the error codes (invalid_config, agent_already_exists, file_error, etc.) are preserved exactly so existing REST clients don't break. The ACP layer will need to map these to JSON-RPC error codes.

  2. Validation function signatures: Consider this result type pattern for consistency with existing codebase patterns:

    interface ValidationResult<T> {
      ok: boolean;
      data?: T;
      error?: string;
      code?: string;
    }
  3. Agent name validation consistency: The AGENT_TYPE_PATTERN and length constants (lines 33-50) should be shared between REST route validation and ACP dispatch validation to prevent divergence.

  4. Builtin agent collision check: The parseAgentConfig function's builtin agent name check (lines 867-875) must be preserved in the refactored version. This prevents creating agents that shadow built-in subagents.

  5. Race condition handling: The create route's post-write reload check (lines 174-194) logs a specific error format when create succeeds but reload fails. This pattern should be replicated in the ACP dispatch path.

🟢 Medium

Implementation suggestions:

  1. Extract constants to shared module: The validation constants (AGENT_TYPE_PATTERN, AGENT_TYPE_MIN_LENGTH, AGENT_TYPE_MAX_LENGTH, MAX_DESCRIPTION_BYTES, etc.) are defined at the top of workspaceAgents.ts. Consider whether these should be exported for reuse in the ACP dispatch layer or kept private to the REST routes.

  2. Consistent error shape: The validators return different error shapes:

    • parseAgentConfig returns undefined on error
    • parseStringArray returns null on error
    • sanitizeRunConfig returns null on error

    Unify these to use the same result-object pattern for easier consumption by both REST and ACP layers.

  3. Test coverage for edge cases: Ensure tests cover:

    • Agent names at boundary lengths (2 chars, 64 chars)
    • Names with Unicode letters/numbers (per AGENT_TYPE_PATTERN)
    • Payloads exceeding byte limits
    • Collision detection at both project and user levels
    • Case-insensitive name resolution

🔵 Low

Nice-to-have improvements:

  1. Consider extracting parseStringArray and rejectIfPresentWrongType: These utility functions (lines 1085-1117 and 1119-1134) are generic and could be reused elsewhere. They're already pure functions, so they just need to be exported.

  2. Documentation for ACP method signatures: Add JSDoc comments to each ACP dispatch case documenting:

    • Required/optional parameters
    • Response shape
    • Error codes that may be returned
    • Parity with REST endpoint
  3. Consider a shared validation module: If wave 3+ adds more REST/ACP parity methods, a dedicated packages/cli/src/serve/validators/ directory could house the pure validation functions.

✅ Highlights

  • Excellent defensive patterns: The route-layer validation for :agentType URL parameters (lines 33-50) provides defense-in-depth against malformed names before they reach SubagentManager
  • Thoughtful error disclosure: The FILE_ERROR handling (lines 207-223) correctly gates detailed filesystem paths behind QWEN_SERVE_DEBUG to avoid leaking operator filesystem layout
  • Comprehensive field validation: The validation functions check type, length, byte size, and semantic constraints (e.g., approvalMode must be in APPROVAL_MODES)
  • Good audit trail: The agent_changed event publication (line 195) ensures clients are notified of mutations
  • Proxy-based Config stub: The createDaemonSubagentManager function (lines 1330-1370) uses a Proxy to catch any future SubagentManager dependency creep on unimplemented Config methods

Pre-Merge Checklist

Before this PR can be reviewed for implementation quality:

  • Push actual implementation code (currently placeholder only)
  • Refactor parseAgentConfig, parseAgentUpdates, sanitizeRunConfig to pure functions
  • Update REST routes to wrap pure validators with Express adapters
  • Add 5 ACP dispatch cases in dispatch.ts
  • Write tests for each method (happy path + validation errors)
  • Verify existing REST agent tests still pass
  • Run npm run build and npm test successfully
  • Manual verification: CRUD agents via /acp JSON-RPC, verify parity with REST

…dening

Add 24 `_qwen/*` extension methods to the ACP HTTP dispatch, achieving
near-complete REST parity for the `/acp` transport. After this PR,
ACP-native clients can access session extensions, workspace memory,
file operations, auth device-flow, tools, and MCP management.

Methods added:
- Session (6): recap, btw, shell, detach, context_usage, tasks
- Memory (2): workspace/memory (read), workspace/memory/write
- Files (7): file/{read,read_bytes,stat,list,glob,write,edit}
- Auth (4): workspace/auth/{status,device_flow/start,get,cancel}
- Workspace (5): tools, mcp/tools, mcp/servers/{add,remove}, sessions/delete

Production hardening:
- Protocol compliance: 415 (bad Content-Type), 406 (bad Accept),
  501 (batch JSON-RPC), 404 (unknown connection, was 400)
- Error mapping: FsError (13 kinds), WorkspaceMemoryFileTooLarge/WriteTimeout,
  TooManyActiveDeviceFlows, UnsupportedDeviceFlowProvider, UpstreamDeviceFlow,
  McpServerNotFound — all with structured errorKind in JSON-RPC data
- Input validation: BTW_MAX_INPUT_LENGTH (4096), memory content 1MB limit,
  scope/mode reject-on-invalid (was silent default)
- buildInitializeResult advertises all new methods

Files changed:
- dispatch.ts: +600 lines (24 case branches + toRpcError extensions)
- index.ts: +30 lines (415/406/501/404 protocol checks)
- server.ts: +5 lines (wire fsFactory + deviceFlowRegistry)
- workspaceMemory.ts: export collectWorkspaceMemoryStatus
- transport.test.ts: +257 lines (15 test cases)

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@chiga0 chiga0 force-pushed the feat/acp-rest-parity-wave1 branch from 8ec3d77 to c4d9668 Compare June 4, 2026 13:55
@chiga0

chiga0 commented Jun 7, 2026

Copy link
Copy Markdown
Collaborator Author

Agents CRUD methods merged into the unified PR #4827 (wave 1+2 combined).

@chiga0 chiga0 closed this Jun 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants