@ossdeveloper/github-compliance 1.0.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/errors.ts ADDED
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Error message builder for compliance violations
3
+ *
4
+ * Generates user-friendly error messages when compliance checks fail.
5
+ * These messages are shown to the LLM when a write operation is blocked.
6
+ */
7
+
8
+ import { ERROR_REASONS } from "./schema";
9
+
10
+ export interface ValidationResult {
11
+ valid: boolean;
12
+ reason: string;
13
+ message: string;
14
+ }
15
+
16
+ /**
17
+ * Build a detailed blocked error message for compliance violations
18
+ *
19
+ * The error message includes:
20
+ * - The reason for blocking
21
+ * - Instructions on how to create a valid compliance record
22
+ * - Reference to the skill documentation
23
+ *
24
+ * @param validation - Validation result with reason and message
25
+ * @returns Formatted error message string
26
+ */
27
+ export function buildBlockedError(validation: ValidationResult): string {
28
+ const skillInstruction = `skill({ name: 'github-compliance' })`;
29
+
30
+ const baseError = `
31
+ [GITHUB COMPLIANCE BLOCKED]
32
+
33
+ Reason: ${validation.message}
34
+
35
+ To proceed:
36
+ 1. Load the github-compliance skill:
37
+ ${skillInstruction}
38
+
39
+ 2. Follow the workflow to create a valid compliance record:
40
+ - Read CONTRIBUTING.md for the target repository
41
+ - Search for existing issues/PRs to avoid duplicates
42
+ - Draft your issue/PR content
43
+ - Present draft to user and wait for explicit approval
44
+ - Call create_compliance_record with approval details
45
+
46
+ 3. After compliance record is created, retry this operation.
47
+
48
+ Skill location: ~/.config/opencode/skills/github-compliance/SKILL.md
49
+ `;
50
+
51
+ if (validation.reason === ERROR_REASONS.NO_RECORD) {
52
+ return baseError;
53
+ }
54
+
55
+ if (validation.reason === ERROR_REASONS.NOT_APPROVED) {
56
+ return `
57
+ [GITHUB COMPLIANCE BLOCKED]
58
+
59
+ Reason: Compliance record exists but has not been approved by user.
60
+
61
+ Your draft was created but not yet approved. Please:
62
+ 1. Present the draft to the user
63
+ 2. Wait for explicit approval ("yes", "proceed", etc.)
64
+ 3. Call create_compliance_record with approved status
65
+ 4. Retry this operation
66
+
67
+ ${baseError}
68
+ `;
69
+ }
70
+
71
+ if (validation.reason === ERROR_REASONS.ALREADY_USED) {
72
+ return `
73
+ [GITHUB COMPLIANCE BLOCKED]
74
+
75
+ Reason: This compliance record has already been used.
76
+
77
+ Each GitHub write operation requires its own compliance record.
78
+ Please create a new compliance record for this operation.
79
+
80
+ ${baseError}
81
+ `;
82
+ }
83
+
84
+ if (validation.reason === ERROR_REASONS.EXPIRED) {
85
+ return `
86
+ [GITHUB COMPLIANCE BLOCKED]
87
+
88
+ Reason: Your compliance record has expired (30 minute limit).
89
+
90
+ Please create a new compliance record by:
91
+ 1. Loading the skill: ${skillInstruction}
92
+ 2. Presenting draft to user and getting approval again
93
+ 3. Creating a new compliance record
94
+ 4. Retrying this operation
95
+
96
+ ${baseError}
97
+ `;
98
+ }
99
+
100
+ if (validation.reason === ERROR_REASONS.DATABASE_ERROR) {
101
+ return `
102
+ [GITHUB COMPLIANCE BLOCKED]
103
+
104
+ Reason: Compliance database error (${validation.message})
105
+
106
+ Please try again. If the problem persists, restart your OpenCode session.
107
+
108
+ ${baseError}
109
+ `;
110
+ }
111
+
112
+ return baseError;
113
+ }
114
+
115
+ /**
116
+ * Build a compliance JSON snippet for embedding in issue/PR body
117
+ *
118
+ * This JSON is added to the end of issue/PR bodies to provide
119
+ * transparency about the compliance process that was followed.
120
+ *
121
+ * @param clientIssueId - UUID from the compliance record
122
+ * @param githubUsername - GitHub username of approver
123
+ * @param approvedAt - ISO8601 timestamp of approval
124
+ * @param checksPerformed - List of check types performed
125
+ * @returns Formatted markdown string with JSON compliance record
126
+ */
127
+ export function buildComplianceJson(
128
+ clientIssueId: string,
129
+ githubUsername: string,
130
+ approvedAt: string,
131
+ checksPerformed: string[]
132
+ ): string {
133
+ const compliance = {
134
+ mcp_compliance: {
135
+ version: "1.0",
136
+ client: {
137
+ name: "opencode",
138
+ client_issue_id: clientIssueId
139
+ },
140
+ human_approval: {
141
+ github_username: githubUsername,
142
+ approved_at: approvedAt
143
+ },
144
+ checks_performed: checksPerformed
145
+ }
146
+ };
147
+
148
+ return `\n\n---\n\n## MCP Compliance Record\n\n\`\`\`json\n${JSON.stringify(compliance, null, 2)}\n\`\`\`\n`;
149
+ }
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@ossdeveloper/github-compliance",
3
+ "version": "1.0.0",
4
+ "description": "GitHub compliance plugin for OpenCode - enforces human approval for write operations",
5
+ "main": "dist/plugin.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "bun build plugin.ts --target=bun --outfile=dist/plugin.js"
9
+ },
10
+ "dependencies": {},
11
+ "peerDependencies": {
12
+ "@opencode-ai/plugin": "^1.0.0"
13
+ }
14
+ }
package/plugin.ts ADDED
@@ -0,0 +1,177 @@
1
+ /**
2
+ * GitHub Compliance Plugin for OpenCode
3
+ *
4
+ * Enforces mandatory human approval for GitHub write operations.
5
+ * Blocks write operations without valid compliance records in SQLite.
6
+ */
7
+
8
+ import type { Plugin } from "@opencode-ai/plugin";
9
+ import {
10
+ initDatabase,
11
+ getComplianceRecord,
12
+ validateCompliance,
13
+ markRecordUsed,
14
+ logAudit,
15
+ logFailedAttempt,
16
+ type ComplianceRecord
17
+ } from "./database";
18
+ import {
19
+ isGitHubWriteTool,
20
+ extractToolInfo,
21
+ extractGitHubUrl,
22
+ extractGitHubId,
23
+ type ToolInfo
24
+ } from "./compliance";
25
+ import { buildBlockedError } from "./errors";
26
+ import { tools } from "./tools";
27
+ import { ERROR_REASONS } from "./schema";
28
+
29
+ export const GitHubCompliancePlugin: Plugin = async (_ctx) => {
30
+ await initDatabase();
31
+
32
+ return {
33
+ /** Custom tools exposed to the LLM */
34
+ tool: tools,
35
+
36
+ /**
37
+ * Pre-execution hook for tool calls
38
+ *
39
+ * Intercepts GitHub write tools and validates compliance:
40
+ * - Checks for existing compliance record in SQLite
41
+ * - Validates record status (approved, not expired, not used)
42
+ * - Blocks tool execution if validation fails
43
+ * - Marks record as 'used' if validation succeeds
44
+ */
45
+ "tool.execute.before": async (input, output) => {
46
+ // Only intercept GitHub write tools - let everything else pass through
47
+ if (!isGitHubWriteTool(input.tool)) {
48
+ return;
49
+ }
50
+
51
+ let toolInfo: ToolInfo;
52
+ let record: ComplianceRecord | null = null;
53
+
54
+ // Extract tool-specific information for compliance lookup
55
+ try {
56
+ toolInfo = extractToolInfo(input.tool, input.args as Record<string, unknown>);
57
+ } catch (error) {
58
+ // Log extraction failures for debugging
59
+ await logFailedAttempt({
60
+ tool_name: input.tool,
61
+ owner: "unknown",
62
+ repo: "unknown",
63
+ reason: ERROR_REASONS.EXTRACTION_ERROR,
64
+ content_hash: null,
65
+ error_details: `Failed to extract tool info: ${error instanceof Error ? error.message : String(error)}`
66
+ });
67
+
68
+ throw new Error(buildBlockedError({
69
+ valid: false,
70
+ reason: ERROR_REASONS.DATABASE_ERROR,
71
+ message: `Failed to parse tool arguments: ${error instanceof Error ? error.message : String(error)}`
72
+ }));
73
+ }
74
+
75
+ try {
76
+ // Look up compliance record by tool, repo, and content hash
77
+ record = await getComplianceRecord(
78
+ toolInfo.toolName,
79
+ toolInfo.owner,
80
+ toolInfo.repo,
81
+ toolInfo.contentHash
82
+ );
83
+
84
+ // Validate the record (status, expiration, etc.)
85
+ const validation = await validateCompliance(record, toolInfo);
86
+
87
+ if (!validation.valid) {
88
+ // Log rejected attempt for audit
89
+ await logFailedAttempt({
90
+ tool_name: toolInfo.toolName,
91
+ owner: toolInfo.owner,
92
+ repo: toolInfo.repo,
93
+ reason: validation.reason,
94
+ content_hash: toolInfo.contentHash,
95
+ provided_record_id: record?.id || null,
96
+ error_details: validation.message
97
+ });
98
+
99
+ // Block the tool execution with detailed error message
100
+ throw new Error(buildBlockedError(validation));
101
+ }
102
+
103
+ // Record is valid - mark as used to prevent replay
104
+ await markRecordUsed(record.id);
105
+
106
+ // Attach metadata for after hook
107
+ (output as Record<string, unknown>).__complianceRecordId = record.id;
108
+ (output as Record<string, unknown>).__toolInfo = toolInfo;
109
+
110
+ } catch (error) {
111
+ // Re-throw blocked errors as-is
112
+ if (error instanceof Error && error.message.includes("[GITHUB COMPLIANCE BLOCKED]")) {
113
+ throw error;
114
+ }
115
+
116
+ // Unexpected errors - log and block
117
+ await logFailedAttempt({
118
+ tool_name: toolInfo.toolName,
119
+ owner: toolInfo.owner,
120
+ repo: toolInfo.repo,
121
+ reason: ERROR_REASONS.UNEXPECTED_ERROR,
122
+ content_hash: toolInfo.contentHash,
123
+ provided_record_id: record?.id || null,
124
+ error_details: error instanceof Error ? error.message : String(error)
125
+ });
126
+
127
+ throw new Error(buildBlockedError({
128
+ valid: false,
129
+ reason: ERROR_REASONS.DATABASE_ERROR,
130
+ message: error instanceof Error ? error.message : String(error)
131
+ }));
132
+ }
133
+ },
134
+
135
+ /**
136
+ * Post-execution hook for successful tool calls
137
+ *
138
+ * Logs successful GitHub write operations to audit trail.
139
+ * Only logs for GitHub write tools that were validated.
140
+ */
141
+ "tool.execute.after": async (input, output) => {
142
+ // Only process GitHub write tools we intercepted
143
+ if (!isGitHubWriteTool(input.tool)) {
144
+ return;
145
+ }
146
+
147
+ // Skip if no compliance record was attached (shouldn't happen)
148
+ const complianceRecordId = (output as Record<string, unknown>).__complianceRecordId as string | undefined;
149
+ const toolInfo = (output as Record<string, unknown>).__toolInfo as ToolInfo | undefined;
150
+
151
+ if (!complianceRecordId || !toolInfo) {
152
+ return;
153
+ }
154
+
155
+ // Extract GitHub response data for audit
156
+ const githubUrl = extractGitHubUrl(output.result);
157
+ const githubId = extractGitHubId(output.result);
158
+
159
+ // Log successful execution
160
+ await logAudit({
161
+ compliance_record_id: complianceRecordId,
162
+ tool_name: toolInfo.toolName,
163
+ owner: toolInfo.owner,
164
+ repo: toolInfo.repo,
165
+ github_username: "unknown",
166
+ status: "success",
167
+ error_message: null,
168
+ github_url: githubUrl,
169
+ github_id: githubId,
170
+ executed_at: new Date().toISOString(),
171
+ content_preview: toolInfo.body ? toolInfo.body.substring(0, 500) : null
172
+ });
173
+ }
174
+ };
175
+ };
176
+
177
+ export default GitHubCompliancePlugin;
package/schema.ts ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * GitHub Compliance Plugin for OpenCode
3
+ *
4
+ * Enforces mandatory human approval for GitHub write operations.
5
+ */
6
+
7
+ // Centralized schema - single source of truth for all column/table names
8
+ export const SCHEMA = {
9
+ tables: {
10
+ COMPLIANCE_RECORDS: "compliance_records",
11
+ COMPLIANCE_CHECKS: "compliance_checks",
12
+ AUDIT_LOG: "audit_log",
13
+ FAILED_ATTEMPTS: "failed_attempts"
14
+ },
15
+ columns: {
16
+ // compliance_records
17
+ ID: "id",
18
+ TOOL_NAME: "tool_name",
19
+ OWNER: "owner",
20
+ REPO: "repo",
21
+ TITLE: "title",
22
+ CONTENT_HASH: "content_hash",
23
+ GITHUB_USERNAME: "github_username",
24
+ APPROVED_AT: "approved_at",
25
+ EXPIRES_AT: "expires_at",
26
+ STATUS: "status",
27
+ CREATED_AT: "created_at",
28
+ CLIENT_NAME: "client_name",
29
+ POLICY_VERSION: "policy_version",
30
+ // compliance_checks
31
+ COMPLIANCE_RECORD_ID: "compliance_record_id",
32
+ CHECK_TYPE: "check_type",
33
+ CHECK_PASSED: "check_passed",
34
+ DETAILS: "details",
35
+ // audit_log & failed_attempts
36
+ ERROR_MESSAGE: "error_message",
37
+ GITHUB_URL: "github_url",
38
+ GITHUB_ID: "github_id",
39
+ EXECUTED_AT: "executed_at",
40
+ CONTENT_PREVIEW: "content_preview",
41
+ // failed_attempts
42
+ PROVIDED_RECORD_ID: "provided_record_id",
43
+ ATTEMPTED_AT: "attempted_at",
44
+ ERROR_DETAILS: "error_details"
45
+ },
46
+ indexes: {
47
+ COMPLIANCE_LOOKUP: "idx_compliance_lookup",
48
+ COMPLIANCE_EXPIRES: "idx_compliance_expires"
49
+ }
50
+ } as const;
51
+
52
+ // Record status enum
53
+ export const RECORD_STATUS = {
54
+ PENDING: "pending",
55
+ APPROVED: "approved",
56
+ EXPIRED: "expired",
57
+ USED: "used"
58
+ } as const;
59
+
60
+ // Check types
61
+ export const CHECK_TYPES = {
62
+ CONTRIBUTING_MD_READ: "contributing_md_read",
63
+ DUPLICATE_SEARCHED: "duplicate_searched",
64
+ TEMPLATE_FOLLOWED: "template_followed",
65
+ HUMAN_APPROVAL_OBTAINED: "human_approval_obtained",
66
+ POLICY_ACKNOWLEDGED: "policy_acknowledged",
67
+ VOUCH_VERIFIED: "vouch_verified"
68
+ } as const;
69
+
70
+ // Error reasons
71
+ export const ERROR_REASONS = {
72
+ NO_RECORD: "NO_RECORD",
73
+ NOT_APPROVED: "NOT_APPROVED",
74
+ ALREADY_USED: "ALREADY_USED",
75
+ EXPIRED: "EXPIRED",
76
+ DATABASE_ERROR: "DATABASE_ERROR",
77
+ EXTRACTION_ERROR: "EXTRACTION_ERROR",
78
+ UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
79
+ } as const;
80
+
81
+ // Record expiration time in milliseconds (30 minutes)
82
+ export const RECORD_TTL_MS = 30 * 60 * 1000;
83
+
84
+ // Record cleanup age in milliseconds (7 days)
85
+ export const RECORD_CLEANUP_AGE_MS = 7 * 24 * 60 * 60 * 1000;
86
+
87
+ // Content preview max length
88
+ export const CONTENT_PREVIEW_MAX_LENGTH = 500;
package/tools.ts ADDED
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Custom tools exposed to the LLM by the GitHub Compliance Plugin
3
+ *
4
+ * These tools allow the LLM to interact with the compliance system:
5
+ * - create_compliance_record: Create a record after user approval
6
+ * - get_compliance_status: Check if a valid record exists
7
+ */
8
+
9
+ import { tool } from "@opencode-ai/plugin";
10
+ import type { Plugin } from "@opencode-ai/plugin";
11
+ import {
12
+ createComplianceRecord,
13
+ getComplianceStatus,
14
+ type CreateRecordInput
15
+ } from "./database";
16
+
17
+ /**
18
+ * Create a compliance record after user approval
19
+ *
20
+ * This tool MUST be called AFTER:
21
+ * 1. Reading CONTRIBUTING.md and other community files
22
+ * 2. Searching for duplicates
23
+ * 3. Drafting the issue/PR content
24
+ * 4. Presenting draft to user
25
+ * 5. Getting explicit user approval ("yes", "proceed", etc.)
26
+ *
27
+ * The tool records that human approval was obtained and creates
28
+ * a valid compliance record that will allow the GitHub write operation.
29
+ */
30
+ export const createComplianceRecordTool = tool({
31
+ description: "Create a compliance record AFTER user approves the draft. MUST be called after explicit user approval. This records that human approval was obtained and allows the GitHub write operation to proceed.",
32
+ args: {
33
+ tool_name: tool.schema.string({ description: "GitHub tool name (e.g., issue_write, pull_request_write)" }),
34
+ owner: tool.schema.string({ description: "Repository owner (user or org)" }),
35
+ repo: tool.schema.string({ description: "Repository name" }),
36
+ title: tool.schema.string().optional({ description: "Issue/PR title" }),
37
+ body: tool.schema.string({ description: "Issue/PR body content" }),
38
+ github_username: tool.schema.string({ description: "GitHub username of human approver" }),
39
+ approved_at: tool.schema.string({ description: "ISO8601 timestamp when user approved" }),
40
+ checks: tool.schema.array(tool.schema.object({
41
+ check_type: tool.schema.string({ description: "Type of check performed" }),
42
+ passed: tool.schema.boolean({ description: "Whether check passed" }),
43
+ details: tool.schema.record(tool.schema.string(), tool.schema.any()).optional({ description: "Additional details" })
44
+ })).optional({ description: "List of compliance checks performed" })
45
+ },
46
+ async execute(args, _context) {
47
+ const input: CreateRecordInput = {
48
+ tool_name: args.tool_name,
49
+ owner: args.owner,
50
+ repo: args.repo,
51
+ title: args.title || null,
52
+ body: args.body,
53
+ github_username: args.github_username,
54
+ approved_at: args.approved_at,
55
+ checks: args.checks || []
56
+ };
57
+
58
+ const recordId = await createComplianceRecord(input);
59
+
60
+ return {
61
+ success: true,
62
+ record_id: recordId,
63
+ message: "Compliance record created. You may now execute the GitHub write operation.",
64
+ expires_at: new Date(Date.now() + 30 * 60 * 1000).toISOString()
65
+ };
66
+ }
67
+ });
68
+
69
+ /**
70
+ * Check compliance status for a planned operation
71
+ *
72
+ * Use this to verify if a valid compliance record already exists
73
+ * before attempting a GitHub write operation.
74
+ */
75
+ export const getComplianceStatusTool = tool({
76
+ description: "Check if a valid compliance record exists for a planned GitHub write operation.",
77
+ args: {
78
+ tool_name: tool.schema.string({ description: "GitHub tool name" }),
79
+ owner: tool.schema.string({ description: "Repository owner" }),
80
+ repo: tool.schema.string({ description: "Repository name" }),
81
+ content_hash: tool.schema.string({ description: "SHA256 hash of the content (format: sha256:...)" })
82
+ },
83
+ async execute(args, _context) {
84
+ const status = await getComplianceStatus(args.tool_name, args.owner, args.repo, args.content_hash);
85
+ return status;
86
+ }
87
+ });
88
+
89
+ /**
90
+ * All custom tools exported by this plugin
91
+ */
92
+ export const tools = {
93
+ create_compliance_record: createComplianceRecordTool,
94
+ get_compliance_status: getComplianceStatusTool
95
+ };