Skip to content

feat(runtime): rebuild MCP and Skill control plane - #15

Open
boh5 wants to merge 2 commits into
mainfrom
codex/mcp-skill-improvements
Open

feat(runtime): rebuild MCP and Skill control plane#15
boh5 wants to merge 2 commits into
mainfrom
codex/mcp-skill-improvements

Conversation

@boh5

@boh5 boh5 commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • hot-apply every user-configured MCP server across all six Agents while preserving the fixed built-in MCP role matrix
  • replace the legacy MCP manager with a live HTTP/STDIO runtime, strict lifecycle controls, draft Test, saved Reconnect, cancellation, redaction, and exact per-model-call descriptors
  • add five-tier Skill discovery, diagnostics, bounded catalog projection, immutable execution overlays, restart validation, and the Composer Skill picker
  • expose MCP and Skill control-plane surfaces in Settings and update architecture, configuration, integration, release, and Goal documentation

Breaking change

MCP server configuration now requires an explicit type and enabled field. The legacy url, headers, and timeout-only shape is rejected with no compatibility fallback.

Validation

  • bun run typecheck
  • bun run test: 2845 Agent Core unit, 143 integration, and 83 architecture tests passed
  • bun run build
  • focused MCP client/runtime/transport tests: 24 passed, 121 expectations
  • browser QA at 1440px and 390px in light and dark themes, including keyboard and screen-reader states
  • git diff --check

Review record

The implementation completed four independent review iterations. Fixes from those reviews include operation-level MCP discovery deadlines, cursor-cycle rejection, cross-project live-update coverage, complete real HTTP/STDIO lifecycle coverage, and graceful remote HTTP session termination.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added project Skills browsing, diagnostics, precedence indicators, prompt previews, pagination, and /skill use activation.
    • Added live MCP server management with HTTP and STDIO configuration, status and tool inventory, draft testing, reconnect controls, and hot application of saved settings.
    • Added execution-level Skill snapshots for consistent reads during runs.
  • Bug Fixes
    • Improved MCP cancellation, timeout handling, failure isolation, secret redaction, and dialog layering.
  • Documentation
    • Updated configuration, architecture, integration, and settings guidance for the new MCP and Skills behavior.

Hot-apply user MCP servers across all agents, add strict HTTP and STDIO lifecycle management, and expose MCP controls in Settings.

Add five-tier Skill discovery, diagnostics, immutable execution overlays, and the Composer picker.

BREAKING CHANGE: MCP server configuration now requires type and enabled, and the legacy url, headers, and timeout shape is rejected.
@cubic-dev-ai

cubic-dev-ai Bot commented Aug 9, 2026

Copy link
Copy Markdown

This PR has 13,221 reviewable changed lines after ignored/generated files are excluded, above this repository's 10,000-changed-line automatic review limit. The raw diff is 14,456 lines before ignored/generated files are excluded.

Most of the diff comes from:

  • packages/agent-core/src/config/mcp.test.ts (~688 changed lines)
  • packages/agent-core/src/mcp/client.test.ts (~645 changed lines)
  • packages/agent-core/src/runtime-mcp.test.ts (~631 changed lines)
  • packages/agent-core/src/mcp/runtime-service.ts (~621 changed lines)
  • packages/agent-core/src/skills/service.test.ts (~531 changed lines)

Comment @cubic-dev-ai review this to review it anyway. If the largest files are generated or fixture data, add them to your ignored files in review settings or ignorePatterns in cubic.yaml - cubic will then review the rest automatically. You can also raise this limit in review settings.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The pull request replaces legacy MCP management with a live HTTP/STDIO runtime. It adds strict MCP configuration, runtime controls, Skill discovery and snapshots, paginated inventories, slash-command activation, execution metadata, and related server and web interfaces.

Changes

MCP runtime and configuration

Layer / File(s) Summary
Live MCP runtime and configuration contracts
packages/agent-core/src/mcp/..., packages/agent-core/src/config/..., packages/protocol/src/types.ts
MCP now supports typed HTTP and STDIO servers, independent timeouts, lifecycle status, inventory, draft testing, reconnection, hot application, redaction, cancellation, and shutdown.
Runtime and execution integration
packages/agent-core/src/runtime.ts, packages/agent-core/src/agents/..., packages/agent-core/src/execution/..., packages/agent-core/src/tools/...
Runtime wiring uses live MCP snapshots at model boundaries. Tool scheduling uses resolved descriptors and durable settlement for cancellation, late results, and unknown effectful outcomes.
Server and web MCP surfaces
apps/server/src/routes/mcp.ts, apps/server/src/server-host.ts, apps/web/src/api/mcp.ts, apps/web/src/components/features/settings-panels.tsx
The server and web client expose MCP status, inventory, draft testing, reconnection, transport editing, tool listings, and separate persistence and live-apply results.

Skill control plane

Layer / File(s) Summary
Skill catalogs, snapshots, projections, and pagination
packages/agent-core/src/skills/..., packages/protocol/src/types.ts
Skill discovery now uses ordered project and user source tiers, strict winning-package validation, immutable digest-bound snapshots, prompt projections, diagnostics, and bounded cursor pagination.
Execution and command activation
packages/agent-core/src/execution/session-execution-manager.ts, packages/agent-core/src/session-input/service.ts, packages/agent-core/src/commands/skill.ts
Skill bindings are resolved per logical Execution and retained through suspension and resume. /skill use commands are normalized, validated, persisted as pending messages, and checked for idempotent replay.
Skill APIs and user interfaces
apps/server/src/routes/skills.ts, apps/web/src/api/skills.ts, apps/web/src/components/features/ChatInput.tsx, apps/web/src/components/features/settings-panels.tsx
Project and Session skill inventories are exposed through paginated APIs. Chat input supports filtered Skill selection, and Settings displays diagnostics and prompt projection data.

Possibly related PRs

  • boh5/archcode#13: Modifies overlapping configuration and Settings save flows, but addresses Runtime/Config recovery.
  • boh5/archcode#14: Extends the Skill package system in the same package-reader, service, and type areas.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the primary MCP and Skill control-plane rebuild.
Description check ✅ Passed The description provides a detailed summary, breaking-change note, validation evidence, and documentation coverage for the implementation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@boh5

boh5 commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

@cubic-dev-ai review this

@cubic-dev-ai

cubic-dev-ai Bot commented Aug 9, 2026

Copy link
Copy Markdown

@cubic-dev-ai review this

@boh5 I have started the AI code review. It will take a few minutes to complete.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/agent-core/src/mcp/errors.ts (1)

40-56: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make error messages match reason.

When reason is "aborted" or "timeout", both messages still state that the operation “failed”. tool-adapter.ts returns these messages with cancellation or timeout result codes. Generate reason-specific messages for consistent tool feedback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/agent-core/src/mcp/errors.ts` around lines 40 - 56, Update the
constructors for the MCP connection and tool execution error classes so their
messages use the provided reason: report cancellation for "aborted", timeout for
"timeout", and retain failure wording for "failed". Preserve the existing
server, tool, and cause details while ensuring tool-adapter result messages
remain consistent with their reason codes.
🧹 Nitpick comments (14)
packages/agent-core/src/mcp/naming.ts (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use Bun’s native Bun.CryptoHasher API instead of node:crypto. Preserve the current SHA-256 updates and digest("hex") output.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/agent-core/src/mcp/naming.ts` at line 1, Replace the node:crypto
createHash usage in the naming module with Bun.CryptoHasher, preserving the
existing SHA-256 update sequence and digest("hex") output behavior.

Source: Coding guidelines

packages/agent-core/src/mcp/tool-adapter.ts (1)

19-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the MCP input-validation boundary.

ToolRegistry.#prepareInput applies safeParse to mcpToolInputSchema, but .catchall(z.unknown()) accepts any object. aiInputSchema only describes inputs for the model; this code does not validate mcpTool.inputSchema before dispatch. If the MCP server is the validation boundary, document the local exception to the strict Zod-schema rule and add a regression test for invalid arguments.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/agent-core/src/mcp/tool-adapter.ts` at line 19, Document that
mcpToolInputSchema intentionally accepts arbitrary object arguments because MCP
performs validation before dispatch, while aiInputSchema only describes model
inputs. Add a regression test covering invalid arguments through
ToolRegistry.#prepareInput and confirm they are passed to the MCP tool without
local schema rejection.

Source: Coding guidelines

packages/agent-core/src/mcp/client.ts (2)

238-278: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the duplicated discovery deadline check.

Lines 263 and 266 run the same this.now() >= deadline check with only a synchronous tools.push between them. One check is enough.

♻️ Proposed simplification
         throwIfAborted(signal);
         if (this.now() >= deadline) throw discoveryTimeoutError(timeoutMs);
 
         tools.push(...result.tools.map((tool) => tool as McpToolLike));
-        if (this.now() >= deadline) throw discoveryTimeoutError(timeoutMs);
         if (result.nextCursor === undefined) break;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/agent-core/src/mcp/client.ts` around lines 238 - 278, Remove one of
the duplicated this.now() >= deadline checks in listTools, retaining a single
discovery deadline validation around the synchronous tools.push and pagination
handling. Preserve the existing timeout behavior and error flow through
discoveryTimeoutError.

371-383: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Bounded stderr can split a multi-byte UTF-8 sequence.

Line 378 slices the buffer at a fixed byte count and then decodes. A truncated multi-byte character produces a replacement character in the log. Use the same UTF-8-safe truncation helper that the Skill projection code uses, if one is exported.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/agent-core/src/mcp/client.ts` around lines 371 - 383, Update
`#attachBoundedStderr` to use the exported UTF-8-safe truncation helper used by
the Skill projection code instead of slicing the encoded buffer directly at
MAX_STDERR_LOG_BYTES. Preserve the existing stderr conversion, redaction, logger
event, and serverName context while ensuring truncation never splits a
multi-byte character.
packages/protocol/src/types.test.ts (1)

181-195: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Bind the MCP fixtures to their protocol types.

The fixtures use as const with no type relationship to McpServerStatus or McpServerInventoryResponse. The assertions only compare an object to its own JSON round trip, so this test passes even if the protocol types change or the fixture stops matching them. The new binding test at Line 63 already uses satisfies; apply the same here.

💚 Proposed change
-    const statuses = {
+    const statuses = {
       docs: { state: "ready", toolCount: 1, warningCount: 0, connectedAt: 123 },
       local: { state: "disabled", updatedAt: 124 },
-    } as const;
-    const inventory = {
+    } satisfies Record<string, McpServerStatus>;
+    const inventory = {
       servers: {
         docs: [{ serverName: "docs", name: "search", registryName: "mcp__docs__search", description: "Search docs" }],
         local: [],
       },
-    } as const;
+    } satisfies McpServerInventoryResponse;

Add McpServerStatus and McpServerInventoryResponse to the type imports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/protocol/src/types.test.ts` around lines 181 - 195, Bind the
statuses and inventory fixtures in the test using the protocol types: add
McpServerStatus and McpServerInventoryResponse to the type imports, then apply
satisfies McpServerStatus and satisfies McpServerInventoryResponse alongside as
const. Keep the existing serializeRoundTrip assertions unchanged.
apps/web/src/components/features/ChatInput.tsx (1)

551-565: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep non-option content out of the listbox container.

In the loading state and the "No matching Skills." state, the container keeps role="listbox" while its only children are <p role="status"> elements. A listbox must own option (or group) elements. Screen readers can drop or misreport these paragraphs, and an empty listbox is announced as an empty list.

Use the same treatment you already apply to the failed state: render role="group" when no options exist.

♿ Proposed change
-          role={skillUseInput !== null && skillInventoryState === "failed" ? "group" : "listbox"}
+          role={skillUseInput !== null && slashOptionCount === 0 ? "group" : "listbox"}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/features/ChatInput.tsx` around lines 551 - 565,
Update the role expression on the slash menu container in the ChatInput render
so loading and empty “No matching Skills.” states use role="group" when no
selectable options exist, matching the existing failed-state treatment. Preserve
role="listbox" for states with actual options, and keep the existing option
rendering unchanged.

Source: Coding guidelines

apps/web/src/components/features/settings-panels.tsx (1)

483-493: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider narrowing the inventory effect dependency.

The effect depends on the servers status record identity. Each MCP status event replaces that object and triggers a new getMcpInventory() request. During a connect/discovery sequence this produces repeated fetches. A derived key such as a joined name:state string reduces the request count.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/features/settings-panels.tsx` around lines 483 - 493,
Update the inventory-loading useEffect dependency around getMcpInventory so it
no longer depends directly on the servers object identity. Derive a stable key
from each server’s name and state (for example, joined name:state values) and
depend on that key instead, while preserving the existing active,
expectedRevision, and runtimeAvailable triggers and mounted guards.
packages/agent-core/src/mcp/fixtures/stdio-server.ts (1)

6-27: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: share the fixture tool definitions with the HTTP fixture.

tool() and both request handlers duplicate paginatedServer() in packages/agent-core/src/mcp/transports.integration.test.ts (lines 29-53). Extract the tool list and handler registration into one exported helper, and keep only the transport bootstrap here. The two pagination fixtures then cannot drift.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/agent-core/src/mcp/fixtures/stdio-server.ts` around lines 6 - 27,
Extract the shared paginated tool definition and request-handler registration
from the stdio fixture and the HTTP fixture’s paginatedServer() into one
exported helper. Update both fixtures to call that helper while retaining only
their transport-specific server bootstrap, preserving the existing pagination
and tool-call behavior.
packages/agent-core/src/commands/skill.ts (1)

25-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Import SkillService through the Skills barrel.

Line 26 imports from ../skills/service. Import SkillService from ../skills instead. This preserves the public module boundary.

Proposed change
+import type { SkillService } from "../skills";
 
-  readonly skillService: import("../skills/service").SkillService;
+  readonly skillService: SkillService;

As per coding guidelines, use barrel exports through index.ts files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/agent-core/src/commands/skill.ts` around lines 25 - 31, Update the
validateSkillActivation input type to reference SkillService through the public
../skills barrel export instead of the internal ../skills/service module,
preserving the existing type and behavior.

Source: Coding guidelines

packages/agent-core/src/agents/factory-types.ts (1)

6-7: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the tools barrel for AnyToolDescriptor.

Export AnyToolDescriptor from packages/agent-core/src/tools/index.ts, then import it through ../tools.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/agent-core/src/agents/factory-types.ts` around lines 6 - 7, Export
AnyToolDescriptor from the tools barrel at tools/index.ts, then update the
import in factory-types.ts to reference ../tools instead of ../tools/types.

Source: Coding guidelines

packages/agent-core/src/skills/package-reader.ts (2)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace node:crypto with Bun.CryptoHasher in both new hashing sites. Both files import createHash from node:crypto although Bun provides an equivalent, and packages/agent-core/src/prompt/compiler.ts already uses new Bun.CryptoHasher("sha256").

  • packages/agent-core/src/skills/package-reader.ts#L1-L1: drop the node:crypto import and build the snapshot digest with Bun.CryptoHasher, updating digestSnapshot, updateDigestField, and updateDigestLength signatures.
  • packages/agent-core/src/skills/service.ts#L1-L1: drop the node:crypto import and compute stableDigest with Bun.CryptoHasher.

As per coding guidelines: "Use Bun as the runtime and prefer Bun-native APIs over node:* imports; use node:* only when Bun has no suitable alternative."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/agent-core/src/skills/package-reader.ts` at line 1, Replace the
node:crypto createHash usage at
packages/agent-core/src/skills/package-reader.ts#L1-L1 and
packages/agent-core/src/skills/service.ts#L1-L1 with Bun.CryptoHasher. In
package-reader.ts, update digestSnapshot, updateDigestField, and
updateDigestLength to use the Bun hasher while preserving snapshot digest
behavior; in service.ts, compute stableDigest with Bun.CryptoHasher and remove
both node:crypto imports.

Source: Coding guidelines


229-244: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use SkillPackageResourcePathError for all path validation failures.

Lines 229-236 now throw SkillPackageResourcePathError, but the depth check and the SKILL.md check still throw a plain Error. Callers cannot classify path failures reliably. Convert the remaining two throws.

♻️ Proposed change
   if (segments.length > SKILL_RESOURCE_MAX_DEPTH) {
-    throw new Error(`Skill resource depth exceeds ${SKILL_RESOURCE_MAX_DEPTH}`);
+    throw new SkillPackageResourcePathError(`Skill resource depth exceeds ${SKILL_RESOURCE_MAX_DEPTH}`);
   }
   if (segments[0]?.toLowerCase() === SKILL_ENTRY_FILE.toLowerCase()) {
-    throw new Error("SKILL.md is the package entry and cannot be a resource directory");
+    throw new SkillPackageResourcePathError("SKILL.md is the package entry and cannot be a resource directory");
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/agent-core/src/skills/package-reader.ts` around lines 229 - 244,
Update validateResourcePath so the depth-limit and SKILL.md entry-file checks
also throw SkillPackageResourcePathError, matching the existing error type used
for all other invalid resource paths. Preserve the current validation conditions
and messages.
packages/agent-core/src/skills/projection.ts (1)

16-27: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Reduce the projection fit search from O(n²) to O(n).

Each iteration re-renders and re-measures every included entry. For a large catalog the loop runs once per omitted entry, so cost grows quadratically with the number of skills. Compute each line once, accumulate prefix byte lengths, then select the largest prefix that fits the budget plus the omission line.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/agent-core/src/skills/projection.ts` around lines 16 - 27, Update
the projection fit logic around renderProjection to avoid re-rendering prefixes
in the includedCount loop. Render each normalized entry once, compute cumulative
UTF-8 byte lengths, and use those prefix totals with the omission-line size to
select the largest fitting prefix within maxBytes; preserve the existing frozen
result shape and overflow error.
packages/agent-core/src/skills/service.ts (1)

152-199: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Reduce catalog build cost.

Two problems compound here. sourceNames.includes(name) runs a linear scan for every name and every source, so candidate assembly is quadratic in catalog size. #discoverCandidate is then awaited serially, so every candidate SKILL.md read blocks the next one. catalogForAgent runs on the prompt-compile path and on every skill_list call.

Build one Set per source before the name loop, and resolve candidate discovery with bounded concurrency instead of a fully serial loop.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/agent-core/src/skills/service.ts` around lines 152 - 199, Optimize
catalog construction in the surrounding catalog-building method by precomputing
a Set of names for each entry in namesBySource, then use Set.has(name) instead
of sourceNames.includes(name). Replace the serial awaits around
`#discoverCandidate` with bounded-concurrency processing while preserving
candidate order, winner selection, inventory entries, diagnostics, and entries
output.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/components/features/settings-panels.tsx`:
- Line 601: Update the stdio arguments handler in the settings panel’s textarea
onChange flow to remove empty lines after splitting the input by newline before
assigning draft.args. Preserve undefined for entirely empty input, and ensure
stored arguments contain only non-empty lines.

In `@apps/web/src/components/features/SettingsDialog.tsx`:
- Around line 50-58: Update the snapshot-change useEffect to clear
preserveSaveErrorRevision.current in both branches, including when the current
revision matches; ensure no recorded revision marker survives after processing
any snapshot change.

In `@packages/agent-core/src/config/runtime-secret-literals.ts`:
- Around line 88-93: Update collectRuntimeSecretLiterals around the HTTP server
branch so server.url is excluded from runtime secret literal collection, keeping
header and environment collection unchanged; add a regression test confirming an
HTTP URL such as http:a follows the same save and runtime validation behavior.

In `@packages/agent-core/src/execution/session-execution-manager.ts`:
- Line 439: Update deleteSession to remove every deleted session ID from
`#executionSkillSnapshots` before completing session cleanup. Ensure suspended and
non-terminal executions are evicted even when terminalization methods are not
invoked, while preserving the existing agent, store, and directory deletion
behavior.

In `@packages/agent-core/src/mcp/runtime-service.ts`:
- Around line 365-368: Update the losing-candidate branch in `#connectAndPublish`
so candidate.handle.retire() is wrapped with the same `#logCloseFailure` handling
used by the error path, preventing retire failures from rejecting reconnect
operations for superseded connections.
- Around line 185-205: Update testServer and its in-flight test state to enforce
a shared maximum number of concurrent draft tests, rejecting new drafts once the
limit is reached while preserving the existing duplicate testKey guard. Ensure
the count is incremented only for accepted tests and decremented in the existing
promise.finally cleanup so slots are released on every completion path.
- Around line 597-602: Update createServerRedactionPolicy to include only
secret-bearing values extracted from config.args for stdio servers, while
preserving header/env handling for HTTP and other transports. Filter argument
candidates through the existing SecretRedactionPolicy eligibility rules so short
ordinary flags such as --stdio are excluded and longer non-secret arguments are
not redacted; add coverage for both cases.

In `@packages/agent-core/src/mcp/transports.integration.test.ts`:
- Around line 162-179: Replace the immediate sessionCloseCount assertions after
runtime.reconnect("local") and runtime.apply(...) with expectHttpSessionClose
waits for the retired first handle, matching the existing usage later in the
test. Keep the active-session and inventory assertions, and retain the
pre-operation close counts so the helper verifies exactly one additional close
on each path.

In `@packages/agent-core/src/runtime-mcp.test.ts`:
- Line 178: Remove the duplicate calls declaration in the test setup, retain
only one boundaries declaration near the affected test, and keep a single
streamText property in the object literal. Update the relevant test code in
runtime-mcp.test.ts without changing the remaining setup or behavior.

In `@packages/agent-core/src/runtime-skill-command.test.ts`:
- Around line 85-146: Install the test LLM adapter before the test accepts any
session messages, using the existing
setLlmAdapterForTest()/installTestLlmAdapter() pattern from main.test.ts. Update
the runtime test setup around createRuntime and acceptSessionMessage so
execution uses the stubbed adapter and cannot make live model calls; preserve
the existing assertions and cleanup.

In `@packages/agent-core/src/runtime.ts`:
- Around line 2282-2290: Restrict the fallback lookup in testMcpServer to own
keys of BUILTIN_MCP_SERVERS by using Object.hasOwn or an explicit built-in name
membership check before indexing. Preserve draft server precedence and continue
throwing the existing configuration error when neither source contains
serverName.

In `@packages/agent-core/src/skills/projection.test.ts`:
- Around line 41-42: Update the omitted-skills rendering logic and its assertion
in the projection test so omittedCount === 1 uses singular “Skill omitted,”
while counts greater than one retain “Skills omitted.”

In `@packages/agent-core/src/store/helpers.ts`:
- Around line 922-931: Update the available schema’s renderedText field
alongside byteLength so persisted text is directly limited to the 8,000-byte
maximum, using the existing boundedUtf8String helper and preserving the current
byteLength validation.
- Around line 280-285: Update the executionSkills schema to pass the imported
SKILL_SOURCE_TIERS constant to z.enum instead of duplicating the five tier
values inline. Keep the existing name, digest, and resolutionRoot validations
unchanged, ensuring execution records and prompt traces share the same
source-tier definition.

In `@packages/agent-core/src/tools/builtins/skill-read.test.ts`:
- Line 14: Update the SkillService setup in the skill-read test suite to pass an
isolated temporary userAgentsSkillsRoot under tmpRoot, ensuring skill_read
resolves only from the test directory. Keep the unknown-skill test independent
of any skills installed in $HOME/.agents/skills.

In `@packages/agent-core/src/tools/registry.ts`:
- Around line 119-132: Extract the descriptor invariant validation currently
performed by register() into a reusable helper, then invoke it from
executeResolved() before calling `#execute`(). Ensure resolved descriptors with
traits.destructive set but no permissions are rejected consistently with
registered descriptors, while preserving the existing tool-name mismatch
handling.

In `@packages/protocol/src/guards.ts`:
- Around line 219-227: Update isExecutionSkillBinding to validate digest as a
64-character hexadecimal string rather than any string, while preserving the
existing field and source checks. Add rejection coverage for short and malformed
digest values.

In `@packages/protocol/src/types.ts`:
- Around line 973-997: Update README.md to document the MCP configuration schema
represented by ConfigMcpServerSettings, including required type and enabled
fields, HTTP and stdio-specific properties, the connectTimeoutMs,
discoveryTimeoutMs, and callTimeoutMs fields replacing timeout, and the
mcp.disabledBuiltins setting.

---

Outside diff comments:
In `@packages/agent-core/src/mcp/errors.ts`:
- Around line 40-56: Update the constructors for the MCP connection and tool
execution error classes so their messages use the provided reason: report
cancellation for "aborted", timeout for "timeout", and retain failure wording
for "failed". Preserve the existing server, tool, and cause details while
ensuring tool-adapter result messages remain consistent with their reason codes.

---

Nitpick comments:
In `@apps/web/src/components/features/ChatInput.tsx`:
- Around line 551-565: Update the role expression on the slash menu container in
the ChatInput render so loading and empty “No matching Skills.” states use
role="group" when no selectable options exist, matching the existing
failed-state treatment. Preserve role="listbox" for states with actual options,
and keep the existing option rendering unchanged.

In `@apps/web/src/components/features/settings-panels.tsx`:
- Around line 483-493: Update the inventory-loading useEffect dependency around
getMcpInventory so it no longer depends directly on the servers object identity.
Derive a stable key from each server’s name and state (for example, joined
name:state values) and depend on that key instead, while preserving the existing
active, expectedRevision, and runtimeAvailable triggers and mounted guards.

In `@packages/agent-core/src/agents/factory-types.ts`:
- Around line 6-7: Export AnyToolDescriptor from the tools barrel at
tools/index.ts, then update the import in factory-types.ts to reference ../tools
instead of ../tools/types.

In `@packages/agent-core/src/commands/skill.ts`:
- Around line 25-31: Update the validateSkillActivation input type to reference
SkillService through the public ../skills barrel export instead of the internal
../skills/service module, preserving the existing type and behavior.

In `@packages/agent-core/src/mcp/client.ts`:
- Around line 238-278: Remove one of the duplicated this.now() >= deadline
checks in listTools, retaining a single discovery deadline validation around the
synchronous tools.push and pagination handling. Preserve the existing timeout
behavior and error flow through discoveryTimeoutError.
- Around line 371-383: Update `#attachBoundedStderr` to use the exported
UTF-8-safe truncation helper used by the Skill projection code instead of
slicing the encoded buffer directly at MAX_STDERR_LOG_BYTES. Preserve the
existing stderr conversion, redaction, logger event, and serverName context
while ensuring truncation never splits a multi-byte character.

In `@packages/agent-core/src/mcp/fixtures/stdio-server.ts`:
- Around line 6-27: Extract the shared paginated tool definition and
request-handler registration from the stdio fixture and the HTTP fixture’s
paginatedServer() into one exported helper. Update both fixtures to call that
helper while retaining only their transport-specific server bootstrap,
preserving the existing pagination and tool-call behavior.

In `@packages/agent-core/src/mcp/naming.ts`:
- Line 1: Replace the node:crypto createHash usage in the naming module with
Bun.CryptoHasher, preserving the existing SHA-256 update sequence and
digest("hex") output behavior.

In `@packages/agent-core/src/mcp/tool-adapter.ts`:
- Line 19: Document that mcpToolInputSchema intentionally accepts arbitrary
object arguments because MCP performs validation before dispatch, while
aiInputSchema only describes model inputs. Add a regression test covering
invalid arguments through ToolRegistry.#prepareInput and confirm they are passed
to the MCP tool without local schema rejection.

In `@packages/agent-core/src/skills/package-reader.ts`:
- Line 1: Replace the node:crypto createHash usage at
packages/agent-core/src/skills/package-reader.ts#L1-L1 and
packages/agent-core/src/skills/service.ts#L1-L1 with Bun.CryptoHasher. In
package-reader.ts, update digestSnapshot, updateDigestField, and
updateDigestLength to use the Bun hasher while preserving snapshot digest
behavior; in service.ts, compute stableDigest with Bun.CryptoHasher and remove
both node:crypto imports.
- Around line 229-244: Update validateResourcePath so the depth-limit and
SKILL.md entry-file checks also throw SkillPackageResourcePathError, matching
the existing error type used for all other invalid resource paths. Preserve the
current validation conditions and messages.

In `@packages/agent-core/src/skills/projection.ts`:
- Around line 16-27: Update the projection fit logic around renderProjection to
avoid re-rendering prefixes in the includedCount loop. Render each normalized
entry once, compute cumulative UTF-8 byte lengths, and use those prefix totals
with the omission-line size to select the largest fitting prefix within
maxBytes; preserve the existing frozen result shape and overflow error.

In `@packages/agent-core/src/skills/service.ts`:
- Around line 152-199: Optimize catalog construction in the surrounding
catalog-building method by precomputing a Set of names for each entry in
namesBySource, then use Set.has(name) instead of sourceNames.includes(name).
Replace the serial awaits around `#discoverCandidate` with bounded-concurrency
processing while preserving candidate order, winner selection, inventory
entries, diagnostics, and entries output.

In `@packages/protocol/src/types.test.ts`:
- Around line 181-195: Bind the statuses and inventory fixtures in the test
using the protocol types: add McpServerStatus and McpServerInventoryResponse to
the type imports, then apply satisfies McpServerStatus and satisfies
McpServerInventoryResponse alongside as const. Keep the existing
serializeRoundTrip assertions unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 71239106-38b9-434c-b4a1-da80979a3940

📥 Commits

Reviewing files that changed from the base of the PR and between 9f1f47f and 25019ed.

📒 Files selected for processing (168)
  • AGENTS.md
  • CHANGELOG.md
  • apps/server/src/app.test.ts
  • apps/server/src/app.ts
  • apps/server/src/errors.ts
  • apps/server/src/routes/attachments.test.ts
  • apps/server/src/routes/compression.test.ts
  • apps/server/src/routes/config.test.ts
  • apps/server/src/routes/files.test.ts
  • apps/server/src/routes/mcp.test.ts
  • apps/server/src/routes/mcp.ts
  • apps/server/src/routes/messages.test.ts
  • apps/server/src/routes/projects.test.ts
  • apps/server/src/routes/sessions.test.ts
  • apps/server/src/routes/skills.test.ts
  • apps/server/src/routes/skills.ts
  • apps/server/src/server-host.test.ts
  • apps/server/src/server-host.ts
  • apps/web/src/api/config.test.ts
  • apps/web/src/api/config.ts
  • apps/web/src/api/mcp.test.ts
  • apps/web/src/api/mcp.ts
  • apps/web/src/api/skills.test.ts
  • apps/web/src/api/skills.ts
  • apps/web/src/components/composite/ExecutionWorkstream.interaction.tsx
  • apps/web/src/components/features/ChatHeader.test.tsx
  • apps/web/src/components/features/ChatInput.test.tsx
  • apps/web/src/components/features/ChatInput.tsx
  • apps/web/src/components/features/ComposerQueueList.interaction.tsx
  • apps/web/src/components/features/SessionComposerDock.interaction.tsx
  • apps/web/src/components/features/SettingsDialog.interaction.tsx
  • apps/web/src/components/features/SettingsDialog.test.tsx
  • apps/web/src/components/features/SettingsDialog.tsx
  • apps/web/src/components/features/TodoProgressButton.interaction.tsx
  • apps/web/src/components/features/settings-helpers.ts
  • apps/web/src/components/features/settings-panels.tsx
  • apps/web/src/components/ui/Dialog.test.tsx
  • apps/web/src/components/ui/Dialog.tsx
  • apps/web/src/context/global-sse.test.tsx
  • apps/web/src/context/settings-modal.tsx
  • apps/web/src/lib/execution-status-presentation.test.ts
  • apps/web/src/lib/execution-workstream.test.ts
  • apps/web/src/routes/session.test.tsx
  • apps/web/src/store/mcp-status-store.test.ts
  • apps/web/src/store/session-store.test.ts
  • config.example.json
  • design-system/pages/settings.md
  • docs/architecture.md
  • docs/configuration.md
  • docs/goals/mcp-skill-control-plane-hard-cut-plan-goal.md
  • docs/goals/mcp-skill-control-plane-hard-cut-progress.md
  • docs/integrations.md
  • packages/agent-core/src/__arch__/architecture.test.ts
  • packages/agent-core/src/__arch__/tool-output-boundaries.test.ts
  • packages/agent-core/src/__arch__/tool-output-policy-matrix.test.ts
  • packages/agent-core/src/agents/configured-agent-mcp.test.ts
  • packages/agent-core/src/agents/configured-agent.test.ts
  • packages/agent-core/src/agents/configured-agent.ts
  • packages/agent-core/src/agents/definitions/analyst.ts
  • packages/agent-core/src/agents/definitions/build.ts
  • packages/agent-core/src/agents/definitions/definitions.test.ts
  • packages/agent-core/src/agents/definitions/discussion.ts
  • packages/agent-core/src/agents/definitions/explore.ts
  • packages/agent-core/src/agents/definitions/lead.ts
  • packages/agent-core/src/agents/definitions/librarian.ts
  • packages/agent-core/src/agents/factory-types.ts
  • packages/agent-core/src/agents/factory.test.ts
  • packages/agent-core/src/agents/factory.ts
  • packages/agent-core/src/agents/query/loop.test.ts
  • packages/agent-core/src/agents/query/loop.ts
  • packages/agent-core/src/agents/query/provider-secret-redaction.integration.test.ts
  • packages/agent-core/src/agents/query/recovery.test.ts
  • packages/agent-core/src/agents/query/types.ts
  • packages/agent-core/src/agents/session-agent-manager.test.ts
  • packages/agent-core/src/agents/session-agent-manager.ts
  • packages/agent-core/src/agents/types.ts
  • packages/agent-core/src/attachments/read-paths.test.ts
  • packages/agent-core/src/background/tasks/title-generation.test.ts
  • packages/agent-core/src/commands/skill.test.ts
  • packages/agent-core/src/commands/skill.ts
  • packages/agent-core/src/commands/types.ts
  • packages/agent-core/src/config/index.ts
  • packages/agent-core/src/config/mcp.test.ts
  • packages/agent-core/src/config/mcp.ts
  • packages/agent-core/src/config/runtime-secret-literals.test.ts
  • packages/agent-core/src/config/runtime-secret-literals.ts
  • packages/agent-core/src/config/server-config-service.test.ts
  • packages/agent-core/src/config/server-config-service.ts
  • packages/agent-core/src/events/session-event-bridge.test.ts
  • packages/agent-core/src/execution/session-execution-manager.test.ts
  • packages/agent-core/src/execution/session-execution-manager.ts
  • packages/agent-core/src/execution/session-tool-batch-scheduler.test.ts
  • packages/agent-core/src/execution/session-tool-batch-scheduler.ts
  • packages/agent-core/src/index.ts
  • packages/agent-core/src/lead-architecture-flows.integration.test.ts
  • packages/agent-core/src/main.test.ts
  • packages/agent-core/src/mcp/builtin-servers.ts
  • packages/agent-core/src/mcp/client.test.ts
  • packages/agent-core/src/mcp/client.ts
  • packages/agent-core/src/mcp/errors.ts
  • packages/agent-core/src/mcp/fixtures/stdio-server.ts
  • packages/agent-core/src/mcp/index.ts
  • packages/agent-core/src/mcp/manager.test.ts
  • packages/agent-core/src/mcp/manager.ts
  • packages/agent-core/src/mcp/naming.test.ts
  • packages/agent-core/src/mcp/naming.ts
  • packages/agent-core/src/mcp/runtime-service.test.ts
  • packages/agent-core/src/mcp/runtime-service.ts
  • packages/agent-core/src/mcp/tool-adapter.test.ts
  • packages/agent-core/src/mcp/tool-adapter.ts
  • packages/agent-core/src/mcp/transports.integration.test.ts
  • packages/agent-core/src/prompt/compiler.test.ts
  • packages/agent-core/src/prompt/compiler.ts
  • packages/agent-core/src/prompt/live-eval.ts
  • packages/agent-core/src/prompt/types.ts
  • packages/agent-core/src/runtime-automations.integration.test.ts
  • packages/agent-core/src/runtime-automations.test.ts
  • packages/agent-core/src/runtime-data/service.test.ts
  • packages/agent-core/src/runtime-mcp.test.ts
  • packages/agent-core/src/runtime-skill-command.test.ts
  • packages/agent-core/src/runtime.ts
  • packages/agent-core/src/session-input/model-selection.test.ts
  • packages/agent-core/src/session-input/service.test.ts
  • packages/agent-core/src/session-input/service.ts
  • packages/agent-core/src/skills/index.ts
  • packages/agent-core/src/skills/package-reader.test.ts
  • packages/agent-core/src/skills/package-reader.ts
  • packages/agent-core/src/skills/pagination.test.ts
  • packages/agent-core/src/skills/pagination.ts
  • packages/agent-core/src/skills/projection.test.ts
  • packages/agent-core/src/skills/projection.ts
  • packages/agent-core/src/skills/service.test.ts
  • packages/agent-core/src/skills/service.ts
  • packages/agent-core/src/skills/types.ts
  • packages/agent-core/src/store/helpers.test.ts
  • packages/agent-core/src/store/helpers.ts
  • packages/agent-core/src/store/logical-execution.test.ts
  • packages/agent-core/src/store/message-phase-hard-cut.test.ts
  • packages/agent-core/src/store/session-store-manager.test.ts
  • packages/agent-core/src/store/store.test.ts
  • packages/agent-core/src/testing/test-execution-fixtures.ts
  • packages/agent-core/src/testing/test-mcp-runtime.ts
  • packages/agent-core/src/tool-output/artifact-lifecycle.test.ts
  • packages/agent-core/src/tool-output/live-bash.integration.test.ts
  • packages/agent-core/src/tools/builtins/skill-list.test.ts
  • packages/agent-core/src/tools/builtins/skill-list.ts
  • packages/agent-core/src/tools/builtins/skill-read.test.ts
  • packages/agent-core/src/tools/builtins/skill-read.ts
  • packages/agent-core/src/tools/concurrency/partition.test.ts
  • packages/agent-core/src/tools/concurrency/partition.ts
  • packages/agent-core/src/tools/errors.test.ts
  • packages/agent-core/src/tools/errors.ts
  • packages/agent-core/src/tools/index.ts
  • packages/agent-core/src/tools/permission/index.ts
  • packages/agent-core/src/tools/permission/mcp.test.ts
  • packages/agent-core/src/tools/permission/mcp.ts
  • packages/agent-core/src/tools/registry.test.ts
  • packages/agent-core/src/tools/registry.ts
  • packages/agent-core/src/tools/types.ts
  • packages/protocol/src/execution.test.ts
  • packages/protocol/src/execution.ts
  • packages/protocol/src/guards.test.ts
  • packages/protocol/src/guards.ts
  • packages/protocol/src/message-phase-hard-cut.test.ts
  • packages/protocol/src/reduce.test.ts
  • packages/protocol/src/reduce.ts
  • packages/protocol/src/types.test.ts
  • packages/protocol/src/types.ts
💤 Files with no reviewable changes (9)
  • apps/server/src/routes/projects.test.ts
  • packages/agent-core/src/tools/permission/mcp.ts
  • packages/agent-core/src/tools/index.ts
  • apps/server/src/routes/sessions.test.ts
  • apps/server/src/routes/files.test.ts
  • packages/agent-core/src/tools/permission/index.ts
  • packages/agent-core/src/tools/permission/mcp.test.ts
  • packages/agent-core/src/mcp/manager.ts
  • packages/agent-core/src/mcp/manager.test.ts

