You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The Planner, background workers, and public research workers run directly on @github/copilot-sdk, and SDK types reach into chat, jobs, tools, and
permission code. Chopin is open source and hosted by whoever deploys it, so it
shouldn't depend on one vendor's harness. Copilot SDK also can't provide some
capabilities we want in the sidebar chat, such as a classifier model and image
generation.
This RFC adopts Vercel's AI SDK Harnesses
(@ai-sdk/harness) as Chopin's harness layer instead of writing our own. HarnessV1 is the adapter contract, HarnessAgent runs sessions, and each
harness owns its own agent loop. Adapters such as Pi, Claude Code, and Codex
come from their catalog.
The split is simple: the harness brings the agent loop and the model; Chopin
brings the tools. Document tools, repository tools, and GitHub pull-request
tools are Chopin's and reach every harness as host-executed AI SDK tools bound
to the channel's repository. Every harness built-in stays inactive. Copilot stays
the default through a small Chopin-owned HarnessV1 adapter over the in-process
Copilot SDK.
Agent.Agent exposes the raw CopilotSession, so callers reach into session.on / session.send and SDK event names.
Chopin's tools are typed as the Copilot SDK's Tool, so the tools that
define what Chopin is can't be handed to another runtime.
The Planner's tool audit only logs, while worker opens fail closed.
The runtime connects to GitHub MCP with the owner's user token, which can
read every installation repository the owner can access. Repository scope
is enforced afterward by inspecting MCP call arguments (hasForeignScope).
2.2 The Problem
Product: self-hosted deployments can't bring their own runtime, and
classifier and image-generation capabilities are unreachable.
Positioning: open source, self-hosted Chopin is coupled to one vendor.
Technical debt: Chopin's tools and security boundary are written in one
SDK's vocabulary.
3. Goals and Non-Goals
3.1 Functional Goals
@ai-sdk/harness (HarnessV1, HarnessAgent) is the only harness
abstraction. Chopin defines no parallel port types.
@github/copilot-sdk is imported only by the Chopin Copilot SDK adapter,
enforced by a lint rule.
HARNESS selects an adapter factory from Chopin's harness map. The default
is the Copilot SDK adapter.
Chopin's document, repository, and GitHub tools are AI SDK tool()s,
executed in the Chopin process and bound to the channel's repository
through toolsContext.
Every harness built-in is inactive for every Chopin agent.
GitHub MCP is reached only through createMCPClient in the Chopin
process. No harness adapter receives a GitHub credential or an mcpServers entry.
Workers use HarnessAgent structured output.
@ai-sdk/harness-pi passes the contract suite as the second adapter.
The author's runtime follows as its own HarnessV1 package and carries a
harness-provided classifier model end to end.
3.2 Non-Goals (Out of Scope)
No Chopin-defined harness port, capability object, event union, or
permission request type. We use AI SDK's.
No Chopin-owned agent loop. Harnesses own their loops; Chopin adds no ToolLoopAgent or provider-model path.
No harness built-ins (read, bash, write, edit, web fetch, and so
on). The Planner still has no checkout, shell, or host filesystem.
No resumable sessions: no detach, stop, resumeFrom, or continueFrom. Sessions are destroyed after each turn.
No human approval pauses. Permissions are configuration only.
No harness that hasn't passed the contract suite, and no runtime loading
of adapters by package name.
No GitHub App private key or installation tokens.
No change to the MCP (/mcp) coding-agent boundary.
Not in scope: restricting image hosts in documents. Any https: image
renders today (widgets/image.tsx), which is a pre-existing exfiltration
path tracked separately.
Not in scope: storing or serving harness-generated images in documents.
The airlock is openPlannerSession: the only place a session receives tools,
and every tool it receives is bound to one repository. GitHub credentials stay
in the Chopin process.
4.2 Architectural Pattern
AI SDK's harness pattern, used as documented. An adapter factory
(createPi(), createCopilotSdk()) produces a HarnessV1, and a HarnessAgent built from it is constructed once at module scope. Harnesses own
their agent loops and models. Chopin supplies instructions, host tools, activeTools, and output, and consumes the resulting stream. Selecting a
harness is a map from the HARNESS name to an adapter factory.
Tools that express Chopin's domain (documents, repositories, pull requests) are
Chopin's, not the harness's. Capabilities that belong to a runtime (a
classifier model, image generation) come from the harness.
Chopin's own doors: harnessFor, githubTools ⚠, openPlannerSession ⚠, translate.
Doors used from AI SDK: createMCPClient, mcpClient.tools, HarnessAgent.createSession, HarnessAgent.stream, HarnessAgent.generate, session.destroy.
Read together: a deployment picks one tested harness. A Planner session is
opened only with Chopin's tools, each bound to the channel's repository. The
harness runs the loop, and Chopin projects what happened into the Conversation.
Sessions never outlive a turn, and GitHub credentials never leave the Chopin
process.
5. Detailed Design
5.1 The Doors (Entrypoint Contracts)
importtype{HarnessV1}from"@ai-sdk/harness";import{HarnessAgent}from"@ai-sdk/harness/agent";import{createMCPClient}from"@ai-sdk/mcp";import{Output,tool}from"ai";// — Harness selection: a name → factory map, as AI SDK adapters are meant to be used. —constharnesses={"copilot-sdk": createCopilotSdk,pi: createPi,// adding an entry requires passing the contract suite and a security review}satisfiesRecord<string,(settings: HarnessSettings)=>HarnessV1>;typeHarnessConfig={harness: keyoftypeofharnesses;model?: string;// harness-specific model id, passed to HarnessAgentsettings: HarnessSettings;// forwarded verbatim to the factory, including `auth`};functionharnessFor(config: HarnessConfig,deployment: DeploymentMode): HarnessV1;// Guarantee: returns the configured adapter from the harness map.// Refuses at startup: unknown harness; an auth mode that falls back to a subscription// logged in on the host (for example "auto") unless the server is local-only.// — Chopin tools: host-executed, bound to the channel's repository through toolsContext. —letrepositoryTools={
read_repository_file,// today's tools, ported from SDK Tool to AI SDK tool();
list_repository_tree,// no input field names a repository; the handler reads it
search_repository,// from context.repository.
repository_history,};constGITHUB_TOOL_SCHEMAS={// `schemas` loads only these tools. Schemas omit owner and repo; descriptions are ours.// Today's effective set: the read-only pull_requests toolset minus search_pull_requests,// which the current gate already denies.pull_request_read: {inputSchema: z.object({method: z.enum(["get","get_diff","get_status","get_files","get_commits","get_review_comments","get_reviews","get_comments","get_check_runs",]),pullNumber: z.number(),page: z.number().optional(),perPage: z.number().optional(),after: z.string().optional(),}),},list_pull_requests: {inputSchema: z.object({state: z.string().optional(),base: z.string().optional(),head: z.string().optional(),sort: z.string().optional(),direction: z.string().optional(),page: z.number().optional(),perPage: z.number().optional(),}),},};functiongithubTools(owner: ActiveOwnerBinding): Promise<Result<GitHubTools,GitHubToolsError>>;// Guarantee: returns host-executed AI SDK tools that can only address the repository// supplied at call time through toolsContext.// Steps: createMCPClient({ transport: { type: "http", url, headers: owner token, read-only,// toolsets}}) → mcpClient.tools({ schemas: GITHUB_TOOL_SCHEMAS }) → bindRepository.// GitHubToolsError = Unavailable | MissingTool(name)// Refusal: no schema has an owner or repo field, and bindRepository overwrites both from// context, so a model cannot name another repository. Search tools are not in the allowlist.functionbindRepository(tools: ToolSet,descriptions: Record<string,string>): ToolSet;// For each tool: tool({ description: ours, inputSchema, contextSchema: RepositoryContext,// execute: (input, { context, ...rest }) =>// mcpTool.execute({ ...input, owner: context.owner, repo: context.repo }, rest) })// — Agents: HarnessAgent configurations constructed once at module scope. —letplannerAgent=newHarnessAgent({
harness,model: config.model,tools: { ...documentTools, ...repositoryTools, ...githubTools},activeTools: [...documentToolNames, ...repositoryToolNames, ...githubToolNames],// no built-inspermissionMode: "allow-reads",instructions: plannerInstructions,prepareCall: ({ options, ...call})=>({ ...call,instructions: withRepository(options)}),});letsummaryAgent=newHarnessAgent({
harness,activeTools: [],output: Output.object({schema: DescriptionSchema}),instructions: summaryInstructions,});letresearchAgent=newHarnessAgent({
harness,tools: {web_search: publicWebSearch},// host tool over GitHub MCP web_searchactiveTools: ["web_search"],output: Output.object({schema: ResearchReportSchema}),instructions: researchInstructions,});// — Sessions. —functionopenPlannerSession(owner: ActiveOwnerBinding,channel: ChannelRef,): Promise<Result<PlannerSession,OpenError>>;// Guarantee: returns a session whose only tools are Chopin's, bound to the channel's// repository, with every harness built-in inactive.// Steps: authorize owner → githubTools(owner) → empty just-bash sandbox →// plannerAgent.createSession({ sandboxSession, toolsContext: { room, repository, owner }}).// OpenError = GitHubToolsError | HarnessCapabilityUnsupported | Timeout | ShuttingDown// — Projection. —functiontranslate(room: Room,part: HarnessStreamPart): void;// Guarantee: projects one stream part into Chat.* protocol state.// A tool-call for a tool outside activeTools aborts the turn and logs a boundary// failure. This detects a broken adapter after the fact; it does not prevent the call.
Per-door audit:
Door
Joint
One sentence
Every exit
Refusals
Trust transition
Chokepoint
harnessFor
✅ choose the deployment's harness
✅ "returns the configured adapter"
unknown harness or host-login fallback on a public server → startup failure
untested adapters are not in the map's type
n/a
✅ only place an adapter is constructed
githubTools ⚠
✅ give the agent GitHub reads
✅ "returns GitHub tools bound to one repository"
server down → Unavailable; tool missing → MissingTool
no repository field in any schema; search tools not loaded
✅ owner token → repository-bound tools
✅ only path from a harness to GitHub MCP
openPlannerSession ⚠
✅ start a Planner session
✅ "opens a session with only Chopin's bound tools"
any step fails → sandbox destroyed, named error
no built-ins; no unbound tools
✅ the only place a model gains tools
✅ only door to a Planner session
translate
✅ show the turn
✅ "projects one part into the Conversation"
inactive tool → abort turn, boundary failure
—
n/a
n/a
5.2 How Chopin Uses AI SDK
Chopin need
AI SDK mechanism
Swap runtimes
HarnessV1 adapter factories; HARNESS picks one from the map
Agent loop and model
The harness, through HarnessAgent
Planner may only use Chopin tools
activeTools lists only host tools; permissionMode: "allow-reads" as a second layer
Document and repository tools
Host-executed AI SDK tool()s
GitHub pull requests and checks
createMCPClient + mcpClient.tools({ schemas }), wrapped by bindRepository
Room, repository, owner context
toolsContext with per-tool contextSchema; host-only, never serialized
Per-turn instructions
prepareCall; settings fixed for the whole turn
Worker results
output: Output.object({ schema })
Harness capabilities
Built-ins, models, and stream parts the harness offers
Model credentials
Adapter auth / credentials settings
Cancellation
abortSignal on stream(), wired to the active owner's signal
Session end
session.destroy() after every turn
Chopin configuration choices on top of AI SDK defaults:
activeTools lists only Chopin's host tools, so no built-in is active. permissionMode is allow-reads rather than the default allow-all as a
second layer; any approval request the stream still produces is denied
automatically, so no turn waits for a human.
Adapters receive no mcpServers. External MCP servers are consumed on the
host with createMCPClient, so credentials and scope stay under Chopin's
control on every adapter.
HarnessAgent requires a sandbox. Chopin passes an empty just-bash session
that no active tool can reach.
detach, stop, resumeFrom, continueFrom, and suspendTurn are unused.
5.3 The Copilot SDK Adapter
The official @ai-sdk/harness-github-copilot adapter runs Copilot CLI inside a
network sandbox through ACP. Its documented limitations rule it out for the
Planner:
it can't filter built-ins ("filtering GitHub Copilot built-ins throws an
unsupported-capability error");
it doesn't support built-in approval requests;
it needs a network sandbox with an exposed port.
Chopin therefore ships createCopilotSdk(), a host-process HarnessV1 that:
wraps @github/copilot-sdk in the Chopin process, moving today's client.ts hardening and runtime.ts generations inside it, so the Copilot
CLI still runs as its own process over stdio;
declares no built-ins and registers only the host tools HarnessAgent
passes, keeping today's live tool check against Copilot session metadata;
disables Copilot's built-in GitHub MCP, because GitHub tools arrive as host
tools;
implements output internally with a result tool, since the Copilot SDK path
has no structured output today, so Chopin's workers have one code path;
Scope: the schemas allowlist decides which GitHub MCP tools exist, and bindRepository decides which repository they address. The repository tools
already take the repository from their options rather than their input. The
read-only header and toolset selection stay as a second layer. The
argument-inspecting gate (hasForeignScope) is deleted.
Descriptions:bindRepository supplies Chopin's own descriptions and
schemas, so text from GitHub's server never reaches the model as tool
metadata. Tool results still come from GitHub, as today.
Connections: the MCP client is cached per owner token and closed when the
token rotates or expires, instead of reconnecting every turn.
Revalidation: the active owner's signal is the turn's abortSignal.
Losing repository permission ends the turn.
5.5 Deployment Modes
Hosted (default):HARNESS_AUTH must name an explicit mode or supply
credentials. A mode that falls back to a subscription logged in on the host is
refused, so a server's own subscription is never lent to users.
Shared model keys: when HARNESS_AUTH supplies an operator key, every
admitted writer's turns bill that key. Chopin adds no quotas in this revision;
the self-hosting guide documents this and points operators to their
provider's spend limits. Existing per-job bounds carry over as adapter
settings (for example a maximum turn count, or the Copilot adapter's maxAiCredits).
Local: the harness uses its own signed-in state (for example auth: "auto"), so Chopin stores no model credentials. Host-login fallbacks
are allowed only when the server is bound to a loopback interface. When Support device-flow sign-in for local instances with persistent credentials #157
lands, this check reads its local-mode setting instead.
GitHub access is identical in both modes: the signed-in user's App token,
consumed on the host.
5.6 Data Model / Schema
No storage change. GitHub and model credentials are process-local.
5.7 Algorithms and State Management
Fixed per turn:prepareCall settles model, instructions, and tools when
a turn starts.
No replay: an interrupted turn is never resent. The session and sandbox
are destroyed.
Persistence before publication: host tools keep today's semantics.
In-process harnesses: host-process adapters such as Pi run their loop in
the Chopin process. Their work is mostly model I/O, and Chopin's tools are
I/O-bound API calls, so no large CPU work runs in the room process. Moving
harness sessions into a worker is a later hardening if an adapter proves
CPU-heavy.
6. Alternatives Considered
Option
Pros
Cons
Decision
A: Keep Copilot SDK only
No work
Vendor coupling; no classifier or image generation
Rejected
B: Chopin-owned port modeled on AI SDK
Insulated from upstream changes
Reimplements an existing, maintained layer; every adapter is ours to write
Rejected
C: ACP as the primary integration
One protocol for many agents
Clients can't list or restrict the agent's own tools; AI SDK's ACP adapters inherit this
Rejected for the Planner
D: Adopt @ai-sdk/harness; Chopin tools as host tools
Maintained catalog; harness owns the loop; Chopin owns tools
Experimental, fast-moving upstream API
Selected
E: Official AI SDK Copilot adapter
No Chopin adapter code
Can't filter built-ins; needs a network sandbox
Rejected for now
F: Repository snapshot with harness read/grep/glob
Harness-native tools; regex search
Copies the repository into the server; large repositories become unusable
Rejected
G: GitHub MCP in each adapter's mcpServers
Standard adapter setting
The harness holds the token; scope depends on each adapter
Rejected
H: Repository-scoped installation tokens
GitHub enforces one-repository scope
Needs the App private key, which conflicts with #157's local mode
Rejected; not needed
I: A gh command in the sandbox
Familiar CLI for models
Requires active bash; command allowlists are weaker than typed schemas
Rejected
7. Cross-Cutting Concerns
7.1 Security and Privacy
Chopin owns every tool: the harness sees only Chopin's host tools. None
of their schemas can name a repository.
GitHub credentials stay in Chopin: the owner's token is used only by
Chopin's tools and the host MCP client.
Adapter trust:builtinTools and supportsBuiltinToolFiltering are
declarations the adapter makes about itself. The contract suite checks those
declarations and the tools a session is given; it can't prove what the
underlying runtime does internally. Adding an adapter to the map is a trust
decision reviewed as a security change. The Copilot SDK adapter additionally
checks the live session's tool metadata.
No lent subscriptions: host-login auth fallbacks are refused on servers
that aren't loopback-only.
Credentials stay host-side:toolsContext and credentials are never
serialized or shown to the model, and never enter PostgreSQL, logs, or browser
storage.
Unchanged invariant: the Planner still has no checkout, shell, or host
filesystem.
7.2 Backwards Compatibility
Default HARNESS is copilot-sdk. AGENT and MODEL keep their meaning.
No new secrets or GitHub App permissions are required.
Chopin's tools keep their names and behavior; only their type changes.
Internal modules (agent/client.ts, agent/runtime.ts, agent/permissions.ts)
are deleted without shims.
7.3 Upstream Churn
@ai-sdk/harness shipped 120 stable releases between 1.0.0 (2026-06-25) and
1.0.124 (2026-09-24), and its docs say to expect breaking changes. Chopin pins
exact versions of @ai-sdk/harness, @ai-sdk/mcp, and each adapter, upgrades
deliberately, and runs the contract suite on every bump. The Copilot SDK adapter
depends on HarnessV1 directly, so it needs a named owner.
8. Test Plan
Phase 1: spike. Confirm with a throwaway script:
@ai-sdk/harness and @ai-sdk/mcp run under Bun 1.3.2;
Pi with activeTools limited to host tools exposes nothing else;
mcpClient.tools({ schemas }) against GitHub's remote MCP server works
with an App user token, including for an account without Copilot;
a host-process Copilot SDK adapter can implement HarnessV1.doStart.
Phase 2: contract suite and fake. Red: contract tests over a fake HarnessV1 asserting the active tool set, output, abort, and destroy.
Green: harness/contract.ts.
Phase 3: Copilot SDK adapter. Red: contract suite against createCopilotSdk() with a stubbed Copilot client, including output
through its internal result tool. Green: move client.ts and runtime.ts
into the adapter, and add the lint restriction.
Phase 4: Chopin tools on AI SDK. Red: repository tools read the
repository from toolsContext; a bound GitHub tool called with an injected owner/repo still addresses the channel's repository. Green: port tools.ts and repository.ts; add harness/github-tools.ts.
Phase 5: Planner on HarnessAgent. Red: chat/service.test.ts drives translate with AI SDK stream parts, including the inactive-tool check.
Green: switch chat to plannerAgent.stream; remove SessionEvent and permissions.ts.
Phase 6: workers on structured output. Red: summary and research return
parsed output. Green: switch both jobs.
Phase 7: deployment modes. Red: a host-login auth fallback is refused on a
public bind and accepted on loopback. Green: startup check.
Phase 8: Pi adapter. Red: contract suite against @ai-sdk/harness-pi.
Green: add it to the harness map.
Phase 9: the author's runtime. Red: contract suite against the runtime's
own HarnessV1 package. Green: add it to the harness map, with its
classifier model end to end.
Phase 10: E2E harness coverage. Add a fake HarnessV1 and a fake GitHub
MCP server to e2e, so a scripted Planner turn runs through the real
projection, tools, and sockets alongside the existing AGENT=off suite.
Existing coverage must stay green: bun test, bun run test:postgres, bun run e2e, bun run types, bun run ci.
Interactive verification:
HARNESS unset: @chopin streams a reply that cites a repository file and a
pull request from the channel's repository; the log lists only Chopin's tools.
HARNESS=pi with an explicit HARNESS_AUTH: the same conversation works.
A prompt asking for a pull request in another repository returns data only
from the channel's repository.
HARNESS_AUTH=auto on 0.0.0.0 refuses to start; on 127.0.0.1 it starts.
grep -rn "@github/copilot-sdk" apps/server/src matches only the adapter.
9. Open Questions / Unresolved Issues
Resolved:
Harness layer: adopt @ai-sdk/harness; the harness owns the loop.
Tools: Chopin's document, repository, and GitHub tools are host tools
bound through toolsContext; no harness built-ins; no snapshot.
GitHub access:createMCPClient on the host with a schemas
allowlist and bindRepository; no installation tokens.
GitHub tool allowlist: today's effective set, list_pull_requests and pull_request_read with all nine methods; no search tools.
Sessions: disposable, destroyed after every turn.
Permissions: configuration only; no human approval pauses.
Distribution: adapters in the harness map only, each reviewed and
passing the contract suite.
Local model credentials: the harness's own signed-in state; Chopin
stores none.
Shared-key costs: documented, no Chopin quotas in this revision.
Second adapter:@ai-sdk/harness-pi, then the author's runtime with
its classifier.
No open questions remain for this revision. The Phase 1 spike can still
reopen a decision if an assumption fails.
Pluggable agent harness on AI SDK Harnesses: Technical Design Document / RFC
apps/server/src/agent)1. Executive Summary
The Planner, background workers, and public research workers run directly on
@github/copilot-sdk, and SDK types reach into chat, jobs, tools, andpermission code. Chopin is open source and hosted by whoever deploys it, so it
shouldn't depend on one vendor's harness. Copilot SDK also can't provide some
capabilities we want in the sidebar chat, such as a classifier model and image
generation.
This RFC adopts Vercel's AI SDK Harnesses
(
@ai-sdk/harness) as Chopin's harness layer instead of writing our own.HarnessV1is the adapter contract,HarnessAgentruns sessions, and eachharness owns its own agent loop. Adapters such as Pi, Claude Code, and Codex
come from their catalog.
The split is simple: the harness brings the agent loop and the model; Chopin
brings the tools. Document tools, repository tools, and GitHub pull-request
tools are Chopin's and reach every harness as host-executed AI SDK tools bound
to the channel's repository. Every harness built-in stays inactive. Copilot stays
the default through a small Chopin-owned
HarnessV1adapter over the in-processCopilot SDK.
2. Context and Motivation
2.1 Current State
Agent.Agentexposes the rawCopilotSession, so callers reach intosession.on/session.sendand SDK event names.Tool, so the tools thatdefine what Chopin is can't be handed to another runtime.
read every installation repository the owner can access. Repository scope
is enforced afterward by inspecting MCP call arguments (
hasForeignScope).2.2 The Problem
classifier and image-generation capabilities are unreachable.
SDK's vocabulary.
3. Goals and Non-Goals
3.1 Functional Goals
@ai-sdk/harness(HarnessV1,HarnessAgent) is the only harnessabstraction. Chopin defines no parallel port types.
@github/copilot-sdkis imported only by the Chopin Copilot SDK adapter,enforced by a lint rule.
HARNESSselects an adapter factory from Chopin's harness map. The defaultis the Copilot SDK adapter.
tool()s,executed in the Chopin process and bound to the channel's repository
through
toolsContext.createMCPClientin the Chopinprocess. No harness adapter receives a GitHub credential or an
mcpServersentry.HarnessAgentstructuredoutput.@ai-sdk/harness-pipasses the contract suite as the second adapter.The author's runtime follows as its own
HarnessV1package and carries aharness-provided classifier model end to end.
3.2 Non-Goals (Out of Scope)
permission request type. We use AI SDK's.
ToolLoopAgentor provider-model path.read,bash,write,edit, web fetch, and soon). The Planner still has no checkout, shell, or host filesystem.
detach,stop,resumeFrom, orcontinueFrom. Sessions are destroyed after each turn.of adapters by package name.
/mcp) coding-agent boundary.https:imagerenders today (
widgets/image.tsx), which is a pre-existing exfiltrationpath tracked separately.
4. Proposed Solution (High-Level Design)
4.1 System Architecture Diagram
flowchart TB Chat[chat/service] --> Agents Summary[jobs/document-summary] --> Agents Research[jobs/research-workspace] --> Agents Agents["Chopin agents<br/>HarnessAgent configs<br/>(planner, summary, research)"] Agents --> HA["@ai-sdk/harness<br/>HarnessAgent · agent loop owned by harness"] HA --> Map{"harness map<br/>(HARNESS)"} Map --> CopilotSdk["Chopin Copilot SDK adapter<br/>host process"] Map --> Pi["@ai-sdk/harness-pi<br/>host process"] Map -.-> Other["other tested adapters"] Tools["Chopin tools (host-executed)<br/>document · repository · GitHub MCP<br/>bound via toolsContext"] --> HA MCP["@ai-sdk/mcp createMCPClient<br/>GitHub MCP, owner token"] --> Tools REST["GitHub REST<br/>owner token"] --> ToolsThe airlock is
openPlannerSession: the only place a session receives tools,and every tool it receives is bound to one repository. GitHub credentials stay
in the Chopin process.
4.2 Architectural Pattern
AI SDK's harness pattern, used as documented. An adapter factory
(
createPi(),createCopilotSdk()) produces aHarnessV1, and aHarnessAgentbuilt from it is constructed once at module scope. Harnesses owntheir agent loops and models. Chopin supplies instructions, host tools,
activeTools, andoutput, and consumes the resulting stream. Selecting aharness is a map from the
HARNESSname to an adapter factory.Tools that express Chopin's domain (documents, repositories, pull requests) are
Chopin's, not the harness's. Capabilities that belong to a runtime (a
classifier model, image generation) come from the harness.
4.3 Key Components
HarnessAgent,HarnessV1@ai-sdk/harness@ai-sdk/mcpHARNESSname → adapter factory; startup checksHarnessAgentconfigurationstool()sHarnessV1over@github/copilot-sdkChat.*protocol messagesapps/server/src/harness/harnesses.tsharnessesname → factory map;harnessFor(config); startup checksapps/server/src/harness/agents.tsplannerAgent,summaryAgent,researchAgentdefinitionsapps/server/src/harness/session.tsopenPlannerSession; empty sandbox;toolsContextapps/server/src/harness/github-tools.tsgithubTools(MCP client,schemasallowlist,bindRepository)apps/server/src/harness/contract.tsapps/server/src/harness/copilot-sdk/adapter.tsagent/client.ts)createCopilotSdk()implementingHarnessV1apps/server/src/harness/copilot-sdk/runtime.tsagent/runtime.ts)apps/server/src/agent/tools.tstool()withcontextSchemaapps/server/src/agent/repository.tstool(); repository fromtoolsContextapps/server/src/agent/permissions.ts,agent/planner.tsactiveTools,permissionMode, and agent instructionsapps/server/src/agent/client.ts,agent/runtime.tsapps/server/src/chat/service.tsagent.stream();translate(room, part)over AI SDK stream partsapps/server/src/jobs/document-summary.ts,research-workspace.tsHarnessAgentwithoutputschemasapps/server/src/config.tsHARNESS,HARNESS_AUTH.oxlintrc.json@github/copilot-sdkto the adapter directorye2e/HarnessV1and fake GitHub MCP server for harness-path coveragedocs/hosted-agent.md,docs/self-hosting.md,docs/architecture.md4.4 The Door Set at a Glance
Chopin's own doors:
harnessFor,githubTools⚠,openPlannerSession⚠,translate.Doors used from AI SDK:
createMCPClient,mcpClient.tools,HarnessAgent.createSession,HarnessAgent.stream,HarnessAgent.generate,session.destroy.Read together: a deployment picks one tested harness. A Planner session is
opened only with Chopin's tools, each bound to the channel's repository. The
harness runs the loop, and Chopin projects what happened into the Conversation.
Sessions never outlive a turn, and GitHub credentials never leave the Chopin
process.
5. Detailed Design
5.1 The Doors (Entrypoint Contracts)
Per-door audit:
harnessForgithubTools⚠Unavailable; tool missing →MissingToolopenPlannerSession⚠translate5.2 How Chopin Uses AI SDK
HarnessV1adapter factories;HARNESSpicks one from the mapHarnessAgentactiveToolslists only host tools;permissionMode: "allow-reads"as a second layertool()screateMCPClient+mcpClient.tools({ schemas }), wrapped bybindRepositorytoolsContextwith per-toolcontextSchema; host-only, never serializedprepareCall; settings fixed for the whole turnoutput: Output.object({ schema })auth/credentialssettingsabortSignalonstream(), wired to the active owner'ssignalsession.destroy()after every turnChopin configuration choices on top of AI SDK defaults:
activeToolslists only Chopin's host tools, so no built-in is active.permissionModeisallow-readsrather than the defaultallow-allas asecond layer; any approval request the stream still produces is denied
automatically, so no turn waits for a human.
mcpServers. External MCP servers are consumed on thehost with
createMCPClient, so credentials and scope stay under Chopin'scontrol on every adapter.
HarnessAgentrequires a sandbox. Chopin passes an empty just-bash sessionthat no active tool can reach.
detach,stop,resumeFrom,continueFrom, andsuspendTurnare unused.5.3 The Copilot SDK Adapter
The official
@ai-sdk/harness-github-copilotadapter runs Copilot CLI inside anetwork sandbox through ACP. Its documented limitations rule it out for the
Planner:
unsupported-capability error");
Chopin therefore ships
createCopilotSdk(), a host-processHarnessV1that:@github/copilot-sdkin the Chopin process, moving today'sclient.tshardening andruntime.tsgenerations inside it, so the CopilotCLI still runs as its own process over stdio;
HarnessAgentpasses, keeping today's live tool check against Copilot session metadata;
tools;
outputinternally with a result tool, since the Copilot SDK pathhas no structured output today, so Chopin's workers have one code path;
credentialsresolver keyedby session ID and resolved from the active owner. Device-flow user tokens from
Support device-flow sign-in for local instances with persistent credentials #157 work the same way as web-flow tokens here.
If AI SDK's Copilot adapter later supports built-in filtering, Chopin can
switch to it and delete this adapter.
5.4 GitHub Tools
is used only by the host MCP client and the repository tools. No adapter or
model sees it.
schemasallowlist decides which GitHub MCP tools exist, andbindRepositorydecides which repository they address. The repository toolsalready take the repository from their options rather than their input. The
read-only header and toolset selection stay as a second layer. The
argument-inspecting gate (
hasForeignScope) is deleted.bindRepositorysupplies Chopin's own descriptions andschemas, so text from GitHub's server never reaches the model as tool
metadata. Tool results still come from GitHub, as today.
token rotates or expires, instead of reconnecting every turn.
signalis the turn'sabortSignal.Losing repository permission ends the turn.
5.5 Deployment Modes
HARNESS_AUTHmust name an explicit mode or supplycredentials. A mode that falls back to a subscription logged in on the host is
refused, so a server's own subscription is never lent to users.
HARNESS_AUTHsupplies an operator key, everyadmitted writer's turns bill that key. Chopin adds no quotas in this revision;
the self-hosting guide documents this and points operators to their
provider's spend limits. Existing per-job bounds carry over as adapter
settings (for example a maximum turn count, or the Copilot adapter's
maxAiCredits).auth: "auto"), so Chopin stores no model credentials. Host-login fallbacksare allowed only when the server is bound to a loopback interface. When Support device-flow sign-in for local instances with persistent credentials #157
lands, this check reads its local-mode setting instead.
GitHub access is identical in both modes: the signed-in user's App token,
consumed on the host.
5.6 Data Model / Schema
No storage change. GitHub and model credentials are process-local.
5.7 Algorithms and State Management
prepareCallsettles model, instructions, and tools whena turn starts.
are destroyed.
the Chopin process. Their work is mostly model I/O, and Chopin's tools are
I/O-bound API calls, so no large CPU work runs in the room process. Moving
harness sessions into a worker is a later hardening if an adapter proves
CPU-heavy.
6. Alternatives Considered
@ai-sdk/harness; Chopin tools as host toolsread/grep/globmcpServersghcommand in the sandboxbash; command allowlists are weaker than typed schemas7. Cross-Cutting Concerns
7.1 Security and Privacy
of their schemas can name a repository.
Chopin's tools and the host MCP client.
builtinToolsandsupportsBuiltinToolFilteringaredeclarations the adapter makes about itself. The contract suite checks those
declarations and the tools a session is given; it can't prove what the
underlying runtime does internally. Adding an adapter to the map is a trust
decision reviewed as a security change. The Copilot SDK adapter additionally
checks the live session's tool metadata.
that aren't loopback-only.
toolsContextand credentials are neverserialized or shown to the model, and never enter PostgreSQL, logs, or browser
storage.
filesystem.
7.2 Backwards Compatibility
HARNESSiscopilot-sdk.AGENTandMODELkeep their meaning.No new secrets or GitHub App permissions are required.
agent/client.ts,agent/runtime.ts,agent/permissions.ts)are deleted without shims.
7.3 Upstream Churn
@ai-sdk/harnessshipped 120 stable releases between 1.0.0 (2026-06-25) and1.0.124 (2026-09-24), and its docs say to expect breaking changes. Chopin pins
exact versions of
@ai-sdk/harness,@ai-sdk/mcp, and each adapter, upgradesdeliberately, and runs the contract suite on every bump. The Copilot SDK adapter
depends on
HarnessV1directly, so it needs a named owner.8. Test Plan
@ai-sdk/harnessand@ai-sdk/mcprun under Bun 1.3.2;activeToolslimited to host tools exposes nothing else;mcpClient.tools({ schemas })against GitHub's remote MCP server workswith an App user token, including for an account without Copilot;
HarnessV1.doStart.HarnessV1asserting the active tool set,output, abort, and destroy.Green:
harness/contract.ts.createCopilotSdk()with a stubbed Copilot client, includingoutputthrough its internal result tool. Green: move
client.tsandruntime.tsinto the adapter, and add the lint restriction.
repository from
toolsContext; a bound GitHub tool called with an injectedowner/repostill addresses the channel's repository. Green: porttools.tsandrepository.ts; addharness/github-tools.ts.HarnessAgent. Red:chat/service.test.tsdrivestranslatewith AI SDK stream parts, including the inactive-tool check.Green: switch chat to
plannerAgent.stream; removeSessionEventandpermissions.ts.parsed
output. Green: switch both jobs.public bind and accepted on loopback. Green: startup check.
@ai-sdk/harness-pi.Green: add it to the harness map.
own
HarnessV1package. Green: add it to the harness map, with itsclassifier model end to end.
HarnessV1and a fake GitHubMCP server to
e2e, so a scripted Planner turn runs through the realprojection, tools, and sockets alongside the existing
AGENT=offsuite.Existing coverage must stay green:
bun test,bun run test:postgres,bun run e2e,bun run types,bun run ci.Interactive verification:
HARNESSunset:@chopinstreams a reply that cites a repository file and apull request from the channel's repository; the log lists only Chopin's tools.
HARNESS=piwith an explicitHARNESS_AUTH: the same conversation works.from the channel's repository.
HARNESS_AUTH=autoon0.0.0.0refuses to start; on127.0.0.1it starts.grep -rn "@github/copilot-sdk" apps/server/srcmatches only the adapter.9. Open Questions / Unresolved Issues
Resolved:
@ai-sdk/harness; the harness owns the loop.bound through
toolsContext; no harness built-ins; no snapshot.createMCPClienton the host with aschemasallowlist and
bindRepository; no installation tokens.list_pull_requestsandpull_request_readwith all nine methods; no search tools.passing the contract suite.
stores none.
@ai-sdk/harness-pi, then the author's runtime withits classifier.
No open questions remain for this revision. The Phase 1 spike can still
reopen a decision if an assumption fails.