@codemcp/ade-harnesses 0.0.2 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-format.log +1 -1
- package/.turbo/turbo-lint.log +1 -1
- package/.turbo/turbo-test.log +15 -12
- package/.turbo/turbo-typecheck.log +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/permission-policy.d.ts +7 -0
- package/dist/permission-policy.js +152 -0
- package/dist/util.d.ts +1 -1
- package/dist/util.js +16 -2
- package/dist/writers/claude-code.js +51 -20
- package/dist/writers/cline.js +2 -2
- package/dist/writers/copilot.js +61 -8
- package/dist/writers/cursor.js +48 -2
- package/dist/writers/kiro.js +54 -38
- package/dist/writers/opencode.js +26 -23
- package/dist/writers/roo-code.js +38 -2
- package/dist/writers/universal.js +41 -3
- package/dist/writers/windsurf.js +43 -1
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/permission-policy.ts +173 -0
- package/src/util.ts +20 -7
- package/src/writers/claude-code.spec.ts +163 -5
- package/src/writers/claude-code.ts +63 -20
- package/src/writers/cline.spec.ts +146 -3
- package/src/writers/cline.ts +2 -2
- package/src/writers/copilot.spec.ts +157 -1
- package/src/writers/copilot.ts +76 -9
- package/src/writers/cursor.spec.ts +104 -1
- package/src/writers/cursor.ts +65 -3
- package/src/writers/kiro.spec.ts +228 -0
- package/src/writers/kiro.ts +77 -40
- package/src/writers/opencode.spec.ts +258 -0
- package/src/writers/opencode.ts +40 -27
- package/src/writers/roo-code.spec.ts +129 -1
- package/src/writers/roo-code.ts +49 -2
- package/src/writers/universal.spec.ts +134 -0
- package/src/writers/universal.ts +57 -4
- package/src/writers/windsurf.spec.ts +111 -3
- package/src/writers/windsurf.ts +64 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -6,9 +6,9 @@ import {
|
|
|
6
6
|
writeJson,
|
|
7
7
|
writeMcpServers,
|
|
8
8
|
writeAgentMd,
|
|
9
|
-
writeInlineSkills,
|
|
10
9
|
writeGitHooks
|
|
11
10
|
} from "../util.js";
|
|
11
|
+
import { allowsCapability, keepsWebOnAsk } from "../permission-policy.js";
|
|
12
12
|
|
|
13
13
|
export const claudeCodeWriter: HarnessWriter = {
|
|
14
14
|
id: "claude-code",
|
|
@@ -26,7 +26,6 @@ export const claudeCodeWriter: HarnessWriter = {
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
await writeClaudeSettings(config, projectRoot);
|
|
29
|
-
await writeInlineSkills(config, projectRoot);
|
|
30
29
|
await writeGitHooks(config.git_hooks, projectRoot);
|
|
31
30
|
}
|
|
32
31
|
};
|
|
@@ -35,30 +34,74 @@ async function writeClaudeSettings(
|
|
|
35
34
|
config: LogicalConfig,
|
|
36
35
|
projectRoot: string
|
|
37
36
|
): Promise<void> {
|
|
38
|
-
const servers = config.mcp_servers;
|
|
39
|
-
if (servers.length === 0) return;
|
|
40
|
-
|
|
41
37
|
const settingsPath = join(projectRoot, ".claude", "settings.json");
|
|
42
38
|
const existing = await readJsonOrEmpty(settingsPath);
|
|
39
|
+
const existingPerms = (existing.permissions as Record<string, unknown>) ?? {};
|
|
40
|
+
const existingAllow = asStringArray(existingPerms.allow);
|
|
41
|
+
const existingAsk = asStringArray(existingPerms.ask);
|
|
43
42
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
for (const tool of allowed) {
|
|
51
|
-
allowRules.push(`MCP(${server.ref}:${tool})`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
43
|
+
const autonomyRules = getClaudeAutonomyRules(config);
|
|
44
|
+
const mcpRules = getClaudeMcpAllowRules(config);
|
|
45
|
+
const allowRules = [
|
|
46
|
+
...new Set([...existingAllow, ...autonomyRules.allow, ...mcpRules])
|
|
47
|
+
];
|
|
48
|
+
const askRules = [...new Set([...existingAsk, ...autonomyRules.ask])];
|
|
55
49
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
if (
|
|
51
|
+
allowRules.length === 0 &&
|
|
52
|
+
askRules.length === 0 &&
|
|
53
|
+
config.mcp_servers.length === 0
|
|
54
|
+
) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
59
57
|
|
|
60
58
|
await writeJson(settingsPath, {
|
|
61
59
|
...existing,
|
|
62
|
-
permissions: {
|
|
60
|
+
permissions: {
|
|
61
|
+
...existingPerms,
|
|
62
|
+
...(allowRules.length > 0 ? { allow: allowRules } : {}),
|
|
63
|
+
...(askRules.length > 0 ? { ask: askRules } : {})
|
|
64
|
+
}
|
|
63
65
|
});
|
|
64
66
|
}
|
|
67
|
+
|
|
68
|
+
function asStringArray(value: unknown): string[] {
|
|
69
|
+
return Array.isArray(value)
|
|
70
|
+
? value.filter((entry): entry is string => typeof entry === "string")
|
|
71
|
+
: [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getClaudeMcpAllowRules(config: LogicalConfig): string[] {
|
|
75
|
+
const allowRules: string[] = [];
|
|
76
|
+
|
|
77
|
+
for (const server of config.mcp_servers) {
|
|
78
|
+
const allowedTools = server.allowedTools;
|
|
79
|
+
if (!allowedTools || allowedTools.includes("*")) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const tool of allowedTools) {
|
|
84
|
+
allowRules.push(`mcp__${server.ref}__${tool}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return allowRules;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getClaudeAutonomyRules(config: LogicalConfig): {
|
|
92
|
+
allow: string[];
|
|
93
|
+
ask: string[];
|
|
94
|
+
} {
|
|
95
|
+
const ask = keepsWebOnAsk(config) ? ["WebFetch", "WebSearch"] : [];
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
allow: [
|
|
99
|
+
...(allowsCapability(config, "read") ? ["Read"] : []),
|
|
100
|
+
...(allowsCapability(config, "edit_write") ? ["Edit"] : []),
|
|
101
|
+
...(allowsCapability(config, "search_list") ? ["Glob", "Grep"] : []),
|
|
102
|
+
...(allowsCapability(config, "bash_unsafe") ? ["Bash"] : []),
|
|
103
|
+
...(allowsCapability(config, "task_agent") ? ["TodoWrite"] : [])
|
|
104
|
+
],
|
|
105
|
+
ask
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -2,9 +2,57 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
|
2
2
|
import { mkdtemp, rm, readFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
AutonomyProfile,
|
|
7
|
+
LogicalConfig,
|
|
8
|
+
PermissionPolicy
|
|
9
|
+
} from "@codemcp/ade-core";
|
|
6
10
|
import { clineWriter } from "./cline.js";
|
|
7
11
|
|
|
12
|
+
function autonomyPolicy(profile: AutonomyProfile): PermissionPolicy {
|
|
13
|
+
switch (profile) {
|
|
14
|
+
case "rigid":
|
|
15
|
+
return {
|
|
16
|
+
profile,
|
|
17
|
+
capabilities: {
|
|
18
|
+
read: "ask",
|
|
19
|
+
edit_write: "ask",
|
|
20
|
+
search_list: "ask",
|
|
21
|
+
bash_safe: "ask",
|
|
22
|
+
bash_unsafe: "ask",
|
|
23
|
+
web: "ask",
|
|
24
|
+
task_agent: "ask"
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
case "sensible-defaults":
|
|
28
|
+
return {
|
|
29
|
+
profile,
|
|
30
|
+
capabilities: {
|
|
31
|
+
read: "allow",
|
|
32
|
+
edit_write: "allow",
|
|
33
|
+
search_list: "allow",
|
|
34
|
+
bash_safe: "allow",
|
|
35
|
+
bash_unsafe: "ask",
|
|
36
|
+
web: "ask",
|
|
37
|
+
task_agent: "allow"
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
case "max-autonomy":
|
|
41
|
+
return {
|
|
42
|
+
profile,
|
|
43
|
+
capabilities: {
|
|
44
|
+
read: "allow",
|
|
45
|
+
edit_write: "allow",
|
|
46
|
+
search_list: "allow",
|
|
47
|
+
bash_safe: "allow",
|
|
48
|
+
bash_unsafe: "allow",
|
|
49
|
+
web: "ask",
|
|
50
|
+
task_agent: "allow"
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
8
56
|
describe("clineWriter", () => {
|
|
9
57
|
let dir: string;
|
|
10
58
|
|
|
@@ -21,7 +69,7 @@ describe("clineWriter", () => {
|
|
|
21
69
|
expect(clineWriter.label).toBe("Cline");
|
|
22
70
|
});
|
|
23
71
|
|
|
24
|
-
it("writes .
|
|
72
|
+
it("writes cline_mcp_settings.json with MCP servers", async () => {
|
|
25
73
|
const config: LogicalConfig = {
|
|
26
74
|
mcp_servers: [
|
|
27
75
|
{
|
|
@@ -41,7 +89,7 @@ describe("clineWriter", () => {
|
|
|
41
89
|
|
|
42
90
|
await clineWriter.install(config, dir);
|
|
43
91
|
|
|
44
|
-
const raw = await readFile(join(dir, ".
|
|
92
|
+
const raw = await readFile(join(dir, "cline_mcp_settings.json"), "utf-8");
|
|
45
93
|
const parsed = JSON.parse(raw);
|
|
46
94
|
expect(parsed.mcpServers["workflows"]).toEqual({
|
|
47
95
|
command: "npx",
|
|
@@ -50,6 +98,36 @@ describe("clineWriter", () => {
|
|
|
50
98
|
});
|
|
51
99
|
});
|
|
52
100
|
|
|
101
|
+
it("forwards explicit MCP approvals unchanged from provisioning", async () => {
|
|
102
|
+
const config: LogicalConfig = {
|
|
103
|
+
mcp_servers: [
|
|
104
|
+
{
|
|
105
|
+
ref: "workflows",
|
|
106
|
+
command: "npx",
|
|
107
|
+
args: ["-y", "@codemcp/workflows"],
|
|
108
|
+
env: {},
|
|
109
|
+
allowedTools: ["whats_next", "proceed_to_phase"]
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
instructions: [],
|
|
113
|
+
cli_actions: [],
|
|
114
|
+
knowledge_sources: [],
|
|
115
|
+
skills: [],
|
|
116
|
+
git_hooks: [],
|
|
117
|
+
setup_notes: []
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
await clineWriter.install(config, dir);
|
|
121
|
+
|
|
122
|
+
const raw = await readFile(join(dir, "cline_mcp_settings.json"), "utf-8");
|
|
123
|
+
const parsed = JSON.parse(raw);
|
|
124
|
+
expect(parsed.mcpServers["workflows"]).toEqual({
|
|
125
|
+
command: "npx",
|
|
126
|
+
args: ["-y", "@codemcp/workflows"],
|
|
127
|
+
alwaysAllow: ["whats_next", "proceed_to_phase"]
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
53
131
|
it("writes .clinerules with instructions", async () => {
|
|
54
132
|
const config: LogicalConfig = {
|
|
55
133
|
mcp_servers: [],
|
|
@@ -66,4 +144,69 @@ describe("clineWriter", () => {
|
|
|
66
144
|
const content = await readFile(join(dir, ".clinerules"), "utf-8");
|
|
67
145
|
expect(content).toContain("Follow TDD.");
|
|
68
146
|
});
|
|
147
|
+
|
|
148
|
+
it("does not invent built-in auto-approval settings for autonomy profiles", async () => {
|
|
149
|
+
const rigidRoot = join(dir, "rigid");
|
|
150
|
+
const sensibleRoot = join(dir, "sensible");
|
|
151
|
+
const maxRoot = join(dir, "max");
|
|
152
|
+
|
|
153
|
+
const rigidConfig: LogicalConfig = {
|
|
154
|
+
mcp_servers: [
|
|
155
|
+
{
|
|
156
|
+
ref: "workflows",
|
|
157
|
+
command: "npx",
|
|
158
|
+
args: ["-y", "@codemcp/workflows"],
|
|
159
|
+
env: {}
|
|
160
|
+
}
|
|
161
|
+
],
|
|
162
|
+
instructions: ["Use approvals for risky actions."],
|
|
163
|
+
cli_actions: [],
|
|
164
|
+
knowledge_sources: [],
|
|
165
|
+
skills: [],
|
|
166
|
+
git_hooks: [],
|
|
167
|
+
setup_notes: [],
|
|
168
|
+
permission_policy: autonomyPolicy("rigid")
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const sensibleConfig: LogicalConfig = {
|
|
172
|
+
...rigidConfig,
|
|
173
|
+
permission_policy: autonomyPolicy("sensible-defaults")
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const maxConfig: LogicalConfig = {
|
|
177
|
+
...rigidConfig,
|
|
178
|
+
permission_policy: autonomyPolicy("max-autonomy")
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
await clineWriter.install(rigidConfig, rigidRoot);
|
|
182
|
+
await clineWriter.install(sensibleConfig, sensibleRoot);
|
|
183
|
+
await clineWriter.install(maxConfig, maxRoot);
|
|
184
|
+
|
|
185
|
+
const rigidSettings = JSON.parse(
|
|
186
|
+
await readFile(join(rigidRoot, "cline_mcp_settings.json"), "utf-8")
|
|
187
|
+
);
|
|
188
|
+
const sensibleSettings = JSON.parse(
|
|
189
|
+
await readFile(join(sensibleRoot, "cline_mcp_settings.json"), "utf-8")
|
|
190
|
+
);
|
|
191
|
+
const maxSettings = JSON.parse(
|
|
192
|
+
await readFile(join(maxRoot, "cline_mcp_settings.json"), "utf-8")
|
|
193
|
+
);
|
|
194
|
+
const maxRules = await readFile(join(maxRoot, ".clinerules"), "utf-8");
|
|
195
|
+
|
|
196
|
+
expect(rigidSettings).toEqual(sensibleSettings);
|
|
197
|
+
expect(sensibleSettings).toEqual(maxSettings);
|
|
198
|
+
expect(maxSettings).toEqual({
|
|
199
|
+
mcpServers: {
|
|
200
|
+
workflows: {
|
|
201
|
+
command: "npx",
|
|
202
|
+
args: ["-y", "@codemcp/workflows"],
|
|
203
|
+
alwaysAllow: ["*"]
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
expect(maxRules).toContain("Use approvals for risky actions.");
|
|
208
|
+
expect(maxRules).not.toContain("browser_action");
|
|
209
|
+
expect(maxRules).not.toContain("execute_command");
|
|
210
|
+
expect(maxRules).not.toContain("web");
|
|
211
|
+
});
|
|
69
212
|
});
|
package/src/writers/cline.ts
CHANGED
|
@@ -11,10 +11,10 @@ import {
|
|
|
11
11
|
export const clineWriter: HarnessWriter = {
|
|
12
12
|
id: "cline",
|
|
13
13
|
label: "Cline",
|
|
14
|
-
description: "VS Code AI agent — .
|
|
14
|
+
description: "VS Code AI agent — cline_mcp_settings.json + .clinerules",
|
|
15
15
|
async install(config: LogicalConfig, projectRoot: string) {
|
|
16
16
|
await writeMcpServers(config.mcp_servers, {
|
|
17
|
-
path: join(projectRoot, ".
|
|
17
|
+
path: join(projectRoot, "cline_mcp_settings.json"),
|
|
18
18
|
transform: alwaysAllowEntry
|
|
19
19
|
});
|
|
20
20
|
|
|
@@ -2,9 +2,57 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
|
2
2
|
import { mkdtemp, rm, readFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
AutonomyProfile,
|
|
7
|
+
LogicalConfig,
|
|
8
|
+
PermissionPolicy
|
|
9
|
+
} from "@codemcp/ade-core";
|
|
6
10
|
import { copilotWriter } from "./copilot.js";
|
|
7
11
|
|
|
12
|
+
function autonomyPolicy(profile: AutonomyProfile): PermissionPolicy {
|
|
13
|
+
switch (profile) {
|
|
14
|
+
case "rigid":
|
|
15
|
+
return {
|
|
16
|
+
profile,
|
|
17
|
+
capabilities: {
|
|
18
|
+
read: "ask",
|
|
19
|
+
edit_write: "ask",
|
|
20
|
+
search_list: "ask",
|
|
21
|
+
bash_safe: "ask",
|
|
22
|
+
bash_unsafe: "ask",
|
|
23
|
+
web: "ask",
|
|
24
|
+
task_agent: "ask"
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
case "sensible-defaults":
|
|
28
|
+
return {
|
|
29
|
+
profile,
|
|
30
|
+
capabilities: {
|
|
31
|
+
read: "allow",
|
|
32
|
+
edit_write: "allow",
|
|
33
|
+
search_list: "allow",
|
|
34
|
+
bash_safe: "allow",
|
|
35
|
+
bash_unsafe: "ask",
|
|
36
|
+
web: "ask",
|
|
37
|
+
task_agent: "allow"
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
case "max-autonomy":
|
|
41
|
+
return {
|
|
42
|
+
profile,
|
|
43
|
+
capabilities: {
|
|
44
|
+
read: "allow",
|
|
45
|
+
edit_write: "allow",
|
|
46
|
+
search_list: "allow",
|
|
47
|
+
bash_safe: "allow",
|
|
48
|
+
bash_unsafe: "allow",
|
|
49
|
+
web: "ask",
|
|
50
|
+
task_agent: "allow"
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
8
56
|
describe("copilotWriter", () => {
|
|
9
57
|
let dir: string;
|
|
10
58
|
|
|
@@ -96,7 +144,115 @@ describe("copilotWriter", () => {
|
|
|
96
144
|
expect(content).toContain("name: ade");
|
|
97
145
|
expect(content).toContain("tools:");
|
|
98
146
|
expect(content).toContain(" - workflows/*");
|
|
147
|
+
expect(content).toContain("mcp-servers:");
|
|
148
|
+
expect(content).toContain(" workflows:");
|
|
149
|
+
expect(content).toContain(" type: stdio");
|
|
150
|
+
expect(content).toContain(' command: "npx"');
|
|
151
|
+
expect(content).toContain(' args: ["-y","@codemcp/workflows"]');
|
|
152
|
+
expect(content).toContain(' tools: ["*"]');
|
|
153
|
+
expect(content).toContain(" - read");
|
|
99
154
|
expect(content).toContain(" - edit");
|
|
155
|
+
expect(content).toContain(" - search");
|
|
156
|
+
expect(content).toContain(" - execute");
|
|
157
|
+
expect(content).toContain(" - agent");
|
|
158
|
+
expect(content).toContain(" - web");
|
|
159
|
+
expect(content).not.toContain("runCommands");
|
|
160
|
+
expect(content).not.toContain("runTasks");
|
|
161
|
+
expect(content).not.toContain("fetch");
|
|
162
|
+
expect(content).not.toContain("githubRepo");
|
|
100
163
|
expect(content).toContain("Follow TDD.");
|
|
101
164
|
});
|
|
165
|
+
|
|
166
|
+
it("derives the tools allowlist from autonomy while keeping web access approval-gated", async () => {
|
|
167
|
+
const rigidRoot = join(dir, "rigid");
|
|
168
|
+
const sensibleRoot = join(dir, "sensible");
|
|
169
|
+
const maxRoot = join(dir, "max");
|
|
170
|
+
|
|
171
|
+
const rigidConfig: LogicalConfig = {
|
|
172
|
+
mcp_servers: [
|
|
173
|
+
{
|
|
174
|
+
ref: "workflows",
|
|
175
|
+
command: "npx",
|
|
176
|
+
args: ["-y", "@codemcp/workflows"],
|
|
177
|
+
env: {}
|
|
178
|
+
}
|
|
179
|
+
],
|
|
180
|
+
instructions: [],
|
|
181
|
+
cli_actions: [],
|
|
182
|
+
knowledge_sources: [],
|
|
183
|
+
skills: [],
|
|
184
|
+
git_hooks: [],
|
|
185
|
+
setup_notes: [],
|
|
186
|
+
permission_policy: autonomyPolicy("rigid")
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const sensibleConfig: LogicalConfig = {
|
|
190
|
+
...rigidConfig,
|
|
191
|
+
mcp_servers: [
|
|
192
|
+
{
|
|
193
|
+
ref: "workflows",
|
|
194
|
+
command: "npx",
|
|
195
|
+
args: ["-y", "@codemcp/workflows"],
|
|
196
|
+
env: {},
|
|
197
|
+
allowedTools: ["whats_next", "proceed_to_phase"]
|
|
198
|
+
}
|
|
199
|
+
],
|
|
200
|
+
permission_policy: autonomyPolicy("sensible-defaults")
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const maxConfig: LogicalConfig = {
|
|
204
|
+
...rigidConfig,
|
|
205
|
+
permission_policy: autonomyPolicy("max-autonomy")
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
await copilotWriter.install(rigidConfig, rigidRoot);
|
|
209
|
+
await copilotWriter.install(sensibleConfig, sensibleRoot);
|
|
210
|
+
await copilotWriter.install(maxConfig, maxRoot);
|
|
211
|
+
|
|
212
|
+
const rigidAgent = await readFile(
|
|
213
|
+
join(rigidRoot, ".github", "agents", "ade.agent.md"),
|
|
214
|
+
"utf-8"
|
|
215
|
+
);
|
|
216
|
+
const sensibleAgent = await readFile(
|
|
217
|
+
join(sensibleRoot, ".github", "agents", "ade.agent.md"),
|
|
218
|
+
"utf-8"
|
|
219
|
+
);
|
|
220
|
+
const maxAgent = await readFile(
|
|
221
|
+
join(maxRoot, ".github", "agents", "ade.agent.md"),
|
|
222
|
+
"utf-8"
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
expect(rigidAgent).not.toContain(" - server/workflows/*");
|
|
226
|
+
expect(rigidAgent).toContain(" - workflows/*");
|
|
227
|
+
expect(rigidAgent).not.toContain(" - read");
|
|
228
|
+
expect(rigidAgent).not.toContain(" - edit");
|
|
229
|
+
expect(rigidAgent).not.toContain(" - search");
|
|
230
|
+
expect(rigidAgent).not.toContain(" - execute");
|
|
231
|
+
expect(rigidAgent).not.toContain(" - agent");
|
|
232
|
+
expect(rigidAgent).not.toContain(" - web");
|
|
233
|
+
|
|
234
|
+
expect(sensibleAgent).toContain(" - read");
|
|
235
|
+
expect(sensibleAgent).toContain(" - edit");
|
|
236
|
+
expect(sensibleAgent).toContain(" - search");
|
|
237
|
+
expect(sensibleAgent).toContain(" - agent");
|
|
238
|
+
expect(sensibleAgent).not.toContain(" - execute");
|
|
239
|
+
expect(sensibleAgent).not.toContain(" - todo");
|
|
240
|
+
expect(sensibleAgent).not.toContain(" - web");
|
|
241
|
+
expect(sensibleAgent).toContain(" - workflows/whats_next");
|
|
242
|
+
expect(sensibleAgent).toContain(" - workflows/proceed_to_phase");
|
|
243
|
+
expect(sensibleAgent).not.toContain(" - workflows/*");
|
|
244
|
+
expect(sensibleAgent).toContain(
|
|
245
|
+
' tools: ["whats_next","proceed_to_phase"]'
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
expect(maxAgent).toContain(" - read");
|
|
249
|
+
expect(maxAgent).toContain(" - edit");
|
|
250
|
+
expect(maxAgent).toContain(" - search");
|
|
251
|
+
expect(maxAgent).toContain(" - execute");
|
|
252
|
+
expect(maxAgent).toContain(" - agent");
|
|
253
|
+
expect(maxAgent).toContain(" - todo");
|
|
254
|
+
expect(maxAgent).not.toContain(" - web");
|
|
255
|
+
expect(maxAgent).toContain(" - workflows/*");
|
|
256
|
+
expect(maxAgent).toContain("mcp-servers:");
|
|
257
|
+
});
|
|
102
258
|
});
|
package/src/writers/copilot.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import type { LogicalConfig } from "@codemcp/ade-core";
|
|
2
|
+
import type { LogicalConfig, McpServerEntry } from "@codemcp/ade-core";
|
|
3
3
|
import type { HarnessWriter } from "../types.js";
|
|
4
4
|
import {
|
|
5
5
|
writeMcpServers,
|
|
@@ -7,6 +7,11 @@ import {
|
|
|
7
7
|
writeAgentMd,
|
|
8
8
|
writeGitHooks
|
|
9
9
|
} from "../util.js";
|
|
10
|
+
import {
|
|
11
|
+
allowsCapability,
|
|
12
|
+
hasPermissionPolicy,
|
|
13
|
+
keepsWebOnAsk
|
|
14
|
+
} from "../permission-policy.js";
|
|
10
15
|
|
|
11
16
|
export const copilotWriter: HarnessWriter = {
|
|
12
17
|
id: "copilot",
|
|
@@ -20,19 +25,81 @@ export const copilotWriter: HarnessWriter = {
|
|
|
20
25
|
});
|
|
21
26
|
|
|
22
27
|
const tools = [
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"runCommands",
|
|
26
|
-
"runTasks",
|
|
27
|
-
"fetch",
|
|
28
|
-
"githubRepo",
|
|
29
|
-
...config.mcp_servers.map((s) => `${s.ref}/*`)
|
|
28
|
+
...getBuiltInTools(config),
|
|
29
|
+
...getForwardedMcpTools(config.mcp_servers)
|
|
30
30
|
];
|
|
31
31
|
|
|
32
32
|
await writeAgentMd(config, {
|
|
33
33
|
path: join(projectRoot, ".github", "agents", "ade.agent.md"),
|
|
34
|
-
extraFrontmatter: [
|
|
34
|
+
extraFrontmatter: [
|
|
35
|
+
"tools:",
|
|
36
|
+
...tools.map((t) => ` - ${t}`),
|
|
37
|
+
...renderCopilotAgentMcpServers(config.mcp_servers)
|
|
38
|
+
]
|
|
35
39
|
});
|
|
36
40
|
await writeGitHooks(config.git_hooks, projectRoot);
|
|
37
41
|
}
|
|
38
42
|
};
|
|
43
|
+
|
|
44
|
+
function getBuiltInTools(config: LogicalConfig): string[] {
|
|
45
|
+
if (!hasPermissionPolicy(config)) {
|
|
46
|
+
return ["read", "edit", "search", "execute", "agent", "web"];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return [
|
|
50
|
+
...(allowsCapability(config, "read") ? ["read"] : []),
|
|
51
|
+
...(allowsCapability(config, "edit_write") ? ["edit"] : []),
|
|
52
|
+
...(allowsCapability(config, "search_list") ? ["search"] : []),
|
|
53
|
+
...(allowsCapability(config, "bash_unsafe") ? ["execute"] : []),
|
|
54
|
+
...(allowsCapability(config, "task_agent") ? ["agent"] : []),
|
|
55
|
+
...(allowsCapability(config, "task_agent") &&
|
|
56
|
+
allowsCapability(config, "bash_unsafe")
|
|
57
|
+
? ["todo"]
|
|
58
|
+
: []),
|
|
59
|
+
...(!keepsWebOnAsk(config) && allowsCapability(config, "web")
|
|
60
|
+
? ["web"]
|
|
61
|
+
: [])
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getForwardedMcpTools(servers: McpServerEntry[]): string[] {
|
|
66
|
+
return servers.flatMap((server) => {
|
|
67
|
+
const allowedTools = server.allowedTools ?? ["*"];
|
|
68
|
+
if (allowedTools.includes("*")) {
|
|
69
|
+
return [`${server.ref}/*`];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return allowedTools.map((tool) => `${server.ref}/${tool}`);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function renderCopilotAgentMcpServers(servers: McpServerEntry[]): string[] {
|
|
77
|
+
if (servers.length === 0) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const lines = ["mcp-servers:"];
|
|
82
|
+
|
|
83
|
+
for (const server of servers) {
|
|
84
|
+
lines.push(` ${formatYamlKey(server.ref)}:`);
|
|
85
|
+
lines.push(" type: stdio");
|
|
86
|
+
lines.push(` command: ${JSON.stringify(server.command)}`);
|
|
87
|
+
lines.push(` args: ${JSON.stringify(server.args)}`);
|
|
88
|
+
lines.push(` tools: ${JSON.stringify(server.allowedTools ?? ["*"])}`);
|
|
89
|
+
|
|
90
|
+
if (Object.keys(server.env).length > 0) {
|
|
91
|
+
lines.push(" env:");
|
|
92
|
+
for (const [key, value] of Object.entries(server.env)) {
|
|
93
|
+
lines.push(` ${formatYamlKey(key)}: ${JSON.stringify(value)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return lines;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function formatYamlKey(value: string): string {
|
|
102
|
+
return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(value)
|
|
103
|
+
? value
|
|
104
|
+
: JSON.stringify(value);
|
|
105
|
+
}
|