Problem Statement
Users need fine-grained control over which query parameters are allowed through L7 policy rules. The current implementation (after PR #614) strips query strings from path matching, which fixes exact-path rules but means query parameters pass through unconditionally. For APIs like ClawHub where the skill slug and version are specified as query parameters (/api/v1/download?slug=my-skill&version=1.0), users cannot restrict which parameter values are permitted.
Refs: #607
Technical Context
PR #614 implemented Part 1 of the fix: stripping query strings from path matching so that path: /api/v1/download correctly matches requests with any query parameters. The infrastructure for Part 2 is already in place:
- The
query field is now captured in L7Request and L7RequestInfo structs
- The query string is passed to Rego in
input.request.query
- The query string is logged in the
l7_query field
What's missing is the policy schema support and Rego matching logic to filter on specific query parameter key/value patterns.
Affected Components
| Component |
Key Files |
Role |
| Policy schema (proto) |
proto/sandbox.proto |
Defines the L7Allow message structure |
| Policy schema (YAML) |
crates/openshell-policy/src/lib.rs |
Defines L7AllowDef serde struct |
| HTTP parser |
crates/openshell-sandbox/src/l7/rest.rs |
Parses query string into key/value map |
| L7 relay |
crates/openshell-sandbox/src/l7/relay.rs |
Constructs Rego input with query params |
| Rego policy |
crates/openshell-sandbox/data/sandbox-policy.rego |
Evaluates query param matching rules |
| L7 validation |
crates/openshell-sandbox/src/l7/mod.rs |
Validates query patterns, expands presets |
| OPA engine |
crates/openshell-sandbox/src/opa.rs |
Serializes rules to Rego data format |
Technical Investigation
Architecture Overview
L7 policy evaluation follows a two-phase model:
- L4 phase: CONNECT-level check validates host:port + binary identity
- L7 phase: Per-request check validates HTTP method + path (+ query params with this feature)
The Rego input for L7 evaluation currently includes:
{
"network": { "host": "...", "port": ... },
"exec": { "path": "...", "ancestors": [...], "cmdline_paths": [...] },
"request": { "method": "...", "path": "...", "query": "..." }
}
The query field is the raw query string (e.g., slug=my-skill&version=1.0). For structured matching, this needs to be parsed into a key/value map.
Code References
| Location |
Description |
proto/sandbox.proto:96-103 |
L7Allow message - add map<string, string> query = 4; |
crates/openshell-policy/src/lib.rs:114-123 |
L7AllowDef struct - add query: HashMap<String, String> |
crates/openshell-policy/src/lib.rs:171-180 |
to_proto() - map query field |
crates/openshell-policy/src/lib.rs:271-280 |
from_proto() - map query field |
crates/openshell-sandbox/src/l7/rest.rs:119-140 |
HTTP parser - add parse_query_params() helper |
crates/openshell-sandbox/src/l7/provider.rs:28-42 |
L7Request - add query_params: HashMap<String, String> |
crates/openshell-sandbox/src/l7/mod.rs:72-80 |
L7RequestInfo - add query_params: HashMap<String, String> |
crates/openshell-sandbox/src/l7/relay.rs:188-203 |
Rego input - add query_params to input.request |
crates/openshell-sandbox/data/sandbox-policy.rego:205-211 |
Add query_params_match() rule |
crates/openshell-sandbox/src/l7/mod.rs:339-371 |
Preset expansion - decide default query behavior |
crates/openshell-sandbox/src/l7/mod.rs:277-296 |
Validation - validate query patterns |
crates/openshell-sandbox/src/opa.rs:664-679 |
proto_to_opa_data_json - include query in rule serialization |
Current Behavior
- HTTP parser extracts
query as raw string from request URI
- Query string is passed to Rego as
input.request.query (string)
- Rego only evaluates
method_matches and path_matches - no query matching
- All query parameters pass through if path and method match
What Would Need to Change
Policy YAML schema - Add optional query field to rule definitions:
rules:
- allow:
method: GET
path: /api/v1/skills/download
query:
slug: "allowed-skill-*"
version: "1.*"
HTTP parser - Parse query string into HashMap<String, String>:
- Handle URL decoding (
%20 → space, %2F → /)
- Handle duplicate keys (last-wins or comma-join)
- Handle empty values (
?foo= → {"foo": ""})
- Handle valueless keys (
?foo → {"foo": ""})
Rego evaluation - Add query_params_match(request, rule):
- If
rule.allow.query is empty/absent, match any query params
- For each key in
rule.allow.query, check:
- Key exists in
request.query_params
- Value matches
glob.match(pattern, [], value) (no delimiter for values)
- All specified keys must match; extra params in request are allowed
Preset expansion - Access presets (read-only, read-write, full) should not include query restrictions (empty = match all).
Alternative Approaches Considered
-
Glob on raw query string: Simple but fragile - ?a=1&b=2 and ?b=2&a=1 are semantically identical but wouldn't match the same pattern. Query parameter order is not guaranteed.
-
Regex matching: More powerful but harder to validate and potentially dangerous (ReDoS). Glob patterns are sufficient for most use cases.
-
Exact value matching only: Simpler but less flexible - can't express "any version starting with 1.*".
Recommendation: Structured key/value matching with glob patterns on values (approach in this spike).
Patterns to Follow
- Existing
method_matches and path_matches Rego helpers use glob matching
- Validation follows the pattern in
validate_l7_policies() - accumulate warnings/errors
- Preset expansion uses
rule_json() helper to construct rule objects
Proposed Approach
Parse query strings into structured key/value maps in Rust, pass to Rego as input.request.query_params, and add an optional query field to L7AllowDef that specifies key → glob-pattern mappings. The Rego request_allowed_for_endpoint rule gains a query_params_match check that requires all specified keys to be present with matching values. Unspecified keys in the request are allowed through (additive matching, not restrictive).
Scope Assessment
- Complexity: Medium - multiple files across proto, policy, sandbox, and Rego layers
- Confidence: High - clear path forward, infrastructure already in place from Part 1
- Estimated files to change: 12
- Issue type:
feat
Risks & Open Questions
-
Duplicate query keys: If URL has ?tag=a&tag=b, what does query_params["tag"] contain?
- Option A: Last-wins (
"b")
- Option B: Comma-join (
"a,b")
- Recommendation: Comma-join, matching HTTP convention for multi-value headers
-
URL decoding: Should %XX sequences be decoded before matching?
- Recommendation: Yes, decode using
percent_encoding::percent_decode_str to prevent trivial bypasses like %2F for /
-
Case sensitivity: Query param keys and values are case-sensitive per RFC 3986
- Recommendation: Keep as-is, no case normalization
-
Backwards compatibility: Existing policies without query field should continue to work
- Recommendation: Empty/absent
query means "match any query params" (current behavior)
Test Considerations
- Unit tests for
parse_query_params(): URL decoding, duplicate keys, empty values, valueless keys
- Rego tests for
query_params_match(): exact match, glob match, missing key denied, extra params allowed
- OPA engine integration tests: full policy evaluation with query param rules
- Smoke test with httpbin.org or similar public API
Existing test patterns in crates/openshell-sandbox/src/opa.rs (l7_* tests) and crates/openshell-sandbox/src/l7/rest.rs should be followed.
Created by spike investigation. Use build-from-issue to plan and implement.
Problem Statement
Users need fine-grained control over which query parameters are allowed through L7 policy rules. The current implementation (after PR #614) strips query strings from path matching, which fixes exact-path rules but means query parameters pass through unconditionally. For APIs like ClawHub where the skill slug and version are specified as query parameters (
/api/v1/download?slug=my-skill&version=1.0), users cannot restrict which parameter values are permitted.Refs: #607
Technical Context
PR #614 implemented Part 1 of the fix: stripping query strings from path matching so that
path: /api/v1/downloadcorrectly matches requests with any query parameters. The infrastructure for Part 2 is already in place:queryfield is now captured inL7RequestandL7RequestInfostructsinput.request.queryl7_queryfieldWhat's missing is the policy schema support and Rego matching logic to filter on specific query parameter key/value patterns.
Affected Components
proto/sandbox.protoL7Allowmessage structurecrates/openshell-policy/src/lib.rsL7AllowDefserde structcrates/openshell-sandbox/src/l7/rest.rscrates/openshell-sandbox/src/l7/relay.rscrates/openshell-sandbox/data/sandbox-policy.regocrates/openshell-sandbox/src/l7/mod.rscrates/openshell-sandbox/src/opa.rsTechnical Investigation
Architecture Overview
L7 policy evaluation follows a two-phase model:
The Rego input for L7 evaluation currently includes:
{ "network": { "host": "...", "port": ... }, "exec": { "path": "...", "ancestors": [...], "cmdline_paths": [...] }, "request": { "method": "...", "path": "...", "query": "..." } }The
queryfield is the raw query string (e.g.,slug=my-skill&version=1.0). For structured matching, this needs to be parsed into a key/value map.Code References
proto/sandbox.proto:96-103L7Allowmessage - addmap<string, string> query = 4;crates/openshell-policy/src/lib.rs:114-123L7AllowDefstruct - addquery: HashMap<String, String>crates/openshell-policy/src/lib.rs:171-180to_proto()- map query fieldcrates/openshell-policy/src/lib.rs:271-280from_proto()- map query fieldcrates/openshell-sandbox/src/l7/rest.rs:119-140parse_query_params()helpercrates/openshell-sandbox/src/l7/provider.rs:28-42L7Request- addquery_params: HashMap<String, String>crates/openshell-sandbox/src/l7/mod.rs:72-80L7RequestInfo- addquery_params: HashMap<String, String>crates/openshell-sandbox/src/l7/relay.rs:188-203query_paramstoinput.requestcrates/openshell-sandbox/data/sandbox-policy.rego:205-211query_params_match()rulecrates/openshell-sandbox/src/l7/mod.rs:339-371crates/openshell-sandbox/src/l7/mod.rs:277-296crates/openshell-sandbox/src/opa.rs:664-679proto_to_opa_data_json- include query in rule serializationCurrent Behavior
queryas raw string from request URIinput.request.query(string)method_matchesandpath_matches- no query matchingWhat Would Need to Change
Policy YAML schema - Add optional
queryfield to rule definitions:HTTP parser - Parse query string into
HashMap<String, String>:%20→ space,%2F→/)?foo=→{"foo": ""})?foo→{"foo": ""})Rego evaluation - Add
query_params_match(request, rule):rule.allow.queryis empty/absent, match any query paramsrule.allow.query, check:request.query_paramsglob.match(pattern, [], value)(no delimiter for values)Preset expansion - Access presets (
read-only,read-write,full) should not include query restrictions (empty = match all).Alternative Approaches Considered
Glob on raw query string: Simple but fragile -
?a=1&b=2and?b=2&a=1are semantically identical but wouldn't match the same pattern. Query parameter order is not guaranteed.Regex matching: More powerful but harder to validate and potentially dangerous (ReDoS). Glob patterns are sufficient for most use cases.
Exact value matching only: Simpler but less flexible - can't express "any version starting with 1.*".
Recommendation: Structured key/value matching with glob patterns on values (approach in this spike).
Patterns to Follow
method_matchesandpath_matchesRego helpers use glob matchingvalidate_l7_policies()- accumulate warnings/errorsrule_json()helper to construct rule objectsProposed Approach
Parse query strings into structured key/value maps in Rust, pass to Rego as
input.request.query_params, and add an optionalqueryfield toL7AllowDefthat specifies key → glob-pattern mappings. The Regorequest_allowed_for_endpointrule gains aquery_params_matchcheck that requires all specified keys to be present with matching values. Unspecified keys in the request are allowed through (additive matching, not restrictive).Scope Assessment
featRisks & Open Questions
Duplicate query keys: If URL has
?tag=a&tag=b, what doesquery_params["tag"]contain?"b")"a,b")URL decoding: Should
%XXsequences be decoded before matching?percent_encoding::percent_decode_strto prevent trivial bypasses like%2Ffor/Case sensitivity: Query param keys and values are case-sensitive per RFC 3986
Backwards compatibility: Existing policies without
queryfield should continue to workquerymeans "match any query params" (current behavior)Test Considerations
parse_query_params(): URL decoding, duplicate keys, empty values, valueless keysquery_params_match(): exact match, glob match, missing key denied, extra params allowedExisting test patterns in
crates/openshell-sandbox/src/opa.rs(l7_*tests) andcrates/openshell-sandbox/src/l7/rest.rsshould be followed.Created by spike investigation. Use
build-from-issueto plan and implement.