@llnvd/openclaw-url-guard 0.0.1 → 0.1.1

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 CHANGED
@@ -6,54 +6,45 @@ OpenClaw plugin that guards web tool access with hostname allowlist/blocklist po
6
6
  > warranty of any kind. We make no guarantees about its security, reliability, or fitness for
7
7
  > any particular purpose. Use at your own risk.
8
8
 
9
- ## Install
9
+ ## How It Works
10
10
 
11
- ### From Git (recommended for now)
11
+ This plugin uses OpenClaw's `before_tool_call` hook to intercept calls to `web_fetch` and
12
+ `web_search`. When either tool is called:
12
13
 
13
- The npm package is not yet published. Install directly from Codeberg:
14
+ 1. The plugin extracts the URL from the request
15
+ 2. Checks it against your configured allowlist/blocklist policy
16
+ 3. Optionally queries URLhaus threat feed
17
+ 4. Optionally applies trust scoring
18
+ 5. **Blocks** the call if the URL fails any check, or **allows** it to proceed
14
19
 
15
- ```bash
16
- # Clone and build
17
- git clone https://codeberg.org/llnvd/openclaw-url-guard.git
18
- cd openclaw-url-guard
19
- npm install
20
- npm run build
21
-
22
- # Install as OpenClaw plugin (from the repo directory)
23
- openclaw plugins install .
24
- ```
20
+ This approach is transparent to the LLM — it uses the standard `web_fetch` and `web_search`
21
+ tools, but they're guarded by your policy.
25
22
 
26
- Or install directly via npm/git URL:
23
+ ## Install
27
24
 
28
25
  ```bash
29
- npm install git+https://codeberg.org/llnvd/openclaw-url-guard.git
26
+ npm install @llnvd/openclaw-url-guard
30
27
  ```
31
28
 
32
- ### From npm (once published)
29
+ Or from git:
33
30
 
34
31
  ```bash
35
- npm install @llnvd/openclaw-url-guard
32
+ git clone https://codeberg.org/llnvd/openclaw-url-guard.git
33
+ cd openclaw-url-guard
34
+ npm install
35
+ npm run build
36
+ openclaw plugins install .
36
37
  ```
37
38
 
38
- ## Quick Start (Secure Installation)
39
+ ## Quick Start
39
40
 
40
- This plugin provides guarded alternatives to the native `web_fetch` and `web_search` tools.
41
- **For full protection, you must disable the native tools** — otherwise the LLM can bypass
42
- the guard by calling the unprotected tools directly.
41
+ Add to your OpenClaw config (`~/.openclaw/openclaw.json`):
43
42
 
44
- Add to OpenClaw config (`~/.openclaw/openclaw.json`):
45
-
46
- ```json5
43
+ ```json
47
44
  {
48
- // Disable native web tools (required for security)
49
- "tools": {
50
- "deny": ["web_fetch", "web_search"]
51
- },
52
-
53
- // Enable the url-guard plugin
54
45
  "plugins": {
55
46
  "entries": {
56
- "@llnvd/openclaw-url-guard": {
47
+ "openclaw-url-guard": {
57
48
  "enabled": true,
58
49
  "config": {
59
50
  "mode": "allowlist",
@@ -62,8 +53,10 @@ Add to OpenClaw config (`~/.openclaw/openclaw.json`):
62
53
  "developer.mozilla.org",
63
54
  "en.wikipedia.org",
64
55
  "github.com",
65
- "stackoverflow.com"
66
- ]
56
+ "stackoverflow.com",
57
+ "*.stackexchange.com"
58
+ ],
59
+ "blockPrivateIps": true
67
60
  }
68
61
  }
69
62
  }
@@ -71,120 +64,118 @@ Add to OpenClaw config (`~/.openclaw/openclaw.json`):
71
64
  }
