axconfig 3.5.0 → 3.5.2

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.
@@ -47,10 +47,25 @@ export declare function collectTools(rules: PermissionRule[]): Set<CanonicalTool
47
47
  export declare function collectBashPatterns(rules: PermissionRule[]): string[];
48
48
  /**
49
49
  * Build bash permission config from patterns and tool permissions.
50
+ *
51
+ * IMPORTANT: The wildcard "*" rule must be added FIRST in the config object.
52
+ * OpenCode's `disabled()` function uses `findLast()` to find any rule matching
53
+ * the permission name, then checks if that rule has `pattern === "*"` and
54
+ * `action === "deny"`. If the "*" rule is last, bash gets disabled entirely
55
+ * even when specific patterns are allowed. By adding "*" first, `findLast()`
56
+ * returns a specific pattern rule instead, keeping bash available to the model.
57
+ *
58
+ * At execution time, `evaluate()` correctly handles rule precedence because it
59
+ * uses `findLast()` with BOTH permission AND pattern matching - so specific
60
+ * patterns still take priority over the wildcard when the command matches.
50
61
  */
51
62
  export declare function buildBashPermission(allowPatterns: string[], denyPatterns: string[], bashAllowed: boolean, bashDenied: boolean): OpenCodePermission | Record<string, OpenCodePermission>;
52
63
  /**
53
64
  * Build tool permission config with optional path patterns.
65
+ *
66
+ * IMPORTANT: Like buildBashPermission, the wildcard "*" rule must be added
67
+ * FIRST to prevent OpenCode's disabled() from hiding the tool entirely.
68
+ * See buildBashPermission comment for detailed explanation.
54
69
  */
55
70
  export declare function buildToolPermission(canonicalName: CanonicalTool, opencodeName: string, allowedTools: Set<CanonicalTool>, deniedTools: Set<CanonicalTool>, allowPathPatterns: Map<string, string[]>, denyPathPatterns: Map<string, string[]>): OpenCodePermission | Record<string, OpenCodePermission>;
56
71
  /**
@@ -76,28 +76,69 @@ export function collectBashPatterns(rules) {
76
76
  .filter((r) => r.type === "bash")
77
77
  .map((r) => r.pattern);
78
78
  }
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
+ }
79
102
  /**
80
103
  * Build bash permission config from patterns and tool permissions.
104
+ *
105
+ * IMPORTANT: The wildcard "*" rule must be added FIRST in the config object.
106
+ * OpenCode's `disabled()` function uses `findLast()` to find any rule matching
107
+ * the permission name, then checks if that rule has `pattern === "*"` and
108
+ * `action === "deny"`. If the "*" rule is last, bash gets disabled entirely
109
+ * even when specific patterns are allowed. By adding "*" first, `findLast()`
110
+ * returns a specific pattern rule instead, keeping bash available to the model.
111
+ *
112
+ * At execution time, `evaluate()` correctly handles rule precedence because it
113
+ * uses `findLast()` with BOTH permission AND pattern matching - so specific
114
+ * patterns still take priority over the wildcard when the command matches.
81
115
  */
82
116
  export function buildBashPermission(allowPatterns, denyPatterns, bashAllowed, bashDenied) {
83
117
  if (bashDenied && allowPatterns.length === 0) {
84
118
  return "deny";
85
119
  }
86
120
  if (allowPatterns.length > 0 || denyPatterns.length > 0) {
87
- const bashConfig = {};
121
+ // Add wildcard FIRST so specific patterns come after in ruleset order.
122
+ // This prevents OpenCode's disabled() from hiding bash from the model.
123
+ const bashConfig = {
124
+ "*": bashAllowed ? "allow" : "deny",
125
+ };
88
126
  for (const pattern of allowPatterns) {
89
- bashConfig[pattern] = "allow";
127
+ bashConfig[normalizeBashPattern(pattern)] = "allow";
90
128
  }
91
129
  for (const pattern of denyPatterns) {
92
- bashConfig[pattern] = "deny";
130
+ bashConfig[normalizeBashPattern(pattern)] = "deny";
93
131
  }
94
- bashConfig["*"] = bashAllowed ? "allow" : "deny";
95
132
  return bashConfig;
96
133
  }
97
134
  return bashAllowed ? "allow" : "deny";
98
135
  }
99
136
  /**
100
137
  * Build tool permission config with optional path patterns.
138
+ *
139
+ * IMPORTANT: Like buildBashPermission, the wildcard "*" rule must be added
140
+ * FIRST to prevent OpenCode's disabled() from hiding the tool entirely.
141
+ * See buildBashPermission comment for detailed explanation.
101
142
  */
102
143
  export function buildToolPermission(canonicalName, opencodeName, allowedTools, deniedTools, allowPathPatterns, denyPathPatterns) {
103
144
  const hasAllowPatterns = allowPathPatterns.has(opencodeName);
@@ -114,14 +155,17 @@ export function buildToolPermission(canonicalName, opencodeName, allowedTools, d
114
155
  return "deny";
115
156
  }
116
157
  if (hasAllowPatterns || hasDenyPatterns) {
117
- const patternConfig = {};
158
+ // Add wildcard FIRST so specific patterns come after in ruleset order.
159
+ // This prevents OpenCode's disabled() from hiding the tool from the model.
160
+ const patternConfig = {
161
+ "*": effectiveAllowed ? "allow" : "deny",
162
+ };
118
163
  for (const pattern of allowPathPatterns.get(opencodeName) ?? []) {
119
164
  patternConfig[pattern] = "allow";
120
165
  }
121
166
  for (const pattern of denyPathPatterns.get(opencodeName) ?? []) {
122
167
  patternConfig[pattern] = "deny";
123
168
  }
124
- patternConfig["*"] = effectiveAllowed ? "allow" : "deny";
125
169
  return patternConfig;
126
170
  }
127
171
  return effectiveAllowed ? "allow" : "deny";
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "axconfig",
3
3
  "author": "Łukasz Jerciński",
4
4
  "license": "MIT",
5
- "version": "3.5.0",
5
+ "version": "3.5.2",
6
6
  "description": "Unified configuration management for AI coding agents - common API for permissions, settings, and config across Claude Code, Codex, Gemini CLI, and OpenCode",
7
7
  "repository": {
8
8
  "type": "git",