Skip to content

toolgate: more value checks, list elements and nested arguments - #65

Open
cinar wants to merge 3 commits into
open-experiments:mainfrom
cinar:toolgate-value-kinds
Open

cinar wants to merge 3 commits into
open-experiments:mainfrom
cinar:toolgate-value-kinds

Conversation

@cinar

@cinar cinar commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

Depends on #64. Please review and merge #64 first. This branch is stacked on it, so until #64 merges this PR also shows #64's two commits. Only the last commit, 785de45, is new here.

Extends aex-toolgate's argument-value rules in the same style: declarative, deterministic, fail closed, one record per call. The existing kinds' checks and messages are unchanged, and all existing tests pass.

New rule kinds

kind checks example
floor numeric minimum (min) quantity ≥ 1
denylist denied values, compared without regard to case never pay NEW-ACCT-9911, nor new-acct-9911
pattern an RE2 expression the whole value must match (RE2 can't be driven into exponential time) IDs look like ACME-ACCT-\d{3}
domain the host of an email address, a URL or a bare host name is in allowed or a subdomain of one cfo@eu.corp.example passes; cfo@evilcorp.example and https://corp.example@evil.example don't
max_items at most max items in a list (a lone value counts as one) at most 3 recipients
compare arg op match_arg must hold: lt/le/gt/ge on numbers, eq/ne on numbers or plain values a refund may not exceed the original charge

domain parses the value rather than matching a suffix, so URL userinfo tricks resolve to the real host. Several @ signs or a non-ASCII (homoglyph) host fail closed.

Lists and nested arguments

  • each: true applies a single-value rule to every element of a list argument, and to a lone value as a list of one. Without it a list still fails closed, as today. Allowed on ceiling, floor, allowlist, denylist, suffix, prefix, pattern, domain and sensitive_field.
  • Paths: arg and match_arg may be paths. payment.amount reads a field of an object argument; items[*].amount reads the field of every element of a list, each of which is checked. An argument literally named with a dot is found by its exact name first. A path missing from the call, or running into a value of the wrong shape, fails closed like an absent argument. Lookup rules resolve paths too.

How

Rule.evaluate is split into argument resolution (resolveArg: plain argument, path, or [*] path) and a per-value checkValue. The existing switch moved into checkValue verbatim, changing only its return shape, so every existing kind decides and explains exactly as before.

Validation

The policy is refused when:

  • floor has no min;
  • denylist has an empty list;
  • pattern is missing or doesn't compile;
  • a domain entry isn't a bare lower-case ASCII domain (e.g. @corp.example, Corp.example);
  • max_items has max below 1;
  • compare has a bad op or no match_arg, or uses [*] on either side;
  • each is set on a kind that doesn't check single values;
  • a path is malformed (a..b, a[*]b).

Tests

values_test.go covers:

  • each new kind, including its fail-closed cases (quoted numbers, padding, several @, a homoglyph host, a URL without a host);
  • each over lists, including the unchanged behaviour without it;
  • nested and [*] paths, and an argument literally named with a dot;
  • compare on flat and nested arguments;
  • every validation error.

Everything passes under -race (the pattern cache is shared across requests).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ptdu2pZMVdE5x6XkwcUDjj

cinar and others added 3 commits September 30, 2026 12:44
Decide-only: Gate.DecideOnly and POST /v1/decide/{tool} rule on a call and
record it without forwarding or holding it. The artifact's outcome says the
gate did not act, and only toolcall.requested and toolcall.decided are
published. For callers that execute tools themselves (a benchmark harness)
and for measuring a policy in shadow before it enforces. Operator-only: an
agent that can ask what would pass can search the policy for values that
slip through.

Tool tags: tool_tags labels tools with classes, and a rule can name a tag
instead of a tool, so one rule governs every tool carrying it. New rule kind
"tool" fires on the action itself (e.g. every destructive tool escalates);
it takes no arg and needs a tool or a tag. Validation refuses tags on tools
missing from tool_scopes, rules on tags no tool carries, rules setting both
tool and tag, and unscoped tool rules.

Existing behaviour is unchanged; all existing tests pass. New tests: the
twelve calls through /v1/decide (same decisions, nothing forwarded, no hold,
chain verifies), the agent refused at /v1/decide, tag scoping, tag
validation, and a tool rule by name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ptdu2pZMVdE5x6XkwcUDjj
CVE-2026-84445 (HIGH, denial of service via malformed RPC requests) was
published for grpc v1.83.1 after main's last security scan, so Trivy now
fails for every service that links it. v1.83.2 has the fix. It is an
indirect dependency in all fourteen modules that require it; bumping it
raises golang.org/x/net to v0.58.0 and, in five modules, golang.org/x/text
to v0.41.0 and golang.org/x/sys to v0.47.0, the minimums grpc v1.83.2
requires. Every module builds, vets and passes its tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ptdu2pZMVdE5x6XkwcUDjj
New rule kinds, each deterministic and failing closed when a value cannot be
checked:
- floor: numeric minimum (min).
- denylist: denied values, compared without regard to case.
- pattern: an RE2 expression the whole value must match.
- domain: the host of an email address, URL or bare host name must be an
  allowed domain or a subdomain of one; parsed, so URL userinfo tricks read
  as the real host; several @ or a non-ASCII host fail closed.
- max_items: at most max items in a list (a lone value counts as one).
- compare: arg op match_arg must hold (lt, le, gt, ge on numbers; eq, ne on
  numbers or plain values).

Lists and nesting: each: true applies a single-value rule to every element
of a list (without it a list still fails closed, as before), and arg and
match_arg may be paths, "payment.amount" or "items[*].amount"; a missing
path or a value of the wrong shape fails closed like an absent argument.

evaluate is split into argument resolution and a per-value checkValue; the
existing kinds' checks and messages are unchanged and all existing tests
pass. New tests cover each kind, lists, paths, compare and validation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ptdu2pZMVdE5x6XkwcUDjj

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant