Repository navigation
refactor(contracts): plugin-owned contracts, part 4 (mcp, mcp-apps, feedback, a2a) - #54
Merged
Merged
Conversation
Moves a2aGetStatus verbatim from the @borg/contracts root to plugins/a2a/src/contract.ts, exported as ./contract. a2aConfigSchema, a2aStatusSchema and their types stay in the root because the kernel's A2AService uses them; the contract imports a2aStatusSchema from there. The wire snapshot is untouched.
…back/contract Moves feedbackAskInputSchema, feedbackAsk, feedbackRequested and feedbackResolved verbatim from the @borg/contracts root to plugins/feedback/src/contract.ts. chat and graphs now depend on @borg/plugin-feedback/contract. feedbackAnswerSchema and FeedbackAnswer stay in the root: the kernel's InteractionService parses answers with it and the plugin SDK exposes the type. The interaction kind, source and choice schemas stay for the same reason. The kernel's human-input rule is keyed on the borg.feedback plugin id and the borg.feedback.ask command id, not on these schemas. The PluginManager tests used the real definitions only as carriers of those ids, so they now declare local stand-ins with the same ids (the file is already on the define* allowlist) instead of making the kernel package depend on a plugin. The duplicate-choice-id assertion on feedbackAskInputSchema moves unchanged from the kernel's interaction-service test to plugins/feedback/test/contract.test.ts. The wire snapshot is untouched.
Moves, verbatim, from the @borg/contracts root: - to @borg/plugin-mcp/contract: the borg.mcp.* surface. That is mcpServerStatusSchema, mcpServerSnapshotSchema, mcpListServers, mcpGetStatus and mcpRefresh, plus the borg.mcp.appDiscovered event with the schemas its payload uses (mcpAppCspSchema, mcpAppPermissionsSchema, mcpAppToolSchema, MCP_APP_MAX_MESSAGE_BYTES). mcpAppSnapshotSchema also goes here because it is defined as that payload plus a version, and mcp's own tests parse it. - to @borg/plugin-mcp-apps/contract: the borg.mcpApps.* surface. That is mcpAppsInvokeTool, mcpAppsCancelTool, mcpAppToolResponded and their request, argument and response schemas, plus MCP_APP_RENDERER_ID and MCP_APP_BRIDGE_CHANNEL. mcp-apps now depends on @borg/plugin-mcp/contract, never the reverse. The desktop's embedded-content protocol imports the bridge constants from the two contracts. mcpServerConfigSchema stays in the root because personaSchema embeds it. So does its private mcpServerIdSchema. The mcp contract takes that same instance through mcpServerConfigSchema.options[0].shape.id, so id validation cannot drift, and no new schema is exported onto the wire surface. The snapshot digests for mcpGetStatus, mcpRefresh and appDiscovered confirm that it is identical. The wire snapshot is untouched.
Moves packages/contracts/src/mcp-app-csp.ts, unchanged, to plugins/mcp-apps/src/mcp-app-csp.ts, and its test to plugins/mcp-apps/test/mcp-app-csp.test.ts. The @borg/contracts root stops re-exporting it. Only mcp-apps and the desktop host use these CSP and permission helpers, and the kernel does not. mcp-apps publishes the file as @borg/plugin-mcp-apps/mcp-app-csp, built to dist/mcp-app-csp.js. The desktop's embedded-content policy and protocol import it from there, as #31 asks ("mcp-app-csp.ts lives in plugins/mcp-apps"). It exports functions, not schemas, so the wire snapshot is untouched.
…n-chat/contract Moves embeddedContentRegistered verbatim from the @borg/contracts root to plugins/chat/src/contract.ts. The event attaches embedded content to a chat session. Chat consumes it, and producers such as mcp-apps already depend on @borg/plugin-chat/contract. embeddedContentSnapshotSchema and EmbeddedContentSnapshot stay in the root because the plugin SDK's embedded-content renderer API exposes the type. The event keeps its borg.embeddedContent.registered id, so the wire snapshot is untouched.
… rule set The Contracts section now lists what stays in the @borg/contracts root and why (kernel types, and the only root event, the kernel-guarded borg.channel.inboundMessage). It describes the one-way contract dependencies (mcp-apps on mcp, embedded content in chat's contract) and helper subpaths such as @borg/plugin-mcp-apps/mcp-app-csp. The CI list spells out every rule check-plugin-boundaries enforces.
Per review, the mcp contract now imports mcpServerIdSchema from @borg/contracts instead of reaching into mcpServerConfigSchema.options[0].shape.id. The schema stays defined in the root next to the persona MCP server config that uses it. Sanctioned wire snapshot edit (approved by SE): exporting the schema adds exactly one line to apps/desktop/test/wire-contract.snapshot.txt, "schema mcpServerIdSchema 044fca3cfee9272f". No existing line or digest changes; the command and event counts stay at 82 and 24.
danielgerlag
added a commit
that referenced
this pull request
Sep 26, 2026
docs/boundaries.md holds every rule scripts/check-plugin-boundaries.mjs and the other two check:boundaries scripts enforce, read from the scripts on main. It also corrects the ./contract tsconfig rule (the tsconfig named by the build script, with src/contract.ts, src/*.ts or src/**/*.ts in include) and lists the contracts subpaths as they are after #54. docs/desktop.md holds the desktop setup and feature detail unchanged. docs/plugin-authoring.md gets the same tsconfig correction and links to docs/boundaries.md. CONTRIBUTING.md links to it and states the /contract exception to the plugin import rule. Part of #33
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #31
This is part 4, the last slice of the contracts split. The remaining plugin commands and events (mcp, mcp-apps, feedback, a2a, embedded content) move to their plugins, and
mcp-app-csp.tsmoves toplugins/mcp-apps. After this PR the@borg/contractsroot defines no plugin command. Its only event is the kernel-guardedborg.channel.inboundMessage.Commits
refactor(a2a):a2aGetStatusmoves to@borg/plugin-a2a/contract.a2aConfigSchema,a2aStatusSchemaand their types stay in the root because the kernel'sA2AServiceuses them.refactor(feedback):feedbackAskInputSchema,feedbackAsk,feedbackRequestedandfeedbackResolvedmove to@borg/plugin-feedback/contract. chat and graphs now depend on that contract.feedbackAnswerSchemaandFeedbackAnswerstay in the root:InteractionServiceparses answers with them, and the SDK exposes the type.borg.feedbackandborg.feedback.ask, not on the feedback schemas. Soplugin-manager.test.tsnow declares local stand-ins with the same ids instead of making the kernel package depend on a plugin. That file is already on the define* allowlist.plugins/feedback/test/contract.test.ts.refactor(mcp): the mcp and mcp-apps contracts move into their plugins.@borg/plugin-mcp/contractgets theborg.mcp.*surface: server status, snapshot,listServers,getStatus,refresh, andappDiscoveredwith its payload schemas (CSP, permissions, tool,MCP_APP_MAX_MESSAGE_BYTES). It also getsmcpAppSnapshotSchema, which is that payload plus a version, and which mcp's tests parse.@borg/plugin-mcp-apps/contractgets theborg.mcpApps.*surface:invokeTool,cancelTool,toolResponded, and their schemas, plusMCP_APP_RENDERER_IDandMCP_APP_BRIDGE_CHANNEL.refactor(mcp-apps):packages/contracts/src/mcp-app-csp.tsand its test move unchanged toplugins/mcp-apps. The file is published as@borg/plugin-mcp-apps/mcp-app-csp, and the desktop's embedded-content policy and protocol import it from there. The root no longer re-exports it.refactor(chat):embeddedContentRegisteredmoves to@borg/plugin-chat/contract.embeddedContentSnapshotSchemastays in the root because the SDK's renderer API exposes it.docs: records what stays in the root and why, the one-way contract dependencies, and the full rule set enforced bycheck-plugin-boundaries. This covers the Split @borg/contracts: plugins own their commands and events #31 criterion "boundary rules are documented".refactor(contracts): export mcpServerIdSchema from the root: a review follow-up. It includes the one sanctioned snapshot edit, described below.All moves are verbatim. I checked this by diffing the lines removed from
packages/contracts/src/index.tsagainst the new files; only import lines differ.apps/desktop/test/wire-contract.snapshot.txtkeeps all 82 commands and 24 events with the same ids, timeouts and digests. Its only change is the one sanctioned line added in commit 7.mcpServerIdSchemaand the sanctioned snapshot editThe mcp commands and
appDiscoveredvalidate server ids withmcpServerIdSchema. It stays defined in the root, next to the persona MCP server config that uses it. Per review, commit 7 exports it, and the mcp contract imports it from@borg/contracts; the earliermcpServerConfigSchema.options[0].shape.idreach-in is gone.Sanctioned wire snapshot edit (approved by SE): exporting the schema adds exactly one line to
apps/desktop/test/wire-contract.snapshot.txt:No other line or digest changed, and the command and event counts are still 82 and 24. This is the only snapshot edit in the whole split.
What stays in the
@borg/contractsroot, and whydefineCommand,defineEvent, the envelope, definition types and command errors.feedbackAnswerSchema.InteractionService, the desktop IPC and the SDK use them.ModelGateway.CostLedger.LoopManager.mcpServerConfigSchemaandmcpServerIdSchema, because persona config embeds MCP servers. The mcp contract reuses the id schema.WorkspaceService.ToolServiceand the SDK.channelInboundMessage,KERNEL_ONLY_EVENT_IDSand the attachment handle. The kernel publishes and guards this event.oauthGrantKey.embeddedContentSnapshotSchema: the SDK renderer API.A2AService.Contracts root size (
packages/contracts/src/index.ts)Planted violations
I planted each of these in the new code, ran
node scripts/check-plugin-boundaries.mjs, saw the failure, and reverted it:plugin dependency cycle: @borg/plugin-mcp -> @borg/plugin-mcp-apps -> @borg/plugin-mcpplugin-manager.tsimports@borg/plugin-feedback/contractpackages/kernel may import no @borg/plugin-* package other than @borg/plugin-sdk@borg/plugin-chat/contractpackages/plugin-sdk may import no @borg/plugin-* package other than @borg/plugin-sdk@borg/plugin-mcp/contractpackages/contracts/src may import only zod and relative files inside that directoryimport "./main"inplugins/feedback/src/contract.tssrc/contract.ts may import only zod, @borg/contracts, @borg/contracts/<subpath>, or @borg/plugin-<name>/contract./contractexport removed@borg/plugin-a2a has src/contract.ts but does not export ./contractdefineEvent(...)inplugins/chat/src/main.ts... calls defineCommand/defineEvent; only packages/contracts/src and plugins/*/src/contract.ts declare bus contracts@borg/plugin-mcp-apps/main... may import @borg/plugin-mcp-apps only as @borg/plugin-mcp-apps/contract@borg/plugin-feedbackdependency removed from graphsplugins/graphs/src/executor.ts imports @borg/plugin-feedback/contract but @borg/plugin-graphs does not list @borg/plugin-feedback in dependencies#31 acceptance criteria
packages/contracts/src/index.tscontains no plugin-specific command or event definitions. Only the kernel-ownedborg.channel.inboundMessageevent remains.mcp-app-csp.tslives inplugins/mcp-apps.pnpm check:boundariespasses, and the boundary rules are documented indocs/architecture.md.pnpm typecheck,pnpm testandpnpm test:e2epass.Gates (Node 24, Linux), at 17b6454
pnpm install --frozen-lockfile: OK.pnpm check:boundaries: pass. The lockfile diff is importer entries only:zodfor four plugins,@borg/plugin-feedbackfor chat and graphs, and@borg/plugin-mcpfor mcp-apps.pnpm typecheck: pass.pnpm test: 139 files, 933 tests passed.xvfb-run pnpm test:e2e: 44 passed, 2 skipped, 0 failed.Out of scope: the #45 items and #48.