@codemcp/ade-harnesses 0.0.2 → 0.1.0
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/permission-policy.d.ts +7 -0
- package/dist/permission-policy.js +152 -0
- package/dist/writers/claude-code.js +50 -18
- 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/permission-policy.ts +173 -0
- package/src/writers/claude-code.spec.ts +160 -3
- package/src/writers/claude-code.ts +63 -18
- 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
|
@@ -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
|
+
}
|
|
@@ -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 { cursorWriter } from "./cursor.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("cursorWriter", () => {
|
|
9
57
|
let dir: string;
|
|
10
58
|
|
|
@@ -71,6 +119,61 @@ describe("cursorWriter", () => {
|
|
|
71
119
|
expect(content).toContain("Use conventional commits.");
|
|
72
120
|
});
|
|
73
121
|
|
|
122
|
+
it("documents autonomy limits in Cursor rules without inventing built-in permission config", async () => {
|
|
123
|
+
const config: LogicalConfig = {
|
|
124
|
+
mcp_servers: [
|
|
125
|
+
{
|
|
126
|
+
ref: "workflows",
|
|
127
|
+
command: "npx",
|
|
128
|
+
args: ["-y", "@codemcp/workflows"],
|
|
129
|
+
env: {},
|
|
130
|
+
allowedTools: ["whats_next"]
|
|
131
|
+
}
|
|
132
|
+
],
|
|
133
|
+
instructions: [],
|
|
134
|
+
cli_actions: [],
|
|
135
|
+
knowledge_sources: [],
|
|
136
|
+
skills: [],
|
|
137
|
+
git_hooks: [],
|
|
138
|
+
setup_notes: [],
|
|
139
|
+
permission_policy: autonomyPolicy("sensible-defaults")
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
await cursorWriter.install(config, dir);
|
|
143
|
+
|
|
144
|
+
const content = await readFile(
|
|
145
|
+
join(dir, ".cursor", "rules", "ade.mdc"),
|
|
146
|
+
"utf-8"
|
|
147
|
+
);
|
|
148
|
+
expect(content).toContain(
|
|
149
|
+
"Cursor autonomy note (documented, not enforced): sensible-defaults."
|
|
150
|
+
);
|
|
151
|
+
expect(content).toContain(
|
|
152
|
+
"Cursor has no verified committed project-local built-in ask/allow/deny config surface"
|
|
153
|
+
);
|
|
154
|
+
expect(content).toContain(
|
|
155
|
+
"Prefer handling these built-in capabilities without extra approval when Cursor permits it: read project files, edit and write project files, search and list project contents, run safe local shell commands, delegate or decompose work into agent tasks."
|
|
156
|
+
);
|
|
157
|
+
expect(content).toContain(
|
|
158
|
+
"Request approval before these capabilities: run high-impact shell commands, use web or network access."
|
|
159
|
+
);
|
|
160
|
+
expect(content).toContain(
|
|
161
|
+
"Web and network access must remain approval-gated."
|
|
162
|
+
);
|
|
163
|
+
expect(content).toContain(
|
|
164
|
+
"MCP server registration stays in .cursor/mcp.json; MCP tool approvals remain owned by provisioning"
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const raw = await readFile(join(dir, ".cursor", "mcp.json"), "utf-8");
|
|
168
|
+
const parsed = JSON.parse(raw);
|
|
169
|
+
expect(parsed).not.toHaveProperty("permissions");
|
|
170
|
+
expect(parsed.mcpServers["workflows"]).toEqual({
|
|
171
|
+
command: "npx",
|
|
172
|
+
args: ["-y", "@codemcp/workflows"]
|
|
173
|
+
});
|
|
174
|
+
expect(parsed.mcpServers["workflows"]).not.toHaveProperty("allowedTools");
|
|
175
|
+
});
|
|
176
|
+
|
|
74
177
|
it("includes agentskills server from mcp_servers", async () => {
|
|
75
178
|
const config: LogicalConfig = {
|
|
76
179
|
mcp_servers: [
|
package/src/writers/cursor.ts
CHANGED
|
@@ -1,8 +1,33 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import type { LogicalConfig } from "@codemcp/ade-core";
|
|
3
|
+
import type { AutonomyCapability, LogicalConfig } from "@codemcp/ade-core";
|
|
4
4
|
import type { HarnessWriter } from "../types.js";
|
|
5
5
|
import { writeMcpServers, writeGitHooks } from "../util.js";
|
|
6
|
+
import {
|
|
7
|
+
getAutonomyProfile,
|
|
8
|
+
getCapabilityDecision,
|
|
9
|
+
hasPermissionPolicy
|
|
10
|
+
} from "../permission-policy.js";
|
|
11
|
+
|
|
12
|
+
const CURSOR_CAPABILITY_ORDER: AutonomyCapability[] = [
|
|
13
|
+
"read",
|
|
14
|
+
"edit_write",
|
|
15
|
+
"search_list",
|
|
16
|
+
"bash_safe",
|
|
17
|
+
"bash_unsafe",
|
|
18
|
+
"web",
|
|
19
|
+
"task_agent"
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const CURSOR_CAPABILITY_LABELS: Record<AutonomyCapability, string> = {
|
|
23
|
+
read: "read project files",
|
|
24
|
+
edit_write: "edit and write project files",
|
|
25
|
+
search_list: "search and list project contents",
|
|
26
|
+
bash_safe: "run safe local shell commands",
|
|
27
|
+
bash_unsafe: "run high-impact shell commands",
|
|
28
|
+
web: "use web or network access",
|
|
29
|
+
task_agent: "delegate or decompose work into agent tasks"
|
|
30
|
+
};
|
|
6
31
|
|
|
7
32
|
export const cursorWriter: HarnessWriter = {
|
|
8
33
|
id: "cursor",
|
|
@@ -13,7 +38,9 @@ export const cursorWriter: HarnessWriter = {
|
|
|
13
38
|
path: join(projectRoot, ".cursor", "mcp.json")
|
|
14
39
|
});
|
|
15
40
|
|
|
16
|
-
|
|
41
|
+
const rulesBody = getCursorRulesBody(config);
|
|
42
|
+
|
|
43
|
+
if (rulesBody.length > 0) {
|
|
17
44
|
const rulesDir = join(projectRoot, ".cursor", "rules");
|
|
18
45
|
await mkdir(rulesDir, { recursive: true });
|
|
19
46
|
|
|
@@ -23,7 +50,7 @@ export const cursorWriter: HarnessWriter = {
|
|
|
23
50
|
"globs: *",
|
|
24
51
|
"---",
|
|
25
52
|
"",
|
|
26
|
-
...
|
|
53
|
+
...rulesBody.flatMap((line) => [line, ""])
|
|
27
54
|
].join("\n");
|
|
28
55
|
|
|
29
56
|
await writeFile(join(rulesDir, "ade.mdc"), content, "utf-8");
|
|
@@ -31,3 +58,38 @@ export const cursorWriter: HarnessWriter = {
|
|
|
31
58
|
await writeGitHooks(config.git_hooks, projectRoot);
|
|
32
59
|
}
|
|
33
60
|
};
|
|
61
|
+
|
|
62
|
+
function getCursorRulesBody(config: LogicalConfig): string[] {
|
|
63
|
+
return [...config.instructions, ...getCursorAutonomyNotes(config)];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getCursorAutonomyNotes(config: LogicalConfig): string[] {
|
|
67
|
+
if (!hasPermissionPolicy(config)) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const allowedCapabilities = CURSOR_CAPABILITY_ORDER.filter(
|
|
72
|
+
(capability) => getCapabilityDecision(config, capability) === "allow"
|
|
73
|
+
).map((capability) => CURSOR_CAPABILITY_LABELS[capability]);
|
|
74
|
+
|
|
75
|
+
const approvalGatedCapabilities = CURSOR_CAPABILITY_ORDER.filter(
|
|
76
|
+
(capability) => getCapabilityDecision(config, capability) === "ask"
|
|
77
|
+
).map((capability) => CURSOR_CAPABILITY_LABELS[capability]);
|
|
78
|
+
|
|
79
|
+
return [
|
|
80
|
+
`Cursor autonomy note (documented, not enforced): ${getAutonomyProfile(config) ?? "custom"}.`,
|
|
81
|
+
"Cursor has no verified committed project-local built-in ask/allow/deny config surface, so ADE documents autonomy intent here instead of writing unsupported permission config.",
|
|
82
|
+
...(allowedCapabilities.length > 0
|
|
83
|
+
? [
|
|
84
|
+
`Prefer handling these built-in capabilities without extra approval when Cursor permits it: ${allowedCapabilities.join(", ")}.`
|
|
85
|
+
]
|
|
86
|
+
: []),
|
|
87
|
+
...(approvalGatedCapabilities.length > 0
|
|
88
|
+
? [
|
|
89
|
+
`Request approval before these capabilities: ${approvalGatedCapabilities.join(", ")}.`
|
|
90
|
+
]
|
|
91
|
+
: []),
|
|
92
|
+
"Web and network access must remain approval-gated.",
|
|
93
|
+
"MCP server registration stays in .cursor/mcp.json; MCP tool approvals remain owned by provisioning and are not enforced or re-modeled in this rules file."
|
|
94
|
+
];
|
|
95
|
+
}
|