@doingdev/opencode-claude-manager-plugin 0.1.12 → 0.1.15
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/dist/claude/claude-agent-sdk-adapter.d.ts +3 -1
- package/dist/claude/claude-agent-sdk-adapter.js +28 -1
- package/dist/claude/tool-approval-manager.d.ts +27 -0
- package/dist/claude/tool-approval-manager.js +232 -0
- package/dist/index.d.ts +1 -1
- package/dist/manager/persistent-manager.d.ts +1 -0
- package/dist/manager/session-controller.d.ts +1 -0
- package/dist/manager/session-controller.js +1 -0
- package/dist/plugin/claude-manager.plugin.js +144 -7
- package/dist/plugin/service-factory.d.ts +2 -0
- package/dist/plugin/service-factory.js +4 -1
- package/dist/types/contracts.d.ts +27 -0
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Options, type Query, type SDKSessionInfo, type SessionMessage, type SettingSource } from '@anthropic-ai/claude-agent-sdk';
|
|
2
2
|
import type { ClaudeCapabilitySnapshot, ClaudeSessionEvent, ClaudeSessionRunResult, ClaudeSessionSummary, ClaudeSessionTranscriptMessage, RunClaudeSessionInput } from '../types/contracts.js';
|
|
3
|
+
import type { ToolApprovalManager } from './tool-approval-manager.js';
|
|
3
4
|
export type ClaudeSessionEventHandler = (event: ClaudeSessionEvent) => void | Promise<void>;
|
|
4
5
|
interface ClaudeAgentSdkFacade {
|
|
5
6
|
query(params: {
|
|
@@ -15,7 +16,8 @@ interface ClaudeAgentSdkFacade {
|
|
|
15
16
|
}
|
|
16
17
|
export declare class ClaudeAgentSdkAdapter {
|
|
17
18
|
private readonly sdkFacade;
|
|
18
|
-
|
|
19
|
+
private readonly approvalManager?;
|
|
20
|
+
constructor(sdkFacade?: ClaudeAgentSdkFacade, approvalManager?: ToolApprovalManager | undefined);
|
|
19
21
|
runSession(input: RunClaudeSessionInput, onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
|
|
20
22
|
listSavedSessions(cwd?: string): Promise<ClaudeSessionSummary[]>;
|
|
21
23
|
getTranscript(sessionId: string, cwd?: string): Promise<ClaudeSessionTranscriptMessage[]>;
|
|
@@ -9,12 +9,27 @@ const TOOL_INPUT_PREVIEW_MAX = 2000;
|
|
|
9
9
|
const USER_MESSAGE_MAX = 4000;
|
|
10
10
|
export class ClaudeAgentSdkAdapter {
|
|
11
11
|
sdkFacade;
|
|
12
|
-
|
|
12
|
+
approvalManager;
|
|
13
|
+
constructor(sdkFacade = defaultFacade, approvalManager) {
|
|
13
14
|
this.sdkFacade = sdkFacade;
|
|
15
|
+
this.approvalManager = approvalManager;
|
|
14
16
|
}
|
|
15
17
|
async runSession(input, onEvent) {
|
|
16
18
|
const options = this.buildOptions(input);
|
|
17
19
|
const includePartials = options.includePartialMessages === true;
|
|
20
|
+
const abortController = new AbortController();
|
|
21
|
+
options.abortController = abortController;
|
|
22
|
+
const externalSignal = input.abortSignal;
|
|
23
|
+
let onAbort;
|
|
24
|
+
if (externalSignal) {
|
|
25
|
+
if (externalSignal.aborted) {
|
|
26
|
+
abortController.abort();
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
onAbort = () => abortController.abort();
|
|
30
|
+
externalSignal.addEventListener('abort', onAbort, { once: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
18
33
|
const sessionQuery = this.sdkFacade.query({
|
|
19
34
|
prompt: input.prompt,
|
|
20
35
|
options,
|
|
@@ -58,6 +73,9 @@ export class ClaudeAgentSdkAdapter {
|
|
|
58
73
|
}
|
|
59
74
|
}
|
|
60
75
|
finally {
|
|
76
|
+
if (onAbort && externalSignal) {
|
|
77
|
+
externalSignal.removeEventListener('abort', onAbort);
|
|
78
|
+
}
|
|
61
79
|
sessionQuery.close();
|
|
62
80
|
}
|
|
63
81
|
return {
|
|
@@ -145,6 +163,15 @@ export class ClaudeAgentSdkAdapter {
|
|
|
145
163
|
if (!input.resumeSessionId) {
|
|
146
164
|
delete options.resume;
|
|
147
165
|
}
|
|
166
|
+
if (this.approvalManager) {
|
|
167
|
+
const manager = this.approvalManager;
|
|
168
|
+
options.canUseTool = async (toolName, toolInput, opts) => {
|
|
169
|
+
return manager.evaluate(toolName, toolInput, {
|
|
170
|
+
title: opts.title,
|
|
171
|
+
agentID: opts.agentID,
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
}
|
|
148
175
|
return options;
|
|
149
176
|
}
|
|
150
177
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ToolApprovalDecision, ToolApprovalPolicy, ToolApprovalRule } from '../types/contracts.js';
|
|
2
|
+
export declare class ToolApprovalManager {
|
|
3
|
+
private policy;
|
|
4
|
+
private decisions;
|
|
5
|
+
private readonly maxDecisions;
|
|
6
|
+
constructor(policy?: Partial<ToolApprovalPolicy>, maxDecisions?: number);
|
|
7
|
+
evaluate(toolName: string, input: Record<string, unknown>, options?: {
|
|
8
|
+
title?: string;
|
|
9
|
+
agentID?: string;
|
|
10
|
+
}): {
|
|
11
|
+
behavior: 'allow';
|
|
12
|
+
} | {
|
|
13
|
+
behavior: 'deny';
|
|
14
|
+
message: string;
|
|
15
|
+
};
|
|
16
|
+
getDecisions(limit?: number): ToolApprovalDecision[];
|
|
17
|
+
getDeniedDecisions(limit?: number): ToolApprovalDecision[];
|
|
18
|
+
clearDecisions(): void;
|
|
19
|
+
getPolicy(): ToolApprovalPolicy;
|
|
20
|
+
setPolicy(policy: ToolApprovalPolicy): void;
|
|
21
|
+
addRule(rule: ToolApprovalRule, position?: number): void;
|
|
22
|
+
removeRule(ruleId: string): boolean;
|
|
23
|
+
setDefaultAction(action: 'allow' | 'deny'): void;
|
|
24
|
+
setEnabled(enabled: boolean): void;
|
|
25
|
+
private findMatchingRule;
|
|
26
|
+
private recordDecision;
|
|
27
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
const DEFAULT_MAX_DECISIONS = 500;
|
|
2
|
+
const INPUT_PREVIEW_MAX = 300;
|
|
3
|
+
function getDefaultRules() {
|
|
4
|
+
return [
|
|
5
|
+
// Safe read-only tools
|
|
6
|
+
{
|
|
7
|
+
id: 'allow-read',
|
|
8
|
+
toolPattern: 'Read',
|
|
9
|
+
action: 'allow',
|
|
10
|
+
description: 'Allow reading files',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: 'allow-grep',
|
|
14
|
+
toolPattern: 'Grep',
|
|
15
|
+
action: 'allow',
|
|
16
|
+
description: 'Allow grep searches',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'allow-glob',
|
|
20
|
+
toolPattern: 'Glob',
|
|
21
|
+
action: 'allow',
|
|
22
|
+
description: 'Allow glob file searches',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'allow-ls',
|
|
26
|
+
toolPattern: 'LS',
|
|
27
|
+
action: 'allow',
|
|
28
|
+
description: 'Allow directory listing',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'allow-list',
|
|
32
|
+
toolPattern: 'ListDirectory',
|
|
33
|
+
action: 'allow',
|
|
34
|
+
description: 'Allow listing directories',
|
|
35
|
+
},
|
|
36
|
+
// Edit tools
|
|
37
|
+
{
|
|
38
|
+
id: 'allow-edit',
|
|
39
|
+
toolPattern: 'Edit',
|
|
40
|
+
action: 'allow',
|
|
41
|
+
description: 'Allow file edits',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'allow-multiedit',
|
|
45
|
+
toolPattern: 'MultiEdit',
|
|
46
|
+
action: 'allow',
|
|
47
|
+
description: 'Allow multi-edits',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'allow-write',
|
|
51
|
+
toolPattern: 'Write',
|
|
52
|
+
action: 'allow',
|
|
53
|
+
description: 'Allow file writes',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'allow-notebook',
|
|
57
|
+
toolPattern: 'NotebookEdit',
|
|
58
|
+
action: 'allow',
|
|
59
|
+
description: 'Allow notebook edits',
|
|
60
|
+
},
|
|
61
|
+
// Bash - deny dangerous patterns first, then allow the rest
|
|
62
|
+
{
|
|
63
|
+
id: 'deny-bash-rm-rf-root',
|
|
64
|
+
toolPattern: 'Bash',
|
|
65
|
+
inputPattern: 'rm -rf /',
|
|
66
|
+
action: 'deny',
|
|
67
|
+
denyMessage: 'Destructive rm -rf on root path is not allowed.',
|
|
68
|
+
description: 'Block rm -rf on root',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'deny-bash-force-push',
|
|
72
|
+
toolPattern: 'Bash',
|
|
73
|
+
inputPattern: 'git push --force',
|
|
74
|
+
action: 'deny',
|
|
75
|
+
denyMessage: 'Force push is not allowed.',
|
|
76
|
+
description: 'Block git force push',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'deny-bash-reset-hard',
|
|
80
|
+
toolPattern: 'Bash',
|
|
81
|
+
inputPattern: 'git reset --hard',
|
|
82
|
+
action: 'deny',
|
|
83
|
+
denyMessage: 'git reset --hard is not allowed from Claude Code. Use the manager git_reset tool instead.',
|
|
84
|
+
description: 'Block git reset --hard',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'allow-bash',
|
|
88
|
+
toolPattern: 'Bash',
|
|
89
|
+
action: 'allow',
|
|
90
|
+
description: 'Allow bash commands (after dangerous patterns filtered)',
|
|
91
|
+
},
|
|
92
|
+
// Agent / misc
|
|
93
|
+
{
|
|
94
|
+
id: 'allow-agent',
|
|
95
|
+
toolPattern: 'Agent',
|
|
96
|
+
action: 'allow',
|
|
97
|
+
description: 'Allow agent delegation',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'allow-todowrite',
|
|
101
|
+
toolPattern: 'TodoWrite',
|
|
102
|
+
action: 'allow',
|
|
103
|
+
description: 'Allow todo tracking',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'allow-todoread',
|
|
107
|
+
toolPattern: 'TodoRead',
|
|
108
|
+
action: 'allow',
|
|
109
|
+
description: 'Allow todo reading',
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
export class ToolApprovalManager {
|
|
114
|
+
policy;
|
|
115
|
+
decisions = [];
|
|
116
|
+
maxDecisions;
|
|
117
|
+
constructor(policy, maxDecisions) {
|
|
118
|
+
this.policy = {
|
|
119
|
+
rules: policy?.rules ?? getDefaultRules(),
|
|
120
|
+
defaultAction: policy?.defaultAction ?? 'allow',
|
|
121
|
+
defaultDenyMessage: policy?.defaultDenyMessage ?? 'Tool call denied by approval policy.',
|
|
122
|
+
enabled: policy?.enabled ?? true,
|
|
123
|
+
};
|
|
124
|
+
this.maxDecisions = maxDecisions ?? DEFAULT_MAX_DECISIONS;
|
|
125
|
+
}
|
|
126
|
+
evaluate(toolName, input, options) {
|
|
127
|
+
if (!this.policy.enabled) {
|
|
128
|
+
return { behavior: 'allow' };
|
|
129
|
+
}
|
|
130
|
+
const inputJson = safeJsonStringify(input);
|
|
131
|
+
const matchedRule = this.findMatchingRule(toolName, inputJson);
|
|
132
|
+
const action = matchedRule?.action ?? this.policy.defaultAction;
|
|
133
|
+
const denyMessage = action === 'deny'
|
|
134
|
+
? (matchedRule?.denyMessage ??
|
|
135
|
+
this.policy.defaultDenyMessage ??
|
|
136
|
+
'Denied by policy.')
|
|
137
|
+
: undefined;
|
|
138
|
+
this.recordDecision({
|
|
139
|
+
timestamp: new Date().toISOString(),
|
|
140
|
+
toolName,
|
|
141
|
+
inputPreview: inputJson.slice(0, INPUT_PREVIEW_MAX),
|
|
142
|
+
title: options?.title,
|
|
143
|
+
matchedRuleId: matchedRule?.id ?? 'default',
|
|
144
|
+
action,
|
|
145
|
+
denyMessage,
|
|
146
|
+
agentId: options?.agentID,
|
|
147
|
+
});
|
|
148
|
+
if (action === 'deny') {
|
|
149
|
+
return { behavior: 'deny', message: denyMessage };
|
|
150
|
+
}
|
|
151
|
+
return { behavior: 'allow' };
|
|
152
|
+
}
|
|
153
|
+
getDecisions(limit) {
|
|
154
|
+
const all = [...this.decisions].reverse();
|
|
155
|
+
return limit ? all.slice(0, limit) : all;
|
|
156
|
+
}
|
|
157
|
+
getDeniedDecisions(limit) {
|
|
158
|
+
const denied = this.decisions.filter((d) => d.action === 'deny').reverse();
|
|
159
|
+
return limit ? denied.slice(0, limit) : denied;
|
|
160
|
+
}
|
|
161
|
+
clearDecisions() {
|
|
162
|
+
this.decisions = [];
|
|
163
|
+
}
|
|
164
|
+
getPolicy() {
|
|
165
|
+
return { ...this.policy, rules: [...this.policy.rules] };
|
|
166
|
+
}
|
|
167
|
+
setPolicy(policy) {
|
|
168
|
+
this.policy = { ...policy, rules: [...policy.rules] };
|
|
169
|
+
}
|
|
170
|
+
addRule(rule, position) {
|
|
171
|
+
if (position !== undefined &&
|
|
172
|
+
position >= 0 &&
|
|
173
|
+
position < this.policy.rules.length) {
|
|
174
|
+
this.policy.rules.splice(position, 0, rule);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
this.policy.rules.push(rule);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
removeRule(ruleId) {
|
|
181
|
+
const index = this.policy.rules.findIndex((r) => r.id === ruleId);
|
|
182
|
+
if (index === -1) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
this.policy.rules.splice(index, 1);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
setDefaultAction(action) {
|
|
189
|
+
this.policy.defaultAction = action;
|
|
190
|
+
}
|
|
191
|
+
setEnabled(enabled) {
|
|
192
|
+
this.policy.enabled = enabled;
|
|
193
|
+
}
|
|
194
|
+
findMatchingRule(toolName, inputJson) {
|
|
195
|
+
for (const rule of this.policy.rules) {
|
|
196
|
+
if (!matchesToolPattern(rule.toolPattern, toolName)) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (rule.inputPattern && !inputJson.includes(rule.inputPattern)) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
return rule;
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
recordDecision(decision) {
|
|
207
|
+
this.decisions.push(decision);
|
|
208
|
+
if (this.decisions.length > this.maxDecisions) {
|
|
209
|
+
this.decisions = this.decisions.slice(-this.maxDecisions);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function matchesToolPattern(pattern, toolName) {
|
|
214
|
+
if (pattern === '*') {
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
if (!pattern.includes('*')) {
|
|
218
|
+
return pattern === toolName;
|
|
219
|
+
}
|
|
220
|
+
const regex = new RegExp('^' +
|
|
221
|
+
pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') +
|
|
222
|
+
'$');
|
|
223
|
+
return regex.test(toolName);
|
|
224
|
+
}
|
|
225
|
+
function safeJsonStringify(value) {
|
|
226
|
+
try {
|
|
227
|
+
return JSON.stringify(value);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return String(value);
|
|
231
|
+
}
|
|
232
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Plugin } from '@opencode-ai/plugin';
|
|
2
2
|
import { ClaudeManagerPlugin } from './plugin/claude-manager.plugin.js';
|
|
3
|
-
export type { ClaudeCapabilitySnapshot, ClaudeMetadataSnapshot, ClaudeSessionRunResult, ClaudeSessionSummary, ClaudeSessionTranscriptMessage, ManagerPromptRegistry, RunClaudeSessionInput, SessionContextSnapshot, GitDiffResult, GitOperationResult, PersistentRunRecord, PersistentRunResult, ActiveSessionState, ContextWarningLevel, } from './types/contracts.js';
|
|
3
|
+
export type { ClaudeCapabilitySnapshot, ClaudeMetadataSnapshot, ClaudeSessionRunResult, ClaudeSessionSummary, ClaudeSessionTranscriptMessage, ManagerPromptRegistry, RunClaudeSessionInput, SessionContextSnapshot, GitDiffResult, GitOperationResult, PersistentRunRecord, PersistentRunResult, ActiveSessionState, ContextWarningLevel, ToolApprovalRule, ToolApprovalPolicy, ToolApprovalDecision, } from './types/contracts.js';
|
|
4
4
|
export { ClaudeManagerPlugin };
|
|
5
5
|
export declare const plugin: Plugin;
|
|
@@ -19,6 +19,7 @@ export declare class PersistentManager {
|
|
|
19
19
|
*/
|
|
20
20
|
sendMessage(cwd: string, message: string, options?: {
|
|
21
21
|
model?: string;
|
|
22
|
+
abortSignal?: AbortSignal;
|
|
22
23
|
}, onEvent?: ClaudeSessionEventHandler): Promise<{
|
|
23
24
|
sessionId: string | undefined;
|
|
24
25
|
finalText: string;
|
|
@@ -16,6 +16,7 @@ export declare class SessionController {
|
|
|
16
16
|
sendMessage(cwd: string, message: string, options?: {
|
|
17
17
|
model?: string;
|
|
18
18
|
settingSources?: Array<'user' | 'project' | 'local'>;
|
|
19
|
+
abortSignal?: AbortSignal;
|
|
19
20
|
}, onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
|
|
20
21
|
/**
|
|
21
22
|
* Send /compact to the current session to compress context.
|
|
@@ -30,6 +30,7 @@ export class SessionController {
|
|
|
30
30
|
includePartialMessages: true,
|
|
31
31
|
model: options?.model,
|
|
32
32
|
settingSources: options?.settingSources ?? ['user', 'project', 'local'],
|
|
33
|
+
abortSignal: options?.abortSignal,
|
|
33
34
|
};
|
|
34
35
|
if (this.activeSessionId) {
|
|
35
36
|
// Resume existing session
|
|
@@ -11,6 +11,9 @@ const MANAGER_TOOL_IDS = [
|
|
|
11
11
|
'claude_manager_metadata',
|
|
12
12
|
'claude_manager_sessions',
|
|
13
13
|
'claude_manager_runs',
|
|
14
|
+
'claude_manager_approval_policy',
|
|
15
|
+
'claude_manager_approval_decisions',
|
|
16
|
+
'claude_manager_approval_update',
|
|
14
17
|
];
|
|
15
18
|
export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
16
19
|
const services = getOrCreatePluginServices(worktree);
|
|
@@ -30,16 +33,23 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
30
33
|
toolId === 'claude_manager_send' ||
|
|
31
34
|
toolId === 'claude_manager_git_commit' ||
|
|
32
35
|
toolId === 'claude_manager_git_reset' ||
|
|
33
|
-
toolId === 'claude_manager_clear'
|
|
36
|
+
toolId === 'claude_manager_clear' ||
|
|
37
|
+
toolId === 'claude_manager_approval_update'
|
|
34
38
|
? 'deny'
|
|
35
39
|
: 'allow';
|
|
36
40
|
}
|
|
37
41
|
config.agent['claude-manager'] ??= {
|
|
38
42
|
description: 'Primary agent that operates Claude Code through a persistent session, reviews work via git diff, and commits/resets changes.',
|
|
39
43
|
mode: 'primary',
|
|
40
|
-
color: '
|
|
44
|
+
color: '#D97757',
|
|
41
45
|
permission: {
|
|
42
46
|
'*': 'deny',
|
|
47
|
+
read: 'allow',
|
|
48
|
+
grep: 'allow',
|
|
49
|
+
glob: 'allow',
|
|
50
|
+
codesearch: 'allow',
|
|
51
|
+
webfetch: 'allow',
|
|
52
|
+
websearch: 'allow',
|
|
43
53
|
...managerPermissions,
|
|
44
54
|
},
|
|
45
55
|
prompt: managerPromptRegistry.managerSystemPrompt,
|
|
@@ -106,15 +116,21 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
106
116
|
async execute(args, context) {
|
|
107
117
|
const cwd = args.cwd ?? context.worktree;
|
|
108
118
|
const hasActiveSession = services.manager.getStatus().sessionId !== null;
|
|
119
|
+
const promptPreview = args.message.length > 100
|
|
120
|
+
? args.message.slice(0, 100) + '...'
|
|
121
|
+
: args.message;
|
|
109
122
|
context.metadata({
|
|
110
123
|
title: hasActiveSession
|
|
111
124
|
? 'Claude Code: Resuming session...'
|
|
112
125
|
: 'Claude Code: Initializing...',
|
|
113
|
-
metadata: {
|
|
126
|
+
metadata: {
|
|
127
|
+
sessionId: services.manager.getStatus().sessionId,
|
|
128
|
+
prompt: promptPreview,
|
|
129
|
+
},
|
|
114
130
|
});
|
|
115
131
|
let turnsSoFar = 0;
|
|
116
132
|
let costSoFar = 0;
|
|
117
|
-
const result = await services.manager.sendMessage(cwd, args.message, { model: args.model }, (event) => {
|
|
133
|
+
const result = await services.manager.sendMessage(cwd, args.message, { model: args.model, abortSignal: context.abort }, (event) => {
|
|
118
134
|
if (event.turns !== undefined) {
|
|
119
135
|
turnsSoFar = event.turns;
|
|
120
136
|
}
|
|
@@ -124,8 +140,19 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
124
140
|
const costLabel = `$${costSoFar.toFixed(4)}`;
|
|
125
141
|
if (event.type === 'tool_call') {
|
|
126
142
|
let toolName = 'tool';
|
|
143
|
+
let inputPreview = '';
|
|
127
144
|
try {
|
|
128
|
-
|
|
145
|
+
const parsed = JSON.parse(event.text);
|
|
146
|
+
toolName = parsed.name ?? 'tool';
|
|
147
|
+
if (parsed.input) {
|
|
148
|
+
const inputStr = typeof parsed.input === 'string'
|
|
149
|
+
? parsed.input
|
|
150
|
+
: JSON.stringify(parsed.input);
|
|
151
|
+
inputPreview =
|
|
152
|
+
inputStr.length > 150
|
|
153
|
+
? inputStr.slice(0, 150) + '...'
|
|
154
|
+
: inputStr;
|
|
155
|
+
}
|
|
129
156
|
}
|
|
130
157
|
catch {
|
|
131
158
|
// ignore parse errors
|
|
@@ -135,17 +162,39 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
135
162
|
metadata: {
|
|
136
163
|
sessionId: event.sessionId,
|
|
137
164
|
type: event.type,
|
|
138
|
-
|
|
165
|
+
tool: toolName,
|
|
166
|
+
input: inputPreview,
|
|
139
167
|
},
|
|
140
168
|
});
|
|
141
169
|
}
|
|
142
170
|
else if (event.type === 'assistant') {
|
|
171
|
+
const thinkingPreview = event.text.length > 150
|
|
172
|
+
? event.text.slice(0, 150) + '...'
|
|
173
|
+
: event.text;
|
|
143
174
|
context.metadata({
|
|
144
175
|
title: `Claude Code: Thinking... (${turnsSoFar} turns, ${costLabel})`,
|
|
145
176
|
metadata: {
|
|
146
177
|
sessionId: event.sessionId,
|
|
147
178
|
type: event.type,
|
|
148
|
-
|
|
179
|
+
thinking: thinkingPreview,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
else if (event.type === 'init') {
|
|
184
|
+
context.metadata({
|
|
185
|
+
title: `Claude Code: Session started`,
|
|
186
|
+
metadata: {
|
|
187
|
+
sessionId: event.sessionId,
|
|
188
|
+
prompt: promptPreview,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
else if (event.type === 'error') {
|
|
193
|
+
context.metadata({
|
|
194
|
+
title: `Claude Code: Error`,
|
|
195
|
+
metadata: {
|
|
196
|
+
sessionId: event.sessionId,
|
|
197
|
+
error: event.text.slice(0, 200),
|
|
149
198
|
},
|
|
150
199
|
});
|
|
151
200
|
}
|
|
@@ -299,6 +348,94 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
299
348
|
return JSON.stringify(runs, null, 2);
|
|
300
349
|
},
|
|
301
350
|
}),
|
|
351
|
+
claude_manager_approval_policy: tool({
|
|
352
|
+
description: 'View the current tool approval policy: rules, default action, and enabled status.',
|
|
353
|
+
args: {},
|
|
354
|
+
async execute(_args, context) {
|
|
355
|
+
annotateToolRun(context, 'Reading approval policy', {});
|
|
356
|
+
return JSON.stringify(services.approvalManager.getPolicy(), null, 2);
|
|
357
|
+
},
|
|
358
|
+
}),
|
|
359
|
+
claude_manager_approval_decisions: tool({
|
|
360
|
+
description: 'View recent tool approval decisions. Shows what tools were allowed or denied. ' +
|
|
361
|
+
'Use deniedOnly to see only denied calls.',
|
|
362
|
+
args: {
|
|
363
|
+
limit: tool.schema.number().optional(),
|
|
364
|
+
deniedOnly: tool.schema.boolean().optional(),
|
|
365
|
+
},
|
|
366
|
+
async execute(args, context) {
|
|
367
|
+
annotateToolRun(context, 'Reading approval decisions', {});
|
|
368
|
+
const decisions = args.deniedOnly
|
|
369
|
+
? services.approvalManager.getDeniedDecisions(args.limit)
|
|
370
|
+
: services.approvalManager.getDecisions(args.limit);
|
|
371
|
+
return JSON.stringify({ total: decisions.length, decisions }, null, 2);
|
|
372
|
+
},
|
|
373
|
+
}),
|
|
374
|
+
claude_manager_approval_update: tool({
|
|
375
|
+
description: 'Update the tool approval policy. Add/remove rules, change default action, or enable/disable. ' +
|
|
376
|
+
'Rules are evaluated top-to-bottom; first match wins.',
|
|
377
|
+
args: {
|
|
378
|
+
action: tool.schema.enum([
|
|
379
|
+
'addRule',
|
|
380
|
+
'removeRule',
|
|
381
|
+
'setDefault',
|
|
382
|
+
'setEnabled',
|
|
383
|
+
'clearDecisions',
|
|
384
|
+
]),
|
|
385
|
+
ruleId: tool.schema.string().optional(),
|
|
386
|
+
toolPattern: tool.schema.string().optional(),
|
|
387
|
+
inputPattern: tool.schema.string().optional(),
|
|
388
|
+
ruleAction: tool.schema.enum(['allow', 'deny']).optional(),
|
|
389
|
+
denyMessage: tool.schema.string().optional(),
|
|
390
|
+
description: tool.schema.string().optional(),
|
|
391
|
+
position: tool.schema.number().optional(),
|
|
392
|
+
defaultAction: tool.schema.enum(['allow', 'deny']).optional(),
|
|
393
|
+
enabled: tool.schema.boolean().optional(),
|
|
394
|
+
},
|
|
395
|
+
async execute(args, context) {
|
|
396
|
+
annotateToolRun(context, `Updating approval: ${args.action}`, {});
|
|
397
|
+
if (args.action === 'addRule') {
|
|
398
|
+
if (!args.ruleId || !args.toolPattern || !args.ruleAction) {
|
|
399
|
+
return JSON.stringify({
|
|
400
|
+
error: 'addRule requires ruleId, toolPattern, and ruleAction',
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
services.approvalManager.addRule({
|
|
404
|
+
id: args.ruleId,
|
|
405
|
+
toolPattern: args.toolPattern,
|
|
406
|
+
inputPattern: args.inputPattern,
|
|
407
|
+
action: args.ruleAction,
|
|
408
|
+
denyMessage: args.denyMessage,
|
|
409
|
+
description: args.description,
|
|
410
|
+
}, args.position);
|
|
411
|
+
}
|
|
412
|
+
else if (args.action === 'removeRule') {
|
|
413
|
+
if (!args.ruleId) {
|
|
414
|
+
return JSON.stringify({ error: 'removeRule requires ruleId' });
|
|
415
|
+
}
|
|
416
|
+
const removed = services.approvalManager.removeRule(args.ruleId);
|
|
417
|
+
return JSON.stringify({ removed });
|
|
418
|
+
}
|
|
419
|
+
else if (args.action === 'setDefault') {
|
|
420
|
+
if (!args.defaultAction) {
|
|
421
|
+
return JSON.stringify({
|
|
422
|
+
error: 'setDefault requires defaultAction',
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
services.approvalManager.setDefaultAction(args.defaultAction);
|
|
426
|
+
}
|
|
427
|
+
else if (args.action === 'setEnabled') {
|
|
428
|
+
if (args.enabled === undefined) {
|
|
429
|
+
return JSON.stringify({ error: 'setEnabled requires enabled' });
|
|
430
|
+
}
|
|
431
|
+
services.approvalManager.setEnabled(args.enabled);
|
|
432
|
+
}
|
|
433
|
+
else if (args.action === 'clearDecisions') {
|
|
434
|
+
services.approvalManager.clearDecisions();
|
|
435
|
+
}
|
|
436
|
+
return JSON.stringify(services.approvalManager.getPolicy(), null, 2);
|
|
437
|
+
},
|
|
438
|
+
}),
|
|
302
439
|
},
|
|
303
440
|
};
|
|
304
441
|
};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { ClaudeSessionService } from '../claude/claude-session.service.js';
|
|
2
|
+
import { ToolApprovalManager } from '../claude/tool-approval-manager.js';
|
|
2
3
|
import { PersistentManager } from '../manager/persistent-manager.js';
|
|
3
4
|
interface ClaudeManagerPluginServices {
|
|
4
5
|
manager: PersistentManager;
|
|
5
6
|
sessions: ClaudeSessionService;
|
|
7
|
+
approvalManager: ToolApprovalManager;
|
|
6
8
|
}
|
|
7
9
|
export declare function getOrCreatePluginServices(worktree: string): ClaudeManagerPluginServices;
|
|
8
10
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ClaudeAgentSdkAdapter } from '../claude/claude-agent-sdk-adapter.js';
|
|
2
2
|
import { ClaudeSessionService } from '../claude/claude-session.service.js';
|
|
3
|
+
import { ToolApprovalManager } from '../claude/tool-approval-manager.js';
|
|
3
4
|
import { ClaudeMetadataService } from '../metadata/claude-metadata.service.js';
|
|
4
5
|
import { RepoClaudeConfigReader } from '../metadata/repo-claude-config-reader.js';
|
|
5
6
|
import { FileRunStateStore } from '../state/file-run-state-store.js';
|
|
@@ -15,7 +16,8 @@ export function getOrCreatePluginServices(worktree) {
|
|
|
15
16
|
if (cachedServices) {
|
|
16
17
|
return cachedServices;
|
|
17
18
|
}
|
|
18
|
-
const
|
|
19
|
+
const approvalManager = new ToolApprovalManager();
|
|
20
|
+
const sdkAdapter = new ClaudeAgentSdkAdapter(undefined, approvalManager);
|
|
19
21
|
const metadataService = new ClaudeMetadataService(new RepoClaudeConfigReader(), sdkAdapter);
|
|
20
22
|
const sessionService = new ClaudeSessionService(sdkAdapter, metadataService);
|
|
21
23
|
const contextTracker = new ContextTracker();
|
|
@@ -29,6 +31,7 @@ export function getOrCreatePluginServices(worktree) {
|
|
|
29
31
|
const services = {
|
|
30
32
|
manager,
|
|
31
33
|
sessions: sessionService,
|
|
34
|
+
approvalManager,
|
|
32
35
|
};
|
|
33
36
|
serviceCache.set(worktree, services);
|
|
34
37
|
return services;
|
|
@@ -67,6 +67,7 @@ export interface RunClaudeSessionInput {
|
|
|
67
67
|
includePartialMessages?: boolean;
|
|
68
68
|
settingSources?: ClaudeSettingSource[];
|
|
69
69
|
maxTurns?: number;
|
|
70
|
+
abortSignal?: AbortSignal;
|
|
70
71
|
}
|
|
71
72
|
export interface ClaudeSessionRunResult {
|
|
72
73
|
sessionId?: string;
|
|
@@ -166,3 +167,29 @@ export interface PersistentRunRecord {
|
|
|
166
167
|
export interface PersistentRunResult {
|
|
167
168
|
run: PersistentRunRecord;
|
|
168
169
|
}
|
|
170
|
+
export interface ToolApprovalRule {
|
|
171
|
+
id: string;
|
|
172
|
+
description?: string;
|
|
173
|
+
/** Tool name — exact match or glob with * wildcard */
|
|
174
|
+
toolPattern: string;
|
|
175
|
+
/** Optional substring match against JSON-serialized tool input */
|
|
176
|
+
inputPattern?: string;
|
|
177
|
+
action: 'allow' | 'deny';
|
|
178
|
+
denyMessage?: string;
|
|
179
|
+
}
|
|
180
|
+
export interface ToolApprovalPolicy {
|
|
181
|
+
rules: ToolApprovalRule[];
|
|
182
|
+
defaultAction: 'allow' | 'deny';
|
|
183
|
+
defaultDenyMessage?: string;
|
|
184
|
+
enabled: boolean;
|
|
185
|
+
}
|
|
186
|
+
export interface ToolApprovalDecision {
|
|
187
|
+
timestamp: string;
|
|
188
|
+
toolName: string;
|
|
189
|
+
inputPreview: string;
|
|
190
|
+
title?: string;
|
|
191
|
+
matchedRuleId: string;
|
|
192
|
+
action: 'allow' | 'deny';
|
|
193
|
+
denyMessage?: string;
|
|
194
|
+
agentId?: string;
|
|
195
|
+
}
|