Skip to content

Commit f71cf9f

Browse files
authored
fix: refactor tool approval operations (#648)
* fix: refactor tool approval operations * fix: add Japanese tool approval translations * fix: preserve edit approval on sqlite rollback
1 parent 8465e53 commit f71cf9f

121 files changed

Lines changed: 1517 additions & 286 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/web/src/i18n/locales/en.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,13 +1223,13 @@
12231223
"confirmReset": "Are you sure you want to reset all tool approval rules to their default state?",
12241224
"resetWarningExtra": "Custom bypass and review rules will be lost.",
12251225
"title": "Tool Call Approval",
1226-
"intro": "Require approval before the bot modifies files or runs commands. Configure rules for each tool below. Bypass patterns auto-approve; must-review patterns force approval.",
1226+
"intro": "Require approval before the bot reads files, changes files, or runs commands. Configure rules for each operation below. Bypass patterns auto-approve; must-review patterns force approval.",
12271227
"tools": {
1228-
"write": "Create new files inside the container",
1229-
"edit": "Modify existing files inside the container",
1228+
"read": "Read files and list directories inside the workspace",
1229+
"write": "Write files, edit files, and apply patches inside the workspace",
12301230
"exec": "Execute shell commands inside the container"
12311231
},
1232-
"toolDisabledHint": "Approval is currently off for this tool. The rules below take effect only after enabling the switch.",
1232+
"toolDisabledHint": "Approval is currently off for this operation. The rules below take effect only after enabling the switch.",
12331233
"bypass": "Bypass (allow list)",
12341234
"bypassHint": "Calls matching any of these patterns are auto-approved without prompting.",
12351235
"mustReview": "Must review (deny list)",
@@ -1881,7 +1881,7 @@
18811881
"desktopEnabled": "Desktop",
18821882
"desktopEnabledDescription": "Enable the VNC desktop. Memoh will automatically provision Debian/Ubuntu or Alpine workspaces if missing.",
18831883
"toolApproval": "Tool Call Approval",
1884-
"toolApprovalDescription": "Require human approval for sensitive write, edit, and exec tool calls, with bypass templates for safe paths or commands.",
1884+
"toolApprovalDescription": "Require human approval for read, write, and exec operations, with bypass templates for safe paths or commands.",
18851885
"acpSetupMode": "Setup",
18861886
"acpSetupApiKey": "API Key",
18871887
"acpSetupOAuth": "OAuth",

apps/web/src/i18n/locales/ja.json

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1190,55 +1190,55 @@
11901190
"toolApproval": {
11911191
"posture": {
11921192
"title": "セキュリティ態勢",
1193-
"hardened": "硬化した",
1194-
"description": "承認ポリシーは、実行境界を積極的に強制します"
1193+
"hardened": "強化済み",
1194+
"description": "承認ポリシーが実行境界を適用しています"
11951195
},
11961196
"metrics": {
11971197
"activeRules": "アクティブなルール",
11981198
"totalDefined": "定義済み合計",
11991199
"blockedCount": "ブロック",
1200-
"sessionTrust": "Sessionの信頼"
1200+
"sessionTrust": "セッションの信頼"
12011201
},
12021202
"recentActivity": "最近の活動",
12031203
"trust": "信頼",
12041204
"actions": {
1205-
"trustCall": "トラストコール",
1206-
"trustDir": "トラストディレクトリ"
1205+
"trustCall": "この呼び出しを信頼",
1206+
"trustDir": "このディレクトリを信頼"
12071207
},
12081208
"noActivity": "最近の活動はありません。",
12091209
"warnings": {
1210-
"disabled": "Toolの承認は現在無効になっています",
1211-
"breachRisk": "リセットすると、すべてのカスタム セキュリティ ルールがクリアされます"
1210+
"disabled": "ツール承認は現在無効です",
1211+
"breachRisk": "リセットすると、カスタムセキュリティルールがすべて削除されます"
12121212
},
1213-
"restoreSecurity": "Toolの承認を有効にする",
1213+
"restoreSecurity": "ツール承認を有効にする",
12141214
"examples": "",
12151215
"status": {
12161216
"active": "アクティブ",
12171217
"implicitDeny": "暗黙的な拒否",
1218-
"sealed": "密封された"
1218+
"sealed": "封鎖済み"
12191219
},
12201220
"dangerZone": "危険な操作",
1221-
"dangerZoneDesc": "主要なセキュリティ ポリシーを管理します",
1221+
"dangerZoneDesc": "主要なセキュリティポリシーを管理します",
12221222
"resetDefaults": "デフォルトにリセット",
1223-
"confirmReset": "すべてのTool承認ルールをデフォルトのステータスにリセットしてもよろしいですか?",
1224-
"resetWarningExtra": "カスタム バイパス ルールとレビュー ルールは失われます",
1225-
"title": "Tool呼び出しの承認",
1226-
"intro": "Botがファイルを変更したりコマンドを実行したりする前に承認を必要とします。以下の各Toolのルールを設定します。バイパスパターンは自動承認されます。必ず確認する必要があるパターンは承認を強制します",
1223+
"confirmReset": "すべてのツール承認ルールをデフォルト状態に戻しますか?",
1224+
"resetWarningExtra": "カスタムのバイパスルールとレビュー必須ルールは失われます",
1225+
"title": "ツール呼び出しの承認",
1226+
"intro": "Bot がファイルを読み取る、ファイルを変更する、またはコマンドを実行する前に承認を要求します。操作ごとにルールを設定できます。バイパスパターンは自動承認され、レビュー必須パターンは承認を強制します",
12271227
"tools": {
1228-
"write": "Container内に新しいファイルを作成する",
1229-
"edit": "Container内の既存のファイルを変更する",
1230-
"exec": "Container内でシェルコマンドを実行する"
1228+
"read": "ワークスペース内のファイルを読み取り、ディレクトリを一覧表示する",
1229+
"write": "ワークスペース内でファイルの書き込み、編集、patch の適用を行う",
1230+
"exec": "コンテナ内でシェルコマンドを実行する"
12311231
},
1232-
"toolDisabledHint": "現在、このToolは承認されていません。以下のルールは、スイッチを有効にした後にのみ有効になります",
1232+
"toolDisabledHint": "この操作では現在、承認は要求されていません。以下のルールは、スイッチを有効にした後に適用されます",
12331233
"bypass": "バイパス (許可リスト)",
1234-
"bypassHint": "これらのパターンのいずれかに一致する通話は、プロンプトを表示せずに自動的に承認されます",
1235-
"mustReview": "確認する必要があります (拒否リスト)",
1236-
"mustReviewHint": "これらのパターンのいずれかに一致する呼び出しは、このToolが自動承認されている場合でも、常に承認をトリガーします",
1234+
"bypassHint": "これらのパターンのいずれかに一致する呼び出しは、確認なしで自動承認されます",
1235+
"mustReview": "レビュー必須 (拒否リスト)",
1236+
"mustReviewHint": "これらのパターンのいずれかに一致する呼び出しは、このツールが自動承認されている場合でも常に承認を要求します",
12371237
"placeholders": {
1238-
"fileBypass": "/tmp/**\n/データ/**",
1238+
"fileBypass": "/tmp/**\n/data/**",
12391239
"fileMustReview": "/etc/**\n.env*",
1240-
"execBypass": "ls\n障害者\ngit ステータス",
1241-
"execMustReview": "rm -rf *\n須藤 *"
1240+
"execBypass": "ls\npwd\ngit status",
1241+
"execMustReview": "rm -rf *\nsudo *"
12421242
},
12431243
"unsavedChanges": "未保存の変更"
12441244
},

apps/web/src/i18n/locales/zh.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,13 +1223,13 @@
12231223
"confirmReset": "确定要将所有工具的审核规则恢复到初始状态吗?",
12241224
"resetWarningExtra": "这将丢失你添加的所有白名单和黑名单。",
12251225
"title": "工具调用审核",
1226-
"intro": "在 Bot 修改文件或运行命令前进行拦截审核。为不同工具配置独立规则:白名单自动放行,黑名单强制审核。",
1226+
"intro": "在 Bot 读取文件、修改文件或运行命令前进行拦截审核。为不同操作配置独立规则:白名单自动放行,黑名单强制审核。",
12271227
"tools": {
1228-
"write": "在容器内创建新文件",
1229-
"edit": "修改容器内已存在的文件",
1228+
"read": "读取工作区文件和列出目录",
1229+
"write": "写入文件、编辑文件和应用 patch",
12301230
"exec": "在容器内执行 shell 命令"
12311231
},
1232-
"toolDisabledHint": "该工具当前未要求审核。下方规则在打开开关后才会生效。",
1232+
"toolDisabledHint": "该操作当前未要求审核。下方规则在打开开关后才会生效。",
12331233
"bypass": "跳过审核(白名单)",
12341234
"bypassHint": "匹配以下模式的调用将直接放行,不弹审核。",
12351235
"mustReview": "必须审核(黑名单)",
@@ -1881,7 +1881,7 @@
18811881
"desktopEnabled": "桌面",
18821882
"desktopEnabledDescription": "启用 VNC 桌面。若缺少环境,Memoh 会在打开时自动配置 Debian/Ubuntu 或 Alpine 工作区。",
18831883
"toolApproval": "工具调用审核",
1884-
"toolApprovalDescription": "拦截并人工审核 write、edit、exec 等敏感工具的调用。你可以配置免审核的白名单路径或命令。",
1884+
"toolApprovalDescription": "拦截并人工审核 read、write、exec 三类操作。你可以配置免审核的白名单路径或命令。",
18851885
"acpSetupMode": "配置方式",
18861886
"acpSetupApiKey": "API Key",
18871887
"acpSetupOAuth": "OAuth",

apps/web/src/pages/bots/components/bot-tool-approval.vue

Lines changed: 12 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ import {
240240
Badge,
241241
} from '@memohai/ui'
242242
import {
243-
FilePlus2,
243+
Files,
244244
FilePen,
245245
SquareTerminal,
246246
ShieldCheck,
@@ -255,56 +255,24 @@ import { useQuery, useMutation, useQueryCache } from '@pinia/colada'
255255
import { getBotsByBotIdSettings, putBotsByBotIdSettings } from '@memohai/sdk'
256256
import type { SettingsSettings } from '@memohai/sdk'
257257
import { resolveApiErrorMessage } from '@/utils/api-error'
258+
import {
259+
defaultToolApprovalConfig,
260+
normalizeToolApprovalConfig,
261+
type ApprovalTool,
262+
type ToolApprovalConfig,
263+
type ToolApprovalExecPolicy,
264+
type ToolApprovalFilePolicy,
265+
} from './tool-approval-config'
258266
259267
const props = defineProps<{
260268
botId: string
261269
}>()
262270
263-
type ApprovalTool = 'write' | 'edit' | 'exec'
264-
265-
interface ToolApprovalFilePolicy {
266-
require_approval: boolean
267-
bypass_globs: string[]
268-
force_review_globs: string[]
269-
}
270-
271-
interface ToolApprovalExecPolicy {
272-
require_approval: boolean
273-
bypass_commands: string[]
274-
force_review_commands: string[]
275-
}
276-
277-
interface ToolApprovalConfig {
278-
enabled: boolean
279-
write: ToolApprovalFilePolicy
280-
edit: ToolApprovalFilePolicy
281-
exec: ToolApprovalExecPolicy
282-
}
283-
284-
const defaultToolApprovalConfig = (): ToolApprovalConfig => ({
285-
enabled: false,
286-
write: {
287-
require_approval: true,
288-
bypass_globs: ['/data/**', '/tmp/**'],
289-
force_review_globs: [],
290-
},
291-
edit: {
292-
require_approval: true,
293-
bypass_globs: ['/data/**', '/tmp/**'],
294-
force_review_globs: [],
295-
},
296-
exec: {
297-
require_approval: false,
298-
bypass_commands: [],
299-
force_review_commands: [],
300-
},
301-
})
302-
303-
const approvalTools: ApprovalTool[] = ['write', 'edit', 'exec']
271+
const approvalTools: ApprovalTool[] = ['read', 'write', 'exec']
304272
305273
const TOOL_META: Record<ApprovalTool, { icon: Component; descKey: string }> = {
306-
write: { icon: FilePlus2, descKey: 'bots.toolApproval.tools.write' },
307-
edit: { icon: FilePen, descKey: 'bots.toolApproval.tools.edit' },
274+
read: { icon: Files, descKey: 'bots.toolApproval.tools.read' },
275+
write: { icon: FilePen, descKey: 'bots.toolApproval.tools.write' },
308276
exec: { icon: SquareTerminal, descKey: 'bots.toolApproval.tools.exec' },
309277
}
310278
@@ -339,30 +307,6 @@ const form = reactive<{ tool_approval_config: ToolApprovalConfig }>({
339307
tool_approval_config: defaultToolApprovalConfig(),
340308
})
341309
342-
function normalizeToolApprovalConfig(raw: unknown): ToolApprovalConfig {
343-
const defaults = defaultToolApprovalConfig()
344-
if (!raw || typeof raw !== 'object') return defaults
345-
const value = raw as Partial<ToolApprovalConfig>
346-
return {
347-
enabled: value.enabled ?? defaults.enabled,
348-
write: {
349-
require_approval: value.write?.require_approval ?? defaults.write.require_approval,
350-
bypass_globs: value.write?.bypass_globs ?? defaults.write.bypass_globs,
351-
force_review_globs: value.write?.force_review_globs ?? defaults.write.force_review_globs,
352-
},
353-
edit: {
354-
require_approval: value.edit?.require_approval ?? defaults.edit.require_approval,
355-
bypass_globs: value.edit?.bypass_globs ?? defaults.edit.bypass_globs,
356-
force_review_globs: value.edit?.force_review_globs ?? defaults.edit.force_review_globs,
357-
},
358-
exec: {
359-
require_approval: value.exec?.require_approval ?? defaults.exec.require_approval,
360-
bypass_commands: value.exec?.bypass_commands ?? defaults.exec.bypass_commands,
361-
force_review_commands: value.exec?.force_review_commands ?? defaults.exec.force_review_commands,
362-
},
363-
}
364-
}
365-
366310
function toolApprovalPolicy(tool: ApprovalTool) {
367311
return form.tool_approval_config[tool]
368312
}
@@ -454,5 +398,3 @@ async function handleSave() {
454398
}
455399
}
456400
</script>
457-
458-
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { defaultToolApprovalConfig, normalizeToolApprovalConfig } from './tool-approval-config'
3+
4+
describe('normalizeToolApprovalConfig', () => {
5+
it('uses read/write/exec defaults for empty config', () => {
6+
expect(normalizeToolApprovalConfig(undefined)).toEqual(defaultToolApprovalConfig())
7+
})
8+
9+
it('merges legacy edit policy into write policy', () => {
10+
const normalized = normalizeToolApprovalConfig({
11+
enabled: true,
12+
write: {
13+
require_approval: false,
14+
bypass_globs: ['/data/**', '/workspace/cache/**'],
15+
force_review_globs: ['/workspace/secrets/**'],
16+
},
17+
edit: {
18+
require_approval: true,
19+
bypass_globs: ['/data/**', '/tmp/**'],
20+
force_review_globs: ['.env*'],
21+
},
22+
exec: {
23+
require_approval: false,
24+
bypass_commands: ['pwd'],
25+
force_review_commands: ['sudo *'],
26+
},
27+
})
28+
29+
expect(normalized.enabled).toBe(true)
30+
expect(normalized.write).toEqual({
31+
require_approval: true,
32+
bypass_globs: ['/data/**', '/workspace/cache/**', '/tmp/**'],
33+
force_review_globs: ['/workspace/secrets/**', '.env*'],
34+
})
35+
expect('edit' in normalized).toBe(false)
36+
})
37+
38+
it('keeps read independent from write compatibility merging', () => {
39+
const normalized = normalizeToolApprovalConfig({
40+
read: {
41+
require_approval: true,
42+
bypass_globs: ['/docs/**'],
43+
force_review_globs: ['/private/**'],
44+
},
45+
edit: {
46+
require_approval: true,
47+
bypass_globs: ['/tmp/**'],
48+
force_review_globs: [],
49+
},
50+
})
51+
52+
expect(normalized.read).toEqual({
53+
require_approval: true,
54+
bypass_globs: ['/docs/**'],
55+
force_review_globs: ['/private/**'],
56+
})
57+
expect(normalized.write.require_approval).toBe(true)
58+
expect(normalized.write.bypass_globs).toEqual(['/data/**', '/tmp/**'])
59+
})
60+
})

0 commit comments

Comments
 (0)