72
65
  ```
73
66
 
74
- This configuration:
75
- 1. **Denies** the native `web_fetch` and `web_search` tools (they won't be sent to the LLM)
76
- 2. **Enables** the plugin's `safe_web_fetch` and `safe_web_search` as the only web access tools
77
-
78
- The guarded tools:
79
- - `safe_web_fetch` — fetches URLs through the policy filter
80
- - `safe_web_search` — searches the web and filters results by policy
81
-
82
- > **⚠️ Security Note:** Without `tools.deny`, the native tools remain available and a
83
- > prompt injection attack could instruct the LLM to use `web_fetch` instead of
84
- > `safe_web_fetch`, completely bypassing the guard.
85
-
86
- Optional threat intel feed settings:
87
-
88
- ```yaml
89
- plugins:
90
- - name: "@llnvd/openclaw-url-guard"
91
- config:
92
- mode: allowlist
93
- allowlist:
94
- - docs.python.org
95
- threatFeeds:
96
- urlhaus: true
97
- mode: fail-open # default: fail-open, alternative: fail-closed
98
- ```
67
+ Restart the gateway and the plugin will automatically guard all `web_fetch` and `web_search` calls.
99
68
 
100
- URLhaus behavior:
69
+ ## Configuration
101
70
 
102
- - applies to `safe_web_fetch` only
103
- - runs after local allowlist/blocklist checks pass
104
- - blocks when URLhaus lists the URL
105
- - on URLhaus API/network failure:
106
- - `fail-open`: allow request
107
- - `fail-closed`: block request
71
+ ### Modes
108
72
 
109
- ## Security Defaults
73
+ | Mode | Description |
74
+ |------|-------------|
75
+ | `allowlist` | Only URLs matching the allowlist are allowed (default) |
76
+ | `blocklist` | All URLs allowed except those matching the blocklist |
77
+ | `hybrid` | URL must be in allowlist AND not in blocklist |
110
78
 
111
- - only `http://` and `https://` URLs are accepted
112
- - private/internal IP targets are blocked by default (`blockPrivateIps: true`)
113
- - URLhaus lookups time out after 5 seconds to avoid hanging requests
114
- - blocked tool responses are minimal by default; set `logging.verboseErrors: true` for detailed reasons and score metadata
79
+ ### Basic Options
115
80
 
116
- ## Trust Scoring (Issue #3)
81
+ ```json
82
+ {
83
+ "mode": "allowlist",
84
+ "allowlist": ["github.com", "*.githubusercontent.com"],
85
+ "blocklist": ["evil.com"],
86
+ "blockPrivateIps": true
87
+ }
88
+ ```
117
89
 
118
- Optional trust scoring replaces binary allow/block with a score range from `+10` to `-10`.
90
+ - **allowlist/blocklist**: Hostname patterns. Use `*.domain.com` for wildcard subdomains.
91
+ - **blockPrivateIps**: Block requests to private/internal IPs (default: `true`)
119
92
 
120
- - `+10` to `+7`: Trusted (always allow, skips threat feed checks)
121
- - `+6` to `+3`: Preferred (allow, still check threat feeds)
122
- - `+2` to `-2`: Neutral
123
- - `-3` to `-6`: Suspicious (allow with warning when logging is enabled)
124
- - `-7` to `-9`: Risky (blocked by default)
125
- - `-10`: Forbidden (always blocked)
126
-
127
- Scoring is disabled by default for backward compatibility. Enable it with:
128
-
129
- ```yaml
130
- plugins:
131
- - name: "@llnvd/openclaw-url-guard"
132
- config:
133
- mode: allowlist
134
- scoring:
135
- enabled: true
136
- defaultScore: 0
137
- minScore: -6
138
- rules:
139
- - domain: github.com
140
- score: 8
141
- reason: trusted source
93
+ ### Threat Feeds
94
+
95
+ Enable URLhaus threat feed lookups:
96
+
97
+ ```json
98
+ {
99
+ "mode": "allowlist",
100
+ "allowlist": ["*"],
101
+ "threatFeeds": {
102
+ "urlhaus": true,
103
+ "mode": "fail-open"
104
+ }
105
+ }
142
106
  ```
143
107
 
144
- ## Documentation
108
+ - `urlhaus`: Enable URLhaus API lookups
109
+ - `mode`: `fail-open` (allow on API failure) or `fail-closed` (block on API failure)
145
110
 
146
- - [Documentation Index](./docs/README.md)
147
- - [OpenClaw Integration](./docs/openclaw-integration.md) — **start here**
148
- - [Agent Installation Guide](./docs/AGENT-INSTALL.md) — for AI agents installing this plugin
149
- - [API Reference](./docs/api-reference.md)
150
- - [Configuration](./docs/configuration.md)
151
- - [Trust Scoring](./docs/scoring.md)
152
- - [Testing](./docs/testing.md)
153
- - [Usage Modes](./docs/usage-modes.md)
111
+ ### Trust Scoring
154
112
 
155
- ## Development
113
+ Optional scoring system for fine-grained control:
156
114
 
157
- ```bash
158
- npm test
115
+ ```json
116
+ {
117
+ "mode": "allowlist",
118
+ "allowlist": ["*"],
119
+ "scoring": {
120
+ "enabled": true,
121
+ "defaultScore": 0,
122
+ "minScore": -6,
123
+ "rules": [
124
+ { "domain": "github.com", "score": 10, "reason": "highly trusted" },
125
+ { "domain": "*.sketch.com", "score": -5, "reason": "known suspicious" }
126
+ ]
127
+ }
128
+ }
159
129
  ```
