diff --git a/.dockerignore b/.dockerignore index 36a7c09..2dba8ee 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,7 +11,7 @@ LICENSE # Compiled binaries advanced_server -examples/advanced_server/advanced_server +examples/contextual_access/advanced_server/advanced_server # IDE / editor .vscode diff --git a/README.md b/README.md index fd58d31..a2f5f2d 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ These servers implement webhook endpoints that integrate with an engine's hook s | `POST /pre` | Validate/modify tool inputs before execution | | `POST /post` | Validate/modify tool outputs after execution | +Post-hook requests from remote MCP servers also carry `content`, the content blocks the server returned alongside `output`. The examples that redact or filter `output` apply the same change to `content` and return it as `override.content`. MCP gateways render only text blocks, so pii_redactor and content_filter handle just those; advanced_server handles every block. + ## Architecture ``` diff --git a/examples/contextual_access/ab_testing/Dockerfile b/examples/contextual_access/ab_testing/Dockerfile index 7f9e1e6..d7ecf83 100644 --- a/examples/contextual_access/ab_testing/Dockerfile +++ b/examples/contextual_access/ab_testing/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder +FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder ARG TARGETOS ARG TARGETARCH @@ -11,15 +11,15 @@ RUN go mod download # Copy shared package and example source COPY pkg/ pkg/ -COPY examples/ab_testing/ examples/ab_testing/ +COPY examples/contextual_access/ab_testing/ examples/contextual_access/ab_testing/ RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ - go build -ldflags="-s -w" -trimpath -o /bin/server ./examples/ab_testing + go build -ldflags="-s -w" -trimpath -o /bin/server ./examples/contextual_access/ab_testing FROM gcr.io/distroless/static-debian12 COPY --from=builder /bin/server /bin/server -EXPOSE 8080 +EXPOSE 8888 ENTRYPOINT ["/bin/server"] diff --git a/examples/contextual_access/ab_testing/README.md b/examples/contextual_access/ab_testing/README.md index d6d0935..6cc2387 100644 --- a/examples/contextual_access/ab_testing/README.md +++ b/examples/contextual_access/ab_testing/README.md @@ -14,7 +14,7 @@ A minimal hook server that demonstrates how to **A/B test and canary-deploy tool ```bash # Run with experiment config -go run ./examples/ab_testing -config experiments.yaml +go run ./examples/contextual_access/ab_testing -config ./examples/contextual_access/ab_testing/example-config.yaml ``` ## Config File Format @@ -79,7 +79,7 @@ experiments: ```bash # Start with example config -go run ./examples/ab_testing -config experiments.yaml & +go run ./examples/contextual_access/ab_testing -config ./examples/contextual_access/ab_testing/example-config.yaml & # Send pre-hook requests for different users for i in $(seq 1 20); do diff --git a/examples/contextual_access/ab_testing/main.go b/examples/contextual_access/ab_testing/main.go index 3e57f06..9d8b2ee 100644 --- a/examples/contextual_access/ab_testing/main.go +++ b/examples/contextual_access/ab_testing/main.go @@ -8,7 +8,7 @@ // // Usage: // -// go run ./examples/ab_testing -port 8888 -config experiments.yaml +// go run ./examples/contextual_access/ab_testing -port 8888 -config ./examples/contextual_access/ab_testing/example-config.yaml package main import ( diff --git a/examples/contextual_access/advanced_server/Dockerfile b/examples/contextual_access/advanced_server/Dockerfile index 16a978a..d19195a 100644 --- a/examples/contextual_access/advanced_server/Dockerfile +++ b/examples/contextual_access/advanced_server/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder +FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder ARG TARGETOS ARG TARGETARCH @@ -11,15 +11,15 @@ RUN go mod download # Copy shared package and example source COPY pkg/ pkg/ -COPY examples/advanced_server/ examples/advanced_server/ +COPY examples/contextual_access/advanced_server/ examples/contextual_access/advanced_server/ RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ - go build -ldflags="-s -w" -trimpath -o /bin/server ./examples/advanced_server + go build -ldflags="-s -w" -trimpath -o /bin/server ./examples/contextual_access/advanced_server FROM gcr.io/distroless/static-debian12 COPY --from=builder /bin/server /bin/server -EXPOSE 8080 +EXPOSE 8888 ENTRYPOINT ["/bin/server"] diff --git a/examples/contextual_access/advanced_server/README.md b/examples/contextual_access/advanced_server/README.md index 34d16ef..3693b7b 100644 --- a/examples/contextual_access/advanced_server/README.md +++ b/examples/contextual_access/advanced_server/README.md @@ -7,7 +7,7 @@ A comprehensive hook server with a web dashboard for managing access rules, PII ### 1. Basic Rules (Access, Pre, Post) - **Access control**: Block users, toolkits, or specific tools from being visible - **Pre-execution rules**: Block or modify tool requests before execution -- **Post-execution rules**: Block or modify tool responses after execution +- **Post-execution rules**: Block or modify tool responses after execution. A rule that overrides the output also clears the server's `content` blocks, so clients get the new output instead of the original text. - **Pattern matching**: Exact, glob (`*`), and regex (`~pattern`) patterns - **Input/output matching**: Filter based on request content @@ -35,16 +35,16 @@ A comprehensive hook server with a web dashboard for managing access rules, PII ```bash # Run with defaults (port 8888, no auth) -go run ./examples/advanced_server +go run ./examples/contextual_access/advanced_server # Run with a configuration file -go run ./examples/advanced_server -config ./examples/advanced_server/example-config.yaml +go run ./examples/contextual_access/advanced_server -config ./examples/contextual_access/advanced_server/example-config.yaml # Run with authentication -go run ./examples/advanced_server -token "my-secret-token" +go run ./examples/contextual_access/advanced_server -token "my-secret-token" # Run with TLS -go run ./examples/advanced_server -tls -cert server.crt -key server.key +go run ./examples/contextual_access/advanced_server -tls -cert server.crt -key server.key ``` Then open `http://localhost:8888/` in your browser to access the dashboard. @@ -104,7 +104,7 @@ See [example-config.yaml](example-config.yaml) for a full example with all optio ## PII Redaction Details -The PII redactor scans all string values in tool response outputs. When PII is detected: +The PII redactor scans all string values in tool response outputs, and the string fields of any `content` blocks (sent by remote MCP servers), returning the redacted blocks as `override.content`. Base64 payloads (`data`, `blob`) are left as-is. When PII is detected: - **Redact mode**: Replaces PII with labeled placeholders (e.g., `[EMAIL REDACTED]`) - **Block mode**: Returns an error response instead of the tool output diff --git a/examples/contextual_access/advanced_server/ab_testing.go b/examples/contextual_access/advanced_server/ab_testing.go index 1580ae1..fbf4203 100644 --- a/examples/contextual_access/advanced_server/ab_testing.go +++ b/examples/contextual_access/advanced_server/ab_testing.go @@ -17,18 +17,18 @@ import ( // ABTestManager manages experiment state and variant assignment. type ABTestManager struct { mu sync.RWMutex - assignments map[string]string // "user:experiment" -> variant name + assignments map[string]string // "user:experiment" -> variant name stats map[string]*ExperimentStats // experiment name -> stats } // ExperimentStats tracks usage statistics for an experiment. type ExperimentStats struct { - Name string `json:"name"` - TotalRequests int `json:"total_requests"` - VariantCounts map[string]int `json:"variant_counts"` - UniqueUsers map[string]map[string]bool `json:"-"` // variant -> set of user IDs (not serialised) - VariantUsers map[string]int `json:"variant_users"` // variant -> unique user count - LastRequestTime *time.Time `json:"last_request_time,omitempty"` + Name string `json:"name"` + TotalRequests int `json:"total_requests"` + VariantCounts map[string]int `json:"variant_counts"` + UniqueUsers map[string]map[string]bool `json:"-"` // variant -> set of user IDs (not serialised) + VariantUsers map[string]int `json:"variant_users"` // variant -> unique user count + LastRequestTime *time.Time `json:"last_request_time,omitempty"` } // NewABTestManager creates a new A/B test manager. @@ -208,11 +208,11 @@ type RegistryResponse struct { // arcadeToolResponse represents a single tool from the Arcade engine API. type arcadeToolResponse struct { - Name string `json:"name"` - Description string `json:"description"` - FullyQualifiedName string `json:"fully_qualified_name"` - QualifiedName string `json:"qualified_name"` - Toolkit arcadeToolkitResponse `json:"toolkit"` + Name string `json:"name"` + Description string `json:"description"` + FullyQualifiedName string `json:"fully_qualified_name"` + QualifiedName string `json:"qualified_name"` + Toolkit arcadeToolkitResponse `json:"toolkit"` } // arcadeToolkitResponse represents toolkit info nested in a tool response. diff --git a/examples/contextual_access/advanced_server/example-config.yaml b/examples/contextual_access/advanced_server/example-config.yaml index 97ac877..19bcbca 100644 --- a/examples/contextual_access/advanced_server/example-config.yaml +++ b/examples/contextual_access/advanced_server/example-config.yaml @@ -3,7 +3,7 @@ # This file demonstrates all available configuration options. # The server hot-reloads this file when it changes. # -# Usage: go run ./examples/advanced_server -config example-config.yaml +# Usage: go run ./examples/contextual_access/advanced_server -config ./examples/contextual_access/advanced_server/example-config.yaml # Health endpoint configuration health: diff --git a/examples/contextual_access/advanced_server/main.go b/examples/contextual_access/advanced_server/main.go index 60137ba..573c05f 100644 --- a/examples/contextual_access/advanced_server/main.go +++ b/examples/contextual_access/advanced_server/main.go @@ -8,8 +8,8 @@ // // Usage: // -// go run ./examples/advanced_server -port 8888 -config config.yaml -// go run ./examples/advanced_server -port 8888 -token secret123 +// go run ./examples/contextual_access/advanced_server -port 8888 -config ./examples/contextual_access/advanced_server/example-config.yaml +// go run ./examples/contextual_access/advanced_server -port 8888 -token secret123 package main import ( @@ -440,7 +440,7 @@ func (s *HookServer) evaluatePostRules(req server.PostHookRequest) (*server.Post ruleMatch := "" for i, rule := range postCfg.Rules { if s.matchPostRule(rule, userID, req) { - result = s.applyPostRule(rule) + result = s.applyPostRule(rule, req) ruleMatch = fmt.Sprintf("post.rules[%d]", i) break } @@ -455,30 +455,36 @@ func (s *HookServer) evaluatePostRules(req server.PostHookRequest) (*server.Post // Always apply PII redaction on top of whatever result we have. // PII is a security/compliance feature and should never be bypassed by rules. - // Scan both inputs and output for PII — inputs may contain sensitive data - // that the tool could echo back, and output may not always be populated. + // Scan inputs, output, and content for PII — inputs may contain sensitive + // data that the tool could echo back, output may not always be populated, + // and content (the blocks a remote server sent alongside the output) + // reaches the client too. piiFound := false if cfg.PII != nil && cfg.PII.Enabled { - hasContent := req.Output != nil || (req.Inputs != nil && len(*req.Inputs) > 0) - if hasContent { + hasData := req.Output != nil || req.Content != nil || (req.Inputs != nil && len(*req.Inputs) > 0) + if hasData { detector := NewPIIDetector(cfg.PII) - // Scan both output and inputs for PII - var outputScan, inputScan PIIScanResult + // Scan output, content, and inputs for PII + var outputScan, contentScan, inputScan PIIScanResult if req.Output != nil { outputScan = detector.ScanAndSummarizeAny(req.Output) } + if req.Content != nil { + contentScan = detector.ScanAndSummarizeContent(*req.Content) + } if req.Inputs != nil { inputScan = detector.ScanAndSummarizeAny(*req.Inputs) } + outputPII := outputScan.ContainsPII || contentScan.ContainsPII - if outputScan.ContainsPII || inputScan.ContainsPII { + if outputPII || inputScan.ContainsPII { piiFound = true if cfg.PII.Action == "block" { // Block the response entirely, regardless of rule result errMsg := "Response blocked: PII detected" - if outputScan.ContainsPII && inputScan.ContainsPII { + if outputPII && inputScan.ContainsPII { errMsg = "Response blocked: PII detected in inputs and output" } else if inputScan.ContainsPII { errMsg = "Response blocked: PII detected in inputs" @@ -510,6 +516,20 @@ func (s *HookServer) evaluatePostRules(req server.PostHookRequest) (*server.Post } result.Override.Output = redacted } + + // Redact content too, or the server's original text reaches the + // client. As with output, a rule's override takes precedence. + contentToRedact := req.Content + if result.Override != nil && result.Override.Content != nil { + contentToRedact = result.Override.Content + } + if contentToRedact != nil { + redacted := detector.RedactContent(*contentToRedact) + if result.Override == nil { + result.Override = &server.PostHookOverride{} + } + result.Override.Content = &redacted + } return result, joinRuleMatch(ruleMatch, "pii:redact"), true } } @@ -544,7 +564,7 @@ func (s *HookServer) matchPostRule(rule PostRule, userID string, req server.Post return true } -func (s *HookServer) applyPostRule(rule PostRule) *server.PostHookResult { +func (s *HookServer) applyPostRule(rule PostRule, req server.PostHookRequest) *server.PostHookResult { result := &server.PostHookResult{ Code: actionToCode(rule.Action), } @@ -559,6 +579,11 @@ func (s *HookServer) applyPostRule(rule PostRule) *server.PostHookResult { result.Override = &server.PostHookOverride{ Output: output, } + // Clear the server's content blocks so clients get the + // replacement output (as text) instead of the original result. + if req.Content != nil { + result.Override.Content = &[]server.ContentBlock{} + } } } diff --git a/examples/contextual_access/advanced_server/pii.go b/examples/contextual_access/advanced_server/pii.go index b15090a..f602192 100644 --- a/examples/contextual_access/advanced_server/pii.go +++ b/examples/contextual_access/advanced_server/pii.go @@ -4,6 +4,8 @@ import ( "fmt" "regexp" "strings" + + "github.com/ArcadeAI/logical-extensions-examples/pkg/server" ) // ============================================================================= @@ -40,7 +42,7 @@ func NewPIIDetector(cfg *PIIConfig) *PIIDetector { d.labels["ssn"] = "[SSN REDACTED]" } if cfg.Types.Phone { - d.patterns["phone"] = regexp.MustCompile(`\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b`) + d.patterns["phone"] = regexp.MustCompile(`(?:\+?\b1[-.\s]?\(?|\(|\b)\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b`) d.labels["phone"] = "[PHONE REDACTED]" } if cfg.Types.CreditCard { @@ -193,6 +195,21 @@ func (d *PIIDetector) ScanAndSummarize(data map[string]interface{}) PIIScanResul func (d *PIIDetector) ScanAndSummarizeAny(data interface{}) PIIScanResult { var matches []PIIMatch d.scanValue(data, "", &matches) + return summarize(matches) +} + +// ScanAndSummarizeContent scans the string fields of post-hook content blocks +// (text, uri, annotations, _meta, and an embedded resource's text) for PII and +// returns a summary. +func (d *PIIDetector) ScanAndSummarizeContent(blocks []server.ContentBlock) PIIScanResult { + var matches []PIIMatch + for i, b := range blocks { + d.scanBlockFields(b.AdditionalProperties, fmt.Sprintf("content[%d]", i), &matches) + } + return summarize(matches) +} + +func summarize(matches []PIIMatch) PIIScanResult { counts := make(map[string]int) for _, m := range matches { counts[m.Type]++ @@ -208,3 +225,50 @@ func (d *PIIDetector) ScanAndSummarizeAny(data interface{}) PIIScanResult { func (d *PIIDetector) RedactAny(data interface{}) interface{} { return d.redactValue(data) } + +// RedactContent redacts PII from the string fields of post-hook content blocks, +// keeping each block's type and base64 payloads unchanged. +func (d *PIIDetector) RedactContent(blocks []server.ContentBlock) []server.ContentBlock { + result := make([]server.ContentBlock, len(blocks)) + for i, b := range blocks { + result[i] = server.ContentBlock{Type: b.Type, AdditionalProperties: d.redactBlockFields(b.AdditionalProperties)} + } + return result +} + +// isBinaryField reports whether a content block field holds a base64 payload +// (image and audio "data", a resource's "blob"). Regexes could corrupt these, +// so they are left as-is. +func isBinaryField(key string) bool { + return key == "data" || key == "blob" +} + +func (d *PIIDetector) scanBlockFields(fields map[string]interface{}, path string, matches *[]PIIMatch) { + for key, val := range fields { + if isBinaryField(key) { + continue + } + newPath := path + "." + key + if resource, ok := val.(map[string]interface{}); ok && key == "resource" { + d.scanBlockFields(resource, newPath, matches) + continue + } + d.scanValue(val, newPath, matches) + } +} + +func (d *PIIDetector) redactBlockFields(fields map[string]interface{}) map[string]interface{} { + result := make(map[string]interface{}, len(fields)) + for key, val := range fields { + resource, isMap := val.(map[string]interface{}) + switch { + case isBinaryField(key): + result[key] = val + case isMap && key == "resource": + result[key] = d.redactBlockFields(resource) + default: + result[key] = d.redactValue(val) + } + } + return result +} diff --git a/examples/contextual_access/basic_rules/Dockerfile b/examples/contextual_access/basic_rules/Dockerfile index 536d04b..49a2e14 100644 --- a/examples/contextual_access/basic_rules/Dockerfile +++ b/examples/contextual_access/basic_rules/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder +FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder ARG TARGETOS ARG TARGETARCH @@ -11,15 +11,15 @@ RUN go mod download # Copy shared package and example source COPY pkg/ pkg/ -COPY examples/basic_rules/ examples/basic_rules/ +COPY examples/contextual_access/basic_rules/ examples/contextual_access/basic_rules/ RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ - go build -ldflags="-s -w" -trimpath -o /bin/server ./examples/basic_rules + go build -ldflags="-s -w" -trimpath -o /bin/server ./examples/contextual_access/basic_rules FROM gcr.io/distroless/static-debian12 COPY --from=builder /bin/server /bin/server -EXPOSE 8080 +EXPOSE 8888 ENTRYPOINT ["/bin/server"] diff --git a/examples/contextual_access/basic_rules/README.md b/examples/contextual_access/basic_rules/README.md index b81f0e5..ebcf92a 100644 --- a/examples/contextual_access/basic_rules/README.md +++ b/examples/contextual_access/basic_rules/README.md @@ -5,17 +5,14 @@ A configurable test server for validating the CATE webhook hook system. It imple ## Quick Start ```bash -# From the engine directory -cd apps/engine - # Run with defaults (port 8888, no auth, allow all) -go run ./tools/webhook-test-server +go run ./examples/contextual_access/basic_rules # Run with authentication -go run ./tools/webhook-test-server -token "my-secret-token" +go run ./examples/contextual_access/basic_rules -token "my-secret-token" # Run with configuration file (enables blocking/modification rules) -go run ./tools/webhook-test-server -config ./tools/webhook-test-server/example-config.yaml +go run ./examples/contextual_access/basic_rules -config ./examples/contextual_access/basic_rules/example-config.yaml ``` ## Command Line Flags @@ -33,7 +30,7 @@ The server can be configured via a YAML file that supports: - **Access control**: Allow/deny tools based on user, toolkit, or tool name - **Pre-execution hooks**: Block execution or modify inputs, secrets, headers, server routing -- **Post-execution hooks**: Block responses or modify outputs +- **Post-execution hooks**: Block responses or modify outputs. An output override also clears the server's `content` blocks (sent by remote MCP servers), so clients get the new output instead of the original text. - **Pattern matching**: Exact match, glob patterns (`*`), or regex (`~pattern`) ### Example Configuration diff --git a/examples/contextual_access/basic_rules/example-config.yaml b/examples/contextual_access/basic_rules/example-config.yaml index ff6a336..318708f 100644 --- a/examples/contextual_access/basic_rules/example-config.yaml +++ b/examples/contextual_access/basic_rules/example-config.yaml @@ -3,7 +3,7 @@ # This file demonstrates all available configuration options. # The server hot-reloads this file when it changes. # -# Usage: go run ./tools/webhook-test-server -config example-config.yaml +# Usage: go run ./examples/contextual_access/basic_rules -config ./examples/contextual_access/basic_rules/example-config.yaml # Health endpoint configuration health: diff --git a/examples/contextual_access/basic_rules/generate-test-certs.sh b/examples/contextual_access/basic_rules/generate-test-certs.sh index 9f0149d..0668b54 100755 --- a/examples/contextual_access/basic_rules/generate-test-certs.sh +++ b/examples/contextual_access/basic_rules/generate-test-certs.sh @@ -81,7 +81,7 @@ echo " client.crt - Client certificate (use as client_cert in plugin config)" echo " client.key - Client private key (use as client_key in plugin config)" echo "" echo "To start the mTLS test server:" -echo " go run ./tools/webhook-test-server -port 8888 -tls -cert server.crt -key server.key -ca ca.crt" +echo " go run ./examples/contextual_access/basic_rules -port 8888 -tls -cert server.crt -key server.key -ca ca.crt" echo "" echo "To test with curl:" echo " curl --cacert ca.crt --cert client.crt --key client.key https://localhost:8888/health" diff --git a/examples/contextual_access/basic_rules/main.go b/examples/contextual_access/basic_rules/main.go index 981c47c..2de7568 100644 --- a/examples/contextual_access/basic_rules/main.go +++ b/examples/contextual_access/basic_rules/main.go @@ -4,13 +4,13 @@ // Usage: // // # Basic HTTP server with bearer token auth -// go run ./tools/webhook-test-server -port 8888 -token secret123 -config config.yaml +// go run ./examples/contextual_access/basic_rules -port 8888 -token secret123 -config ./examples/contextual_access/basic_rules/example-config.yaml // // # HTTPS server (TLS) -// go run ./tools/webhook-test-server -port 8888 -tls -cert server.crt -key server.key +// go run ./examples/contextual_access/basic_rules -port 8888 -tls -cert server.crt -key server.key // // # mTLS server (requires client certificate) -// go run ./tools/webhook-test-server -port 8888 -tls -cert server.crt -key server.key -ca ca.crt +// go run ./examples/contextual_access/basic_rules -port 8888 -tls -cert server.crt -key server.key -ca ca.crt // // The server logs all incoming requests and provides configurable responses. package main @@ -488,7 +488,7 @@ func (ts *TestServer) evaluatePostRules(req server.PostHookRequest) (*server.Pos for i, rule := range postCfg.Rules { if ts.matchPostRule(rule, userID, req) { - result := ts.applyPostRule(rule) + result := ts.applyPostRule(rule, req) return result, fmt.Sprintf("post.rules[%d]", i) } } @@ -524,7 +524,7 @@ func (ts *TestServer) matchPostRule(rule PostRule, userID string, req server.Pos return true } -func (ts *TestServer) applyPostRule(rule PostRule) *server.PostHookResult { +func (ts *TestServer) applyPostRule(rule PostRule, req server.PostHookRequest) *server.PostHookResult { result := &server.PostHookResult{ Code: ts.actionToCode(rule.Action), } @@ -539,6 +539,11 @@ func (ts *TestServer) applyPostRule(rule PostRule) *server.PostHookResult { result.Override = &server.PostHookOverride{ Output: output, } + // Clear the server's content blocks so clients get the + // replacement output (as text) instead of the original result. + if req.Content != nil { + result.Override.Content = &[]server.ContentBlock{} + } } } diff --git a/examples/contextual_access/content_filter/Dockerfile b/examples/contextual_access/content_filter/Dockerfile index 40a20e0..091d0f4 100644 --- a/examples/contextual_access/content_filter/Dockerfile +++ b/examples/contextual_access/content_filter/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder +FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder ARG TARGETOS ARG TARGETARCH @@ -11,15 +11,15 @@ RUN go mod download # Copy shared package and example source COPY pkg/ pkg/ -COPY examples/content_filter/ examples/content_filter/ +COPY examples/contextual_access/content_filter/ examples/contextual_access/content_filter/ RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ - go build -ldflags="-s -w" -trimpath -o /bin/server ./examples/content_filter + go build -ldflags="-s -w" -trimpath -o /bin/server ./examples/contextual_access/content_filter FROM gcr.io/distroless/static-debian12 COPY --from=builder /bin/server /bin/server -EXPOSE 8080 +EXPOSE 8888 ENTRYPOINT ["/bin/server"] diff --git a/examples/contextual_access/content_filter/README.md b/examples/contextual_access/content_filter/README.md index 3b9829c..7095b80 100644 --- a/examples/contextual_access/content_filter/README.md +++ b/examples/contextual_access/content_filter/README.md @@ -6,13 +6,13 @@ A minimal hook server that demonstrates how to **filter tool calls and responses - **Access hook**: Block entire toolkits from being visible - **Pre-execution hook**: Block tool execution when inputs contain prohibited content (keywords or regex patterns) -- **Post-execution hook**: Block or replace prohibited content in tool outputs +- **Post-execution hook**: Block or replace prohibited content in tool outputs and content text blocks ## Quick Start ```bash # Run with a config file -go run ./examples/content_filter -config filter-rules.yaml +go run ./examples/contextual_access/content_filter -config ./examples/contextual_access/content_filter/example-config.yaml ``` ## Config File Format @@ -56,24 +56,26 @@ blocked_output_patterns: ## How It Works +Rules match each value on its own, so a keyword or pattern doesn't match across two separate fields. + ### Input Filtering (Pre-Hook) -1. All tool input values are flattened into a single string +1. Each tool input value is checked on its own 2. Blocked keywords are checked (case-insensitive substring match) 3. Blocked input patterns are checked (regex match) 4. If any match is found, the tool execution is blocked with an error message ### Output Filtering (Post-Hook) -1. All tool output values are flattened into a single string +1. Each tool output value, and each value in `content` text blocks (sent by remote MCP servers), is checked on its own 2. Blocked keywords are checked 3. Blocked output patterns are checked: - `action: "block"` - Reject the entire response - - `action: "replace"` - Replace matching content with the replacement string + - `action: "replace"` - Replace matching content with the replacement string, in both the output and `content` text blocks (returned as `override.content`). Other block types pass through unchanged. ## Testing ```bash # Start the server with example rules -go run ./examples/content_filter -config filter-rules.yaml & +go run ./examples/contextual_access/content_filter -config ./examples/contextual_access/content_filter/example-config.yaml & # Test pre-hook - should be blocked (contains blocked keyword) curl -X POST http://localhost:8888/pre \ diff --git a/examples/contextual_access/content_filter/main.go b/examples/contextual_access/content_filter/main.go index 82ab45a..467cc48 100644 --- a/examples/contextual_access/content_filter/main.go +++ b/examples/contextual_access/content_filter/main.go @@ -2,13 +2,13 @@ // // This minimal hook server shows: // - Blocking tool execution based on input content (pre-hook) -// - Blocking or replacing tool output based on content (post-hook) +// - Blocking or replacing tool output and content text blocks based on content (post-hook) // - Using keyword lists and pattern matching for content filtering // // Usage: // -// go run ./examples/content_filter -port 8888 -// go run ./examples/content_filter -port 8888 -config filter-rules.yaml +// go run ./examples/contextual_access/content_filter -port 8888 +// go run ./examples/contextual_access/content_filter -port 8888 -config ./examples/contextual_access/content_filter/example-config.yaml package main import ( @@ -144,12 +144,15 @@ func (s *FilterServer) PreHook(c *gin.Context) { return } - // Serialize all inputs to a single string for keyword/pattern checking - inputStr := flattenValue(req.Inputs) + // Rules are checked against each input value on its own, so anchored + // patterns work. + fields := leafValues(req.Inputs) // Check blocked keywords in inputs for _, keyword := range s.config.BlockedKeywords { - if strings.Contains(strings.ToLower(inputStr), strings.ToLower(keyword)) { + if anyField(fields, func(f string) bool { + return strings.Contains(strings.ToLower(f), strings.ToLower(keyword)) + }) { errMsg := fmt.Sprintf("Input contains blocked content: %q", keyword) log.Printf("[PRE] Blocked: %s", errMsg) c.JSON(http.StatusOK, server.PreHookResult{ @@ -162,7 +165,7 @@ func (s *FilterServer) PreHook(c *gin.Context) { // Check regex patterns against inputs for _, cp := range s.compiledInputs { - if cp.pattern.MatchString(inputStr) { + if anyField(fields, cp.pattern.MatchString) { msg := cp.rule.Message if msg == "" { msg = fmt.Sprintf("Input matched blocked pattern: %s", cp.rule.Name) @@ -196,11 +199,21 @@ func (s *FilterServer) PostHook(c *gin.Context) { return } - outputStr := flattenValue(req.Output) + // Rules are checked against each output value on its own, so anchored + // patterns work. Content text blocks a remote server sent alongside the + // output reach the client too, so they are checked and rewritten the same way. + fields := leafValues(req.Output) + var content []server.ContentBlock + if req.Content != nil { + content = *req.Content + fields = append(fields, contentValues(content)...) + } // Check blocked keywords in output for _, keyword := range s.config.BlockedKeywords { - if strings.Contains(strings.ToLower(outputStr), strings.ToLower(keyword)) { + if anyField(fields, func(f string) bool { + return strings.Contains(strings.ToLower(f), strings.ToLower(keyword)) + }) { errMsg := fmt.Sprintf("Output contains blocked content: %q", keyword) log.Printf("[POST] Blocked: %s", errMsg) c.JSON(http.StatusOK, server.PostHookResult{ @@ -215,7 +228,7 @@ func (s *FilterServer) PostHook(c *gin.Context) { modified := false result := copyValue(req.Output) for _, cp := range s.compiledOutputs { - if cp.pattern.MatchString(outputStr) { + if anyField(fields, cp.pattern.MatchString) { if cp.rule.Action == "block" { msg := cp.rule.Message if msg == "" { @@ -229,8 +242,9 @@ func (s *FilterServer) PostHook(c *gin.Context) { return } if cp.rule.Action == "replace" { - // Replace matching content in all string values + // Replace matching content in all output string values and content text blocks result = replaceInValue(result, cp.pattern, cp.rule.Replacement) + content = replaceInText(content, cp.pattern, cp.rule.Replacement) modified = true log.Printf("[POST] Replaced content matching pattern %q", cp.rule.Name) } @@ -238,9 +252,13 @@ func (s *FilterServer) PostHook(c *gin.Context) { } if modified { + override := &server.PostHookOverride{Output: result} + if req.Content != nil { + override.Content = &content + } c.JSON(http.StatusOK, server.PostHookResult{ Code: server.OK, - Override: &server.PostHookOverride{Output: result}, + Override: override, }) return } @@ -331,6 +349,63 @@ func replaceInValue(v interface{}, pattern *regexp.Regexp, replacement string) i } } +// leafValues collects every non-null value in v, recursively, as a string. +func leafValues(v interface{}) []string { + switch val := v.(type) { + case nil: + return nil + case map[string]interface{}: + var leaves []string + for _, item := range val { + leaves = append(leaves, leafValues(item)...) + } + return leaves + case []interface{}: + var leaves []string + for _, item := range val { + leaves = append(leaves, leafValues(item)...) + } + return leaves + default: + return []string{flattenValue(val)} + } +} + +// contentValues collects the values in content text blocks. Other block types +// pass through unchanged; extend this if your servers put text there. +func contentValues(blocks []server.ContentBlock) []string { + var leaves []string + for _, b := range blocks { + if b.Type == "text" { + leaves = append(leaves, leafValues(b.AdditionalProperties)...) + } + } + return leaves +} + +// anyField reports whether match is true for any of fields. +func anyField(fields []string, match func(string) bool) bool { + for _, f := range fields { + if match(f) { + return true + } + } + return false +} + +// replaceInText replaces regex matches in content text blocks and passes other +// blocks through unchanged. +func replaceInText(blocks []server.ContentBlock, pattern *regexp.Regexp, replacement string) []server.ContentBlock { + result := make([]server.ContentBlock, len(blocks)) + for i, b := range blocks { + result[i] = b + if b.Type == "text" { + result[i].AdditionalProperties = replaceInValue(b.AdditionalProperties, pattern, replacement).(map[string]interface{}) + } + } + return result +} + // matchGlob matches a glob pattern against a value. func matchGlob(pattern, value string) bool { if pattern == "" || pattern == "*" { @@ -404,7 +479,7 @@ func main() { fmt.Printf("\nContent Filter Hook Server listening on %s\n", addr) fmt.Printf(" POST /access - Filter out blocked toolkits\n") fmt.Printf(" POST /pre - Block inputs with prohibited content\n") - fmt.Printf(" POST /post - Block or replace prohibited output content\n\n") + fmt.Printf(" POST /post - Block or replace prohibited output and content text blocks\n\n") if err := router.Run(addr); err != nil { log.Fatal("Failed to start server:", err) diff --git a/examples/contextual_access/pii_redactor/Dockerfile b/examples/contextual_access/pii_redactor/Dockerfile index 608a18d..7d79c3d 100644 --- a/examples/contextual_access/pii_redactor/Dockerfile +++ b/examples/contextual_access/pii_redactor/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder +FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder ARG TARGETOS ARG TARGETARCH @@ -11,15 +11,15 @@ RUN go mod download # Copy shared package and example source COPY pkg/ pkg/ -COPY examples/pii_redactor/ examples/pii_redactor/ +COPY examples/contextual_access/pii_redactor/ examples/contextual_access/pii_redactor/ RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ - go build -ldflags="-s -w" -trimpath -o /bin/server ./examples/pii_redactor + go build -ldflags="-s -w" -trimpath -o /bin/server ./examples/contextual_access/pii_redactor FROM gcr.io/distroless/static-debian12 COPY --from=builder /bin/server /bin/server -EXPOSE 8080 +EXPOSE 8888 ENTRYPOINT ["/bin/server"] diff --git a/examples/contextual_access/pii_redactor/README.md b/examples/contextual_access/pii_redactor/README.md index 7401976..9787a47 100644 --- a/examples/contextual_access/pii_redactor/README.md +++ b/examples/contextual_access/pii_redactor/README.md @@ -4,7 +4,7 @@ A minimal hook server that demonstrates how to **detect and redact personally id ## What It Shows -- **Post-execution hook**: Scans all string values in tool outputs for PII patterns +- **Post-execution hook**: Scans all string values in tool outputs and content text blocks for PII patterns - **Redact mode**: Replaces detected PII with labeled placeholders - **Block mode**: Rejects the entire response if PII is detected - Recursive scanning of nested objects and arrays @@ -13,13 +13,13 @@ A minimal hook server that demonstrates how to **detect and redact personally id ```bash # Redact all PII types (default) -go run ./examples/pii_redactor +go run ./examples/contextual_access/pii_redactor # Only detect specific PII types -go run ./examples/pii_redactor -types "email,ssn,credit_card" +go run ./examples/contextual_access/pii_redactor -types "email,ssn,credit_card" # Block responses instead of redacting -go run ./examples/pii_redactor -action block +go run ./examples/contextual_access/pii_redactor -action block ``` ## Supported PII Types @@ -37,7 +37,7 @@ go run ./examples/pii_redactor -action block 1. The **access** and **pre-execution** hooks are pass-throughs (PII redaction only applies to outputs) 2. The **post-execution hook** receives the tool's output -3. All string values in the output are recursively scanned for PII patterns +3. All string values in the output are recursively scanned for PII patterns, and so are `content` text blocks (sent by remote MCP servers). The redacted blocks are returned as `override.content`; other block types pass through unchanged. 4. Based on the configured action: - **Redact**: Each PII match is replaced with a type-specific placeholder - **Block**: The entire response is rejected with an error listing the PII types found @@ -46,7 +46,7 @@ go run ./examples/pii_redactor -action block ```bash # Start the server -go run ./examples/pii_redactor & +go run ./examples/contextual_access/pii_redactor & # Test with PII in output - will be redacted curl -X POST http://localhost:8888/post \ diff --git a/examples/contextual_access/pii_redactor/main.go b/examples/contextual_access/pii_redactor/main.go index 3e03a2e..ec35b0e 100644 --- a/examples/contextual_access/pii_redactor/main.go +++ b/examples/contextual_access/pii_redactor/main.go @@ -1,15 +1,15 @@ // pii_redactor demonstrates how to detect and redact PII from tool outputs. // // This minimal hook server shows: -// - Scanning tool outputs for PII (emails, IPs, SSNs, phone numbers, etc.) +// - Scanning tool outputs and content text blocks for PII (emails, IPs, SSNs, phone numbers, etc.) // - Replacing detected PII with labeled placeholders // - Optionally blocking responses that contain PII instead of redacting // // Usage: // -// go run ./examples/pii_redactor -port 8888 -// go run ./examples/pii_redactor -port 8888 -action block -// go run ./examples/pii_redactor -port 8888 -types "email,ssn,credit_card" +// go run ./examples/contextual_access/pii_redactor -port 8888 +// go run ./examples/contextual_access/pii_redactor -port 8888 -action block +// go run ./examples/contextual_access/pii_redactor -port 8888 -types "email,ssn,credit_card" package main import ( @@ -56,7 +56,7 @@ func AllPIIPatterns() map[string]PIIPattern { }, "phone": { Name: "phone", - Regex: regexp.MustCompile(`\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b`), + Regex: regexp.MustCompile(`(?:\+?\b1[-.\s]?\(?|\(|\b)\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b`), Replacement: "[PHONE REDACTED]", }, "credit_card": { @@ -158,8 +158,12 @@ func (s *RedactorServer) PostHook(c *gin.Context) { return } - // Scan all output values for PII + // Scan all output values for PII, and the content text blocks a remote + // server sent alongside them: clients see those too, so both must be redacted. piiFound := s.scanValue(req.Output) + if req.Content != nil { + piiFound = append(piiFound, s.scanContent(*req.Content)...) + } if len(piiFound) == 0 { // No PII detected - pass through c.JSON(http.StatusOK, server.PostHookResult{Code: server.OK}) @@ -167,7 +171,7 @@ func (s *RedactorServer) PostHook(c *gin.Context) { } // Log what was found - log.Printf("[POST] PII detected in %s.%s output:", req.Tool.Toolkit, req.Tool.Name) + log.Printf("[POST] PII detected in %s.%s result:", req.Tool.Toolkit, req.Tool.Name) for _, match := range piiFound { log.Printf(" - %s: %q", match.typeName, match.value) } @@ -184,12 +188,16 @@ func (s *RedactorServer) PostHook(c *gin.Context) { return } - // Redact PII in the output - redacted := s.redactValue(req.Output) - log.Printf("[POST] Redacted %d PII item(s) in output", len(piiFound)) + // Redact PII in the output and content text blocks + override := &server.PostHookOverride{Output: s.redactValue(req.Output)} + if req.Content != nil { + content := s.redactContent(*req.Content) + override.Content = &content + } + log.Printf("[POST] Redacted %d PII item(s) in result", len(piiFound)) c.JSON(http.StatusOK, server.PostHookResult{ Code: server.OK, - Override: &server.PostHookOverride{Output: redacted}, + Override: override, }) } @@ -261,6 +269,31 @@ func (s *RedactorServer) redactValue(v interface{}) interface{} { } } +// scanContent scans text blocks for PII. Other block types pass through +// unscanned; extend this if your servers put text there. +func (s *RedactorServer) scanContent(blocks []server.ContentBlock) []piiMatch { + var matches []piiMatch + for _, b := range blocks { + if b.Type == "text" { + matches = append(matches, s.scanMap(b.AdditionalProperties)...) + } + } + return matches +} + +// redactContent redacts PII in text blocks and passes other blocks through +// unchanged. +func (s *RedactorServer) redactContent(blocks []server.ContentBlock) []server.ContentBlock { + result := make([]server.ContentBlock, len(blocks)) + for i, b := range blocks { + result[i] = b + if b.Type == "text" { + result[i].AdditionalProperties = s.redactMap(b.AdditionalProperties) + } + } + return result +} + // ============================================================================= // Helpers // ============================================================================= @@ -333,7 +366,7 @@ func main() { addr := fmt.Sprintf(":%d", port) fmt.Printf("Listening on %s\n", addr) - fmt.Printf(" POST /post - Scan and redact PII from tool outputs\n\n") + fmt.Printf(" POST /post - Scan and redact PII from tool outputs and content text blocks\n\n") if err := router.Run(addr); err != nil { log.Fatal("Failed to start server:", err) diff --git a/examples/contextual_access/user_blocking/Dockerfile b/examples/contextual_access/user_blocking/Dockerfile index 5151be1..034f9f4 100644 --- a/examples/contextual_access/user_blocking/Dockerfile +++ b/examples/contextual_access/user_blocking/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder +FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder ARG TARGETOS ARG TARGETARCH @@ -11,15 +11,15 @@ RUN go mod download # Copy shared package and example source COPY pkg/ pkg/ -COPY examples/user_blocking/ examples/user_blocking/ +COPY examples/contextual_access/user_blocking/ examples/contextual_access/user_blocking/ RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ - go build -ldflags="-s -w" -trimpath -o /bin/server ./examples/user_blocking + go build -ldflags="-s -w" -trimpath -o /bin/server ./examples/contextual_access/user_blocking FROM gcr.io/distroless/static-debian12 COPY --from=builder /bin/server /bin/server -EXPOSE 8080 +EXPOSE 8888 ENTRYPOINT ["/bin/server"] diff --git a/examples/contextual_access/user_blocking/README.md b/examples/contextual_access/user_blocking/README.md index 62ea64d..edad216 100644 --- a/examples/contextual_access/user_blocking/README.md +++ b/examples/contextual_access/user_blocking/README.md @@ -12,10 +12,10 @@ A minimal hook server that demonstrates how to **block specific users** from acc ```bash # Block users via command line -go run ./examples/user_blocking -block "user1,user2,user3" +go run ./examples/contextual_access/user_blocking -block "user1,user2,user3" # Block users via config file -go run ./examples/user_blocking -config blocked-users.yaml +go run ./examples/contextual_access/user_blocking -config ./examples/contextual_access/user_blocking/example-config.yaml ``` ## Config File Format @@ -39,7 +39,7 @@ blocked_users: ```bash # Start the server -go run ./examples/user_blocking -block "blocked-user" & +go run ./examples/contextual_access/user_blocking -block "blocked-user" & # Test access hook - user is blocked curl -X POST http://localhost:8888/access \ diff --git a/examples/contextual_access/user_blocking/main.go b/examples/contextual_access/user_blocking/main.go index 13bad9a..8eed7b7 100644 --- a/examples/contextual_access/user_blocking/main.go +++ b/examples/contextual_access/user_blocking/main.go @@ -7,8 +7,8 @@ // // Usage: // -// go run ./examples/user_blocking -port 8888 -config blocked-users.yaml -// go run ./examples/user_blocking -port 8888 -block "user1,user2,user3" +// go run ./examples/contextual_access/user_blocking -port 8888 -config ./examples/contextual_access/user_blocking/example-config.yaml +// go run ./examples/contextual_access/user_blocking -port 8888 -block "user1,user2,user3" package main import ( diff --git a/pkg/server/schema.gen.go b/pkg/server/schema.gen.go index 0bd04c7..2c6c588 100644 --- a/pkg/server/schema.gen.go +++ b/pkg/server/schema.gen.go @@ -4,6 +4,9 @@ package server import ( + "encoding/json" + "fmt" + "github.com/gin-gonic/gin" ) @@ -52,6 +55,13 @@ type Authorization struct { ProviderId *string `json:"provider_id,omitempty"` } +// ContentBlock A block in Arcade's protocol-neutral content-block format: `text`, `image`, `audio`, `resource_link`, or `resource`, with `annotations` and `_meta`. The format follows the MCP content-block model, so MCP tooling and documentation apply, and `type` selects the block's other fields as MCP defines them: https://modelcontextprotocol.io/specification/2026-07-28/server/tools#tool-result. Content a server returns in another protocol would be converted into this format. +type ContentBlock struct { + // Type The block type, e.g. "text", "image", "audio", "resource_link", or "resource" + Type string `json:"type"` + AdditionalProperties map[string]interface{} `json:"-"` +} + // ErrorResponse Error response from webhook server type ErrorResponse struct { // Code Response code from hook server @@ -84,12 +94,18 @@ type OAuth2Details struct { // PostHookOverride Override response parameters type PostHookOverride struct { + // Content Replace the content blocks, in the same format as the request's `content`. Omit to forward the request's `content` unchanged. An empty list removes every block, and clients then receive `output` rendered as a single text block. A hook that rewrites `output` should rewrite `content` too, or the server's own text reaches the client as it was sent. + Content *[]ContentBlock `json:"content,omitempty"` + // Output Override the output value (any JSON type — string, number, object, array, etc.) Output interface{} `json:"output,omitempty"` } // PostHookRequest Post-hook request from engine to hook server type PostHookRequest struct { + // Content The content blocks the tool's server returned alongside its structured result, in Arcade's protocol-neutral content-block format (see `ContentBlock`). Today the only source is remote MCP servers. Fields the block definitions include are forwarded, annotations and `_meta` included, except that a `resource_link` block may arrive without its `title`, `size`, `icons`, or `_meta`. Fields outside the block definitions may be dropped. Clients receive the text blocks as the unstructured side of the result. Absent when the server sent no content of its own, in which case clients receive `output` rendered as text. Also absent on error results, whose error text arrives in `execution_error`. + Content *[]ContentBlock `json:"content,omitempty"` + // Context Tool execution context Context ToolContext `json:"context"` @@ -186,6 +202,30 @@ type ToolAuthRequirements struct { ProviderType *string `json:"provider_type,omitempty"` } +// ToolBehavior Behavior metadata for a tool +type ToolBehavior struct { + // Destructive Whether the tool can delete or irreversibly modify data + Destructive *bool `json:"destructive,omitempty"` + + // Idempotent Whether repeated calls with the same inputs produce the same result + Idempotent *bool `json:"idempotent,omitempty"` + + // OpenWorld Whether the tool can affect state outside its defined outputs + OpenWorld *bool `json:"open_world,omitempty"` + + // Operations Operations this tool performs (e.g., "read", "create", "update", "delete", "opaque"). Sourced from the tool's Behavior.Operations metadata. See https://docs.arcade.dev/en/guides/create-tools/tool-basics/add-tool-metadata for valid values. + Operations *[]string `json:"operations,omitempty"` + + // ReadOnly Whether the tool only reads data + ReadOnly *bool `json:"read_only,omitempty"` +} + +// ToolClassification Classification metadata for a tool +type ToolClassification struct { + // ServiceDomains Service domains this tool interfaces with (e.g., "crm", "email", "calendar"). Sourced from the tool's Classification.ServiceDomains metadata. See https://docs.arcade.dev/en/guides/create-tools/tool-basics/add-tool-metadata for valid values. + ServiceDomains *[]string `json:"service_domains,omitempty"` +} + // ToolContext Tool execution context type ToolContext struct { Authorization *[]Authorization `json:"authorization,omitempty"` @@ -202,6 +242,9 @@ type ToolContext struct { // ToolInfo Tool identification information type ToolInfo struct { + // Metadata Tool metadata + Metadata *ToolVersionInfoMetadata `json:"metadata,omitempty"` + // Name Tool name Name string `json:"name"` @@ -214,6 +257,9 @@ type ToolInfo struct { // ToolVersionInfo Version-specific information for a tool type ToolVersionInfo struct { + // Metadata Tool metadata + Metadata *ToolVersionInfoMetadata `json:"metadata,omitempty"` + // Requirements Requirements for a toolkit (group of tools) Requirements *ToolkitRequirements `json:"requirements,omitempty"` @@ -221,6 +267,18 @@ type ToolVersionInfo struct { Version *string `json:"version,omitempty"` } +// ToolVersionInfoMetadata Tool metadata +type ToolVersionInfoMetadata struct { + // Behavior Behavior metadata for a tool + Behavior *ToolBehavior `json:"behavior,omitempty"` + + // Classification Classification metadata for a tool + Classification *ToolClassification `json:"classification,omitempty"` + + // Extras Arbitrary additional metadata (e.g., {"IdP": "entra_id"}) + Extras *map[string]interface{} `json:"extras,omitempty"` +} + // ToolkitInfo Information about a group of tools type ToolkitInfo struct { // Tools Map of tool name to array of tool version info (there may be multiple versions of tools) @@ -248,6 +306,72 @@ type PostHookJSONRequestBody = PostHookRequest // PreHookJSONRequestBody defines body for PreHook for application/json ContentType. type PreHookJSONRequestBody = PreHookRequest +// Getter for additional properties for ContentBlock. Returns the specified +// element and whether it was found +func (a ContentBlock) Get(fieldName string) (value interface{}, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for ContentBlock +func (a *ContentBlock) Set(fieldName string, value interface{}) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]interface{}) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for ContentBlock to handle AdditionalProperties +func (a *ContentBlock) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["type"]; found { + err = json.Unmarshal(raw, &a.Type) + if err != nil { + return fmt.Errorf("error reading 'type': %w", err) + } + delete(object, "type") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]interface{}) + for fieldName, fieldBuf := range object { + var fieldVal interface{} + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for ContentBlock to handle AdditionalProperties +func (a ContentBlock) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + object["type"], err = json.Marshal(a.Type) + if err != nil { + return nil, fmt.Errorf("error marshaling 'type': %w", err) + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + // ServerInterface represents all server handlers. type ServerInterface interface { // Access control hook @@ -275,6 +399,7 @@ type MiddlewareFunc func(c *gin.Context) // AccessHook operation middleware func (siw *ServerInterfaceWrapper) AccessHook(c *gin.Context) { + c.Set(BearerAuthScopes, []string{}) for _, middleware := range siw.HandlerMiddlewares { @@ -289,6 +414,7 @@ func (siw *ServerInterfaceWrapper) AccessHook(c *gin.Context) { // HealthCheck operation middleware func (siw *ServerInterfaceWrapper) HealthCheck(c *gin.Context) { + for _, middleware := range siw.HandlerMiddlewares { middleware(c) if c.IsAborted() { @@ -301,6 +427,7 @@ func (siw *ServerInterfaceWrapper) HealthCheck(c *gin.Context) { // PostHook operation middleware func (siw *ServerInterfaceWrapper) PostHook(c *gin.Context) { + c.Set(BearerAuthScopes, []string{}) for _, middleware := range siw.HandlerMiddlewares { @@ -315,6 +442,7 @@ func (siw *ServerInterfaceWrapper) PostHook(c *gin.Context) { // PreHook operation middleware func (siw *ServerInterfaceWrapper) PreHook(c *gin.Context) { + c.Set(BearerAuthScopes, []string{}) for _, middleware := range siw.HandlerMiddlewares {