From e4751a70e6b87e2175fb125f31da84fd2fd5f96f Mon Sep 17 00:00:00 2001 From: Onur Cinar Date: Wed, 30 Sep 2026 12:44:55 -0700 Subject: [PATCH 1/3] toolgate: decide-only endpoint and tool tags 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 Claude-Session: https://claude.ai/code/session_01Ptdu2pZMVdE5x6XkwcUDjj --- docs/TOOLGATE.md | 5 +- .../internal/httpapi/decide_test.go | 75 +++++++++++++++ src/aex-toolgate/internal/httpapi/router.go | 84 ++++++++++++---- src/internal/toolgate/gate.go | 24 +++++ src/internal/toolgate/policy.go | 70 +++++++++++++- src/internal/toolgate/tags_test.go | 95 +++++++++++++++++++ 6 files changed, 327 insertions(+), 26 deletions(-) create mode 100644 src/aex-toolgate/internal/httpapi/decide_test.go create mode 100644 src/internal/toolgate/tags_test.go diff --git a/docs/TOOLGATE.md b/docs/TOOLGATE.md index 085eb44..b18d788 100644 --- a/docs/TOOLGATE.md +++ b/docs/TOOLGATE.md @@ -23,7 +23,9 @@ The study behind the component compared three configurations over the same twelv | `["*"]` | passes the gateway; the policy's own grant then applies unchanged | 2. **Values that cannot be checked are refused.** A rule whose argument is absent, is not a number where a number is required, or carries more than one value in one argument (`"a@evil.example,b@corp.example"`) fails closed rather than being skipped. Such a decision reports `cannot verify: ...` rather than the rule's own message, so an approver is not told a bank account changed when the call never mentioned one. -2. **Rules.** In `ModeFull`, argument-value rules are evaluated in policy order and the first that fires decides: `ceiling` (numeric maximum), `allowlist`, `lookup` (the value must match what a table maps another argument to, such as the vendor account on file for this invoice), `sensitive_field` (escalate to a person), `suffix` (email domains), `prefix` (storage destinations). A rule's `effect` is `deny` or `escalate`. +2. **Rules.** In `ModeFull`, argument-value rules are evaluated in policy order and the first that fires decides: `ceiling` (numeric maximum), `allowlist`, `lookup` (the value must match what a table maps another argument to, such as the vendor account on file for this invoice), `sensitive_field` (escalate to a person), `suffix` (email domains), `prefix` (storage destinations), `tool` (the action itself, whatever the arguments). A rule's `effect` is `deny` or `escalate`. + + **Tool tags.** A policy can label tools with classes in `tool_tags` (`{"delete_file": ["destructive"], "payout": ["money"]}`), and a rule can name a `tag` instead of a `tool`, so one rule governs every tool carrying the tag: `{"id": "T1", "tag": "destructive", "kind": "tool", "effect": "escalate"}` holds every destructive action for a person, and `{"id": "T2", "tag": "money", "kind": "ceiling", "arg": "amount", "max": 100}` caps every money tool. A rule sets `tool` or `tag`, not both. Validation refuses a tag on a tool missing from `tool_scopes`, a rule on a tag no tool carries, and a `tool` rule with an `arg` or with neither a tool nor a tag, since each would leave a tool silently ungoverned or a rule firing everywhere. Argument rules scoped by tag keep the fail-closed rule for absent arguments: a tagged tool must supply the argument its tag's rule checks. 3. **Execution.** Allowed calls run through the caller's executor; refused calls do not run; escalated calls are held until `Resolve` records a human decision under the approver's identity. 4. **Artifact.** One record per decision, chained to the previous record for the provider. 5. **Events.** `toolcall.requested`, `toolcall.decided`, then `toolcall.executed`, `toolcall.refused` or `toolcall.escalated`; a resolved hold adds `toolcall.approved` (or `toolcall.refused`). The AEX `events.Publisher` satisfies the gate's `Publisher` interface, so records land in JetStream with the standard envelope. @@ -78,6 +80,7 @@ agent -> aex-gateway -> aex-toolgate -> provider tools | Endpoint | What it does | |---|---| | `POST /v1/tools/{tool}` | body: the argument values as a JSON object. The gate decides; an allowed call is forwarded to `UPSTREAM_URL` + `UPSTREAM_PREFIX` + `/{tool}` and the tool's answer is returned with `X-Toolgate-Decision`, `X-Toolgate-Rule` and `X-Toolgate-Hash`. A refused call answers 403 with the rule and the message; a held call answers 202 with the hold hash. | +| `POST /v1/decide/{tool}` | **operator only.** Same body and identity headers as `/v1/tools/{tool}`. The gate decides and records, but forwards nothing and holds nothing; the artifact's outcome is `decided only: not executed by the gate`, and only `toolcall.requested` and `toolcall.decided` are published. Answers 200 with `decision`, `rule`, `scope`, `approval`, `message`, `outcome`, `hash`, `call_id` (and the `X-Toolgate-*` headers). For callers that execute tools themselves, such as a benchmark harness, or for measuring a policy in shadow before it enforces. Operator-only because a gated agent that can ask what would pass, without consequence, can search the policy for values that slip through. | | `POST /v1/holds/{hash}` | **operator only.** Body `{"approver": "...", "approved": true}`. Settles a held call: runs the tool when approved, appends the human decision to the chain, publishes `toolcall.approved` or `toolcall.refused`. | | `GET /v1/records`, `GET /v1/records/verify` | **operator only.** The provider's chain and its verification. | | `GET /health`, `GET /ready` | probes | diff --git a/src/aex-toolgate/internal/httpapi/decide_test.go b/src/aex-toolgate/internal/httpapi/decide_test.go new file mode 100644 index 0000000..90eb4f7 --- /dev/null +++ b/src/aex-toolgate/internal/httpapi/decide_test.go @@ -0,0 +1,75 @@ +package httpapi + +import ( + "encoding/json" + "net/http" + "testing" + + "github.com/parlakisik/agent-exchange/internal/toolgate" +) + +// The twelve calls through /v1/decide reach the same decisions as through +// /v1/tools, but nothing is forwarded, nothing is held, and every record says +// the gate did not act. +func TestDecideOnlyRulesWithoutExecuting(t *testing.T) { + srv, upstream, calls, executed := setup(t) + defer srv.Close() + defer upstream.Close() + + want := map[string]string{ + "C01": "allow", "C02": "allow", "C03": "allow", "C04": "allow", "C05": "deny", "C06": "deny", + "C07": "escalate", "C08": "allow", "C09": "deny", "C10": "deny", "C11": "deny", "C12": "deny", + } + var heldHash string + for _, c := range calls { + resp, body := post(t, srv.URL+"/v1/decide/"+c.Tool, c.Args, + map[string]string{"Authorization": "Bearer " + testOperatorToken, "X-AEX-Call-ID": c.ID}) + if resp.StatusCode != http.StatusOK { + t.Fatalf("%s: got %d, want 200 whatever the decision", c.ID, resp.StatusCode) + } + if got := body["decision"]; got != want[c.ID] || resp.Header.Get("X-Toolgate-Decision") != want[c.ID] { + t.Errorf("%s: decision %v (header %q), want %s", c.ID, got, resp.Header.Get("X-Toolgate-Decision"), want[c.ID]) + } + if body["outcome"] != toolgate.OutcomeDecideOnly { + t.Errorf("%s: outcome %v, want the decide-only outcome", c.ID, body["outcome"]) + } + if c.ID == "C07" { + heldHash, _ = body["hash"].(string) + } + } + if len(*executed) != 0 { + t.Errorf("decide-only forwarded calls to the tool: %v", *executed) + } + + // The escalated call left no hold behind to settle. + resp, _ := post(t, srv.URL+"/v1/holds/"+heldHash, map[string]any{"approver": "user:x", "approved": true}, operatorHeaders()) + if resp.StatusCode == http.StatusOK { + t.Error("a decide-only escalation must not create a hold") + } + + _, raw := getAs(t, srv.URL+"/v1/records/verify", operatorHeaders()) + var v map[string]any + _ = json.Unmarshal(raw, &v) + if v["ok"] != true || v["records"] != float64(12) { + t.Errorf("chain: %v, want ok with 12 records", v) + } +} + +// Decide-only is an operator endpoint: the gated agent must not be able to ask +// the gate what would pass. +func TestDecideOnlyRefusesTheGatedAgent(t *testing.T) { + srv, upstream, _, _ := setup(t) + defer srv.Close() + defer upstream.Close() + + resp, _ := post(t, srv.URL+"/v1/decide/send_payment", map[string]any{"invoice_id": "INV-1042", "amount": 1.0}, nil) + if resp.StatusCode != http.StatusUnauthorized { + t.Errorf("no credential: got %d, want 401", resp.StatusCode) + } + _, raw := getAs(t, srv.URL+"/v1/records/verify", operatorHeaders()) + var v map[string]any + _ = json.Unmarshal(raw, &v) + if v["records"] != float64(0) { + t.Errorf("a refused decide request must leave no record, got %v", v["records"]) + } +} diff --git a/src/aex-toolgate/internal/httpapi/router.go b/src/aex-toolgate/internal/httpapi/router.go index 90d5f95..0dff5e4 100644 --- a/src/aex-toolgate/internal/httpapi/router.go +++ b/src/aex-toolgate/internal/httpapi/router.go @@ -24,8 +24,13 @@ // Those headers are attribution, not authentication: they are whatever the // caller sent, and none of them widens an authorization decision. // -// The operator endpoints (POST /v1/holds/{hash}, GET /v1/records and -// /v1/records/verify) are outside that model. They belong to the provider's +// POST /v1/decide/{tool} takes the same body and headers as /v1/tools/{tool} +// but only decides and records: nothing is forwarded, nothing is held. It is an +// operator endpoint, because an agent that can ask "would this pass?" without +// consequence can search the policy for values that slip through. +// +// The operator endpoints (POST /v1/decide/{tool}, POST /v1/holds/{hash}, GET +// /v1/records and /v1/records/verify) are outside that model. They belong to the provider's // operator, not to the agent being gated, and require // Authorization: Bearer . With no token configured they are // refused rather than left open. @@ -87,6 +92,7 @@ func New(gate *toolgate.Gate, upstreamURL, upstreamPrefix string, timeout time.D mux.HandleFunc("GET /health", s.health) mux.HandleFunc("GET /ready", s.health) mux.HandleFunc("POST /v1/tools/{tool}", s.authorize) + mux.HandleFunc("POST /v1/decide/{tool}", s.decide) mux.HandleFunc("POST /v1/holds/{hash}", s.resolve) mux.HandleFunc("GET /v1/records", s.records) mux.HandleFunc("GET /v1/records/verify", s.verify) @@ -111,25 +117,9 @@ func (s *Server) authorize(w http.ResponseWriter, r *http.Request) { r.Header.Set("X-Request-ID", newRequestID()) } w.Header().Set("X-Request-ID", r.Header.Get("X-Request-ID")) - var args map[string]any - if r.Body != nil { - b, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) - if err != nil { - writeError(w, http.StatusBadRequest, "bad_request", "cannot read body") - return - } - if len(bytes.TrimSpace(b)) > 0 { - // UseNumber keeps integers exact. Decoding into float64 rewrites - // anything past 2^53 (an account or invoice number), so the tool - // would receive a different value than the agent sent and the - // artifact would record the rewritten one. - dec := json.NewDecoder(bytes.NewReader(b)) - dec.UseNumber() - if err := dec.Decode(&args); err != nil { - writeError(w, http.StatusBadRequest, "bad_request", "body must be a JSON object of argument values") - return - } - } + args, ok := readArgs(w, r) + if !ok { + return } call := callFromRequest(r, tool, args) @@ -174,6 +164,58 @@ func (s *Server) authorize(w http.ResponseWriter, r *http.Request) { } } +// decide rules on a call and records it without forwarding or holding it. The +// answer is 200 whatever the decision: the request succeeded, and the decision +// is its content (also in the X-Toolgate-* headers, as on /v1/tools). +func (s *Server) decide(w http.ResponseWriter, r *http.Request) { + if !s.requireOperator(w, r) { + return + } + if r.Header.Get("X-Request-ID") == "" { + r.Header.Set("X-Request-ID", newRequestID()) + } + w.Header().Set("X-Request-ID", r.Header.Get("X-Request-ID")) + args, ok := readArgs(w, r) + if !ok { + return + } + call := callFromRequest(r, r.PathValue("tool"), args) + a := s.gate.DecideOnly(r.Context(), call) + w.Header().Set("X-Toolgate-Decision", a.Decision) + w.Header().Set("X-Toolgate-Rule", a.Rule) + w.Header().Set("X-Toolgate-Hash", a.Hash) + writeJSON(w, http.StatusOK, map[string]any{"decision": a.Decision, "rule": a.Rule, "scope": a.Scope, + "approval": a.Approval, "message": s.gate.Decide(call).Message, "outcome": a.Outcome, + "hash": a.Hash, "call_id": a.CallID}) +} + +// readArgs decodes the argument values from the request body, answering 400 +// itself when it cannot. +func readArgs(w http.ResponseWriter, r *http.Request) (map[string]any, bool) { + var args map[string]any + if r.Body == nil { + return args, true + } + b, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) + if err != nil { + writeError(w, http.StatusBadRequest, "bad_request", "cannot read body") + return nil, false + } + if len(bytes.TrimSpace(b)) > 0 { + // UseNumber keeps integers exact. Decoding into float64 rewrites + // anything past 2^53 (an account or invoice number), so the tool + // would receive a different value than the agent sent and the + // artifact would record the rewritten one. + dec := json.NewDecoder(bytes.NewReader(b)) + dec.UseNumber() + if err := dec.Decode(&args); err != nil { + writeError(w, http.StatusBadRequest, "bad_request", "body must be a JSON object of argument values") + return nil, false + } + } + return args, true +} + // forward sends the allowed call to the upstream tool endpoint. func (s *Server) forward(ctx context.Context, in *http.Request, c toolgate.Call) (upstreamResult, error) { body, _ := json.Marshal(c.Args) diff --git a/src/internal/toolgate/gate.go b/src/internal/toolgate/gate.go index a6bd268..6da5fd7 100644 --- a/src/internal/toolgate/gate.go +++ b/src/internal/toolgate/gate.go @@ -141,6 +141,9 @@ func (g *Gate) Decide(call Call) Decision { return Decision{Outcome: DecisionAllow, Rule: ScopeRuleID, Scope: scope, Approval: ApprovalAutomatic} } for _, r := range g.policy.Rules { + if !g.policy.applies(r, call.Tool) { + continue + } var fired bool var effect, reason string if r.Kind == KindLookup { @@ -214,6 +217,27 @@ func (g *Gate) Authorize(ctx context.Context, call Call, exec Executor) (Artifac return a, execErr } +// OutcomeDecideOnly is the outcome of a call decided by DecideOnly: the gate +// ruled and recorded, and the caller, not the gate, runs or skips the tool. +const OutcomeDecideOnly = "decided only: not executed by the gate" + +// DecideOnly decides the call and records the artifact without executing it +// and without holding it. It is for callers that execute tools themselves and +// ask the gate only for its ruling: a benchmark harness, or a policy measured +// in shadow before it enforces. The artifact's outcome says the gate did not +// act, so the record never reads as an enforced refusal or execution; an +// escalated call is recorded as escalated, but no hold exists to settle. +// Only toolcall.requested and toolcall.decided are published. +func (g *Gate) DecideOnly(ctx context.Context, call Call) Artifact { + d := g.Decide(call) + a := g.newArtifact(call, d) + a.Outcome = OutcomeDecideOnly + a = g.commit(a) + g.publish(ctx, EventRequested, a) + g.publish(ctx, EventDecided, a) + return a +} + // Resolve settles a held call. approver is the identity of the person who // decided; approved true runs the executor. A second artifact is appended so // the chain carries both the hold and the human decision. diff --git a/src/internal/toolgate/policy.go b/src/internal/toolgate/policy.go index bd37c44..ed1453f 100644 --- a/src/internal/toolgate/policy.go +++ b/src/internal/toolgate/policy.go @@ -24,6 +24,11 @@ const ( // KindSuffix denies when the string argument Arg does not end with one of // Allowed. Used for email domains. KindSuffix = "suffix" + // KindTool fires whenever the rule applies, whatever the arguments: the + // action itself is what the rule governs ("every destructive tool needs a + // person"). It takes no Arg and must be limited by Tool or Tag, so it can + // never fire on every tool by accident. + KindTool = "tool" // KindPrefix denies when the string argument Arg does not start with one of // Allowed. Used for storage destinations. KindPrefix = "prefix" @@ -35,11 +40,14 @@ const ( EffectEscalate = "escalate" ) -// Rule is one argument-value rule. Tool restricts the rule to one tool; an -// empty Tool applies it to every tool that carries the argument. +// Rule is one argument-value rule. Tool restricts the rule to one tool and Tag +// to the tools the policy's ToolTags label with that tag; a rule sets at most +// one of them. An empty Tool and Tag applies it to every tool that carries the +// argument. type Rule struct { ID string `json:"id"` Tool string `json:"tool,omitempty"` + Tag string `json:"tag,omitempty"` Kind string `json:"kind"` Arg string `json:"arg"` Max float64 `json:"max,omitempty"` @@ -59,6 +67,27 @@ type Policy struct { ToolScopes map[string]string `json:"tool_scopes"` Rules []Rule `json:"rules"` Lookups map[string]map[string]string `json:"lookups,omitempty"` + // ToolTags labels tools with classes ("destructive", "payment", ...) so + // one rule can govern many tools. Every tagged tool must be in ToolScopes. + ToolTags map[string][]string `json:"tool_tags,omitempty"` +} + +// hasTag reports whether the policy labels tool with tag. +func (p Policy) hasTag(tool, tag string) bool { + for _, t := range p.ToolTags[tool] { + if t == tag { + return true + } + } + return false +} + +// applies reports whether rule r governs calls to tool. +func (p Policy) applies(r Rule, tool string) bool { + if r.Tool != "" && r.Tool != tool { + return false + } + return r.Tag == "" || p.hasTag(tool, r.Tag) } // ScopeRuleID is the rule id recorded when the scope check decides a call. @@ -85,16 +114,46 @@ func (p Policy) Validate() error { if len(p.ToolScopes) == 0 { return fmt.Errorf("toolgate: policy maps no tools to scopes") } + tags := map[string]bool{} + for tool, ts := range p.ToolTags { + // A tag on a tool the policy does not know is a typo that would + // silently leave the real tool ungoverned. + if _, ok := p.ToolScopes[tool]; !ok { + return fmt.Errorf("toolgate: tool_tags names %q, which is not in tool_scopes", tool) + } + for _, t := range ts { + tags[t] = true + } + } seen := map[string]bool{} for _, r := range p.Rules { - if r.ID == "" || r.Arg == "" { - return fmt.Errorf("toolgate: rule %q needs an id and an arg", r.ID) + if r.ID == "" { + return fmt.Errorf("toolgate: rule needs an id") } if seen[r.ID] { return fmt.Errorf("toolgate: duplicate rule id %q", r.ID) } seen[r.ID] = true + if r.Tool != "" && r.Tag != "" { + return fmt.Errorf("toolgate: rule %q sets both tool and tag; use one", r.ID) + } + // Same reasoning as for tool_tags: a rule on a tag no tool carries + // governs nothing, and nobody would notice. + if r.Tag != "" && !tags[r.Tag] { + return fmt.Errorf("toolgate: rule %q uses tag %q, which no tool carries", r.ID, r.Tag) + } + if r.Kind == KindTool { + if r.Arg != "" { + return fmt.Errorf("toolgate: rule %q of kind tool takes no arg", r.ID) + } + if r.Tool == "" && r.Tag == "" { + return fmt.Errorf("toolgate: rule %q of kind tool needs a tool or a tag", r.ID) + } + } else if r.Arg == "" { + return fmt.Errorf("toolgate: rule %q needs an arg", r.ID) + } switch r.Kind { + case KindTool: case KindCeiling: case KindAllowlist, KindSuffix, KindPrefix: if len(r.Allowed) == 0 { @@ -209,6 +268,9 @@ func (r Rule) evaluate(call Call) (fired bool, effect, reason string) { return false, "", "" } effect = effectOrDeny(r) + if r.Kind == KindTool { + return true, effect, "" + } v, ok := call.Args[r.Arg] if !ok { // The rule constrains an argument the call did not supply, so the diff --git a/src/internal/toolgate/tags_test.go b/src/internal/toolgate/tags_test.go new file mode 100644 index 0000000..a56c22d --- /dev/null +++ b/src/internal/toolgate/tags_test.go @@ -0,0 +1,95 @@ +package toolgate + +import ( + "strings" + "testing" +) + +func taggedPolicy() Policy { + return Policy{ + Provider: "p", + GrantedScopes: []string{"files", "pay"}, + ToolScopes: map[string]string{"read_file": "files", "delete_file": "files", "wipe_disk": "files", "refund": "pay", "payout": "pay"}, + ToolTags: map[string][]string{ + "delete_file": {"destructive"}, "wipe_disk": {"destructive"}, + "refund": {"money"}, "payout": {"money"}, + }, + Rules: []Rule{ + {ID: "T1-destructive", Tag: "destructive", Kind: KindTool, Effect: EffectEscalate, Message: "destructive actions need a person"}, + {ID: "T2-money-ceiling", Tag: "money", Kind: KindCeiling, Arg: "amount", Max: 100, Message: "over the per-call limit"}, + }, + } +} + +// One rule governs every tool carrying its tag, and no other tool. +func TestTagRulesGovernTaggedToolsOnly(t *testing.T) { + g, err := New(taggedPolicy()) + if err != nil { + t.Fatal(err) + } + cases := []struct { + tool string + args map[string]any + want string + rule string + }{ + {"read_file", map[string]any{"path": "/tmp/x"}, DecisionAllow, ScopeRuleID}, + {"delete_file", map[string]any{"path": "/tmp/x"}, DecisionEscalate, "T1-destructive"}, + {"wipe_disk", nil, DecisionEscalate, "T1-destructive"}, + {"refund", map[string]any{"amount": 50}, DecisionAllow, ScopeRuleID}, + {"payout", map[string]any{"amount": 500}, DecisionDeny, "T2-money-ceiling"}, + // A tagged tool must still carry the argument its tag's rule checks. + {"refund", map[string]any{}, DecisionDeny, "T2-money-ceiling"}, + } + for _, c := range cases { + d := g.Decide(Call{Tool: c.tool, Args: c.args}) + if d.Outcome != c.want || d.Rule != c.rule { + t.Errorf("%s %v: got %s/%s, want %s/%s", c.tool, c.args, d.Outcome, d.Rule, c.want, c.rule) + } + } +} + +// Tags and tool rules fail validation on the mistakes that would otherwise +// leave a tool silently ungoverned or a rule firing everywhere. +func TestTagPolicyValidation(t *testing.T) { + bad := []struct { + name string + edit func(*Policy) + error string + }{ + {"tag on unknown tool", func(p *Policy) { p.ToolTags["delet_file"] = []string{"destructive"} }, "not in tool_scopes"}, + {"rule on unused tag", func(p *Policy) { p.Rules[0].Tag = "destrutive" }, "no tool carries"}, + {"tool and tag", func(p *Policy) { p.Rules[0].Tool = "delete_file" }, "both tool and tag"}, + {"tool rule with arg", func(p *Policy) { p.Rules[0].Arg = "path" }, "takes no arg"}, + {"tool rule unscoped", func(p *Policy) { p.Rules[0].Tag = "" }, "needs a tool or a tag"}, + {"arg rule without arg", func(p *Policy) { p.Rules[1].Arg = "" }, "needs an arg"}, + } + for _, b := range bad { + p := taggedPolicy() + b.edit(&p) + err := p.Validate() + if err == nil || !strings.Contains(err.Error(), b.error) { + t.Errorf("%s: got %v, want an error containing %q", b.name, err, b.error) + } + } + if err := taggedPolicy().Validate(); err != nil { + t.Errorf("valid tagged policy rejected: %v", err) + } +} + +// A tool rule scoped to one tool works without tags at all. +func TestToolRuleByName(t *testing.T) { + p := taggedPolicy() + p.ToolTags = nil + p.Rules = []Rule{{ID: "T3", Tool: "wipe_disk", Kind: KindTool}} + g, err := New(p) + if err != nil { + t.Fatal(err) + } + if d := g.Decide(Call{Tool: "wipe_disk"}); d.Outcome != DecisionDeny || d.Rule != "T3" { + t.Errorf("wipe_disk: got %s/%s, want deny/T3", d.Outcome, d.Rule) + } + if d := g.Decide(Call{Tool: "delete_file"}); d.Outcome != DecisionAllow { + t.Errorf("delete_file: got %s, want allow", d.Outcome) + } +} From ccbf7c6e1df5d7cecd75906923ea9e65b6df5efa Mon Sep 17 00:00:00 2001 From: Onur Cinar Date: Wed, 30 Sep 2026 12:57:15 -0700 Subject: [PATCH 2/3] deps: bump google.golang.org/grpc to v1.83.2 (CVE-2026-84445) 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 Claude-Session: https://claude.ai/code/session_01Ptdu2pZMVdE5x6XkwcUDjj --- src/aex-bid-evaluator/go.mod | 4 ++-- src/aex-bid-evaluator/go.sum | 4 ++++ src/aex-bid-gateway/go.mod | 4 ++-- src/aex-bid-gateway/go.sum | 4 ++++ src/aex-certauth/go.mod | 4 ++-- src/aex-certauth/go.sum | 4 ++++ src/aex-contract-engine/go.mod | 4 ++-- src/aex-contract-engine/go.sum | 4 ++++ src/aex-credentials-provider/go.mod | 8 ++++---- src/aex-credentials-provider/go.sum | 8 ++++++++ src/aex-gateway/go.mod | 8 ++++---- src/aex-gateway/go.sum | 8 ++++++++ src/aex-identity/go.mod | 4 ++-- src/aex-identity/go.sum | 4 ++++ src/aex-provider-registry/go.mod | 4 ++-- src/aex-provider-registry/go.sum | 4 ++++ src/aex-settlement/go.mod | 4 ++-- src/aex-settlement/go.sum | 4 ++++ src/aex-telemetry/go.mod | 8 ++++---- src/aex-telemetry/go.sum | 8 ++++++++ src/aex-token-bank/go.mod | 8 ++++---- src/aex-token-bank/go.sum | 8 ++++++++ src/aex-trust-broker/go.mod | 4 ++-- src/aex-trust-broker/go.sum | 4 ++++ src/aex-work-publisher/go.mod | 4 ++-- src/aex-work-publisher/go.sum | 4 ++++ src/internal/telemetry/go.mod | 8 ++++---- src/internal/telemetry/go.sum | 8 ++++++++ 28 files changed, 114 insertions(+), 38 deletions(-) diff --git a/src/aex-bid-evaluator/go.mod b/src/aex-bid-evaluator/go.mod index d5b5774..019fc8a 100644 --- a/src/aex-bid-evaluator/go.mod +++ b/src/aex-bid-evaluator/go.mod @@ -45,12 +45,12 @@ require ( go.opentelemetry.io/otel/trace v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect golang.org/x/crypto v0.55.0 // indirect - golang.org/x/net v0.57.0 // indirect + golang.org/x/net v0.58.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.41.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/grpc v1.83.1 // indirect + google.golang.org/grpc v1.83.2 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/src/aex-bid-evaluator/go.sum b/src/aex-bid-evaluator/go.sum index 18c975e..2457c4f 100644 --- a/src/aex-bid-evaluator/go.sum +++ b/src/aex-bid-evaluator/go.sum @@ -87,6 +87,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= @@ -118,6 +120,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/src/aex-bid-gateway/go.mod b/src/aex-bid-gateway/go.mod index 68402e9..d652665 100644 --- a/src/aex-bid-gateway/go.mod +++ b/src/aex-bid-gateway/go.mod @@ -45,12 +45,12 @@ require ( go.opentelemetry.io/otel/trace v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect golang.org/x/crypto v0.55.0 // indirect - golang.org/x/net v0.57.0 // indirect + golang.org/x/net v0.58.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.41.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/grpc v1.83.1 // indirect + google.golang.org/grpc v1.83.2 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/src/aex-bid-gateway/go.sum b/src/aex-bid-gateway/go.sum index 18c975e..2457c4f 100644 --- a/src/aex-bid-gateway/go.sum +++ b/src/aex-bid-gateway/go.sum @@ -87,6 +87,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= @@ -118,6 +120,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/src/aex-certauth/go.mod b/src/aex-certauth/go.mod index f7a41f2..b93acae 100644 --- a/src/aex-certauth/go.mod +++ b/src/aex-certauth/go.mod @@ -54,12 +54,12 @@ require ( go.opentelemetry.io/otel/trace v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect golang.org/x/crypto v0.55.0 // indirect - golang.org/x/net v0.57.0 // indirect + golang.org/x/net v0.58.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.41.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/grpc v1.83.1 // indirect + google.golang.org/grpc v1.83.2 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/src/aex-certauth/go.sum b/src/aex-certauth/go.sum index e03a8ba..549206b 100644 --- a/src/aex-certauth/go.sum +++ b/src/aex-certauth/go.sum @@ -93,6 +93,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= @@ -124,6 +126,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/src/aex-contract-engine/go.mod b/src/aex-contract-engine/go.mod index 135951b..e453549 100644 --- a/src/aex-contract-engine/go.mod +++ b/src/aex-contract-engine/go.mod @@ -45,12 +45,12 @@ require ( go.opentelemetry.io/otel/trace v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect golang.org/x/crypto v0.55.0 // indirect - golang.org/x/net v0.57.0 // indirect + golang.org/x/net v0.58.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.41.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/grpc v1.83.1 // indirect + google.golang.org/grpc v1.83.2 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/src/aex-contract-engine/go.sum b/src/aex-contract-engine/go.sum index 18c975e..2457c4f 100644 --- a/src/aex-contract-engine/go.sum +++ b/src/aex-contract-engine/go.sum @@ -87,6 +87,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= @@ -118,6 +120,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/src/aex-credentials-provider/go.mod b/src/aex-credentials-provider/go.mod index ed7976a..fd1cdff 100644 --- a/src/aex-credentials-provider/go.mod +++ b/src/aex-credentials-provider/go.mod @@ -35,11 +35,11 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect go.opentelemetry.io/otel/trace v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect - golang.org/x/net v0.56.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/text v0.39.0 // indirect + golang.org/x/net v0.58.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/grpc v1.83.1 // indirect + google.golang.org/grpc v1.83.2 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/src/aex-credentials-provider/go.sum b/src/aex-credentials-provider/go.sum index c31227b..26b96fb 100644 --- a/src/aex-credentials-provider/go.sum +++ b/src/aex-credentials-provider/go.sum @@ -57,10 +57,16 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= @@ -69,6 +75,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/src/aex-gateway/go.mod b/src/aex-gateway/go.mod index 02e0640..d9d0a6c 100644 --- a/src/aex-gateway/go.mod +++ b/src/aex-gateway/go.mod @@ -36,11 +36,11 @@ require ( go.opentelemetry.io/otel/trace v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/atomic v1.11.0 // indirect - golang.org/x/net v0.56.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/text v0.39.0 // indirect + golang.org/x/net v0.58.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/grpc v1.83.1 // indirect + google.golang.org/grpc v1.83.2 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/src/aex-gateway/go.sum b/src/aex-gateway/go.sum index ffb4cd9..7fff254 100644 --- a/src/aex-gateway/go.sum +++ b/src/aex-gateway/go.sum @@ -73,10 +73,16 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= @@ -85,6 +91,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/src/aex-identity/go.mod b/src/aex-identity/go.mod index 2d73443..61db72f 100644 --- a/src/aex-identity/go.mod +++ b/src/aex-identity/go.mod @@ -41,12 +41,12 @@ require ( go.opentelemetry.io/otel/trace v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect golang.org/x/crypto v0.55.0 // indirect - golang.org/x/net v0.57.0 // indirect + golang.org/x/net v0.58.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.41.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/grpc v1.83.1 // indirect + google.golang.org/grpc v1.83.2 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/src/aex-identity/go.sum b/src/aex-identity/go.sum index 647e143..08e93f5 100644 --- a/src/aex-identity/go.sum +++ b/src/aex-identity/go.sum @@ -82,6 +82,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= @@ -113,6 +115,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/src/aex-provider-registry/go.mod b/src/aex-provider-registry/go.mod index 16797b3..7ed6d8e 100644 --- a/src/aex-provider-registry/go.mod +++ b/src/aex-provider-registry/go.mod @@ -41,12 +41,12 @@ require ( go.opentelemetry.io/otel/trace v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect golang.org/x/crypto v0.55.0 // indirect - golang.org/x/net v0.57.0 // indirect + golang.org/x/net v0.58.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.41.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/grpc v1.83.1 // indirect + google.golang.org/grpc v1.83.2 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/src/aex-provider-registry/go.sum b/src/aex-provider-registry/go.sum index 647e143..08e93f5 100644 --- a/src/aex-provider-registry/go.sum +++ b/src/aex-provider-registry/go.sum @@ -82,6 +82,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= @@ -113,6 +115,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/src/aex-settlement/go.mod b/src/aex-settlement/go.mod index 0bca340..5487ebd 100644 --- a/src/aex-settlement/go.mod +++ b/src/aex-settlement/go.mod @@ -54,12 +54,12 @@ require ( go.opentelemetry.io/otel/trace v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect golang.org/x/crypto v0.55.0 // indirect - golang.org/x/net v0.57.0 // indirect + golang.org/x/net v0.58.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.41.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/grpc v1.83.1 // indirect + google.golang.org/grpc v1.83.2 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/src/aex-settlement/go.sum b/src/aex-settlement/go.sum index 1139d39..3b0123f 100644 --- a/src/aex-settlement/go.sum +++ b/src/aex-settlement/go.sum @@ -90,6 +90,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= @@ -121,6 +123,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/src/aex-telemetry/go.mod b/src/aex-telemetry/go.mod index dc7c4d6..9222e2b 100644 --- a/src/aex-telemetry/go.mod +++ b/src/aex-telemetry/go.mod @@ -28,12 +28,12 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect go.opentelemetry.io/otel/trace v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect - golang.org/x/net v0.56.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/text v0.39.0 // indirect + golang.org/x/net v0.58.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/grpc v1.83.1 // indirect + google.golang.org/grpc v1.83.2 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/src/aex-telemetry/go.sum b/src/aex-telemetry/go.sum index c31227b..26b96fb 100644 --- a/src/aex-telemetry/go.sum +++ b/src/aex-telemetry/go.sum @@ -57,10 +57,16 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= @@ -69,6 +75,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/src/aex-token-bank/go.mod b/src/aex-token-bank/go.mod index 6e35fb7..0db070d 100644 --- a/src/aex-token-bank/go.mod +++ b/src/aex-token-bank/go.mod @@ -30,12 +30,12 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect go.opentelemetry.io/otel/trace v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect - golang.org/x/net v0.56.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/text v0.39.0 // indirect + golang.org/x/net v0.58.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/grpc v1.83.1 // indirect + google.golang.org/grpc v1.83.2 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/src/aex-token-bank/go.sum b/src/aex-token-bank/go.sum index c31227b..26b96fb 100644 --- a/src/aex-token-bank/go.sum +++ b/src/aex-token-bank/go.sum @@ -57,10 +57,16 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= @@ -69,6 +75,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/src/aex-trust-broker/go.mod b/src/aex-trust-broker/go.mod index 6ceacc3..6562e09 100644 --- a/src/aex-trust-broker/go.mod +++ b/src/aex-trust-broker/go.mod @@ -41,12 +41,12 @@ require ( go.opentelemetry.io/otel/trace v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect golang.org/x/crypto v0.55.0 // indirect - golang.org/x/net v0.57.0 // indirect + golang.org/x/net v0.58.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.41.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/grpc v1.83.1 // indirect + google.golang.org/grpc v1.83.2 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/src/aex-trust-broker/go.sum b/src/aex-trust-broker/go.sum index 647e143..08e93f5 100644 --- a/src/aex-trust-broker/go.sum +++ b/src/aex-trust-broker/go.sum @@ -82,6 +82,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= @@ -113,6 +115,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/src/aex-work-publisher/go.mod b/src/aex-work-publisher/go.mod index c60894e..6b32f4a 100644 --- a/src/aex-work-publisher/go.mod +++ b/src/aex-work-publisher/go.mod @@ -66,7 +66,7 @@ require ( go.opentelemetry.io/otel/trace v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect golang.org/x/crypto v0.55.0 // indirect - golang.org/x/net v0.57.0 // indirect + golang.org/x/net v0.58.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect @@ -75,6 +75,6 @@ require ( google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/grpc v1.83.1 // indirect + google.golang.org/grpc v1.83.2 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/src/aex-work-publisher/go.sum b/src/aex-work-publisher/go.sum index f154bb0..253f3bc 100644 --- a/src/aex-work-publisher/go.sum +++ b/src/aex-work-publisher/go.sum @@ -126,6 +126,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -165,6 +167,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/src/internal/telemetry/go.mod b/src/internal/telemetry/go.mod index 7735671..9a0dc1b 100644 --- a/src/internal/telemetry/go.mod +++ b/src/internal/telemetry/go.mod @@ -29,11 +29,11 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect go.opentelemetry.io/otel/metric v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect - golang.org/x/net v0.56.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/text v0.39.0 // indirect + golang.org/x/net v0.58.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/grpc v1.83.1 // indirect + google.golang.org/grpc v1.83.2 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/src/internal/telemetry/go.sum b/src/internal/telemetry/go.sum index c31227b..26b96fb 100644 --- a/src/internal/telemetry/go.sum +++ b/src/internal/telemetry/go.sum @@ -57,10 +57,16 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= @@ -69,6 +75,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= From 785de45ee3e5a45ac27a19eb55e45caf3ddeeb08 Mon Sep 17 00:00:00 2001 From: Onur Cinar Date: Wed, 30 Sep 2026 13:46:46 -0700 Subject: [PATCH 3/3] toolgate: more value checks, list elements and nested arguments 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 Claude-Session: https://claude.ai/code/session_01Ptdu2pZMVdE5x6XkwcUDjj --- docs/TOOLGATE.md | 4 + src/internal/toolgate/policy.go | 389 +++++++++++++++++++++++++-- src/internal/toolgate/values_test.go | 185 +++++++++++++ 3 files changed, 553 insertions(+), 25 deletions(-) create mode 100644 src/internal/toolgate/values_test.go diff --git a/docs/TOOLGATE.md b/docs/TOOLGATE.md index b18d788..d8d527b 100644 --- a/docs/TOOLGATE.md +++ b/docs/TOOLGATE.md @@ -25,6 +25,10 @@ The study behind the component compared three configurations over the same twelv 2. **Values that cannot be checked are refused.** A rule whose argument is absent, is not a number where a number is required, or carries more than one value in one argument (`"a@evil.example,b@corp.example"`) fails closed rather than being skipped. Such a decision reports `cannot verify: ...` rather than the rule's own message, so an approver is not told a bank account changed when the call never mentioned one. 2. **Rules.** In `ModeFull`, argument-value rules are evaluated in policy order and the first that fires decides: `ceiling` (numeric maximum), `allowlist`, `lookup` (the value must match what a table maps another argument to, such as the vendor account on file for this invoice), `sensitive_field` (escalate to a person), `suffix` (email domains), `prefix` (storage destinations), `tool` (the action itself, whatever the arguments). A rule's `effect` is `deny` or `escalate`. + **More value checks.** `floor` (numeric minimum, `min`), `denylist` (`denied` values, compared without regard to case), `pattern` (`pattern`, an RE2 expression the whole value must match; RE2 cannot be driven into exponential time), `domain` (the host of an email address, a URL or a bare host name must be one of `allowed` or a subdomain of one; parsed, so `https://corp.example@evil.example` reads as `evil.example`; several `@` or a non-ASCII host fail closed), `max_items` (at most `max` items in a list; a lone value counts as one), and `compare` (`arg` `op` `match_arg` must hold, `op` one of `lt`, `le`, `gt`, `ge` on numbers, `eq`, `ne` on numbers or plain values, e.g. a refund may not exceed the original charge). + + **Lists and nested arguments.** `each: true` applies a single-value rule (ceiling, floor, allowlist, denylist, suffix, prefix, pattern, domain, sensitive_field) to every element of a list argument and to a lone value as a list of one; without it a list still fails closed. `arg` and `match_arg` may be paths: `payment.amount` reads a field of an object argument, `items[*].amount` 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 that does not exist in the call, or runs into a value of the wrong shape, fails closed like an absent argument. + **Tool tags.** A policy can label tools with classes in `tool_tags` (`{"delete_file": ["destructive"], "payout": ["money"]}`), and a rule can name a `tag` instead of a `tool`, so one rule governs every tool carrying the tag: `{"id": "T1", "tag": "destructive", "kind": "tool", "effect": "escalate"}` holds every destructive action for a person, and `{"id": "T2", "tag": "money", "kind": "ceiling", "arg": "amount", "max": 100}` caps every money tool. A rule sets `tool` or `tag`, not both. Validation refuses a tag on a tool missing from `tool_scopes`, a rule on a tag no tool carries, and a `tool` rule with an `arg` or with neither a tool nor a tag, since each would leave a tool silently ungoverned or a rule firing everywhere. Argument rules scoped by tag keep the fail-closed rule for absent arguments: a tagged tool must supply the argument its tag's rule checks. 3. **Execution.** Allowed calls run through the caller's executor; refused calls do not run; escalated calls are held until `Resolve` records a human decision under the approver's identity. 4. **Artifact.** One record per decision, chained to the previous record for the provider. diff --git a/src/internal/toolgate/policy.go b/src/internal/toolgate/policy.go index ed1453f..b0d68b8 100644 --- a/src/internal/toolgate/policy.go +++ b/src/internal/toolgate/policy.go @@ -3,8 +3,11 @@ package toolgate import ( "encoding/json" "fmt" + "net/url" "os" + "regexp" "strings" + "sync" ) // Rule kinds. Rules are evaluated in the order they appear in the policy, @@ -29,6 +32,24 @@ const ( // person"). It takes no Arg and must be limited by Tool or Tag, so it can // never fire on every tool by accident. KindTool = "tool" + // KindFloor denies when the numeric argument Arg is below Min. + KindFloor = "floor" + // KindDenylist denies when the argument Arg is one of Denied (compared + // without regard to case, so a denied account cannot be dodged by case). + KindDenylist = "denylist" + // KindPattern denies when the argument Arg does not match Pattern in full. + // Patterns are RE2, so a pattern cannot be made to run in exponential time. + KindPattern = "pattern" + // KindDomain denies when the host of the argument Arg (an email address, a + // URL or a bare host name) is not one of Allowed or a subdomain of one. + KindDomain = "domain" + // KindMaxItems denies when the argument Arg holds more than Max items (a + // lone value counts as one): bulk sends, bulk deletes. + KindMaxItems = "max_items" + // KindCompare denies unless Arg Op MatchArg holds: lt, le, gt, ge on + // numbers, eq and ne on numbers or plain values ("a refund may not exceed + // the original charge"). + KindCompare = "compare" // KindPrefix denies when the string argument Arg does not start with one of // Allowed. Used for storage destinations. KindPrefix = "prefix" @@ -56,8 +77,32 @@ type Rule struct { MatchArg string `json:"match_arg,omitempty"` Effect string `json:"effect,omitempty"` Message string `json:"message,omitempty"` + // Min is the floor's lower bound; a pointer so that a floor of 0 is + // distinguishable from a floor nobody set. + Min *float64 `json:"min,omitempty"` + Denied []string `json:"denied,omitempty"` + Pattern string `json:"pattern,omitempty"` + // Op is the relation a compare rule requires between Arg and MatchArg. + Op string `json:"op,omitempty"` + // Each applies a value rule to every element of a list argument. Without + // it a list cannot be checked as one value and the rule fails closed. + Each bool `json:"each,omitempty"` } +// Arg (and MatchArg) name an argument, or a path into one: "recipient.email" +// reads a field of an object argument, and "items[*].amount" the field of +// every element of a list, each of which the rule then checks. An argument +// whose own name contains a dot is still found by its exact name first. + +// eachKinds are the kinds that check one value at a time and so can apply to +// each element of a list. +var eachKinds = map[string]bool{ + KindCeiling: true, KindFloor: true, KindAllowlist: true, KindDenylist: true, KindSuffix: true, + KindPrefix: true, KindPattern: true, KindDomain: true, KindSensitiveField: true, +} + +var compareOps = map[string]bool{"lt": true, "le": true, "gt": true, "ge": true, "eq": true, "ne": true} + // Policy is a provider's authorization policy: the scopes this session holds, // the scope each tool requires, the argument-value rules, and any lookup // tables the lookup rules read. @@ -151,9 +196,58 @@ func (p Policy) Validate() error { } } else if r.Arg == "" { return fmt.Errorf("toolgate: rule %q needs an arg", r.ID) + } else if err := validPath(r.Arg); err != nil { + return fmt.Errorf("toolgate: rule %q: arg %v", r.ID, err) + } + if r.Each && !eachKinds[r.Kind] { + return fmt.Errorf("toolgate: rule %q of kind %s cannot apply to each element", r.ID, r.Kind) } switch r.Kind { case KindTool: + case KindFloor: + if r.Min == nil { + return fmt.Errorf("toolgate: rule %q needs min", r.ID) + } + case KindDenylist: + if len(r.Denied) == 0 { + return fmt.Errorf("toolgate: rule %q needs a denied list", r.ID) + } + case KindPattern: + if r.Pattern == "" { + return fmt.Errorf("toolgate: rule %q needs a pattern", r.ID) + } + if _, err := compiledPattern(r.Pattern); err != nil { + return fmt.Errorf("toolgate: rule %q has a bad pattern: %v", r.ID, err) + } + case KindDomain: + if len(r.Allowed) == 0 { + return fmt.Errorf("toolgate: rule %q needs an allowed list of domains", r.ID) + } + for _, d := range r.Allowed { + // A domain entry is a bare lower-case ASCII host name: "@corp.example", + // ".corp.example" or "https://corp.example" would never match. + if d == "" || d != strings.ToLower(d) || strings.ContainsAny(d, "@/:* ") || + strings.HasPrefix(d, ".") || strings.HasSuffix(d, ".") || !isASCII(d) { + return fmt.Errorf("toolgate: rule %q: %q is not a bare lower-case domain", r.ID, d) + } + } + case KindMaxItems: + if r.Max < 1 { + return fmt.Errorf("toolgate: rule %q needs max of at least 1", r.ID) + } + case KindCompare: + if !compareOps[r.Op] { + return fmt.Errorf("toolgate: rule %q needs op lt, le, gt, ge, eq or ne", r.ID) + } + if r.MatchArg == "" { + return fmt.Errorf("toolgate: rule %q needs match_arg", r.ID) + } + if strings.Contains(r.Arg+r.MatchArg, "[*]") { + return fmt.Errorf("toolgate: rule %q compares single values; [*] is not allowed", r.ID) + } + if err := validPath(r.MatchArg); err != nil { + return fmt.Errorf("toolgate: rule %q: match_arg %v", r.ID, err) + } case KindCeiling: case KindAllowlist, KindSuffix, KindPrefix: if len(r.Allowed) == 0 { @@ -271,73 +365,146 @@ func (r Rule) evaluate(call Call) (fired bool, effect, reason string) { if r.Kind == KindTool { return true, effect, "" } - v, ok := call.Args[r.Arg] - if !ok { - // The rule constrains an argument the call did not supply, so the - // constraint cannot be checked. Fail closed: an absent argument must - // not be a way around the rule (a tool whose upstream accepts a - // synonym, an alternate casing or its own default would otherwise - // sail straight past). evaluateLookup does the same. - return true, effect, unverifiable(r.Arg + " is required by this rule and was not supplied") + if r.Kind == KindCompare { + return r.evaluateCompare(call, effect) + } + vals, multi, why := resolveArg(call.Args, r.Arg) + if why != "" { + // The rule constrains an argument the call did not supply (or a path + // that does not exist in it), so the constraint cannot be checked. + // Fail closed: an absent argument must not be a way around the rule + // (a tool whose upstream accepts a synonym, an alternate casing or its + // own default would otherwise sail straight past). evaluateLookup + // does the same. + return true, effect, unverifiable(why) + } + if r.Kind == KindMaxItems { + n, ok := countItems(vals, multi) + if !ok { + return true, effect, unverifiable(r.Arg + " is null, so its items cannot be counted") + } + return float64(n) > r.Max, effect, "" + } + if r.Each && !multi { + // A rule marked each checks every element of a list argument, and a + // lone value as a list of one. Without each, a list still reaches + // checkValue whole and fails closed there, as before. + if list, ok := vals[0].([]any); ok { + vals = list + } + } + for _, v := range vals { + if fired, reason := r.checkValue(v); fired { + return true, effect, reason + } } + return false, "", "" +} + +// checkValue applies the rule's value check to one value. It is the single +// place each kind compares; evaluate decides which values reach it (the +// argument, an element of it, or a value found along a nested path). +func (r Rule) checkValue(v any) (fired bool, reason string) { switch r.Kind { case KindCeiling: f, ok := toFloat(v) if !ok { // Not a number, so it cannot be compared against the ceiling. // Fail closed: a quoted "999999" must not read as under the limit. - return true, effect, unverifiable(r.Arg + " is not a number, so it cannot be compared against the limit") + return true, unverifiable(r.Arg + " is not a number, so it cannot be compared against the limit") } - return f > r.Max, effect, "" + return f > r.Max, "" case KindAllowlist: s, ok := atomicValue(v) if !ok { - return true, effect, unverifiable(r.Arg + " is not a single plain value") + return true, unverifiable(r.Arg + " is not a single plain value") } for _, a := range r.Allowed { if s == a { - return false, "", "" + return false, "" } } - return true, effect, "" + return true, "" case KindSuffix: s, ok := atomicValue(v) if !ok { - return true, effect, unverifiable(r.Arg + " holds more than one value, or padding, so a suffix match is meaningless") + return true, unverifiable(r.Arg + " holds more than one value, or padding, so a suffix match is meaningless") } for _, a := range r.Allowed { if strings.HasSuffix(s, a) { - return false, "", "" + return false, "" } } - return true, effect, "" + return true, "" case KindPrefix: s, ok := atomicValue(v) if !ok { - return true, effect, unverifiable(r.Arg + " holds more than one value, or padding, so a prefix match is meaningless") + return true, unverifiable(r.Arg + " holds more than one value, or padding, so a prefix match is meaningless") } // An allowed prefix must not be escapable by walking back up out of // it: "corp-internal://../../../s3://attacker/" starts with the // allowed prefix and lands somewhere else entirely. if strings.Contains(s, "..") { - return true, effect, unverifiable(r.Arg + " walks back out of the allowed prefix") + return true, unverifiable(r.Arg + " walks back out of the allowed prefix") } for _, a := range r.Allowed { if strings.HasPrefix(s, a) { - return false, "", "" + return false, "" } } - return true, effect, "" + return true, "" + case KindFloor: + f, ok := toFloat(v) + if !ok { + return true, unverifiable(r.Arg + " is not a number, so it cannot be compared against the floor") + } + return f < *r.Min, "" + case KindDenylist: + s, ok := atomicValue(v) + if !ok { + return true, unverifiable(r.Arg + " is not a single plain value") + } + for _, d := range r.Denied { + if strings.EqualFold(s, d) { + return true, "" + } + } + return false, "" + case KindPattern: + s, ok := atomicValue(v) + if !ok { + return true, unverifiable(r.Arg + " is not a single plain value") + } + re, err := compiledPattern(r.Pattern) + if err != nil { + return true, unverifiable("the rule's pattern does not compile") + } + return !re.MatchString(s), "" + case KindDomain: + s, ok := atomicValue(v) + if !ok { + return true, unverifiable(r.Arg + " is not a single plain value") + } + host, why := hostOf(s) + if why != "" { + return true, unverifiable(r.Arg + " " + why) + } + for _, d := range r.Allowed { + if host == d || strings.HasSuffix(host, "."+d) { + return false, "" + } + } + return true, "" case KindSensitiveField: s, ok := atomicValue(v) if !ok { // The argument naming the field is not a lone scalar, so which // field is being changed cannot be established. Fail closed. - return true, effect, unverifiable(r.Arg + " is not a single plain value") + return true, unverifiable(r.Arg + " is not a single plain value") } - return normalizeField(s) == normalizeField(r.Field), effect, "" + return normalizeField(s) == normalizeField(r.Field), "" } - return false, "", "" + return false, "" } // evaluateLookup applies a lookup rule using the policy's table. @@ -345,7 +512,7 @@ func (p Policy) evaluateLookup(r Rule, call Call) (fired bool, effect, reason st if r.Tool != "" && r.Tool != call.Tool { return false, "", "" } - v, ok := call.Args[r.Arg] + v, ok := singleArg(call.Args, r.Arg) if !ok { // The value this rule checks is absent, so it cannot be checked // against the table. Fail closed for the same reason evaluate does: @@ -354,7 +521,7 @@ func (p Policy) evaluateLookup(r Rule, call Call) (fired bool, effect, reason st // where the money is allowed to go. return true, effectOrDeny(r), unverifiable(r.Arg + " is required by this rule and was not supplied") } - key, ok := call.Args[r.MatchArg] + key, ok := singleArg(call.Args, r.MatchArg) if !ok { return true, effectOrDeny(r), unverifiable(r.MatchArg + " was not supplied, so " + r.Arg + " cannot be checked against the table") } @@ -396,3 +563,175 @@ func toFloat(v any) (float64, bool) { } return 0, false } + +// evaluateCompare applies a compare rule: the rule fires unless Arg Op +// MatchArg holds. +func (r Rule) evaluateCompare(call Call, effect string) (fired bool, eff, reason string) { + a, ok := singleArg(call.Args, r.Arg) + if !ok { + return true, effect, unverifiable(r.Arg + " is required by this rule and was not supplied") + } + b, ok := singleArg(call.Args, r.MatchArg) + if !ok { + return true, effect, unverifiable(r.MatchArg + " was not supplied, so " + r.Arg + " cannot be compared with it") + } + fa, okA := toFloat(a) + fb, okB := toFloat(b) + if okA && okB { + var holds bool + switch r.Op { + case "lt": + holds = fa < fb + case "le": + holds = fa <= fb + case "gt": + holds = fa > fb + case "ge": + holds = fa >= fb + case "eq": + holds = fa == fb + case "ne": + holds = fa != fb + } + return !holds, effect, "" + } + if r.Op != "eq" && r.Op != "ne" { + // Ordering needs numbers; "999" against 1000 must not be decided as text. + return true, effect, unverifiable(r.Arg + " and " + r.MatchArg + " are not both numbers, so they cannot be ordered") + } + sa, okA := atomicValue(a) + sb, okB := atomicValue(b) + if !okA || !okB { + return true, effect, unverifiable(r.Arg + " or " + r.MatchArg + " is not a single plain value") + } + return (sa == sb) != (r.Op == "eq"), effect, "" +} + +// resolveArg finds the values a rule checks. It returns one value for a plain +// argument or path, and every value found for a path through "[*]" (multi), +// or a reason when the argument or path is absent. +func resolveArg(args map[string]any, path string) (vals []any, multi bool, why string) { + if v, ok := args[path]; ok { + return []any{v}, false, "" + } + if !strings.ContainsAny(path, ".[") { + return nil, false, path + " is required by this rule and was not supplied" + } + cur := []any{map[string]any(args)} + for _, seg := range strings.Split(path, ".") { + name, wild := strings.CutSuffix(seg, "[*]") + var next []any + for _, c := range cur { + obj, ok := c.(map[string]any) + if !ok { + return nil, false, path + " does not lead to a value in this call" + } + v, ok := obj[name] + if !ok { + return nil, false, path + " is required by this rule and was not supplied" + } + if !wild { + next = append(next, v) + continue + } + list, ok := v.([]any) + if !ok { + return nil, false, path + " expects a list at " + name + } + next = append(next, list...) + multi = true + } + cur = next + } + return cur, multi, "" +} + +// singleArg resolves a path that must lead to exactly one value. +func singleArg(args map[string]any, path string) (any, bool) { + vals, multi, why := resolveArg(args, path) + if why != "" || multi || len(vals) != 1 { + return nil, false + } + return vals[0], true +} + +// countItems counts what a max_items rule limits: the values a [*] path +// found, the elements of a list, or one for a lone value. Null is uncountable. +func countItems(vals []any, multi bool) (int, bool) { + if multi { + return len(vals), true + } + switch v := vals[0].(type) { + case nil: + return 0, false + case []any: + return len(v), true + } + return 1, true +} + +// validPath checks an argument path: non-empty segments, [*] only at a +// segment's end. +func validPath(path string) error { + for _, seg := range strings.Split(path, ".") { + name, _ := strings.CutSuffix(seg, "[*]") + if name == "" || strings.ContainsAny(name, "[]*") { + return fmt.Errorf("%q is not a valid argument path", path) + } + } + return nil +} + +// hostOf extracts the host a domain rule checks from an email address, a URL +// or a bare host name, lower-cased. It refuses what it cannot read without +// guessing: several "@", a URL without a host, and non-ASCII (homoglyph) +// hosts. +func hostOf(s string) (host, why string) { + switch { + case strings.Contains(s, "://"): + u, err := url.Parse(s) + if err != nil || u.Hostname() == "" { + return "", "is not a URL with a host" + } + // url.Parse already takes the host after any userinfo, so + // "https://corp.example@evil.example" reads as evil.example. + host = u.Hostname() + case strings.Contains(s, "@"): + if strings.Count(s, "@") != 1 { + return "", "has more than one @, so its domain is ambiguous" + } + host = s[strings.Index(s, "@")+1:] + default: + host = s + } + host = strings.TrimSuffix(strings.ToLower(host), ".") + if host == "" || !isASCII(host) { + return "", "has no plain ASCII host" + } + return host, "" +} + +func isASCII(s string) bool { + for i := 0; i < len(s); i++ { + if s[i] > 0x7e || s[i] < 0x21 { + return false + } + } + return true +} + +var patterns sync.Map // pattern source -> *regexp.Regexp + +// compiledPattern compiles a pattern once, anchored so it must match the +// whole value. +func compiledPattern(p string) (*regexp.Regexp, error) { + if re, ok := patterns.Load(p); ok { + return re.(*regexp.Regexp), nil + } + re, err := regexp.Compile(`^(?:` + p + `)$`) + if err != nil { + return nil, err + } + patterns.Store(p, re) + return re, nil +} diff --git a/src/internal/toolgate/values_test.go b/src/internal/toolgate/values_test.go new file mode 100644 index 0000000..73707a9 --- /dev/null +++ b/src/internal/toolgate/values_test.go @@ -0,0 +1,185 @@ +package toolgate + +import ( + "encoding/json" + "strings" + "testing" +) + +func f64(v float64) *float64 { return &v } + +// decideWith builds a one-tool gate around rules and decides one call. +func decideWith(t *testing.T, args string, rules ...Rule) Decision { + t.Helper() + p := Policy{Provider: "p", GrantedScopes: []string{"s"}, ToolScopes: map[string]string{"t": "s"}, Rules: rules} + g, err := New(p) + if err != nil { + t.Fatalf("policy rejected: %v", err) + } + var a map[string]any + dec := json.NewDecoder(strings.NewReader(args)) + dec.UseNumber() // as the HTTP service decodes + if err := dec.Decode(&a); err != nil { + t.Fatal(err) + } + return g.Decide(Call{Tool: "t", Args: a}) +} + +type valueCase struct { + name, args string + want string // allow, deny, or "unverifiable" (deny with a cannot-verify message) +} + +func runCases(t *testing.T, rule Rule, cases []valueCase) { + t.Helper() + rule.ID, rule.Tool = "R", "t" + for _, c := range cases { + d := decideWith(t, c.args, rule) + got := d.Outcome + if d.Outcome == DecisionDeny && strings.HasPrefix(d.Message, "cannot verify:") { + got = "unverifiable" + } + if got != c.want { + t.Errorf("%s %s: got %s (%s), want %s", rule.Kind, c.name, got, d.Message, c.want) + } + } +} + +func TestFloor(t *testing.T) { + runCases(t, Rule{Kind: KindFloor, Arg: "qty", Min: f64(1)}, []valueCase{ + {"at floor", `{"qty": 1}`, DecisionAllow}, + {"below", `{"qty": 0}`, DecisionDeny}, + {"negative", `{"qty": -5}`, DecisionDeny}, + {"quoted number", `{"qty": "7"}`, "unverifiable"}, + {"absent", `{}`, "unverifiable"}, + }) +} + +func TestDenylistIgnoresCase(t *testing.T) { + runCases(t, Rule{Kind: KindDenylist, Arg: "acct", Denied: []string{"NEW-ACCT-9911"}}, []valueCase{ + {"other", `{"acct": "ACME-ACCT-001"}`, DecisionAllow}, + {"denied", `{"acct": "NEW-ACCT-9911"}`, DecisionDeny}, + {"denied, other case", `{"acct": "new-acct-9911"}`, DecisionDeny}, + {"padded", `{"acct": " NEW-ACCT-9911"}`, "unverifiable"}, + {"two values", `{"acct": "A,NEW-ACCT-9911"}`, "unverifiable"}, + }) +} + +func TestPatternMatchesWholeValue(t *testing.T) { + runCases(t, Rule{Kind: KindPattern, Arg: "id", Pattern: `ACME-ACCT-\d{3}`}, []valueCase{ + {"matches", `{"id": "ACME-ACCT-001"}`, DecisionAllow}, + {"prefix only", `{"id": "ACME-ACCT-0012"}`, DecisionDeny}, + {"embedded", `{"id": "xACME-ACCT-001"}`, DecisionDeny}, + {"list", `{"id": ["ACME-ACCT-001"]}`, "unverifiable"}, + }) +} + +func TestDomain(t *testing.T) { + runCases(t, Rule{Kind: KindDomain, Arg: "to", Allowed: []string{"corp.example"}}, []valueCase{ + {"email", `{"to": "cfo@corp.example"}`, DecisionAllow}, + {"email, subdomain", `{"to": "cfo@eu.corp.example"}`, DecisionAllow}, + {"email, upper case", `{"to": "CFO@CORP.EXAMPLE"}`, DecisionAllow}, + {"url", `{"to": "https://files.corp.example/x"}`, DecisionAllow}, + {"bare host", `{"to": "corp.example"}`, DecisionAllow}, + {"lookalike suffix", `{"to": "cfo@evilcorp.example"}`, DecisionDeny}, + {"lookalike parent", `{"to": "cfo@corp.example.evil.example"}`, DecisionDeny}, + {"url userinfo trick", `{"to": "https://corp.example@evil.example/x"}`, DecisionDeny}, + {"two @", `{"to": "a@corp.example@evil.example"}`, "unverifiable"}, + {"homoglyph", `{"to": "cfo@corp.exаmple"}`, "unverifiable"}, + {"url without host", `{"to": "file:///etc/passwd"}`, "unverifiable"}, + }) +} + +func TestMaxItems(t *testing.T) { + runCases(t, Rule{Kind: KindMaxItems, Arg: "to", Max: 2}, []valueCase{ + {"two", `{"to": ["a", "b"]}`, DecisionAllow}, + {"three", `{"to": ["a", "b", "c"]}`, DecisionDeny}, + {"lone value", `{"to": "a"}`, DecisionAllow}, + {"null", `{"to": null}`, "unverifiable"}, + }) + runCases(t, Rule{Kind: KindMaxItems, Arg: "items[*].id", Max: 1}, []valueCase{ + {"one via path", `{"items": [{"id": 1}]}`, DecisionAllow}, + {"two via path", `{"items": [{"id": 1}, {"id": 2}]}`, DecisionDeny}, + }) +} + +// each applies a single-value rule to every element; without it a list fails +// closed as before. +func TestEachElement(t *testing.T) { + rule := Rule{Kind: KindDomain, Arg: "to", Allowed: []string{"corp.example"}, Each: true} + runCases(t, rule, []valueCase{ + {"all inside", `{"to": ["a@corp.example", "b@eu.corp.example"]}`, DecisionAllow}, + {"one outside", `{"to": ["a@corp.example", "x@evil.example"]}`, DecisionDeny}, + {"lone value", `{"to": "a@corp.example"}`, DecisionAllow}, + {"empty list", `{"to": []}`, DecisionAllow}, + }) + rule.Each = false + runCases(t, rule, []valueCase{{"list without each", `{"to": ["a@corp.example"]}`, "unverifiable"}}) +} + +func TestNestedPaths(t *testing.T) { + runCases(t, Rule{Kind: KindCeiling, Arg: "payment.amount", Max: 100}, []valueCase{ + {"inside", `{"payment": {"amount": 50}}`, DecisionAllow}, + {"over", `{"payment": {"amount": 500}}`, DecisionDeny}, + {"missing field", `{"payment": {"currency": "USD"}}`, "unverifiable"}, + {"not an object", `{"payment": 500}`, "unverifiable"}, + }) + runCases(t, Rule{Kind: KindCeiling, Arg: "items[*].amount", Max: 100}, []valueCase{ + {"every item inside", `{"items": [{"amount": 10}, {"amount": 90}]}`, DecisionAllow}, + {"one item over", `{"items": [{"amount": 10}, {"amount": 900}]}`, DecisionDeny}, + {"one item without it", `{"items": [{"amount": 10}, {"note": "x"}]}`, "unverifiable"}, + {"not a list", `{"items": {"amount": 10}}`, "unverifiable"}, + }) + // An argument literally named with a dot is found by its exact name first. + runCases(t, Rule{Kind: KindCeiling, Arg: "a.b", Max: 1}, []valueCase{ + {"exact name", `{"a.b": 5}`, DecisionDeny}, + }) +} + +func TestCompare(t *testing.T) { + runCases(t, Rule{Kind: KindCompare, Arg: "refund", Op: "le", MatchArg: "charge"}, []valueCase{ + {"less", `{"refund": 10, "charge": 20}`, DecisionAllow}, + {"equal", `{"refund": 20, "charge": 20}`, DecisionAllow}, + {"more", `{"refund": 30, "charge": 20}`, DecisionDeny}, + {"text", `{"refund": "30", "charge": 20}`, "unverifiable"}, + {"other absent", `{"refund": 30}`, "unverifiable"}, + }) + runCases(t, Rule{Kind: KindCompare, Arg: "from", Op: "ne", MatchArg: "to"}, []valueCase{ + {"different", `{"from": "A-1", "to": "B-2"}`, DecisionAllow}, + {"same", `{"from": "A-1", "to": "A-1"}`, DecisionDeny}, + }) + runCases(t, Rule{Kind: KindCompare, Arg: "order.total", Op: "eq", MatchArg: "payment.amount"}, []valueCase{ + {"nested equal", `{"order": {"total": 5}, "payment": {"amount": 5}}`, DecisionAllow}, + {"nested differ", `{"order": {"total": 5}, "payment": {"amount": 6}}`, DecisionDeny}, + }) +} + +func TestValueKindValidation(t *testing.T) { + bad := []struct { + rule Rule + error string + }{ + {Rule{Kind: KindFloor, Arg: "x"}, "needs min"}, + {Rule{Kind: KindDenylist, Arg: "x"}, "needs a denied list"}, + {Rule{Kind: KindPattern, Arg: "x"}, "needs a pattern"}, + {Rule{Kind: KindPattern, Arg: "x", Pattern: "("}, "bad pattern"}, + {Rule{Kind: KindDomain, Arg: "x"}, "allowed list of domains"}, + {Rule{Kind: KindDomain, Arg: "x", Allowed: []string{"@corp.example"}}, "not a bare lower-case domain"}, + {Rule{Kind: KindDomain, Arg: "x", Allowed: []string{"Corp.example"}}, "not a bare lower-case domain"}, + {Rule{Kind: KindMaxItems, Arg: "x"}, "max of at least 1"}, + {Rule{Kind: KindCompare, Arg: "x", MatchArg: "y", Op: "lte"}, "needs op"}, + {Rule{Kind: KindCompare, Arg: "x", Op: "lt"}, "needs match_arg"}, + {Rule{Kind: KindCompare, Arg: "x[*].a", Op: "lt", MatchArg: "y"}, "[*] is not allowed"}, + {Rule{Kind: KindMaxItems, Arg: "x", Max: 2, Each: true}, "cannot apply to each"}, + {Rule{Kind: KindCeiling, Arg: "a..b", Max: 1}, "not a valid argument path"}, + {Rule{Kind: KindCeiling, Arg: "a[*]b", Max: 1}, "not a valid argument path"}, + } + for _, b := range bad { + b.rule.ID, b.rule.Tool = "R", "t" + p := Policy{Provider: "p", GrantedScopes: []string{"s"}, ToolScopes: map[string]string{"t": "s"}, Rules: []Rule{b.rule}} + err := p.Validate() + if err == nil || !strings.Contains(err.Error(), b.error) { + t.Errorf("%s %+v: got %v, want an error containing %q", b.rule.Kind, b.rule, err, b.error) + } + } +}