@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/database.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Manages SQLite database for compliance records, audit logs, and failed attempts.
|
|
5
5
|
* Uses centralized schema from schema.ts for all table/column names.
|
|
6
|
+
* Uses centralized content hashing from contracts.ts.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import Database from "bun:sqlite";
|
|
9
|
-
import { createHash } from "crypto";
|
|
10
10
|
import { mkdir } from "fs";
|
|
11
11
|
import { homedir } from "os";
|
|
12
12
|
import {
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
RECORD_CLEANUP_AGE_MS,
|
|
17
17
|
CONTENT_PREVIEW_MAX_LENGTH
|
|
18
18
|
} from "./schema";
|
|
19
|
+
import { computeContentHash } from "./contracts";
|
|
19
20
|
|
|
20
21
|
const DB_DIR = `${homedir()}/.opencode`;
|
|
21
22
|
const DB_PATH = `${DB_DIR}/github-compliance.db`;
|
|
@@ -114,14 +115,6 @@ function uuid(): string {
|
|
|
114
115
|
return crypto.randomUUID();
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
function computeContentHash(title: string | null, body: string): string {
|
|
118
|
-
const content = JSON.stringify({
|
|
119
|
-
title: title || "",
|
|
120
|
-
body: body || ""
|
|
121
|
-
});
|
|
122
|
-
return "sha256:" + createHash("sha256").update(content).digest("hex");
|
|
123
|
-
}
|
|
124
|
-
|
|
125
118
|
// ============================================================================
|
|
126
119
|
// Database Initialization
|
|
127
120
|
// ============================================================================
|
package/dist/plugin.js
CHANGED
|
@@ -16,7 +16,6 @@ var __export = (target, all) => {
|
|
|
16
16
|
|
|
17
17
|
// database.ts
|
|
18
18
|
import Database from "bun:sqlite";
|
|
19
|
-
import { createHash } from "crypto";
|
|
20
19
|
import { mkdir } from "fs";
|
|
21
20
|
import { homedir } from "os";
|
|
22
21
|
|
|
@@ -79,12 +78,32 @@ var RECORD_TTL_MS = 30 * 60 * 1000;
|
|
|
79
78
|
var RECORD_CLEANUP_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
80
79
|
var CONTENT_PREVIEW_MAX_LENGTH = 500;
|
|
81
80
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
var
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
81
|
+
// contracts.ts
|
|
82
|
+
import { createHash } from "crypto";
|
|
83
|
+
var TOOL_PREFIX_NORMALIZE = {
|
|
84
|
+
github_issue_write: "issue_write",
|
|
85
|
+
github_issue_update: "issue_update",
|
|
86
|
+
github_pull_request_write: "pull_request_write",
|
|
87
|
+
github_pull_request_update: "pull_request_update",
|
|
88
|
+
github_add_issue_comment: "add_issue_comment",
|
|
89
|
+
github_add_pull_request_comment: "add_pull_request_comment",
|
|
90
|
+
github_pull_request_review: "pull_request_review",
|
|
91
|
+
github_pull_request_review_comment: "pull_request_review_comment",
|
|
92
|
+
github_create_pull_request: "create_pull_request",
|
|
93
|
+
github_update_issue: "update_issue",
|
|
94
|
+
github_update_pull_request: "update_pull_request",
|
|
95
|
+
github_push_files: "push_files",
|
|
96
|
+
github_create_branch: "create_branch",
|
|
97
|
+
github_create_or_update_file: "create_or_update_file",
|
|
98
|
+
github_delete_file: "delete_file",
|
|
99
|
+
github_create_repository: "create_repository",
|
|
100
|
+
github_create_pull_request_with_copilot: "create_pull_request_with_copilot",
|
|
101
|
+
github_update_pull_request_branch: "update_pull_request_branch",
|
|
102
|
+
github_reply_to_pull_request_comment: "reply_to_pull_request_comment",
|
|
103
|
+
github_add_reply_to_pull_request_comment: "add_reply_to_pull_request_comment"
|
|
104
|
+
};
|
|
105
|
+
function normalizeToolName(toolName) {
|
|
106
|
+
return TOOL_PREFIX_NORMALIZE[toolName] || toolName;
|
|
88
107
|
}
|
|
89
108
|
function computeContentHash(title, body) {
|
|
90
109
|
const content = JSON.stringify({
|
|
@@ -93,6 +112,39 @@ function computeContentHash(title, body) {
|
|
|
93
112
|
});
|
|
94
113
|
return "sha256:" + createHash("sha256").update(content).digest("hex");
|
|
95
114
|
}
|
|
115
|
+
function extractRecordFields(toolName, args) {
|
|
116
|
+
const normalizedToolName = normalizeToolName(toolName);
|
|
117
|
+
if (!args || typeof args !== "object") {
|
|
118
|
+
return {
|
|
119
|
+
toolName: normalizedToolName,
|
|
120
|
+
owner: "",
|
|
121
|
+
repo: "",
|
|
122
|
+
title: null,
|
|
123
|
+
body: null,
|
|
124
|
+
contentHash: computeContentHash(null, null)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const owner = args.owner || "";
|
|
128
|
+
const repo = args.repo || "";
|
|
129
|
+
const title = args.title || null;
|
|
130
|
+
const body = args.body || null;
|
|
131
|
+
return {
|
|
132
|
+
toolName: normalizedToolName,
|
|
133
|
+
owner,
|
|
134
|
+
repo,
|
|
135
|
+
title,
|
|
136
|
+
body,
|
|
137
|
+
contentHash: computeContentHash(title, body)
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// database.ts
|
|
142
|
+
var DB_DIR = `${homedir()}/.opencode`;
|
|
143
|
+
var DB_PATH = `${DB_DIR}/github-compliance.db`;
|
|
144
|
+
var db = null;
|
|
145
|
+
function uuid() {
|
|
146
|
+
return crypto.randomUUID();
|
|
147
|
+
}
|
|
96
148
|
async function initDatabase() {
|
|
97
149
|
if (db)
|
|
98
150
|
return;
|
|
@@ -318,7 +370,6 @@ async function logFailedAttempt(entry) {
|
|
|
318
370
|
}
|
|
319
371
|
|
|
320
372
|
// compliance.ts
|
|
321
|
-
import { createHash as createHash2 } from "crypto";
|
|
322
373
|
var GITHUB_WRITE_TOOLS_UNPREFIXED = [
|
|
323
374
|
"issue_write",
|
|
324
375
|
"issue_update",
|
|
@@ -346,27 +397,6 @@ var GITHUB_WRITE_TOOLS = [...GITHUB_WRITE_TOOLS_UNPREFIXED, ...GITHUB_WRITE_TOOL
|
|
|
346
397
|
function isGitHubWriteTool(toolName) {
|
|
347
398
|
return GITHUB_WRITE_TOOLS.includes(toolName);
|
|
348
399
|
}
|
|
349
|
-
function extractToolInfo(toolName, args) {
|
|
350
|
-
const owner = args.owner || "";
|
|
351
|
-
const repo = args.repo || "";
|
|
352
|
-
const title = args.title || null;
|
|
353
|
-
const body = args.body || null;
|
|
354
|
-
return {
|
|
355
|
-
toolName,
|
|
356
|
-
owner,
|
|
357
|
-
repo,
|
|
358
|
-
title,
|
|
359
|
-
body,
|
|
360
|
-
contentHash: computeContentHash2(title, body)
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
function computeContentHash2(title, body) {
|
|
364
|
-
const content = JSON.stringify({
|
|
365
|
-
title: title || "",
|
|
366
|
-
body: body || ""
|
|
367
|
-
});
|
|
368
|
-
return "sha256:" + createHash2("sha256").update(content).digest("hex");
|
|
369
|
-
}
|
|
370
400
|
function extractGitHubId(result) {
|
|
371
401
|
if (!result || typeof result !== "object")
|
|
372
402
|
return null;
|
|
@@ -12856,6 +12886,77 @@ var tools = {
|
|
|
12856
12886
|
};
|
|
12857
12887
|
|
|
12858
12888
|
// plugin.ts
|
|
12889
|
+
function isMcpTool(toolName) {
|
|
12890
|
+
return toolName.startsWith("github_");
|
|
12891
|
+
}
|
|
12892
|
+
function extractToolInfoFromArgs(toolName, args) {
|
|
12893
|
+
const fields = extractRecordFields(toolName, args);
|
|
12894
|
+
return {
|
|
12895
|
+
toolName: fields.toolName,
|
|
12896
|
+
owner: fields.owner,
|
|
12897
|
+
repo: fields.repo,
|
|
12898
|
+
title: fields.title,
|
|
12899
|
+
body: fields.body,
|
|
12900
|
+
contentHash: fields.contentHash
|
|
12901
|
+
};
|
|
12902
|
+
}
|
|
12903
|
+
function extractArgs(input, output) {
|
|
12904
|
+
if (output.args && typeof output.args === "object") {
|
|
12905
|
+
return output.args;
|
|
12906
|
+
}
|
|
12907
|
+
if (input.arguments && typeof input.arguments === "object") {
|
|
12908
|
+
return input.arguments;
|
|
12909
|
+
}
|
|
12910
|
+
if (input.args && typeof input.args === "object") {
|
|
12911
|
+
return input.args;
|
|
12912
|
+
}
|
|
12913
|
+
if (typeof input.arguments === "string") {
|
|
12914
|
+
try {
|
|
12915
|
+
return JSON.parse(input.arguments);
|
|
12916
|
+
} catch {}
|
|
12917
|
+
}
|
|
12918
|
+
return null;
|
|
12919
|
+
}
|
|
12920
|
+
async function validateAndProcess(toolName, owner, repo, contentHash, toolInfo) {
|
|
12921
|
+
const record2 = await getComplianceRecord(toolName, owner, repo, contentHash);
|
|
12922
|
+
const validation = await validateCompliance(record2, {
|
|
12923
|
+
toolName,
|
|
12924
|
+
owner,
|
|
12925
|
+
repo,
|
|
12926
|
+
contentHash
|
|
12927
|
+
});
|
|
12928
|
+
if (!validation.valid) {
|
|
12929
|
+
await logFailedAttempt({
|
|
12930
|
+
tool_name: toolName,
|
|
12931
|
+
owner,
|
|
12932
|
+
repo,
|
|
12933
|
+
reason: validation.reason,
|
|
12934
|
+
content_hash: contentHash,
|
|
12935
|
+
provided_record_id: record2?.id || null,
|
|
12936
|
+
error_details: validation.message
|
|
12937
|
+
});
|
|
12938
|
+
return { allowed: false, record: record2, validation };
|
|
12939
|
+
}
|
|
12940
|
+
await markRecordUsed(record2.id);
|
|
12941
|
+
return { allowed: true, record: record2, validation };
|
|
12942
|
+
}
|
|
12943
|
+
async function logSuccess(toolInfo, record2, result) {
|
|
12944
|
+
const githubUrl = extractGitHubUrl(result);
|
|
12945
|
+
const githubId = extractGitHubId(result);
|
|
12946
|
+
await logAudit({
|
|
12947
|
+
compliance_record_id: record2.id,
|
|
12948
|
+
tool_name: toolInfo.toolName,
|
|
12949
|
+
owner: toolInfo.owner,
|
|
12950
|
+
repo: toolInfo.repo,
|
|
12951
|
+
github_username: record2.github_username,
|
|
12952
|
+
status: "success",
|
|
12953
|
+
error_message: null,
|
|
12954
|
+
github_url: githubUrl,
|
|
12955
|
+
github_id: githubId,
|
|
12956
|
+
executed_at: new Date().toISOString(),
|
|
12957
|
+
content_preview: toolInfo.body ? toolInfo.body.substring(0, 500) : null
|
|
12958
|
+
});
|
|
12959
|
+
}
|
|
12859
12960
|
var GitHubCompliancePlugin = async (_ctx) => {
|
|
12860
12961
|
await initDatabase();
|
|
12861
12962
|
return {
|
|
@@ -12864,10 +12965,16 @@ var GitHubCompliancePlugin = async (_ctx) => {
|
|
|
12864
12965
|
if (!isGitHubWriteTool(input.tool)) {
|
|
12865
12966
|
return;
|
|
12866
12967
|
}
|
|
12968
|
+
const isMcp = isMcpTool(input.tool);
|
|
12969
|
+
if (isMcp) {
|
|
12970
|
+
output.__needsMcpValidation = true;
|
|
12971
|
+
output.__mcpToolName = input.tool;
|
|
12972
|
+
return;
|
|
12973
|
+
}
|
|
12867
12974
|
let toolInfo;
|
|
12868
|
-
let record2 = null;
|
|
12869
12975
|
try {
|
|
12870
|
-
|
|
12976
|
+
const args = extractArgs(input, output);
|
|
12977
|
+
toolInfo = extractToolInfoFromArgs(input.tool, args);
|
|
12871
12978
|
} catch (error45) {
|
|
12872
12979
|
await logFailedAttempt({
|
|
12873
12980
|
tool_name: input.tool,
|
|
@@ -12884,21 +12991,10 @@ var GitHubCompliancePlugin = async (_ctx) => {
|
|
|
12884
12991
|
}));
|
|
12885
12992
|
}
|
|
12886
12993
|
try {
|
|
12887
|
-
record2 = await
|
|
12888
|
-
|
|
12889
|
-
if (!validation.valid) {
|
|
12890
|
-
await logFailedAttempt({
|
|
12891
|
-
tool_name: toolInfo.toolName,
|
|
12892
|
-
owner: toolInfo.owner,
|
|
12893
|
-
repo: toolInfo.repo,
|
|
12894
|
-
reason: validation.reason,
|
|
12895
|
-
content_hash: toolInfo.contentHash,
|
|
12896
|
-
provided_record_id: record2?.id || null,
|
|
12897
|
-
error_details: validation.message
|
|
12898
|
-
});
|
|
12994
|
+
const { allowed, record: record2, validation } = await validateAndProcess(toolInfo.toolName, toolInfo.owner, toolInfo.repo, toolInfo.contentHash, toolInfo);
|
|
12995
|
+
if (!allowed) {
|
|
12899
12996
|
throw new Error(buildBlockedError(validation));
|
|
12900
12997
|
}
|
|
12901
|
-
await markRecordUsed(record2.id);
|
|
12902
12998
|
output.__complianceRecordId = record2.id;
|
|
12903
12999
|
output.__toolInfo = toolInfo;
|
|
12904
13000
|
} catch (error45) {
|
|
@@ -12908,10 +13004,10 @@ var GitHubCompliancePlugin = async (_ctx) => {
|
|
|
12908
13004
|
await logFailedAttempt({
|
|
12909
13005
|
tool_name: toolInfo.toolName,
|
|
12910
13006
|
owner: toolInfo.owner,
|
|
12911
|
-
|
|
13007
|
+
toolInfo: toolInfo.repo,
|
|
12912
13008
|
reason: ERROR_REASONS.UNEXPECTED_ERROR,
|
|
12913
13009
|
content_hash: toolInfo.contentHash,
|
|
12914
|
-
provided_record_id:
|
|
13010
|
+
provided_record_id: null,
|
|
12915
13011
|
error_details: error45 instanceof Error ? error45.message : String(error45)
|
|
12916
13012
|
});
|
|
12917
13013
|
throw new Error(buildBlockedError({
|
|
@@ -12925,26 +13021,63 @@ var GitHubCompliancePlugin = async (_ctx) => {
|
|
|
12925
13021
|
if (!isGitHubWriteTool(input.tool)) {
|
|
12926
13022
|
return;
|
|
12927
13023
|
}
|
|
13024
|
+
const isMcp = isMcpTool(input.tool);
|
|
13025
|
+
const needsMcpValidation = output.__needsMcpValidation === true;
|
|
13026
|
+
if (isMcp && needsMcpValidation) {
|
|
13027
|
+
let toolInfo2;
|
|
13028
|
+
try {
|
|
13029
|
+
const args = input.args;
|
|
13030
|
+
toolInfo2 = extractToolInfoFromArgs(input.tool, args || null);
|
|
13031
|
+
} catch (error45) {
|
|
13032
|
+
await logFailedAttempt({
|
|
13033
|
+
tool_name: input.tool,
|
|
13034
|
+
owner: "unknown",
|
|
13035
|
+
repo: "unknown",
|
|
13036
|
+
reason: ERROR_REASONS.EXTRACTION_ERROR,
|
|
13037
|
+
content_hash: null,
|
|
13038
|
+
error_details: `Failed to extract MCP tool info in after hook: ${error45 instanceof Error ? error45.message : String(error45)}`
|
|
13039
|
+
});
|
|
13040
|
+
return;
|
|
13041
|
+
}
|
|
13042
|
+
try {
|
|
13043
|
+
const { allowed, record: record3, validation } = await validateAndProcess(toolInfo2.toolName, toolInfo2.owner, toolInfo2.repo, toolInfo2.contentHash, toolInfo2);
|
|
13044
|
+
if (!allowed) {
|
|
13045
|
+
await logFailedAttempt({
|
|
13046
|
+
tool_name: toolInfo2.toolName,
|
|
13047
|
+
owner: toolInfo2.owner,
|
|
13048
|
+
repo: toolInfo2.repo,
|
|
13049
|
+
reason: validation.reason,
|
|
13050
|
+
content_hash: toolInfo2.contentHash,
|
|
13051
|
+
provided_record_id: record3?.id || null,
|
|
13052
|
+
error_details: "MCP tool executed without valid compliance record"
|
|
13053
|
+
});
|
|
13054
|
+
return;
|
|
13055
|
+
}
|
|
13056
|
+
if (record3) {
|
|
13057
|
+
await logSuccess(toolInfo2, record3, output.result);
|
|
13058
|
+
}
|
|
13059
|
+
} catch (error45) {
|
|
13060
|
+
await logFailedAttempt({
|
|
13061
|
+
tool_name: toolInfo2.toolName,
|
|
13062
|
+
owner: toolInfo2.owner,
|
|
13063
|
+
repo: toolInfo2.repo,
|
|
13064
|
+
reason: ERROR_REASONS.UNEXPECTED_ERROR,
|
|
13065
|
+
content_hash: toolInfo2.contentHash,
|
|
13066
|
+
provided_record_id: null,
|
|
13067
|
+
error_details: error45 instanceof Error ? error45.message : String(error45)
|
|
13068
|
+
});
|
|
13069
|
+
}
|
|
13070
|
+
return;
|
|
13071
|
+
}
|
|
12928
13072
|
const complianceRecordId = output.__complianceRecordId;
|
|
12929
13073
|
const toolInfo = output.__toolInfo;
|
|
12930
13074
|
if (!complianceRecordId || !toolInfo) {
|
|
12931
13075
|
return;
|
|
12932
13076
|
}
|
|
12933
|
-
const
|
|
12934
|
-
|
|
12935
|
-
|
|
12936
|
-
|
|
12937
|
-
tool_name: toolInfo.toolName,
|
|
12938
|
-
owner: toolInfo.owner,
|
|
12939
|
-
repo: toolInfo.repo,
|
|
12940
|
-
github_username: "unknown",
|
|
12941
|
-
status: "success",
|
|
12942
|
-
error_message: null,
|
|
12943
|
-
github_url: githubUrl,
|
|
12944
|
-
github_id: githubId,
|
|
12945
|
-
executed_at: new Date().toISOString(),
|
|
12946
|
-
content_preview: toolInfo.body ? toolInfo.body.substring(0, 500) : null
|
|
12947
|
-
});
|
|
13077
|
+
const record2 = await getComplianceRecord(toolInfo.toolName, toolInfo.owner, toolInfo.repo, toolInfo.contentHash);
|
|
13078
|
+
if (record2) {
|
|
13079
|
+
await logSuccess(toolInfo, record2, output.result);
|
|
13080
|
+
}
|
|
12948
13081
|
}
|
|
12949
13082
|
};
|
|
12950
13083
|
};
|
package/package.json
CHANGED