@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/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
- // database.ts
83
- var DB_DIR = `${homedir()}/.opencode`;
84
- var DB_PATH = `${DB_DIR}/github-compliance.db`;
85
- var db = null;
86
- function uuid() {
87
- return crypto.randomUUID();
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
- toolInfo = extractToolInfo(input.tool, input.args);
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 getComplianceRecord(toolInfo.toolName, toolInfo.owner, toolInfo.repo, toolInfo.contentHash);
12888
- const validation = await validateCompliance(record2, toolInfo);
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
- repo: toolInfo.repo,
13007
+ toolInfo: toolInfo.repo,
12912
13008
  reason: ERROR_REASONS.UNEXPECTED_ERROR,
12913
13009
  content_hash: toolInfo.contentHash,
12914
- provided_record_id: record2?.id || null,
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 githubUrl = extractGitHubUrl(output.result);
12934
- const githubId = extractGitHubId(output.result);
12935
- await logAudit({
12936
- compliance_record_id: complianceRecordId,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossdeveloper/github-compliance",
3
- "version": "1.0.2",
3
+ "version": "1.3.0",
4
4
  "description": "GitHub compliance plugin for OpenCode - enforces human approval for write operations",
5
5
  "main": "dist/plugin.js",
6
6
  "type": "module",