@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
package/dist/writers/roo-code.js
CHANGED
|
@@ -1,15 +1,51 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import { writeMcpServers, alwaysAllowEntry, writeRulesFile, writeGitHooks } from "../util.js";
|
|
2
|
+
import { readJsonOrEmpty, writeMcpServers, alwaysAllowEntry, writeRulesFile, writeGitHooks, writeJson } from "../util.js";
|
|
3
|
+
import { allowsCapability, hasPermissionPolicy } from "../permission-policy.js";
|
|
3
4
|
export const rooCodeWriter = {
|
|
4
5
|
id: "roo-code",
|
|
5
6
|
label: "Roo Code",
|
|
6
|
-
description: "AI coding agent — .roo/mcp.json + .roorules",
|
|
7
|
+
description: "AI coding agent — .roo/mcp.json + .roomodes + .roorules",
|
|
7
8
|
async install(config, projectRoot) {
|
|
8
9
|
await writeMcpServers(config.mcp_servers, {
|
|
9
10
|
path: join(projectRoot, ".roo", "mcp.json"),
|
|
10
11
|
transform: alwaysAllowEntry
|
|
11
12
|
});
|
|
13
|
+
await writeRooModes(config, projectRoot);
|
|
12
14
|
await writeRulesFile(config.instructions, join(projectRoot, ".roorules"));
|
|
13
15
|
await writeGitHooks(config.git_hooks, projectRoot);
|
|
14
16
|
}
|
|
15
17
|
};
|
|
18
|
+
async function writeRooModes(config, projectRoot) {
|
|
19
|
+
if (!hasPermissionPolicy(config)) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const roomodesPath = join(projectRoot, ".roomodes");
|
|
23
|
+
const existing = await readJsonOrEmpty(roomodesPath);
|
|
24
|
+
const existingCustomModes = asRecord(existing.customModes);
|
|
25
|
+
await writeJson(roomodesPath, {
|
|
26
|
+
...existing,
|
|
27
|
+
customModes: {
|
|
28
|
+
...existingCustomModes,
|
|
29
|
+
ade: {
|
|
30
|
+
slug: "ade",
|
|
31
|
+
name: "ADE",
|
|
32
|
+
roleDefinition: "ADE — Agentic Development Environment mode generated by ADE.",
|
|
33
|
+
groups: getRooModeGroups(config),
|
|
34
|
+
source: "project"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function asRecord(value) {
|
|
40
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
41
|
+
? value
|
|
42
|
+
: {};
|
|
43
|
+
}
|
|
44
|
+
function getRooModeGroups(config) {
|
|
45
|
+
return [
|
|
46
|
+
...(allowsCapability(config, "read") ? ["read"] : []),
|
|
47
|
+
...(allowsCapability(config, "edit_write") ? ["edit"] : []),
|
|
48
|
+
...(allowsCapability(config, "bash_unsafe") ? ["command"] : []),
|
|
49
|
+
...(config.mcp_servers.length > 0 ? ["mcp"] : [])
|
|
50
|
+
];
|
|
51
|
+
}
|
|
@@ -1,16 +1,54 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { writeFile } from "node:fs/promises";
|
|
3
3
|
import { writeMcpServers, writeGitHooks } from "../util.js";
|
|
4
|
+
const CAPABILITY_ORDER = [
|
|
5
|
+
"read",
|
|
6
|
+
"edit_write",
|
|
7
|
+
"search_list",
|
|
8
|
+
"bash_safe",
|
|
9
|
+
"bash_unsafe",
|
|
10
|
+
"web",
|
|
11
|
+
"task_agent"
|
|
12
|
+
];
|
|
13
|
+
function formatCapabilityGuidance(capability, decision) {
|
|
14
|
+
return `- \`${capability}\`: ${decision}`;
|
|
15
|
+
}
|
|
16
|
+
function renderAutonomyGuidance(config) {
|
|
17
|
+
const policy = config.permission_policy;
|
|
18
|
+
if (!policy) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const capabilityLines = CAPABILITY_ORDER.map((capability) => formatCapabilityGuidance(capability, policy.capabilities[capability]));
|
|
22
|
+
return [
|
|
23
|
+
"## Autonomy",
|
|
24
|
+
"",
|
|
25
|
+
"Universal harness limitation: `AGENTS.md` + `.mcp.json` provide documentation and server registration only; there is no enforceable harness-level permission schema here.",
|
|
26
|
+
"",
|
|
27
|
+
"Treat this autonomy profile as documentation-only guidance for built-in/basic operations.",
|
|
28
|
+
"",
|
|
29
|
+
`Profile: \`${policy.profile}\``,
|
|
30
|
+
"",
|
|
31
|
+
"Built-in/basic capability guidance:",
|
|
32
|
+
...capabilityLines,
|
|
33
|
+
"",
|
|
34
|
+
"MCP permissions are not re-modeled by autonomy here; any MCP approvals must come from provisioning-aware consuming harnesses rather than the Universal writer."
|
|
35
|
+
].join("\n");
|
|
36
|
+
}
|
|
4
37
|
export const universalWriter = {
|
|
5
38
|
id: "universal",
|
|
6
39
|
label: "Universal (AGENTS.md + .mcp.json)",
|
|
7
|
-
description: "Cross-tool standard — AGENTS.md + .mcp.json (
|
|
40
|
+
description: "Cross-tool standard — AGENTS.md + .mcp.json (portable instructions and MCP registration, not enforceable permissions)",
|
|
8
41
|
async install(config, projectRoot) {
|
|
9
|
-
|
|
42
|
+
const autonomyGuidance = renderAutonomyGuidance(config);
|
|
43
|
+
const instructionSections = [...config.instructions];
|
|
44
|
+
if (autonomyGuidance) {
|
|
45
|
+
instructionSections.push(autonomyGuidance);
|
|
46
|
+
}
|
|
47
|
+
if (instructionSections.length > 0) {
|
|
10
48
|
const lines = [
|
|
11
49
|
"# AGENTS",
|
|
12
50
|
"",
|
|
13
|
-
...
|
|
51
|
+
...instructionSections.flatMap((instruction) => [instruction, ""])
|
|
14
52
|
];
|
|
15
53
|
await writeFile(join(projectRoot, "AGENTS.md"), lines.join("\n"), "utf-8");
|
|
16
54
|
}
|
package/dist/writers/windsurf.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { writeMcpServers, alwaysAllowEntry, writeRulesFile, writeGitHooks } from "../util.js";
|
|
3
|
+
import { hasPermissionPolicy } from "../permission-policy.js";
|
|
3
4
|
export const windsurfWriter = {
|
|
4
5
|
id: "windsurf",
|
|
5
6
|
label: "Windsurf",
|
|
@@ -9,7 +10,48 @@ export const windsurfWriter = {
|
|
|
9
10
|
path: join(projectRoot, ".windsurf", "mcp.json"),
|
|
10
11
|
transform: alwaysAllowEntry
|
|
11
12
|
});
|
|
12
|
-
await writeRulesFile(config
|
|
13
|
+
await writeRulesFile(getWindsurfRules(config), join(projectRoot, ".windsurfrules"));
|
|
13
14
|
await writeGitHooks(config.git_hooks, projectRoot);
|
|
14
15
|
}
|
|
15
16
|
};
|
|
17
|
+
function getWindsurfRules(config) {
|
|
18
|
+
if (!hasPermissionPolicy(config)) {
|
|
19
|
+
return config.instructions;
|
|
20
|
+
}
|
|
21
|
+
const { capabilities } = config.permission_policy;
|
|
22
|
+
const allow = listCapabilities(capabilities, "allow");
|
|
23
|
+
const ask = listCapabilities(capabilities, "ask");
|
|
24
|
+
const deny = listCapabilities(capabilities, "deny");
|
|
25
|
+
const autonomyGuidance = [
|
|
26
|
+
"Windsurf limitation: ADE could not verify a stable committed project-local permission schema for Windsurf built-in tools, so this autonomy policy is advisory only and should be applied conservatively.",
|
|
27
|
+
formatGuidance(allow, ask, deny)
|
|
28
|
+
];
|
|
29
|
+
return [...autonomyGuidance, ...config.instructions];
|
|
30
|
+
}
|
|
31
|
+
function listCapabilities(capabilities, decision) {
|
|
32
|
+
return Object.entries(capabilities)
|
|
33
|
+
.filter(([, value]) => value === decision)
|
|
34
|
+
.map(([capability]) => CAPABILITY_LABELS[capability]);
|
|
35
|
+
}
|
|
36
|
+
function formatGuidance(allow, ask, deny) {
|
|
37
|
+
const lines = ["Autonomy guidance for Windsurf built-in capabilities:"];
|
|
38
|
+
if (allow.length > 0) {
|
|
39
|
+
lines.push(`- May proceed without extra approval: ${allow.join(", ")}.`);
|
|
40
|
+
}
|
|
41
|
+
if (ask.length > 0) {
|
|
42
|
+
lines.push(`- Ask before: ${ask.join(", ")}.`);
|
|
43
|
+
}
|
|
44
|
+
if (deny.length > 0) {
|
|
45
|
+
lines.push(`- Do not use unless the user explicitly overrides: ${deny.join(", ")}.`);
|
|
46
|
+
}
|
|
47
|
+
return lines.join("\n");
|
|
48
|
+
}
|
|
49
|
+
const CAPABILITY_LABELS = {
|
|
50
|
+
read: "read files",
|
|
51
|
+
edit_write: "edit and write files",
|
|
52
|
+
search_list: "search and list files",
|
|
53
|
+
bash_safe: "safe local shell commands",
|
|
54
|
+
bash_unsafe: "unsafe local shell commands",
|
|
55
|
+
web: "web and network access",
|
|
56
|
+
task_agent: "task or agent delegation"
|
|
57
|
+
};
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"@codemcp/skills": "^2.3.0",
|
|
11
|
-
"@codemcp/ade-core": "0.0
|
|
11
|
+
"@codemcp/ade-core": "0.1.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"@typescript-eslint/eslint-plugin": "^8.21.0",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"rimraf": "^6.0.1",
|
|
20
20
|
"typescript": "^5.7.3"
|
|
21
21
|
},
|
|
22
|
-
"version": "0.0
|
|
22
|
+
"version": "0.1.0",
|
|
23
23
|
"scripts": {
|
|
24
24
|
"build": "tsc -p tsconfig.build.json",
|
|
25
25
|
"clean:build": "rimraf ./dist",
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AutonomyCapability,
|
|
3
|
+
LogicalConfig,
|
|
4
|
+
PermissionDecision,
|
|
5
|
+
PermissionRule
|
|
6
|
+
} from "@codemcp/ade-core";
|
|
7
|
+
|
|
8
|
+
const SENSIBLE_DEFAULTS_RULES: Record<string, PermissionRule> = {
|
|
9
|
+
read: {
|
|
10
|
+
"*": "allow",
|
|
11
|
+
"*.env": "deny",
|
|
12
|
+
"*.env.*": "deny",
|
|
13
|
+
"*.env.example": "allow"
|
|
14
|
+
},
|
|
15
|
+
edit: "allow",
|
|
16
|
+
glob: "allow",
|
|
17
|
+
grep: "allow",
|
|
18
|
+
list: "allow",
|
|
19
|
+
lsp: "allow",
|
|
20
|
+
task: "allow",
|
|
21
|
+
todoread: "deny",
|
|
22
|
+
todowrite: "deny",
|
|
23
|
+
skill: "deny",
|
|
24
|
+
webfetch: "ask",
|
|
25
|
+
websearch: "ask",
|
|
26
|
+
codesearch: "ask",
|
|
27
|
+
bash: {
|
|
28
|
+
"*": "deny",
|
|
29
|
+
"grep *": "allow",
|
|
30
|
+
"rg *": "allow",
|
|
31
|
+
"find *": "allow",
|
|
32
|
+
"fd *": "allow",
|
|
33
|
+
ls: "allow",
|
|
34
|
+
"ls *": "allow",
|
|
35
|
+
"cat *": "allow",
|
|
36
|
+
"head *": "allow",
|
|
37
|
+
"tail *": "allow",
|
|
38
|
+
"wc *": "allow",
|
|
39
|
+
"sort *": "allow",
|
|
40
|
+
"uniq *": "allow",
|
|
41
|
+
"diff *": "allow",
|
|
42
|
+
"echo *": "allow",
|
|
43
|
+
"printf *": "allow",
|
|
44
|
+
pwd: "allow",
|
|
45
|
+
"which *": "allow",
|
|
46
|
+
"type *": "allow",
|
|
47
|
+
whoami: "allow",
|
|
48
|
+
date: "allow",
|
|
49
|
+
"date *": "allow",
|
|
50
|
+
env: "allow",
|
|
51
|
+
"tree *": "allow",
|
|
52
|
+
"file *": "allow",
|
|
53
|
+
"stat *": "allow",
|
|
54
|
+
"readlink *": "allow",
|
|
55
|
+
"realpath *": "allow",
|
|
56
|
+
"dirname *": "allow",
|
|
57
|
+
"basename *": "allow",
|
|
58
|
+
"sed *": "allow",
|
|
59
|
+
"awk *": "allow",
|
|
60
|
+
"cut *": "allow",
|
|
61
|
+
"tr *": "allow",
|
|
62
|
+
"tee *": "allow",
|
|
63
|
+
"xargs *": "allow",
|
|
64
|
+
"jq *": "allow",
|
|
65
|
+
"yq *": "allow",
|
|
66
|
+
"mkdir *": "allow",
|
|
67
|
+
"touch *": "allow",
|
|
68
|
+
"cp *": "ask",
|
|
69
|
+
"mv *": "ask",
|
|
70
|
+
"ln *": "ask",
|
|
71
|
+
"npm *": "ask",
|
|
72
|
+
"node *": "ask",
|
|
73
|
+
"pip *": "ask",
|
|
74
|
+
"python *": "ask",
|
|
75
|
+
"python3 *": "ask",
|
|
76
|
+
"rm *": "deny",
|
|
77
|
+
"rmdir *": "deny",
|
|
78
|
+
"curl *": "deny",
|
|
79
|
+
"wget *": "deny",
|
|
80
|
+
"chmod *": "deny",
|
|
81
|
+
"chown *": "deny",
|
|
82
|
+
"sudo *": "deny",
|
|
83
|
+
"su *": "deny",
|
|
84
|
+
"sh *": "deny",
|
|
85
|
+
"bash *": "deny",
|
|
86
|
+
"zsh *": "deny",
|
|
87
|
+
"eval *": "deny",
|
|
88
|
+
"exec *": "deny",
|
|
89
|
+
"source *": "deny",
|
|
90
|
+
". *": "deny",
|
|
91
|
+
"nohup *": "deny",
|
|
92
|
+
"dd *": "deny",
|
|
93
|
+
"mkfs *": "deny",
|
|
94
|
+
"mount *": "deny",
|
|
95
|
+
"umount *": "deny",
|
|
96
|
+
"kill *": "deny",
|
|
97
|
+
"killall *": "deny",
|
|
98
|
+
"pkill *": "deny",
|
|
99
|
+
"nc *": "deny",
|
|
100
|
+
"ncat *": "deny",
|
|
101
|
+
"ssh *": "deny",
|
|
102
|
+
"scp *": "deny",
|
|
103
|
+
"rsync *": "deny",
|
|
104
|
+
"docker *": "deny",
|
|
105
|
+
"kubectl *": "deny",
|
|
106
|
+
"systemctl *": "deny",
|
|
107
|
+
"service *": "deny",
|
|
108
|
+
"crontab *": "deny",
|
|
109
|
+
reboot: "deny",
|
|
110
|
+
"shutdown *": "deny",
|
|
111
|
+
"passwd *": "deny",
|
|
112
|
+
"useradd *": "deny",
|
|
113
|
+
"userdel *": "deny",
|
|
114
|
+
"iptables *": "deny"
|
|
115
|
+
},
|
|
116
|
+
external_directory: "deny",
|
|
117
|
+
doom_loop: "deny"
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export function getAutonomyProfile(config: LogicalConfig) {
|
|
121
|
+
return config.permission_policy?.profile;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function hasPermissionPolicy(config: LogicalConfig): boolean {
|
|
125
|
+
return config.permission_policy !== undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function getCapabilityDecision(
|
|
129
|
+
config: LogicalConfig,
|
|
130
|
+
capability: AutonomyCapability
|
|
131
|
+
): PermissionDecision | undefined {
|
|
132
|
+
return config.permission_policy?.capabilities?.[capability];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function allowsCapability(
|
|
136
|
+
config: LogicalConfig,
|
|
137
|
+
capability: AutonomyCapability
|
|
138
|
+
): boolean {
|
|
139
|
+
return getCapabilityDecision(config, capability) === "allow";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function keepsWebOnAsk(config: LogicalConfig): boolean {
|
|
143
|
+
return getCapabilityDecision(config, "web") === "ask";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function getHarnessPermissionRules(
|
|
147
|
+
config: LogicalConfig
|
|
148
|
+
): Record<string, PermissionRule> | undefined {
|
|
149
|
+
switch (config.permission_policy?.profile) {
|
|
150
|
+
case "rigid":
|
|
151
|
+
return {
|
|
152
|
+
"*": "ask",
|
|
153
|
+
webfetch: "ask",
|
|
154
|
+
websearch: "ask",
|
|
155
|
+
codesearch: "ask",
|
|
156
|
+
external_directory: "deny",
|
|
157
|
+
doom_loop: "deny"
|
|
158
|
+
};
|
|
159
|
+
case "sensible-defaults":
|
|
160
|
+
return SENSIBLE_DEFAULTS_RULES;
|
|
161
|
+
case "max-autonomy":
|
|
162
|
+
return {
|
|
163
|
+
"*": "allow",
|
|
164
|
+
webfetch: "ask",
|
|
165
|
+
websearch: "ask",
|
|
166
|
+
codesearch: "ask",
|
|
167
|
+
external_directory: "deny",
|
|
168
|
+
doom_loop: "deny"
|
|
169
|
+
};
|
|
170
|
+
default:
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -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 { claudeCodeWriter } from "./claude-code.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("claudeCodeWriter", () => {
|
|
9
57
|
let dir: string;
|
|
10
58
|
|
|
@@ -80,7 +128,38 @@ describe("claudeCodeWriter", () => {
|
|
|
80
128
|
});
|
|
81
129
|
});
|
|
82
130
|
|
|
83
|
-
it("
|
|
131
|
+
it("forwards explicit MCP tool permissions using Claude rule names", async () => {
|
|
132
|
+
const config: LogicalConfig = {
|
|
133
|
+
mcp_servers: [
|
|
134
|
+
{
|
|
135
|
+
ref: "workflows",
|
|
136
|
+
command: "npx",
|
|
137
|
+
args: ["-y", "@codemcp/workflows"],
|
|
138
|
+
env: {},
|
|
139
|
+
allowedTools: ["use_skill", "whats_next"]
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
instructions: [],
|
|
143
|
+
cli_actions: [],
|
|
144
|
+
knowledge_sources: [],
|
|
145
|
+
skills: [],
|
|
146
|
+
git_hooks: [],
|
|
147
|
+
setup_notes: []
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
await claudeCodeWriter.install(config, dir);
|
|
151
|
+
|
|
152
|
+
const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8");
|
|
153
|
+
const settings = JSON.parse(raw);
|
|
154
|
+
expect(settings.permissions.allow).toEqual(
|
|
155
|
+
expect.arrayContaining([
|
|
156
|
+
"mcp__workflows__use_skill",
|
|
157
|
+
"mcp__workflows__whats_next"
|
|
158
|
+
])
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("does not invent wildcard MCP permission rules", async () => {
|
|
84
163
|
const config: LogicalConfig = {
|
|
85
164
|
mcp_servers: [
|
|
86
165
|
{
|
|
@@ -102,7 +181,85 @@ describe("claudeCodeWriter", () => {
|
|
|
102
181
|
|
|
103
182
|
const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8");
|
|
104
183
|
const settings = JSON.parse(raw);
|
|
105
|
-
expect(settings.permissions.allow).
|
|
184
|
+
expect(settings.permissions.allow ?? []).toEqual([]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("keeps web on ask for rigid autonomy without broad built-in allows", async () => {
|
|
188
|
+
const config: LogicalConfig = {
|
|
189
|
+
mcp_servers: [],
|
|
190
|
+
instructions: [],
|
|
191
|
+
cli_actions: [],
|
|
192
|
+
knowledge_sources: [],
|
|
193
|
+
skills: [],
|
|
194
|
+
git_hooks: [],
|
|
195
|
+
setup_notes: [],
|
|
196
|
+
permission_policy: autonomyPolicy("rigid")
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
await claudeCodeWriter.install(config, dir);
|
|
200
|
+
|
|
201
|
+
const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8");
|
|
202
|
+
const settings = JSON.parse(raw);
|
|
203
|
+
expect(settings.permissions.allow ?? []).toEqual([]);
|
|
204
|
+
expect(settings.permissions.ask).toEqual(
|
|
205
|
+
expect.arrayContaining(["WebFetch", "WebSearch"])
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("maps sensible-defaults to Claude built-in permission rules", async () => {
|
|
210
|
+
const config: LogicalConfig = {
|
|
211
|
+
mcp_servers: [],
|
|
212
|
+
instructions: [],
|
|
213
|
+
cli_actions: [],
|
|
214
|
+
knowledge_sources: [],
|
|
215
|
+
skills: [],
|
|
216
|
+
git_hooks: [],
|
|
217
|
+
setup_notes: [],
|
|
218
|
+
permission_policy: autonomyPolicy("sensible-defaults")
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
await claudeCodeWriter.install(config, dir);
|
|
222
|
+
|
|
223
|
+
const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8");
|
|
224
|
+
const settings = JSON.parse(raw);
|
|
225
|
+
expect(settings.permissions.allow).toEqual(
|
|
226
|
+
expect.arrayContaining(["Read", "Edit", "Glob", "Grep", "TodoWrite"])
|
|
227
|
+
);
|
|
228
|
+
expect(settings.permissions.allow).not.toContain("Bash");
|
|
229
|
+
expect(settings.permissions.ask).toEqual(
|
|
230
|
+
expect.arrayContaining(["WebFetch", "WebSearch"])
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("maps max-autonomy to broad Claude built-in permission rules while preserving web ask", async () => {
|
|
235
|
+
const config: LogicalConfig = {
|
|
236
|
+
mcp_servers: [],
|
|
237
|
+
instructions: [],
|
|
238
|
+
cli_actions: [],
|
|
239
|
+
knowledge_sources: [],
|
|
240
|
+
skills: [],
|
|
241
|
+
git_hooks: [],
|
|
242
|
+
setup_notes: [],
|
|
243
|
+
permission_policy: autonomyPolicy("max-autonomy")
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
await claudeCodeWriter.install(config, dir);
|
|
247
|
+
|
|
248
|
+
const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8");
|
|
249
|
+
const settings = JSON.parse(raw);
|
|
250
|
+
expect(settings.permissions.allow).toEqual(
|
|
251
|
+
expect.arrayContaining([
|
|
252
|
+
"Read",
|
|
253
|
+
"Edit",
|
|
254
|
+
"Bash",
|
|
255
|
+
"Glob",
|
|
256
|
+
"Grep",
|
|
257
|
+
"TodoWrite"
|
|
258
|
+
])
|
|
259
|
+
);
|
|
260
|
+
expect(settings.permissions.ask).toEqual(
|
|
261
|
+
expect.arrayContaining(["WebFetch", "WebSearch"])
|
|
262
|
+
);
|
|
106
263
|
});
|
|
107
264
|
|
|
108
265
|
it("includes agentskills server from mcp_servers", async () => {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
writeInlineSkills,
|
|
10
10
|
writeGitHooks
|
|
11
11
|
} from "../util.js";
|
|
12
|
+
import { allowsCapability, keepsWebOnAsk } from "../permission-policy.js";
|
|
12
13
|
|
|
13
14
|
export const claudeCodeWriter: HarnessWriter = {
|
|
14
15
|
id: "claude-code",
|
|
@@ -35,30 +36,74 @@ async function writeClaudeSettings(
|
|
|
35
36
|
config: LogicalConfig,
|
|
36
37
|
projectRoot: string
|
|
37
38
|
): Promise<void> {
|
|
38
|
-
const servers = config.mcp_servers;
|
|
39
|
-
if (servers.length === 0) return;
|
|
40
|
-
|
|
41
39
|
const settingsPath = join(projectRoot, ".claude", "settings.json");
|
|
42
40
|
const existing = await readJsonOrEmpty(settingsPath);
|
|
41
|
+
const existingPerms = (existing.permissions as Record<string, unknown>) ?? {};
|
|
42
|
+
const existingAllow = asStringArray(existingPerms.allow);
|
|
43
|
+
const existingAsk = asStringArray(existingPerms.ask);
|
|
43
44
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
for (const tool of allowed) {
|
|
51
|
-
allowRules.push(`MCP(${server.ref}:${tool})`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
45
|
+
const autonomyRules = getClaudeAutonomyRules(config);
|
|
46
|
+
const mcpRules = getClaudeMcpAllowRules(config);
|
|
47
|
+
const allowRules = [
|
|
48
|
+
...new Set([...existingAllow, ...autonomyRules.allow, ...mcpRules])
|
|
49
|
+
];
|
|
50
|
+
const askRules = [...new Set([...existingAsk, ...autonomyRules.ask])];
|
|
55
51
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
if (
|
|
53
|
+
allowRules.length === 0 &&
|
|
54
|
+
askRules.length === 0 &&
|
|
55
|
+
config.mcp_servers.length === 0
|
|
56
|
+
) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
59
|
|
|
60
60
|
await writeJson(settingsPath, {
|
|
61
61
|
...existing,
|
|
62
|
-
permissions: {
|
|
62
|
+
permissions: {
|
|
63
|
+
...existingPerms,
|
|
64
|
+
...(allowRules.length > 0 ? { allow: allowRules } : {}),
|
|
65
|
+
...(askRules.length > 0 ? { ask: askRules } : {})
|
|
66
|
+
}
|
|
63
67
|
});
|
|
64
68
|
}
|
|
69
|
+
|
|
70
|
+
function asStringArray(value: unknown): string[] {
|
|
71
|
+
return Array.isArray(value)
|
|
72
|
+
? value.filter((entry): entry is string => typeof entry === "string")
|
|
73
|
+
: [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getClaudeMcpAllowRules(config: LogicalConfig): string[] {
|
|
77
|
+
const allowRules: string[] = [];
|
|
78
|
+
|
|
79
|
+
for (const server of config.mcp_servers) {
|
|
80
|
+
const allowedTools = server.allowedTools;
|
|
81
|
+
if (!allowedTools || allowedTools.includes("*")) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const tool of allowedTools) {
|
|
86
|
+
allowRules.push(`mcp__${server.ref}__${tool}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return allowRules;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getClaudeAutonomyRules(config: LogicalConfig): {
|
|
94
|
+
allow: string[];
|
|
95
|
+
ask: string[];
|
|
96
|
+
} {
|
|
97
|
+
const ask = keepsWebOnAsk(config) ? ["WebFetch", "WebSearch"] : [];
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
allow: [
|
|
101
|
+
...(allowsCapability(config, "read") ? ["Read"] : []),
|
|
102
|
+
...(allowsCapability(config, "edit_write") ? ["Edit"] : []),
|
|
103
|
+
...(allowsCapability(config, "search_list") ? ["Glob", "Grep"] : []),
|
|
104
|
+
...(allowsCapability(config, "bash_unsafe") ? ["Bash"] : []),
|
|
105
|
+
...(allowsCapability(config, "task_agent") ? ["TodoWrite"] : [])
|
|
106
|
+
],
|
|
107
|
+
ask
|
|
108
|
+
};
|
|
109
|
+
}
|