Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 60 additions & 18 deletions ts/packages/copilot-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ User prompt
Explicit @typeagent run commands and optional structured action MCP tools
remain available. They are not automatic recording or execution fallbacks.

Registered alongside routing (calls are disabled in bypass mode):
Registered alongside routing (calls are disabled in bypass mode by default):
typeagent-workspace MCP tools
typeagent-macros MCP tools -> checked program execution
typeagent-macros MCP tools -> checked program execution (explicit bypass opt-in below)
```

The hook output fields `handled`, `responseContent`, and `handledBy` are supported in current Copilot CLI behavior, allowing the hook to skip the agentic loop entirely when TypeAgent handles a request. For local runtime debugging against the runtime repo, use `pnpm copilot:dev`.
Expand Down Expand Up @@ -652,7 +652,8 @@ No owner setting is required: the default and only enabled owner is `plugin`.
`plugin` and `off`. Close and restart the native session after changing it.
An obsolete explicit `typeagent` value is an actionable error, not a different
catalog selection. `off` disables recipes, including recording; `bypass` disables
recipe matching/execution and MCP calls, but permits explicitly armed recordings.
recipe matching and MCP calls by default, but permits explicitly armed recordings.
The macro-tool opt-in permits discovery and execution without prompt matching.
Unmatched ordinary prompts go to native Copilot, not the
TypeAgent planner. Explicit TypeAgent commands/tools remain optional backend
operations; they are not required by the local recipe path.
Expand Down Expand Up @@ -811,8 +812,10 @@ set a workspace learning preference, then arm the next interaction:

To record Copilot-native or third-party MCP tools without recipe routing, use
`/typeagent-mode bypass` instead. The selected task stays with Copilot, including
the success receipt required for learning. TypeAgent workspace and macro MCP
calls remain disabled in bypass; use the extension recording/status commands.
the success receipt required for learning. TypeAgent workspace MCP calls remain
disabled, and macro MCP calls are disabled by default; use the extension
recording/status commands. The macro-tool opt-in described below is not required
for recording.

For the MCP-mode example, send this as the next ordinary message, rather than
using the natural-language file-reading shortcut:
Expand Down Expand Up @@ -944,7 +947,7 @@ job data or create a manual draft to force a retry.
### Review and run a prepared program

Use the lifecycle MCP tools in direct, MCP or dev mode. They call the selected
owner; bypass disables them. After learning produces a candidate:
owner; bypass disables them by default. After learning produces a candidate:

1. Inspect its returned candidate and version, then call `validate_macro`.
2. In Prepare mode, review it and call `approve_macro`. Drafts cannot run.
Expand All @@ -956,6 +959,41 @@ owner; bypass disables them. After learning produces a candidate:
`processCommand` does not create or approve macros. The lifecycle tools have no
`preference` parameter, manual step-draft creation or adapted-candidate submission.

### Explicit macro tools while Copilot handles prompts

To let Copilot discover and run existing approved macros without enabling prompt
routing, opt in before starting Copilot:

```powershell
$env:TYPEAGENT_MODE = 'bypass'
$env:TYPEAGENT_BYPASS_MACRO_TOOLS = 'true'
pnpm copilot --experimental
```

Alternatively, set `"bypassMacroTools": true` alongside `"mode": "bypass"` in
the plugin config. The environment variable overrides the saved setting and
accepts `true`/`false`, `1`/`0`, or `on`/`off`; invalid values are rejected.
The default is `false`. Restart Copilot after changing environment variables;
config changes are read on subsequent calls.

Only `list_macros`, `search_macros`, `inspect_macro`, `get_macro_requirements`,
`run_macro`, and `get_macro_run` are permitted by this opt-in. Validation,
approval, disablement, and deletion remain blocked in bypass; use direct, MCP,
or dev mode for those lifecycle operations. `@typeagent status` and
`/typeagent-status` report `Macro tools: discovery and execution only` when
the bypass opt-in is active.

Ordinary prompts still go to Copilot without local recipe matching or automatic
TypeAgent delegation. Workspace and other TypeAgent MCP tools remain subject to
their existing mode gates. This setting does not change learning preferences,
enable a disabled recipe owner, or grant tool/backend permissions.

Execution uses the same local owner and current approved immutable program
version as other modes. Unsupported tools, backend denials, failed or cancelled
runs remain failures; there is no planner fallback or automatic retry. This does
not add replay support for arbitrary native Copilot tools or make a captured
trace into a runnable macro. Approval and backend authorization are separate.

### Route approved macros from natural language

The plugin-local matcher uses the current approved program's generated grammar
Expand All @@ -977,7 +1015,8 @@ disable and deletion refresh schemas and reconcile stored routes.

### Recording controls and recovery

Program authoring/execution is unconditional. Only these recording boundaries
Program authoring/execution has no legacy feature flag. Explicit macro access in
bypass is separately opt-in as described above. These recording boundaries
remain configurable (enabled by default):

| Environment variable | Boundary |
Expand Down Expand Up @@ -1092,8 +1131,9 @@ that mode selection requires natural-language dispatch.

Ordinary prompts use local checked-program matching followed by Copilot on a
miss, in `direct`, `mcp` and `dev`. None of these modes selects a backend macro
owner or author. `bypass` disables recipe matching/execution and MCP calls,
while still allowing explicitly armed recordings.
owner or author. `bypass` disables recipe matching and MCP calls by default,
while still allowing explicitly armed recordings. The explicit macro-tool opt-in
above permits discovery and execution without prompt matching.

The settings remain for explicit TypeAgent commands and MCP availability.
`@typeagent run <request>` intentionally delegates to the TypeAgent dispatcher;
Expand Down Expand Up @@ -1153,20 +1193,22 @@ The plugin stores config at `%USERPROFILE%\.typeagent-copilot\config.json` (Wind
```json
{
"mode": "direct",
"mcpRouting": "delegate"
"mcpRouting": "delegate",
"bypassMacroTools": false
}
```

**Environment variable overrides** (take precedence over config file):

| Variable | Default | Description |
| --------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `TYPEAGENT_MODE` | `direct` | `direct`, `mcp`, `dev`, or `bypass` |
| `TYPEAGENT_HOST` | `localhost` | TypeAgent server host |
| `TYPEAGENT_PORT` | `8999` | TypeAgent server port |
| `TYPEAGENT_CONVERSATION_ID` | Session-selected server default | Optional existing public conversation ID for all routes; overrides config `conversationId` and automatic selection |
| `TYPEAGENT_PLUGIN_DATA` | `~/.typeagent-copilot` | Config directory |
| `TYPEAGENT_WORKSPACE_ROOTS` | Copilot process working directory | Approved roots for workspace MCP tools, separated by the platform path delimiter |
| Variable | Default | Description |
| ------------------------------ | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `TYPEAGENT_MODE` | `direct` | `direct`, `mcp`, `dev`, or `bypass` |
| `TYPEAGENT_BYPASS_MACRO_TOOLS` | `false` | Opt in to explicit macro discovery/execution in bypass; overrides config `bypassMacroTools` without changing routing or permissions |
| `TYPEAGENT_HOST` | `localhost` | TypeAgent server host |
| `TYPEAGENT_PORT` | `8999` | TypeAgent server port |
| `TYPEAGENT_CONVERSATION_ID` | Session-selected server default | Optional existing public conversation ID for all routes; overrides config `conversationId` and automatic selection |
| `TYPEAGENT_PLUGIN_DATA` | `~/.typeagent-copilot` | Config directory |
| `TYPEAGENT_WORKSPACE_ROOTS` | Copilot process working directory | Approved roots for workspace MCP tools, separated by the platform path delimiter |

---

Expand Down
12 changes: 10 additions & 2 deletions ts/packages/copilot-plugin/src/extension/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
// Licensed under the MIT License.

import type { CommandDefinition } from "@github/copilot-sdk";
import { getConfigPath, getModeLabel } from "../shared/plugin-config.js";
import {
getConfigPath,
getMacroToolAvailability,
getMode,
getModeLabel,
} from "../shared/plugin-config.js";
import {
getModeDescription,
handleModeSetting,
Expand Down Expand Up @@ -70,7 +75,10 @@ function statusText(): string {
"TypeAgent configuration",
`Mode: ${getModeLabel()}`,
`Routing: ${getModeDescription()}`,
"Ordinary prompts: local recipes, then Copilot",
getMode() === "bypass"
? "Ordinary prompts: Copilot (no local recipe matching)"
: "Ordinary prompts: local recipes, then Copilot",
`Macro tools: ${getMacroToolAvailability()}`,
`Server: ws://${host}:${port}`,
`Config: ${getConfigPath()}`,
"Mode settings are shared by sessions using this config.",
Expand Down
8 changes: 6 additions & 2 deletions ts/packages/copilot-plugin/src/hooks/hook-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
getConfigPath,
getMode,
getModeLabel,
getMacroToolAvailability,
getRecipeOwner,
type Mode,
type RecipeOwner,
Expand Down Expand Up @@ -193,8 +194,11 @@ function handleStatusCommand(
"",
`- Mode: **${getModeLabel()}**`,
`- Routing: ${getModeDescription()}`,
"- Ordinary prompts: local recipes, then Copilot",
`- Macro workspace tools: **${mode === "bypass" ? "disabled" : "available"}**`,
mode === "bypass"
? "- Ordinary prompts: Copilot (no local recipe matching)"
: "- Ordinary prompts: local recipes, then Copilot",
`- Workspace tools: **${mode === "bypass" ? "disabled" : "available"}**`,
`- Macro tools: **${getMacroToolAvailability()}**`,
`- Server: ws://${host}:${port}`,
`- Config: ${configPath}`,
"- Mode settings are shared by sessions using this config.",
Expand Down
87 changes: 56 additions & 31 deletions ts/packages/copilot-plugin/src/mcp/macroServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { randomUUID } from "node:crypto";
import { z } from "zod";
import { getMode, type Mode } from "../shared/plugin-config.js";
import {
getBypassMacroToolsEnabled,
getMode,
type Mode,
} from "../shared/plugin-config.js";
import {
connectToMacroOwner,
type MacroOwnerConnection,
Expand All @@ -15,11 +19,13 @@ import {
export interface MacroServerDependencies {
connect: () => Promise<MacroOwnerConnection>;
getMode: () => Mode;
getBypassMacroToolsEnabled?: () => boolean;
}

const defaultDependencies: MacroServerDependencies = {
connect: connectToMacroOwner,
getMode,
getBypassMacroToolsEnabled,
};

function result(value: unknown): CallToolResult {
Expand Down Expand Up @@ -57,39 +63,46 @@ export class MacroCatalogAdapter {
state?: "draft" | "approved" | "disabled" | undefined;
limit?: number | undefined;
}) {
return this.call((connection) =>
connection.listMacros({
...(request.state ? { state: request.state } : {}),
...(request.limit !== undefined
? { limit: request.limit }
: {}),
}),
return this.call(
(connection) =>
connection.listMacros({
...(request.state ? { state: request.state } : {}),
...(request.limit !== undefined
? { limit: request.limit }
: {}),
}),
true,
);
}

searchMacros(request: { query: string; limit?: number | undefined }) {
return this.call((connection) =>
connection.searchMacros({
query: request.query,
...(request.limit !== undefined
? { limit: request.limit }
: {}),
}),
return this.call(
(connection) =>
connection.searchMacros({
query: request.query,
...(request.limit !== undefined
? { limit: request.limit }
: {}),
}),
true,
);
}

inspectMacro(request: { macroId: string; version?: number | undefined }) {
return this.call((connection) =>
connection.inspectMacro(this.macroRef(request)),
return this.call(
(connection) => connection.inspectMacro(this.macroRef(request)),
true,
);
}

getMacroRequirements(request: {
macroId: string;
version?: number | undefined;
}) {
return this.call((connection) =>
connection.getMacroRequirements(this.macroRef(request)),
return this.call(
(connection) =>
connection.getMacroRequirements(this.macroRef(request)),
true,
);
}

Expand Down Expand Up @@ -117,7 +130,10 @@ export class MacroCatalogAdapter {
}

getMacroRun(request: { runId: string }) {
return this.call((connection) => connection.getMacroRun(request.runId));
return this.call(
(connection) => connection.getMacroRun(request.runId),
true,
);
}

async runMacro(
Expand All @@ -130,16 +146,14 @@ export class MacroCatalogAdapter {
},
signal?: AbortSignal,
): Promise<CallToolResult> {
if (this.dependencies.getMode() === "bypass") {
return errorResult(
"TypeAgent macro tools are disabled in bypass mode.",
);
}
const runId = randomUUID();
let connection: MacroOwnerConnection | undefined;
let removeAbort: (() => void) | undefined;
try {
this.assertAccess(true);
signal?.throwIfAborted();
const runId = randomUUID();
connection = await this.dependencies.connect();
signal?.throwIfAborted();
const activeConnection = connection;
if (signal) {
const cancel = () => {
Expand Down Expand Up @@ -188,14 +202,11 @@ export class MacroCatalogAdapter {

private async call(
operation: (connection: MacroOwnerConnection) => Promise<unknown>,
allowInBypass = false,
): Promise<CallToolResult> {
if (this.dependencies.getMode() === "bypass") {
return errorResult(
"TypeAgent macro tools are disabled in bypass mode.",
);
}
let connection: MacroOwnerConnection | undefined;
try {
this.assertAccess(allowInBypass);
connection = await this.dependencies.connect();
return result(await operation(connection));
} catch (error) {
Expand All @@ -204,6 +215,20 @@ export class MacroCatalogAdapter {
await connection?.close();
}
}

private assertAccess(allowInBypass: boolean): void {
if (this.dependencies.getMode() !== "bypass") return;
if (
allowInBypass &&
this.dependencies.getBypassMacroToolsEnabled?.() === true
)
return;
throw new Error(
allowInBypass
? "TypeAgent macro tools are disabled in bypass mode. Opt in with bypassMacroTools or TYPEAGENT_BYPASS_MACRO_TOOLS."
: "TypeAgent macro management tools are disabled in bypass mode.",
);
}
}

const macroRefSchema = {
Expand Down
33 changes: 33 additions & 0 deletions ts/packages/copilot-plugin/src/shared/plugin-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface PluginConfig {
/** Catalog revisions to expose to this Copilot session. */
selectedSkills?: SkillSelection[];
recipeOwner?: RecipeOwner;
bypassMacroTools?: boolean;
recipeReadOnlyMcpTools?: { mcpServerName: string; toolName: string }[];
recipeInferenceCliPath?: string;
[key: string]: unknown;
Expand Down Expand Up @@ -163,6 +164,38 @@ export function getModeLabel(): string {
return mode === "mcp" ? `mcp (${getMcpRouting()})` : mode;
}

export function getBypassMacroToolsEnabled(): boolean {
const environment = process.env.TYPEAGENT_BYPASS_MACRO_TOOLS;
if (environment !== undefined) {
switch (environment.trim().toLowerCase()) {
case "true":
case "1":
case "on":
return true;
case "false":
case "0":
case "off":
return false;
default:
throw new Error(
"TYPEAGENT_BYPASS_MACRO_TOOLS must be true/false, 1/0, or on/off.",
);
}
}
const configured = readConfig()?.bypassMacroTools;
if (configured === undefined) return false;
if (typeof configured !== "boolean")
throw new Error("bypassMacroTools must be a boolean.");
return configured;
}

export function getMacroToolAvailability(): string {
if (getMode() !== "bypass") return "available";
return getBypassMacroToolsEnabled()
? "discovery and execution only"
: "disabled";
}

export function getSelectedSkills(): SkillSelection[] {
const environment = process.env.TYPEAGENT_SELECTED_SKILLS;
const value =
Expand Down
Loading
Loading