@ossdeveloper/github-compliance 1.0.2 → 1.3.0
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/ARCHITECTURE.md +150 -60
- package/CHANGELOG.md +28 -1
- package/README.md +64 -15
- package/compliance.ts +22 -37
- package/contracts.ts +137 -0
- package/database.ts +2 -9
- package/dist/plugin.js +194 -61
- package/package.json +1 -1
- package/plugin.ts +249 -68
package/ARCHITECTURE.md
CHANGED
|
@@ -10,14 +10,34 @@ Deep dive into the GitHub Compliance Plugin design.
|
|
|
10
10
|
4. **Audit Trail**: Complete history of all operations for debugging
|
|
11
11
|
5. **Centralized Schema**: Single source of truth for all database definitions
|
|
12
12
|
|
|
13
|
+
## MCP Tool Handling
|
|
14
|
+
|
|
15
|
+
The plugin must handle two types of GitHub tools differently:
|
|
16
|
+
|
|
17
|
+
### Non-MCP Tools (e.g., custom tools you create)
|
|
18
|
+
- `tool.execute.before` has access to `output.args`
|
|
19
|
+
- Can validate AND block before execution
|
|
20
|
+
- `tool.execute.after` logs success
|
|
21
|
+
|
|
22
|
+
### MCP Tools (GitHub MCP - prefixed with `github_`)
|
|
23
|
+
- `tool.execute.before` does NOT have access to `output.args` (OpenCode limitation)
|
|
24
|
+
- Cannot validate before execution
|
|
25
|
+
- Must defer validation to `tool.execute.after` where `input.args` IS available
|
|
26
|
+
- Validation happens post-execution (for audit purposes)
|
|
27
|
+
|
|
28
|
+
**Important**: For MCP tools, if compliance is invalid, the tool has already executed.
|
|
29
|
+
The plugin logs the violation for audit but cannot block the operation.
|
|
30
|
+
|
|
13
31
|
## Component Flow
|
|
14
32
|
|
|
33
|
+
### Non-MCP Tools (Standard Flow)
|
|
34
|
+
|
|
15
35
|
```
|
|
16
36
|
┌──────────────────────────────────────────────────────────────────────────┐
|
|
17
37
|
│ tool.execute.before │
|
|
18
38
|
│ │
|
|
19
39
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │
|
|
20
|
-
│ │ isWriteTool? │────▶│ extractInfo
|
|
40
|
+
│ │ isWriteTool? │────▶│ extractInfo │────▶│ getComplianceRecord │ │
|
|
21
41
|
│ └──────────────┘ └──────────────┘ └──────────────────────────┘ │
|
|
22
42
|
│ │ │
|
|
23
43
|
│ ▼ │
|
|
@@ -30,13 +50,109 @@ Deep dive into the GitHub Compliance Plugin design.
|
|
|
30
50
|
│ ▼ ▼ │ │
|
|
31
51
|
│ ┌──────────────────┐ ┌──────────────────────┐ │ │
|
|
32
52
|
│ │ valid: false │ │ valid: true │ │ │
|
|
33
|
-
│ │ logFailedAttempt
|
|
34
|
-
│ │ throw BlockedErr
|
|
53
|
+
│ │ logFailedAttempt│ │ markRecordUsed │ │ │
|
|
54
|
+
│ │ throw BlockedErr│ │ attach metadata │ │ │
|
|
35
55
|
│ └──────────────────┘ └──────────────────────┘ │ │
|
|
36
56
|
│ │
|
|
37
57
|
└──────────────────────────────────────────────────────────────────────────┘
|
|
38
58
|
```
|
|
39
59
|
|
|
60
|
+
### MCP Tools (Deferred Validation Flow)
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
┌──────────────────────────────────────────────────────────────────────────┐
|
|
64
|
+
│ tool.execute.before │
|
|
65
|
+
│ │
|
|
66
|
+
│ ┌──────────────┐ ┌──────────────┐ │
|
|
67
|
+
│ │ isWriteTool? │────▶│ isMcpTool? │ │
|
|
68
|
+
│ └──────────────┘ └──────────────┘ │
|
|
69
|
+
│ │ │
|
|
70
|
+
│ │ (yes) │
|
|
71
|
+
│ ▼ │
|
|
72
|
+
│ ┌──────────────────┐ │
|
|
73
|
+
│ │ Skip validation │ │
|
|
74
|
+
│ │ Set __needsMcpValidation │ │
|
|
75
|
+
│ │ marker on output │ │
|
|
76
|
+
│ └──────────────────┘ │
|
|
77
|
+
│ │
|
|
78
|
+
└──────────────────────────────────────────────────────────────────────────┘
|
|
79
|
+
│
|
|
80
|
+
│ Tool executes
|
|
81
|
+
▼
|
|
82
|
+
┌──────────────────────────────────────────────────────────────────────────┐
|
|
83
|
+
│ tool.execute.after │
|
|
84
|
+
│ │
|
|
85
|
+
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │
|
|
86
|
+
│ │ needsMcpVal? │────▶│ extractInfo │────▶│ getComplianceRecord │ │
|
|
87
|
+
│ └──────────────┘ │ from input.args └──────────────────────────┘ │
|
|
88
|
+
│ └──────────────┘ │
|
|
89
|
+
│ │ │
|
|
90
|
+
│ ▼ │
|
|
91
|
+
│ ┌────────────────────────┐ │
|
|
92
|
+
│ │ validateCompliance │ │
|
|
93
|
+
│ └────────────────────────┘ │
|
|
94
|
+
│ │ │
|
|
95
|
+
│ ┌─────────────────────┼─────────────────────┐ │
|
|
96
|
+
│ │ │ │ │
|
|
97
|
+
│ ▼ ▼ ▼ │
|
|
98
|
+
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
|
99
|
+
│ │ valid: false │ │ valid: true │ │ extraction err │ │
|
|
100
|
+
│ │ log violation │ │ log success │ │ log error │ │
|
|
101
|
+
│ │ (can't block) │ │ │ │ │ │
|
|
102
|
+
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
|
103
|
+
│ │
|
|
104
|
+
└──────────────────────────────────────────────────────────────────────────┘
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## OpenCode Plugin API Notes
|
|
108
|
+
|
|
109
|
+
The OpenCode SDK defines hooks as:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
"tool.execute.before"?: (input: {
|
|
113
|
+
tool: string;
|
|
114
|
+
sessionID: string;
|
|
115
|
+
callID: string;
|
|
116
|
+
}, output: {
|
|
117
|
+
args: any; // NOTE: NOT populated for MCP tools!
|
|
118
|
+
}) => Promise<void>;
|
|
119
|
+
|
|
120
|
+
"tool.execute.after"?: (input: {
|
|
121
|
+
tool: string;
|
|
122
|
+
sessionID: string;
|
|
123
|
+
callID: string;
|
|
124
|
+
args: any; // IS populated for MCP tools
|
|
125
|
+
}, output: {
|
|
126
|
+
title: string;
|
|
127
|
+
output: string;
|
|
128
|
+
metadata: any;
|
|
129
|
+
}) => Promise<void>;
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
This is why MCP tools require deferred validation.
|
|
133
|
+
|
|
134
|
+
## Centralized Contracts (contracts.ts)
|
|
135
|
+
|
|
136
|
+
All content hashing and tool name normalization is centralized in `contracts.ts`:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// Tool name normalization - handles github_ prefix
|
|
140
|
+
export function normalizeToolName(toolName: string): string {
|
|
141
|
+
return TOOL_PREFIX_NORMALIZE[toolName] || toolName;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Content hash - used by both tools.ts and plugin.ts
|
|
145
|
+
export function computeContentHash(title: string | null, body: string | null): string {
|
|
146
|
+
const content = JSON.stringify({ title: title || "", body: body || "" });
|
|
147
|
+
return "sha256:" + createHash("sha256").update(content).digest("hex");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Field extraction - unified interface for compliance lookups
|
|
151
|
+
export function extractRecordFields(toolName: string, args: Record<string, unknown> | null): ComplianceRecordFields;
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Key invariant**: `computeContentHash` is called with DECODED (not base64) values, both when creating records and looking them up.
|
|
155
|
+
|
|
40
156
|
## Schema Design (schema.ts)
|
|
41
157
|
|
|
42
158
|
All database definitions are centralized in `schema.ts`:
|
|
@@ -63,7 +179,7 @@ export const SCHEMA = {
|
|
|
63
179
|
export const RECORD_STATUS = {
|
|
64
180
|
PENDING: "pending",
|
|
65
181
|
APPROVED: "approved",
|
|
66
|
-
|
|
182
|
+
EXPIRED: "expired",
|
|
67
183
|
USED: "used"
|
|
68
184
|
}
|
|
69
185
|
```
|
|
@@ -88,7 +204,7 @@ pending ────▶ approved ────▶ used
|
|
|
88
204
|
### Content Hash Calculation
|
|
89
205
|
|
|
90
206
|
```typescript
|
|
91
|
-
// In
|
|
207
|
+
// In contracts.ts
|
|
92
208
|
const content = JSON.stringify({
|
|
93
209
|
title: title || "",
|
|
94
210
|
body: body || ""
|
|
@@ -112,15 +228,15 @@ CREATE INDEX idx_compliance_expires ON compliance_records(expires_at);
|
|
|
112
228
|
|
|
113
229
|
1. **tool.execute.before**
|
|
114
230
|
- Runs BEFORE tool execution
|
|
115
|
-
- Can
|
|
116
|
-
-
|
|
231
|
+
- For non-MCP: Can validate and block
|
|
232
|
+
- For MCP: Cannot validate (no args), marks for post-validation
|
|
117
233
|
|
|
118
234
|
2. **Tool executes** (if before hook allowed)
|
|
119
235
|
|
|
120
236
|
3. **tool.execute.after**
|
|
121
237
|
- Runs AFTER tool execution
|
|
122
|
-
-
|
|
123
|
-
-
|
|
238
|
+
- For MCP: Performs validation, logs success or violation
|
|
239
|
+
- Cannot block execution (already happened)
|
|
124
240
|
|
|
125
241
|
## Custom Tool Execution
|
|
126
242
|
|
|
@@ -138,59 +254,19 @@ Custom tools (from `tools.ts`) do NOT go through the `tool.execute.before` hook.
|
|
|
138
254
|
|
|
139
255
|
All error reasons are defined in `ERROR_REASONS` enum in `schema.ts`.
|
|
140
256
|
|
|
141
|
-
## Extensibility Points
|
|
142
|
-
|
|
143
|
-
### 1. New Check Types
|
|
144
|
-
|
|
145
|
-
Add to `CHECK_TYPES` in `schema.ts`:
|
|
146
|
-
|
|
147
|
-
```typescript
|
|
148
|
-
export const CHECK_TYPES = {
|
|
149
|
-
// ... existing ...
|
|
150
|
-
VOUCH_VERIFIED: "vouch_verified"
|
|
151
|
-
}
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
LLM will automatically include them when calling `create_compliance_record`.
|
|
155
|
-
|
|
156
|
-
### 2. New Validation Rules
|
|
157
|
-
|
|
158
|
-
Add to `validateCompliance()` in `database.ts`:
|
|
159
|
-
|
|
160
|
-
```typescript
|
|
161
|
-
if (record.status === RECORD_STATUS.PENDING) {
|
|
162
|
-
return { valid: false, reason: ERROR_REASONS.NOT_APPROVED, ... };
|
|
163
|
-
}
|
|
164
|
-
// Add new validation here
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
### 3. New Columns
|
|
168
|
-
|
|
169
|
-
To add a new column:
|
|
170
|
-
|
|
171
|
-
1. Add to `SCHEMA.columns` in `schema.ts`
|
|
172
|
-
2. Add to `CREATE TABLE` in `initDatabase()` in `database.ts`
|
|
173
|
-
3. Add to `ComplianceRecord` interface in `database.ts`
|
|
174
|
-
|
|
175
|
-
### 4. New Table
|
|
176
|
-
|
|
177
|
-
To add a new table:
|
|
178
|
-
|
|
179
|
-
1. Add table name to `SCHEMA.tables` in `schema.ts`
|
|
180
|
-
2. Add column constants to `SCHEMA.columns` in `schema.ts`
|
|
181
|
-
3. Add `CREATE TABLE` in `initDatabase()` in `database.ts`
|
|
182
|
-
4. Create CRUD functions in `database.ts`
|
|
183
|
-
|
|
184
257
|
## Security Considerations
|
|
185
258
|
|
|
186
259
|
1. **No Fabrication**: LLM cannot fake compliance records - they must actually call `create_compliance_record`
|
|
187
260
|
2. **No Bypass**: Plugin hook cannot be circumvented by LLM
|
|
188
261
|
3. **Content Integrity**: Hash prevents silent content changes
|
|
189
262
|
4. **Expiration**: 30-minute window limits abuse window
|
|
263
|
+
5. **Audit Trail**: All violations are logged for review
|
|
264
|
+
|
|
265
|
+
**Limitation for MCP Tools**: Since MCP tools execute before compliance can be validated, a determined LLM could potentially execute a write operation before the plugin detects the violation. The violation IS logged and auditable, but the operation completes. Consider this when designing high-security workflows.
|
|
190
266
|
|
|
191
267
|
## Testing Considerations
|
|
192
268
|
|
|
193
|
-
### Happy Path
|
|
269
|
+
### Happy Path (Non-MCP)
|
|
194
270
|
1. Load skill
|
|
195
271
|
2. Read CONTRIBUTING.md
|
|
196
272
|
3. Search duplicates
|
|
@@ -201,16 +277,30 @@ To add a new table:
|
|
|
201
277
|
8. Call issue_write
|
|
202
278
|
9. Success logged
|
|
203
279
|
|
|
280
|
+
### Happy Path (MCP)
|
|
281
|
+
1. Load skill
|
|
282
|
+
2. Read CONTRIBUTING.md
|
|
283
|
+
3. Search duplicates
|
|
284
|
+
4. Draft content
|
|
285
|
+
5. Present to user
|
|
286
|
+
6. Get approval
|
|
287
|
+
7. Call create_compliance_record
|
|
288
|
+
8. Call github_issue_write
|
|
289
|
+
9. Tool executes (before hook can't validate)
|
|
290
|
+
10. After hook validates, finds valid record
|
|
291
|
+
11. Success logged
|
|
292
|
+
|
|
204
293
|
### Rejection Paths
|
|
205
|
-
- No record → Blocked with NO_RECORD
|
|
206
|
-
- Expired record → Blocked with EXPIRED
|
|
294
|
+
- No record → Blocked with NO_RECORD (or logged if MCP)
|
|
295
|
+
- Expired record → Blocked with EXPIRED (or logged if MCP)
|
|
207
296
|
- Content changed → Blocked (hash mismatch)
|
|
208
297
|
- Record already used → Blocked with ALREADY_USED
|
|
209
298
|
|
|
210
299
|
## Future Improvements
|
|
211
300
|
|
|
212
|
-
1. **
|
|
213
|
-
2. **
|
|
214
|
-
3. **
|
|
215
|
-
4. **
|
|
216
|
-
5. **
|
|
301
|
+
1. **Pre-validation for MCP**: Work with OpenCode to enable args in before hook for MCP tools
|
|
302
|
+
2. **Cleanup Job**: Cron-like cleanup of old records
|
|
303
|
+
3. **Metrics**: Track compliance pass/fail rates
|
|
304
|
+
4. **Policy Fetching**: Pull compliance policy from repo
|
|
305
|
+
5. **Multi-Client Support**: Share compliance records across clients
|
|
306
|
+
6. **Signature Verification**: Cryptographic signing of records
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the GitHub Compliance Plugin.
|
|
4
4
|
|
|
5
|
+
## [1.3.0] - 2026-03-21
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **MCP Tool Argument Extraction Bug**: Fixed critical bug where MCP tools (prefixed with `github_`) could not be validated because `output.args` is not populated in `tool.execute.before` hook
|
|
10
|
+
- MCP tools now use deferred validation: `tool.execute.before` skips validation and marks tool for post-execution validation
|
|
11
|
+
- `tool.execute.after` performs validation when `input.args` IS available
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Rewrote `plugin.ts` to use centralized `extractRecordFields` from `contracts.ts`
|
|
16
|
+
- Added helper functions `isMcpTool()`, `extractArgs()`, `validateAndProcess()`, `logSuccess()`
|
|
17
|
+
- Separated validation logic for non-MCP (pre-execution blocking) and MCP (post-execution audit) flows
|
|
18
|
+
- Refactored error handling to be more robust
|
|
19
|
+
|
|
20
|
+
### Architecture Updates
|
|
21
|
+
|
|
22
|
+
- Updated ARCHITECTURE.md with new MCP tool handling flow
|
|
23
|
+
- Added OpenCode Plugin API notes explaining the args availability difference between hooks
|
|
24
|
+
- Added security consideration for MCP tool limitation
|
|
25
|
+
|
|
26
|
+
### Benefits
|
|
27
|
+
|
|
28
|
+
- MCP tools can now be properly audited for compliance
|
|
29
|
+
- Non-MCP tools continue to work with pre-execution blocking
|
|
30
|
+
- Centralized contracts ensure consistent field extraction everywhere
|
|
31
|
+
|
|
5
32
|
## [1.2.0] - 2026-03-21
|
|
6
33
|
|
|
7
34
|
### Added
|
|
@@ -97,4 +124,4 @@ All notable changes to the GitHub Compliance Plugin.
|
|
|
97
124
|
|
|
98
125
|
### Database Location
|
|
99
126
|
|
|
100
|
-
`~/.opencode/github-compliance.db`
|
|
127
|
+
`~/.opencode/github-compliance.db`
|
package/README.md
CHANGED
|
@@ -13,28 +13,53 @@ This plugin intercepts GitHub write operations and blocks them unless a valid co
|
|
|
13
13
|
│ OpenCode │
|
|
14
14
|
│ ┌───────────┐ ┌──────────────────────────────────────────┐ │
|
|
15
15
|
│ │ LLM │───▶│ Plugin (tool.execute.before) │ │
|
|
16
|
-
│ └───────────┘ │ -
|
|
17
|
-
│ │ -
|
|
18
|
-
│
|
|
16
|
+
│ └───────────┘ │ - MCP tools: Skip (no args), mark for after│ │
|
|
17
|
+
│ │ - Other tools: Check SQLite, block/allow │ │
|
|
18
|
+
│ └──────────────────────────────────────────┘ │
|
|
19
|
+
│ │ │
|
|
20
|
+
│ ▼ (if allowed) │
|
|
21
|
+
│ ┌──────────────────────────────────────────┐ │
|
|
22
|
+
│ │ GitHub MCP executes │ │
|
|
23
|
+
│ └──────────────────────────────────────────┘ │
|
|
24
|
+
│ │ │
|
|
25
|
+
│ ▼ │
|
|
26
|
+
│ ┌───────────┐ ┌──────────────────────────────────────────┐ │
|
|
27
|
+
│ │ LLM │◀───│ Plugin (tool.execute.after) │ │
|
|
28
|
+
│ └───────────┘ │ - MCP tools: Validate now, log result │ │
|
|
29
|
+
│ │ - Other tools: Log success │ │
|
|
19
30
|
│ └──────────────────────────────────────────┘ │
|
|
20
31
|
│ │ │
|
|
21
32
|
│ ▼ │
|
|
22
33
|
│ ┌──────────────────────────────────────────┐ │
|
|
23
|
-
│ │ Custom Tools (LLM calls these)
|
|
34
|
+
│ │ Custom Tools (LLM calls these) │ │
|
|
24
35
|
│ │ - create_compliance_record │ │
|
|
25
36
|
│ │ - get_compliance_status │ │
|
|
26
37
|
│ └──────────────────────────────────────────┘ │
|
|
27
38
|
│ │ │
|
|
28
39
|
└────────────────────────────────────┼────────────────────────────┘
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
│
|
|
41
|
+
▼
|
|
42
|
+
┌─────────────────────┐
|
|
43
|
+
│ SQLite Database │
|
|
44
|
+
│ ~/.opencode/ │
|
|
45
|
+
│ github-compliance.db│
|
|
46
|
+
└─────────────────────┘
|
|
36
47
|
```
|
|
37
48
|
|
|
49
|
+
## MCP Tool Handling
|
|
50
|
+
|
|
51
|
+
**Important**: MCP tools (prefixed with `github_`) behave differently than other tools:
|
|
52
|
+
|
|
53
|
+
| Aspect | Non-MCP Tools | MCP Tools (github_*) |
|
|
54
|
+
|--------|---------------|----------------------|
|
|
55
|
+
| Args in `before` hook | Available in `output.args` | NOT available |
|
|
56
|
+
| Can block before execution | Yes | No (no args) |
|
|
57
|
+
| Args in `after` hook | Available in `input.args` | Available |
|
|
58
|
+
| Validation timing | Pre-execution | Post-execution |
|
|
59
|
+
| Can prevent execution | Yes | No (already executed) |
|
|
60
|
+
|
|
61
|
+
For MCP tools, the plugin validates after execution and logs violations for audit, but cannot block the operation.
|
|
62
|
+
|
|
38
63
|
## Components
|
|
39
64
|
|
|
40
65
|
| File | Purpose |
|
|
@@ -42,6 +67,7 @@ This plugin intercepts GitHub write operations and blocks them unless a valid co
|
|
|
42
67
|
| `schema.ts` | Centralized constants for table/column names, enums |
|
|
43
68
|
| `database.ts` | SQLite operations, schema, CRUD |
|
|
44
69
|
| `compliance.ts` | Tool identification, content hashing, validation logic |
|
|
70
|
+
| `contracts.ts` | Centralized field extraction, hash computation, tool name normalization |
|
|
45
71
|
| `errors.ts` | Error message generation |
|
|
46
72
|
| `tools.ts` | Custom tools exposed to LLM |
|
|
47
73
|
| `plugin.ts` | Main entry, hooks into tool.execute.before/after |
|
|
@@ -112,8 +138,10 @@ Log of rejected operations with reasons.
|
|
|
112
138
|
|
|
113
139
|
## Workflow
|
|
114
140
|
|
|
141
|
+
### For Non-MCP Tools
|
|
142
|
+
|
|
115
143
|
```
|
|
116
|
-
1. LLM attempts
|
|
144
|
+
1. LLM attempts issue_write
|
|
117
145
|
↓
|
|
118
146
|
2. plugin.tool.execute.before intercepts
|
|
119
147
|
↓
|
|
@@ -122,7 +150,28 @@ Log of rejected operations with reasons.
|
|
|
122
150
|
4. No record → BLOCK with error message
|
|
123
151
|
Record valid → Mark as used, allow execution
|
|
124
152
|
↓
|
|
125
|
-
5.
|
|
153
|
+
5. plugin.tool.execute.after logs success
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### For MCP Tools (github_*)
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
1. LLM attempts github_issue_write
|
|
160
|
+
↓
|
|
161
|
+
2. plugin.tool.execute.before intercepts
|
|
162
|
+
↓
|
|
163
|
+
3. Detects MCP tool (no args available)
|
|
164
|
+
↓
|
|
165
|
+
4. Marks tool for post-execution validation
|
|
166
|
+
↓
|
|
167
|
+
5. GitHub MCP executes the operation
|
|
168
|
+
↓
|
|
169
|
+
6. plugin.tool.execute.after intercepts
|
|
170
|
+
↓
|
|
171
|
+
7. Now has args, validates compliance
|
|
172
|
+
↓
|
|
173
|
+
8. Valid record → Log success
|
|
174
|
+
Invalid/no record → Log violation (cannot block)
|
|
126
175
|
```
|
|
127
176
|
|
|
128
177
|
## Custom Tools
|
|
@@ -268,6 +317,6 @@ Hash is computed from `title` and `body` only. Do not include:
|
|
|
268
317
|
|
|
269
318
|
## Version
|
|
270
319
|
|
|
271
|
-
Current: 1.
|
|
320
|
+
Current: 1.3.0
|
|
272
321
|
|
|
273
|
-
See [CHANGELOG.md](./CHANGELOG.md) for version history.
|
|
322
|
+
See [CHANGELOG.md](./CHANGELOG.md) for version history.
|
package/compliance.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Compliance validation logic for GitHub
|
|
2
|
+
* Compliance validation logic for GitHub Compliance Plugin
|
|
3
3
|
*
|
|
4
4
|
* Handles tool identification, content hashing, and result parsing.
|
|
5
|
+
*
|
|
6
|
+
* IMPORTANT: All content hashing uses centralized functions from contracts.ts
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
|
-
import { createHash } from "crypto";
|
|
8
9
|
import { SCHEMA, RECORD_STATUS } from "./schema";
|
|
10
|
+
import {
|
|
11
|
+
extractRecordFields,
|
|
12
|
+
computeContentHash,
|
|
13
|
+
normalizeToolName,
|
|
14
|
+
type ComplianceRecordFields
|
|
15
|
+
} from "./contracts";
|
|
9
16
|
|
|
10
17
|
/**
|
|
11
18
|
* List of GitHub write tools that require compliance validation
|
|
@@ -63,50 +70,28 @@ export function isGitHubWriteTool(toolName: string): boolean {
|
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
/**
|
|
66
|
-
* Extract compliance-relevant information from a tool call
|
|
73
|
+
* Extract compliance-relevant information from a tool call.
|
|
67
74
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
75
|
+
* This function delegates to the centralized extractRecordFields from contracts.ts
|
|
76
|
+
* to ensure consistent field extraction and content hashing.
|
|
70
77
|
*
|
|
71
78
|
* @param toolName - Name of the tool being called
|
|
72
|
-
* @param args - Arguments passed to the tool
|
|
79
|
+
* @param args - Arguments passed to the tool (can be null/undefined)
|
|
73
80
|
* @returns Extracted ToolInfo with content hash
|
|
74
81
|
*/
|
|
75
|
-
export function extractToolInfo(toolName: string, args: Record<string, unknown>): ToolInfo {
|
|
76
|
-
const
|
|
77
|
-
const repo = (args.repo as string) || "";
|
|
78
|
-
const title = (args.title as string) || null;
|
|
79
|
-
const body = (args.body as string) || null;
|
|
80
|
-
|
|
82
|
+
export function extractToolInfo(toolName: string, args: Record<string, unknown> | null | undefined): ToolInfo {
|
|
83
|
+
const fields: ComplianceRecordFields = extractRecordFields(toolName, args);
|
|
81
84
|
return {
|
|
82
|
-
toolName,
|
|
83
|
-
owner,
|
|
84
|
-
repo,
|
|
85
|
-
title,
|
|
86
|
-
body,
|
|
87
|
-
contentHash:
|
|
85
|
+
toolName: fields.toolName,
|
|
86
|
+
owner: fields.owner,
|
|
87
|
+
repo: fields.repo,
|
|
88
|
+
title: fields.title,
|
|
89
|
+
body: fields.body,
|
|
90
|
+
contentHash: fields.contentHash
|
|
88
91
|
};
|
|
89
92
|
}
|
|
90
93
|
|
|
91
|
-
|
|
92
|
-
* Compute a SHA256 hash of the content for integrity verification
|
|
93
|
-
*
|
|
94
|
-
* The hash is computed over a normalized JSON string containing
|
|
95
|
-
* the content-determining fields. This ensures:
|
|
96
|
-
* - Same content = same hash
|
|
97
|
-
* - Different content = different hash
|
|
98
|
-
*
|
|
99
|
-
* @param title - Issue/PR title (or null)
|
|
100
|
-
* @param body - Issue/PR body (or null)
|
|
101
|
-
* @returns Hash string in format "sha256:..."
|
|
102
|
-
*/
|
|
103
|
-
export function computeContentHash(title: string | null, body: string | null): string {
|
|
104
|
-
const content = JSON.stringify({
|
|
105
|
-
title: title || "",
|
|
106
|
-
body: body || ""
|
|
107
|
-
});
|
|
108
|
-
return "sha256:" + createHash("sha256").update(content).digest("hex");
|
|
109
|
-
}
|
|
94
|
+
export { computeContentHash, normalizeToolName } from "./contracts";
|
|
110
95
|
|
|
111
96
|
/**
|
|
112
97
|
* Extract GitHub issue/PR ID from a tool execution result
|
package/contracts.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Compliance Plugin - Centralized Contracts
|
|
3
|
+
*
|
|
4
|
+
* This module defines the contract structures and normalization functions
|
|
5
|
+
* used across the plugin to ensure consistency between:
|
|
6
|
+
* - Skill documentation (what LLM uses)
|
|
7
|
+
* - create_compliance_record tool (what gets stored)
|
|
8
|
+
* - tool.execute.before hook (what gets looked up)
|
|
9
|
+
*
|
|
10
|
+
* All content hashing, tool name normalization, and field extraction
|
|
11
|
+
* should go through functions in this module.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createHash } from "crypto";
|
|
15
|
+
import { SCHEMA } from "./schema";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* GitHub MCP tools use a `github_` prefix (e.g., github_issue_write).
|
|
19
|
+
* But the skill and compliance records use unprefixed names (e.g., issue_write).
|
|
20
|
+
* This map allows normalization between the two.
|
|
21
|
+
*/
|
|
22
|
+
const TOOL_PREFIX_NORMALIZE: Record<string, string> = {
|
|
23
|
+
github_issue_write: "issue_write",
|
|
24
|
+
github_issue_update: "issue_update",
|
|
25
|
+
github_pull_request_write: "pull_request_write",
|
|
26
|
+
github_pull_request_update: "pull_request_update",
|
|
27
|
+
github_add_issue_comment: "add_issue_comment",
|
|
28
|
+
github_add_pull_request_comment: "add_pull_request_comment",
|
|
29
|
+
github_pull_request_review: "pull_request_review",
|
|
30
|
+
github_pull_request_review_comment: "pull_request_review_comment",
|
|
31
|
+
github_create_pull_request: "create_pull_request",
|
|
32
|
+
github_update_issue: "update_issue",
|
|
33
|
+
github_update_pull_request: "update_pull_request",
|
|
34
|
+
github_push_files: "push_files",
|
|
35
|
+
github_create_branch: "create_branch",
|
|
36
|
+
github_create_or_update_file: "create_or_update_file",
|
|
37
|
+
github_delete_file: "delete_file",
|
|
38
|
+
github_create_repository: "create_repository",
|
|
39
|
+
github_create_pull_request_with_copilot: "create_pull_request_with_copilot",
|
|
40
|
+
github_update_pull_request_branch: "update_pull_request_branch",
|
|
41
|
+
github_reply_to_pull_request_comment: "reply_to_pull_request_comment",
|
|
42
|
+
github_add_reply_to_pull_request_comment: "add_reply_to_pull_request_comment"
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Normalize a tool name to its unprefixed form.
|
|
47
|
+
* Both "issue_write" and "github_issue_write" return "issue_write".
|
|
48
|
+
*
|
|
49
|
+
* @param toolName - The tool name to normalize
|
|
50
|
+
* @returns Normalized tool name (unprefixed)
|
|
51
|
+
*/
|
|
52
|
+
export function normalizeToolName(toolName: string): string {
|
|
53
|
+
return TOOL_PREFIX_NORMALIZE[toolName] || toolName;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Compute a SHA256 hash of the content for integrity verification.
|
|
58
|
+
*
|
|
59
|
+
* This is the CENTRALIZED content hash computation.
|
|
60
|
+
* Used by BOTH:
|
|
61
|
+
* - tools.ts when creating compliance records
|
|
62
|
+
* - plugin.ts when looking up compliance records
|
|
63
|
+
*
|
|
64
|
+
* IMPORTANT: The skill instructs LLM to base64 encode title/body when
|
|
65
|
+
* calling create_compliance_record. tools.ts decodes them before passing
|
|
66
|
+
* to createComplianceRecord(). So the values stored in DB are DECODED.
|
|
67
|
+
*
|
|
68
|
+
* When the plugin looks up records, it receives the RAW (decoded) values
|
|
69
|
+
* from the tool arguments. So we compute hash from the same decoded values.
|
|
70
|
+
*
|
|
71
|
+
* @param title - Issue/PR title (or null)
|
|
72
|
+
* @param body - Issue/PR body (or null)
|
|
73
|
+
* @returns Hash string in format "sha256:..."
|
|
74
|
+
*/
|
|
75
|
+
export function computeContentHash(title: string | null, body: string | null): string {
|
|
76
|
+
const content = JSON.stringify({
|
|
77
|
+
title: title || "",
|
|
78
|
+
body: body || ""
|
|
79
|
+
});
|
|
80
|
+
return "sha256:" + createHash("sha256").update(content).digest("hex");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Fields required for compliance record creation and lookup.
|
|
85
|
+
* This interface represents the minimal set of fields needed to:
|
|
86
|
+
* 1. Create a compliance record (tools.ts)
|
|
87
|
+
* 2. Look up a compliance record (plugin.ts)
|
|
88
|
+
*/
|
|
89
|
+
export interface ComplianceRecordFields {
|
|
90
|
+
toolName: string;
|
|
91
|
+
owner: string;
|
|
92
|
+
repo: string;
|
|
93
|
+
title: string | null;
|
|
94
|
+
body: string | null;
|
|
95
|
+
contentHash: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Extract compliance-relevant fields from tool arguments.
|
|
100
|
+
*
|
|
101
|
+
* This function handles:
|
|
102
|
+
* - Null/undefined args
|
|
103
|
+
* - Tool name normalization (github_ prefix)
|
|
104
|
+
* - Content hash computation
|
|
105
|
+
*
|
|
106
|
+
* @param toolName - Name of the tool being called (can be prefixed or unprefixed)
|
|
107
|
+
* @param args - Arguments passed to the tool (can be null/undefined)
|
|
108
|
+
* @returns Extracted fields with computed content hash
|
|
109
|
+
*/
|
|
110
|
+
export function extractRecordFields(toolName: string, args: Record<string, unknown> | null | undefined): ComplianceRecordFields {
|
|
111
|
+
const normalizedToolName = normalizeToolName(toolName);
|
|
112
|
+
|
|
113
|
+
if (!args || typeof args !== 'object') {
|
|
114
|
+
return {
|
|
115
|
+
toolName: normalizedToolName,
|
|
116
|
+
owner: "",
|
|
117
|
+
repo: "",
|
|
118
|
+
title: null,
|
|
119
|
+
body: null,
|
|
120
|
+
contentHash: computeContentHash(null, null)
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const owner = (args.owner as string) || "";
|
|
125
|
+
const repo = (args.repo as string) || "";
|
|
126
|
+
const title = (args.title as string) || null;
|
|
127
|
+
const body = (args.body as string) || null;
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
toolName: normalizedToolName,
|
|
131
|
+
owner,
|
|
132
|
+
repo,
|
|
133
|
+
title,
|
|
134
|
+
body,
|
|
135
|
+
contentHash: computeContentHash(title, body)
|
|
136
|
+
};
|
|
137
|
+
}
|