@llnvd/openclaw-url-guard 0.0.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.
Files changed (83) hide show
  1. package/.forgejo/workflows/ci.yaml +117 -0
  2. package/.husky/pre-commit +1 -0
  3. package/.oxlintrc.json +14 -0
  4. package/.prettierignore +3 -0
  5. package/.prettierrc +7 -0
  6. package/CONTRIBUTING.md +173 -0
  7. package/LICENSE +21 -0
  8. package/README.md +191 -0
  9. package/TASK.md +39 -0
  10. package/dist/package.json +39 -0
  11. package/dist/src/config.d.ts +150 -0
  12. package/dist/src/config.js +206 -0
  13. package/dist/src/filters/matcher.d.ts +6 -0
  14. package/dist/src/filters/matcher.js +111 -0
  15. package/dist/src/filters/urlhaus.d.ts +8 -0
  16. package/dist/src/filters/urlhaus.js +141 -0
  17. package/dist/src/index.d.ts +3 -0
  18. package/dist/src/index.js +16 -0
  19. package/dist/src/scoring/engine.d.ts +2 -0
  20. package/dist/src/scoring/engine.js +118 -0
  21. package/dist/src/tools/safeFetch.d.ts +2 -0
  22. package/dist/src/tools/safeFetch.js +121 -0
  23. package/dist/src/tools/safeSearch.d.ts +2 -0
  24. package/dist/src/tools/safeSearch.js +81 -0
  25. package/dist/src/types.d.ts +99 -0
  26. package/dist/src/types.js +2 -0
  27. package/docs/AGENT-INSTALL.md +118 -0
  28. package/docs/DEPLOY_GIST.md +84 -0
  29. package/docs/README.md +40 -0
  30. package/docs/api-reference.md +274 -0
  31. package/docs/configuration.md +218 -0
  32. package/docs/openclaw-integration.md +155 -0
  33. package/docs/scoring.md +81 -0
  34. package/docs/testing.md +53 -0
  35. package/docs/usage-modes.md +127 -0
  36. package/examples/openclaw-config-avoid-sites.yaml +49 -0
  37. package/examples/openclaw-config-learning-sites.yaml +43 -0
  38. package/examples/openclaw-config-scoring.yaml +43 -0
  39. package/openclaw.plugin.json +66 -0
  40. package/openspec/changes/e2e-real-openclaw-tests/design.md +106 -0
  41. package/openspec/changes/e2e-real-openclaw-tests/proposal.md +35 -0
  42. package/openspec/changes/e2e-real-openclaw-tests/specs/e2e-real-instance/spec.md +197 -0
  43. package/openspec/changes/e2e-real-openclaw-tests/tasks.md +34 -0
  44. package/openspec/config.yaml +8 -0
  45. package/openspec/specs/e2e-real-instance/spec.md +197 -0
  46. package/package.json +39 -0
  47. package/src/config.ts +228 -0
  48. package/src/filters/matcher.ts +126 -0
  49. package/src/filters/urlhaus.ts +170 -0
  50. package/src/index.ts +14 -0
  51. package/src/scoring/engine.ts +144 -0
  52. package/src/tools/safeFetch.ts +163 -0
  53. package/src/tools/safeSearch.ts +108 -0
  54. package/src/types.ts +136 -0
  55. package/tests/e2e/cases/backward-compat.test.ts +22 -0
  56. package/tests/e2e/cases/blocklist-block.test.ts +18 -0
  57. package/tests/e2e/cases/scoring-suspicious.test.ts +30 -0
  58. package/tests/e2e/cases/trusted-bypass.test.ts +31 -0
  59. package/tests/e2e/cases/urlhaus-live.test.ts +33 -0
  60. package/tests/e2e/fixtures/config.ts +60 -0
  61. package/tests/e2e/fixtures/test-urls.ts +5 -0
  62. package/tests/e2e/harness.ts +73 -0
  63. package/tests/integration/allowlist.test.ts +87 -0
  64. package/tests/integration/blocklist.test.ts +48 -0
  65. package/tests/integration/bypass-protection.test.ts +151 -0
  66. package/tests/integration/config-validation.test.ts +55 -0
  67. package/tests/integration/error-handling.test.ts +57 -0
  68. package/tests/integration/fixtures/urlhaus-sample.csv +6 -0
  69. package/tests/integration/helpers/client.ts +185 -0
  70. package/tests/integration/helpers/config.ts +103 -0
  71. package/tests/integration/helpers/gateway.ts +185 -0
  72. package/tests/integration/helpers/test-mode.ts +20 -0
  73. package/tests/integration/hybrid.test.ts +56 -0
  74. package/tests/integration/ssrf.test.ts +61 -0
  75. package/tests/integration/urlhaus.test.ts +56 -0
  76. package/tests/integration.test.ts +72 -0
  77. package/tests/matcher.test.ts +62 -0
  78. package/tests/safeFetch.test.ts +247 -0
  79. package/tests/safeSearch.test.ts +80 -0
  80. package/tests/scoring.test.ts +106 -0
  81. package/tests/security.test.ts +130 -0
  82. package/tests/urlhaus.test.ts +124 -0
  83. package/tsconfig.json +16 -0