<Field label="Transport"><select className={selectClass} value={server.type} onChange={(event) => replaceTransport(event.target.value as "http" | "stdio")}><option value="http">HTTP</option><option value="stdio">STDIO</option></select></Field>
{server.type === "http" ? <Field label="HTTP URL" error={errors[`mcp.servers.${name}.url`]}><TextInput value={server.url} onChange={(next) => update((draft) => { if (draft.type === "http") draft.url = next; })} /></Field> : <>
<Field label="Command" error={errors[`mcp.servers.${name}.command`]}><TextInput value={server.command} onChange={(next) => update((draft) => { if (draft.type === "stdio") draft.command = next; })} /></Field>
<Field label="Arguments (one per line)"><textarea rows={3} value={server.args?.join("\n") ?? ""} onChange={(event) => update((draft) => { if (draft.type === "stdio") draft.args = event.target.value ? event.target.value.split("\n") : undefined; })} className="min-h-20 resize-y rounded-sm border border-border-control bg-bg-base px-3 py-2 font-mono text-[12px] leading-[18px] text-text-primary outline-none transition-colors duration-[var(--motion-hover)] hover:border-text-secondary focus:border-brand focus:ring-2 focus:ring-brand-subtle" /></Field>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Filter empty argument lines.

event.target.value.split("\n") keeps empty strings. A trailing newline or a blank line produces an empty "" argument that is passed to the STDIO process. Drop empty lines before storing.

🛠️ Proposed fix
-<Field label="Arguments (one per line)"><textarea rows={3} value={server.args?.join("\n") ?? ""} onChange={(event) => update((draft) => { if (draft.type === "stdio") draft.args = event.target.value ? event.target.value.split("\n") : undefined; })}
+<Field label="Arguments (one per line)"><textarea rows={3} value={server.args?.join("\n") ?? ""} onChange={(event) => update((draft) => { if (draft.type === "stdio") { const args = event.target.value.split("\n").filter((line) => line.trim() !== ""); draft.args = args.length > 0 ? args : undefined; } })}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Field label="Arguments (one per line)"><textarea rows={3} value={server.args?.join("\n") ?? ""} onChange={(event) => update((draft) => { if (draft.type === "stdio") draft.args = event.target.value ? event.target.value.split("\n") : undefined; })} className="min-h-20 resize-y rounded-sm border border-border-control bg-bg-base px-3 py-2 font-mono text-[12px] leading-[18px] text-text-primary outline-none transition-colors duration-[var(--motion-hover)] hover:border-text-secondary focus:border-brand focus:ring-2 focus:ring-brand-subtle" /></Field>
<Field label="Arguments (one per line)"><textarea rows={3} value={server.args?.join("\n") ?? ""} onChange={(event) => update((draft) => { if (draft.type === "stdio") { const args = event.target.value.split("\n").filter((line) => line.trim() !== ""); draft.args = args.length > 0 ? args : undefined; } })} className="min-h-20 resize-y rounded-sm border border-border-control bg-bg-base px-3 py-2 font-mono text-[12px] leading-[18px] text-text-primary outline-none transition-colors duration-[var(--motion-hover)] hover:border-text-secondary focus:border-brand focus:ring-2 focus:ring-brand-subtle" /></Field>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/features/settings-panels.tsx` at line 601, Update the
stdio arguments handler in the settings panel’s textarea onChange flow to remove
empty lines after splitting the input by newline before assigning draft.args.
Preserve undefined for entirely empty input, and ensure stored arguments contain
only non-empty lines.

Comment on lines 50 to +58
useEffect(() => {
setDraft(cloneConfig(snapshot.config));
setErrors({});
setJsonErrors({});
setSaveError(undefined);
if (preserveSaveErrorRevision.current === snapshot.revision) {
preserveSaveErrorRevision.current = undefined;
} else {
setSaveError(undefined);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clear preserveSaveErrorRevision on every snapshot change.

The else branch leaves the marker set. If the reload returns a different revision, the marker survives. A later snapshot that happens to carry the recorded revision then preserves a stale save error. Reset the ref in both branches.

♻️ Proposed fix
     if (preserveSaveErrorRevision.current === snapshot.revision) {
       preserveSaveErrorRevision.current = undefined;
     } else {
+      preserveSaveErrorRevision.current = undefined;
       setSaveError(undefined);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
setDraft(cloneConfig(snapshot.config));
setErrors({});
setJsonErrors({});
setSaveError(undefined);
if (preserveSaveErrorRevision.current === snapshot.revision) {
preserveSaveErrorRevision.current = undefined;
} else {
setSaveError(undefined);
}
useEffect(() => {
setDraft(cloneConfig(snapshot.config));
setErrors({});
setJsonErrors({});
if (preserveSaveErrorRevision.current === snapshot.revision) {
preserveSaveErrorRevision.current = undefined;
} else {
preserveSaveErrorRevision.current = undefined;
setSaveError(undefined);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/features/SettingsDialog.tsx` around lines 50 - 58,
Update the snapshot-change useEffect to clear preserveSaveErrorRevision.current
in both branches, including when the current revision matches; ensure no
recorded revision marker survives after processing any snapshot change.

Comment on lines +88 to +93
if (server.type === "http") {
literals.push({ path: `${prefix}.url`, value: server.url });
collectRecordValues(literals, `${prefix}.headers`, server.headers);
} else {
collectRecordValues(literals, `${prefix}.env`, server.env);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect SecretLiteralRegistry bounds and MCP literal validation.
rg -n -C8 'class SecretLiteralRegistry' packages/agent-core/src/config/runtime-secret-literals.ts
rg -n -C6 'validateMcpSecretLiteralPolicy' packages/agent-core/src/config/server-config-service.ts

Repository: boh5/archcode

Length of output: 1813


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- runtime-secret-literals.ts ---'
sed -n '1,125p' packages/agent-core/src/config/runtime-secret-literals.ts
printf '%s\n' '--- server-config-service.ts ---'
sed -n '840,910p' packages/agent-core/src/config/server-config-service.ts
printf '%s\n' '--- related references ---'
rg -n -C4 'SecretLiteralRegistry|runtime secret literal|MCP.*literal|short literal|minimum.*length|MIN.*LENGTH' packages/agent-core/src packages/agent-core/test packages 2>/dev/null | head -300

Repository: boh5/archcode

Length of output: 33678


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- secret literal bounds ---'
rg -n -C5 'SECRET_LITERAL_(MIN|MAX)' packages/agent-core/src/security packages/agent-core/src
printf '%s\n' '--- runtime collection tests and activation callers ---'
rg -n -C6 'collectRuntimeSecretLiterals|Runtime secret literal configuration|mcp\.servers\..*\.url|server\.url' packages/agent-core/src --glob '*.{ts,tsx}' | head -400
printf '%s\n' '--- MCP save-validation tests ---'
sed -n '1450,1515p' packages/agent-core/src/config/server-config-service.test.ts

Repository: boh5/archcode

Length of output: 24755


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- MCP URL schema and resolution ---'
rg -n -C8 'url:|mcpHttpServerConfigSchema|mcp.*http|http.*server' packages/agent-core/src/config/mcp.ts
printf '%s\n' '--- runtime startup and redaction setup ---'
sed -n '525,560p' packages/agent-core/src/runtime.ts
printf '%s\n' '--- runtime literal tests ---'
sed -n '1,145p' packages/agent-core/src/config/runtime-secret-literals.test.ts

Repository: boh5/archcode

Length of output: 8649


🏁 Script executed:

#!/bin/bash
sed -n '268,286p' packages/agent-core/src/config/mcp.ts

node - <<'JS'
const candidates = ["http://a", "http://", "https://a", "h://a", "x:", "http:a"];
for (const value of candidates) {
  try {
    const parsed = new URL(value);
    const accepted = parsed.protocol === "http:" || parsed.protocol === "https:";
    console.log(JSON.stringify({ value, bytes: Buffer.byteLength(value, "utf8"), parsed: true, accepted }));
  } catch {
    console.log(JSON.stringify({ value, bytes: Buffer.byteLength(value, "utf8"), parsed: false, accepted: false }));
  }
}
JS

Repository: boh5/archcode

Length of output: 1162


Align MCP URL validation with runtime literal collection.

SECRET_LITERAL_MIN_BYTES is 8, but validateMcpSecretLiteralPolicy excludes server.url. The current URL validation accepts http:a (6 bytes), so save succeeds and runtime initialization rejects the same configuration. Exclude server.url from collectRuntimeSecretLiterals, or validate it during save. Add a regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/agent-core/src/config/runtime-secret-literals.ts` around lines 88 -
93, Update collectRuntimeSecretLiterals around the HTTP server branch so
server.url is excluded from runtime secret literal collection, keeping header
and environment collection unchanged; add a regression test confirming an HTTP
URL such as http:a follows the same save and runtime validation behavior.

readonly #familyControls = new Map<string, SessionFamilyControlState>();
readonly #runtimeMutations = new Set<symbol>();
readonly #runtimeChangeListeners = new Set<SessionRuntimeChangeListener>();
readonly #executionSkillSnapshots = new Map<string, ReadonlyMap<string, SkillPackageSnapshot>>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

#executionSkillSnapshots is not evicted when a Session is deleted.

Entries are removed in #finalizeExecution, #terminalizeSuspendedForInspection, and #terminalizeSuspendedFamily. All three require the execution to reach a terminal boundary. deleteSession (line 2476) disposes agents, removes stores, and deletes session directories, but it leaves cached snapshot maps behind. If a Session with a suspended execution is deleted, its SkillPackageSnapshot map (including the skill body text) stays reachable for the process lifetime.

Delete the cached entries for every removed session id inside deleteSession.

🧹 Proposed eviction on session deletion
       for (const id of sessionIds) {
         this.#config.sessionAgentManager.dispose(workspaceRoot, id);
         this.#config.untrackSession(workspaceRoot, id);
+        const prefix = `${scopedKey(workspaceRoot, id)}\0`;
+        for (const key of this.#executionSkillSnapshots.keys()) {
+          if (key.startsWith(prefix)) this.#executionSkillSnapshots.delete(key);
+        }
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/agent-core/src/execution/session-execution-manager.ts` at line 439,
Update deleteSession to remove every deleted session ID from
`#executionSkillSnapshots` before completing session cleanup. Ensure suspended and
non-terminal executions are evicted even when terminalization methods are not
invoked, while preserving the existing agent, store, and directory deletion
behavior.

Comment on lines +185 to +205
async testServer(
serverName: string,
config: ResolvedMcpServerConfig,
options: { signal?: AbortSignal } = {},
): Promise<McpTestResult> {
this.#assertOpen();
const testKey = `${serverName}:${stableSerialize(config)}`;
if (this.#testsInFlight.has(testKey)) {
throw new Error(`MCP server test already in progress for "${serverName}"`);
}
const controller = new AbortController();
const removeAbortForwarder = forwardAbort(options.signal, controller);
const flight: DraftTestFlight = { controller };
const promise = this.#runDraftTest(serverName, config, flight).finally(() => {
removeAbortForwarder();
if (this.#testsInFlight.get(testKey) === flight) this.#testsInFlight.delete(testKey);
});
flight.promise = promise;
this.#testsInFlight.set(testKey, flight);
return await promise;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound the number of concurrent draft tests.

testKey includes stableSerialize(config), so the duplicate guard only blocks an identical repeat. A caller that varies any field (URL, header value, args, serverName) starts an unlimited number of parallel drafts. Each draft opens a live HTTP connection or spawns a stdio child process through McpClient, and nothing releases the slot until the connect and discovery timeouts expire. A settings client that submits drafts in a loop can exhaust sockets or process slots for the whole runtime.

Add a maximum in-flight draft count and reject beyond it.

🛡️ Sketch
+  static readonly `#MAX_DRAFT_TESTS` = 4;
+
   async testServer(
     serverName: string,
     config: ResolvedMcpServerConfig,
     options: { signal?: AbortSignal } = {},
   ): Promise<McpTestResult> {
     this.#assertOpen();
     const testKey = `${serverName}:${stableSerialize(config)}`;
     if (this.#testsInFlight.has(testKey)) {
       throw new Error(`MCP server test already in progress for "${serverName}"`);
     }
+    if (this.#testsInFlight.size >= McpRuntimeService.#MAX_DRAFT_TESTS) {
+      throw new Error("Too many MCP server tests are in progress");
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async testServer(
serverName: string,
config: ResolvedMcpServerConfig,
options: { signal?: AbortSignal } = {},
): Promise<McpTestResult> {
this.#assertOpen();
const testKey = `${serverName}:${stableSerialize(config)}`;
if (this.#testsInFlight.has(testKey)) {
throw new Error(`MCP server test already in progress for "${serverName}"`);
}
const controller = new AbortController();
const removeAbortForwarder = forwardAbort(options.signal, controller);
const flight: DraftTestFlight = { controller };
const promise = this.#runDraftTest(serverName, config, flight).finally(() => {
removeAbortForwarder();
if (this.#testsInFlight.get(testKey) === flight) this.#testsInFlight.delete(testKey);
});
flight.promise = promise;
this.#testsInFlight.set(testKey, flight);
return await promise;
}
static readonly `#MAX_DRAFT_TESTS` = 4;
async testServer(
serverName: string,
config: ResolvedMcpServerConfig,
options: { signal?: AbortSignal } = {},
): Promise<McpTestResult> {
this.#assertOpen();
const testKey = `${serverName}:${stableSerialize(config)}`;
if (this.#testsInFlight.has(testKey)) {
throw new Error(`MCP server test already in progress for "${serverName}"`);
}
if (this.#testsInFlight.size >= McpRuntimeService.#MAX_DRAFT_TESTS) {
throw new Error("Too many MCP server tests are in progress");
}
const controller = new AbortController();
const removeAbortForwarder = forwardAbort(options.signal, controller);
const flight: DraftTestFlight = { controller };
const promise = this.#runDraftTest(serverName, config, flight).finally(() => {
removeAbortForwarder();
if (this.#testsInFlight.get(testKey) === flight) this.#testsInFlight.delete(testKey);
});
flight.promise = promise;
this.#testsInFlight.set(testKey, flight);
return await promise;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/agent-core/src/mcp/runtime-service.ts` around lines 185 - 205,
Update testServer and its in-flight test state to enforce a shared maximum
number of concurrent draft tests, rejecting new drafts once the limit is reached
while preserving the existing duplicate testKey guard. Ensure the count is
incremented only for accepted tests and decremented in the existing
promise.finally cleanup so slots are released on every completion path.

Comment on lines +922 to +931
available: z.strictObject({
includedEntries: z.array(z.strictObject({
name: z.string(),
description: z.string(),
source: z.enum(SKILL_SOURCE_TIERS),
})),
omittedCount: z.number().int().nonnegative(),
renderedText: z.string(),
byteLength: z.number().int().nonnegative().max(8_000),
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

byteLength does not bound renderedText.

byteLength is a self-declared number capped at 8,000. renderedText has no bound. A persisted trace can declare byteLength: 6 and still carry an arbitrarily large renderedText, so the cap does not limit the persisted payload. Other bounded strings in this file use boundedUtf8String(...).

Tie the two values together, or bound renderedText directly.

🛡️ Proposed fix
     available: z.strictObject({
       includedEntries: z.array(z.strictObject({
         name: z.string(),
         description: z.string(),
         source: z.enum(SKILL_SOURCE_TIERS),
       })),
       omittedCount: z.number().int().nonnegative(),
-      renderedText: z.string(),
+      renderedText: boundedUtf8String(8_000),
       byteLength: z.number().int().nonnegative().max(8_000),
-    }),
+    }).refine(
+      (available) => utf8Bytes(available.renderedText) === available.byteLength,
+      "byteLength must equal the rendered catalog size",
+    ),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/agent-core/src/store/helpers.ts` around lines 922 - 931, Update the
available schema’s renderedText field alongside byteLength so persisted text is
directly limited to the 8,000-byte maximum, using the existing boundedUtf8String
helper and preserving the current byteLength validation.

import { formatResolvedSkillResource, SkillReadInputSchema, skillReadTool } from "./skill-read";

const tmpRoot = join(import.meta.dir, "__test_tmp__", "skill-read", crypto.randomUUID());
const tmpRoot = join(tmpdir(), "archcode-skill-read-tool", crypto.randomUUID());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find SkillService test constructions that omit userAgentsSkillsRoot.
rg -nP --type=ts -C2 'new SkillService\(' -g '**/*.test.ts' -g '**/*.ts'

Repository: boh5/archcode

Length of output: 151


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
git ls-files 'packages/agent-core/src/tools/builtins/*skill*' 'packages/agent-core/src/**/*SkillService*'
printf '%s\n' '--- SkillService references ---'
rg -n -C3 'SkillService|userAgentsSkillsRoot|userSkillsRoot' packages/agent-core/src
printf '%s\n' '--- test file outline ---'
if [ -f packages/agent-core/src/tools/builtins/skill-read.test.ts ]; then
  ast-grep outline packages/agent-core/src/tools/builtins/skill-read.test.ts
fi

Repository: boh5/archcode

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- skill-read.test.ts ---'
sed -n '1,90p' packages/agent-core/src/tools/builtins/skill-read.test.ts
printf '%s\n' '--- SkillService implementation candidates ---'
fd -t f -e ts . packages/agent-core/src/skills | sort
printf '%s\n' '--- defaults and tier resolution ---'
rg -n -C5 'userAgentsSkillsRoot|homedir\(\)|agents.*skills|resolve.*Skill|listSkills|readSkill' packages/agent-core/src/skills

Repository: boh5/archcode

Length of output: 12825


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- SkillService catalog/read paths ---'
sed -n '112,230p' packages/agent-core/src/skills/service.ts
sed -n '320,370p' packages/agent-core/src/skills/service.ts
printf '%s\n' '--- skill-read assertions and service constructions ---'
rg -n -C4 'catalog|read|not found|new SkillService|skillReadTool.execute|expect\(.*toEqual|toContain' packages/agent-core/src/tools/builtins/skill-read.test.ts

Repository: boh5/archcode

Length of output: 18116


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re

service = Path("packages/agent-core/src/skills/service.ts").read_text()
test = Path("packages/agent-core/src/tools/builtins/skill-read.test.ts").read_text()

default = re.search(
    r"userAgentsSkillsRoot\s*=\s*resolve\(options\.userAgentsSkillsRoot \?\? join\(homedir\(\), \"\.agents\", \"skills\"\)\)",
    service,
)
source_order = re.search(
    r'\{ source: "user-archcode".*?\n\s*\{ source: "user-agents".*?root: this\.userAgentsSkillsRoot',
    service,
    re.S,
)
context = re.search(
    r"new SkillService\(\{\s*userSkillsRoot\s*\}\)",
    test,
)
unknown_case = re.search(
    r'test\("unknown skill name returns structured error".*?makeContext\(\["missing-skill"\]\)',
    test,
    re.S,
)

print({
    "defaults_user_agents_root_to_home": bool(default),
    "user_agents_is_a_resolution_tier": bool(source_order),
    "skill_read_context_omits_user_agents_root": bool(context),
    "unknown_skill_case_depends_on_resolution": bool(unknown_case),
})
if not all((default, source_order, context, unknown_case)):
    raise SystemExit("expected evidence was not found")
PY

Repository: boh5/archcode

Length of output: 338


Isolate the user-agents skill root in this suite.

Pass a temporary userAgentsSkillsRoot to SkillService. Otherwise, skill_read can resolve skills from $HOME/.agents/skills, making the “unknown skill” test environment-dependent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/agent-core/src/tools/builtins/skill-read.test.ts` at line 14, Update
the SkillService setup in the skill-read test suite to pass an isolated
temporary userAgentsSkillsRoot under tmpRoot, ensuring skill_read resolves only
from the test directory. Keep the unknown-skill test independent of any skills
installed in $HOME/.agents/skills.

Comment on lines +119 to +132
executeResolved(
descriptor: AnyToolDescriptor,
toolCall: ToolCallLike,
context: ToolExecutionContext,
): Promise<RegistryExecutionOutcome> {
if (descriptor.name !== toolCall.toolName) {
return this.settleSystem(toolCall, context, createToolErrorResult({
kind: "unknown-tool",
code: "TOOL_UNKNOWN",
message: `Resolved tool "${descriptor.name}" does not match call "${toolCall.toolName}"`,
}));
}
return this.#execute(toolCall, context, undefined, descriptor);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Validate run-local descriptors before execution.

executeResolved() bypasses the checks in register(). A descriptor with traits.destructive: true and no permissions can reach #execute() and execute without a permission decision.

Extract the descriptor invariant checks from register(). Apply them before executing a resolved descriptor.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/agent-core/src/tools/registry.ts` around lines 119 - 132, Extract
the descriptor invariant validation currently performed by register() into a
reusable helper, then invoke it from executeResolved() before calling
`#execute`(). Ensure resolved descriptors with traits.destructive set but no
permissions are rejected consistently with registered descriptors, while
preserving the existing tool-name mismatch handling.

Comment on lines +219 to +227
function isExecutionSkillBinding(value: unknown): boolean {
const binding = record(value);
return binding !== undefined
&& exact(binding, ["name", "source", "digest", "resolutionRoot"])
&& isString(binding.name)
&& oneOf(binding.source, ["project-archcode", "project-agents", "user-archcode", "user-agents", "builtin"])
&& isString(binding.digest)
&& isString(binding.resolutionRoot);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Validate the execution Skill digest format.

isExecutionSkillBinding accepts any string for digest. It therefore accepts malformed claim-time Skill identities. Require the declared 64-character digest format and add rejection coverage for short or malformed values.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/protocol/src/guards.ts` around lines 219 - 227, Update
isExecutionSkillBinding to validate digest as a 64-character hexadecimal string
rather than any string, while preserving the existing field and source checks.
Add rejection coverage for short and malformed digest values.

Comment on lines +973 to +997
export interface ConfigMcpTimeoutSettings {
connectTimeoutMs?: number;
discoveryTimeoutMs?: number;
callTimeoutMs?: number;
}

export interface ConfigMcpHttpServerSettings<Secret> extends ConfigMcpTimeoutSettings {
type: "http";
enabled: boolean;
url: string;
headers?: Record<string, Secret>;
timeout?: number;
}

export interface ConfigMcpStdioServerSettings<Secret> extends ConfigMcpTimeoutSettings {
type: "stdio";
enabled: boolean;
command: string;
args?: string[];
env?: Record<string, Secret>;
}

export type ConfigMcpServerSettings<Secret> =
| ConfigMcpHttpServerSettings<Secret>
| ConfigMcpStdioServerSettings<Secret>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether README.md still documents the removed MCP configuration shape.
rg -n -C5 '"mcp"|mcp\.servers|timeout|disabledBuiltins' README.md

Repository: boh5/archcode

Length of output: 151


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- tracked documentation files ---'
git ls-files | rg '(^|/)(README\.md|config\.example\.json|configuration\.md)$' || true

printf '%s\n' '--- README references ---'
if [ -f README.md ]; then
  rg -n -i -C3 'MCP|mcp|disabledBuiltins|connectTimeoutMs|discoveryTimeoutMs|callTimeoutMs|timeout' README.md || true
else
  printf '%s\n' 'README.md not found at repository root'
fi

printf '%s\n' '--- relevant configuration documentation ---'
for f in $(git ls-files | rg '(^|/)(config\.example\.json|configuration\.md)$'); do
  printf '\n### %s\n' "$f"
  rg -n -i -C2 'MCP|mcp|disabledBuiltins|connectTimeoutMs|discoveryTimeoutMs|callTimeoutMs|timeout' "$f" || true
done

printf '%s\n' '--- changed-file summary ---'
git diff --stat
git diff --name-only

Repository: boh5/archcode

Length of output: 3861


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- protocol MCP types ---'
sed -n '960,1030p' packages/protocol/src/types.ts

printf '%s\n' '--- README configuration and integration sections ---'
sed -n '125,185p' README.md

printf '%s\n' '--- configuration example MCP section ---'
sed -n '35,70p' config.example.json

printf '%s\n' '--- repository references to legacy and current MCP fields ---'
rg -n '(^|[^A-Za-z])(timeout|connectTimeoutMs|discoveryTimeoutMs|callTimeoutMs|disabledBuiltins)([^A-Za-z]|$)' --glob '*.ts' --glob '*.tsx' --glob '*.md' --glob '*.json' .

Repository: boh5/archcode

Length of output: 50369


Update README.md with the MCP configuration schema.

The README does not document the breaking MCP changes: required type and enabled, the three timeout fields replacing timeout, and mcp.disabledBuiltins.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/protocol/src/types.ts` around lines 973 - 997, Update README.md to
document the MCP configuration schema represented by ConfigMcpServerSettings,
including required type and enabled fields, HTTP and stdio-specific properties,
the connectTimeoutMs, discoveryTimeoutMs, and callTimeoutMs fields replacing
timeout, and the mcp.disabledBuiltins setting.

Source: Coding guidelines

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

23 issues found across 168 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/agent-core/src/runtime.ts">

<violation number="1" location="packages/agent-core/src/runtime.ts:1384">
P2: Invalid `/skill use ...` commands cannot be retried with the same `clientRequestId`: the first attempt is stored as a completed command, but this specialized replay lookup treats that command receipt as an idempotency conflict. Preserve the generic command-replay path for validation failures or existing command receipts before attempting Skill-message replay.</violation>
</file>

<file name="packages/agent-core/src/config/mcp.test.ts">

<violation number="1" location="packages/agent-core/src/config/mcp.test.ts:303">
P3: The URL-scheme allow-list is now tested only against literal URLs. The previous test that validated the http/https restriction on a scheme produced by env expansion ("rejects non-http URL after env expansion", e.g. url "${MCP_PROTO}://chat" with MCP_PROTO=ws) was removed and not replaced, and the new resolveMcpConfig env-expansion tests only expand to valid http/https schemes. Since resolveMcpConfig expands the URL before validateHttpUrl (expandString then validateHttpUrl), a scheme-injection through an env override is a real security-boundary path that currently has no regression coverage.</violation>
</file>

<file name="packages/agent-core/src/skills/pagination.ts">

<violation number="1" location="packages/agent-core/src/skills/pagination.ts:52">
P2: Empty pages can exceed `maxSerializedBytes` because this fast path bypasses serialization; validate the empty page against the same byte limit (or throw) so the helper preserves its advertised bound.</violation>
</file>

<file name="apps/server/src/routes/mcp.ts">

<violation number="1" location="apps/server/src/routes/mcp.ts:48">
P2: Testing a stale or semantically invalid draft returns `INTERNAL_ERROR` instead of actionable revision/validation errors, so Settings cannot distinguish a fixable draft problem from a runtime failure. Catch `ConfigRevisionConflictError` and `ConfigSemanticValidationError` here and map them through the existing config HTTP errors.</violation>
</file>

<file name="packages/agent-core/src/skills/projection.ts">

<violation number="1" location="packages/agent-core/src/skills/projection.ts:17">
P2: Large Skill directories make prompt construction quadratic despite the 8 KB output cap, delaying or timing out every model call. Build the included prefix incrementally (then account for the omission footer) so projection work remains linear in discovered Skills.</violation>
</file>

<file name="packages/protocol/src/types.ts">

<violation number="1" location="packages/protocol/src/types.ts:29">
P1: Pre-update session files will no longer open because these new persisted fields are required but no load-time migration defaults legacy snapshots. Migrate missing Skill fields before strict validation (including queued messages and prompt traces) or version the session format.</violation>
</file>

<file name="packages/agent-core/src/tools/registry.ts">

<violation number="1" location="packages/agent-core/src/tools/registry.ts:131">
P1: Destructive MCP tools execute without the confirmation guard because `executeResolved` bypasses `register`'s required-permission invariant. Validate resolved descriptors or attach the same permission policy before executing them.</violation>
</file>

<file name="packages/agent-core/src/testing/test-mcp-runtime.ts">

<violation number="1" location="packages/agent-core/src/testing/test-mcp-runtime.ts:55">
P2: Snapshots share the caller-owned descriptor map instead of being run-local; mutations through the supplied `Map` or a snapshot leak into later model boundaries. Return a fresh map while retaining descriptor identities, matching `McpRuntimeService`.</violation>
</file>

<file name="packages/agent-core/src/execution/session-execution-manager.ts">

<violation number="1" location="packages/agent-core/src/execution/session-execution-manager.ts:439">
P2: Deleting a suspended Session leaves its full execution Skill snapshot retained for the manager lifetime because `deleteSession` never removes entries from this map; clear all snapshot keys for deleted session IDs during deletion.</violation>
</file>

<file name="packages/agent-core/src/skills/service.test.ts">

<violation number="1" location="packages/agent-core/src/skills/service.test.ts:91">
P2: The rewritten reserved-builtin test drops the Agent-gating assertions that the replaced test covered: it only checks unshadowability for the first reserved name and no longer verifies that a reserved lifecycle builtin is excluded when the agent's allowedNames don't include it (readForAgent → null) and omitted from listForAgent. That gating is an ownership/security boundary (reserved lifecycle skills must only be reachable by the owning agent), so a regression there would now pass CI. Consider restoring the loop over all RESERVED_BUILTIN_SKILL_NAMES and re-adding the `readForAgent(... ["codemap"])` → null and listing-exclusion assertions.</violation>
</file>

<file name="packages/agent-core/src/skills/package-reader.ts">

<violation number="1" location="packages/agent-core/src/skills/package-reader.ts:187">
P2: Atomic Skill package replacement during capture can produce a snapshot with the old `SKILL.md` and new resources; retain initial root identity and verify it after capture before publishing the execution snapshot.

(Based on your team's feedback about bounded Skill filesystem race defenses.)</violation>

<violation number="2" location="packages/agent-core/src/skills/package-reader.ts:230">
P3: Some invalid resource paths still return `TOOL_SKILL_READ_FAILED` instead of the new invalid-input result; throw `SkillPackageResourcePathError` for the depth and entry-path validation branches too.</violation>
</file>

<file name="packages/agent-core/src/config/server-config-service.ts">

<violation number="1" location="packages/agent-core/src/config/server-config-service.ts:863">
P1: Saving MCP credentials can commit a configuration that prevents Runtime startup after restart because this checks MCP literals in isolation rather than the combined runtime registry. Validate the full persisted runtime literal set (and account for externally supplied literals) before committing.</violation>
</file>

<file name="packages/agent-core/src/mcp/tool-adapter.ts">

<violation number="1" location="packages/agent-core/src/mcp/tool-adapter.ts:100">
P1: A post-dispatch effectful call with a generic SDK/network failure is treated as an ordinary retryable error, although the remote server may already have applied it. Mark every dispatched effectful failure as `unknownResult` so scheduler requires inspection before continuation.</violation>
</file>

<file name="packages/agent-core/src/execution/session-tool-batch-scheduler.ts">

<violation number="1" location="packages/agent-core/src/execution/session-tool-batch-scheduler.ts:1358">
P2: A failed tool-result checkpoint can erase a concurrent event-less batch transition, such as another parallel call becoming blocked, because the event-id guard does not detect state patches. Preserve/rebase later store changes rather than replacing the full state snapshot on persistence failure.</violation>
</file>

<file name="packages/agent-core/src/skills/types.ts">

<violation number="1" location="packages/agent-core/src/skills/types.ts:1">
P2: Keep source tiers in one shared contract; this duplicate tuple can let protocol validation and agent Skill resolution diverge when a tier changes. Re-export the protocol tuple here (and use it for resolution order where applicable).</violation>
</file>

<file name="apps/web/src/components/features/settings-panels.tsx">

<violation number="1" location="apps/web/src/components/features/settings-panels.tsx:558">
P2: Closing Settings or leaving MCP does not cancel an in-flight draft test, so a remote HTTP/STDIO discovery can continue until its timeout; wire an AbortController through `testMcpDraft` and abort it during panel cleanup/inactivation.</violation>

<violation number="2" location="apps/web/src/components/features/settings-panels.tsx:563">
P2: A reconnect can overwrite newer status updates for other MCP servers with its stale snapshot, leaving their displayed state incorrect until another event; merge/version status snapshots rather than replacing the SSE-owned map.</violation>

<violation number="3" location="apps/web/src/components/features/settings-panels.tsx:601">
P2: The STDIO arguments parser currently keeps blank lines as `""`, so a trailing newline or spacer line is persisted as an actual argv entry. Filtering out empty lines before assigning `draft.args` avoids passing unintended empty arguments to the child process.</violation>
</file>

<file name="packages/agent-core/src/mcp/client.ts">

<violation number="1" location="packages/agent-core/src/mcp/client.ts:372">
P1: STDIO stderr can leak configured tokens when a secret spans two data chunks, because each chunk is redacted independently. Buffer a bounded overlap of prior bytes and redact before emitting log output.</violation>

<violation number="2" location="packages/agent-core/src/mcp/client.ts:378">
P2: A continuous STDERR writer can generate unlimited logs: this 4 KiB cap applies per chunk, not per connection. Track a cumulative stderr budget or rate-limit/drop subsequent output after it is exhausted.</violation>
</file>

<file name="packages/agent-core/src/execution/session-execution-manager.test.ts">

<violation number="1" location="packages/agent-core/src/execution/session-execution-manager.test.ts:1133">
P2: The restart test waits for the terminal `failed` status with a fixed 20-iteration `setTimeout(resolve, 0)` microtask poll. The failed record is written and flushed in `#runExecution`'s finally block on a different tick from the reconcile rejection, so the loop is a timing bet that can flake under load. Consider awaiting the execution's terminal settlement deterministically (e.g. expose/await a settled signal) or polling with a real deadline and larger/macrotask spacing so the assertion isn't sensitive to scheduler timing.</violation>
</file>

<file name="apps/web/src/components/features/SettingsDialog.tsx">

<violation number="1" location="apps/web/src/components/features/SettingsDialog.tsx:57">
P2: The snapshot-change effect clears `saveError` in the mismatch branch but leaves `preserveSaveErrorRevision.current` set. That stale marker can match a later snapshot revision and incorrectly preserve an old MCP-apply error. Clearing the ref in both branches keeps error preservation scoped to the intended revision.</violation>
</file>

Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.

Re-trigger cubic

maxSteps: number;
activeTimeoutMs?: number;
binding: ExecutionModelBindingSummary;
executionSkills: ExecutionSkillBinding[];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Pre-update session files will no longer open because these new persisted fields are required but no load-time migration defaults legacy snapshots. Migrate missing Skill fields before strict validation (including queued messages and prompt traces) or version the session format.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/protocol/src/types.ts, line 29:

<comment>Pre-update session files will no longer open because these new persisted fields are required but no load-time migration defaults legacy snapshots. Migrate missing Skill fields before strict validation (including queued messages and prompt traces) or version the session format.</comment>

<file context>
@@ -26,6 +26,55 @@ export interface ExecutionStartEvent {
   maxSteps: number;
   activeTimeoutMs?: number;
   binding: ExecutionModelBindingSummary;
+  executionSkills: ExecutionSkillBinding[];
+}
+
</file context>

message: `Resolved tool "${descriptor.name}" does not match call "${toolCall.toolName}"`,
}));
}
return this.#execute(toolCall, context, undefined, descriptor);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Destructive MCP tools execute without the confirmation guard because executeResolved bypasses register's required-permission invariant. Validate resolved descriptors or attach the same permission policy before executing them.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/agent-core/src/tools/registry.ts, line 131:

<comment>Destructive MCP tools execute without the confirmation guard because `executeResolved` bypasses `register`'s required-permission invariant. Validate resolved descriptors or attach the same permission policy before executing them.</comment>

<file context>
@@ -122,6 +116,21 @@ export class ToolRegistry {
+        message: `Resolved tool "${descriptor.name}" does not match call "${toolCall.toolName}"`,
+      }));
+    }
+    return this.#execute(toolCall, context, undefined, descriptor);
+  }
+
</file context>

}
if (literals.length === 0) return;
try {
new SecretLiteralRegistry(literals);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Saving MCP credentials can commit a configuration that prevents Runtime startup after restart because this checks MCP literals in isolation rather than the combined runtime registry. Validate the full persisted runtime literal set (and account for externally supplied literals) before committing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/agent-core/src/config/server-config-service.ts, line 863:

<comment>Saving MCP credentials can commit a configuration that prevents Runtime startup after restart because this checks MCP literals in isolation rather than the combined runtime registry. Validate the full persisted runtime literal set (and account for externally supplied literals) before committing.</comment>

<file context>
@@ -761,6 +841,35 @@ function validateConfig(value: unknown): ArchCodeConfig {
+  }
+  if (literals.length === 0) return;
+  try {
+    new SecretLiteralRegistry(literals);
+  } catch (cause) {
+    if (cause instanceof ConfigSemanticValidationError) {
</file context>

: reason === "timeout"
? "TOOL_MCP_CALL_TIMEOUT"
: "TOOL_MCP_ERROR";
const unknownResult = attempted && !traits.readOnly && (reason === "aborted" || reason === "timeout");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: A post-dispatch effectful call with a generic SDK/network failure is treated as an ordinary retryable error, although the remote server may already have applied it. Mark every dispatched effectful failure as unknownResult so scheduler requires inspection before continuation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/agent-core/src/mcp/tool-adapter.ts, line 100:

<comment>A post-dispatch effectful call with a generic SDK/network failure is treated as an ordinary retryable error, although the remote server may already have applied it. Mark every dispatched effectful failure as `unknownResult` so scheduler requires inspection before continuation.</comment>

<file context>
@@ -2,100 +2,126 @@ import { jsonSchema } from "ai";
+          : reason === "timeout"
+            ? "TOOL_MCP_CALL_TIMEOUT"
+            : "TOOL_MCP_ERROR";
+        const unknownResult = attempted && !traits.readOnly && (reason === "aborted" || reason === "timeout");
         return createMcpErrorResult(
           serverName,
</file context>
Suggested change
const unknownResult = attempted && !traits.readOnly && (reason === "aborted" || reason === "timeout");
const unknownResult = attempted && !traits.readOnly;

}

#attachBoundedStderr(): void {
this.#transport.stderr?.on("data", (chunk) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: STDIO stderr can leak configured tokens when a secret spans two data chunks, because each chunk is redacted independently. Buffer a bounded overlap of prior bytes and redact before emitting log output.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/agent-core/src/mcp/client.ts, line 372:

<comment>STDIO stderr can leak configured tokens when a secret spans two data chunks, because each chunk is redacted independently. Buffer a bounded overlap of prior bytes and redact before emitting log output.</comment>

<file context>
@@ -257,68 +280,171 @@ export class McpClient {
+  }
+
+  #attachBoundedStderr(): void {
+    this.#transport.stderr?.on("data", (chunk) => {
+      const raw = typeof chunk === "string"
+        ? chunk
</file context>

restartError = error;
}
expect(restartError).toMatchObject({ code: "SKILL_PACKAGE_CHANGED" });
for (let attempt = 0; attempt < 20 && coldStore.getState().executions[0]?.status !== "failed"; attempt += 1) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The restart test waits for the terminal failed status with a fixed 20-iteration setTimeout(resolve, 0) microtask poll. The failed record is written and flushed in #runExecution's finally block on a different tick from the reconcile rejection, so the loop is a timing bet that can flake under load. Consider awaiting the execution's terminal settlement deterministically (e.g. expose/await a settled signal) or polling with a real deadline and larger/macrotask spacing so the assertion isn't sensitive to scheduler timing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/agent-core/src/execution/session-execution-manager.test.ts, line 1133:

<comment>The restart test waits for the terminal `failed` status with a fixed 20-iteration `setTimeout(resolve, 0)` microtask poll. The failed record is written and flushed in `#runExecution`'s finally block on a different tick from the reconcile rejection, so the loop is a timing bet that can flake under load. Consider awaiting the execution's terminal settlement deterministically (e.g. expose/await a settled signal) or polling with a real deadline and larger/macrotask spacing so the assertion isn't sensitive to scheduler timing.</comment>

<file context>
@@ -842,6 +845,301 @@ describe("SessionExecutionManager", () => {
+      restartError = error;
+    }
+    expect(restartError).toMatchObject({ code: "SKILL_PACKAGE_CHANGED" });
+    for (let attempt = 0; attempt < 20 && coldStore.getState().executions[0]?.status !== "failed"; attempt += 1) {
+      await new Promise<void>((resolve) => setTimeout(resolve, 0));
+    }
</file context>

<Field label="Transport"><select className={selectClass} value={server.type} onChange={(event) => replaceTransport(event.target.value as "http" | "stdio")}><option value="http">HTTP</option><option value="stdio">STDIO</option></select></Field>
{server.type === "http" ? <Field label="HTTP URL" error={errors[`mcp.servers.${name}.url`]}><TextInput value={server.url} onChange={(next) => update((draft) => { if (draft.type === "http") draft.url = next; })} /></Field> : <>
<Field label="Command" error={errors[`mcp.servers.${name}.command`]}><TextInput value={server.command} onChange={(next) => update((draft) => { if (draft.type === "stdio") draft.command = next; })} /></Field>
<Field label="Arguments (one per line)"><textarea rows={3} value={server.args?.join("\n") ?? ""} onChange={(event) => update((draft) => { if (draft.type === "stdio") draft.args = event.target.value ? event.target.value.split("\n") : undefined; })} className="min-h-20 resize-y rounded-sm border border-border-control bg-bg-base px-3 py-2 font-mono text-[12px] leading-[18px] text-text-primary outline-none transition-colors duration-[var(--motion-hover)] hover:border-text-secondary focus:border-brand focus:ring-2 focus:ring-brand-subtle" /></Field>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The STDIO arguments parser currently keeps blank lines as "", so a trailing newline or spacer line is persisted as an actual argv entry. Filtering out empty lines before assigning draft.args avoids passing unintended empty arguments to the child process.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/components/features/settings-panels.tsx, line 601:

<comment>The STDIO arguments parser currently keeps blank lines as `""`, so a trailing newline or spacer line is persisted as an actual argv entry. Filtering out empty lines before assigning `draft.args` avoids passing unintended empty arguments to the child process.</comment>

<file context>
@@ -406,18 +547,66 @@ export function SettingsMcpPanel({ config, servers, onChange, errors = {}, runti
+      <Field label="Transport"><select className={selectClass} value={server.type} onChange={(event) => replaceTransport(event.target.value as "http" | "stdio")}><option value="http">HTTP</option><option value="stdio">STDIO</option></select></Field>
+      {server.type === "http" ? <Field label="HTTP URL" error={errors[`mcp.servers.${name}.url`]}><TextInput value={server.url} onChange={(next) => update((draft) => { if (draft.type === "http") draft.url = next; })} /></Field> : <>
+        <Field label="Command" error={errors[`mcp.servers.${name}.command`]}><TextInput value={server.command} onChange={(next) => update((draft) => { if (draft.type === "stdio") draft.command = next; })} /></Field>
+        <Field label="Arguments (one per line)"><textarea rows={3} value={server.args?.join("\n") ?? ""} onChange={(event) => update((draft) => { if (draft.type === "stdio") draft.args = event.target.value ? event.target.value.split("\n") : undefined; })} className="min-h-20 resize-y rounded-sm border border-border-control bg-bg-base px-3 py-2 font-mono text-[12px] leading-[18px] text-text-primary outline-none transition-colors duration-[var(--motion-hover)] hover:border-text-secondary focus:border-brand focus:ring-2 focus:ring-brand-subtle" /></Field>
+      </>}
+      <Field label="Connect timeout (ms)"><NumberField value={server.connectTimeoutMs} onChange={(next) => update((draft) => { draft.connectTimeoutMs = next; })} /></Field>
</file context>
Suggested change
<Field label="Arguments (one per line)"><textarea rows={3} value={server.args?.join("\n") ?? ""} onChange={(event) => update((draft) => { if (draft.type === "stdio") draft.args = event.target.value ? event.target.value.split("\n") : undefined; })} className="min-h-20 resize-y rounded-sm border border-border-control bg-bg-base px-3 py-2 font-mono text-[12px] leading-[18px] text-text-primary outline-none transition-colors duration-[var(--motion-hover)] hover:border-text-secondary focus:border-brand focus:ring-2 focus:ring-brand-subtle" /></Field>
<Field label="Arguments (one per line)"><textarea rows={3} value={server.args?.join("\n") ?? ""} onChange={(event) => update((draft) => { if (draft.type === "stdio") { const args = event.target.value.split("\n").filter((line) => line.trim() !== ""); draft.args = args.length > 0 ? args : undefined; } })} className="min-h-20 resize-y rounded-sm border border-border-control bg-bg-base px-3 py-2 font-mono text-[12px] leading-[18px] text-text-primary outline-none transition-colors duration-[var(--motion-hover)] hover:border-text-secondary focus:border-brand focus:ring-2 focus:ring-brand-subtle" /></Field>

if (preserveSaveErrorRevision.current === snapshot.revision) {
preserveSaveErrorRevision.current = undefined;
} else {
setSaveError(undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The snapshot-change effect clears saveError in the mismatch branch but leaves preserveSaveErrorRevision.current set. That stale marker can match a later snapshot revision and incorrectly preserve an old MCP-apply error. Clearing the ref in both branches keeps error preservation scoped to the intended revision.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/components/features/SettingsDialog.tsx, line 57:

<comment>The snapshot-change effect clears `saveError` in the mismatch branch but leaves `preserveSaveErrorRevision.current` set. That stale marker can match a later snapshot revision and incorrectly preserve an old MCP-apply error. Clearing the ref in both branches keeps error preservation scoped to the intended revision.</comment>

<file context>
@@ -46,12 +45,17 @@ export function SettingsBody({ snapshot, adapterCatalog, servers, onReload, runt
+    if (preserveSaveErrorRevision.current === snapshot.revision) {
+      preserveSaveErrorRevision.current = undefined;
+    } else {
+      setSaveError(undefined);
+    }
     setSavedWhileRuntimeUnavailable(false);
</file context>
Suggested change
setSaveError(undefined);
preserveSaveErrorRevision.current = undefined;
setSaveError(undefined);

expect(err).toBeInstanceOf(ConfigEnvExpansionError);
expect((err as ConfigEnvExpansionError).variableName).toBe("MISSING_TOKEN_ENV");
expect((err as ConfigEnvExpansionError).message).not.toContain("secret-sentinel");
test("rejects non-HTTP schemes and malformed URLs", () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The URL-scheme allow-list is now tested only against literal URLs. The previous test that validated the http/https restriction on a scheme produced by env expansion ("rejects non-http URL after env expansion", e.g. url "${MCP_PROTO}://chat" with MCP_PROTO=ws) was removed and not replaced, and the new resolveMcpConfig env-expansion tests only expand to valid http/https schemes. Since resolveMcpConfig expands the URL before validateHttpUrl (expandString then validateHttpUrl), a scheme-injection through an env override is a real security-boundary path that currently has no regression coverage.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/agent-core/src/config/mcp.test.ts, line 303:

<comment>The URL-scheme allow-list is now tested only against literal URLs. The previous test that validated the http/https restriction on a scheme produced by env expansion ("rejects non-http URL after env expansion", e.g. url "${MCP_PROTO}://chat" with MCP_PROTO=ws) was removed and not replaced, and the new resolveMcpConfig env-expansion tests only expand to valid http/https schemes. Since resolveMcpConfig expands the URL before validateHttpUrl (expandString then validateHttpUrl), a scheme-injection through an env override is a real security-boundary path that currently has no regression coverage.</comment>

<file context>
@@ -2,532 +2,347 @@ import { afterEach, describe, expect, test } from "bun:test";
-      expect(err).toBeInstanceOf(ConfigEnvExpansionError);
-      expect((err as ConfigEnvExpansionError).variableName).toBe("MISSING_TOKEN_ENV");
-      expect((err as ConfigEnvExpansionError).message).not.toContain("secret-sentinel");
+  test("rejects non-HTTP schemes and malformed URLs", () => {
+    for (const url of ["ftp://files.example.com", "ws://socket.example.com", "not a valid url", "//example.com/rpc"]) {
+      expect(() => resolveMcpConfig({ servers: { docs: { ...HTTP_SERVER, url } } })).toThrow(McpConfigError);
</file context>

if (resource.includes("\0")) throw new Error("Skill resource path must not contain NUL bytes");
if (resource.includes("\\")) throw new Error("Skill resource path must use POSIX separators");
if (posix.isAbsolute(resource)) throw new Error("Skill resource path must be relative");
if (resource.length === 0) throw new SkillPackageResourcePathError("Skill resource path must not be empty");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Some invalid resource paths still return TOOL_SKILL_READ_FAILED instead of the new invalid-input result; throw SkillPackageResourcePathError for the depth and entry-path validation branches too.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/agent-core/src/skills/package-reader.ts, line 230:

<comment>Some invalid resource paths still return `TOOL_SKILL_READ_FAILED` instead of the new invalid-input result; throw `SkillPackageResourcePathError` for the depth and entry-path validation branches too.</comment>

<file context>
@@ -156,14 +166,74 @@ export function readBuiltinSkillResource(
-  if (resource.includes("\0")) throw new Error("Skill resource path must not contain NUL bytes");
-  if (resource.includes("\\")) throw new Error("Skill resource path must use POSIX separators");
-  if (posix.isAbsolute(resource)) throw new Error("Skill resource path must be relative");
+  if (resource.length === 0) throw new SkillPackageResourcePathError("Skill resource path must not be empty");
+  if (resource.includes("\0")) throw new SkillPackageResourcePathError("Skill resource path must not contain NUL bytes");
+  if (resource.includes("\\")) throw new SkillPackageResourcePathError("Skill resource path must use POSIX separators");
</file context>

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.

1 participant