You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The safe capability allow-list is encoded three times (SAFE_BETA_CAPABILITY_PATTERNS at mcp-execution-design.ts:7-18, the regex at filesystem-grants.ts:41, and MCP_CATALOG[id].runtime.capabilities at catalog.ts:18,37 — the ostensible source of truth, never read by admission). There is no explicit "deferred live MCP" capability class, so a deferred GitHub write is reported with the generic "outside the allowed beta scope" string. There is no normalized decision object any surface can share.
This slice creates the contract and shared primitives that the rest of the epic (S2–S6) consumes. It does not yet migrate the callers (that is S2).
Desired Outcome
A single admission decision (McpAdmissionDecision) and a single set of capability primitives exist, sourced from the MCP catalog, ready for every surface to consume. deferred_live_mcp is a first-class, named capability class and admission mode.
User Story
As a Forge maintainer, I want one contract and one capability classifier so that preview, approval, handoff, filesystem recovery, and UI copy can be made structurally incapable of disagreeing.
Requirements
Create web/lib/mcps/capability-normalization.ts — the ONLY home for:
normalizeCapability(cap): trim().toLowerCase().replace(/\s+/g,'_') (replaces the copies at mcp-execution-design.ts:537-539 and filesystem-grants.ts:34-36).
classifyCapability(mcpId, cap): McpCapabilityClass — reads the bounded_read_only set from MCP_CATALOG[mcpId].runtime.capabilities (plus filesystem project-alias spellings), folds in the planning_only set (filesystem.project.write) and an explicit deferred_live_mcp set (github write|branch|pull_requests.write|merge|settings|secret|actions.write, filesystem write|delete|admin, and any capability outside the catalog read/list/search allow-list).
coverageKeysForGrant(cap) / coverageKeysForProhibition(cap) — one alias model for filesystem.read ↔ filesystem.project.read, replacing approvedCoverageCapabilityKeys/filesystemProjectAlias/filesystemUnqualifiedAlias/prohibitedCoverageCapabilityKeys (mcp-execution-design.ts:545-580) and canonicalFilesystemProjectCapability (filesystem-grants.ts:38-45). Pick and document one direction (align with ADR 0008).
REQUIREMENT_CAPABILITY_FIELDS = ['permissions','capabilities','requiredCapabilities','mcpCapabilities'] and mergeCapabilityFields(entry) — the single union used by broker (capabilityArray:498-516), grant gate (requirementCapabilities:77-84), UI, and executor (mcpCapabilityList:1252-1260).
isMcpHealthy(status) / mcpHealthReason(mcpId, status) — one health predicate and one message source (replacing healthyStatus:297-299, the inline copy at :779, and filesystemGrantHealthError:137-156).
canProceedWithoutMcp(requirement, fallback).
Create web/lib/mcps/admission.ts exporting the types (McpCapabilityClass, McpAdmissionMode, McpAdmissionStatus, McpRecoveryAction, McpAdmissionDecision — exact shapes in ADR 0009), the core producer admitMcpRequirement(input), and the three shape-preserving adapters decisionsToValidation, decisionsToGrantPreview, decisionsToBrokerCheck. Types may re-export from web/lib/mcps/types.ts.
Make classifyCapability the single source: delete SAFE_BETA_CAPABILITY_PATTERNS as a separate literal (fold into the catalog-driven classifier). MCP_CATALOG.runtime.capabilities (web/lib/mcps/catalog.ts) becomes the declarative allow-list.
Author docs/adr/0009-mcp-admission-contract.md (done in the epic pass) and align terminology (planning-only / bounded read-only / deferred live MCP) in roadmap and task-detail copy at the wording level.
Ship unit tests for classifyCapability (each class), mergeCapabilityFields (each field), coverageKeysForGrant/Prohibition (alias directions), and admitMcpRequirement (each mode/status/recoveryAction).
Acceptance Criteria
web/lib/mcps/capability-normalization.ts and web/lib/mcps/admission.ts exist and export the contract, primitives, producer, and adapters from ADR 0009.
classifyCapability returns bounded_read_only for filesystem.project.read|list|search, planning_only for filesystem.project.write, and deferred_live_mcp for github write/branch/pr/merge/settings/secret and filesystem write/delete/admin.
The safe allow-list is sourced from MCP_CATALOG.runtime.capabilities; SAFE_BETA_CAPABILITY_PATTERNS is removed as a separate literal.
admitMcpRequirement returns a valid McpAdmissionDecision with mode, status, reason, recoveryAction, normalizedCapabilities, evidenceRefs for every case in ADR 0009.
Unit tests cover every class/field/alias/mode. npm run lint, npx tsc --noEmit, npm test, npm run build pass.
Issue Type
Feature — child slice S1 of EPIC #172 (MCP Execution Readiness and Bounded Context Grants). Canonical architecture:
docs/adr/0009-mcp-admission-contract.md.Problem Statement
Forge re-derives the same MCP beta capability policy in four overlapping code paths that do not agree (see #172 and ADR 0009):
validateMcpExecutionDesign—web/worker/mcp-execution-design.ts:326-454(approval validation)deriveMcpGrantDecisions/decisionStatus—web/worker/mcp-execution-design.ts:753-867(preview)evaluateWorkPackageMcpBroker—web/worker/mcp-execution-design.ts:599-739(handoff)requiresFilesystemGrantApproval/summarizeFilesystemCapabilities—web/lib/mcps/filesystem-grants.ts:90-339The safe capability allow-list is encoded three times (
SAFE_BETA_CAPABILITY_PATTERNSatmcp-execution-design.ts:7-18, the regex atfilesystem-grants.ts:41, andMCP_CATALOG[id].runtime.capabilitiesatcatalog.ts:18,37— the ostensible source of truth, never read by admission). There is no explicit "deferred live MCP" capability class, so a deferred GitHub write is reported with the generic "outside the allowed beta scope" string. There is no normalized decision object any surface can share.This slice creates the contract and shared primitives that the rest of the epic (S2–S6) consumes. It does not yet migrate the callers (that is S2).
Desired Outcome
A single admission decision (
McpAdmissionDecision) and a single set of capability primitives exist, sourced from the MCP catalog, ready for every surface to consume.deferred_live_mcpis a first-class, named capability class and admission mode.User Story
As a Forge maintainer, I want one contract and one capability classifier so that preview, approval, handoff, filesystem recovery, and UI copy can be made structurally incapable of disagreeing.
Requirements
Create
web/lib/mcps/capability-normalization.ts— the ONLY home for:normalizeCapability(cap):trim().toLowerCase().replace(/\s+/g,'_')(replaces the copies atmcp-execution-design.ts:537-539andfilesystem-grants.ts:34-36).classifyCapability(mcpId, cap): McpCapabilityClass— reads thebounded_read_onlyset fromMCP_CATALOG[mcpId].runtime.capabilities(plus filesystem project-alias spellings), folds in theplanning_onlyset (filesystem.project.write) and an explicitdeferred_live_mcpset (githubwrite|branch|pull_requests.write|merge|settings|secret|actions.write, filesystemwrite|delete|admin, and any capability outside the catalog read/list/search allow-list).coverageKeysForGrant(cap)/coverageKeysForProhibition(cap)— one alias model forfilesystem.read↔filesystem.project.read, replacingapprovedCoverageCapabilityKeys/filesystemProjectAlias/filesystemUnqualifiedAlias/prohibitedCoverageCapabilityKeys(mcp-execution-design.ts:545-580) andcanonicalFilesystemProjectCapability(filesystem-grants.ts:38-45). Pick and document one direction (align with ADR 0008).REQUIREMENT_CAPABILITY_FIELDS = ['permissions','capabilities','requiredCapabilities','mcpCapabilities']andmergeCapabilityFields(entry)— the single union used by broker (capabilityArray:498-516), grant gate (requirementCapabilities:77-84), UI, and executor (mcpCapabilityList:1252-1260).isMcpHealthy(status)/mcpHealthReason(mcpId, status)— one health predicate and one message source (replacinghealthyStatus:297-299, the inline copy at:779, andfilesystemGrantHealthError:137-156).canProceedWithoutMcp(requirement, fallback).Create
web/lib/mcps/admission.tsexporting the types (McpCapabilityClass,McpAdmissionMode,McpAdmissionStatus,McpRecoveryAction,McpAdmissionDecision— exact shapes in ADR 0009), the core produceradmitMcpRequirement(input), and the three shape-preserving adaptersdecisionsToValidation,decisionsToGrantPreview,decisionsToBrokerCheck. Types may re-export fromweb/lib/mcps/types.ts.Make
classifyCapabilitythe single source: deleteSAFE_BETA_CAPABILITY_PATTERNSas a separate literal (fold into the catalog-driven classifier).MCP_CATALOG.runtime.capabilities(web/lib/mcps/catalog.ts) becomes the declarative allow-list.Author
docs/adr/0009-mcp-admission-contract.md(done in the epic pass) and align terminology (planning-only / bounded read-only / deferred live MCP) in roadmap and task-detail copy at the wording level.Ship unit tests for
classifyCapability(each class),mergeCapabilityFields(each field),coverageKeysForGrant/Prohibition(alias directions), andadmitMcpRequirement(each mode/status/recoveryAction).Acceptance Criteria
web/lib/mcps/capability-normalization.tsandweb/lib/mcps/admission.tsexist and export the contract, primitives, producer, and adapters from ADR 0009.classifyCapabilityreturnsbounded_read_onlyforfilesystem.project.read|list|search,planning_onlyforfilesystem.project.write, anddeferred_live_mcpfor github write/branch/pr/merge/settings/secret and filesystem write/delete/admin.MCP_CATALOG.runtime.capabilities;SAFE_BETA_CAPABILITY_PATTERNSis removed as a separate literal.admitMcpRequirementreturns a validMcpAdmissionDecisionwithmode,status,reason,recoveryAction,normalizedCapabilities,evidenceRefsfor every case in ADR 0009.npm run lint,npx tsc --noEmit,npm test,npm run buildpass.Dependencies
Implementation Scope
Medium — new shared modules plus terminology alignment; no behavior migration (that is S2).