@@ -0,0 +1,197 @@
1
+ # E2E Real Instance Integration Tests
2
+
3
+ ## ADDED Requirements
4
+
5
+ ### Requirement: Gateway lifecycle management
6
+
7
+ The system SHALL provide helper functions to start, health-check, and stop a real OpenClaw gateway instance for testing purposes.
8
+
9
+ #### Scenario: Start gateway with custom config
10
+
11
+ - **WHEN** a test suite starts with a given `UrlGuardConfig`
12
+ - **THEN** the helper SHALL generate a temp config file with the plugin configured
13
+ - **AND** spawn `openclaw gateway` as a subprocess on a dynamically assigned port
14
+ - **AND** wait for the health endpoint to respond (HTTP 200) with exponential backoff
15
+ - **AND** fail the test suite if the gateway does not become healthy within 30 seconds
16
+
17
+ #### Scenario: Stop gateway on suite teardown
18
+
19
+ - **WHEN** a test suite completes (pass or fail)
20
+ - **THEN** the helper SHALL kill the gateway subprocess and delete temp config files
21
+ - **AND** if the test runner crashes, a `process.on('exit')` handler SHALL kill any tracked gateway PIDs
22
+
23
+ #### Scenario: Port isolation
24
+
25
+ - **WHEN** multiple test suites run (sequentially or in parallel)
26
+ - **THEN** each suite SHALL use a unique port (via port 0 binding or find-free-port)
27
+ - **AND** no port collisions SHALL occur
28
+
29
+ ### Requirement: Allowlist enforcement through real gateway
30
+
31
+ The system SHALL verify that allowlist mode works correctly through the real OpenClaw gateway.
32
+
33
+ #### Scenario: Allowed domain passes through
34
+
35
+ - **GIVEN** a gateway configured with mode `allowlist` and allowlist `["example.com"]`
36
+ - **WHEN** a tool call for `web_fetch` with URL `https://example.com/page` is sent to the gateway
37
+ - **THEN** the response SHALL indicate the request was allowed
38
+
39
+ #### Scenario: Unlisted domain is blocked
40
+
41
+ - **GIVEN** a gateway configured with mode `allowlist` and allowlist `["example.com"]`
42
+ - **WHEN** a tool call for `web_fetch` with URL `https://evil.com/payload` is sent
43
+ - **THEN** the response SHALL be a structured block response
44
+ - **AND** the `reason` field SHALL be `"not in allowlist"`
45
+ - **AND** the `policy` field SHALL be `"allowlist"`
46
+
47
+ #### Scenario: Wildcard allowlist matching
48
+
49
+ - **GIVEN** a gateway configured with allowlist `["*.example.com"]`
50
+ - **WHEN** a tool call for `https://api.example.com/data` is sent
51
+ - **THEN** the request SHALL be allowed
52
+ - **AND** a request for `https://example.com/data` (bare domain) SHALL be blocked
53
+
54
+ ### Requirement: Blocklist enforcement through real gateway
55
+
56
+ The system SHALL verify blocklist mode through the real gateway.
57
+
58
+ #### Scenario: Blocked domain is rejected
59
+
60
+ - **GIVEN** a gateway configured with mode `blocklist` and blocklist `["evil.com", "*.malware.org"]`
61
+ - **WHEN** a tool call for `https://evil.com/exploit` is sent
62
+ - **THEN** the response SHALL be a structured block response with reason `"in blocklist"`
63
+
64
+ #### Scenario: Unlisted domain passes in blocklist mode
65
+
66
+ - **GIVEN** a gateway configured with mode `blocklist` and blocklist `["evil.com"]`
67
+ - **WHEN** a tool call for `https://safe.com/page` is sent
68
+ - **THEN** the request SHALL be allowed
69
+
70
+ ### Requirement: Hybrid mode enforcement through real gateway
71
+
72
+ The system SHALL verify hybrid mode (allowlist + blocklist) through the real gateway.
73
+
74
+ #### Scenario: Allowed and not blocked passes
75
+
76
+ - **GIVEN** a gateway with mode `hybrid`, allowlist `["*.example.com"]`, blocklist `["blocked.example.com"]`
77
+ - **WHEN** a tool call for `https://api.example.com/data` is sent
78
+ - **THEN** the request SHALL be allowed
79
+
80
+ #### Scenario: Allowed but also blocked is rejected
81
+
82
+ - **GIVEN** the same hybrid config
83
+ - **WHEN** a tool call for `https://blocked.example.com/data` is sent
84
+ - **THEN** the response SHALL be a structured block response
85
+
86
+ #### Scenario: Not in allowlist is rejected in hybrid mode
87
+
88
+ - **GIVEN** the same hybrid config
89
+ - **WHEN** a tool call for `https://outside.com/page` is sent
90
+ - **THEN** the response SHALL be blocked with reason `"not in allowlist"`
91
+
92
+ ### Requirement: SSRF protection through real gateway
93
+
94
+ The system SHALL verify that private/internal IP addresses are blocked at the integration level.
95
+
96
+ #### Scenario: Localhost is blocked
97
+
98
+ - **GIVEN** a gateway configured with `blockPrivateIps: true`
99
+ - **WHEN** a tool call for `http://127.0.0.1/admin` is sent
100
+ - **THEN** the response SHALL be a structured block response with reason `"private or internal IP blocked"`
101
+
102
+ #### Scenario: Link-local address is blocked
103
+
104
+ - **GIVEN** the same config
105
+ - **WHEN** a tool call for `http://169.254.169.254/metadata` is sent (cloud metadata endpoint)
106
+ - **THEN** the response SHALL be blocked
107
+
108
+ #### Scenario: IPv6 loopback is blocked
109
+
110
+ - **GIVEN** the same config
111
+ - **WHEN** a tool call for `http://[::1]/admin` is sent
112
+ - **THEN** the response SHALL be blocked
113
+
114
+ ### Requirement: URLhaus blocking with fixture feed
115
+
116
+ The system SHALL verify URLhaus-based blocking using a deterministic fixture feed.
117
+
118
+ #### Scenario: Known malicious URL is blocked
119
+
120
+ - **GIVEN** a gateway configured with URLhaus enabled, pointing to `tests/integration/fixtures/urlhaus-sample.csv`
121
+ - **WHEN** a tool call for a URL present in the fixture feed is sent
122
+ - **THEN** the response SHALL be blocked with a reason indicating URLhaus match
123
+
124
+ #### Scenario: Clean URL passes URLhaus check
125
+
126
+ - **GIVEN** the same config
127
+ - **WHEN** a tool call for `https://clean-example.com/safe` (not in fixture) is sent
128
+ - **THEN** the request SHALL be allowed (assuming it passes other policy checks)
129
+
130
+ ### Requirement: Config validation error handling
131
+
132
+ The system SHALL produce clear errors when given malformed configuration.
133
+
134
+ #### Scenario: Missing required mode field
135
+
136
+ - **GIVEN** a config file with the plugin configured but `mode` field omitted
137
+ - **WHEN** the gateway starts
138
+ - **THEN** the gateway SHALL fail to start or the plugin SHALL report a clear validation error
139
+ - **AND** the error message SHALL mention the missing `mode` field
140
+
141
+ #### Scenario: Invalid mode value
142
+
143
+ - **GIVEN** a config with `mode: "invalid_mode"`
144
+ - **WHEN** the gateway starts
145
+ - **THEN** a clear validation error SHALL be reported
146
+
147
+ #### Scenario: Allowlist mode without allowlist array
148
+
149
+ - **GIVEN** a config with `mode: "allowlist"` but no `allowlist` field
150
+ - **WHEN** a tool call is made
151
+ - **THEN** the behavior SHALL be well-defined (either block all, or error clearly)
152
+
153
+ ### Requirement: Structured block response format
154
+
155
+ All blocked requests through the real gateway SHALL return a structured response.
156
+
157
+ #### Scenario: Block response contains required fields
158
+
159
+ - **WHEN** any request is blocked by the plugin
160
+ - **THEN** the response SHALL contain at minimum:
161
+ - `blocked`: `true`
162
+ - `reason`: a human-readable explanation (matching `getBlockReason()` output)
163
+ - `url`: the original requested URL
164
+
165
+ ### Requirement: Test gating and CI integration
166
+
167
+ The integration tests SHALL be gated and integrated into CI.
168
+
169
+ #### Scenario: Tests skip when not opted in
170
+
171
+ - **WHEN** `INTEGRATION_TESTS` env var is not set or not `"true"`
172
+ - **THEN** all integration tests SHALL be skipped gracefully (not fail)
173
+
174
+ #### Scenario: npm script available
175
+
176
+ - **WHEN** a developer runs `npm run test:integration`
177
+ - **THEN** integration tests SHALL execute with `INTEGRATION_TESTS=true` set automatically
178
+
179
+ #### Scenario: CI runs on main only
180
+
181
+ - **GIVEN** the CI workflow configuration
182
+ - **THEN** the integration test job SHALL only run on pushes to `main` (not on PRs)
183
+ - **AND** the job SHALL have a timeout of 5 minutes
184
+ - **AND** the job SHALL depend on unit and E2E test jobs passing first
185
+
186
+ ### Requirement: Documentation
187
+
188
+ #### Scenario: Testing docs exist
189
+
190
+ - **THEN** `docs/testing.md` SHALL document all test tiers (unit, E2E mock, E2E integration)
191
+ - **AND** explain how to run integration tests locally
192
+ - **AND** explain the env var gating mechanism
193
+
194
+ #### Scenario: README updated
195
+
196
+ - **THEN** the README testing section SHALL reference the integration test tier
197
+ - **AND** include the `npm run test:integration` command
@@ -0,0 +1,34 @@
1
+ # Tasks
2
+
3
+ ## Phase 1: Infrastructure
4
+
5
+ - [ ] Create `tests/integration/helpers/gateway.ts` — gateway lifecycle (start, stop, health check, PID tracking, cleanup)
6
+ - [ ] Create `tests/integration/helpers/config.ts` — temp config file generation from `UrlGuardConfig`
7
+ - [ ] Create `tests/integration/helpers/client.ts` — HTTP client for sending tool calls to gateway
8
+ - [ ] Create `tests/integration/fixtures/urlhaus-sample.csv` — static URLhaus feed snapshot (5-10 entries)
9
+ - [ ] Add `test:integration` script to `package.json` (sets `INTEGRATION_TESTS=true`, runs vitest on `tests/integration/`)
10
+
11
+ ## Phase 2: Core Test Suites
12
+
13
+ - [ ] Create `tests/integration/allowlist.test.ts` — allowlist enforcement (allowed, blocked, wildcard, bare domain)
14
+ - [ ] Create `tests/integration/blocklist.test.ts` — blocklist enforcement (blocked, unlisted passes)
15
+ - [ ] Create `tests/integration/hybrid.test.ts` — hybrid mode (allowed+not-blocked passes, allowed+blocked rejected, not-in-allowlist rejected)
16
+ - [ ] Create `tests/integration/ssrf.test.ts` — SSRF protection (localhost, link-local, IPv6 loopback, cloud metadata)
17
+ - [ ] Create `tests/integration/urlhaus.test.ts` — URLhaus fixture-based blocking
18
+
19
+ ## Phase 3: Error Handling & Validation
20
+
21
+ - [ ] Create `tests/integration/config-validation.test.ts` — malformed config errors (missing mode, invalid mode, missing allowlist)
22
+ - [ ] Create `tests/integration/error-handling.test.ts` — gateway error behavior, structured block response format validation
23
+
24
+ ## Phase 4: CI & Documentation
25
+
26
+ - [ ] Add `test-integration-real` job to `.forgejo/workflows/ci.yaml` (main-only, 5min timeout, depends on unit+e2e)
27
+ - [ ] Create `docs/testing.md` — document all test tiers, local setup, env var gating
28
+ - [ ] Update `README.md` testing section — reference integration tier, `npm run test:integration`
29
+
30
+ ## Dependencies
31
+
32
+ - Phase 2 depends on Phase 1 (helpers must exist)
33
+ - Phase 3 depends on Phase 1
34
+ - Phase 4 can start after Phase 1 (CI job) or after Phase 3 (docs)
@@ -0,0 +1,8 @@
1
+ schema: spec-driven
2
+
3
+ context: |
4
+ Tech stack: TypeScript, Vitest, Node.js 20
5
+ OpenClaw plugin for URL security (allowlist/blocklist/hybrid modes)
6
+ Uses URLhaus threat feed, trust scoring, SSRF protection
7
+ CI: Forgejo Actions on Codeberg (codeberg-tiny/codeberg-small runners)
8
+ Conventions: conventional commits, ESLint + Prettier, vitest for testing
@@ -0,0 +1,197 @@
1
+ # E2E Real Instance Integration Tests
2
+
3
+ ## ADDED Requirements
4
+
5
+ ### Requirement: Gateway lifecycle management
6
+
7
+ The system SHALL provide helper functions to start, health-check, and stop a real OpenClaw gateway instance for testing purposes.
8
+
9
+ #### Scenario: Start gateway with custom config
10
+
11
+ - **WHEN** a test suite starts with a given `UrlGuardConfig`
12
+ - **THEN** the helper SHALL generate a temp config file with the plugin configured
13
+ - **AND** spawn `openclaw gateway` as a subprocess on a dynamically assigned port
14
+ - **AND** wait for the health endpoint to respond (HTTP 200) with exponential backoff
15
+ - **AND** fail the test suite if the gateway does not become healthy within 30 seconds
16
+
17
+ #### Scenario: Stop gateway on suite teardown
18
+
19
+ - **WHEN** a test suite completes (pass or fail)
20
+ - **THEN** the helper SHALL kill the gateway subprocess and delete temp config files
21
+ - **AND** if the test runner crashes, a `process.on('exit')` handler SHALL kill any tracked gateway PIDs
22
+
23
+ #### Scenario: Port isolation
24
+
25
+ - **WHEN** multiple test suites run (sequentially or in parallel)
26
+ - **THEN** each suite SHALL use a unique port (via port 0 binding or find-free-port)
27
+ - **AND** no port collisions SHALL occur
28
+
29
+ ### Requirement: Allowlist enforcement through real gateway
30
+
31
+ The system SHALL verify that allowlist mode works correctly through the real OpenClaw gateway.
32
+
33
+ #### Scenario: Allowed domain passes through
34
+
35
+ - **GIVEN** a gateway configured with mode `allowlist` and allowlist `["example.com"]`
36
+ - **WHEN** a tool call for `web_fetch` with URL `https://example.com/page` is sent to the gateway
37
+ - **THEN** the response SHALL indicate the request was allowed
38
+
39
+ #### Scenario: Unlisted domain is blocked
40
+
41
+ - **GIVEN** a gateway configured with mode `allowlist` and allowlist `["example.com"]`
42
+ - **WHEN** a tool call for `web_fetch` with URL `https://evil.com/payload` is sent
43
+ - **THEN** the response SHALL be a structured block response
44
+ - **AND** the `reason` field SHALL be `"not in allowlist"`
45
+ - **AND** the `policy` field SHALL be `"allowlist"`
46
+
47
+ #### Scenario: Wildcard allowlist matching
48
+
49
+ - **GIVEN** a gateway configured with allowlist `["*.example.com"]`
50
+ - **WHEN** a tool call for `https://api.example.com/data` is sent
51
+ - **THEN** the request SHALL be allowed
52
+ - **AND** a request for `https://example.com/data` (bare domain) SHALL be blocked
53
+
54
+ ### Requirement: Blocklist enforcement through real gateway
55
+
56
+ The system SHALL verify blocklist mode through the real gateway.
57
+
58
+ #### Scenario: Blocked domain is rejected
59
+
60
+ - **GIVEN** a gateway configured with mode `blocklist` and blocklist `["evil.com", "*.malware.org"]`
61
+ - **WHEN** a tool call for `https://evil.com/exploit` is sent
62
+ - **THEN** the response SHALL be a structured block response with reason `"in blocklist"`
63
+
64
+ #### Scenario: Unlisted domain passes in blocklist mode
65
+
66
+ - **GIVEN** a gateway configured with mode `blocklist` and blocklist `["evil.com"]`
67
+ - **WHEN** a tool call for `https://safe.com/page` is sent
68
+ - **THEN** the request SHALL be allowed
69
+
70
+ ### Requirement: Hybrid mode enforcement through real gateway
71
+
72
+ The system SHALL verify hybrid mode (allowlist + blocklist) through the real gateway.
73
+
74
+ #### Scenario: Allowed and not blocked passes
75
+
76
+ - **GIVEN** a gateway with mode `hybrid`, allowlist `["*.example.com"]`, blocklist `["blocked.example.com"]`
77
+ - **WHEN** a tool call for `https://api.example.com/data` is sent
78
+ - **THEN** the request SHALL be allowed
79
+
80
+ #### Scenario: Allowed but also blocked is rejected
81
+
82
+ - **GIVEN** the same hybrid config
83
+ - **WHEN** a tool call for `https://blocked.example.com/data` is sent
84
+ - **THEN** the response SHALL be a structured block response
85
+
86
+ #### Scenario: Not in allowlist is rejected in hybrid mode
87
+
88
+ - **GIVEN** the same hybrid config
89
+ - **WHEN** a tool call for `https://outside.com/page` is sent
90
+ - **THEN** the response SHALL be blocked with reason `"not in allowlist"`
91
+
92
+ ### Requirement: SSRF protection through real gateway
93
+
94
+ The system SHALL verify that private/internal IP addresses are blocked at the integration level.
95
+
96
+ #### Scenario: Localhost is blocked
97
+
98
+ - **GIVEN** a gateway configured with `blockPrivateIps: true`
99
+ - **WHEN** a tool call for `http://127.0.0.1/admin` is sent
100
+ - **THEN** the response SHALL be a structured block response with reason `"private or internal IP blocked"`
101
+
102
+ #### Scenario: Link-local address is blocked
103
+
104
+ - **GIVEN** the same config
105
+ - **WHEN** a tool call for `http://169.254.169.254/metadata` is sent (cloud metadata endpoint)
106
+ - **THEN** the response SHALL be blocked
107
+
108
+ #### Scenario: IPv6 loopback is blocked
109
+
110
+ - **GIVEN** the same config
111
+ - **WHEN** a tool call for `http://[::1]/admin` is sent
112
+ - **THEN** the response SHALL be blocked
113
+
114
+ ### Requirement: URLhaus blocking with fixture feed
115
+
116
+ The system SHALL verify URLhaus-based blocking using a deterministic fixture feed.
117
+
118
+ #### Scenario: Known malicious URL is blocked
119
+
120
+ - **GIVEN** a gateway configured with URLhaus enabled, pointing to `tests/integration/fixtures/urlhaus-sample.csv`
121
+ - **WHEN** a tool call for a URL present in the fixture feed is sent
122
+ - **THEN** the response SHALL be blocked with a reason indicating URLhaus match
123
+
124
+ #### Scenario: Clean URL passes URLhaus check
125
+
126
+ - **GIVEN** the same config
127
+ - **WHEN** a tool call for `https://clean-example.com/safe` (not in fixture) is sent
128
+ - **THEN** the request SHALL be allowed (assuming it passes other policy checks)
129
+
130
+ ### Requirement: Config validation error handling
131
+
132
+ The system SHALL produce clear errors when given malformed configuration.
133
+
134
+ #### Scenario: Missing required mode field
135
+
136
+ - **GIVEN** a config file with the plugin configured but `mode` field omitted
137
+ - **WHEN** the gateway starts
138
+ - **THEN** the gateway SHALL fail to start or the plugin SHALL report a clear validation error
139
+ - **AND** the error message SHALL mention the missing `mode` field
140
+
141
+ #### Scenario: Invalid mode value
142
+
143
+ - **GIVEN** a config with `mode: "invalid_mode"`
144
+ - **WHEN** the gateway starts
145
+ - **THEN** a clear validation error SHALL be reported
146
+
147
+ #### Scenario: Allowlist mode without allowlist array
148
+
149
+ - **GIVEN** a config with `mode: "allowlist"` but no `allowlist` field
150
+ - **WHEN** a tool call is made
151
+ - **THEN** the behavior SHALL be well-defined (either block all, or error clearly)
152
+
153
+ ### Requirement: Structured block response format
154
+
155
+ All blocked requests through the real gateway SHALL return a structured response.
156
+
157
+ #### Scenario: Block response contains required fields
158
+
159
+ - **WHEN** any request is blocked by the plugin
160
+ - **THEN** the response SHALL contain at minimum:
161
+ - `blocked`: `true`
162
+ - `reason`: a human-readable explanation (matching `getBlockReason()` output)
163
+ - `url`: the original requested URL
164
+
165
+ ### Requirement: Test gating and CI integration
166
+
167
+ The integration tests SHALL be gated and integrated into CI.
168
+
169
+ #### Scenario: Tests skip when not opted in
170
+
171
+ - **WHEN** `INTEGRATION_TESTS` env var is not set or not `"true"`
172
+ - **THEN** all integration tests SHALL be skipped gracefully (not fail)
173
+
174
+ #### Scenario: npm script available
175
+
176
+ - **WHEN** a developer runs `npm run test:integration`
177
+ - **THEN** integration tests SHALL execute with `INTEGRATION_TESTS=true` set automatically
178
+
179
+ #### Scenario: CI runs on main only
180
+
181
+ - **GIVEN** the CI workflow configuration
182
+ - **THEN** the integration test job SHALL only run on pushes to `main` (not on PRs)
183
+ - **AND** the job SHALL have a timeout of 5 minutes
184
+ - **AND** the job SHALL depend on unit and E2E test jobs passing first
185
+
186
+ ### Requirement: Documentation
187
+
188
+ #### Scenario: Testing docs exist
189
+
190
+ - **THEN** `docs/testing.md` SHALL document all test tiers (unit, E2E mock, E2E integration)
191
+ - **AND** explain how to run integration tests locally
192
+ - **AND** explain the env var gating mechanism
193
+
194
+ #### Scenario: README updated
195
+
196
+ - **THEN** the README testing section SHALL reference the integration test tier
197
+ - **AND** include the `npm run test:integration` command
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@llnvd/openclaw-url-guard",
3
+ "version": "0.0.1",
4
+ "description": "OpenClaw plugin for URL allowlisting/blocklisting in web_fetch and web_search tools",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc -p tsconfig.json",
9
+ "test": "vitest run",
10
+ "test:e2e": "vitest run tests/e2e/",
11
+ "test:integration": "INTEGRATION_TESTS=true vitest run tests/integration/",
12
+ "lint": "oxlint src/ tests/",
13
+ "lint:fix": "oxlint --fix src/ tests/",
14
+ "format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts'",
15
+ "format:check": "prettier --check 'src/**/*.ts' 'tests/**/*.ts'",
16
+ "check": "npm run lint && npm run format:check && npm run test",
17
+ "prepare": "husky"
18
+ },
19
+ "lint-staged": {
20
+ "*.ts": [
21
+ "oxlint --fix",
22
+ "prettier --write"
23
+ ]
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^25.3.5",
27
+ "husky": "^9.1.7",
28
+ "lint-staged": "^15.4.3",
29
+ "oxlint": "^1.51.0",
30
+ "prettier": "^3.5.3",
31
+ "typescript": "^5.6.3",
32
+ "vitest": "^2.1.8"
33
+ },
34
+ "openclaw": {
35
+ "extensions": [
36
+ "./dist/src/index.js"
37
+ ]
38
+ }
39
+ }