160
130
 
161
- Run E2E tests only:
131
+ Score ranges:
132
+ - `+10` to `+7`: Trusted (always allow)
133
+ - `+6` to `+3`: Preferred
134
+ - `+2` to `-2`: Neutral
135
+ - `-3` to `-6`: Suspicious
136
+ - `-7` to `-10`: Blocked
162
137
 
163
- ```bash
164
- npm run test:e2e
138
+ ## Security Defaults
139
+
140
+ - Only `http://` and `https://` protocols accepted
141
+ - Private/internal IP targets blocked by default
142
+ - URLhaus lookups timeout after 5 seconds
143
+ - Invalid URLs are blocked
144
+
145
+ ## Logging
146
+
147
+ Enable verbose logging to see guard decisions:
148
+
149
+ ```json
150
+ {
151
+ "logging": {
152
+ "enabled": true,
153
+ "logBlocked": true,
154
+ "verboseErrors": true
155
+ }
156
+ }
165
157
  ```
166
158
 
167
- Enable live URLhaus network E2E test:
159
+ ## Development
168
160
 
169
161
  ```bash
170
- E2E_NETWORK_TESTS=true npm run test:e2e
162
+ npm install
163
+ npm run build
164
+ npm test
171
165
  ```
172
166
 
173
- See [CONTRIBUTING.md](./CONTRIBUTING.md) for contributor workflow.
174
-
175
- ## CI Pipeline
167
+ Run E2E tests:
176
168
 
177
- This project uses Codeberg Actions (Forgejo Actions) for continuous integration.
169
+ ```bash
170
+ npm run test:e2e
171
+ ```
178
172
 
179
- | Job | Runner | Trigger | Description |
180
- |-----|--------|---------|-------------|
181
- | Lint & Format | `codeberg-tiny` | push, PR | oxlint + prettier |
182
- | Build | `codeberg-tiny` | push, PR | TypeScript compilation |
183
- | Unit Tests | `codeberg-small` | push, PR | Core functionality tests |
184
- | E2E Tests | `codeberg-small` | push, PR | Integration test harness |
185
- | Live Feed Tests | `codeberg-small` | main push only | URLhaus network tests |
173
+ ## Documentation
186
174
 
187
- Workflow: [`.forgejo/workflows/ci.yaml`](./.forgejo/workflows/ci.yaml)
175
+ - [Configuration](./docs/configuration.md)
176
+ - [API Reference](./docs/api-reference.md)
177
+ - [Trust Scoring](./docs/scoring.md)
178
+ - [Testing](./docs/testing.md)
188
179
 
189
180
  ## License
190
181
 
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llnvd/openclaw-url-guard",
3
- "version": "0.0.1",
3
+ "version": "0.1.1",
4
4
  "description": "OpenClaw plugin for URL allowlisting/blocklisting in web_fetch and web_search tools",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,3 +1,200 @@
