@h-rig/guard-plugin 0.0.6-alpha.156

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 ADDED
@@ -0,0 +1 @@
1
+ # @h-rig/guard-plugin
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bun
2
+ import type { HookContext, HookResult } from "@rig/contracts";
3
+ export declare const AUDIT_TRAIL_HOOK_ID = "@rig/guard-plugin:audit-trail";
4
+ export declare function auditTrailHandler(ctx: HookContext): HookResult;
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // packages/guard-plugin/src/hooks/audit-trail.ts
5
+ import { appendFileSync, mkdirSync } from "fs";
6
+ import { resolve } from "path";
7
+ import { runTypedHook } from "@rig/hook-kit";
8
+ import { resolveHarnessPaths } from "@rig/runtime/control-plane/native/utils";
9
+ var AUDIT_TRAIL_HOOK_ID = "@rig/guard-plugin:audit-trail";
10
+ function auditTrailHandler(ctx) {
11
+ const projectRoot = ctx.projectRoot;
12
+ const toolInput = ctx.toolInput;
13
+ const paths = resolveHarnessPaths(projectRoot);
14
+ mkdirSync(paths.logsDir, { recursive: true });
15
+ const taskId = ctx.taskId || "unknown";
16
+ const tool = ctx.toolName || "unknown";
17
+ const filePath = String(toolInput.file_path ?? toolInput.path ?? "none");
18
+ const command = String(toolInput.command ?? toolInput.cmd ?? "").slice(0, 180);
19
+ const payload = {
20
+ timestamp: new Date().toISOString(),
21
+ task: taskId,
22
+ tool,
23
+ path: filePath
24
+ };
25
+ if (tool === "Bash" && command) {
26
+ payload.command_preview = command;
27
+ }
28
+ appendFileSync(resolve(paths.logsDir, "audit.jsonl"), `${JSON.stringify(payload)}
29
+ `, "utf-8");
30
+ return { decision: "allow" };
31
+ }
32
+ if (import.meta.main || process.env.RIG_HOOK_ROLE === "audit-trail") {
33
+ runTypedHook(auditTrailHandler, { event: "PreToolUse" });
34
+ }
35
+ export {
36
+ auditTrailHandler,
37
+ AUDIT_TRAIL_HOOK_ID
38
+ };
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * import-guard hook — blocks cross-module internal imports.
4
+ *
5
+ * Single-channel plugin hook (see safety-guard.ts for the pattern).
6
+ */
7
+ import type { HookContext, HookResult } from "@rig/contracts";
8
+ export declare const IMPORT_GUARD_HOOK_ID = "@rig/guard-plugin:import-guard";
9
+ export declare function importGuardHandler(ctx: HookContext): Promise<HookResult>;
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // packages/guard-plugin/src/hooks/import-guard.ts
5
+ import { resolveTaskScopes, resolvePolicyContent, runTypedHook } from "@rig/hook-kit";
6
+ import { evaluate, seedPolicyFromContent } from "@rig/runtime/control-plane/runtime/guard";
7
+ var IMPORT_GUARD_HOOK_ID = "@rig/guard-plugin:import-guard";
8
+ async function importGuardHandler(ctx) {
9
+ const projectRoot = ctx.projectRoot;
10
+ seedPolicyFromContent(resolvePolicyContent(projectRoot));
11
+ const toolInput = ctx.toolInput;
12
+ const content = String(toolInput.content ?? toolInput.new_string ?? "");
13
+ if (!content) {
14
+ return { decision: "allow" };
15
+ }
16
+ const filePath = String(toolInput.file_path ?? toolInput.path ?? "");
17
+ const evaluation = {
18
+ type: "content-write",
19
+ file_path: filePath,
20
+ content
21
+ };
22
+ const taskId = ctx.taskId || "";
23
+ const scopes = taskId ? await resolveTaskScopes(projectRoot, taskId) : [];
24
+ const taskWorkspace = process.env.RIG_TASK_WORKSPACE || "";
25
+ const decision = evaluate({
26
+ projectRoot,
27
+ taskScopes: scopes,
28
+ evaluation,
29
+ ...taskId ? { taskId } : {},
30
+ ...taskWorkspace ? { taskWorkspace } : {}
31
+ });
32
+ const importViolations = decision.matchedRules.filter((r) => r.category === "import");
33
+ if (importViolations.length > 0 && decision.action === "block") {
34
+ const reasons = importViolations.map((r) => r.reason).join(`
35
+ `);
36
+ return {
37
+ decision: "block",
38
+ reason: `Cross-module internal import detected:
39
+ ${reasons}
40
+ Only import from a module public API (index.ts) or npm packages.`
41
+ };
42
+ }
43
+ return { decision: "allow" };
44
+ }
45
+ if (import.meta.main || process.env.RIG_HOOK_ROLE === "import-guard") {
46
+ runTypedHook(importGuardHandler, { event: "PreToolUse" });
47
+ }
48
+ export {
49
+ importGuardHandler,
50
+ IMPORT_GUARD_HOOK_ID
51
+ };
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bun
2
+ import type { HookContext, HookResult } from "@rig/contracts";
3
+ export declare const POST_EDIT_LINT_HOOK_ID = "@rig/guard-plugin:post-edit-lint";
4
+ export declare function postEditLintHandler(ctx: HookContext): HookResult;
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // packages/guard-plugin/src/hooks/post-edit-lint.ts
5
+ import { existsSync, readFileSync } from "fs";
6
+ import { runTypedHook } from "@rig/hook-kit";
7
+ var POST_EDIT_LINT_HOOK_ID = "@rig/guard-plugin:post-edit-lint";
8
+ function postEditLintHandler(ctx) {
9
+ const toolInput = ctx.toolInput;
10
+ const filePath = String(toolInput.file_path ?? toolInput.path ?? "");
11
+ if (!filePath || !/\.(ts|tsx|js|jsx)$/.test(filePath) || !existsSync(filePath)) {
12
+ return { decision: "allow" };
13
+ }
14
+ const content = readFileSync(filePath, "utf-8");
15
+ const warnings = [];
16
+ const consoleCount = (content.match(/console\.(log|debug)\(/g) || []).length;
17
+ if (consoleCount > 3) {
18
+ warnings.push(`${filePath}: ${consoleCount} console.log/debug calls. Remove before completion.`);
19
+ }
20
+ if (!/\.(test|spec)\./.test(filePath)) {
21
+ const markers = content.split(/\r?\n/).map((line, index) => ({ line, index: index + 1 })).filter((item) => /TODO|FIXME|HACK|\[STUB\]/.test(item.line)).slice(0, 5);
22
+ if (markers.length > 0) {
23
+ warnings.push(`${filePath}: contains TODO/FIXME/HACK/STUB markers:
24
+ ${markers.map((m) => `${m.index}: ${m.line}`).join(`
25
+ `)}`);
26
+ }
27
+ }
28
+ if (/catch\s*\([^)]*\)\s*\{\s*\}/.test(content)) {
29
+ warnings.push(`${filePath}: Empty catch block detected.`);
30
+ }
31
+ if (warnings.length > 0) {
32
+ return {
33
+ decision: "allow",
34
+ systemMessage: `POST-EDIT WARNINGS:
35
+ - ${warnings.join(`
36
+ - `)}
37
+
38
+ These are not blocking, but must be resolved before task completion.`
39
+ };
40
+ }
41
+ return { decision: "allow" };
42
+ }
43
+ if (import.meta.main || process.env.RIG_HOOK_ROLE === "post-edit-lint") {
44
+ runTypedHook(postEditLintHandler, { event: "PostToolUse" });
45
+ }
46
+ export {
47
+ postEditLintHandler,
48
+ POST_EDIT_LINT_HOOK_ID
49
+ };
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * safety-guard hook — blocks dangerous commands and content patterns.
4
+ *
5
+ * Single-channel plugin hook: the logic lives in `safetyGuardHandler`
6
+ * (a typed HookImplementation contributed by @rig/guard-plugin). The
7
+ * self-exec guard at the bottom keeps the standalone per-run binary and
8
+ * the seed's basename-role dispatch working by running the same handler
9
+ * through @rig/hook-kit's `runTypedHook`.
10
+ */
11
+ import type { HookContext, HookResult } from "@rig/contracts";
12
+ export declare const SAFETY_GUARD_HOOK_ID = "@rig/guard-plugin:safety-guard";
13
+ export declare function safetyGuardHandler(ctx: HookContext): Promise<HookResult>;
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // packages/guard-plugin/src/hooks/safety-guard.ts
5
+ import { resolveTaskScopes, resolvePolicyContent, runTypedHook } from "@rig/hook-kit";
6
+ import { evaluate, seedPolicyFromContent } from "@rig/runtime/control-plane/runtime/guard";
7
+ var SAFETY_GUARD_HOOK_ID = "@rig/guard-plugin:safety-guard";
8
+ async function safetyGuardHandler(ctx) {
9
+ const projectRoot = ctx.projectRoot;
10
+ seedPolicyFromContent(resolvePolicyContent(projectRoot));
11
+ const tool = ctx.toolName ?? "";
12
+ const toolInput = ctx.toolInput;
13
+ const command = String(toolInput.command ?? toolInput.cmd ?? "");
14
+ const content = String(toolInput.content ?? toolInput.new_string ?? "");
15
+ if (tool === "Bash" && !command) {
16
+ return { decision: "block", reason: "Bash tool payload did not include a command field." };
17
+ }
18
+ const taskId = ctx.taskId || "";
19
+ const scopes = taskId ? await resolveTaskScopes(projectRoot, taskId) : [];
20
+ const taskWorkspace = process.env.RIG_TASK_WORKSPACE || "";
21
+ const baseContext = {
22
+ projectRoot,
23
+ taskScopes: scopes,
24
+ evaluation: { type: "command", command: "" },
25
+ ...taskId ? { taskId } : {},
26
+ ...taskWorkspace ? { taskWorkspace } : {}
27
+ };
28
+ if (tool === "Bash" && command) {
29
+ const decision = evaluate({
30
+ ...baseContext,
31
+ evaluation: { type: "command", command }
32
+ });
33
+ if (!decision.allowed && decision.action === "block") {
34
+ const reasons = decision.matchedRules.map((r) => r.reason).join(`
35
+ `);
36
+ return { decision: "block", reason: reasons };
37
+ }
38
+ if (taskWorkspace && taskWorkspace !== projectRoot) {
39
+ const hostProjectRoot = process.env.RIG_HOST_PROJECT_ROOT || projectRoot;
40
+ const rootReposPrefix = `${hostProjectRoot}/repos/`;
41
+ if (command.includes(rootReposPrefix)) {
42
+ return {
43
+ decision: "block",
44
+ reason: `Absolute root repo path detected (${rootReposPrefix}). Use relative paths or worktree paths under ${taskWorkspace}.`
45
+ };
46
+ }
47
+ }
48
+ }
49
+ if (content) {
50
+ const filePath = String(toolInput.file_path ?? toolInput.path ?? "");
51
+ const contentDecision = evaluate({
52
+ ...baseContext,
53
+ evaluation: { type: "content-write", file_path: filePath, content }
54
+ });
55
+ const contentRules = contentDecision.matchedRules.filter((r) => r.category === "content");
56
+ if (contentRules.length > 0 && contentDecision.action === "block") {
57
+ const reasons = contentRules.map((r) => r.reason).join(`
58
+ `);
59
+ return { decision: "block", reason: reasons };
60
+ }
61
+ }
62
+ return { decision: "allow" };
63
+ }
64
+ if (import.meta.main || process.env.RIG_HOOK_ROLE === "safety-guard") {
65
+ runTypedHook(safetyGuardHandler, { event: "PreToolUse" });
66
+ }
67
+ export {
68
+ safetyGuardHandler,
69
+ SAFETY_GUARD_HOOK_ID
70
+ };
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * scope-guard hook — enforces task scope boundaries.
4
+ *
5
+ * Single-channel plugin hook (see safety-guard.ts for the pattern).
6
+ */
7
+ import type { HookContext, HookResult } from "@rig/contracts";
8
+ export declare const SCOPE_GUARD_HOOK_ID = "@rig/guard-plugin:scope-guard";
9
+ export declare function scopeGuardHandler(ctx: HookContext): Promise<HookResult>;
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // packages/guard-plugin/src/hooks/scope-guard.ts
5
+ import { resolveTaskScopes, resolvePolicyContent, runTypedHook } from "@rig/hook-kit";
6
+ import { evaluate, seedPolicyFromContent } from "@rig/runtime/control-plane/runtime/guard";
7
+ var SCOPE_GUARD_HOOK_ID = "@rig/guard-plugin:scope-guard";
8
+ async function scopeGuardHandler(ctx) {
9
+ const projectRoot = ctx.projectRoot;
10
+ seedPolicyFromContent(resolvePolicyContent(projectRoot));
11
+ const tool = ctx.toolName ?? "";
12
+ const toolInput = ctx.toolInput;
13
+ const taskWorkspace = process.env.RIG_TASK_WORKSPACE || "";
14
+ const hostProjectRoot = process.env.RIG_HOST_PROJECT_ROOT || projectRoot;
15
+ const runtimeMode = (process.env.RIG_TASK_RUNTIME_MODE || "").trim().toLowerCase();
16
+ const runtimeIsolationActive = runtimeMode !== "" && runtimeMode !== "off" || taskWorkspace !== "" && taskWorkspace !== projectRoot;
17
+ const rootReposPrefix = `${hostProjectRoot}/repos/`;
18
+ for (const filePath of ctx.filePaths) {
19
+ if (!filePath) {
20
+ continue;
21
+ }
22
+ const resolvesInsideTaskWorkspace = taskWorkspace !== "" && (filePath === taskWorkspace || filePath.startsWith(`${taskWorkspace}/`));
23
+ if (filePath.startsWith(rootReposPrefix) && !resolvesInsideTaskWorkspace && (taskWorkspace !== "" || runtimeIsolationActive)) {
24
+ return {
25
+ decision: "block",
26
+ reason: `Absolute root repo path detected (${rootReposPrefix}). Use task-runtime paths under ${taskWorkspace || "$RIG_TASK_WORKSPACE"}.`
27
+ };
28
+ }
29
+ if ((tool === "Read" || tool === "Glob" || tool === "Grep") && /^repos\//.test(filePath) && (taskWorkspace !== "" || runtimeIsolationActive)) {
30
+ return {
31
+ decision: "block",
32
+ reason: `Relative repo path '${filePath}' is blocked in runtime mode. Use absolute paths under ${taskWorkspace || "$RIG_TASK_WORKSPACE"}.`
33
+ };
34
+ }
35
+ }
36
+ const evaluation = {
37
+ type: "tool-call",
38
+ tool_name: tool,
39
+ tool_input: toolInput
40
+ };
41
+ const taskId = ctx.taskId || "";
42
+ if (!taskId) {
43
+ return { decision: "allow" };
44
+ }
45
+ const scopes = await resolveTaskScopes(projectRoot, taskId);
46
+ if (scopes.length === 0) {
47
+ return { decision: "allow" };
48
+ }
49
+ const decision = evaluate({
50
+ projectRoot,
51
+ taskScopes: scopes,
52
+ evaluation,
53
+ ...taskId ? { taskId } : {},
54
+ ...taskWorkspace ? { taskWorkspace } : {}
55
+ });
56
+ if (!decision.allowed && decision.action === "block") {
57
+ const reasons = decision.matchedRules.map((r) => r.reason).join(`
58
+ `);
59
+ return { decision: "block", reason: reasons };
60
+ }
61
+ return { decision: "allow" };
62
+ }
63
+ if (import.meta.main || process.env.RIG_HOOK_ROLE === "scope-guard") {
64
+ runTypedHook(scopeGuardHandler, { event: "PreToolUse" });
65
+ }
66
+ export {
67
+ scopeGuardHandler,
68
+ SCOPE_GUARD_HOOK_ID
69
+ };
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * test-integrity-guard hook — prevents test tampering (.skip, .only, etc.).
4
+ *
5
+ * Single-channel plugin hook (see safety-guard.ts for the pattern).
6
+ */
7
+ import type { HookContext, HookResult } from "@rig/contracts";
8
+ export declare const TEST_INTEGRITY_GUARD_HOOK_ID = "@rig/guard-plugin:test-integrity-guard";
9
+ export declare function testIntegrityGuardHandler(ctx: HookContext): Promise<HookResult>;
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // packages/guard-plugin/src/hooks/test-integrity-guard.ts
5
+ import { resolveTaskScopes, resolvePolicyContent, isTestFilePath, runTypedHook } from "@rig/hook-kit";
6
+ import { evaluate, seedPolicyFromContent } from "@rig/runtime/control-plane/runtime/guard";
7
+ var TEST_INTEGRITY_GUARD_HOOK_ID = "@rig/guard-plugin:test-integrity-guard";
8
+ async function testIntegrityGuardHandler(ctx) {
9
+ const projectRoot = ctx.projectRoot;
10
+ seedPolicyFromContent(resolvePolicyContent(projectRoot));
11
+ const toolInput = ctx.toolInput;
12
+ const filePath = String(toolInput.file_path ?? toolInput.path ?? "");
13
+ const content = String(toolInput.content ?? toolInput.new_string ?? "");
14
+ if (!filePath || !content) {
15
+ return { decision: "allow" };
16
+ }
17
+ const evaluation = {
18
+ type: "content-write",
19
+ file_path: filePath,
20
+ content
21
+ };
22
+ const taskId = ctx.taskId || "";
23
+ const scopes = taskId ? await resolveTaskScopes(projectRoot, taskId) : [];
24
+ const taskWorkspace = process.env.RIG_TASK_WORKSPACE || "";
25
+ const decision = evaluate({
26
+ projectRoot,
27
+ taskScopes: scopes,
28
+ evaluation,
29
+ ...taskId ? { taskId } : {},
30
+ ...taskWorkspace ? { taskWorkspace } : {}
31
+ });
32
+ const testViolations = decision.matchedRules.filter((r) => r.category === "test-integrity");
33
+ if (testViolations.length > 0 && decision.action === "block") {
34
+ const reasons = testViolations.map((r) => r.reason).join(`
35
+ `);
36
+ return { decision: "block", reason: reasons };
37
+ }
38
+ if (isTestFilePath(filePath) && /(test|it)\s*\(/.test(content) && !/expect\s*\(|assert/.test(content)) {
39
+ return {
40
+ decision: "allow",
41
+ systemMessage: `WARNING: Test block in ${filePath} may have no assertions.`
42
+ };
43
+ }
44
+ return { decision: "allow" };
45
+ }
46
+ if (import.meta.main || process.env.RIG_HOOK_ROLE === "test-integrity-guard") {
47
+ runTypedHook(testIntegrityGuardHandler, { event: "PreToolUse" });
48
+ }
49
+ export {
50
+ testIntegrityGuardHandler,
51
+ TEST_INTEGRITY_GUARD_HOOK_ID
52
+ };
@@ -0,0 +1,7 @@
1
+ export * from "./plugin";
2
+ export { safetyGuardHandler, SAFETY_GUARD_HOOK_ID } from "./hooks/safety-guard";
3
+ export { scopeGuardHandler, SCOPE_GUARD_HOOK_ID } from "./hooks/scope-guard";
4
+ export { importGuardHandler, IMPORT_GUARD_HOOK_ID } from "./hooks/import-guard";
5
+ export { testIntegrityGuardHandler, TEST_INTEGRITY_GUARD_HOOK_ID } from "./hooks/test-integrity-guard";
6
+ export { auditTrailHandler, AUDIT_TRAIL_HOOK_ID } from "./hooks/audit-trail";
7
+ export { postEditLintHandler, POST_EDIT_LINT_HOOK_ID } from "./hooks/post-edit-lint";
@@ -0,0 +1,368 @@
1
+ // @bun
2
+ // packages/guard-plugin/src/plugin.ts
3
+ import { definePlugin } from "@rig/core/config";
4
+
5
+ // packages/guard-plugin/src/hooks/safety-guard.ts
6
+ import { resolveTaskScopes, resolvePolicyContent, runTypedHook } from "@rig/hook-kit";
7
+ import { evaluate, seedPolicyFromContent } from "@rig/runtime/control-plane/runtime/guard";
8
+ var SAFETY_GUARD_HOOK_ID = "@rig/guard-plugin:safety-guard";
9
+ async function safetyGuardHandler(ctx) {
10
+ const projectRoot = ctx.projectRoot;
11
+ seedPolicyFromContent(resolvePolicyContent(projectRoot));
12
+ const tool = ctx.toolName ?? "";
13
+ const toolInput = ctx.toolInput;
14
+ const command = String(toolInput.command ?? toolInput.cmd ?? "");
15
+ const content = String(toolInput.content ?? toolInput.new_string ?? "");
16
+ if (tool === "Bash" && !command) {
17
+ return { decision: "block", reason: "Bash tool payload did not include a command field." };
18
+ }
19
+ const taskId = ctx.taskId || "";
20
+ const scopes = taskId ? await resolveTaskScopes(projectRoot, taskId) : [];
21
+ const taskWorkspace = process.env.RIG_TASK_WORKSPACE || "";
22
+ const baseContext = {
23
+ projectRoot,
24
+ taskScopes: scopes,
25
+ evaluation: { type: "command", command: "" },
26
+ ...taskId ? { taskId } : {},
27
+ ...taskWorkspace ? { taskWorkspace } : {}
28
+ };
29
+ if (tool === "Bash" && command) {
30
+ const decision = evaluate({
31
+ ...baseContext,
32
+ evaluation: { type: "command", command }
33
+ });
34
+ if (!decision.allowed && decision.action === "block") {
35
+ const reasons = decision.matchedRules.map((r) => r.reason).join(`
36
+ `);
37
+ return { decision: "block", reason: reasons };
38
+ }
39
+ if (taskWorkspace && taskWorkspace !== projectRoot) {
40
+ const hostProjectRoot = process.env.RIG_HOST_PROJECT_ROOT || projectRoot;
41
+ const rootReposPrefix = `${hostProjectRoot}/repos/`;
42
+ if (command.includes(rootReposPrefix)) {
43
+ return {
44
+ decision: "block",
45
+ reason: `Absolute root repo path detected (${rootReposPrefix}). Use relative paths or worktree paths under ${taskWorkspace}.`
46
+ };
47
+ }
48
+ }
49
+ }
50
+ if (content) {
51
+ const filePath = String(toolInput.file_path ?? toolInput.path ?? "");
52
+ const contentDecision = evaluate({
53
+ ...baseContext,
54
+ evaluation: { type: "content-write", file_path: filePath, content }
55
+ });
56
+ const contentRules = contentDecision.matchedRules.filter((r) => r.category === "content");
57
+ if (contentRules.length > 0 && contentDecision.action === "block") {
58
+ const reasons = contentRules.map((r) => r.reason).join(`
59
+ `);
60
+ return { decision: "block", reason: reasons };
61
+ }
62
+ }
63
+ return { decision: "allow" };
64
+ }
65
+ if (process.env.RIG_HOOK_ROLE === "safety-guard") {
66
+ runTypedHook(safetyGuardHandler, { event: "PreToolUse" });
67
+ }
68
+
69
+ // packages/guard-plugin/src/hooks/scope-guard.ts
70
+ import { resolveTaskScopes as resolveTaskScopes2, resolvePolicyContent as resolvePolicyContent2, runTypedHook as runTypedHook2 } from "@rig/hook-kit";
71
+ import { evaluate as evaluate2, seedPolicyFromContent as seedPolicyFromContent2 } from "@rig/runtime/control-plane/runtime/guard";
72
+ var SCOPE_GUARD_HOOK_ID = "@rig/guard-plugin:scope-guard";
73
+ async function scopeGuardHandler(ctx) {
74
+ const projectRoot = ctx.projectRoot;
75
+ seedPolicyFromContent2(resolvePolicyContent2(projectRoot));
76
+ const tool = ctx.toolName ?? "";
77
+ const toolInput = ctx.toolInput;
78
+ const taskWorkspace = process.env.RIG_TASK_WORKSPACE || "";
79
+ const hostProjectRoot = process.env.RIG_HOST_PROJECT_ROOT || projectRoot;
80
+ const runtimeMode = (process.env.RIG_TASK_RUNTIME_MODE || "").trim().toLowerCase();
81
+ const runtimeIsolationActive = runtimeMode !== "" && runtimeMode !== "off" || taskWorkspace !== "" && taskWorkspace !== projectRoot;
82
+ const rootReposPrefix = `${hostProjectRoot}/repos/`;
83
+ for (const filePath of ctx.filePaths) {
84
+ if (!filePath) {
85
+ continue;
86
+ }
87
+ const resolvesInsideTaskWorkspace = taskWorkspace !== "" && (filePath === taskWorkspace || filePath.startsWith(`${taskWorkspace}/`));
88
+ if (filePath.startsWith(rootReposPrefix) && !resolvesInsideTaskWorkspace && (taskWorkspace !== "" || runtimeIsolationActive)) {
89
+ return {
90
+ decision: "block",
91
+ reason: `Absolute root repo path detected (${rootReposPrefix}). Use task-runtime paths under ${taskWorkspace || "$RIG_TASK_WORKSPACE"}.`
92
+ };
93
+ }
94
+ if ((tool === "Read" || tool === "Glob" || tool === "Grep") && /^repos\//.test(filePath) && (taskWorkspace !== "" || runtimeIsolationActive)) {
95
+ return {
96
+ decision: "block",
97
+ reason: `Relative repo path '${filePath}' is blocked in runtime mode. Use absolute paths under ${taskWorkspace || "$RIG_TASK_WORKSPACE"}.`
98
+ };
99
+ }
100
+ }
101
+ const evaluation = {
102
+ type: "tool-call",
103
+ tool_name: tool,
104
+ tool_input: toolInput
105
+ };
106
+ const taskId = ctx.taskId || "";
107
+ if (!taskId) {
108
+ return { decision: "allow" };
109
+ }
110
+ const scopes = await resolveTaskScopes2(projectRoot, taskId);
111
+ if (scopes.length === 0) {
112
+ return { decision: "allow" };
113
+ }
114
+ const decision = evaluate2({
115
+ projectRoot,
116
+ taskScopes: scopes,
117
+ evaluation,
118
+ ...taskId ? { taskId } : {},
119
+ ...taskWorkspace ? { taskWorkspace } : {}
120
+ });
121
+ if (!decision.allowed && decision.action === "block") {
122
+ const reasons = decision.matchedRules.map((r) => r.reason).join(`
123
+ `);
124
+ return { decision: "block", reason: reasons };
125
+ }
126
+ return { decision: "allow" };
127
+ }
128
+ if (process.env.RIG_HOOK_ROLE === "scope-guard") {
129
+ runTypedHook2(scopeGuardHandler, { event: "PreToolUse" });
130
+ }
131
+
132
+ // packages/guard-plugin/src/hooks/import-guard.ts
133
+ import { resolveTaskScopes as resolveTaskScopes3, resolvePolicyContent as resolvePolicyContent3, runTypedHook as runTypedHook3 } from "@rig/hook-kit";
134
+ import { evaluate as evaluate3, seedPolicyFromContent as seedPolicyFromContent3 } from "@rig/runtime/control-plane/runtime/guard";
135
+ var IMPORT_GUARD_HOOK_ID = "@rig/guard-plugin:import-guard";
136
+ async function importGuardHandler(ctx) {
137
+ const projectRoot = ctx.projectRoot;
138
+ seedPolicyFromContent3(resolvePolicyContent3(projectRoot));
139
+ const toolInput = ctx.toolInput;
140
+ const content = String(toolInput.content ?? toolInput.new_string ?? "");
141
+ if (!content) {
142
+ return { decision: "allow" };
143
+ }
144
+ const filePath = String(toolInput.file_path ?? toolInput.path ?? "");
145
+ const evaluation = {
146
+ type: "content-write",
147
+ file_path: filePath,
148
+ content
149
+ };
150
+ const taskId = ctx.taskId || "";
151
+ const scopes = taskId ? await resolveTaskScopes3(projectRoot, taskId) : [];
152
+ const taskWorkspace = process.env.RIG_TASK_WORKSPACE || "";
153
+ const decision = evaluate3({
154
+ projectRoot,
155
+ taskScopes: scopes,
156
+ evaluation,
157
+ ...taskId ? { taskId } : {},
158
+ ...taskWorkspace ? { taskWorkspace } : {}
159
+ });
160
+ const importViolations = decision.matchedRules.filter((r) => r.category === "import");
161
+ if (importViolations.length > 0 && decision.action === "block") {
162
+ const reasons = importViolations.map((r) => r.reason).join(`
163
+ `);
164
+ return {
165
+ decision: "block",
166
+ reason: `Cross-module internal import detected:
167
+ ${reasons}
168
+ Only import from a module public API (index.ts) or npm packages.`
169
+ };
170
+ }
171
+ return { decision: "allow" };
172
+ }
173
+ if (process.env.RIG_HOOK_ROLE === "import-guard") {
174
+ runTypedHook3(importGuardHandler, { event: "PreToolUse" });
175
+ }
176
+
177
+ // packages/guard-plugin/src/hooks/test-integrity-guard.ts
178
+ import { resolveTaskScopes as resolveTaskScopes4, resolvePolicyContent as resolvePolicyContent4, isTestFilePath, runTypedHook as runTypedHook4 } from "@rig/hook-kit";
179
+ import { evaluate as evaluate4, seedPolicyFromContent as seedPolicyFromContent4 } from "@rig/runtime/control-plane/runtime/guard";
180
+ var TEST_INTEGRITY_GUARD_HOOK_ID = "@rig/guard-plugin:test-integrity-guard";
181
+ async function testIntegrityGuardHandler(ctx) {
182
+ const projectRoot = ctx.projectRoot;
183
+ seedPolicyFromContent4(resolvePolicyContent4(projectRoot));
184
+ const toolInput = ctx.toolInput;
185
+ const filePath = String(toolInput.file_path ?? toolInput.path ?? "");
186
+ const content = String(toolInput.content ?? toolInput.new_string ?? "");
187
+ if (!filePath || !content) {
188
+ return { decision: "allow" };
189
+ }
190
+ const evaluation = {
191
+ type: "content-write",
192
+ file_path: filePath,
193
+ content
194
+ };
195
+ const taskId = ctx.taskId || "";
196
+ const scopes = taskId ? await resolveTaskScopes4(projectRoot, taskId) : [];
197
+ const taskWorkspace = process.env.RIG_TASK_WORKSPACE || "";
198
+ const decision = evaluate4({
199
+ projectRoot,
200
+ taskScopes: scopes,
201
+ evaluation,
202
+ ...taskId ? { taskId } : {},
203
+ ...taskWorkspace ? { taskWorkspace } : {}
204
+ });
205
+ const testViolations = decision.matchedRules.filter((r) => r.category === "test-integrity");
206
+ if (testViolations.length > 0 && decision.action === "block") {
207
+ const reasons = testViolations.map((r) => r.reason).join(`
208
+ `);
209
+ return { decision: "block", reason: reasons };
210
+ }
211
+ if (isTestFilePath(filePath) && /(test|it)\s*\(/.test(content) && !/expect\s*\(|assert/.test(content)) {
212
+ return {
213
+ decision: "allow",
214
+ systemMessage: `WARNING: Test block in ${filePath} may have no assertions.`
215
+ };
216
+ }
217
+ return { decision: "allow" };
218
+ }
219
+ if (process.env.RIG_HOOK_ROLE === "test-integrity-guard") {
220
+ runTypedHook4(testIntegrityGuardHandler, { event: "PreToolUse" });
221
+ }
222
+
223
+ // packages/guard-plugin/src/hooks/audit-trail.ts
224
+ import { appendFileSync, mkdirSync } from "fs";
225
+ import { resolve } from "path";
226
+ import { runTypedHook as runTypedHook5 } from "@rig/hook-kit";
227
+ import { resolveHarnessPaths } from "@rig/runtime/control-plane/native/utils";
228
+ var AUDIT_TRAIL_HOOK_ID = "@rig/guard-plugin:audit-trail";
229
+ function auditTrailHandler(ctx) {
230
+ const projectRoot = ctx.projectRoot;
231
+ const toolInput = ctx.toolInput;
232
+ const paths = resolveHarnessPaths(projectRoot);
233
+ mkdirSync(paths.logsDir, { recursive: true });
234
+ const taskId = ctx.taskId || "unknown";
235
+ const tool = ctx.toolName || "unknown";
236
+ const filePath = String(toolInput.file_path ?? toolInput.path ?? "none");
237
+ const command = String(toolInput.command ?? toolInput.cmd ?? "").slice(0, 180);
238
+ const payload = {
239
+ timestamp: new Date().toISOString(),
240
+ task: taskId,
241
+ tool,
242
+ path: filePath
243
+ };
244
+ if (tool === "Bash" && command) {
245
+ payload.command_preview = command;
246
+ }
247
+ appendFileSync(resolve(paths.logsDir, "audit.jsonl"), `${JSON.stringify(payload)}
248
+ `, "utf-8");
249
+ return { decision: "allow" };
250
+ }
251
+ if (process.env.RIG_HOOK_ROLE === "audit-trail") {
252
+ runTypedHook5(auditTrailHandler, { event: "PreToolUse" });
253
+ }
254
+
255
+ // packages/guard-plugin/src/hooks/post-edit-lint.ts
256
+ import { existsSync, readFileSync } from "fs";
257
+ import { runTypedHook as runTypedHook6 } from "@rig/hook-kit";
258
+ var POST_EDIT_LINT_HOOK_ID = "@rig/guard-plugin:post-edit-lint";
259
+ function postEditLintHandler(ctx) {
260
+ const toolInput = ctx.toolInput;
261
+ const filePath = String(toolInput.file_path ?? toolInput.path ?? "");
262
+ if (!filePath || !/\.(ts|tsx|js|jsx)$/.test(filePath) || !existsSync(filePath)) {
263
+ return { decision: "allow" };
264
+ }
265
+ const content = readFileSync(filePath, "utf-8");
266
+ const warnings = [];
267
+ const consoleCount = (content.match(/console\.(log|debug)\(/g) || []).length;
268
+ if (consoleCount > 3) {
269
+ warnings.push(`${filePath}: ${consoleCount} console.log/debug calls. Remove before completion.`);
270
+ }
271
+ if (!/\.(test|spec)\./.test(filePath)) {
272
+ const markers = content.split(/\r?\n/).map((line, index) => ({ line, index: index + 1 })).filter((item) => /TODO|FIXME|HACK|\[STUB\]/.test(item.line)).slice(0, 5);
273
+ if (markers.length > 0) {
274
+ warnings.push(`${filePath}: contains TODO/FIXME/HACK/STUB markers:
275
+ ${markers.map((m) => `${m.index}: ${m.line}`).join(`
276
+ `)}`);
277
+ }
278
+ }
279
+ if (/catch\s*\([^)]*\)\s*\{\s*\}/.test(content)) {
280
+ warnings.push(`${filePath}: Empty catch block detected.`);
281
+ }
282
+ if (warnings.length > 0) {
283
+ return {
284
+ decision: "allow",
285
+ systemMessage: `POST-EDIT WARNINGS:
286
+ - ${warnings.join(`
287
+ - `)}
288
+
289
+ These are not blocking, but must be resolved before task completion.`
290
+ };
291
+ }
292
+ return { decision: "allow" };
293
+ }
294
+ if (process.env.RIG_HOOK_ROLE === "post-edit-lint") {
295
+ runTypedHook6(postEditLintHandler, { event: "PostToolUse" });
296
+ }
297
+
298
+ // packages/guard-plugin/src/plugin.ts
299
+ var GUARD_PLUGIN_NAME = "@rig/guard-plugin";
300
+ var GUARD_HOOKS = [
301
+ {
302
+ id: SAFETY_GUARD_HOOK_ID,
303
+ event: "PreToolUse",
304
+ matcher: { kind: "all" },
305
+ description: "Blocks dangerous commands and content patterns.",
306
+ handler: safetyGuardHandler
307
+ },
308
+ {
309
+ id: SCOPE_GUARD_HOOK_ID,
310
+ event: "PreToolUse",
311
+ matcher: { kind: "all" },
312
+ description: "Enforces task scope boundaries.",
313
+ handler: scopeGuardHandler
314
+ },
315
+ {
316
+ id: IMPORT_GUARD_HOOK_ID,
317
+ event: "PreToolUse",
318
+ matcher: { kind: "all" },
319
+ description: "Blocks cross-module internal imports.",
320
+ handler: importGuardHandler
321
+ },
322
+ {
323
+ id: TEST_INTEGRITY_GUARD_HOOK_ID,
324
+ event: "PreToolUse",
325
+ matcher: { kind: "all" },
326
+ description: "Prevents test tampering (.skip, .only, missing assertions).",
327
+ handler: testIntegrityGuardHandler
328
+ },
329
+ {
330
+ id: AUDIT_TRAIL_HOOK_ID,
331
+ event: "PreToolUse",
332
+ matcher: { kind: "all" },
333
+ description: "Logs tool invocations to audit.jsonl.",
334
+ handler: auditTrailHandler
335
+ },
336
+ {
337
+ id: POST_EDIT_LINT_HOOK_ID,
338
+ event: "PostToolUse",
339
+ matcher: { kind: "all" },
340
+ description: "Warns about console.log spam, TODO markers, and empty catches.",
341
+ handler: postEditLintHandler
342
+ }
343
+ ];
344
+ function createGuardPlugin() {
345
+ return definePlugin({
346
+ name: GUARD_PLUGIN_NAME,
347
+ version: "0.0.0-alpha.1",
348
+ contributes: {
349
+ hooks: GUARD_HOOKS
350
+ }
351
+ });
352
+ }
353
+ export {
354
+ testIntegrityGuardHandler,
355
+ scopeGuardHandler,
356
+ safetyGuardHandler,
357
+ postEditLintHandler,
358
+ importGuardHandler,
359
+ createGuardPlugin,
360
+ auditTrailHandler,
361
+ TEST_INTEGRITY_GUARD_HOOK_ID,
362
+ SCOPE_GUARD_HOOK_ID,
363
+ SAFETY_GUARD_HOOK_ID,
364
+ POST_EDIT_LINT_HOOK_ID,
365
+ IMPORT_GUARD_HOOK_ID,
366
+ GUARD_PLUGIN_NAME,
367
+ AUDIT_TRAIL_HOOK_ID
368
+ };
@@ -0,0 +1,3 @@
1
+ import { type RigPlugin } from "@rig/core/config";
2
+ export declare const GUARD_PLUGIN_NAME = "@rig/guard-plugin";
3
+ export declare function createGuardPlugin(): RigPlugin;
@@ -0,0 +1,356 @@
1
+ // @bun
2
+ // packages/guard-plugin/src/plugin.ts
3
+ import { definePlugin } from "@rig/core/config";
4
+
5
+ // packages/guard-plugin/src/hooks/safety-guard.ts
6
+ import { resolveTaskScopes, resolvePolicyContent, runTypedHook } from "@rig/hook-kit";
7
+ import { evaluate, seedPolicyFromContent } from "@rig/runtime/control-plane/runtime/guard";
8
+ var SAFETY_GUARD_HOOK_ID = "@rig/guard-plugin:safety-guard";
9
+ async function safetyGuardHandler(ctx) {
10
+ const projectRoot = ctx.projectRoot;
11
+ seedPolicyFromContent(resolvePolicyContent(projectRoot));
12
+ const tool = ctx.toolName ?? "";
13
+ const toolInput = ctx.toolInput;
14
+ const command = String(toolInput.command ?? toolInput.cmd ?? "");
15
+ const content = String(toolInput.content ?? toolInput.new_string ?? "");
16
+ if (tool === "Bash" && !command) {
17
+ return { decision: "block", reason: "Bash tool payload did not include a command field." };
18
+ }
19
+ const taskId = ctx.taskId || "";
20
+ const scopes = taskId ? await resolveTaskScopes(projectRoot, taskId) : [];
21
+ const taskWorkspace = process.env.RIG_TASK_WORKSPACE || "";
22
+ const baseContext = {
23
+ projectRoot,
24
+ taskScopes: scopes,
25
+ evaluation: { type: "command", command: "" },
26
+ ...taskId ? { taskId } : {},
27
+ ...taskWorkspace ? { taskWorkspace } : {}
28
+ };
29
+ if (tool === "Bash" && command) {
30
+ const decision = evaluate({
31
+ ...baseContext,
32
+ evaluation: { type: "command", command }
33
+ });
34
+ if (!decision.allowed && decision.action === "block") {
35
+ const reasons = decision.matchedRules.map((r) => r.reason).join(`
36
+ `);
37
+ return { decision: "block", reason: reasons };
38
+ }
39
+ if (taskWorkspace && taskWorkspace !== projectRoot) {
40
+ const hostProjectRoot = process.env.RIG_HOST_PROJECT_ROOT || projectRoot;
41
+ const rootReposPrefix = `${hostProjectRoot}/repos/`;
42
+ if (command.includes(rootReposPrefix)) {
43
+ return {
44
+ decision: "block",
45
+ reason: `Absolute root repo path detected (${rootReposPrefix}). Use relative paths or worktree paths under ${taskWorkspace}.`
46
+ };
47
+ }
48
+ }
49
+ }
50
+ if (content) {
51
+ const filePath = String(toolInput.file_path ?? toolInput.path ?? "");
52
+ const contentDecision = evaluate({
53
+ ...baseContext,
54
+ evaluation: { type: "content-write", file_path: filePath, content }
55
+ });
56
+ const contentRules = contentDecision.matchedRules.filter((r) => r.category === "content");
57
+ if (contentRules.length > 0 && contentDecision.action === "block") {
58
+ const reasons = contentRules.map((r) => r.reason).join(`
59
+ `);
60
+ return { decision: "block", reason: reasons };
61
+ }
62
+ }
63
+ return { decision: "allow" };
64
+ }
65
+ if (process.env.RIG_HOOK_ROLE === "safety-guard") {
66
+ runTypedHook(safetyGuardHandler, { event: "PreToolUse" });
67
+ }
68
+
69
+ // packages/guard-plugin/src/hooks/scope-guard.ts
70
+ import { resolveTaskScopes as resolveTaskScopes2, resolvePolicyContent as resolvePolicyContent2, runTypedHook as runTypedHook2 } from "@rig/hook-kit";
71
+ import { evaluate as evaluate2, seedPolicyFromContent as seedPolicyFromContent2 } from "@rig/runtime/control-plane/runtime/guard";
72
+ var SCOPE_GUARD_HOOK_ID = "@rig/guard-plugin:scope-guard";
73
+ async function scopeGuardHandler(ctx) {
74
+ const projectRoot = ctx.projectRoot;
75
+ seedPolicyFromContent2(resolvePolicyContent2(projectRoot));
76
+ const tool = ctx.toolName ?? "";
77
+ const toolInput = ctx.toolInput;
78
+ const taskWorkspace = process.env.RIG_TASK_WORKSPACE || "";
79
+ const hostProjectRoot = process.env.RIG_HOST_PROJECT_ROOT || projectRoot;
80
+ const runtimeMode = (process.env.RIG_TASK_RUNTIME_MODE || "").trim().toLowerCase();
81
+ const runtimeIsolationActive = runtimeMode !== "" && runtimeMode !== "off" || taskWorkspace !== "" && taskWorkspace !== projectRoot;
82
+ const rootReposPrefix = `${hostProjectRoot}/repos/`;
83
+ for (const filePath of ctx.filePaths) {
84
+ if (!filePath) {
85
+ continue;
86
+ }
87
+ const resolvesInsideTaskWorkspace = taskWorkspace !== "" && (filePath === taskWorkspace || filePath.startsWith(`${taskWorkspace}/`));
88
+ if (filePath.startsWith(rootReposPrefix) && !resolvesInsideTaskWorkspace && (taskWorkspace !== "" || runtimeIsolationActive)) {
89
+ return {
90
+ decision: "block",
91
+ reason: `Absolute root repo path detected (${rootReposPrefix}). Use task-runtime paths under ${taskWorkspace || "$RIG_TASK_WORKSPACE"}.`
92
+ };
93
+ }
94
+ if ((tool === "Read" || tool === "Glob" || tool === "Grep") && /^repos\//.test(filePath) && (taskWorkspace !== "" || runtimeIsolationActive)) {
95
+ return {
96
+ decision: "block",
97
+ reason: `Relative repo path '${filePath}' is blocked in runtime mode. Use absolute paths under ${taskWorkspace || "$RIG_TASK_WORKSPACE"}.`
98
+ };
99
+ }
100
+ }
101
+ const evaluation = {
102
+ type: "tool-call",
103
+ tool_name: tool,
104
+ tool_input: toolInput
105
+ };
106
+ const taskId = ctx.taskId || "";
107
+ if (!taskId) {
108
+ return { decision: "allow" };
109
+ }
110
+ const scopes = await resolveTaskScopes2(projectRoot, taskId);
111
+ if (scopes.length === 0) {
112
+ return { decision: "allow" };
113
+ }
114
+ const decision = evaluate2({
115
+ projectRoot,
116
+ taskScopes: scopes,
117
+ evaluation,
118
+ ...taskId ? { taskId } : {},
119
+ ...taskWorkspace ? { taskWorkspace } : {}
120
+ });
121
+ if (!decision.allowed && decision.action === "block") {
122
+ const reasons = decision.matchedRules.map((r) => r.reason).join(`
123
+ `);
124
+ return { decision: "block", reason: reasons };
125
+ }
126
+ return { decision: "allow" };
127
+ }
128
+ if (process.env.RIG_HOOK_ROLE === "scope-guard") {
129
+ runTypedHook2(scopeGuardHandler, { event: "PreToolUse" });
130
+ }
131
+
132
+ // packages/guard-plugin/src/hooks/import-guard.ts
133
+ import { resolveTaskScopes as resolveTaskScopes3, resolvePolicyContent as resolvePolicyContent3, runTypedHook as runTypedHook3 } from "@rig/hook-kit";
134
+ import { evaluate as evaluate3, seedPolicyFromContent as seedPolicyFromContent3 } from "@rig/runtime/control-plane/runtime/guard";
135
+ var IMPORT_GUARD_HOOK_ID = "@rig/guard-plugin:import-guard";
136
+ async function importGuardHandler(ctx) {
137
+ const projectRoot = ctx.projectRoot;
138
+ seedPolicyFromContent3(resolvePolicyContent3(projectRoot));
139
+ const toolInput = ctx.toolInput;
140
+ const content = String(toolInput.content ?? toolInput.new_string ?? "");
141
+ if (!content) {
142
+ return { decision: "allow" };
143
+ }
144
+ const filePath = String(toolInput.file_path ?? toolInput.path ?? "");
145
+ const evaluation = {
146
+ type: "content-write",
147
+ file_path: filePath,
148
+ content
149
+ };
150
+ const taskId = ctx.taskId || "";
151
+ const scopes = taskId ? await resolveTaskScopes3(projectRoot, taskId) : [];
152
+ const taskWorkspace = process.env.RIG_TASK_WORKSPACE || "";
153
+ const decision = evaluate3({
154
+ projectRoot,
155
+ taskScopes: scopes,
156
+ evaluation,
157
+ ...taskId ? { taskId } : {},
158
+ ...taskWorkspace ? { taskWorkspace } : {}
159
+ });
160
+ const importViolations = decision.matchedRules.filter((r) => r.category === "import");
161
+ if (importViolations.length > 0 && decision.action === "block") {
162
+ const reasons = importViolations.map((r) => r.reason).join(`
163
+ `);
164
+ return {
165
+ decision: "block",
166
+ reason: `Cross-module internal import detected:
167
+ ${reasons}
168
+ Only import from a module public API (index.ts) or npm packages.`
169
+ };
170
+ }
171
+ return { decision: "allow" };
172
+ }
173
+ if (process.env.RIG_HOOK_ROLE === "import-guard") {
174
+ runTypedHook3(importGuardHandler, { event: "PreToolUse" });
175
+ }
176
+
177
+ // packages/guard-plugin/src/hooks/test-integrity-guard.ts
178
+ import { resolveTaskScopes as resolveTaskScopes4, resolvePolicyContent as resolvePolicyContent4, isTestFilePath, runTypedHook as runTypedHook4 } from "@rig/hook-kit";
179
+ import { evaluate as evaluate4, seedPolicyFromContent as seedPolicyFromContent4 } from "@rig/runtime/control-plane/runtime/guard";
180
+ var TEST_INTEGRITY_GUARD_HOOK_ID = "@rig/guard-plugin:test-integrity-guard";
181
+ async function testIntegrityGuardHandler(ctx) {
182
+ const projectRoot = ctx.projectRoot;
183
+ seedPolicyFromContent4(resolvePolicyContent4(projectRoot));
184
+ const toolInput = ctx.toolInput;
185
+ const filePath = String(toolInput.file_path ?? toolInput.path ?? "");
186
+ const content = String(toolInput.content ?? toolInput.new_string ?? "");
187
+ if (!filePath || !content) {
188
+ return { decision: "allow" };
189
+ }
190
+ const evaluation = {
191
+ type: "content-write",
192
+ file_path: filePath,
193
+ content
194
+ };
195
+ const taskId = ctx.taskId || "";
196
+ const scopes = taskId ? await resolveTaskScopes4(projectRoot, taskId) : [];
197
+ const taskWorkspace = process.env.RIG_TASK_WORKSPACE || "";
198
+ const decision = evaluate4({
199
+ projectRoot,
200
+ taskScopes: scopes,
201
+ evaluation,
202
+ ...taskId ? { taskId } : {},
203
+ ...taskWorkspace ? { taskWorkspace } : {}
204
+ });
205
+ const testViolations = decision.matchedRules.filter((r) => r.category === "test-integrity");
206
+ if (testViolations.length > 0 && decision.action === "block") {
207
+ const reasons = testViolations.map((r) => r.reason).join(`
208
+ `);
209
+ return { decision: "block", reason: reasons };
210
+ }
211
+ if (isTestFilePath(filePath) && /(test|it)\s*\(/.test(content) && !/expect\s*\(|assert/.test(content)) {
212
+ return {
213
+ decision: "allow",
214
+ systemMessage: `WARNING: Test block in ${filePath} may have no assertions.`
215
+ };
216
+ }
217
+ return { decision: "allow" };
218
+ }
219
+ if (process.env.RIG_HOOK_ROLE === "test-integrity-guard") {
220
+ runTypedHook4(testIntegrityGuardHandler, { event: "PreToolUse" });
221
+ }
222
+
223
+ // packages/guard-plugin/src/hooks/audit-trail.ts
224
+ import { appendFileSync, mkdirSync } from "fs";
225
+ import { resolve } from "path";
226
+ import { runTypedHook as runTypedHook5 } from "@rig/hook-kit";
227
+ import { resolveHarnessPaths } from "@rig/runtime/control-plane/native/utils";
228
+ var AUDIT_TRAIL_HOOK_ID = "@rig/guard-plugin:audit-trail";
229
+ function auditTrailHandler(ctx) {
230
+ const projectRoot = ctx.projectRoot;
231
+ const toolInput = ctx.toolInput;
232
+ const paths = resolveHarnessPaths(projectRoot);
233
+ mkdirSync(paths.logsDir, { recursive: true });
234
+ const taskId = ctx.taskId || "unknown";
235
+ const tool = ctx.toolName || "unknown";
236
+ const filePath = String(toolInput.file_path ?? toolInput.path ?? "none");
237
+ const command = String(toolInput.command ?? toolInput.cmd ?? "").slice(0, 180);
238
+ const payload = {
239
+ timestamp: new Date().toISOString(),
240
+ task: taskId,
241
+ tool,
242
+ path: filePath
243
+ };
244
+ if (tool === "Bash" && command) {
245
+ payload.command_preview = command;
246
+ }
247
+ appendFileSync(resolve(paths.logsDir, "audit.jsonl"), `${JSON.stringify(payload)}
248
+ `, "utf-8");
249
+ return { decision: "allow" };
250
+ }
251
+ if (process.env.RIG_HOOK_ROLE === "audit-trail") {
252
+ runTypedHook5(auditTrailHandler, { event: "PreToolUse" });
253
+ }
254
+
255
+ // packages/guard-plugin/src/hooks/post-edit-lint.ts
256
+ import { existsSync, readFileSync } from "fs";
257
+ import { runTypedHook as runTypedHook6 } from "@rig/hook-kit";
258
+ var POST_EDIT_LINT_HOOK_ID = "@rig/guard-plugin:post-edit-lint";
259
+ function postEditLintHandler(ctx) {
260
+ const toolInput = ctx.toolInput;
261
+ const filePath = String(toolInput.file_path ?? toolInput.path ?? "");
262
+ if (!filePath || !/\.(ts|tsx|js|jsx)$/.test(filePath) || !existsSync(filePath)) {
263
+ return { decision: "allow" };
264
+ }
265
+ const content = readFileSync(filePath, "utf-8");
266
+ const warnings = [];
267
+ const consoleCount = (content.match(/console\.(log|debug)\(/g) || []).length;
268
+ if (consoleCount > 3) {
269
+ warnings.push(`${filePath}: ${consoleCount} console.log/debug calls. Remove before completion.`);
270
+ }
271
+ if (!/\.(test|spec)\./.test(filePath)) {
272
+ const markers = content.split(/\r?\n/).map((line, index) => ({ line, index: index + 1 })).filter((item) => /TODO|FIXME|HACK|\[STUB\]/.test(item.line)).slice(0, 5);
273
+ if (markers.length > 0) {
274
+ warnings.push(`${filePath}: contains TODO/FIXME/HACK/STUB markers:
275
+ ${markers.map((m) => `${m.index}: ${m.line}`).join(`
276
+ `)}`);
277
+ }
278
+ }
279
+ if (/catch\s*\([^)]*\)\s*\{\s*\}/.test(content)) {
280
+ warnings.push(`${filePath}: Empty catch block detected.`);
281
+ }
282
+ if (warnings.length > 0) {
283
+ return {
284
+ decision: "allow",
285
+ systemMessage: `POST-EDIT WARNINGS:
286
+ - ${warnings.join(`
287
+ - `)}
288
+
289
+ These are not blocking, but must be resolved before task completion.`
290
+ };
291
+ }
292
+ return { decision: "allow" };
293
+ }
294
+ if (process.env.RIG_HOOK_ROLE === "post-edit-lint") {
295
+ runTypedHook6(postEditLintHandler, { event: "PostToolUse" });
296
+ }
297
+
298
+ // packages/guard-plugin/src/plugin.ts
299
+ var GUARD_PLUGIN_NAME = "@rig/guard-plugin";
300
+ var GUARD_HOOKS = [
301
+ {
302
+ id: SAFETY_GUARD_HOOK_ID,
303
+ event: "PreToolUse",
304
+ matcher: { kind: "all" },
305
+ description: "Blocks dangerous commands and content patterns.",
306
+ handler: safetyGuardHandler
307
+ },
308
+ {
309
+ id: SCOPE_GUARD_HOOK_ID,
310
+ event: "PreToolUse",
311
+ matcher: { kind: "all" },
312
+ description: "Enforces task scope boundaries.",
313
+ handler: scopeGuardHandler
314
+ },
315
+ {
316
+ id: IMPORT_GUARD_HOOK_ID,
317
+ event: "PreToolUse",
318
+ matcher: { kind: "all" },
319
+ description: "Blocks cross-module internal imports.",
320
+ handler: importGuardHandler
321
+ },
322
+ {
323
+ id: TEST_INTEGRITY_GUARD_HOOK_ID,
324
+ event: "PreToolUse",
325
+ matcher: { kind: "all" },
326
+ description: "Prevents test tampering (.skip, .only, missing assertions).",
327
+ handler: testIntegrityGuardHandler
328
+ },
329
+ {
330
+ id: AUDIT_TRAIL_HOOK_ID,
331
+ event: "PreToolUse",
332
+ matcher: { kind: "all" },
333
+ description: "Logs tool invocations to audit.jsonl.",
334
+ handler: auditTrailHandler
335
+ },
336
+ {
337
+ id: POST_EDIT_LINT_HOOK_ID,
338
+ event: "PostToolUse",
339
+ matcher: { kind: "all" },
340
+ description: "Warns about console.log spam, TODO markers, and empty catches.",
341
+ handler: postEditLintHandler
342
+ }
343
+ ];
344
+ function createGuardPlugin() {
345
+ return definePlugin({
346
+ name: GUARD_PLUGIN_NAME,
347
+ version: "0.0.0-alpha.1",
348
+ contributes: {
349
+ hooks: GUARD_HOOKS
350
+ }
351
+ });
352
+ }
353
+ export {
354
+ createGuardPlugin,
355
+ GUARD_PLUGIN_NAME
356
+ };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@h-rig/guard-plugin",
3
+ "version": "0.0.6-alpha.156",
4
+ "type": "module",
5
+ "description": "First-party runtime policy-guard hooks contributed as a single-channel Rig plugin.",
6
+ "license": "UNLICENSED",
7
+ "files": [
8
+ "dist",
9
+ "README.md"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/src/index.d.ts",
14
+ "import": "./dist/src/index.js"
15
+ },
16
+ "./plugin": {
17
+ "types": "./dist/src/plugin.d.ts",
18
+ "import": "./dist/src/plugin.js"
19
+ },
20
+ "./hooks/safety-guard": {
21
+ "types": "./dist/src/hooks/safety-guard.d.ts",
22
+ "import": "./dist/src/hooks/safety-guard.js"
23
+ },
24
+ "./hooks/scope-guard": {
25
+ "types": "./dist/src/hooks/scope-guard.d.ts",
26
+ "import": "./dist/src/hooks/scope-guard.js"
27
+ },
28
+ "./hooks/import-guard": {
29
+ "types": "./dist/src/hooks/import-guard.d.ts",
30
+ "import": "./dist/src/hooks/import-guard.js"
31
+ },
32
+ "./hooks/test-integrity-guard": {
33
+ "types": "./dist/src/hooks/test-integrity-guard.d.ts",
34
+ "import": "./dist/src/hooks/test-integrity-guard.js"
35
+ },
36
+ "./hooks/audit-trail": {
37
+ "types": "./dist/src/hooks/audit-trail.d.ts",
38
+ "import": "./dist/src/hooks/audit-trail.js"
39
+ },
40
+ "./hooks/post-edit-lint": {
41
+ "types": "./dist/src/hooks/post-edit-lint.d.ts",
42
+ "import": "./dist/src/hooks/post-edit-lint.js"
43
+ }
44
+ },
45
+ "engines": {
46
+ "bun": ">=1.3.11"
47
+ },
48
+ "main": "./dist/src/index.js",
49
+ "module": "./dist/src/index.js",
50
+ "types": "./dist/src/index.d.ts",
51
+ "dependencies": {
52
+ "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.156",
53
+ "@rig/core": "npm:@h-rig/core@0.0.6-alpha.156",
54
+ "@rig/hook-kit": "npm:@h-rig/hook-kit@0.0.6-alpha.156",
55
+ "@rig/runtime": "npm:@h-rig/runtime@0.0.6-alpha.156"
56
+ }
57
+ }