@kkelly-offical/kkcode 0.1.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.
- package/LICENSE +674 -0
- package/README.md +445 -0
- package/package.json +46 -0
- package/src/agent/agent.mjs +170 -0
- package/src/agent/custom-agent-loader.mjs +158 -0
- package/src/agent/generator.mjs +115 -0
- package/src/agent/prompt/architect.txt +36 -0
- package/src/agent/prompt/build-fixer.txt +71 -0
- package/src/agent/prompt/build.txt +101 -0
- package/src/agent/prompt/compaction.txt +12 -0
- package/src/agent/prompt/explore.txt +29 -0
- package/src/agent/prompt/guide.txt +40 -0
- package/src/agent/prompt/longagent.txt +178 -0
- package/src/agent/prompt/plan.txt +50 -0
- package/src/agent/prompt/researcher.txt +23 -0
- package/src/agent/prompt/reviewer.txt +44 -0
- package/src/agent/prompt/security-reviewer.txt +62 -0
- package/src/agent/prompt/tdd-guide.txt +84 -0
- package/src/agent/prompt/title.txt +8 -0
- package/src/command/custom-commands.mjs +57 -0
- package/src/commands/agent.mjs +71 -0
- package/src/commands/audit.mjs +77 -0
- package/src/commands/background.mjs +86 -0
- package/src/commands/chat.mjs +114 -0
- package/src/commands/command.mjs +41 -0
- package/src/commands/config.mjs +44 -0
- package/src/commands/doctor.mjs +148 -0
- package/src/commands/hook.mjs +29 -0
- package/src/commands/init.mjs +141 -0
- package/src/commands/longagent.mjs +100 -0
- package/src/commands/mcp.mjs +89 -0
- package/src/commands/permission.mjs +36 -0
- package/src/commands/prompt.mjs +42 -0
- package/src/commands/review.mjs +266 -0
- package/src/commands/rule.mjs +34 -0
- package/src/commands/session.mjs +235 -0
- package/src/commands/theme.mjs +98 -0
- package/src/commands/usage.mjs +91 -0
- package/src/config/defaults.mjs +195 -0
- package/src/config/import-config.mjs +76 -0
- package/src/config/load-config.mjs +76 -0
- package/src/config/schema.mjs +509 -0
- package/src/context.mjs +40 -0
- package/src/core/constants.mjs +46 -0
- package/src/core/errors.mjs +57 -0
- package/src/core/events.mjs +29 -0
- package/src/core/types.mjs +57 -0
- package/src/github/api.mjs +78 -0
- package/src/github/auth.mjs +286 -0
- package/src/github/flow.mjs +298 -0
- package/src/github/workspace.mjs +212 -0
- package/src/index.mjs +82 -0
- package/src/knowledge/api-design.txt +9 -0
- package/src/knowledge/cpp.txt +10 -0
- package/src/knowledge/docker.txt +10 -0
- package/src/knowledge/dotnet.txt +9 -0
- package/src/knowledge/electron.txt +10 -0
- package/src/knowledge/flutter.txt +10 -0
- package/src/knowledge/go.txt +9 -0
- package/src/knowledge/graphql.txt +10 -0
- package/src/knowledge/java.txt +9 -0
- package/src/knowledge/kotlin.txt +10 -0
- package/src/knowledge/loader.mjs +125 -0
- package/src/knowledge/next.txt +8 -0
- package/src/knowledge/node.txt +8 -0
- package/src/knowledge/nuxt.txt +9 -0
- package/src/knowledge/php.txt +10 -0
- package/src/knowledge/python.txt +10 -0
- package/src/knowledge/react-native.txt +10 -0
- package/src/knowledge/react.txt +9 -0
- package/src/knowledge/ruby.txt +11 -0
- package/src/knowledge/rust.txt +9 -0
- package/src/knowledge/svelte.txt +9 -0
- package/src/knowledge/swift.txt +10 -0
- package/src/knowledge/tailwind.txt +10 -0
- package/src/knowledge/testing.txt +8 -0
- package/src/knowledge/typescript.txt +8 -0
- package/src/knowledge/vue.txt +9 -0
- package/src/mcp/client-http.mjs +157 -0
- package/src/mcp/client-sse.mjs +286 -0
- package/src/mcp/client-stdio.mjs +451 -0
- package/src/mcp/registry.mjs +394 -0
- package/src/mcp/stdio-framing.mjs +127 -0
- package/src/orchestration/background-manager.mjs +358 -0
- package/src/orchestration/background-worker.mjs +245 -0
- package/src/orchestration/longagent-manager.mjs +116 -0
- package/src/orchestration/stage-scheduler.mjs +489 -0
- package/src/orchestration/subagent-router.mjs +62 -0
- package/src/orchestration/task-scheduler.mjs +74 -0
- package/src/permission/engine.mjs +92 -0
- package/src/permission/exec-policy.mjs +372 -0
- package/src/permission/prompt.mjs +39 -0
- package/src/permission/rules.mjs +120 -0
- package/src/permission/workspace-trust.mjs +44 -0
- package/src/plugin/builtin-hooks/console-warn.mjs +41 -0
- package/src/plugin/builtin-hooks/extract-patterns.mjs +75 -0
- package/src/plugin/builtin-hooks/post-edit-format.mjs +57 -0
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +61 -0
- package/src/plugin/builtin-hooks/strategic-compaction.mjs +38 -0
- package/src/plugin/hook-bus.mjs +154 -0
- package/src/provider/anthropic.mjs +389 -0
- package/src/provider/ollama.mjs +236 -0
- package/src/provider/openai-compatible.mjs +1 -0
- package/src/provider/openai.mjs +339 -0
- package/src/provider/retry-policy.mjs +68 -0
- package/src/provider/router.mjs +228 -0
- package/src/provider/sse.mjs +91 -0
- package/src/repl.mjs +2929 -0
- package/src/review/diff-parser.mjs +36 -0
- package/src/review/rejection-queue.mjs +62 -0
- package/src/review/review-store.mjs +21 -0
- package/src/review/risk-score.mjs +61 -0
- package/src/rules/load-rules.mjs +64 -0
- package/src/runtime.mjs +1 -0
- package/src/session/checkpoint.mjs +239 -0
- package/src/session/compaction.mjs +276 -0
- package/src/session/engine.mjs +225 -0
- package/src/session/instinct-manager.mjs +172 -0
- package/src/session/instruction-loader.mjs +25 -0
- package/src/session/longagent-plan.mjs +329 -0
- package/src/session/longagent-scaffold.mjs +100 -0
- package/src/session/longagent.mjs +1462 -0
- package/src/session/loop.mjs +905 -0
- package/src/session/memory-loader.mjs +75 -0
- package/src/session/project-context.mjs +367 -0
- package/src/session/prompt/anthropic.txt +151 -0
- package/src/session/prompt/beast.txt +37 -0
- package/src/session/prompt/max-steps.txt +6 -0
- package/src/session/prompt/plan.txt +9 -0
- package/src/session/prompt/qwen.txt +46 -0
- package/src/session/prompt-loader.mjs +18 -0
- package/src/session/recovery.mjs +52 -0
- package/src/session/store.mjs +503 -0
- package/src/session/system-prompt.mjs +260 -0
- package/src/session/task-validator.mjs +266 -0
- package/src/session/usability-gates.mjs +379 -0
- package/src/skill/builtin/backend-patterns.mjs +123 -0
- package/src/skill/builtin/commit.mjs +64 -0
- package/src/skill/builtin/debug.mjs +45 -0
- package/src/skill/builtin/frontend-patterns.mjs +120 -0
- package/src/skill/builtin/frontend.mjs +188 -0
- package/src/skill/builtin/init.mjs +220 -0
- package/src/skill/builtin/review.mjs +49 -0
- package/src/skill/builtin/security-checklist.mjs +80 -0
- package/src/skill/builtin/tdd.mjs +54 -0
- package/src/skill/generator.mjs +113 -0
- package/src/skill/registry.mjs +336 -0
- package/src/storage/audit-store.mjs +83 -0
- package/src/storage/event-log.mjs +82 -0
- package/src/storage/ghost-commit-store.mjs +235 -0
- package/src/storage/json-store.mjs +53 -0
- package/src/storage/paths.mjs +148 -0
- package/src/theme/color.mjs +64 -0
- package/src/theme/default-theme.mjs +29 -0
- package/src/theme/load-theme.mjs +71 -0
- package/src/theme/markdown.mjs +135 -0
- package/src/theme/schema.mjs +45 -0
- package/src/theme/status-bar.mjs +158 -0
- package/src/tool/audit-wrapper.mjs +38 -0
- package/src/tool/edit-transaction.mjs +126 -0
- package/src/tool/executor.mjs +109 -0
- package/src/tool/file-lock-manager.mjs +85 -0
- package/src/tool/git-auto.mjs +545 -0
- package/src/tool/git-full-auto.mjs +478 -0
- package/src/tool/image-util.mjs +276 -0
- package/src/tool/prompt/background_cancel.txt +1 -0
- package/src/tool/prompt/background_output.txt +1 -0
- package/src/tool/prompt/bash.txt +71 -0
- package/src/tool/prompt/codesearch.txt +18 -0
- package/src/tool/prompt/edit.txt +27 -0
- package/src/tool/prompt/enter_plan.txt +74 -0
- package/src/tool/prompt/exit_plan.txt +62 -0
- package/src/tool/prompt/glob.txt +33 -0
- package/src/tool/prompt/grep.txt +43 -0
- package/src/tool/prompt/list.txt +8 -0
- package/src/tool/prompt/multiedit.txt +20 -0
- package/src/tool/prompt/notebookedit.txt +21 -0
- package/src/tool/prompt/patch.txt +24 -0
- package/src/tool/prompt/question.txt +44 -0
- package/src/tool/prompt/read.txt +40 -0
- package/src/tool/prompt/task.txt +83 -0
- package/src/tool/prompt/todowrite.txt +117 -0
- package/src/tool/prompt/webfetch.txt +38 -0
- package/src/tool/prompt/websearch.txt +43 -0
- package/src/tool/prompt/write.txt +38 -0
- package/src/tool/prompt-loader.mjs +18 -0
- package/src/tool/question-prompt.mjs +86 -0
- package/src/tool/registry.mjs +1309 -0
- package/src/tool/task-tool.mjs +28 -0
- package/src/ui/activity-renderer.mjs +410 -0
- package/src/ui/repl-dashboard.mjs +357 -0
- package/src/usage/pricing.mjs +121 -0
- package/src/usage/usage-meter.mjs +113 -0
- package/src/util/git.mjs +496 -0
- package/src/util/template.mjs +10 -0
- package/src/util/yaml.mjs +100 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { PermissionError } from "../core/errors.mjs"
|
|
2
|
+
import { EVENT_TYPES } from "../core/constants.mjs"
|
|
3
|
+
import { EventBus } from "../core/events.mjs"
|
|
4
|
+
import { evaluatePermission } from "./rules.mjs"
|
|
5
|
+
import { askPermissionInteractive } from "./prompt.mjs"
|
|
6
|
+
|
|
7
|
+
const sessionAllow = new Map()
|
|
8
|
+
let workspaceTrusted = true
|
|
9
|
+
|
|
10
|
+
function cacheKey(tool, pattern) {
|
|
11
|
+
return `${tool}::${pattern || "*"}`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const PermissionEngine = {
|
|
15
|
+
setTrusted(value) { workspaceTrusted = Boolean(value) },
|
|
16
|
+
isTrusted() { return workspaceTrusted },
|
|
17
|
+
clearSession(sessionId) {
|
|
18
|
+
sessionAllow.delete(sessionId)
|
|
19
|
+
},
|
|
20
|
+
listSession(sessionId) {
|
|
21
|
+
return [...(sessionAllow.get(sessionId) || new Set())]
|
|
22
|
+
},
|
|
23
|
+
async check({ config, sessionId, tool, mode, pattern = "*", command = "", risk = 0, reason = "" }) {
|
|
24
|
+
if (!workspaceTrusted) throw new PermissionError("workspace not trusted — run /trust to enable tools")
|
|
25
|
+
const key = cacheKey(tool, pattern)
|
|
26
|
+
const set = sessionAllow.get(sessionId)
|
|
27
|
+
if (set?.has(key)) {
|
|
28
|
+
await EventBus.emit({
|
|
29
|
+
type: EVENT_TYPES.PERMISSION_DECIDED,
|
|
30
|
+
sessionId,
|
|
31
|
+
payload: { tool, decision: "allow_session", source: "cache" }
|
|
32
|
+
})
|
|
33
|
+
return { decision: "allow_session", granted: true }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const decision = evaluatePermission({ config, tool, mode, pattern, command, risk })
|
|
37
|
+
if (decision.action === "allow") {
|
|
38
|
+
await EventBus.emit({
|
|
39
|
+
type: EVENT_TYPES.PERMISSION_DECIDED,
|
|
40
|
+
sessionId,
|
|
41
|
+
payload: { tool, decision: "allow_once", source: decision.source }
|
|
42
|
+
})
|
|
43
|
+
return { decision: "allow_once", granted: true }
|
|
44
|
+
}
|
|
45
|
+
if (decision.action === "deny") {
|
|
46
|
+
await EventBus.emit({
|
|
47
|
+
type: EVENT_TYPES.PERMISSION_DECIDED,
|
|
48
|
+
sessionId,
|
|
49
|
+
payload: { tool, decision: "deny", source: decision.source }
|
|
50
|
+
})
|
|
51
|
+
throw new PermissionError(`permission denied for tool ${tool}`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await EventBus.emit({
|
|
55
|
+
type: EVENT_TYPES.PERMISSION_ASKED,
|
|
56
|
+
sessionId,
|
|
57
|
+
payload: { tool, mode, pattern, reason, risk }
|
|
58
|
+
})
|
|
59
|
+
const reply = await askPermissionInteractive({
|
|
60
|
+
tool,
|
|
61
|
+
sessionId,
|
|
62
|
+
reason,
|
|
63
|
+
defaultAction: config.permission?.non_tty_default || "deny"
|
|
64
|
+
})
|
|
65
|
+
if (reply === "allow_session") {
|
|
66
|
+
const next = sessionAllow.get(sessionId) || new Set()
|
|
67
|
+
next.add(key)
|
|
68
|
+
sessionAllow.set(sessionId, next)
|
|
69
|
+
await EventBus.emit({
|
|
70
|
+
type: EVENT_TYPES.PERMISSION_DECIDED,
|
|
71
|
+
sessionId,
|
|
72
|
+
payload: { tool, decision: "allow_session", source: "interactive" }
|
|
73
|
+
})
|
|
74
|
+
return { decision: "allow_session", granted: true }
|
|
75
|
+
}
|
|
76
|
+
if (reply === "allow_once") {
|
|
77
|
+
await EventBus.emit({
|
|
78
|
+
type: EVENT_TYPES.PERMISSION_DECIDED,
|
|
79
|
+
sessionId,
|
|
80
|
+
payload: { tool, decision: "allow_once", source: "interactive" }
|
|
81
|
+
})
|
|
82
|
+
return { decision: "allow_once", granted: true }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await EventBus.emit({
|
|
86
|
+
type: EVENT_TYPES.PERMISSION_DECIDED,
|
|
87
|
+
sessionId,
|
|
88
|
+
payload: { tool, decision: "deny", source: "interactive" }
|
|
89
|
+
})
|
|
90
|
+
throw new PermissionError(`permission denied for tool ${tool}`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 执行策略 (Execution Policy)
|
|
3
|
+
*
|
|
4
|
+
* 定义 AI Agent 执行命令的安全规则。
|
|
5
|
+
* 参考 Codex 的设计理念:
|
|
6
|
+
* - AI 可以修改文件(通过 patch)
|
|
7
|
+
* - AI 可以查看 Git 状态
|
|
8
|
+
* - AI 不能执行某些危险操作(如 git commit, git push)
|
|
9
|
+
*
|
|
10
|
+
* 安全原则:
|
|
11
|
+
* 1. 最小权限原则 - AI 只拥有完成任务必需的最小权限
|
|
12
|
+
* 2. 用户保留控制权 - 关键操作(commit/push)必须由用户手动执行
|
|
13
|
+
* 3. 透明可审计 - 所有策略决策都有明确的原因
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 决策类型
|
|
18
|
+
*/
|
|
19
|
+
export const Decision = {
|
|
20
|
+
ALLOW: "allow", // 允许执行
|
|
21
|
+
FORBID: "forbid", // 禁止执行
|
|
22
|
+
WARN: "warn", // 警告但允许(记录日志)
|
|
23
|
+
CONFIRM: "confirm" // 需要用户确认
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 规则匹配器
|
|
28
|
+
*/
|
|
29
|
+
function createMatcher(pattern) {
|
|
30
|
+
if (typeof pattern === "string") {
|
|
31
|
+
return (cmd) => cmd.includes(pattern)
|
|
32
|
+
}
|
|
33
|
+
if (pattern instanceof RegExp) {
|
|
34
|
+
return (cmd) => pattern.test(cmd)
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(pattern)) {
|
|
37
|
+
// 数组表示命令必须按顺序包含所有元素(用于匹配 git commit)
|
|
38
|
+
return (cmd) => {
|
|
39
|
+
const parts = cmd.toLowerCase().split(/\s+/)
|
|
40
|
+
let patternIdx = 0
|
|
41
|
+
for (const part of parts) {
|
|
42
|
+
if (patternIdx < pattern.length && part === pattern[patternIdx].toLowerCase()) {
|
|
43
|
+
patternIdx++
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return patternIdx === pattern.length
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return () => false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 执行策略规则
|
|
54
|
+
*/
|
|
55
|
+
const DEFAULT_RULES = [
|
|
56
|
+
// =========================================================================
|
|
57
|
+
// 高危操作 - 禁止
|
|
58
|
+
// =========================================================================
|
|
59
|
+
|
|
60
|
+
{
|
|
61
|
+
name: "forbid_git_commit",
|
|
62
|
+
pattern: ["git", "commit"],
|
|
63
|
+
decision: Decision.FORBID,
|
|
64
|
+
reason: "AI cannot directly create git commits. Use git_snapshot to create temporary snapshots instead. Users must manually run 'git commit' to finalize changes.",
|
|
65
|
+
category: "git_safety"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "forbid_git_push",
|
|
69
|
+
pattern: ["git", "push"],
|
|
70
|
+
decision: Decision.FORBID,
|
|
71
|
+
reason: "AI cannot push to remote repositories. Users must manually review and push changes.",
|
|
72
|
+
category: "git_safety"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "forbid_git_force_push",
|
|
76
|
+
pattern: [/git\s+push\s+.*--force/, /git\s+push\s+.*-f\b/],
|
|
77
|
+
decision: Decision.FORBID,
|
|
78
|
+
reason: "Force push is extremely dangerous and can overwrite remote history. Never allowed for AI.",
|
|
79
|
+
category: "git_safety"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "forbid_git_reset_hard",
|
|
83
|
+
pattern: ["git", "reset", "--hard"],
|
|
84
|
+
decision: Decision.FORBID,
|
|
85
|
+
reason: "Hard reset destroys uncommitted changes. Use git_restore with a snapshot ID instead.",
|
|
86
|
+
category: "git_safety"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "forbid_git_clean_fd",
|
|
90
|
+
pattern: [/git\s+clean\s+.*-f/, /git\s+clean\s+.*-d/],
|
|
91
|
+
decision: Decision.FORBID,
|
|
92
|
+
reason: "git clean -f/-d deletes untracked files and directories. Dangerous operation.",
|
|
93
|
+
category: "git_safety"
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
// =========================================================================
|
|
97
|
+
// 文件系统危险操作 - 禁止
|
|
98
|
+
// =========================================================================
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
name: "forbid_rm_rf",
|
|
102
|
+
pattern: [/rm\s+-rf?\s+\//, /rm\s+.*\*\s+.*\/\.\.?/],
|
|
103
|
+
decision: Decision.FORBID,
|
|
104
|
+
reason: "Dangerous file deletion pattern detected. Cannot delete system directories or use wildcards with relative path traversals.",
|
|
105
|
+
category: "fs_safety"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "forbid_dd_disk",
|
|
109
|
+
pattern: [/dd\s+.*of=\/dev\//],
|
|
110
|
+
decision: Decision.FORBID,
|
|
111
|
+
reason: "Direct disk write operations are forbidden.",
|
|
112
|
+
category: "fs_safety"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: "forbid_mkfs",
|
|
116
|
+
pattern: ["mkfs"],
|
|
117
|
+
decision: Decision.FORBID,
|
|
118
|
+
reason: "Filesystem formatting operations are forbidden.",
|
|
119
|
+
category: "fs_safety"
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
// =========================================================================
|
|
123
|
+
// 网络危险操作 - 禁止
|
|
124
|
+
// =========================================================================
|
|
125
|
+
|
|
126
|
+
{
|
|
127
|
+
name: "forbid_curl_pipe_sh",
|
|
128
|
+
pattern: [/curl\s+.*\|\s*(ba)?sh/, /wget\s+.*\|\s*(ba)?sh/],
|
|
129
|
+
decision: Decision.FORBID,
|
|
130
|
+
reason: "Piping curl/wget output directly to shell is dangerous and can execute arbitrary code.",
|
|
131
|
+
category: "network_safety"
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
// =========================================================================
|
|
135
|
+
// 权限提升操作 - 需要确认
|
|
136
|
+
// =========================================================================
|
|
137
|
+
|
|
138
|
+
{
|
|
139
|
+
name: "confirm_sudo",
|
|
140
|
+
pattern: ["sudo"],
|
|
141
|
+
decision: Decision.CONFIRM,
|
|
142
|
+
reason: "Command requires elevated privileges. Please confirm this is necessary.",
|
|
143
|
+
category: "privilege"
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: "confirm_chmod_777",
|
|
147
|
+
pattern: [/chmod\s+.*777/],
|
|
148
|
+
decision: Decision.WARN,
|
|
149
|
+
reason: "777 permissions grant full access to everyone. Consider using more restrictive permissions.",
|
|
150
|
+
category: "privilege"
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// =========================================================================
|
|
154
|
+
// Git 信息查看 - 允许
|
|
155
|
+
// =========================================================================
|
|
156
|
+
|
|
157
|
+
{
|
|
158
|
+
name: "allow_git_status",
|
|
159
|
+
pattern: ["git", "status"],
|
|
160
|
+
decision: Decision.ALLOW,
|
|
161
|
+
reason: "Reading git status is safe and necessary.",
|
|
162
|
+
category: "git_read"
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: "allow_git_log",
|
|
166
|
+
pattern: ["git", "log"],
|
|
167
|
+
decision: Decision.ALLOW,
|
|
168
|
+
reason: "Reading git history is safe.",
|
|
169
|
+
category: "git_read"
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: "allow_git_diff",
|
|
173
|
+
pattern: ["git", "diff"],
|
|
174
|
+
decision: Decision.ALLOW,
|
|
175
|
+
reason: "Reading git diff is safe.",
|
|
176
|
+
category: "git_read"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "allow_git_show",
|
|
180
|
+
pattern: ["git", "show"],
|
|
181
|
+
decision: Decision.ALLOW,
|
|
182
|
+
reason: "Reading commit details is safe.",
|
|
183
|
+
category: "git_read"
|
|
184
|
+
}
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 执行策略评估结果
|
|
189
|
+
*/
|
|
190
|
+
export class PolicyResult {
|
|
191
|
+
constructor(decision, rule, reason, category) {
|
|
192
|
+
this.decision = decision
|
|
193
|
+
this.rule = rule
|
|
194
|
+
this.reason = reason
|
|
195
|
+
this.category = category
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
isAllowed() {
|
|
199
|
+
return this.decision === Decision.ALLOW
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
isForbidden() {
|
|
203
|
+
return this.decision === Decision.FORBID
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
needsConfirmation() {
|
|
207
|
+
return this.decision === Decision.CONFIRM
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 评估命令是否符合执行策略
|
|
213
|
+
*
|
|
214
|
+
* @param {string} command - 要评估的命令
|
|
215
|
+
* @param {Object} options - 选项
|
|
216
|
+
* @param {boolean} [options.strict=false] - 严格模式(默认禁止任何未明确允许的操作)
|
|
217
|
+
* @param {Array} [options.customRules=[]] - 自定义规则
|
|
218
|
+
* @returns {PolicyResult}
|
|
219
|
+
*/
|
|
220
|
+
export function evaluateCommand(command, options = {}) {
|
|
221
|
+
const { strict = false, customRules = [] } = options
|
|
222
|
+
const rules = [...customRules, ...DEFAULT_RULES]
|
|
223
|
+
|
|
224
|
+
const normalizedCmd = String(command || "").trim()
|
|
225
|
+
|
|
226
|
+
for (const rule of rules) {
|
|
227
|
+
const matcher = createMatcher(rule.pattern)
|
|
228
|
+
if (matcher(normalizedCmd)) {
|
|
229
|
+
return new PolicyResult(
|
|
230
|
+
rule.decision,
|
|
231
|
+
rule.name,
|
|
232
|
+
rule.reason,
|
|
233
|
+
rule.category
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 严格模式:未匹配任何规则则禁止
|
|
239
|
+
if (strict) {
|
|
240
|
+
return new PolicyResult(
|
|
241
|
+
Decision.FORBID,
|
|
242
|
+
"strict_mode",
|
|
243
|
+
"Command not in allowlist (strict mode)",
|
|
244
|
+
"strict"
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 默认允许
|
|
249
|
+
return new PolicyResult(
|
|
250
|
+
Decision.ALLOW,
|
|
251
|
+
"default",
|
|
252
|
+
"No policy rules matched, allowing by default",
|
|
253
|
+
"default"
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 检查 bash 工具调用是否被允许
|
|
259
|
+
*
|
|
260
|
+
* @param {string} command - 命令
|
|
261
|
+
* @param {Object} config - 配置
|
|
262
|
+
* @returns {{allowed: boolean, reason?: string, warning?: string}}
|
|
263
|
+
*/
|
|
264
|
+
export function checkBashAllowed(command, config = {}) {
|
|
265
|
+
// 全自动化模式检查
|
|
266
|
+
const fullAuto = config.git_auto?.full_auto === true
|
|
267
|
+
const allowDangerous = config.git_auto?.allow_dangerous_ops === true
|
|
268
|
+
|
|
269
|
+
// 检查是否配置了全局禁止 git commit/push
|
|
270
|
+
// 全自动化模式下,如果 auto_commit/auto_push 启用,则允许
|
|
271
|
+
const autoCommit = fullAuto && config.git_auto?.auto_commit === true
|
|
272
|
+
const autoPush = fullAuto && config.git_auto?.auto_push === true
|
|
273
|
+
|
|
274
|
+
if (!autoCommit && config.git_auto?.forbid_commit !== false) {
|
|
275
|
+
const commitPattern = /^git\s+commit\b/i
|
|
276
|
+
if (commitPattern.test(command)) {
|
|
277
|
+
return {
|
|
278
|
+
allowed: false,
|
|
279
|
+
reason: "git commit is forbidden for AI. Use git_snapshot to create temporary snapshots, then manually commit when satisfied. Or enable git_auto.full_auto and git_auto.auto_commit for automatic commits."
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (!autoPush && config.git_auto?.forbid_push !== false) {
|
|
285
|
+
const pushPattern = /^git\s+push\b/i
|
|
286
|
+
if (pushPattern.test(command)) {
|
|
287
|
+
return {
|
|
288
|
+
allowed: false,
|
|
289
|
+
reason: "git push is forbidden for AI. Users must manually review and push changes. Or enable git_auto.full_auto and git_auto.auto_push for automatic pushes."
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 执行完整策略评估
|
|
295
|
+
const result = evaluateCommand(command)
|
|
296
|
+
|
|
297
|
+
if (result.isForbidden()) {
|
|
298
|
+
// 全自动化模式下,危险操作可能被允许(如果 allow_dangerous_ops 启用)
|
|
299
|
+
if (fullAuto && allowDangerous && result.category === "git_safety") {
|
|
300
|
+
return {
|
|
301
|
+
allowed: true,
|
|
302
|
+
warning: `Dangerous operation allowed in full-auto mode: ${result.reason}`
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (fullAuto && allowDangerous) {
|
|
307
|
+
return {
|
|
308
|
+
allowed: true,
|
|
309
|
+
warning: `Operation allowed in full-auto mode with dangerous_ops enabled: ${result.reason}`
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
allowed: false,
|
|
315
|
+
reason: result.reason
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return { allowed: true }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* 获取当前执行策略模式
|
|
324
|
+
*
|
|
325
|
+
* @param {Object} config - 配置
|
|
326
|
+
* @returns {{mode: string, restrictions: string[]}}
|
|
327
|
+
*/
|
|
328
|
+
export function getPolicyMode(config = {}) {
|
|
329
|
+
if (config.git_auto?.full_auto === true) {
|
|
330
|
+
const restrictions = []
|
|
331
|
+
if (config.git_auto?.auto_commit !== true) restrictions.push("no_auto_commit")
|
|
332
|
+
if (config.git_auto?.auto_push !== true) restrictions.push("no_auto_push")
|
|
333
|
+
if (config.git_auto?.allow_dangerous_ops !== true) restrictions.push("no_dangerous_ops")
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
mode: "full_auto",
|
|
337
|
+
restrictions: restrictions.length > 0 ? restrictions : ["none"]
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
mode: "safe",
|
|
343
|
+
restrictions: ["no_commit", "no_push", "no_dangerous_ops"]
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* 检查是否为全自动化模式
|
|
349
|
+
*/
|
|
350
|
+
export function isFullAutoMode(config = {}) {
|
|
351
|
+
return config.git_auto?.full_auto === true
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* 获取所有禁止的规则(用于文档)
|
|
356
|
+
*/
|
|
357
|
+
export function getForbiddenRules() {
|
|
358
|
+
return DEFAULT_RULES
|
|
359
|
+
.filter(r => r.decision === Decision.FORBID)
|
|
360
|
+
.map(r => ({
|
|
361
|
+
name: r.name,
|
|
362
|
+
pattern: Array.isArray(r.pattern) ? r.pattern.join(" ") : r.pattern.toString(),
|
|
363
|
+
reason: r.reason
|
|
364
|
+
}))
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* 检查是否为 Git 相关操作
|
|
369
|
+
*/
|
|
370
|
+
export function isGitCommand(command) {
|
|
371
|
+
return /^git\s+/i.test(String(command || ""))
|
|
372
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { stdin as input, stdout as output } from "node:process"
|
|
2
|
+
import { createInterface } from "node:readline/promises"
|
|
3
|
+
|
|
4
|
+
let customPromptHandler = null
|
|
5
|
+
|
|
6
|
+
export function setPermissionPromptHandler(handler) {
|
|
7
|
+
customPromptHandler = typeof handler === "function" ? handler : null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function askPermissionInteractive({ tool, sessionId, reason = "", defaultAction = "deny" }) {
|
|
11
|
+
if (customPromptHandler) {
|
|
12
|
+
const answer = await customPromptHandler({
|
|
13
|
+
tool,
|
|
14
|
+
sessionId,
|
|
15
|
+
reason,
|
|
16
|
+
defaultAction
|
|
17
|
+
})
|
|
18
|
+
if (["allow_once", "allow_session", "deny"].includes(answer)) return answer
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
22
|
+
if (defaultAction === "allow" || defaultAction === "allow_once") return "allow_once"
|
|
23
|
+
return "deny"
|
|
24
|
+
}
|
|
25
|
+
const rl = createInterface({ input, output })
|
|
26
|
+
try {
|
|
27
|
+
console.log("")
|
|
28
|
+
console.log(`Permission requested for tool: ${tool}`)
|
|
29
|
+
console.log(`session: ${sessionId}`)
|
|
30
|
+
if (reason) console.log(`reason: ${reason}`)
|
|
31
|
+
console.log("Choices: [1] allow once [2] allow session [3] deny")
|
|
32
|
+
const answer = (await rl.question("> ")).trim().toLowerCase()
|
|
33
|
+
if (["1", "allow", "allow_once", "once", "y", "yes"].includes(answer)) return "allow_once"
|
|
34
|
+
if (["2", "session", "allow_session", "always"].includes(answer)) return "allow_session"
|
|
35
|
+
return "deny"
|
|
36
|
+
} finally {
|
|
37
|
+
rl.close()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glob-style pattern matching supporting:
|
|
3
|
+
* * — any chars except /
|
|
4
|
+
* ** — any chars including /
|
|
5
|
+
* ? — single char
|
|
6
|
+
* !pat — negation (returns false when inner pattern matches)
|
|
7
|
+
*/
|
|
8
|
+
function globToRegex(pattern) {
|
|
9
|
+
let src = ""
|
|
10
|
+
let i = 0
|
|
11
|
+
while (i < pattern.length) {
|
|
12
|
+
const ch = pattern[i]
|
|
13
|
+
if (ch === "*" && pattern[i + 1] === "*") {
|
|
14
|
+
src += ".*"
|
|
15
|
+
i += 2
|
|
16
|
+
if (pattern[i] === "/") i++ // skip trailing slash after **
|
|
17
|
+
} else if (ch === "*") {
|
|
18
|
+
src += "[^/]*"
|
|
19
|
+
i++
|
|
20
|
+
} else if (ch === "?") {
|
|
21
|
+
src += "[^/]"
|
|
22
|
+
i++
|
|
23
|
+
} else if (".+^${}()|[]\\".includes(ch)) {
|
|
24
|
+
src += `\\${ch}`
|
|
25
|
+
i++
|
|
26
|
+
} else {
|
|
27
|
+
src += ch
|
|
28
|
+
i++
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return new RegExp(`^${src}$`, "i")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function matchGlob(value, pattern) {
|
|
35
|
+
if (!pattern || pattern === "*") return true
|
|
36
|
+
const str = String(value || "")
|
|
37
|
+
const negate = pattern.startsWith("!")
|
|
38
|
+
const pat = negate ? pattern.slice(1) : pattern
|
|
39
|
+
const matched = globToRegex(pat).test(str)
|
|
40
|
+
return negate ? !matched : matched
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Match a list of glob patterns (OR logic, negations filter out).
|
|
45
|
+
* Single string is treated as one pattern.
|
|
46
|
+
*/
|
|
47
|
+
function matchPatterns(value, patterns) {
|
|
48
|
+
if (!patterns) return true
|
|
49
|
+
const list = Array.isArray(patterns) ? patterns : [patterns]
|
|
50
|
+
if (!list.length) return true
|
|
51
|
+
const positives = list.filter((p) => !String(p).startsWith("!"))
|
|
52
|
+
const negatives = list.filter((p) => String(p).startsWith("!"))
|
|
53
|
+
// If any negative matches, reject
|
|
54
|
+
for (const neg of negatives) {
|
|
55
|
+
if (!matchGlob(value, neg)) return false // negation matched → excluded
|
|
56
|
+
}
|
|
57
|
+
// If no positive patterns, pass (only negatives were specified)
|
|
58
|
+
if (!positives.length) return true
|
|
59
|
+
// At least one positive must match
|
|
60
|
+
return positives.some((p) => matchGlob(value, p))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Match command prefix for bash tool rules.
|
|
65
|
+
* command_prefix: "npm test" matches "npm test --verbose"
|
|
66
|
+
* command_prefix: ["git *", "npm *"] matches any git or npm command
|
|
67
|
+
*/
|
|
68
|
+
function matchCommandPrefix(command, prefixes) {
|
|
69
|
+
if (!prefixes) return true
|
|
70
|
+
const list = Array.isArray(prefixes) ? prefixes : [prefixes]
|
|
71
|
+
if (!list.length) return true
|
|
72
|
+
const cmd = String(command || "").trim()
|
|
73
|
+
return list.some((prefix) => {
|
|
74
|
+
if (prefix.includes("*")) return matchGlob(cmd, prefix)
|
|
75
|
+
return cmd === prefix || cmd.startsWith(`${prefix} `)
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function matchRule(rule, input) {
|
|
80
|
+
if (rule.tool !== "*" && rule.tool !== input.tool) return false
|
|
81
|
+
if (Array.isArray(rule.modes) && rule.modes.length && !rule.modes.includes(input.mode)) return false
|
|
82
|
+
if (rule.risk && input.risk && Number(input.risk) < Number(rule.risk)) return false
|
|
83
|
+
|
|
84
|
+
// File glob matching (for read/write/edit/glob/grep tools)
|
|
85
|
+
if (rule.file_patterns) {
|
|
86
|
+
if (!matchPatterns(input.pattern || "", rule.file_patterns)) return false
|
|
87
|
+
} else if (rule.pattern) {
|
|
88
|
+
// Legacy single-pattern support
|
|
89
|
+
if (!matchGlob(input.pattern || input.tool, rule.pattern)) return false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Command prefix matching (for bash tool)
|
|
93
|
+
if (rule.command_prefix && input.tool === "bash") {
|
|
94
|
+
if (!matchCommandPrefix(input.command || input.pattern || "", rule.command_prefix)) return false
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function evaluatePermission({ config, tool, mode, pattern = "*", command = "", risk = 0 }) {
|
|
101
|
+
const permission = config.permission || { default_policy: "ask", rules: [] }
|
|
102
|
+
const rules = Array.isArray(permission.rules) ? permission.rules : []
|
|
103
|
+
for (const rule of rules) {
|
|
104
|
+
if (matchRule(rule, { tool, mode, pattern, command, risk })) {
|
|
105
|
+
return {
|
|
106
|
+
action: rule.action,
|
|
107
|
+
source: "rule",
|
|
108
|
+
rule
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
action: permission.default_policy || "ask",
|
|
114
|
+
source: "default",
|
|
115
|
+
rule: null
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Exported for testing
|
|
120
|
+
export { matchGlob, matchPatterns, matchCommandPrefix }
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { createInterface } from "node:readline"
|
|
4
|
+
import { trustFilePath } from "../storage/paths.mjs"
|
|
5
|
+
|
|
6
|
+
async function readTrustFile(cwd) {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(await readFile(trustFilePath(cwd), "utf8"))
|
|
9
|
+
} catch {
|
|
10
|
+
return null
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function checkWorkspaceTrust({ cwd, cliTrust = false, isTTY = process.stdin.isTTY }) {
|
|
15
|
+
if (cliTrust) {
|
|
16
|
+
await persistTrust(cwd)
|
|
17
|
+
return { trusted: true }
|
|
18
|
+
}
|
|
19
|
+
const data = await readTrustFile(cwd)
|
|
20
|
+
if (data?.trusted === true) return { trusted: true }
|
|
21
|
+
if (!isTTY) return { trusted: false }
|
|
22
|
+
|
|
23
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
24
|
+
const answer = await new Promise((resolve) => {
|
|
25
|
+
rl.question("Do you trust this workspace? [y/N] ", (ans) => { rl.close(); resolve(ans) })
|
|
26
|
+
})
|
|
27
|
+
if (/^y(es)?$/i.test(String(answer).trim())) {
|
|
28
|
+
await persistTrust(cwd)
|
|
29
|
+
return { trusted: true }
|
|
30
|
+
}
|
|
31
|
+
return { trusted: false }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function persistTrust(cwd) {
|
|
35
|
+
const file = trustFilePath(cwd)
|
|
36
|
+
await mkdir(path.dirname(file), { recursive: true })
|
|
37
|
+
await writeFile(file, JSON.stringify({ trusted: true, trustedAt: new Date().toISOString(), cwd }, null, 2), "utf8")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function revokeTrust(cwd) {
|
|
41
|
+
const file = trustFilePath(cwd)
|
|
42
|
+
await mkdir(path.dirname(file), { recursive: true })
|
|
43
|
+
await writeFile(file, JSON.stringify({ trusted: false }, null, 2), "utf8")
|
|
44
|
+
}
|