axconfig 3.5.2 → 3.6.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/README.md +20 -11
- package/dist/agents/claude-reader.js +8 -8
- package/dist/agents/claude.js +22 -10
- package/dist/agents/codex-reader.js +5 -3
- package/dist/agents/codex.js +11 -8
- package/dist/agents/gemini-reader.js +6 -4
- package/dist/agents/gemini.js +37 -16
- package/dist/agents/opencode-permission-builder.d.ts +2 -2
- package/dist/agents/opencode-permission-builder.js +8 -38
- package/dist/agents/opencode-reader.js +9 -18
- package/dist/agents/opencode.js +3 -3
- package/dist/parse-permissions.d.ts +5 -2
- package/dist/parse-permissions.js +12 -5
- package/dist/types.d.ts +2 -2
- package/package.json +9 -8
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# axconfig
|
|
2
2
|
|
|
3
|
-
Unified configuration management for AI coding agents — common API for permissions, settings, and config across Claude Code, Codex, Gemini CLI, and OpenCode.
|
|
3
|
+
Unified configuration management for AI coding agents — common API for permissions, settings, and config across Claude Code, Codex, Gemini CLI, GitHub Copilot CLI, and OpenCode.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -26,11 +26,11 @@ Unified syntax for all agents:
|
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
28
|
# Tool permissions
|
|
29
|
-
read, write,
|
|
29
|
+
read, write, bash, glob, grep, webfetch
|
|
30
30
|
|
|
31
31
|
# Bash command patterns
|
|
32
32
|
bash:git *
|
|
33
|
-
bash:npm run build
|
|
33
|
+
bash:npm run build*
|
|
34
34
|
|
|
35
35
|
# Path restrictions (agent-dependent)
|
|
36
36
|
read:src/**
|
|
@@ -45,19 +45,27 @@ Translates unified permissions to agent-specific formats:
|
|
|
45
45
|
| -------- | --------------------------------------------------------- |
|
|
46
46
|
| claude | JSON `settings.json` with `permissions.allow/deny` arrays |
|
|
47
47
|
| codex | TOML `config.toml` + Starlark `.rules` files |
|
|
48
|
-
|
|
|
48
|
+
| copilot | JSON `config.json` (no pre-configured permissions) |
|
|
49
|
+
| gemini | JSON `settings.json` + TOML policy files in `policies/` |
|
|
49
50
|
| opencode | JSON with `permission.{edit,bash,webfetch}` |
|
|
50
51
|
|
|
51
52
|
### Capability Validation
|
|
52
53
|
|
|
53
54
|
Each agent has different capabilities:
|
|
54
55
|
|
|
55
|
-
| Agent | Tool Perms | Bash Patterns | Path Restrictions | Can Deny Read |
|
|
56
|
-
| -------- | ----------- | ------------- | ----------------- | ------------- |
|
|
57
|
-
| claude | ✓ | ✓ | ✓ | ✓ |
|
|
58
|
-
| codex | ✗ (sandbox) | ✓ | ✗ | ✗ |
|
|
59
|
-
|
|
|
60
|
-
|
|
|
56
|
+
| Agent | Tool Perms | Bash Patterns | Path Restrictions | Can Deny Read | Notes |
|
|
57
|
+
| -------- | ----------- | ------------- | ----------------- | ------------- | -------------------------------------------- |
|
|
58
|
+
| claude | ✓ | ✓ | ✓ | ✓ | Permissions + OS-level sandbox |
|
|
59
|
+
| codex | ✗ (sandbox) | ✓ | ✗ | ✗ | OS-level sandbox (Landlock+seccomp/Seatbelt) |
|
|
60
|
+
| copilot | ✗ | ✗ | ✗ | ✗ | Runtime prompts only (no pre-config) |
|
|
61
|
+
| gemini | ✓ | ✓ | ✗ | ✓ | Permissions only (no sandbox) |
|
|
62
|
+
| opencode | ✓ | ✓ | ✓ | ✓ | UX-only permissions (no sandbox) |
|
|
63
|
+
|
|
64
|
+
**Security notes:**
|
|
65
|
+
|
|
66
|
+
- **Claude Code** manages application-level permissions; OS-level sandbox settings (network filtering, filesystem isolation) are configured separately
|
|
67
|
+
- **Copilot CLI** does not support pre-configured permissions; all actions require runtime approval prompts
|
|
68
|
+
- **OpenCode** permissions are UX-only to keep users informed; they do not provide security isolation
|
|
61
69
|
|
|
62
70
|
axconfig validates permissions against agent capabilities:
|
|
63
71
|
|
|
@@ -217,6 +225,7 @@ src/
|
|
|
217
225
|
└── agents/
|
|
218
226
|
├── claude.ts # Claude Code config builder + reader
|
|
219
227
|
├── codex.ts # Codex config builder + reader
|
|
228
|
+
├── copilot.ts # GitHub Copilot CLI config builder + reader
|
|
220
229
|
├── gemini.ts # Gemini CLI config builder + reader
|
|
221
230
|
└── opencode.ts # OpenCode config builder + reader
|
|
222
231
|
```
|
|
@@ -232,7 +241,7 @@ Run `npx -y axconfig --help` to learn available options.
|
|
|
232
241
|
|
|
233
242
|
Use `axconfig` to manage AI agent configurations with unified permission syntax.
|
|
234
243
|
It translates `--allow` and `--deny` rules to agent-specific formats (Claude Code,
|
|
235
|
-
Codex, Gemini CLI, OpenCode).
|
|
244
|
+
Codex, Gemini CLI, GitHub Copilot CLI, OpenCode).
|
|
236
245
|
```
|
|
237
246
|
|
|
238
247
|
## License
|
|
@@ -13,15 +13,15 @@ import { createJsonConfigOperations, readJsonConfig, } from "../read-write-json-
|
|
|
13
13
|
const REVERSE_TOOL_MAP = {
|
|
14
14
|
Read: "read",
|
|
15
15
|
Write: "write",
|
|
16
|
-
Edit: "
|
|
16
|
+
Edit: "write", // Claude's Edit tool maps to canonical "write"
|
|
17
17
|
Bash: "bash",
|
|
18
18
|
Glob: "glob",
|
|
19
19
|
Grep: "grep",
|
|
20
|
-
WebFetch: "
|
|
21
|
-
WebSearch: "
|
|
20
|
+
WebFetch: "webfetch",
|
|
21
|
+
WebSearch: "webfetch",
|
|
22
22
|
};
|
|
23
23
|
/** Path-restricted tools (lowercase) */
|
|
24
|
-
const PATH_TOOLS = new Set(["read", "write"
|
|
24
|
+
const PATH_TOOLS = new Set(["read", "write"]);
|
|
25
25
|
/**
|
|
26
26
|
* Get the settings.json path for a config directory.
|
|
27
27
|
*/
|
|
@@ -45,11 +45,11 @@ function parseClaudeRule(rule) {
|
|
|
45
45
|
if (parenIndex > 0 && rule.endsWith(")")) {
|
|
46
46
|
const toolName = rule.slice(0, parenIndex);
|
|
47
47
|
const pattern = rule.slice(parenIndex + 1, -1);
|
|
48
|
-
// Bash pattern: "Bash(git:*)" → pattern is "git:*", we want "git"
|
|
48
|
+
// Bash pattern: "Bash(git:*)" → pattern is "git:*", we want "git*"
|
|
49
|
+
// Append * to indicate prefix matching (required by axconfig's canonical format)
|
|
49
50
|
if (toolName === "Bash") {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
: pattern;
|
|
51
|
+
const prefix = pattern.endsWith(":*") ? pattern.slice(0, -2) : pattern;
|
|
52
|
+
const bashPattern = prefix === "" ? "*" : `${prefix}*`;
|
|
53
53
|
return { type: "bash", pattern: bashPattern };
|
|
54
54
|
}
|
|
55
55
|
// Path pattern: "Read(src/**)" → tool is "read", pattern is "src/**"
|
package/dist/agents/claude.js
CHANGED
|
@@ -17,12 +17,11 @@ export { claudeCodeConfigReader } from "./claude-reader.js";
|
|
|
17
17
|
/** Claude Code tool name mapping */
|
|
18
18
|
const TOOL_MAP = {
|
|
19
19
|
read: "Read",
|
|
20
|
-
write: "Write",
|
|
21
|
-
edit: "Edit",
|
|
20
|
+
write: ["Write", "Edit"], // Canonical "write" maps to both Claude's Write and Edit tools
|
|
22
21
|
bash: "Bash",
|
|
23
22
|
glob: "Glob",
|
|
24
23
|
grep: "Grep",
|
|
25
|
-
|
|
24
|
+
webfetch: "WebFetch",
|
|
26
25
|
};
|
|
27
26
|
/** Claude Code capabilities */
|
|
28
27
|
const CAPABILITIES = {
|
|
@@ -33,20 +32,33 @@ const CAPABILITIES = {
|
|
|
33
32
|
};
|
|
34
33
|
/**
|
|
35
34
|
* Translate a permission rule to Claude Code format.
|
|
35
|
+
* Returns an array since some canonical tools map to multiple Claude tools.
|
|
36
36
|
*/
|
|
37
37
|
function translateRule(rule) {
|
|
38
38
|
switch (rule.type) {
|
|
39
39
|
case "tool": {
|
|
40
|
-
|
|
40
|
+
const mapped = TOOL_MAP[rule.name];
|
|
41
|
+
return Array.isArray(mapped) ? mapped : [mapped];
|
|
41
42
|
}
|
|
42
43
|
case "bash": {
|
|
43
|
-
//
|
|
44
|
-
|
|
44
|
+
// User provides pattern with explicit trailing *
|
|
45
|
+
// Strip it to get the prefix for Claude format
|
|
46
|
+
const prefix = rule.pattern.slice(0, -1);
|
|
47
|
+
// bash:* (all commands) → Bash (without parentheses)
|
|
48
|
+
// bash:git* → Bash(git:*)
|
|
49
|
+
if (prefix === "" || prefix.trim() === "") {
|
|
50
|
+
return ["Bash"];
|
|
51
|
+
}
|
|
52
|
+
return [`Bash(${prefix}:*)`];
|
|
45
53
|
}
|
|
46
54
|
case "path": {
|
|
47
55
|
// Claude Code uses "Tool(path/**)" for path patterns
|
|
56
|
+
// write:pattern maps to both Write(pattern) and Edit(pattern)
|
|
57
|
+
if (rule.tool === "write") {
|
|
58
|
+
return [`Write(${rule.pattern})`, `Edit(${rule.pattern})`];
|
|
59
|
+
}
|
|
48
60
|
const toolName = rule.tool.charAt(0).toUpperCase() + rule.tool.slice(1);
|
|
49
|
-
return `${toolName}(${rule.pattern})
|
|
61
|
+
return [`${toolName}(${rule.pattern})`];
|
|
50
62
|
}
|
|
51
63
|
}
|
|
52
64
|
}
|
|
@@ -96,9 +108,9 @@ function build(config, output) {
|
|
|
96
108
|
};
|
|
97
109
|
}
|
|
98
110
|
// Claude Code supports all permission types, so no warnings/errors needed
|
|
99
|
-
// All rules can be translated directly
|
|
100
|
-
const allowRules = permissions.allow.
|
|
101
|
-
const denyRules = permissions.deny.
|
|
111
|
+
// All rules can be translated directly (flatMap since translateRule returns arrays)
|
|
112
|
+
const allowRules = permissions.allow.flatMap((rule) => translateRule(rule));
|
|
113
|
+
const denyRules = permissions.deny.flatMap((rule) => translateRule(rule));
|
|
102
114
|
const settings = {
|
|
103
115
|
...existingSettings,
|
|
104
116
|
permissions: {
|
|
@@ -85,17 +85,19 @@ function readPermissions(configDirectory) {
|
|
|
85
85
|
const sandboxMode = config.sandbox_mode;
|
|
86
86
|
if (sandboxMode === "workspace-write" ||
|
|
87
87
|
sandboxMode === "danger-full-access") {
|
|
88
|
-
allowRules.push({ type: "tool", name: "write" }
|
|
88
|
+
allowRules.push({ type: "tool", name: "write" });
|
|
89
89
|
}
|
|
90
90
|
// Read is always allowed in Codex sandbox
|
|
91
91
|
allowRules.push({ type: "tool", name: "read" });
|
|
92
92
|
// Parse bash patterns from rules files
|
|
93
|
+
// Append * to indicate prefix matching (required by axconfig's canonical format)
|
|
93
94
|
for (const rule of rules) {
|
|
95
|
+
const pattern = rule.pattern === "" ? "*" : `${rule.pattern}*`;
|
|
94
96
|
if (rule.decision === "allow") {
|
|
95
|
-
allowRules.push({ type: "bash", pattern
|
|
97
|
+
allowRules.push({ type: "bash", pattern });
|
|
96
98
|
}
|
|
97
99
|
else {
|
|
98
|
-
denyRules.push({ type: "bash", pattern
|
|
100
|
+
denyRules.push({ type: "bash", pattern });
|
|
99
101
|
}
|
|
100
102
|
}
|
|
101
103
|
const permConfig = {
|
package/dist/agents/codex.js
CHANGED
|
@@ -26,11 +26,11 @@ const CAPABILITIES = {
|
|
|
26
26
|
/**
|
|
27
27
|
* Infer sandbox mode from permissions.
|
|
28
28
|
*
|
|
29
|
-
* - If write
|
|
29
|
+
* - If write is allowed → workspace-write
|
|
30
30
|
* - Otherwise → read-only
|
|
31
31
|
*/
|
|
32
32
|
function inferSandboxMode(permissions) {
|
|
33
|
-
const allowsWrite = permissions.allow.some((r) => r.type === "tool" &&
|
|
33
|
+
const allowsWrite = permissions.allow.some((r) => r.type === "tool" && r.name === "write");
|
|
34
34
|
return allowsWrite ? "workspace-write" : "read-only";
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
@@ -98,9 +98,8 @@ function build(config, output) {
|
|
|
98
98
|
});
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
-
// Tool permissions other than read/write
|
|
102
|
-
const nonFileTools = permissions.allow.filter((r) => r.type === "tool" &&
|
|
103
|
-
!["read", "write", "edit", "bash"].includes(r.name));
|
|
101
|
+
// Tool permissions other than read/write - warn as Codex uses sandbox
|
|
102
|
+
const nonFileTools = permissions.allow.filter((r) => r.type === "tool" && !["read", "write", "bash"].includes(r.name));
|
|
104
103
|
for (const rule of nonFileTools) {
|
|
105
104
|
if (rule.type === "tool") {
|
|
106
105
|
warnings.push({
|
|
@@ -146,13 +145,17 @@ function build(config, output) {
|
|
|
146
145
|
// Generate execpolicy rules
|
|
147
146
|
const rules = ["# Generated by axconfig"];
|
|
148
147
|
if (permissions) {
|
|
149
|
-
// Collect bash patterns
|
|
148
|
+
// Collect bash patterns, stripping trailing * since Codex does prefix matching natively
|
|
149
|
+
// Filter out empty patterns (from bash:*) - Codex doesn't support empty prefix
|
|
150
|
+
// bash:* means "all commands" which is the default when no rules exist
|
|
150
151
|
const allowBash = permissions.allow
|
|
151
152
|
.filter((r) => r.type === "bash")
|
|
152
|
-
.map((r) => r.pattern)
|
|
153
|
+
.map((r) => r.pattern.slice(0, -1).trim())
|
|
154
|
+
.filter((p) => p !== "");
|
|
153
155
|
const denyBash = permissions.deny
|
|
154
156
|
.filter((r) => r.type === "bash")
|
|
155
|
-
.map((r) => r.pattern)
|
|
157
|
+
.map((r) => r.pattern.slice(0, -1).trim())
|
|
158
|
+
.filter((p) => p !== "");
|
|
156
159
|
// Generate allow rules
|
|
157
160
|
for (const pattern of allowBash) {
|
|
158
161
|
rules.push(generatePrefixRule(pattern, "allow"));
|
|
@@ -14,11 +14,11 @@ import { createJsonConfigOperations, readJsonConfig, } from "../read-write-json-
|
|
|
14
14
|
const REVERSE_TOOL_MAP = {
|
|
15
15
|
read_file: "read",
|
|
16
16
|
write_file: "write",
|
|
17
|
-
edit_file: "
|
|
17
|
+
edit_file: "write", // Gemini's edit_file maps to canonical "write"
|
|
18
18
|
run_shell_command: "bash",
|
|
19
19
|
glob: "glob",
|
|
20
20
|
grep: "grep",
|
|
21
|
-
web_fetch: "
|
|
21
|
+
web_fetch: "webfetch",
|
|
22
22
|
};
|
|
23
23
|
/**
|
|
24
24
|
* Get the settings.json path for a config directory.
|
|
@@ -73,12 +73,14 @@ function readPermissions(configDirectory) {
|
|
|
73
73
|
? rule.toolName
|
|
74
74
|
: [rule.toolName];
|
|
75
75
|
// Check for bash command patterns
|
|
76
|
+
// Append * to indicate prefix matching (required by axconfig's canonical format)
|
|
76
77
|
if (rule.commandPrefix) {
|
|
77
78
|
const prefixes = Array.isArray(rule.commandPrefix)
|
|
78
79
|
? rule.commandPrefix
|
|
79
80
|
: [rule.commandPrefix];
|
|
80
81
|
for (const prefix of prefixes) {
|
|
81
|
-
|
|
82
|
+
const pattern = prefix === "" ? "*" : `${prefix}*`;
|
|
83
|
+
targetList.push({ type: "bash", pattern });
|
|
82
84
|
}
|
|
83
85
|
continue;
|
|
84
86
|
}
|
|
@@ -109,7 +111,7 @@ function readModel(configDirectory) {
|
|
|
109
111
|
try {
|
|
110
112
|
const settings = readSettings(configDirectory);
|
|
111
113
|
const modelObject = settings.model;
|
|
112
|
-
if (
|
|
114
|
+
if (modelObject?.name === undefined) {
|
|
113
115
|
return { ok: true, value: undefined };
|
|
114
116
|
}
|
|
115
117
|
if (typeof modelObject.name !== "string") {
|
package/dist/agents/gemini.js
CHANGED
|
@@ -18,12 +18,11 @@ import { readExistingSettings } from "./gemini-settings.js";
|
|
|
18
18
|
/** Gemini CLI tool name mapping */
|
|
19
19
|
const TOOL_MAP = {
|
|
20
20
|
read: "read_file",
|
|
21
|
-
write: "write_file",
|
|
22
|
-
edit: "edit_file",
|
|
21
|
+
write: ["write_file", "edit_file"], // Canonical "write" maps to both Gemini's write_file and edit_file
|
|
23
22
|
bash: "run_shell_command",
|
|
24
23
|
glob: "glob",
|
|
25
24
|
grep: "grep",
|
|
26
|
-
|
|
25
|
+
webfetch: "web_fetch",
|
|
27
26
|
};
|
|
28
27
|
/** Gemini CLI capabilities */
|
|
29
28
|
const CAPABILITIES = {
|
|
@@ -106,29 +105,51 @@ function build(config, output) {
|
|
|
106
105
|
const rules = [];
|
|
107
106
|
if (permissions) {
|
|
108
107
|
// Collect tool permissions (excluding path rules which were warned about)
|
|
108
|
+
// Use flatMap since canonical "write" maps to both write_file and edit_file
|
|
109
109
|
const allowTools = permissions.allow
|
|
110
110
|
.filter((r) => r.type === "tool")
|
|
111
|
-
.
|
|
111
|
+
.flatMap((r) => {
|
|
112
|
+
const mapped = TOOL_MAP[r.name];
|
|
113
|
+
return Array.isArray(mapped) ? mapped : [mapped];
|
|
114
|
+
});
|
|
112
115
|
const denyTools = permissions.deny
|
|
113
116
|
.filter((r) => r.type === "tool")
|
|
114
|
-
.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
117
|
+
.flatMap((r) => {
|
|
118
|
+
const mapped = TOOL_MAP[r.name];
|
|
119
|
+
return Array.isArray(mapped) ? mapped : [mapped];
|
|
120
|
+
});
|
|
121
|
+
// Collect bash patterns, stripping trailing * since Gemini does prefix matching natively
|
|
122
|
+
// bash:* (empty prefix after strip) → allow run_shell_command tool entirely
|
|
123
|
+
// bash:git* → commandPrefix = "git"
|
|
124
|
+
const allowBashPatterns = permissions.allow.filter((r) => r.type === "bash");
|
|
125
|
+
const denyBashPatterns = permissions.deny.filter((r) => r.type === "bash");
|
|
126
|
+
// Separate bash:* (all commands) from specific patterns
|
|
127
|
+
const allowAllBash = allowBashPatterns.some((r) => r.pattern.slice(0, -1).trim() === "");
|
|
128
|
+
const denyAllBash = denyBashPatterns.some((r) => r.pattern.slice(0, -1).trim() === "");
|
|
129
|
+
const allowBash = allowBashPatterns
|
|
130
|
+
.map((r) => r.pattern.slice(0, -1).trim())
|
|
131
|
+
.filter((p) => p !== "");
|
|
132
|
+
const denyBash = denyBashPatterns
|
|
133
|
+
.map((r) => r.pattern.slice(0, -1).trim())
|
|
134
|
+
.filter((p) => p !== "");
|
|
122
135
|
// Generate allow rules (high priority)
|
|
123
|
-
if
|
|
124
|
-
|
|
136
|
+
// Include run_shell_command if bash:* is in allow list
|
|
137
|
+
const effectiveAllowTools = allowAllBash
|
|
138
|
+
? [...allowTools, "run_shell_command"]
|
|
139
|
+
: allowTools;
|
|
140
|
+
if (effectiveAllowTools.length > 0) {
|
|
141
|
+
rules.push(generateToolRule(effectiveAllowTools, "allow", 999));
|
|
125
142
|
}
|
|
126
143
|
if (allowBash.length > 0) {
|
|
127
144
|
rules.push(generateBashRule(allowBash, "allow", 998));
|
|
128
145
|
}
|
|
129
146
|
// Generate deny rules (medium priority)
|
|
130
|
-
if
|
|
131
|
-
|
|
147
|
+
// Include run_shell_command if bash:* is in deny list
|
|
148
|
+
const effectiveDenyTools = denyAllBash
|
|
149
|
+
? [...denyTools, "run_shell_command"]
|
|
150
|
+
: denyTools;
|
|
151
|
+
if (effectiveDenyTools.length > 0) {
|
|
152
|
+
rules.push(generateToolRule(effectiveDenyTools, "deny", 500));
|
|
132
153
|
}
|
|
133
154
|
if (denyBash.length > 0) {
|
|
134
155
|
rules.push(generateBashRule(denyBash, "deny", 499));
|
|
@@ -32,9 +32,9 @@ export declare const BASE_PERMISSION_OVERRIDES: Record<string, OpenCodePermissio
|
|
|
32
32
|
/**
|
|
33
33
|
* Collect path patterns from permission rules, grouped by OpenCode permission name.
|
|
34
34
|
*
|
|
35
|
-
* Note: Only read/write
|
|
35
|
+
* Note: Only read/write rules can have path patterns per the PathPatternRule type.
|
|
36
36
|
* The TOOL_TO_OPENCODE mapping includes glob/grep for tool-level permissions, but these
|
|
37
|
-
* will never appear here since PathRestrictedTool is limited to read/write
|
|
37
|
+
* will never appear here since PathRestrictedTool is limited to read/write.
|
|
38
38
|
*/
|
|
39
39
|
export declare function collectPathPatterns(rules: PermissionRule[]): Map<string, string[]>;
|
|
40
40
|
/**
|
|
@@ -8,11 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
const TOOL_TO_OPENCODE = {
|
|
10
10
|
read: "read",
|
|
11
|
-
|
|
12
|
-
write: "edit", // OpenCode uses single "edit" for both
|
|
11
|
+
write: "edit", // OpenCode uses single "edit" for write operations
|
|
13
12
|
glob: "glob",
|
|
14
13
|
grep: "grep",
|
|
15
|
-
|
|
14
|
+
webfetch: "webfetch",
|
|
16
15
|
bash: "bash",
|
|
17
16
|
};
|
|
18
17
|
/**
|
|
@@ -42,9 +41,9 @@ export const BASE_PERMISSION_OVERRIDES = {
|
|
|
42
41
|
/**
|
|
43
42
|
* Collect path patterns from permission rules, grouped by OpenCode permission name.
|
|
44
43
|
*
|
|
45
|
-
* Note: Only read/write
|
|
44
|
+
* Note: Only read/write rules can have path patterns per the PathPatternRule type.
|
|
46
45
|
* The TOOL_TO_OPENCODE mapping includes glob/grep for tool-level permissions, but these
|
|
47
|
-
* will never appear here since PathRestrictedTool is limited to read/write
|
|
46
|
+
* will never appear here since PathRestrictedTool is limited to read/write.
|
|
48
47
|
*/
|
|
49
48
|
export function collectPathPatterns(rules) {
|
|
50
49
|
const patterns = new Map();
|
|
@@ -76,29 +75,6 @@ export function collectBashPatterns(rules) {
|
|
|
76
75
|
.filter((r) => r.type === "bash")
|
|
77
76
|
.map((r) => r.pattern);
|
|
78
77
|
}
|
|
79
|
-
/**
|
|
80
|
-
* Normalize a bash pattern for OpenCode by adding a trailing wildcard if needed.
|
|
81
|
-
*
|
|
82
|
-
* OpenCode uses an "arity" system to extract command prefixes for permission
|
|
83
|
-
* checking. For example, `gh` has arity 3, so `gh api repos/foo/bar` extracts
|
|
84
|
-
* the prefix `gh api repos/foo/bar` (first 3 tokens). A pattern `gh api` won't
|
|
85
|
-
* match because it's missing the third token.
|
|
86
|
-
*
|
|
87
|
-
* By appending `*` to patterns that don't already have wildcards, we ensure
|
|
88
|
-
* patterns like `gh api` become `gh api*` which matches `gh api repos/...`.
|
|
89
|
-
*
|
|
90
|
-
* @example
|
|
91
|
-
* normalizeBashPattern("gh api") // "gh api*"
|
|
92
|
-
* normalizeBashPattern("git *") // "git *" (already has wildcard)
|
|
93
|
-
* normalizeBashPattern("cat") // "cat*"
|
|
94
|
-
*/
|
|
95
|
-
function normalizeBashPattern(pattern) {
|
|
96
|
-
// Don't add another * if pattern already ends with one
|
|
97
|
-
if (pattern.endsWith("*")) {
|
|
98
|
-
return pattern;
|
|
99
|
-
}
|
|
100
|
-
return `${pattern}*`;
|
|
101
|
-
}
|
|
102
78
|
/**
|
|
103
79
|
* Build bash permission config from patterns and tool permissions.
|
|
104
80
|
*
|
|
@@ -124,10 +100,10 @@ export function buildBashPermission(allowPatterns, denyPatterns, bashAllowed, ba
|
|
|
124
100
|
"*": bashAllowed ? "allow" : "deny",
|
|
125
101
|
};
|
|
126
102
|
for (const pattern of allowPatterns) {
|
|
127
|
-
bashConfig[
|
|
103
|
+
bashConfig[pattern] = "allow";
|
|
128
104
|
}
|
|
129
105
|
for (const pattern of denyPatterns) {
|
|
130
|
-
bashConfig[
|
|
106
|
+
bashConfig[pattern] = "deny";
|
|
131
107
|
}
|
|
132
108
|
return bashConfig;
|
|
133
109
|
}
|
|
@@ -143,14 +119,8 @@ export function buildBashPermission(allowPatterns, denyPatterns, bashAllowed, ba
|
|
|
143
119
|
export function buildToolPermission(canonicalName, opencodeName, allowedTools, deniedTools, allowPathPatterns, denyPathPatterns) {
|
|
144
120
|
const hasAllowPatterns = allowPathPatterns.has(opencodeName);
|
|
145
121
|
const hasDenyPatterns = denyPathPatterns.has(opencodeName);
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
const effectiveAllowed = isEditOrWrite
|
|
149
|
-
? allowedTools.has("edit") || allowedTools.has("write")
|
|
150
|
-
: allowedTools.has(canonicalName);
|
|
151
|
-
const effectiveDenied = isEditOrWrite
|
|
152
|
-
? deniedTools.has("edit") || deniedTools.has("write")
|
|
153
|
-
: deniedTools.has(canonicalName);
|
|
122
|
+
const effectiveAllowed = allowedTools.has(canonicalName);
|
|
123
|
+
const effectiveDenied = deniedTools.has(canonicalName);
|
|
154
124
|
if (effectiveDenied && !hasDenyPatterns && !hasAllowPatterns) {
|
|
155
125
|
return "deny";
|
|
156
126
|
}
|
|
@@ -23,10 +23,10 @@ import { createJsonConfigOperations, readJsonConfig, } from "../read-write-json-
|
|
|
23
23
|
*/
|
|
24
24
|
const OPENCODE_TO_TOOL = {
|
|
25
25
|
read: "read",
|
|
26
|
-
edit:
|
|
26
|
+
edit: "write", // OpenCode's "edit" maps to canonical "write"
|
|
27
27
|
glob: "glob",
|
|
28
28
|
grep: "grep",
|
|
29
|
-
webfetch: "
|
|
29
|
+
webfetch: "webfetch",
|
|
30
30
|
bash: "bash",
|
|
31
31
|
};
|
|
32
32
|
/**
|
|
@@ -55,18 +55,13 @@ function isAllowAction(action) {
|
|
|
55
55
|
* Parse a permission value (simple or object-based) and add rules to the arrays.
|
|
56
56
|
*/
|
|
57
57
|
function parsePermissionValue(opencodeName, value, allowRules, denyRules) {
|
|
58
|
-
const
|
|
59
|
-
if (!
|
|
58
|
+
const canonicalTool = OPENCODE_TO_TOOL[opencodeName];
|
|
59
|
+
if (!canonicalTool)
|
|
60
60
|
return; // Unknown permission, skip
|
|
61
|
-
const tools = Array.isArray(canonicalTools)
|
|
62
|
-
? canonicalTools
|
|
63
|
-
: [canonicalTools];
|
|
64
61
|
if (typeof value === "string") {
|
|
65
62
|
// Simple value: "allow", "deny", or "ask"
|
|
66
63
|
const rules = isAllowAction(value) ? allowRules : denyRules;
|
|
67
|
-
|
|
68
|
-
rules.push({ type: "tool", name: tool });
|
|
69
|
-
}
|
|
64
|
+
rules.push({ type: "tool", name: canonicalTool });
|
|
70
65
|
}
|
|
71
66
|
else if (typeof value === "object") {
|
|
72
67
|
// Object with patterns
|
|
@@ -74,9 +69,7 @@ function parsePermissionValue(opencodeName, value, allowRules, denyRules) {
|
|
|
74
69
|
const rules = isAllowAction(action) ? allowRules : denyRules;
|
|
75
70
|
if (pattern === "*") {
|
|
76
71
|
// Catch-all represents tool-level permission
|
|
77
|
-
|
|
78
|
-
rules.push({ type: "tool", name: tool });
|
|
79
|
-
}
|
|
72
|
+
rules.push({ type: "tool", name: canonicalTool });
|
|
80
73
|
continue;
|
|
81
74
|
}
|
|
82
75
|
if (opencodeName === "bash") {
|
|
@@ -84,13 +77,11 @@ function parsePermissionValue(opencodeName, value, allowRules, denyRules) {
|
|
|
84
77
|
rules.push({ type: "bash", pattern });
|
|
85
78
|
}
|
|
86
79
|
else {
|
|
87
|
-
// Only read/write
|
|
80
|
+
// Only read/write support path patterns in axconfig's type system.
|
|
88
81
|
// OpenCode's glob/grep patterns match against glob patterns or regexes,
|
|
89
82
|
// not file paths, so they're intentionally excluded here.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
rules.push({ type: "path", tool, pattern });
|
|
93
|
-
}
|
|
83
|
+
if (canonicalTool === "read" || canonicalTool === "write") {
|
|
84
|
+
rules.push({ type: "path", tool: canonicalTool, pattern });
|
|
94
85
|
}
|
|
95
86
|
}
|
|
96
87
|
}
|
package/dist/agents/opencode.js
CHANGED
|
@@ -40,10 +40,10 @@ function buildPermissionConfig(permissions) {
|
|
|
40
40
|
// Set permissions for standard tools
|
|
41
41
|
const tools = [
|
|
42
42
|
{ canonical: "read", opencode: "read" },
|
|
43
|
-
{ canonical: "
|
|
43
|
+
{ canonical: "write", opencode: "edit" }, // OpenCode uses "edit" for write operations
|
|
44
44
|
{ canonical: "glob", opencode: "glob" },
|
|
45
45
|
{ canonical: "grep", opencode: "grep" },
|
|
46
|
-
{ canonical: "
|
|
46
|
+
{ canonical: "webfetch", opencode: "webfetch" },
|
|
47
47
|
];
|
|
48
48
|
for (const { canonical, opencode } of tools) {
|
|
49
49
|
permissionConfig[opencode] = buildToolPermission(canonical, opencode, allowedTools, deniedTools, allowPathPatterns, denyPathPatterns);
|
|
@@ -85,7 +85,7 @@ function build(config, output) {
|
|
|
85
85
|
};
|
|
86
86
|
atomicWriteFileSync(configPath, JSON.stringify(openCodeConfig, undefined, 2));
|
|
87
87
|
// Build environment variables:
|
|
88
|
-
// - OPENCODE_CONFIG_DIR:
|
|
88
|
+
// - OPENCODE_CONFIG_DIR: axkit convention for config directory
|
|
89
89
|
// - XDG_DATA_HOME/XDG_CONFIG_HOME: OpenCode uses these for auth and other XDG-based paths
|
|
90
90
|
const runtimeEnvironment = buildAgentRuntimeEnvironment("opencode", output);
|
|
91
91
|
return {
|
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Parses --allow and --deny CLI arguments into PermissionConfig.
|
|
5
5
|
*
|
|
6
|
+
* Bash patterns require an explicit trailing wildcard (*) to indicate
|
|
7
|
+
* prefix matching. Patterns without wildcards will throw an error.
|
|
8
|
+
*
|
|
6
9
|
* @example
|
|
7
10
|
* parsePermissions(
|
|
8
11
|
* ["read,glob,bash:git *"],
|
|
9
|
-
* ["bash:rm
|
|
12
|
+
* ["bash:rm*"]
|
|
10
13
|
* )
|
|
11
14
|
* // Returns:
|
|
12
15
|
* // {
|
|
@@ -16,7 +19,7 @@
|
|
|
16
19
|
* // { type: "bash", pattern: "git *" }
|
|
17
20
|
* // ],
|
|
18
21
|
* // deny: [
|
|
19
|
-
* // { type: "bash", pattern: "rm
|
|
22
|
+
* // { type: "bash", pattern: "rm*" }
|
|
20
23
|
* // ]
|
|
21
24
|
* // }
|
|
22
25
|
*/
|
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Parses --allow and --deny CLI arguments into PermissionConfig.
|
|
5
5
|
*
|
|
6
|
+
* Bash patterns require an explicit trailing wildcard (*) to indicate
|
|
7
|
+
* prefix matching. Patterns without wildcards will throw an error.
|
|
8
|
+
*
|
|
6
9
|
* @example
|
|
7
10
|
* parsePermissions(
|
|
8
11
|
* ["read,glob,bash:git *"],
|
|
9
|
-
* ["bash:rm
|
|
12
|
+
* ["bash:rm*"]
|
|
10
13
|
* )
|
|
11
14
|
* // Returns:
|
|
12
15
|
* // {
|
|
@@ -16,7 +19,7 @@
|
|
|
16
19
|
* // { type: "bash", pattern: "git *" }
|
|
17
20
|
* // ],
|
|
18
21
|
* // deny: [
|
|
19
|
-
* // { type: "bash", pattern: "rm
|
|
22
|
+
* // { type: "bash", pattern: "rm*" }
|
|
20
23
|
* // ]
|
|
21
24
|
* // }
|
|
22
25
|
*/
|
|
@@ -24,14 +27,13 @@
|
|
|
24
27
|
const CANONICAL_TOOLS = new Set([
|
|
25
28
|
"read",
|
|
26
29
|
"write",
|
|
27
|
-
"edit",
|
|
28
30
|
"bash",
|
|
29
31
|
"glob",
|
|
30
32
|
"grep",
|
|
31
|
-
"
|
|
33
|
+
"webfetch",
|
|
32
34
|
]);
|
|
33
35
|
/** Tools that support path restrictions */
|
|
34
|
-
const PATH_TOOLS = new Set(["read", "write"
|
|
36
|
+
const PATH_TOOLS = new Set(["read", "write"]);
|
|
35
37
|
/**
|
|
36
38
|
* Parse a single permission rule string.
|
|
37
39
|
*
|
|
@@ -51,6 +53,11 @@ function parseRule(rule) {
|
|
|
51
53
|
if (pattern === "") {
|
|
52
54
|
throw new Error('Invalid bash pattern: "bash:" requires a command pattern');
|
|
53
55
|
}
|
|
56
|
+
if (!pattern.endsWith("*")) {
|
|
57
|
+
throw new Error(`Bash pattern "${pattern}" requires a trailing wildcard.\n` +
|
|
58
|
+
`Use "bash:${pattern}*" to match commands starting with "${pattern}".\n` +
|
|
59
|
+
`All bash patterns are prefix matches.`);
|
|
60
|
+
}
|
|
54
61
|
return { type: "bash", pattern };
|
|
55
62
|
}
|
|
56
63
|
// Check for path restriction: "read:src/**"
|
package/dist/types.d.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { AgentCli } from "axshared";
|
|
7
7
|
/** Canonical tool names used in axrun permissions */
|
|
8
|
-
type CanonicalTool = "read" | "write" | "
|
|
8
|
+
type CanonicalTool = "read" | "write" | "bash" | "glob" | "grep" | "webfetch";
|
|
9
9
|
/** Tools that support path restrictions */
|
|
10
|
-
type PathRestrictedTool = "read" | "write"
|
|
10
|
+
type PathRestrictedTool = "read" | "write";
|
|
11
11
|
/** Permission rule for a tool (e.g., "read", "bash") */
|
|
12
12
|
interface ToolPermissionRule {
|
|
13
13
|
type: "tool";
|
package/package.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"name": "axconfig",
|
|
3
3
|
"author": "Łukasz Jerciński",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "3.
|
|
6
|
-
"description": "Unified configuration management for AI coding agents - common API for permissions, settings, and config across Claude Code, Codex, Gemini CLI, and OpenCode",
|
|
5
|
+
"version": "3.6.1",
|
|
6
|
+
"description": "Unified configuration management for AI coding agents - common API for permissions, settings, and config across Claude Code, Codex, Gemini CLI, GitHub Copilot CLI, and OpenCode",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/Jercik/axconfig.git"
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"configuration",
|
|
56
56
|
"claude-code",
|
|
57
57
|
"codex",
|
|
58
|
+
"copilot-cli",
|
|
58
59
|
"gemini",
|
|
59
60
|
"opencode",
|
|
60
61
|
"llm",
|
|
@@ -68,22 +69,22 @@
|
|
|
68
69
|
"dependencies": {
|
|
69
70
|
"@commander-js/extra-typings": "^14.0.0",
|
|
70
71
|
"@iarna/toml": "^2.2.5",
|
|
71
|
-
"axshared": "^1.
|
|
72
|
+
"axshared": "^1.9.0",
|
|
72
73
|
"commander": "^14.0.2"
|
|
73
74
|
},
|
|
74
75
|
"devDependencies": {
|
|
75
76
|
"@total-typescript/ts-reset": "^0.6.1",
|
|
76
77
|
"@types/iarna__toml": "^2.0.5",
|
|
77
|
-
"@types/node": "^25.0.
|
|
78
|
-
"@vitest/coverage-v8": "^4.0.
|
|
78
|
+
"@types/node": "^25.0.6",
|
|
79
|
+
"@vitest/coverage-v8": "^4.0.17",
|
|
79
80
|
"eslint": "^9.39.2",
|
|
80
|
-
"eslint-config-
|
|
81
|
+
"eslint-config-axkit": "^1.1.0",
|
|
81
82
|
"fta-check": "^1.5.1",
|
|
82
83
|
"fta-cli": "^3.0.0",
|
|
83
|
-
"knip": "^5.
|
|
84
|
+
"knip": "^5.80.2",
|
|
84
85
|
"prettier": "3.7.4",
|
|
85
86
|
"semantic-release": "^25.0.2",
|
|
86
87
|
"typescript": "^5.9.3",
|
|
87
|
-
"vitest": "^4.0.
|
|
88
|
+
"vitest": "^4.0.17"
|
|
88
89
|
}
|
|
89
90
|
}
|