@doingdev/opencode-claude-manager-plugin 0.1.52 → 0.1.53

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 CHANGED
@@ -81,7 +81,7 @@ If you are testing locally, point OpenCode at the local package or plugin file u
81
81
 
82
82
  - `approval_policy` — view the current tool approval policy.
83
83
  - `approval_decisions` — view recent tool approval decisions.
84
- - `approval_update` — add/remove rules, change default action, or enable/disable approval.
84
+ - `approval_update` — add/remove rules, enable/disable approval, or clear decision history. Policy uses a **deny-list**: tools not matching any rule are **allowed**; use explicit **deny** rules to block. `defaultAction` is always `allow` (cannot be set to deny).
85
85
 
86
86
  ## Agent hierarchy
87
87
 
@@ -3,6 +3,13 @@ import path from 'node:path';
3
3
  import { isFileNotFoundError, writeJsonAtomically } from '../util/fs-helpers.js';
4
4
  const DEFAULT_MAX_DECISIONS = 500;
5
5
  const INPUT_PREVIEW_MAX = 300;
6
+ function normalizePolicy(policy) {
7
+ return {
8
+ ...policy,
9
+ rules: [...policy.rules],
10
+ defaultAction: 'allow',
11
+ };
12
+ }
6
13
  function getDefaultRules() {
7
14
  return [
8
15
  // Safe read-only tools
@@ -125,12 +132,12 @@ export class ToolApprovalManager {
125
132
  maxDecisions;
126
133
  persistPath;
127
134
  constructor(policy, maxDecisions, persistPath) {
128
- this.policy = {
135
+ this.policy = normalizePolicy({
129
136
  rules: policy?.rules ?? getDefaultRules(),
130
- defaultAction: policy?.defaultAction ?? 'allow',
137
+ defaultAction: 'allow',
131
138
  defaultDenyMessage: policy?.defaultDenyMessage ?? 'Tool call denied by approval policy.',
132
139
  enabled: policy?.enabled ?? true,
133
- };
140
+ });
134
141
  this.maxDecisions = maxDecisions ?? DEFAULT_MAX_DECISIONS;
135
142
  this.persistPath = persistPath ?? null;
136
143
  }
@@ -141,12 +148,12 @@ export class ToolApprovalManager {
141
148
  try {
142
149
  const content = await fs.readFile(this.persistPath, 'utf8');
143
150
  const loaded = JSON.parse(content);
144
- this.policy = {
151
+ this.policy = normalizePolicy({
145
152
  rules: loaded.rules ?? getDefaultRules(),
146
- defaultAction: loaded.defaultAction ?? 'allow',
153
+ defaultAction: 'allow',
147
154
  defaultDenyMessage: loaded.defaultDenyMessage ?? 'Tool call denied by approval policy.',
148
155
  enabled: loaded.enabled ?? true,
149
- };
156
+ });
150
157
  }
151
158
  catch (error) {
152
159
  if (!isFileNotFoundError(error)) {
@@ -167,7 +174,7 @@ export class ToolApprovalManager {
167
174
  }
168
175
  const inputJson = safeJsonStringify(input);
169
176
  const matchedRule = this.findMatchingRule(toolName, inputJson);
170
- const action = matchedRule?.action ?? this.policy.defaultAction;
177
+ const action = matchedRule?.action ?? 'allow';
171
178
  const denyMessage = action === 'deny'
172
179
  ? (matchedRule?.denyMessage ?? this.policy.defaultDenyMessage ?? 'Denied by policy.')
173
180
  : undefined;
@@ -201,7 +208,7 @@ export class ToolApprovalManager {
201
208
  return { ...this.policy, rules: [...this.policy.rules] };
202
209
  }
203
210
  async setPolicy(policy) {
204
- this.policy = { ...policy, rules: [...policy.rules] };
211
+ this.policy = normalizePolicy(policy);
205
212
  await this.persistPolicy();
206
213
  }
207
214
  async addRule(rule, position) {
@@ -223,7 +230,10 @@ export class ToolApprovalManager {
223
230
  return true;
224
231
  }
225
232
  async setDefaultAction(action) {
226
- this.policy.defaultAction = action;
233
+ if (action === 'deny') {
234
+ throw new Error('defaultAction cannot be deny; unmatched tools are always allowed. Add explicit deny rules instead.');
235
+ }
236
+ this.policy = normalizePolicy(this.policy);
227
237
  await this.persistPolicy();
228
238
  }
229
239
  async setEnabled(enabled) {
@@ -289,7 +289,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
289
289
  },
290
290
  }),
291
291
  approval_update: tool({
292
- description: 'Update the tool approval policy. Add or remove rules, change the default action, enable or disable approvals, or clear decision history.',
292
+ description: 'Update the tool approval policy. Add or remove rules, enable or disable approvals, or clear decision history. Unmatched tools are always allowed; block only with explicit deny rules (defaultAction cannot be set to deny).',
293
293
  args: {
294
294
  action: tool.schema.enum([
295
295
  'addRule',
@@ -336,6 +336,11 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
336
336
  if (!args.defaultAction) {
337
337
  return JSON.stringify({ error: 'setDefault requires defaultAction' });
338
338
  }
339
+ if (args.defaultAction === 'deny') {
340
+ return JSON.stringify({
341
+ error: 'defaultAction cannot be deny; unmatched tools are always allowed. Add explicit deny rules instead.',
342
+ });
343
+ }
339
344
  await services.approvalManager.setDefaultAction(args.defaultAction);
340
345
  }
341
346
  else if (args.action === 'setEnabled') {
@@ -3,6 +3,13 @@ import path from 'node:path';
3
3
  import { isFileNotFoundError, writeJsonAtomically } from '../util/fs-helpers.js';
4
4
  const DEFAULT_MAX_DECISIONS = 500;
5
5
  const INPUT_PREVIEW_MAX = 300;
6
+ function normalizePolicy(policy) {
7
+ return {
8
+ ...policy,
9
+ rules: [...policy.rules],
10
+ defaultAction: 'allow',
11
+ };
12
+ }
6
13
  function getDefaultRules() {
7
14
  return [
8
15
  // Safe read-only tools
@@ -125,12 +132,12 @@ export class ToolApprovalManager {
125
132
  maxDecisions;
126
133
  persistPath;
127
134
  constructor(policy, maxDecisions, persistPath) {
128
- this.policy = {
135
+ this.policy = normalizePolicy({
129
136
  rules: policy?.rules ?? getDefaultRules(),
130
- defaultAction: policy?.defaultAction ?? 'allow',
137
+ defaultAction: 'allow',
131
138
  defaultDenyMessage: policy?.defaultDenyMessage ?? 'Tool call denied by approval policy.',
132
139
  enabled: policy?.enabled ?? true,
133
- };
140
+ });
134
141
  this.maxDecisions = maxDecisions ?? DEFAULT_MAX_DECISIONS;
135
142
  this.persistPath = persistPath ?? null;
136
143
  }
@@ -141,12 +148,12 @@ export class ToolApprovalManager {
141
148
  try {
142
149
  const content = await fs.readFile(this.persistPath, 'utf8');
143
150
  const loaded = JSON.parse(content);
144
- this.policy = {
151
+ this.policy = normalizePolicy({
145
152
  rules: loaded.rules ?? getDefaultRules(),
146
- defaultAction: loaded.defaultAction ?? 'allow',
153
+ defaultAction: 'allow',
147
154
  defaultDenyMessage: loaded.defaultDenyMessage ?? 'Tool call denied by approval policy.',
148
155
  enabled: loaded.enabled ?? true,
149
- };
156
+ });
150
157
  }
151
158
  catch (error) {
152
159
  if (!isFileNotFoundError(error)) {
@@ -167,7 +174,7 @@ export class ToolApprovalManager {
167
174
  }
168
175
  const inputJson = safeJsonStringify(input);
169
176
  const matchedRule = this.findMatchingRule(toolName, inputJson);
170
- const action = matchedRule?.action ?? this.policy.defaultAction;
177
+ const action = matchedRule?.action ?? 'allow';
171
178
  const denyMessage = action === 'deny'
172
179
  ? (matchedRule?.denyMessage ?? this.policy.defaultDenyMessage ?? 'Denied by policy.')
173
180
  : undefined;
@@ -201,7 +208,7 @@ export class ToolApprovalManager {
201
208
  return { ...this.policy, rules: [...this.policy.rules] };
202
209
  }
203
210
  async setPolicy(policy) {
204
- this.policy = { ...policy, rules: [...policy.rules] };
211
+ this.policy = normalizePolicy(policy);
205
212
  await this.persistPolicy();
206
213
  }
207
214
  async addRule(rule, position) {
@@ -223,7 +230,10 @@ export class ToolApprovalManager {
223
230
  return true;
224
231
  }
225
232
  async setDefaultAction(action) {
226
- this.policy.defaultAction = action;
233
+ if (action === 'deny') {
234
+ throw new Error('defaultAction cannot be deny; unmatched tools are always allowed. Add explicit deny rules instead.');
235
+ }
236
+ this.policy = normalizePolicy(this.policy);
227
237
  await this.persistPolicy();
228
238
  }
229
239
  async setEnabled(enabled) {
@@ -289,7 +289,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
289
289
  },
290
290
  }),
291
291
  approval_update: tool({
292
- description: 'Update the tool approval policy. Add or remove rules, change the default action, enable or disable approvals, or clear decision history.',
292
+ description: 'Update the tool approval policy. Add or remove rules, enable or disable approvals, or clear decision history. Unmatched tools are always allowed; block only with explicit deny rules (defaultAction cannot be set to deny).',
293
293
  args: {
294
294
  action: tool.schema.enum([
295
295
  'addRule',
@@ -336,6 +336,11 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
336
336
  if (!args.defaultAction) {
337
337
  return JSON.stringify({ error: 'setDefault requires defaultAction' });
338
338
  }
339
+ if (args.defaultAction === 'deny') {
340
+ return JSON.stringify({
341
+ error: 'defaultAction cannot be deny; unmatched tools are always allowed. Add explicit deny rules instead.',
342
+ });
343
+ }
339
344
  await services.approvalManager.setDefaultAction(args.defaultAction);
340
345
  }
341
346
  else if (args.action === 'setEnabled') {
@@ -186,7 +186,11 @@ export interface ToolApprovalRule {
186
186
  }
187
187
  export interface ToolApprovalPolicy {
188
188
  rules: ToolApprovalRule[];
189
- defaultAction: 'allow' | 'deny';
189
+ /**
190
+ * Always `allow`: tools that do not match any rule are allowed.
191
+ * Blocking is done only with explicit `deny` rules (deny-list contract).
192
+ */
193
+ defaultAction: 'allow';
190
194
  defaultDenyMessage?: string;
191
195
  enabled: boolean;
192
196
  }
@@ -73,13 +73,12 @@ describe('ToolApprovalManager', () => {
73
73
  const result = manager.evaluate('Bash', { command: 'rm -rf /' });
74
74
  expect(result).toEqual({ behavior: 'allow' });
75
75
  });
76
- it('uses default action when no rule matches', () => {
76
+ it('allows tools when no rule matches (deny-list contract)', () => {
77
77
  const manager = new ToolApprovalManager({
78
78
  rules: [],
79
- defaultAction: 'deny',
80
79
  });
81
80
  const result = manager.evaluate('SomeUnknownTool', {});
82
- expect(result.behavior).toBe('deny');
81
+ expect(result.behavior).toBe('allow');
83
82
  });
84
83
  it('first matching rule wins', () => {
85
84
  const manager = new ToolApprovalManager({
@@ -175,24 +174,22 @@ describe('ToolApprovalManager', () => {
175
174
  const manager = new ToolApprovalManager();
176
175
  expect(await manager.removeRule('nonexistent')).toBe(false);
177
176
  });
178
- it('setPolicy replaces entire policy', async () => {
177
+ it('setPolicy replaces entire policy and normalizes defaultAction to allow', async () => {
179
178
  const manager = new ToolApprovalManager();
180
179
  await manager.setPolicy({
181
180
  rules: [{ id: 'only', toolPattern: '*', action: 'deny' }],
182
- defaultAction: 'deny',
181
+ defaultAction: 'allow',
183
182
  enabled: true,
184
183
  });
185
184
  expect(manager.getPolicy().rules).toHaveLength(1);
186
- expect(manager.getPolicy().defaultAction).toBe('deny');
185
+ expect(manager.getPolicy().defaultAction).toBe('allow');
187
186
  });
188
- it('setDefaultAction changes the fallback', async () => {
187
+ it('setDefaultAction rejects deny', async () => {
189
188
  const manager = new ToolApprovalManager({
190
189
  rules: [],
191
190
  enabled: true,
192
- defaultAction: 'allow',
193
191
  });
194
- await manager.setDefaultAction('deny');
195
- expect(manager.evaluate('Unknown', {}).behavior).toBe('deny');
192
+ await expect(manager.setDefaultAction('deny')).rejects.toThrow(/defaultAction cannot be deny/);
196
193
  });
197
194
  it('setEnabled toggles the manager', async () => {
198
195
  const manager = new ToolApprovalManager({
@@ -205,7 +202,6 @@ describe('ToolApprovalManager', () => {
205
202
  },
206
203
  ],
207
204
  enabled: true,
208
- defaultAction: 'deny',
209
205
  });
210
206
  expect(manager.evaluate('Read', {}).behavior).toBe('deny');
211
207
  await manager.setEnabled(false);
@@ -186,7 +186,11 @@ export interface ToolApprovalRule {
186
186
  }
187
187
  export interface ToolApprovalPolicy {
188
188
  rules: ToolApprovalRule[];
189
- defaultAction: 'allow' | 'deny';
189
+ /**
190
+ * Always `allow`: tools that do not match any rule are allowed.
191
+ * Blocking is done only with explicit `deny` rules (deny-list contract).
192
+ */
193
+ defaultAction: 'allow';
190
194
  defaultDenyMessage?: string;
191
195
  enabled: boolean;
192
196
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doingdev/opencode-claude-manager-plugin",
3
- "version": "0.1.52",
3
+ "version": "0.1.53",
4
4
  "description": "OpenCode plugin that orchestrates Claude Code sessions.",
5
5
  "keywords": [
6
6
  "opencode",