fastgrc-openclaw 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/README.md +83 -0
- package/dist/bin.d.mts +1 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +124 -0
- package/dist/bin.mjs +123 -0
- package/dist/index.d.mts +74 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +148 -0
- package/dist/index.mjs +120 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# fastgrc-openclaw
|
|
2
|
+
|
|
3
|
+
FastGRC compliance plugin for [OpenClaw](https://openclaw.ai). Evaluates every agent tool call against your policy before it executes — blocking, flagging, or logging violations in real time.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install fastgrc-openclaw
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Setup (2 lines)
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
// openclaw.config.ts
|
|
15
|
+
import { FastGRCPlugin } from 'fastgrc-openclaw';
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
plugins: [
|
|
19
|
+
FastGRCPlugin({
|
|
20
|
+
apiKey: process.env.FASTGRC_API_KEY, // fgrc_k1_...
|
|
21
|
+
}),
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Set your environment variable:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
FASTGRC_API_KEY=fgrc_k1_your_key_here
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Get your API key at [fastgrc.ai/connect?source=openclaw](https://fastgrc.ai/connect?source=openclaw) — free, no credit card.
|
|
33
|
+
|
|
34
|
+
## Options
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
FastGRCPlugin({
|
|
38
|
+
apiKey: string; // Required. Your FastGRC API key.
|
|
39
|
+
policyId?: string; // Optional. Target a specific policy. Omit for org-wide default.
|
|
40
|
+
onBlock?: 'throw' // (default) Throw FastGRCBlockedError — OpenClaw surfaces it as an agent error
|
|
41
|
+
| 'warn' // console.warn and allow through
|
|
42
|
+
| 'silent'; // Allow through silently
|
|
43
|
+
timeoutMs?: number; // Max ms to wait for FastGRC API. Default: 3000. Fail-open on timeout.
|
|
44
|
+
baseUrl?: string; // Override FastGRC base URL. Default: https://app.fastgrc.ai
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## How it works
|
|
49
|
+
|
|
50
|
+
The plugin registers a `before_tool_call` hook. For every tool invocation:
|
|
51
|
+
|
|
52
|
+
1. Calls `POST /api/v1/policy-router/evaluate` with the tool name and arguments
|
|
53
|
+
2. On `decision: block` → throws `FastGRCBlockedError` (OpenClaw surfaces this as an agent error with explanation)
|
|
54
|
+
3. On `decision: require_approval` → throws `FastGRCApprovalRequiredError` with a link to your dashboard
|
|
55
|
+
4. On `decision: allow | uncertain` → passes through silently
|
|
56
|
+
5. On timeout or network error → **fail-open** (allows through, logs a warning) — FastGRC never breaks your agent due to infra issues
|
|
57
|
+
|
|
58
|
+
## Error types
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { FastGRCBlockedError, FastGRCApprovalRequiredError } from 'fastgrc-openclaw';
|
|
62
|
+
|
|
63
|
+
// Catch in your agent error handler:
|
|
64
|
+
if (err instanceof FastGRCBlockedError) {
|
|
65
|
+
console.log(err.matchedRule); // Which policy rule triggered
|
|
66
|
+
console.log(err.reasoning); // Human-readable explanation
|
|
67
|
+
console.log(err.policyId); // Policy that made the decision
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (err instanceof FastGRCApprovalRequiredError) {
|
|
71
|
+
console.log(err.dashboardUrl); // Link to approve in FastGRC dashboard
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Policy modes
|
|
76
|
+
|
|
77
|
+
Policies start in **Observability Mode** — violations are logged but never blocked. Switch to enforcement from your [FastGRC dashboard](https://app.fastgrc.ai/dashboard/agent-policies) when ready.
|
|
78
|
+
|
|
79
|
+
## Links
|
|
80
|
+
|
|
81
|
+
- [FastGRC docs](https://docs.fastgrc.ai/integrations/openclaw)
|
|
82
|
+
- [Get your API key](https://fastgrc.ai/connect?source=openclaw)
|
|
83
|
+
- [Dashboard](https://app.fastgrc.ai)
|
package/dist/bin.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
var DEFAULT_BASE_URL = "https://app.fastgrc.ai";
|
|
6
|
+
var DEFAULT_TIMEOUT_MS = 3e3;
|
|
7
|
+
async function evaluate(payload) {
|
|
8
|
+
const {
|
|
9
|
+
toolName,
|
|
10
|
+
args,
|
|
11
|
+
agentId,
|
|
12
|
+
agentType,
|
|
13
|
+
agentName,
|
|
14
|
+
apiKey: apiKey2,
|
|
15
|
+
policyId: policyId2,
|
|
16
|
+
baseUrl: baseUrl2 = DEFAULT_BASE_URL,
|
|
17
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
18
|
+
} = payload;
|
|
19
|
+
const evalUrl = `${baseUrl2.replace(/\/$/, "")}/api/v1/policy-router/evaluate`;
|
|
20
|
+
const content = `tool_name: ${toolName}
|
|
21
|
+
args: ${JSON.stringify(args)}`;
|
|
22
|
+
try {
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
25
|
+
const res = await fetch(evalUrl, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: {
|
|
28
|
+
"Authorization": `Bearer ${apiKey2}`,
|
|
29
|
+
"Content-Type": "application/json"
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({
|
|
32
|
+
subjectContent: content,
|
|
33
|
+
subjectType: "tool_argument",
|
|
34
|
+
direction: "ingress",
|
|
35
|
+
agentId,
|
|
36
|
+
agentType,
|
|
37
|
+
agentName,
|
|
38
|
+
...policyId2 ? { policyId: policyId2 } : {}
|
|
39
|
+
}),
|
|
40
|
+
signal: controller.signal
|
|
41
|
+
});
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
console.warn(`[fastgrc] Evaluate returned ${res.status} \u2014 failing open`);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return await res.json();
|
|
48
|
+
} catch (err) {
|
|
49
|
+
const reason = err instanceof Error && err.name === "AbortError" ? "timeout" : String(err);
|
|
50
|
+
console.warn(`[fastgrc] Evaluate failed (${reason}) \u2014 failing open`);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/bin.ts
|
|
56
|
+
var apiKey = process.env.FASTGRC_API_KEY;
|
|
57
|
+
if (!apiKey) {
|
|
58
|
+
process.stderr.write("[fastgrc-hook] FASTGRC_API_KEY is not set \u2014 allowing tool call\n");
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
var baseUrl = process.env.FASTGRC_BASE_URL ?? "https://app.fastgrc.ai";
|
|
62
|
+
var policyId = process.env.FASTGRC_POLICY_ID;
|
|
63
|
+
async function main() {
|
|
64
|
+
const chunks = [];
|
|
65
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
66
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
67
|
+
const toolName = process.env.HOOK_TOOL_NAME ?? "";
|
|
68
|
+
const toolInputRaw = process.env.HOOK_TOOL_INPUT ?? "{}";
|
|
69
|
+
let args = {};
|
|
70
|
+
let agentId;
|
|
71
|
+
if (raw) {
|
|
72
|
+
try {
|
|
73
|
+
const ctx = JSON.parse(raw);
|
|
74
|
+
args = ctx.tool_input ?? ctx.input ?? ctx.args ?? {};
|
|
75
|
+
agentId = ctx.agent_id ?? ctx.agentId ?? ctx.session_id;
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!toolName && !raw) {
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
const resolvedToolName = toolName || (raw ? JSON.parse(raw)?.tool_name ?? "unknown" : "unknown");
|
|
83
|
+
if (!args || Object.keys(args).length === 0) {
|
|
84
|
+
try {
|
|
85
|
+
args = JSON.parse(toolInputRaw);
|
|
86
|
+
} catch {
|
|
87
|
+
args = {};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const result = await evaluate({
|
|
91
|
+
toolName: resolvedToolName,
|
|
92
|
+
args,
|
|
93
|
+
agentId,
|
|
94
|
+
apiKey,
|
|
95
|
+
// narrowed at module level via early-exit guard above
|
|
96
|
+
policyId,
|
|
97
|
+
baseUrl
|
|
98
|
+
});
|
|
99
|
+
if (!result) {
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
102
|
+
const { decision, reasoning, policyContext } = result;
|
|
103
|
+
const matchedRule = policyContext?.matchedRule;
|
|
104
|
+
if (decision === "block") {
|
|
105
|
+
const msg = matchedRule ? `FastGRC blocked: [${matchedRule}] ${reasoning}` : `FastGRC blocked: ${reasoning}`;
|
|
106
|
+
process.stderr.write(msg + "\n");
|
|
107
|
+
process.exit(2);
|
|
108
|
+
}
|
|
109
|
+
if (decision === "require_approval") {
|
|
110
|
+
process.stderr.write(
|
|
111
|
+
`FastGRC requires approval before this action.
|
|
112
|
+
${reasoning}
|
|
113
|
+
Review at: ${baseUrl}/dashboard/agent-policies
|
|
114
|
+
`
|
|
115
|
+
);
|
|
116
|
+
process.exit(2);
|
|
117
|
+
}
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
main().catch((err) => {
|
|
121
|
+
process.stderr.write(`[fastgrc-hook] Unexpected error: ${err}
|
|
122
|
+
`);
|
|
123
|
+
process.exit(0);
|
|
124
|
+
});
|
package/dist/bin.mjs
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
var DEFAULT_BASE_URL = "https://app.fastgrc.ai";
|
|
5
|
+
var DEFAULT_TIMEOUT_MS = 3e3;
|
|
6
|
+
async function evaluate(payload) {
|
|
7
|
+
const {
|
|
8
|
+
toolName,
|
|
9
|
+
args,
|
|
10
|
+
agentId,
|
|
11
|
+
agentType,
|
|
12
|
+
agentName,
|
|
13
|
+
apiKey: apiKey2,
|
|
14
|
+
policyId: policyId2,
|
|
15
|
+
baseUrl: baseUrl2 = DEFAULT_BASE_URL,
|
|
16
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
17
|
+
} = payload;
|
|
18
|
+
const evalUrl = `${baseUrl2.replace(/\/$/, "")}/api/v1/policy-router/evaluate`;
|
|
19
|
+
const content = `tool_name: ${toolName}
|
|
20
|
+
args: ${JSON.stringify(args)}`;
|
|
21
|
+
try {
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
24
|
+
const res = await fetch(evalUrl, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
"Authorization": `Bearer ${apiKey2}`,
|
|
28
|
+
"Content-Type": "application/json"
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
subjectContent: content,
|
|
32
|
+
subjectType: "tool_argument",
|
|
33
|
+
direction: "ingress",
|
|
34
|
+
agentId,
|
|
35
|
+
agentType,
|
|
36
|
+
agentName,
|
|
37
|
+
...policyId2 ? { policyId: policyId2 } : {}
|
|
38
|
+
}),
|
|
39
|
+
signal: controller.signal
|
|
40
|
+
});
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
console.warn(`[fastgrc] Evaluate returned ${res.status} \u2014 failing open`);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return await res.json();
|
|
47
|
+
} catch (err) {
|
|
48
|
+
const reason = err instanceof Error && err.name === "AbortError" ? "timeout" : String(err);
|
|
49
|
+
console.warn(`[fastgrc] Evaluate failed (${reason}) \u2014 failing open`);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/bin.ts
|
|
55
|
+
var apiKey = process.env.FASTGRC_API_KEY;
|
|
56
|
+
if (!apiKey) {
|
|
57
|
+
process.stderr.write("[fastgrc-hook] FASTGRC_API_KEY is not set \u2014 allowing tool call\n");
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
var baseUrl = process.env.FASTGRC_BASE_URL ?? "https://app.fastgrc.ai";
|
|
61
|
+
var policyId = process.env.FASTGRC_POLICY_ID;
|
|
62
|
+
async function main() {
|
|
63
|
+
const chunks = [];
|
|
64
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
65
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
66
|
+
const toolName = process.env.HOOK_TOOL_NAME ?? "";
|
|
67
|
+
const toolInputRaw = process.env.HOOK_TOOL_INPUT ?? "{}";
|
|
68
|
+
let args = {};
|
|
69
|
+
let agentId;
|
|
70
|
+
if (raw) {
|
|
71
|
+
try {
|
|
72
|
+
const ctx = JSON.parse(raw);
|
|
73
|
+
args = ctx.tool_input ?? ctx.input ?? ctx.args ?? {};
|
|
74
|
+
agentId = ctx.agent_id ?? ctx.agentId ?? ctx.session_id;
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!toolName && !raw) {
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
const resolvedToolName = toolName || (raw ? JSON.parse(raw)?.tool_name ?? "unknown" : "unknown");
|
|
82
|
+
if (!args || Object.keys(args).length === 0) {
|
|
83
|
+
try {
|
|
84
|
+
args = JSON.parse(toolInputRaw);
|
|
85
|
+
} catch {
|
|
86
|
+
args = {};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const result = await evaluate({
|
|
90
|
+
toolName: resolvedToolName,
|
|
91
|
+
args,
|
|
92
|
+
agentId,
|
|
93
|
+
apiKey,
|
|
94
|
+
// narrowed at module level via early-exit guard above
|
|
95
|
+
policyId,
|
|
96
|
+
baseUrl
|
|
97
|
+
});
|
|
98
|
+
if (!result) {
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
const { decision, reasoning, policyContext } = result;
|
|
102
|
+
const matchedRule = policyContext?.matchedRule;
|
|
103
|
+
if (decision === "block") {
|
|
104
|
+
const msg = matchedRule ? `FastGRC blocked: [${matchedRule}] ${reasoning}` : `FastGRC blocked: ${reasoning}`;
|
|
105
|
+
process.stderr.write(msg + "\n");
|
|
106
|
+
process.exit(2);
|
|
107
|
+
}
|
|
108
|
+
if (decision === "require_approval") {
|
|
109
|
+
process.stderr.write(
|
|
110
|
+
`FastGRC requires approval before this action.
|
|
111
|
+
${reasoning}
|
|
112
|
+
Review at: ${baseUrl}/dashboard/agent-policies
|
|
113
|
+
`
|
|
114
|
+
);
|
|
115
|
+
process.exit(2);
|
|
116
|
+
}
|
|
117
|
+
process.exit(0);
|
|
118
|
+
}
|
|
119
|
+
main().catch((err) => {
|
|
120
|
+
process.stderr.write(`[fastgrc-hook] Unexpected error: ${err}
|
|
121
|
+
`);
|
|
122
|
+
process.exit(0);
|
|
123
|
+
});
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
interface FastGRCPluginOptions {
|
|
2
|
+
/** Your FastGRC API key (fgrc_k1_...) */
|
|
3
|
+
apiKey: string;
|
|
4
|
+
/** Specific policy ID to evaluate against. Omit to use the org-wide default. */
|
|
5
|
+
policyId?: string;
|
|
6
|
+
/**
|
|
7
|
+
* What to do on a block decision when used outside the plugin hook (e.g. manual invocation).
|
|
8
|
+
* The plugin hook always returns { block: true } per OpenClaw's hook protocol.
|
|
9
|
+
* - 'throw' (default) — throw FastGRCBlockedError
|
|
10
|
+
* - 'warn' — console.warn and allow through
|
|
11
|
+
* - 'silent' — allow through silently
|
|
12
|
+
*/
|
|
13
|
+
onBlock?: 'throw' | 'warn' | 'silent';
|
|
14
|
+
/** Max ms to wait for the FastGRC API. Fail-open on timeout. Default: 3000 */
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
/** Override the FastGRC base URL. Default: https://app.fastgrc.ai */
|
|
17
|
+
baseUrl?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Thrown when FastGRC's policy blocks a tool call (onBlock: 'throw', outside plugin hook) */
|
|
20
|
+
declare class FastGRCBlockedError extends Error {
|
|
21
|
+
readonly matchedRule: string;
|
|
22
|
+
readonly reasoning: string;
|
|
23
|
+
readonly policyId?: string;
|
|
24
|
+
constructor(matchedRule: string, reasoning: string, policyId?: string);
|
|
25
|
+
}
|
|
26
|
+
/** Thrown when FastGRC requires human approval (outside plugin hook) */
|
|
27
|
+
declare class FastGRCApprovalRequiredError extends Error {
|
|
28
|
+
readonly dashboardUrl: string;
|
|
29
|
+
constructor(dashboardUrl: string, reasoning: string);
|
|
30
|
+
}
|
|
31
|
+
type HookDecision = {
|
|
32
|
+
block: true;
|
|
33
|
+
reason?: string;
|
|
34
|
+
} | {
|
|
35
|
+
block: false;
|
|
36
|
+
} | {
|
|
37
|
+
requireApproval: true;
|
|
38
|
+
reason?: string;
|
|
39
|
+
} | undefined;
|
|
40
|
+
interface AgentContext {
|
|
41
|
+
id?: string;
|
|
42
|
+
name?: string;
|
|
43
|
+
type?: string;
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
}
|
|
46
|
+
interface OpenClawPlugin {
|
|
47
|
+
name: string;
|
|
48
|
+
hooks: {
|
|
49
|
+
before_tool_call?: (toolName: string, args: unknown, context: AgentContext) => Promise<HookDecision> | HookDecision;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
interface EvaluateResponse {
|
|
53
|
+
decision: 'allow' | 'block' | 'verify' | 'uncertain' | 'require_approval';
|
|
54
|
+
confidence: number;
|
|
55
|
+
reasoning: string;
|
|
56
|
+
policyContext?: {
|
|
57
|
+
policyId?: string;
|
|
58
|
+
matchedRule?: string;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
declare function evaluate(payload: {
|
|
62
|
+
toolName: string;
|
|
63
|
+
args: unknown;
|
|
64
|
+
agentId?: string;
|
|
65
|
+
agentType?: string;
|
|
66
|
+
agentName?: string;
|
|
67
|
+
apiKey: string;
|
|
68
|
+
policyId?: string;
|
|
69
|
+
baseUrl?: string;
|
|
70
|
+
timeoutMs?: number;
|
|
71
|
+
}): Promise<EvaluateResponse | null>;
|
|
72
|
+
declare function FastGRCPlugin(options: FastGRCPluginOptions): OpenClawPlugin;
|
|
73
|
+
|
|
74
|
+
export { FastGRCApprovalRequiredError, FastGRCBlockedError, FastGRCPlugin, type FastGRCPluginOptions, evaluate };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
interface FastGRCPluginOptions {
|
|
2
|
+
/** Your FastGRC API key (fgrc_k1_...) */
|
|
3
|
+
apiKey: string;
|
|
4
|
+
/** Specific policy ID to evaluate against. Omit to use the org-wide default. */
|
|
5
|
+
policyId?: string;
|
|
6
|
+
/**
|
|
7
|
+
* What to do on a block decision when used outside the plugin hook (e.g. manual invocation).
|
|
8
|
+
* The plugin hook always returns { block: true } per OpenClaw's hook protocol.
|
|
9
|
+
* - 'throw' (default) — throw FastGRCBlockedError
|
|
10
|
+
* - 'warn' — console.warn and allow through
|
|
11
|
+
* - 'silent' — allow through silently
|
|
12
|
+
*/
|
|
13
|
+
onBlock?: 'throw' | 'warn' | 'silent';
|
|
14
|
+
/** Max ms to wait for the FastGRC API. Fail-open on timeout. Default: 3000 */
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
/** Override the FastGRC base URL. Default: https://app.fastgrc.ai */
|
|
17
|
+
baseUrl?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Thrown when FastGRC's policy blocks a tool call (onBlock: 'throw', outside plugin hook) */
|
|
20
|
+
declare class FastGRCBlockedError extends Error {
|
|
21
|
+
readonly matchedRule: string;
|
|
22
|
+
readonly reasoning: string;
|
|
23
|
+
readonly policyId?: string;
|
|
24
|
+
constructor(matchedRule: string, reasoning: string, policyId?: string);
|
|
25
|
+
}
|
|
26
|
+
/** Thrown when FastGRC requires human approval (outside plugin hook) */
|
|
27
|
+
declare class FastGRCApprovalRequiredError extends Error {
|
|
28
|
+
readonly dashboardUrl: string;
|
|
29
|
+
constructor(dashboardUrl: string, reasoning: string);
|
|
30
|
+
}
|
|
31
|
+
type HookDecision = {
|
|
32
|
+
block: true;
|
|
33
|
+
reason?: string;
|
|
34
|
+
} | {
|
|
35
|
+
block: false;
|
|
36
|
+
} | {
|
|
37
|
+
requireApproval: true;
|
|
38
|
+
reason?: string;
|
|
39
|
+
} | undefined;
|
|
40
|
+
interface AgentContext {
|
|
41
|
+
id?: string;
|
|
42
|
+
name?: string;
|
|
43
|
+
type?: string;
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
}
|
|
46
|
+
interface OpenClawPlugin {
|
|
47
|
+
name: string;
|
|
48
|
+
hooks: {
|
|
49
|
+
before_tool_call?: (toolName: string, args: unknown, context: AgentContext) => Promise<HookDecision> | HookDecision;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
interface EvaluateResponse {
|
|
53
|
+
decision: 'allow' | 'block' | 'verify' | 'uncertain' | 'require_approval';
|
|
54
|
+
confidence: number;
|
|
55
|
+
reasoning: string;
|
|
56
|
+
policyContext?: {
|
|
57
|
+
policyId?: string;
|
|
58
|
+
matchedRule?: string;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
declare function evaluate(payload: {
|
|
62
|
+
toolName: string;
|
|
63
|
+
args: unknown;
|
|
64
|
+
agentId?: string;
|
|
65
|
+
agentType?: string;
|
|
66
|
+
agentName?: string;
|
|
67
|
+
apiKey: string;
|
|
68
|
+
policyId?: string;
|
|
69
|
+
baseUrl?: string;
|
|
70
|
+
timeoutMs?: number;
|
|
71
|
+
}): Promise<EvaluateResponse | null>;
|
|
72
|
+
declare function FastGRCPlugin(options: FastGRCPluginOptions): OpenClawPlugin;
|
|
73
|
+
|
|
74
|
+
export { FastGRCApprovalRequiredError, FastGRCBlockedError, FastGRCPlugin, type FastGRCPluginOptions, evaluate };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
FastGRCApprovalRequiredError: () => FastGRCApprovalRequiredError,
|
|
24
|
+
FastGRCBlockedError: () => FastGRCBlockedError,
|
|
25
|
+
FastGRCPlugin: () => FastGRCPlugin,
|
|
26
|
+
evaluate: () => evaluate
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
var DEFAULT_BASE_URL = "https://app.fastgrc.ai";
|
|
30
|
+
var DEFAULT_TIMEOUT_MS = 3e3;
|
|
31
|
+
var FastGRCBlockedError = class extends Error {
|
|
32
|
+
constructor(matchedRule, reasoning, policyId) {
|
|
33
|
+
super(
|
|
34
|
+
`FastGRC policy blocked this action.
|
|
35
|
+
Rule: ${matchedRule}
|
|
36
|
+
Reason: ${reasoning}`
|
|
37
|
+
);
|
|
38
|
+
this.name = "FastGRCBlockedError";
|
|
39
|
+
this.matchedRule = matchedRule;
|
|
40
|
+
this.reasoning = reasoning;
|
|
41
|
+
this.policyId = policyId;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var FastGRCApprovalRequiredError = class extends Error {
|
|
45
|
+
constructor(dashboardUrl, reasoning) {
|
|
46
|
+
super(
|
|
47
|
+
`FastGRC requires human approval before this action.
|
|
48
|
+
Reason: ${reasoning}
|
|
49
|
+
Review at: ${dashboardUrl}`
|
|
50
|
+
);
|
|
51
|
+
this.name = "FastGRCApprovalRequiredError";
|
|
52
|
+
this.dashboardUrl = dashboardUrl;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
async function evaluate(payload) {
|
|
56
|
+
const {
|
|
57
|
+
toolName,
|
|
58
|
+
args,
|
|
59
|
+
agentId,
|
|
60
|
+
agentType,
|
|
61
|
+
agentName,
|
|
62
|
+
apiKey,
|
|
63
|
+
policyId,
|
|
64
|
+
baseUrl = DEFAULT_BASE_URL,
|
|
65
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
66
|
+
} = payload;
|
|
67
|
+
const evalUrl = `${baseUrl.replace(/\/$/, "")}/api/v1/policy-router/evaluate`;
|
|
68
|
+
const content = `tool_name: ${toolName}
|
|
69
|
+
args: ${JSON.stringify(args)}`;
|
|
70
|
+
try {
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
73
|
+
const res = await fetch(evalUrl, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: {
|
|
76
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
77
|
+
"Content-Type": "application/json"
|
|
78
|
+
},
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
subjectContent: content,
|
|
81
|
+
subjectType: "tool_argument",
|
|
82
|
+
direction: "ingress",
|
|
83
|
+
agentId,
|
|
84
|
+
agentType,
|
|
85
|
+
agentName,
|
|
86
|
+
...policyId ? { policyId } : {}
|
|
87
|
+
}),
|
|
88
|
+
signal: controller.signal
|
|
89
|
+
});
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
console.warn(`[fastgrc] Evaluate returned ${res.status} \u2014 failing open`);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
return await res.json();
|
|
96
|
+
} catch (err) {
|
|
97
|
+
const reason = err instanceof Error && err.name === "AbortError" ? "timeout" : String(err);
|
|
98
|
+
console.warn(`[fastgrc] Evaluate failed (${reason}) \u2014 failing open`);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function FastGRCPlugin(options) {
|
|
103
|
+
const {
|
|
104
|
+
apiKey,
|
|
105
|
+
policyId,
|
|
106
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
107
|
+
baseUrl = DEFAULT_BASE_URL
|
|
108
|
+
} = options;
|
|
109
|
+
const dashboardUrl = `${baseUrl.replace(/\/$/, "")}/dashboard/agent-policies`;
|
|
110
|
+
return {
|
|
111
|
+
name: "fastgrc",
|
|
112
|
+
hooks: {
|
|
113
|
+
async before_tool_call(toolName, args, context) {
|
|
114
|
+
const result = await evaluate({
|
|
115
|
+
toolName,
|
|
116
|
+
args,
|
|
117
|
+
agentId: context.id,
|
|
118
|
+
agentType: context.type,
|
|
119
|
+
agentName: context.name,
|
|
120
|
+
apiKey,
|
|
121
|
+
policyId,
|
|
122
|
+
baseUrl,
|
|
123
|
+
timeoutMs
|
|
124
|
+
});
|
|
125
|
+
if (!result) return { block: false };
|
|
126
|
+
const { decision, reasoning, policyContext } = result;
|
|
127
|
+
const matchedRule = policyContext?.matchedRule ?? "policy rule";
|
|
128
|
+
if (decision === "block") {
|
|
129
|
+
return { block: true, reason: `[${matchedRule}] ${reasoning}` };
|
|
130
|
+
}
|
|
131
|
+
if (decision === "require_approval") {
|
|
132
|
+
return {
|
|
133
|
+
requireApproval: true,
|
|
134
|
+
reason: `${reasoning} \u2014 review at ${dashboardUrl}`
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return { block: false };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
143
|
+
0 && (module.exports = {
|
|
144
|
+
FastGRCApprovalRequiredError,
|
|
145
|
+
FastGRCBlockedError,
|
|
146
|
+
FastGRCPlugin,
|
|
147
|
+
evaluate
|
|
148
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var DEFAULT_BASE_URL = "https://app.fastgrc.ai";
|
|
3
|
+
var DEFAULT_TIMEOUT_MS = 3e3;
|
|
4
|
+
var FastGRCBlockedError = class extends Error {
|
|
5
|
+
constructor(matchedRule, reasoning, policyId) {
|
|
6
|
+
super(
|
|
7
|
+
`FastGRC policy blocked this action.
|
|
8
|
+
Rule: ${matchedRule}
|
|
9
|
+
Reason: ${reasoning}`
|
|
10
|
+
);
|
|
11
|
+
this.name = "FastGRCBlockedError";
|
|
12
|
+
this.matchedRule = matchedRule;
|
|
13
|
+
this.reasoning = reasoning;
|
|
14
|
+
this.policyId = policyId;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var FastGRCApprovalRequiredError = class extends Error {
|
|
18
|
+
constructor(dashboardUrl, reasoning) {
|
|
19
|
+
super(
|
|
20
|
+
`FastGRC requires human approval before this action.
|
|
21
|
+
Reason: ${reasoning}
|
|
22
|
+
Review at: ${dashboardUrl}`
|
|
23
|
+
);
|
|
24
|
+
this.name = "FastGRCApprovalRequiredError";
|
|
25
|
+
this.dashboardUrl = dashboardUrl;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
async function evaluate(payload) {
|
|
29
|
+
const {
|
|
30
|
+
toolName,
|
|
31
|
+
args,
|
|
32
|
+
agentId,
|
|
33
|
+
agentType,
|
|
34
|
+
agentName,
|
|
35
|
+
apiKey,
|
|
36
|
+
policyId,
|
|
37
|
+
baseUrl = DEFAULT_BASE_URL,
|
|
38
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
39
|
+
} = payload;
|
|
40
|
+
const evalUrl = `${baseUrl.replace(/\/$/, "")}/api/v1/policy-router/evaluate`;
|
|
41
|
+
const content = `tool_name: ${toolName}
|
|
42
|
+
args: ${JSON.stringify(args)}`;
|
|
43
|
+
try {
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
46
|
+
const res = await fetch(evalUrl, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: {
|
|
49
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
50
|
+
"Content-Type": "application/json"
|
|
51
|
+
},
|
|
52
|
+
body: JSON.stringify({
|
|
53
|
+
subjectContent: content,
|
|
54
|
+
subjectType: "tool_argument",
|
|
55
|
+
direction: "ingress",
|
|
56
|
+
agentId,
|
|
57
|
+
agentType,
|
|
58
|
+
agentName,
|
|
59
|
+
...policyId ? { policyId } : {}
|
|
60
|
+
}),
|
|
61
|
+
signal: controller.signal
|
|
62
|
+
});
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
console.warn(`[fastgrc] Evaluate returned ${res.status} \u2014 failing open`);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return await res.json();
|
|
69
|
+
} catch (err) {
|
|
70
|
+
const reason = err instanceof Error && err.name === "AbortError" ? "timeout" : String(err);
|
|
71
|
+
console.warn(`[fastgrc] Evaluate failed (${reason}) \u2014 failing open`);
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function FastGRCPlugin(options) {
|
|
76
|
+
const {
|
|
77
|
+
apiKey,
|
|
78
|
+
policyId,
|
|
79
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
80
|
+
baseUrl = DEFAULT_BASE_URL
|
|
81
|
+
} = options;
|
|
82
|
+
const dashboardUrl = `${baseUrl.replace(/\/$/, "")}/dashboard/agent-policies`;
|
|
83
|
+
return {
|
|
84
|
+
name: "fastgrc",
|
|
85
|
+
hooks: {
|
|
86
|
+
async before_tool_call(toolName, args, context) {
|
|
87
|
+
const result = await evaluate({
|
|
88
|
+
toolName,
|
|
89
|
+
args,
|
|
90
|
+
agentId: context.id,
|
|
91
|
+
agentType: context.type,
|
|
92
|
+
agentName: context.name,
|
|
93
|
+
apiKey,
|
|
94
|
+
policyId,
|
|
95
|
+
baseUrl,
|
|
96
|
+
timeoutMs
|
|
97
|
+
});
|
|
98
|
+
if (!result) return { block: false };
|
|
99
|
+
const { decision, reasoning, policyContext } = result;
|
|
100
|
+
const matchedRule = policyContext?.matchedRule ?? "policy rule";
|
|
101
|
+
if (decision === "block") {
|
|
102
|
+
return { block: true, reason: `[${matchedRule}] ${reasoning}` };
|
|
103
|
+
}
|
|
104
|
+
if (decision === "require_approval") {
|
|
105
|
+
return {
|
|
106
|
+
requireApproval: true,
|
|
107
|
+
reason: `${reasoning} \u2014 review at ${dashboardUrl}`
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return { block: false };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export {
|
|
116
|
+
FastGRCApprovalRequiredError,
|
|
117
|
+
FastGRCBlockedError,
|
|
118
|
+
FastGRCPlugin,
|
|
119
|
+
evaluate
|
|
120
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fastgrc-openclaw",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "FastGRC agent compliance plugin for OpenClaw — evaluates every tool call against your policy before it executes",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"fastgrc-hook": "./dist/bin.js"
|
|
17
|
+
},
|
|
18
|
+
"files": ["dist", "README.md"],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"dev": "tsup --watch",
|
|
22
|
+
"typecheck": "tsc --noEmit"
|
|
23
|
+
},
|
|
24
|
+
"keywords": ["fastgrc", "openclaw", "ai-agent", "compliance", "policy", "security"],
|
|
25
|
+
"author": "FastGRC <support@fastgrc.ai>",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"tsup": "^8.0.0",
|
|
29
|
+
"typescript": "^5.0.0"
|
|
30
|
+
},
|
|
31
|
+
"engines": { "node": ">=18" }
|
|
32
|
+
}
|