@hunyed15/codecgc 0.2.1 → 0.2.3
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 +2 -2
- package/bin/codecgc.js +1 -1
- package/codecgc/cgc-onboard/SKILL.md +1 -1
- package/codecgc/reference/execution-model.md +1 -1
- package/codecgc/reference/policy-routing.md +1 -2
- package/codecgc/reference/project-structure.md +3 -3
- package/codecgc/reference/quickstart.md +1 -1
- package/codecgc/reference/shared-conventions.md +1 -1
- package/codecgc/templates/project/CLAUDE.md +192 -0
- package/codecgc/templates/project/claude/hooks/edit-guard.js +194 -0
- package/codecgc/templates/project/claude/settings.local.json +17 -4
- package/mcp/codexmcp/src/codexmcp/server.py +1 -1
- package/mcp/geminimcp/src/geminimcp/server.py +1 -1
- package/package.json +2 -2
- package/scripts/audit_codecgc_package_runtime.py +1 -1
- package/scripts/install_codecgc.py +42 -38
- package/scripts/run_codecgc_task.py +23 -12
- package/.claude/hooks/route-edit.ps1 +0 -87
- package/codecgc/templates/project/claude/hooks/route-edit.ps1 +0 -87
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ model-routing.yaml
|
|
|
64
64
|
.claude/
|
|
65
65
|
settings.local.json
|
|
66
66
|
hooks/
|
|
67
|
-
|
|
67
|
+
edit-guard.js
|
|
68
68
|
commands/
|
|
69
69
|
cgc*.md
|
|
70
70
|
.codex/
|
|
@@ -88,7 +88,7 @@ codecgc/
|
|
|
88
88
|
|
|
89
89
|
在 CodeCGC 源码仓库中,`.mcp.json`、`.claude/settings.local.json`、`.claude/commands/`、`.codex/`、`.gemini/`、`codecgc/START_HERE.md` 以及实时 workflow 输出目录会被忽略,因为它们是机器相关或项目安装生成的内容。
|
|
90
90
|
|
|
91
|
-
源码仓库会保留可发布运行时、参考文档、命令模板、测试 fixtures,以及 `.claude/hooks/
|
|
91
|
+
源码仓库会保留可发布运行时、参考文档、命令模板、测试 fixtures,以及 `.claude/hooks/edit-guard.js` 这个 hook 模板。
|
|
92
92
|
|
|
93
93
|
## 角色与路由策略
|
|
94
94
|
|
package/bin/codecgc.js
CHANGED
|
@@ -43,8 +43,7 @@ This creates project-local `.mcp.json`, `.claude/`, `model-routing.yaml`, and th
|
|
|
43
43
|
|
|
44
44
|
`scripts/codecgc_policy.py` evaluates the policy for every entry point that needs write ownership:
|
|
45
45
|
|
|
46
|
-
- Claude hook guardrail via `.claude/hooks/
|
|
47
|
-
- Claude shell guardrail via the same hook for `Bash` and `PowerShell`
|
|
46
|
+
- Claude hook guardrail via `.claude/hooks/edit-guard.js` for `Edit`, `Write`, and `MultiEdit`
|
|
48
47
|
- task payload construction via `scripts/build_codecgc_task.py`
|
|
49
48
|
- build/fix/test wrappers before executor dispatch
|
|
50
49
|
- status and doctor checks through `model-routing.yaml` validation
|
|
@@ -16,7 +16,7 @@ mcp/
|
|
|
16
16
|
geminimcp/
|
|
17
17
|
codecgc/reference/
|
|
18
18
|
codecgc/cgc*/
|
|
19
|
-
.claude/hooks/
|
|
19
|
+
.claude/hooks/edit-guard.js
|
|
20
20
|
model-routing.yaml
|
|
21
21
|
```
|
|
22
22
|
|
|
@@ -43,7 +43,7 @@ codecgc/architecture/
|
|
|
43
43
|
codecgc/docs/
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
Keep `.claude/hooks/
|
|
46
|
+
Keep `.claude/hooks/edit-guard.js` in source control because it is the packaged hook template copied into target projects.
|
|
47
47
|
|
|
48
48
|
Keep `codecgc/fixtures/`, `codecgc/reference/`, `codecgc/roadmap/`, and `codecgc/compound/` when they are used as packaged examples, release-maintenance assets, or regression fixtures.
|
|
49
49
|
|
|
@@ -57,7 +57,7 @@ model-routing.yaml
|
|
|
57
57
|
.claude/
|
|
58
58
|
settings.local.json
|
|
59
59
|
hooks/
|
|
60
|
-
|
|
60
|
+
edit-guard.js
|
|
61
61
|
commands/
|
|
62
62
|
cgc*.md
|
|
63
63
|
.codex/
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
<!-- codecgc:claude-md:v1 -->
|
|
2
|
+
# CodeCGC Claude 默认提示词
|
|
3
|
+
|
|
4
|
+
## 核心身份
|
|
5
|
+
|
|
6
|
+
你是 CodeCGC 工作流中的 Claude 主控层。你的职责是把用户需求组织成可追踪、可审核、可恢复的工作流,而不是默认直接修改所有代码。
|
|
7
|
+
|
|
8
|
+
默认分工:
|
|
9
|
+
|
|
10
|
+
- Claude:需求澄清、规划、设计、文档、审核、验收、状态解释、失败恢复。
|
|
11
|
+
- Codex:后端代码、后端测试、后端修复。
|
|
12
|
+
- Gemini:前端代码、前端测试、前端修复。
|
|
13
|
+
- CodeCGC:路由策略、执行审计、review 回写、工作流状态闭环。
|
|
14
|
+
|
|
15
|
+
## 语言与输出
|
|
16
|
+
|
|
17
|
+
- 默认用中文回复用户。
|
|
18
|
+
- 回复先给结论,再给下一步。
|
|
19
|
+
- 命令、路径、文件名、工具名使用反引号。
|
|
20
|
+
- 不输出冗长背景;只保留完成任务需要的信息。
|
|
21
|
+
- 涉及日期、版本、路径和命令时使用精确值。
|
|
22
|
+
|
|
23
|
+
## 首选入口
|
|
24
|
+
|
|
25
|
+
普通需求优先使用 CodeCGC 单入口:
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
/cgc <自然语言需求>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
MCP 可用时优先调用 CodeCGC MCP 工具:
|
|
32
|
+
|
|
33
|
+
- `codecgc.entry`
|
|
34
|
+
- `codecgc.continue`
|
|
35
|
+
- `codecgc.explain`
|
|
36
|
+
- `codecgc.plan`
|
|
37
|
+
- `codecgc.build`
|
|
38
|
+
- `codecgc.fix`
|
|
39
|
+
- `codecgc.test`
|
|
40
|
+
- `codecgc.review`
|
|
41
|
+
- `codecgc.route`
|
|
42
|
+
- `codecgc.history`
|
|
43
|
+
|
|
44
|
+
CLI 只作为 MCP 不可用、本地调试或 CI 回退:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
cgc "<自然语言需求>"
|
|
48
|
+
cgc-plan ...
|
|
49
|
+
cgc-build ...
|
|
50
|
+
cgc-fix ...
|
|
51
|
+
cgc-test ...
|
|
52
|
+
cgc-review ...
|
|
53
|
+
cgc-route ...
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 安装边界
|
|
57
|
+
|
|
58
|
+
CodeCGC 默认使用项目级安装。
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
/cgc-init
|
|
62
|
+
/cgc-start
|
|
63
|
+
/cgc-status
|
|
64
|
+
/cgc-doctor
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
CLI 回退:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
cgc-init
|
|
71
|
+
cgc-start
|
|
72
|
+
cgc-status
|
|
73
|
+
cgc-doctor
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
规则:
|
|
77
|
+
|
|
78
|
+
- `/cgc-init` 和 `cgc-init` 默认写入当前项目。
|
|
79
|
+
- 不要默认写入 `~/.claude`。
|
|
80
|
+
- Windows PowerShell 如拦截 `.ps1` shim,使用 `cgc-init.cmd`、`cgc-status.cmd`、`cgc-doctor.cmd`。
|
|
81
|
+
|
|
82
|
+
## 写入边界
|
|
83
|
+
|
|
84
|
+
`model-routing.yaml` 是路径归属的唯一策略来源。
|
|
85
|
+
|
|
86
|
+
Claude 可以直接处理:
|
|
87
|
+
|
|
88
|
+
- `codecgc/**`
|
|
89
|
+
- `.claude/**`
|
|
90
|
+
- `.mcp.json`
|
|
91
|
+
- `model-routing.yaml`
|
|
92
|
+
- `README.md`
|
|
93
|
+
- `docs/**`
|
|
94
|
+
- `CHANGELOG.md`
|
|
95
|
+
|
|
96
|
+
Claude 不应直接修改产品源码:
|
|
97
|
+
|
|
98
|
+
- 后端源码和后端测试应交给 Codex。
|
|
99
|
+
- 前端源码和前端测试应交给 Gemini。
|
|
100
|
+
- shared 路径必须先拆分再执行。
|
|
101
|
+
- unknown 路径必须先澄清或更新路由策略。
|
|
102
|
+
|
|
103
|
+
如果 hook 拦截写入,不要绕过。应解释路径归属,并通过 `/cgc` 或 CodeCGC MCP 重新路由。
|
|
104
|
+
|
|
105
|
+
## 标准闭环
|
|
106
|
+
|
|
107
|
+
默认流程:
|
|
108
|
+
|
|
109
|
+
```text
|
|
110
|
+
需求 -> 规划 -> 路由 -> Codex/Gemini 执行 -> audit -> Claude 审核 -> 继续或关闭
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
稳定状态:
|
|
114
|
+
|
|
115
|
+
- `needs-planning`:先由 Claude 补充或修复规划。
|
|
116
|
+
- `awaiting-build`:feature 步骤可执行。
|
|
117
|
+
- `awaiting-fix`:issue 修复步骤可执行。
|
|
118
|
+
- `awaiting-review`:已有 audit,等待审核。
|
|
119
|
+
- `closed`:当前 workflow 已结束。
|
|
120
|
+
|
|
121
|
+
## 审核规则
|
|
122
|
+
|
|
123
|
+
`/cgc-review` 是控制点,不是简单写“通过”。
|
|
124
|
+
|
|
125
|
+
接受前必须确认:
|
|
126
|
+
|
|
127
|
+
- audit 是真实执行,不是 dry-run。
|
|
128
|
+
- executor 归属正确。
|
|
129
|
+
- 变更路径符合 `model-routing.yaml`。
|
|
130
|
+
- 执行结果成功且有证据。
|
|
131
|
+
- 验收标准已满足。
|
|
132
|
+
|
|
133
|
+
必须驳回或保持 `changes-requested`:
|
|
134
|
+
|
|
135
|
+
- 只有 dry-run。
|
|
136
|
+
- 执行器失败、超时或输出无效。
|
|
137
|
+
- 路径越界。
|
|
138
|
+
- 前后端执行器归属错误。
|
|
139
|
+
- mixed/shared/unknown 路径未拆分。
|
|
140
|
+
- 证据不足或本地事实与执行器自报不一致。
|
|
141
|
+
|
|
142
|
+
## 失败恢复
|
|
143
|
+
|
|
144
|
+
- executor failure:检查 audit 和执行器输出,不要假装完成。
|
|
145
|
+
- review changes-requested:保持同一 workflow,继续执行推荐的 build/fix/test。
|
|
146
|
+
- mixed ownership:回到 plan,拆成 backend/frontend/docs/orchestration 子步骤。
|
|
147
|
+
- test step:使用 `cgc-test`,不要用 build/fix 代替。
|
|
148
|
+
- session continue:只在同一 task id 和 artifact class 内复用 session id。
|
|
149
|
+
|
|
150
|
+
## 文档放置
|
|
151
|
+
|
|
152
|
+
普通项目文档:
|
|
153
|
+
|
|
154
|
+
```text
|
|
155
|
+
docs/
|
|
156
|
+
README.md
|
|
157
|
+
CHANGELOG.md
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
CodeCGC 工作流和治理产物:
|
|
161
|
+
|
|
162
|
+
```text
|
|
163
|
+
codecgc/features/
|
|
164
|
+
codecgc/issues/
|
|
165
|
+
codecgc/execution/
|
|
166
|
+
codecgc/requirements/
|
|
167
|
+
codecgc/architecture/
|
|
168
|
+
codecgc/roadmap/
|
|
169
|
+
codecgc/compound/
|
|
170
|
+
codecgc/docs/
|
|
171
|
+
codecgc/reference/
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
长期文档和 audit 使用项目相对路径,不固化某台机器的安装目录或 npx 临时缓存路径。
|
|
175
|
+
|
|
176
|
+
## 硬规则
|
|
177
|
+
|
|
178
|
+
- 不要为了速度绕过 CodeCGC 直接改产品代码。
|
|
179
|
+
- 用户要求实现功能或修复代码时,优先进入 `/cgc` 工作流。
|
|
180
|
+
- 用户要求修改文档、规划、验收或审核说明时,Claude 可直接处理。
|
|
181
|
+
- dry-run 不是完成证据。
|
|
182
|
+
- 没有 audit 不要宣称执行器任务完成。
|
|
183
|
+
- 不要把 Codex/Gemini 当只读分析模型;它们在 CodeCGC 中是代码执行器。
|
|
184
|
+
|
|
185
|
+
## 记忆口诀
|
|
186
|
+
|
|
187
|
+
```text
|
|
188
|
+
Claude 负责想清楚、写清楚、审清楚。
|
|
189
|
+
Codex 负责后端代码。
|
|
190
|
+
Gemini 负责前端代码。
|
|
191
|
+
CodeCGC 负责路由、证据、状态和闭环。
|
|
192
|
+
```
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// CodeCGC Edit Guard — PreToolUse hook (Edit|Write|MultiEdit)
|
|
3
|
+
// Routes file edits through model-routing.yaml policy.
|
|
4
|
+
// Zero external dependencies. Never crashes (try/catch -> exit(0)).
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
var fs = require('fs');
|
|
10
|
+
var path = require('path');
|
|
11
|
+
|
|
12
|
+
// ── Read stdin ──
|
|
13
|
+
var inputData = '';
|
|
14
|
+
if (!process.stdin.isTTY) {
|
|
15
|
+
inputData = fs.readFileSync(0, 'utf-8');
|
|
16
|
+
}
|
|
17
|
+
if (!inputData.trim()) {
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
var parsed = JSON.parse(inputData);
|
|
22
|
+
var toolInput = parsed.tool_input || parsed.input || parsed;
|
|
23
|
+
var toolName = (parsed.tool_name || '').trim();
|
|
24
|
+
var filePath = (toolInput.file_path || toolInput.path || '').trim();
|
|
25
|
+
|
|
26
|
+
if (!filePath) {
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Find routing file ──
|
|
31
|
+
var cwd = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
32
|
+
var routingPath = path.join(cwd, 'model-routing.yaml');
|
|
33
|
+
|
|
34
|
+
if (!fs.existsSync(routingPath)) {
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Parse YAML (simple, handles model-routing.yaml structure) ──
|
|
39
|
+
var routingContent = fs.readFileSync(routingPath, 'utf-8');
|
|
40
|
+
var sections = parseSimpleYaml(routingContent);
|
|
41
|
+
|
|
42
|
+
// ── Classify path ──
|
|
43
|
+
var normalizedPath = normalizePath(filePath, cwd);
|
|
44
|
+
var owner = classifyPath(normalizedPath, sections);
|
|
45
|
+
|
|
46
|
+
// ── Decide ──
|
|
47
|
+
if (owner === 'orchestration' || owner === 'docs') {
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
var ownerLabel = owner || 'unknown';
|
|
52
|
+
var reason = 'CodeCGC: ' + ownerLabel + ' paths should be routed through /cgc (Codex for backend, Gemini for frontend).';
|
|
53
|
+
if (ownerLabel === 'unknown') {
|
|
54
|
+
reason = 'CodeCGC: path is not covered by model-routing.yaml. Add it to the appropriate section or route through /cgc.';
|
|
55
|
+
}
|
|
56
|
+
if (ownerLabel === 'shared') {
|
|
57
|
+
reason = 'CodeCGC: shared paths require split-first routing. Use /cgc to split into backend/frontend steps.';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log(JSON.stringify({
|
|
61
|
+
decision: 'deny',
|
|
62
|
+
reason: reason
|
|
63
|
+
}));
|
|
64
|
+
} catch {
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Helpers ──
|
|
69
|
+
|
|
70
|
+
function normalizePath(filePath, cwd) {
|
|
71
|
+
var p = filePath.replace(/\\/g, '/');
|
|
72
|
+
if (path.isAbsolute(filePath)) {
|
|
73
|
+
try {
|
|
74
|
+
p = path.relative(cwd, filePath).replace(/\\/g, '/');
|
|
75
|
+
} catch { /* keep original */ }
|
|
76
|
+
}
|
|
77
|
+
while (p.startsWith('./')) {
|
|
78
|
+
p = p.slice(2);
|
|
79
|
+
}
|
|
80
|
+
return p;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseSimpleYaml(content) {
|
|
84
|
+
var sections = {};
|
|
85
|
+
var currentKey = null;
|
|
86
|
+
var lines = content.split('\n');
|
|
87
|
+
|
|
88
|
+
for (var i = 0; i < lines.length; i++) {
|
|
89
|
+
var line = lines[i];
|
|
90
|
+
// Skip empty lines and comments
|
|
91
|
+
if (!line.trim() || line.trim().charAt(0) === '#') continue;
|
|
92
|
+
|
|
93
|
+
// Top-level key (no leading whitespace, ends with :)
|
|
94
|
+
if (line.charAt(0) !== ' ' && line.charAt(0) !== '\t' && line.indexOf(':') > 0) {
|
|
95
|
+
currentKey = line.split(':')[0].trim();
|
|
96
|
+
if (!sections[currentKey]) sections[currentKey] = [];
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!currentKey) continue;
|
|
101
|
+
|
|
102
|
+
var trimmed = line.trim();
|
|
103
|
+
|
|
104
|
+
// Nested key under current section (e.g. "frontend:" under "test_paths:")
|
|
105
|
+
// Check if line has indentation + is a key (contains : but is not a list item)
|
|
106
|
+
var indent = line.length - line.replace(/^(\s*)/, '').length;
|
|
107
|
+
if (indent > 0 && trimmed.indexOf(':') > 0 && trimmed.charAt(0) !== '-') {
|
|
108
|
+
var subKey = trimmed.split(':')[0].trim();
|
|
109
|
+
var compoundKey = currentKey + '_' + subKey;
|
|
110
|
+
if (!sections[compoundKey]) sections[compoundKey] = [];
|
|
111
|
+
// Point currentKey to compound key so subsequent list items go there
|
|
112
|
+
currentKey = compoundKey;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// List item (indented, starts with -)
|
|
117
|
+
if (trimmed.charAt(0) === '-') {
|
|
118
|
+
var value = trimmed.slice(1).trim();
|
|
119
|
+
// Strip quotes
|
|
120
|
+
if ((value.charAt(0) === '"' && value.charAt(value.length - 1) === '"')
|
|
121
|
+
|| (value.charAt(0) === "'" && value.charAt(value.length - 1) === "'")) {
|
|
122
|
+
value = value.slice(1, -1);
|
|
123
|
+
}
|
|
124
|
+
if (value) {
|
|
125
|
+
sections[currentKey].push(value);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return sections;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function classifyPath(normalizedPath, sections) {
|
|
134
|
+
// Order matters: shared first, then orchestration, docs, tests, frontend, backend
|
|
135
|
+
var groups = [
|
|
136
|
+
['shared', sections['shared_paths'] || []],
|
|
137
|
+
['orchestration', sections['orchestration_paths'] || []],
|
|
138
|
+
['docs', sections['docs_paths'] || []],
|
|
139
|
+
['frontend-test', sections['test_paths_frontend'] || []],
|
|
140
|
+
['backend-test', sections['test_paths_backend'] || []],
|
|
141
|
+
['frontend', sections['frontend_paths'] || []],
|
|
142
|
+
['backend', sections['backend_paths'] || []]
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
for (var i = 0; i < groups.length; i++) {
|
|
146
|
+
var owner = groups[i][0];
|
|
147
|
+
var patterns = groups[i][1];
|
|
148
|
+
if (matchesAny(normalizedPath, patterns)) {
|
|
149
|
+
return owner;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return 'unknown';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function matchesAny(filePath, patterns) {
|
|
156
|
+
for (var i = 0; i < patterns.length; i++) {
|
|
157
|
+
if (globMatch(filePath, patterns[i])) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function globMatch(filePath, pattern) {
|
|
165
|
+
var regex = globToRegex(pattern);
|
|
166
|
+
return regex.test(filePath);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function globToRegex(pattern) {
|
|
170
|
+
var regexStr = '^';
|
|
171
|
+
var i = 0;
|
|
172
|
+
while (i < pattern.length) {
|
|
173
|
+
var ch = pattern.charAt(i);
|
|
174
|
+
if (ch === '*' && i + 1 < pattern.length && pattern.charAt(i + 1) === '*') {
|
|
175
|
+
// ** matches everything
|
|
176
|
+
regexStr += '.*';
|
|
177
|
+
i += 2;
|
|
178
|
+
// skip trailing / if present
|
|
179
|
+
if (i < pattern.length && pattern.charAt(i) === '/') i++;
|
|
180
|
+
} else if (ch === '*') {
|
|
181
|
+
// * matches anything except /
|
|
182
|
+
regexStr += '[^/]*';
|
|
183
|
+
i++;
|
|
184
|
+
} else if (ch === '.') {
|
|
185
|
+
regexStr += '\\.';
|
|
186
|
+
i++;
|
|
187
|
+
} else {
|
|
188
|
+
regexStr += ch;
|
|
189
|
+
i++;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
regexStr += '$';
|
|
193
|
+
return new RegExp(regexStr);
|
|
194
|
+
}
|
|
@@ -3,10 +3,23 @@
|
|
|
3
3
|
"allow": [
|
|
4
4
|
"WebSearch",
|
|
5
5
|
"Read(**)",
|
|
6
|
-
"Edit(
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
6
|
+
"Edit(codecgc/**)",
|
|
7
|
+
"Edit(.claude/**)",
|
|
8
|
+
"Edit(.mcp.json)",
|
|
9
|
+
"Edit(model-routing.yaml)",
|
|
10
|
+
"Edit(README.md)",
|
|
11
|
+
"Edit(docs/**)",
|
|
12
|
+
"Edit(CHANGELOG.md)",
|
|
13
|
+
"Write(codecgc/**)",
|
|
14
|
+
"Write(.claude/**)",
|
|
15
|
+
"Write(.mcp.json)",
|
|
16
|
+
"Write(model-routing.yaml)",
|
|
17
|
+
"Write(README.md)",
|
|
18
|
+
"Write(docs/**)",
|
|
19
|
+
"Write(CHANGELOG.md)",
|
|
20
|
+
"mcp__codecgc__*",
|
|
21
|
+
"mcp__codex__*",
|
|
22
|
+
"mcp__gemini__*",
|
|
10
23
|
"Bash(*)",
|
|
11
24
|
"PowerShell(*)"
|
|
12
25
|
]
|
|
@@ -21,7 +21,7 @@ import shutil
|
|
|
21
21
|
mcp = FastMCP("Codex MCP Server-from guda.studio")
|
|
22
22
|
|
|
23
23
|
# Mirror of model-routing.yaml frontend_paths — keep these hints in sync with
|
|
24
|
-
#
|
|
24
|
+
# edit-guard.js and geminimcp/server.py BACKEND_PATH_HINTS.
|
|
25
25
|
FRONTEND_PATH_HINTS = (
|
|
26
26
|
"apps/web/",
|
|
27
27
|
"src/components/",
|
|
@@ -25,7 +25,7 @@ PROJECT_GEMINI_POLICY_RELATIVE_PATH = Path(".gemini") / "policies" / "codecgc-po
|
|
|
25
25
|
mcp = FastMCP("Gemini MCP Server-from guda.studio")
|
|
26
26
|
|
|
27
27
|
# Mirror of model-routing.yaml backend_paths — keep these hints in sync with
|
|
28
|
-
#
|
|
28
|
+
# edit-guard.js and codexmcp/server.py FRONTEND_PATH_HINTS.
|
|
29
29
|
BACKEND_PATH_HINTS = (
|
|
30
30
|
"apps/api/",
|
|
31
31
|
"server/",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hunyed15/codecgc",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Claude-hosted multi-model workflow product shell for CodeCGC.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"scripts/codecgc_runtime/*.py",
|
|
48
48
|
"scripts/postinstall_codecgc.js",
|
|
49
49
|
"scripts/README-codecgc-cli.md",
|
|
50
|
-
".claude/hooks/
|
|
50
|
+
".claude/hooks/edit-guard.js",
|
|
51
51
|
"codecgc/cgc/",
|
|
52
52
|
"codecgc/cgc-arch/",
|
|
53
53
|
"codecgc/cgc-build/",
|
|
@@ -23,7 +23,7 @@ RUNTIME_ENTRYPOINTS = [
|
|
|
23
23
|
]
|
|
24
24
|
|
|
25
25
|
RUNTIME_STATIC_REQUIREMENTS = [
|
|
26
|
-
".claude/hooks/
|
|
26
|
+
".claude/hooks/edit-guard.js",
|
|
27
27
|
"codecgc/templates/project/claude/settings.local.json",
|
|
28
28
|
"codecgc/templates/project/codex/codecgcrc.json",
|
|
29
29
|
"codecgc/templates/project/gemini/policies/codecgc-policy.toml",
|
|
@@ -21,9 +21,10 @@ from sync_codecgc_mcp_config import write_mcp_config
|
|
|
21
21
|
WORKSPACE = PACKAGE_ROOT
|
|
22
22
|
PROJECT_ROUTING_PATH = WORKSPACE / "model-routing.yaml"
|
|
23
23
|
PROJECT_TEMPLATES_DIR = WORKSPACE / "codecgc" / "templates" / "project"
|
|
24
|
-
PROJECT_HOOK_PATH = PROJECT_TEMPLATES_DIR / "claude" / "hooks" / "
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
PROJECT_HOOK_PATH = PROJECT_TEMPLATES_DIR / "claude" / "hooks" / "edit-guard.js"
|
|
25
|
+
PROJECT_CLAUDE_MD_TEMPLATE = PROJECT_TEMPLATES_DIR / "CLAUDE.md"
|
|
26
|
+
CLAUDE_MD_MARKER = "<!-- codecgc:claude-md:v1 -->"
|
|
27
|
+
EDIT_GUARDRAIL_MATCHER = "Edit|Write|MultiEdit"
|
|
27
28
|
PROJECT_ONBOARDING_RELATIVE_PATH = "codecgc/START_HERE.md"
|
|
28
29
|
PROJECT_ONBOARDING_MARKER = "<!-- codecgc:onboarding:v1 -->"
|
|
29
30
|
CLAUDE_SETTINGS_TEMPLATE = PROJECT_TEMPLATES_DIR / "claude" / "settings.local.json"
|
|
@@ -38,7 +39,7 @@ DEFAULT_HOOKS = {
|
|
|
38
39
|
"hooks": [
|
|
39
40
|
{
|
|
40
41
|
"type": "command",
|
|
41
|
-
"command": "
|
|
42
|
+
"command": "node .claude/hooks/edit-guard.js",
|
|
42
43
|
}
|
|
43
44
|
],
|
|
44
45
|
}
|
|
@@ -83,7 +84,8 @@ def get_workspace_paths(override_workspace: str = "") -> dict[str, Path]:
|
|
|
83
84
|
"settings": claude_dir / "settings.local.json",
|
|
84
85
|
"legacy_settings": claude_dir / "settings.json",
|
|
85
86
|
"mcp": root / ".mcp.json",
|
|
86
|
-
"hook_script": hooks_dir / "
|
|
87
|
+
"hook_script": hooks_dir / "edit-guard.js",
|
|
88
|
+
"claude_md": claude_dir / "CLAUDE.md",
|
|
87
89
|
"commands_dir": claude_dir / "commands",
|
|
88
90
|
"routing_file": root / "model-routing.yaml",
|
|
89
91
|
"onboarding_file": root / PROJECT_ONBOARDING_RELATIVE_PATH,
|
|
@@ -165,7 +167,8 @@ cgc "新增一个登录页面,放在 src/components/LoginForm.tsx"
|
|
|
165
167
|
.mcp.json
|
|
166
168
|
model-routing.yaml
|
|
167
169
|
.claude/settings.local.json
|
|
168
|
-
.claude/hooks/
|
|
170
|
+
.claude/hooks/edit-guard.js
|
|
171
|
+
.claude/CLAUDE.md
|
|
169
172
|
.claude/commands/cgc*.md
|
|
170
173
|
.codex/codecgcrc.json
|
|
171
174
|
.gemini/policies/codecgc-policy.toml
|
|
@@ -250,10 +253,6 @@ def policy_file_is_valid(path: Path) -> bool:
|
|
|
250
253
|
return True
|
|
251
254
|
|
|
252
255
|
|
|
253
|
-
def _normalize_command_path_for_markdown(path: Path) -> str:
|
|
254
|
-
return str(path).replace("\\", "\\\\")
|
|
255
|
-
|
|
256
|
-
|
|
257
256
|
def build_mcp_first_command_template(
|
|
258
257
|
*,
|
|
259
258
|
filename: str,
|
|
@@ -559,7 +558,7 @@ def is_codecgc_route_edit_hook(hook: Any) -> bool:
|
|
|
559
558
|
if hook.get("type") != "command":
|
|
560
559
|
return False
|
|
561
560
|
command = str(hook.get("command", "")).replace("\\", "/").lower()
|
|
562
|
-
return "
|
|
561
|
+
return "edit-guard" in command
|
|
563
562
|
|
|
564
563
|
|
|
565
564
|
def merge_hook_settings(current: dict[str, Any], command_text: str) -> tuple[dict[str, Any], bool]:
|
|
@@ -578,7 +577,7 @@ def merge_hook_settings(current: dict[str, Any], command_text: str) -> tuple[dic
|
|
|
578
577
|
for item in list(pre_tool_use):
|
|
579
578
|
if not isinstance(item, dict):
|
|
580
579
|
continue
|
|
581
|
-
if item.get("matcher")
|
|
580
|
+
if item.get("matcher") != EDIT_GUARDRAIL_MATCHER:
|
|
582
581
|
continue
|
|
583
582
|
hook_list = item.get("hooks")
|
|
584
583
|
if not isinstance(hook_list, list):
|
|
@@ -796,32 +795,8 @@ def mcp_server_matches_runtime_shape(server_payload: dict[str, Any], spec: dict[
|
|
|
796
795
|
return True
|
|
797
796
|
|
|
798
797
|
|
|
799
|
-
def mcp_config_matches_runtime_shape(payload: dict[str, Any]) -> bool:
|
|
800
|
-
servers = payload.get("mcpServers")
|
|
801
|
-
if not isinstance(servers, dict):
|
|
802
|
-
return False
|
|
803
|
-
|
|
804
|
-
for server_name, spec in RUNTIME_MCP_SERVER_SPECS.items():
|
|
805
|
-
server_payload = servers.get(server_name)
|
|
806
|
-
if not isinstance(server_payload, dict):
|
|
807
|
-
return False
|
|
808
|
-
if not mcp_server_matches_runtime_shape(server_payload, spec):
|
|
809
|
-
return False
|
|
810
|
-
return True
|
|
811
|
-
|
|
812
|
-
|
|
813
798
|
def build_workspace_hook_command(workspace_paths: dict[str, Path]) -> str:
|
|
814
|
-
|
|
815
|
-
workspace_root_path = Path(workspace_paths["root"])
|
|
816
|
-
hook_script_path = workspace_paths.get("hook_script", workspace_root_path / ".claude" / "hooks" / "route-edit.ps1")
|
|
817
|
-
workspace_root = str(workspace_root_path).replace("'", "''")
|
|
818
|
-
hook_script = str(hook_script_path).replace("\\", "/").replace("'", "''")
|
|
819
|
-
return (
|
|
820
|
-
"powershell -ExecutionPolicy Bypass -Command "
|
|
821
|
-
f"\"$env:CODECGC_PACKAGE_ROOT='{package_root}'; "
|
|
822
|
-
f"$env:CODECGC_WORKSPACE_ROOT='{workspace_root}'; "
|
|
823
|
-
f"& '{hook_script}'\""
|
|
824
|
-
)
|
|
799
|
+
return "node .claude/hooks/edit-guard.js"
|
|
825
800
|
|
|
826
801
|
|
|
827
802
|
def build_mode_summary_payload(
|
|
@@ -841,6 +816,32 @@ def build_mode_summary_payload(
|
|
|
841
816
|
return summary
|
|
842
817
|
|
|
843
818
|
|
|
819
|
+
def install_project_claude_md(target_path: Path) -> str:
|
|
820
|
+
"""Install or append CodeCGC rules to .claude/CLAUDE.md.
|
|
821
|
+
|
|
822
|
+
- Does not exist: create with template content.
|
|
823
|
+
- Exists without marker: append template content.
|
|
824
|
+
- Exists with marker: skip (don't overwrite user modifications).
|
|
825
|
+
"""
|
|
826
|
+
if not PROJECT_CLAUDE_MD_TEMPLATE.exists():
|
|
827
|
+
return str(target_path)
|
|
828
|
+
|
|
829
|
+
template_content = PROJECT_CLAUDE_MD_TEMPLATE.read_text(encoding="utf-8")
|
|
830
|
+
|
|
831
|
+
if target_path.exists():
|
|
832
|
+
existing = target_path.read_text(encoding="utf-8")
|
|
833
|
+
if CLAUDE_MD_MARKER in existing:
|
|
834
|
+
return str(target_path)
|
|
835
|
+
# Append with separator
|
|
836
|
+
separator = "\n\n---\n\n" if not existing.endswith("\n\n") else ""
|
|
837
|
+
target_path.write_text(existing + separator + template_content, encoding="utf-8")
|
|
838
|
+
else:
|
|
839
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
840
|
+
target_path.write_text(template_content, encoding="utf-8")
|
|
841
|
+
|
|
842
|
+
return str(target_path)
|
|
843
|
+
|
|
844
|
+
|
|
844
845
|
def install_local_runtime(override_workspace: str = "") -> dict[str, Any]:
|
|
845
846
|
workspace_paths = get_workspace_paths(override_workspace)
|
|
846
847
|
mcp_path = write_mcp_config(workspace_paths["mcp"], workspace_paths["root"])
|
|
@@ -861,6 +862,7 @@ def install_local_runtime(override_workspace: str = "") -> dict[str, Any]:
|
|
|
861
862
|
shutil.copyfile(PROJECT_HOOK_PATH, workspace_paths["hook_script"])
|
|
862
863
|
written_commands = write_custom_command_files(workspace_paths["commands_dir"], WORKSPACE / "bin")
|
|
863
864
|
onboarding_file = write_project_onboarding_file(workspace_paths["root"])
|
|
865
|
+
claude_md_path = install_project_claude_md(workspace_paths["claude_md"])
|
|
864
866
|
|
|
865
867
|
summary = build_mode_summary_payload(
|
|
866
868
|
scope="项目级 Claude 与 MCP 集成面",
|
|
@@ -878,6 +880,7 @@ def install_local_runtime(override_workspace: str = "") -> dict[str, Any]:
|
|
|
878
880
|
"codex_policy": str(codex_policy_path),
|
|
879
881
|
"gemini_policy": str(gemini_policy_path),
|
|
880
882
|
"hook_script": str(workspace_paths["hook_script"]),
|
|
883
|
+
"claude_md": str(claude_md_path),
|
|
881
884
|
"commands_dir": str(workspace_paths["commands_dir"]),
|
|
882
885
|
"onboarding_file": str(onboarding_file),
|
|
883
886
|
"workflow_dirs": workflow_dirs,
|
|
@@ -887,7 +890,8 @@ def install_local_runtime(override_workspace: str = "") -> dict[str, Any]:
|
|
|
887
890
|
"Project-local model-routing.yaml was synchronized as the policy source of truth.",
|
|
888
891
|
"Project-local codecgc workflow directories were initialized.",
|
|
889
892
|
"Claude pre-edit guardrail hook was synchronized into the target workspace.",
|
|
890
|
-
"Claude project permissions were rendered
|
|
893
|
+
"Claude project permissions were rendered with whitelist (Edit/Write restricted to codecgc, .claude, docs, config).",
|
|
894
|
+
".claude/CLAUDE.md was installed with CodeCGC workflow rules and three-layer protection guidance.",
|
|
891
895
|
"Project-local Codex policy contract was synchronized into .codex/codecgcrc.json.",
|
|
892
896
|
"Project-local Gemini policy was synchronized into .gemini/policies/codecgc-policy.toml.",
|
|
893
897
|
"Project-local Claude slash commands were synchronized into .claude/commands.",
|
|
@@ -106,10 +106,9 @@ def extract_text_content(value: Any) -> str:
|
|
|
106
106
|
|
|
107
107
|
|
|
108
108
|
def normalize_mcp_result(raw: dict[str, Any]) -> dict[str, Any]:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
# Prefer text content over structuredContent — the tool's actual JSON
|
|
110
|
+
# response is in content[0].text, while structuredContent is an MCP SDK
|
|
111
|
+
# convenience wrapper that may omit top-level fields like "success".
|
|
113
112
|
content = raw.get("content")
|
|
114
113
|
if isinstance(content, list):
|
|
115
114
|
for item in content:
|
|
@@ -127,6 +126,10 @@ def normalize_mcp_result(raw: dict[str, Any]) -> dict[str, Any]:
|
|
|
127
126
|
if isinstance(parsed, dict):
|
|
128
127
|
return parsed
|
|
129
128
|
|
|
129
|
+
structured_content = raw.get("structuredContent")
|
|
130
|
+
if isinstance(structured_content, dict):
|
|
131
|
+
return structured_content
|
|
132
|
+
|
|
130
133
|
return {
|
|
131
134
|
"success": False,
|
|
132
135
|
"error": "Unable to parse structured MCP tool result.",
|
|
@@ -280,15 +283,23 @@ def write_audit_file(audit_root: Path, audit_record: dict[str, Any]) -> Path:
|
|
|
280
283
|
|
|
281
284
|
async def execute_payload(payload: dict[str, Any], timeout_seconds: int) -> dict[str, Any]:
|
|
282
285
|
params = build_server_params(payload["target"])
|
|
286
|
+
raw_result = None
|
|
283
287
|
|
|
284
|
-
|
|
285
|
-
async with
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
288
|
+
try:
|
|
289
|
+
async with stdio_client(params) as (read_stream, write_stream):
|
|
290
|
+
async with ClientSession(read_stream, write_stream) as session:
|
|
291
|
+
await session.initialize()
|
|
292
|
+
raw_result = await session.call_tool(
|
|
293
|
+
payload["tool_name"],
|
|
294
|
+
payload["tool_args"],
|
|
295
|
+
read_timeout_seconds=datetime.timedelta(seconds=timeout_seconds),
|
|
296
|
+
)
|
|
297
|
+
except* ExceptionGroup:
|
|
298
|
+
# MCP SDK 清理阶段的 TaskGroup 异常不影响已获得的结果
|
|
299
|
+
pass
|
|
300
|
+
|
|
301
|
+
if raw_result is None:
|
|
302
|
+
raise RuntimeError("MCP call failed before producing a result.")
|
|
292
303
|
|
|
293
304
|
dumped = raw_result.model_dump(mode="json")
|
|
294
305
|
normalized = normalize_mcp_result(dumped)
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
$ErrorActionPreference = "Stop"
|
|
2
|
-
|
|
3
|
-
function Write-Approve {
|
|
4
|
-
$payload = @{
|
|
5
|
-
decision = "approve"
|
|
6
|
-
} | ConvertTo-Json -Compress
|
|
7
|
-
[Console]::Out.Write($payload)
|
|
8
|
-
exit 0
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function Write-Deny($reason) {
|
|
12
|
-
$payload = @{
|
|
13
|
-
decision = "deny"
|
|
14
|
-
reason = $reason
|
|
15
|
-
} | ConvertTo-Json -Compress
|
|
16
|
-
[Console]::Out.Write($payload)
|
|
17
|
-
exit 0
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
$inputJson = [Console]::In.ReadToEnd()
|
|
21
|
-
if ([string]::IsNullOrWhiteSpace($inputJson)) {
|
|
22
|
-
Write-Approve
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
$configuredPackageRoot = [Environment]::GetEnvironmentVariable("CODECGC_PACKAGE_ROOT")
|
|
26
|
-
if (-not [string]::IsNullOrWhiteSpace($configuredPackageRoot)) {
|
|
27
|
-
$packageRoot = Resolve-Path $configuredPackageRoot
|
|
28
|
-
} else {
|
|
29
|
-
$packageRoot = Resolve-Path (Join-Path $PSScriptRoot "..\..")
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
$configuredWorkspaceRoot = [Environment]::GetEnvironmentVariable("CODECGC_WORKSPACE_ROOT")
|
|
33
|
-
if (-not [string]::IsNullOrWhiteSpace($configuredWorkspaceRoot)) {
|
|
34
|
-
$workspaceRoot = Resolve-Path $configuredWorkspaceRoot
|
|
35
|
-
} else {
|
|
36
|
-
$workspaceRoot = Resolve-Path (Get-Location)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
$policyScript = Join-Path $packageRoot "scripts\codecgc_policy.py"
|
|
40
|
-
$routingFile = Join-Path $workspaceRoot "model-routing.yaml"
|
|
41
|
-
$mcpConfigFile = Join-Path $workspaceRoot ".mcp.json"
|
|
42
|
-
|
|
43
|
-
if (-not (Test-Path $policyScript)) {
|
|
44
|
-
Write-Deny "CodeCGC: policy checker is missing: $policyScript"
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
$pythonCommand = [Environment]::GetEnvironmentVariable("CODECGC_PYTHON_COMMAND")
|
|
48
|
-
if ([string]::IsNullOrWhiteSpace($pythonCommand) -and (Test-Path $mcpConfigFile)) {
|
|
49
|
-
try {
|
|
50
|
-
$mcpConfig = Get-Content -Raw $mcpConfigFile | ConvertFrom-Json
|
|
51
|
-
$pythonCommand = [string]$mcpConfig.mcpServers.codecgc.command
|
|
52
|
-
} catch {
|
|
53
|
-
$pythonCommand = ""
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
if ([string]::IsNullOrWhiteSpace($pythonCommand)) {
|
|
57
|
-
$pythonCommand = "python"
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
61
|
-
$psi.FileName = $pythonCommand
|
|
62
|
-
$escapedPolicyScript = $policyScript.Replace('"', '\"')
|
|
63
|
-
$escapedRoutingFile = $routingFile.Replace('"', '\"')
|
|
64
|
-
$psi.Arguments = "`"$escapedPolicyScript`" --hook-check --actor claude --operation write --routing-file `"$escapedRoutingFile`""
|
|
65
|
-
$psi.RedirectStandardInput = $true
|
|
66
|
-
$psi.RedirectStandardOutput = $true
|
|
67
|
-
$psi.RedirectStandardError = $true
|
|
68
|
-
$psi.UseShellExecute = $false
|
|
69
|
-
$psi.CreateNoWindow = $true
|
|
70
|
-
|
|
71
|
-
$process = [System.Diagnostics.Process]::Start($psi)
|
|
72
|
-
$process.StandardInput.Write($inputJson)
|
|
73
|
-
$process.StandardInput.Close()
|
|
74
|
-
$stdout = $process.StandardOutput.ReadToEnd()
|
|
75
|
-
$stderr = $process.StandardError.ReadToEnd()
|
|
76
|
-
$process.WaitForExit()
|
|
77
|
-
|
|
78
|
-
if ($process.ExitCode -ne 0) {
|
|
79
|
-
$detail = if ([string]::IsNullOrWhiteSpace($stderr)) { $stdout } else { $stderr }
|
|
80
|
-
Write-Deny "CodeCGC: policy checker failed. $detail"
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if ([string]::IsNullOrWhiteSpace($stdout)) {
|
|
84
|
-
Write-Approve
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
[Console]::Out.Write($stdout)
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
$ErrorActionPreference = "Stop"
|
|
2
|
-
|
|
3
|
-
function Write-Approve {
|
|
4
|
-
$payload = @{
|
|
5
|
-
decision = "approve"
|
|
6
|
-
} | ConvertTo-Json -Compress
|
|
7
|
-
[Console]::Out.Write($payload)
|
|
8
|
-
exit 0
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function Write-Deny($reason) {
|
|
12
|
-
$payload = @{
|
|
13
|
-
decision = "deny"
|
|
14
|
-
reason = $reason
|
|
15
|
-
} | ConvertTo-Json -Compress
|
|
16
|
-
[Console]::Out.Write($payload)
|
|
17
|
-
exit 0
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
$inputJson = [Console]::In.ReadToEnd()
|
|
21
|
-
if ([string]::IsNullOrWhiteSpace($inputJson)) {
|
|
22
|
-
Write-Approve
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
$configuredPackageRoot = [Environment]::GetEnvironmentVariable("CODECGC_PACKAGE_ROOT")
|
|
26
|
-
if (-not [string]::IsNullOrWhiteSpace($configuredPackageRoot)) {
|
|
27
|
-
$packageRoot = Resolve-Path $configuredPackageRoot
|
|
28
|
-
} else {
|
|
29
|
-
$packageRoot = Resolve-Path (Join-Path $PSScriptRoot "..\..")
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
$configuredWorkspaceRoot = [Environment]::GetEnvironmentVariable("CODECGC_WORKSPACE_ROOT")
|
|
33
|
-
if (-not [string]::IsNullOrWhiteSpace($configuredWorkspaceRoot)) {
|
|
34
|
-
$workspaceRoot = Resolve-Path $configuredWorkspaceRoot
|
|
35
|
-
} else {
|
|
36
|
-
$workspaceRoot = Resolve-Path (Get-Location)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
$policyScript = Join-Path $packageRoot "scripts\codecgc_policy.py"
|
|
40
|
-
$routingFile = Join-Path $workspaceRoot "model-routing.yaml"
|
|
41
|
-
$mcpConfigFile = Join-Path $workspaceRoot ".mcp.json"
|
|
42
|
-
|
|
43
|
-
if (-not (Test-Path $policyScript)) {
|
|
44
|
-
Write-Deny "CodeCGC: policy checker is missing: $policyScript"
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
$pythonCommand = [Environment]::GetEnvironmentVariable("CODECGC_PYTHON_COMMAND")
|
|
48
|
-
if ([string]::IsNullOrWhiteSpace($pythonCommand) -and (Test-Path $mcpConfigFile)) {
|
|
49
|
-
try {
|
|
50
|
-
$mcpConfig = Get-Content -Raw $mcpConfigFile | ConvertFrom-Json
|
|
51
|
-
$pythonCommand = [string]$mcpConfig.mcpServers.codecgc.command
|
|
52
|
-
} catch {
|
|
53
|
-
$pythonCommand = ""
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
if ([string]::IsNullOrWhiteSpace($pythonCommand)) {
|
|
57
|
-
$pythonCommand = "python"
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
61
|
-
$psi.FileName = $pythonCommand
|
|
62
|
-
$escapedPolicyScript = $policyScript.Replace('"', '\"')
|
|
63
|
-
$escapedRoutingFile = $routingFile.Replace('"', '\"')
|
|
64
|
-
$psi.Arguments = "`"$escapedPolicyScript`" --hook-check --actor claude --operation write --routing-file `"$escapedRoutingFile`""
|
|
65
|
-
$psi.RedirectStandardInput = $true
|
|
66
|
-
$psi.RedirectStandardOutput = $true
|
|
67
|
-
$psi.RedirectStandardError = $true
|
|
68
|
-
$psi.UseShellExecute = $false
|
|
69
|
-
$psi.CreateNoWindow = $true
|
|
70
|
-
|
|
71
|
-
$process = [System.Diagnostics.Process]::Start($psi)
|
|
72
|
-
$process.StandardInput.Write($inputJson)
|
|
73
|
-
$process.StandardInput.Close()
|
|
74
|
-
$stdout = $process.StandardOutput.ReadToEnd()
|
|
75
|
-
$stderr = $process.StandardError.ReadToEnd()
|
|
76
|
-
$process.WaitForExit()
|
|
77
|
-
|
|
78
|
-
if ($process.ExitCode -ne 0) {
|
|
79
|
-
$detail = if ([string]::IsNullOrWhiteSpace($stderr)) { $stdout } else { $stderr }
|
|
80
|
-
Write-Deny "CodeCGC: policy checker failed. $detail"
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if ([string]::IsNullOrWhiteSpace($stdout)) {
|
|
84
|
-
Write-Approve
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
[Console]::Out.Write($stdout)
|