Repository navigation
fix(hire): reject employee-level MCP grants no bundled engine can honor - #319
Conversation
PeterGuy326
left a comment
There was a problem hiding this comment.
Request changes on current head 1640e3e.
The server-side fail-closed gate and registry-derived capability check are technically sound, and the hosted CI is green. The remaining blocker is scope/acceptance: this PR says Closes #314, but the issue acceptance criteria also require the CapabilityPicker MCP section to be disabled or persistently annotated while no bundled host supports employee-level MCP. The current diff only adds submit-time failure copy in HireDrawer; it explicitly leaves the picker unchanged, so users can still select and bind a grant before receiving the error.
Please either (a) add the chosen Option-A picker gate/warning using the registry-backed capability state, with focused renderer coverage, or (b) split/retarget this as the server-half implementation, remove Closes #314, and link a follow-up that owns the picker acceptance criterion. After that, update the branch onto current main before requesting re-review; it is currently BEHIND main.
|
PeterGuy326 的 CHANGES_REQUESTED 按 option (b) 处理:
|
平台组 Review(崔泽生,assigned by @冯浩然)总体评价定位精准、实现扎实的 server-half 修复。hire 和 profile-edit 两个入口同时加 fail-closed MCP grant 拒绝逻辑,把"创建成功但每个 turn 静默失败"的缺陷前移到提交时刻。9 文件 +131/-24 diff 干净,无夹带。CI 11/11 全绿。 结论:LGTM。server 侧逻辑可以 approve。 1. MCP 拒绝逻辑——正确
2. 一个设计不对称(非 blocker,建议记 note)hire gate 用 当前(无 Host 声明 MCP)不是 bug,只是保守行为。建议:在 3. 错误文案——清晰可操作
4. 测试覆盖——充分
5. 安全分析——无 bypass 路径
6. 合并阻塞PR 当前 建议(非 blocker)
|
6da57ff to
031ea7d
Compare
|
Follow-up to PeterGuy326 CHANGES_REQUESTED (option b): The previous head still had New head Branch is on current |
031ea7d to
bce8f35
Compare
|
Correction on the previous note: GitHub still treated |
Every bundled engine spawns with an empty MCP config, so a granted binding survived hire and then failed every turn at spawn time (qoder.mcp_binding_unsupported), away from its cause (#314). Gate the grant at the two surfaces that accept it: - POST /hire returns 422 hire_mcp_unsupported before staging or the engine validator sees the request. - PATCH /positions/:id/profile rejects a well-formed grant with 400 position_profile_invalid while keeping the empty-grant clearing path open for pre-existing packages. Both gates read the agent registry instead of a hardcoded list, so a host that ships MCP support unlocks its own grants with no further edits here. The renderer maps the new failure code to explicit zh/en copy. Refs #314. Picker disable / persistent warning remains on follow-up PR #320; this change is the server-half only.
bce8f35 to
f870756
Compare
|
Rebased onto current main |
PeterGuy326
left a comment
There was a problem hiding this comment.
Re-review approve on head f870756.
This takes the option (b) path from my earlier review on 1640e3e: the PR is now the server-half only — hire returns 422 hire_mcp_unsupported fail-closed before staging or engine adjudication, and the profile editor refuses new grants while keeping the explicit empty-grant unbind path. Capability answers come from the registry (agentHostSupportsEmployeeMcp / anyAgentHostSupportsEmployeeMcp), not a copied list. The Closes keyword is gone from body and commit (closingIssuesReferences is empty, #314 stays open), and the picker acceptance criterion is owned by follow-up #320. Unknown and malformed host ids fail closed rather than throwing, and the boundary matrix plus the new hire/profile tests cover the refusal paths. Branch is current with main and hosted CI is green.
|
Node 24 / ubuntu-latest failed on Cannot self-merge. Still |
Tracking record
Refs #314.
Picker/UI acceptance (disable or annotate CapabilityPicker while no bundled host supports employee-level MCP) is owned by follow-up PR #320. This PR is the server-half only and must not auto-close the issue.
Summary
The hire flow and the profile editor both accept employee-level MCP grants, but no bundled engine honors them: every adapter spawns with
--strict-mcp-config --mcp-config '{"mcpServers":{}}', so a granted binding survives creation and then fails every turn at spawn time (qoder.mcp_binding_unsupported), far away from its cause. #314 records the full diagnosis.This PR is the server-half of option A: reject at the two server surfaces that accept a grant, with copy that names the limitation:
POST /hirereturns422 hire_mcp_unsupportedbefore staging, before the engine validator, and before any filesystem effect.PATCH /positions/:id/profilerejects a well-formed grant with400 position_profile_invalid, while an explicit empty grant list (mcpServers: []) stays allowed so pre-existing packages keep a manual unbind workaround.Both gates answer from the agent registry (
agentHostSupportsEmployeeMcp/anyAgentHostSupportsEmployeeMcp) instead of a hardcoded list, so a future host that declares the"mcp"capability unlocks its own grants with no further edits here. The renderer maps the new failure code to explicit zh/en copy in the hire drawer.Intentionally out of scope: disabling or annotating the CapabilityPicker's MCP section at bind time (the AC's UI half). The renderer has no registry data source today; shipping one is a renderer-surface change that deserves its own PR. Until then, a grant is refused at submit time with copy that says what to do (unbind the connectors and retry).
Acceptance criteria
mcpServersgrant is rejected at hire time with copy that names the limitation (A)routes/hire.tsguard returns422 hire_mcp_unsupportedbefore staging;HireDrawer.tsxmaps the code to zh/en copy naming the limitation; regression test inapps/server/test/hire.test.tshire.test.ts/position-profile.test.tsnow assert the no-grant package shape (emptymcpTools, noentrypoints.mcp, emptymcp.jsonservers) and pass unchanged in meaningagentHostSupportsEmployeeMcp/anyAgentHostSupportsEmployeeMcp, which answer fromHOST_DEFINITIONS[...].capabilities; regression testagent-registry.test.tsasserts the registry-derived answersValidation ledger
npm run buildnpm run typecheck:uinpm run typecheck:renderernode --test hire.test.js position-profile.test.js agent-registry.test.js(dist)node --test "apps/server/dist/test/*.test.js"qoder-enginecodex run,review-group-persistenceatomic-write 500,turn-drivertrust probe,workbuddy-health-drivernode CSPRNG crash) in files this PR does not touch; main418bb6c4CI is fully green (10/10 checks), so these are local-environment failures, not regressionsnpm run test:rendereri18n.test.tsxwith the newhire.errMcpCapabilitykey present in both localesGET /git/treesrecursive compares of the published commit tree: 9 modified, 0 added, 0 removed — first against base418bb6c4, then again after rebasing ontoeb01db29(two commits landed on main mid-flight; they touchhttp.ts/server.ts/ CHANGELOG / docs / one unrelated test, zero overlap with the 9 paths here)Environment note: validated on Windows x64. The suite has a small set of pre-existing Windows-environment baseline failures (missing real engine CLIs, POSIX-signal tests, macOS-helper tests); V5 compares against that baseline rather than demanding a fully green run, and main's own CI on the base commit is fully green.
Tests and coverage
apps/server/test/hire.test.ts: the fixture hire no longer carries an MCP grant; the bundled-engine gate test now asserts the no-grant package shape (emptymcpTools, no MCP entrypoint, empty grant list, emptymcp.json, position-cardcapabilities.mcpServers: []); new fail-closed test — an MCP grant is refused422 hire_mcp_unsupportedwith zero engine validator calls, zero apply calls, and no staged skeleton.apps/server/test/position-profile.test.ts: the re-grant fixture drops the MCP grant; the bundled-engine end-to-end test asserts the no-grant package shape; the old "dropping every MCP grant" test is rewritten as "an MCP grant is refused fail-closed while an explicit empty grant list stays allowed" (refused patch leaves the package byte-identical; the clearing patch still returns 200); boundary matrix gains a "MCP grant with no supporting engine" case (well-formed grant,400 position_profile_invalid).apps/server/test/agent-registry.test.ts: new test — the employee-MCP capability gate answers from the registry, not a list copy (all six hosts reportfalse,anyAgentHostSupportsEmployeeMcp()isfalse, unknown ids fail closed).Change classification
Risk and rollback
mcpServersgrant; the default hire path (no MCP) and every existing package are untouched. The clear-grant profile patch keeps working.Breaking or security notes
Behavior change, not an API break: one previously-accepted (but guaranteed-to-fail-at-runtime) request shape is now refused at submit time with
422 hire_mcp_unsupported/400 position_profile_invalid. No security impact. No migration needed.Author checklist
GOVERNANCE.md.Reviewer notes
agent-registry.ts: the gates read the registry rather than a hardcoded engine list, so they are self-disarming once a host declares"mcp". Please scrutinizeagentHostSupportsEmployeeMcp's fail-closed behavior for unknown host ids.position-profile.test.tscarries the largest test adaptation: the old grant-flow assertions (grant → package entrypoint restored) now describe a request shape the API refuses, so they are rewritten as refusal + clearing-path assertions. If you'd rather keep the grant-flow coverage behind a registry override, that is a reasonable ask — the grant pipeline itself (derivePermissionArtifacts) is intentionally left intact.npm run build && node --test apps/server/dist/test/hire.test.js apps/server/dist/test/position-profile.test.js apps/server/dist/test/agent-registry.test.js.