1
- import type { PluginDefinition } from './types';
2
- declare const plugin: PluginDefinition;
1
+ import type { UrlGuardConfig } from './types';
2
+ interface PluginLogger {
3
+ debug?: (message: string) => void;
4
+ info: (message: string) => void;
5
+ warn: (message: string) => void;
6
+ error: (message: string) => void;
7
+ }
8
+ interface BeforeToolCallEvent {
9
+ toolName: string;
10
+ params: Record<string, unknown>;
11
+ runId?: string;
12
+ toolCallId?: string;
13
+ }
14
+ interface BeforeToolCallContext {
15
+ agentId?: string;
16
+ sessionKey?: string;
17
+ sessionId?: string;
18
+ runId?: string;
19
+ toolCallId?: string;
20
+ }
21
+ interface BeforeToolCallResult {
22
+ block?: boolean;
23
+ blockReason?: string;
24
+ params?: Record<string, unknown>;
25
+ }
26
+ type BeforeToolCallHandler = (event: BeforeToolCallEvent, ctx: BeforeToolCallContext) => Promise<BeforeToolCallResult | void> | BeforeToolCallResult | void;
27
+ interface OpenClawPluginApi {
28
+ id: string;
29
+ name: string;
30
+ pluginConfig?: Record<string, unknown>;
31
+ logger: PluginLogger;
32
+ on: (hookName: 'before_tool_call', handler: BeforeToolCallHandler) => void;
33
+ }
34
+ declare const GUARDED_TOOLS: Set<string>;
35
+ /**
36
+ * Check a URL against the guard policy.
37
+ * Returns { allowed: true } or { allowed: false, reason: string }
38
+ */
39
+ declare function checkUrl(url: string, config: UrlGuardConfig, logger: PluginLogger): Promise<{
40
+ allowed: true;
41
+ } | {
42
+ allowed: false;
43
+ reason: string;
44
+ }>;
45
+ declare const plugin: {
46
+ id: string;
47
+ name: string;
48
+ version: string;
49
+ description: string;
50
+ configSchema: {
51
+ readonly type: "object";
52
+ readonly additionalProperties: false;
53
+ readonly required: readonly ["mode"];
54
+ readonly properties: {
55
+ readonly mode: {
56
+ readonly type: "string";
57
+ readonly enum: readonly ["allowlist", "blocklist", "hybrid"];
58
+ };
59
+ readonly allowlist: {
60
+ readonly type: "array";
61
+ readonly items: {
62
+ readonly type: "string";
63
+ };
64
+ };
65
+ readonly blocklist: {
66
+ readonly type: "array";
67
+ readonly items: {
68
+ readonly type: "string";
69
+ };
70
+ };
71
+ readonly blockPrivateIps: {
72
+ readonly type: "boolean";
73
+ };
74
+ readonly threatFeeds: {
75
+ readonly type: "object";
76
+ readonly additionalProperties: false;
77
+ readonly properties: {
78
+ readonly urlhaus: {
79
+ readonly type: "boolean";
80
+ };
81
+ readonly mode: {
82
+ readonly type: "string";
83
+ readonly enum: readonly ["fail-open", "fail-closed"];
84
+ };
85
+ };
86
+ };
87
+ readonly scoring: {
88
+ readonly type: "object";
89
+ readonly additionalProperties: false;
90
+ readonly properties: {
91
+ readonly enabled: {
92
+ readonly type: "boolean";
93
+ };
94
+ readonly defaultScore: {
95
+ readonly type: "number";
96
+ readonly minimum: -10;
97
+ readonly maximum: 10;
98
+ };
99
+ readonly minScore: {
100
+ readonly type: "number";
101
+ readonly minimum: -10;
102
+ readonly maximum: 10;
103
+ };
104
+ readonly rules: {
105
+ readonly type: "array";
106
+ readonly items: {
107
+ readonly type: "object";
108
+ readonly additionalProperties: false;
109
+ readonly required: readonly ["domain", "score"];
110
+ readonly properties: {
111
+ readonly domain: {
112
+ readonly type: "string";
113
+ };
114
+ readonly score: {
115
+ readonly type: "number";
116
+ readonly minimum: -10;
117
+ readonly maximum: 10;
118
+ };
119
+ readonly reason: {
120
+ readonly type: "string";
121
+ };
122
+ };
123
+ };
124
+ };
125
+ readonly feedScores: {
126
+ readonly type: "object";
127
+ readonly additionalProperties: false;
128
+ readonly properties: {
129
+ readonly urlhaus: {
130
+ readonly type: "object";
131
+ readonly additionalProperties: false;
132
+ readonly properties: {
133
+ readonly online: {
134
+ readonly type: "number";
135
+ readonly minimum: -10;
136
+ readonly maximum: 10;
137
+ };
138
+ readonly offline: {
139
+ readonly type: "number";
140
+ readonly minimum: -10;
141
+ readonly maximum: 10;
142
+ };
143
+ };
144
+ };
145
+ readonly spamhaus: {
146
+ readonly type: "object";
147
+ readonly additionalProperties: false;
148
+ readonly properties: {
149
+ readonly botnet_cc: {
150
+ readonly type: "number";
151
+ readonly minimum: -10;
152
+ readonly maximum: 10;
153
+ };
154
+ readonly phishing_domain: {
155
+ readonly type: "number";
156
+ readonly minimum: -10;
157
+ readonly maximum: 10;
158
+ };
159
+ readonly spammer_domain: {
160
+ readonly type: "number";
161
+ readonly minimum: -10;
162
+ readonly maximum: 10;
163
+ };
164
+ readonly abused_redirector: {
165
+ readonly type: "number";
166
+ readonly minimum: -10;
167
+ readonly maximum: 10;
168
+ };
169
+ readonly 'abused_legit_*': {
170
+ readonly type: "number";
171
+ readonly minimum: -10;
172
+ readonly maximum: 10;
173
+ };
174
+ };
175
+ };
176
+ };
177
+ };
178
+ };
179
+ };
180
+ readonly logging: {
181
+ readonly type: "object";
182
+ readonly additionalProperties: false;
183
+ readonly properties: {
184
+ readonly enabled: {
185
+ readonly type: "boolean";
186
+ };
187
+ readonly logBlocked: {
188
+ readonly type: "boolean";
189
+ };
190
+ readonly verboseErrors: {
191
+ readonly type: "boolean";
192
+ };
193
+ };
194
+ };
195
+ };
196
+ };
197
+ register(api: OpenClawPluginApi): void;
198
+ };
3
199
  export default plugin;
200
+ export { checkUrl, GUARDED_TOOLS };
package/dist/src/index.js CHANGED
@@ -3,14 +3,127 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.GUARDED_TOOLS = void 0;
7
+ exports.checkUrl = checkUrl;
6
8
  const config_1 = require("./config");
7
- const safeFetch_1 = require("./tools/safeFetch");
8
- const safeSearch_1 = require("./tools/safeSearch");
9
+ const matcher_1 = require("./filters/matcher");
10
+ const urlhaus_1 = require("./filters/urlhaus");
11
+ const engine_1 = require("./scoring/engine");
9
12
  const package_json_1 = __importDefault(require("../package.json"));
13
+ // Tools we intercept
14
+ const GUARDED_TOOLS = new Set(['web_fetch', 'web_search']);
15
+ exports.GUARDED_TOOLS = GUARDED_TOOLS;
16
+ /**
17
+ * Check a URL against the guard policy.
18
+ * Returns { allowed: true } or { allowed: false, reason: string }
19
+ */
20
+ async function checkUrl(url, config, logger) {
21
+ const hostname = (0, matcher_1.extractHostname)(url);
22
+ if (!hostname) {
23
+ return { allowed: false, reason: 'Invalid URL format' };
24
+ }
25
+ // Check private IP blocking
26
+ if (config.blockPrivateIps && (0, matcher_1.isPrivateIp)(hostname)) {
27
+ return { allowed: false, reason: 'Private/internal IP addresses are blocked' };
28
+ }
29
+ // Check allowlist/blocklist policy
30
+ if (!(0, matcher_1.isAllowed)(url, config)) {
31
+ const reason = (0, matcher_1.getBlockReason)(url, config);
32
+ return { allowed: false, reason: `URL blocked: ${reason}` };
33
+ }
34
+ // Check threat feeds if enabled
35
+ let feedResults;
36
+ if (config.threatFeeds?.urlhaus) {
37
+ try {
38
+ const urlhausResult = await (0, urlhaus_1.lookupUrlhaus)(url);
39
+ if (urlhausResult.listed) {
40
+ return {
41
+ allowed: false,
42
+ reason: `URL blocked by threat feed: ${urlhausResult.reason ?? 'listed in URLhaus'}`,
43
+ };
44
+ }
45
+ if (urlhausResult.feedSignals) {
46
+ feedResults = { signals: urlhausResult.feedSignals };
47
+ }
48
+ if (urlhausResult.unavailable) {
49
+ feedResults = { unavailable: true, reason: urlhausResult.reason };
50
+ if (config.threatFeeds.mode === 'fail-closed') {
51
+ return {
52
+ allowed: false,
53
+ reason: 'Threat feed unavailable and fail-closed mode is enabled',
54
+ };
55
+ }
56
+ logger.warn?.(`[url-guard] Threat feed unavailable: ${urlhausResult.reason}`);
57
+ }
58
+ }
59
+ catch (err) {
60
+ const message = err instanceof Error ? err.message : String(err);
61
+ logger.warn?.(`[url-guard] Threat feed lookup failed: ${message}`);
62
+ if (config.threatFeeds.mode === 'fail-closed') {
63
+ return {
64
+ allowed: false,
65
+ reason: 'Threat feed lookup failed and fail-closed mode is enabled',
66
+ };
67
+ }
68
+ }
69
+ }
70
+ // Apply scoring if enabled
71
+ if (config.scoring?.enabled) {
72
+ const scoreResult = (0, engine_1.calculateScore)(url, config, feedResults);
73
+ if (!scoreResult.allowed) {
74
+ const sources = scoreResult.sources.map((s) => s.source).join(', ');
75
+ return {
76
+ allowed: false,
77
+ reason: `URL blocked by scoring (score: ${scoreResult.finalScore}, sources: ${sources})`,
78
+ };
79
+ }
80
+ }
81
+ return { allowed: true };
82
+ }
10
83
  const plugin = {
11
- name: '@llnvd/openclaw-url-guard',
84
+ id: 'openclaw-url-guard',
85
+ name: 'URL Guard',
12
86
  version: package_json_1.default.version,
13
- tools: [safeFetch_1.safeWebFetchTool, safeSearch_1.safeWebSearchTool],
87
+ description: 'Guards web tool access with hostname allowlist/blocklist policy and optional URLhaus threat feed integration.',
14
88
  configSchema: config_1.configSchema,
89
+ register(api) {
90
+ const config = (0, config_1.normalizeConfig)(api.pluginConfig);
91
+ api.logger.info(`[url-guard] Registering URL Guard plugin v${package_json_1.default.version}`);
92
+ api.logger.info(`[url-guard] Mode: ${config.mode}, Allowlist: ${config.allowlist?.length ?? 0} domains, ` +
93
+ `Blocklist: ${config.blocklist?.length ?? 0} domains`);
94
+ if (config.threatFeeds?.urlhaus) {
95
+ api.logger.info(`[url-guard] URLhaus threat feed enabled (mode: ${config.threatFeeds.mode ?? 'fail-open'})`);
96
+ }
97
+ if (config.scoring?.enabled) {
98
+ api.logger.info(`[url-guard] Scoring enabled (minScore: ${config.scoring.minScore ?? -6}, ` +
99
+ `rules: ${config.scoring.rules?.length ?? 0})`);
100
+ }
101
+ // Register the before_tool_call hook to intercept web_fetch and web_search
102
+ api.on('before_tool_call', async (event, _ctx) => {
103
+ // Only intercept guarded tools
104
+ if (!GUARDED_TOOLS.has(event.toolName)) {
105
+ return;
106
+ }
107
+ // Extract URL from params
108
+ const url = event.params.url;
109
+ if (!url || typeof url !== 'string') {
110
+ // No URL parameter - let it through (tool will handle the error)
111
+ return;
112
+ }
113
+ // Check the URL against our policy
114
+ const result = await checkUrl(url, config, api.logger);
115
+ if (!result.allowed) {
116
+ api.logger.info?.(`[url-guard] Blocked ${event.toolName}: ${url} - ${result.reason}`);
117
+ return {
118
+ block: true,
119
+ blockReason: result.reason,
120
+ };
121
+ }
122
+ // URL is allowed - let the tool execute
123
+ api.logger.debug?.(`[url-guard] Allowed ${event.toolName}: ${url}`);
124
+ return;
125
+ });
126
+ api.logger.info('[url-guard] Hook registered for: web_fetch, web_search');
127
+ },
15
128
  };
16
129
  exports.default = plugin;