GEAP Agent Gateway × Atryum — custom authz Service Extension (live PoC + handoff) - #130
Draft
johnwalz97 wants to merge 4 commits into
Draft
GEAP Agent Gateway × Atryum — custom authz Service Extension (live PoC + handoff)#130johnwalz97 wants to merge 4 commits into
johnwalz97 wants to merge 4 commits into
Conversation
server.go: pass-through non-MCP egress + a path-based mcp_tool_call fallback so the agent's tool-surface access is gated at REQUEST_AUTHZ (the gateway delivers only method+path to a custom ext_authz callout; per-tool-name gating needs CONTENT_AUTHZ/ext_proc). Includes a TEMP raw-request dump used to prove what the gateway sends. KNOWN: this inverts the fail-closed default and breaks TestCheck_Unattributable_FailsClosed - intentional for the live spike; see HANDOFF.md / REVIEW.md (A3) for the fix. Dockerfile.callout: lean build (no UI/licenses) used for the live callout:v5 image. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
examples/adk-plugin/: the harness-level integration path - gates each tool call in-process, sees full args, fail-closed. Complements the network-level gateway plane. Syntax-checked; not yet run against a live google-adk (see HANDOFF.md). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HANDOFF.md: full handoff (proof, live GCP resources, walls+resolutions, known issues+fixes, ext_proc next build, security must-fixes, teardown). REVIEW.md: senior code review (ranked findings + ext_proc spec). driver/: scenario driver for the live agent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Handoff — Atryum × Google Agent Gateway (GEAP) integration
Branch:
john/geap-agent-gateway-spike· Status: live PoC, proven end-to-end, ready for someone to take over and productionize.This document is the full handoff. It covers what was built, what is proven, the exact GCP resources left running, every wall hit and how it was solved, the open issues + suggested fixes (from a senior code review, full text in
REVIEW.md), and the next build. Read this +REVIEW.md+RESULTS.mdand you have everything.TL;DR
Atryum can be the allow/deny decision engine for AI-agent tool calls on Google's Gemini Enterprise Agent Platform, via two complementary planes:
ext_authzauthorization extension on the managed Agent Gateway, bound once, governing every agent routed through it (including no-code Agent Studio and marketplace agents). This is proven live.before_tool_callback/ RunnerPlugingates each tool call in-process for code-first agents (seeexamples/adk-plugin/). Deeper (sees full args) but opt-in per agent.Both call the same Atryum external-hook decision API, so rules / LLM-judge / HITL / audit are shared.
Proven, in Google's own audit log: a real Gemini agent on Agent Engine made a real MCP tool call → the Agent Gateway called Atryum → Atryum denied it → the gateway blocked it:
Flip the Atryum rule and the same agent is allowed and returns the document.
The single most important caveat
The custom
ext_authz(REQUEST_AUTHZ) callout receives onlymethod+pathfrom the gateway — verified by dumping the rawCheckRequest(empty headers, empty contextExtensions, empty body). The MCP tool name is NOT visible at REQUEST_AUTHZ (it lives in the JSON-RPC body). So today's enforcement is at the tool-surface / path level (every MCP tool call isPOST /mcp→ attributed to a synthetic toolmcp_tool_call→ allow/deny the agent's access to the tool surface).Per-tool-name and per-argument gating ("allow
search_documents, denyget_document") requires aCONTENT_AUTHZ/ext_procextension that streams the body. That adapter is spec'd but not yet built — it is the #1 next task (see The next build andREVIEW.md§c).What is proven live (evidence)
agentGateways/agent-gatewaycreated in ~3 min via the codelab TerraformPOST …/mcptrafficauthzPolicies/atryum-policy(REQUEST_AUTHZ, CUSTOM) →authzExtensions/atryum-authzPOST /envoy.service.auth.v3.Authorization/Check HTTP/2 200from gateway egress10.20.0.2atryum-policy: DENIED; result=DENIED. Atryum log:decision=deny reason="matched approval rule (auto_deny)"auto_approve, the same agent retrieves the tax return (Julian & Elena Sterling, SSN 323-45-6789)Three companion write-ups (claude.ai artifacts) exist: the non-ADK-governance scoping, the live-proof page, and the implementation blueprint. Canonical scoping doc:
atryum/docs/geap-atryum-integration-scoping.md.What's in this branch / PR
internal/googlegateway/— dependency-free decision core:types.go(ToolCall, Decision, defaults),service.go(Service.Decide, reuses the existing external-hook invocation path),parse.go(ExtractToolCallfrom ext_authz attrs / MCP JSON-RPC body). (committed inaaf766b)internal/googlegateway/extauthz/server.go— the Envoyext_authzv3 gRPC adapter. Working-tree changes from the live build (this PR): (1) pass-through allow for non-MCP requests; (2) a path-basedmcp_tool_callfallback so/mcptool egress can be gated at REQUEST_AUTHZ; (3) a TEMP raw-request dump used to prove what the gateway sends. See the known-issues below — these three are exactly what the review flags (A3/A7/A8/B).internal/config/config.go—GoogleGatewayConfig+applyGoogleGatewayEnv. (committed)cmd/atryum/main.go— wiring to start the callout per[[google_gateway]]. (committed)Dockerfile.callout— lean callout image build (skips UI/licenses; placeholderweb/index.htmlfor the//go:embed). Used for the livecallout:v5image. (new in this PR)examples/adk-plugin/— the ADK in-process integration (moduleatryum_adk, example agent, README, unit test). (new in this PR — authored by a sub-agent; sanity-checked for syntax, NOT run against a live ADK)examples/google-agent-gateway/—README.md,RESULTS.md(local + earlier-cloud verification),terraform/,local-demo/probe/, plus thisHANDOFF.md,REVIEW.md(senior review), anddriver/(a scenario driver for the live agent).Heads-up for the reviewer: the live-proven
server.godiff is committed as-is (per handoff request) and breaksTestCheck_Unattributable_FailsClosed(the fail-closed default was intentionally inverted to pass-through during the live build). This is issue A3 below — resolve the semantics + update the test as the first cleanup.Live GCP resources (the running demo — left up on purpose)
Project
singular-range-486318-b0, regionus-central1, authed asjohn@validmind.ai. Trial credit was ~$299.86/$300 remaining when last checked; the agent (2 min-instances) + gateway accrue a few $/day.agentGateways/agent-gateway(AGENT_TO_ANYWHERE, DNS-peered toatryumgw.net)authzExtensions/atryum-authz(EXT_AUTHZ_GRPC,callout.atryumgw.net, 10s)authzPolicies/atryum-policy(REQUEST_AUTHZ, CUSTOM) — coexists withagent-gateway-iap-policy(DRY_RUN)atryum-callout(10.0.0.2, no external IP) — docker:atryum(callout:v5, ext_authz :8090) +nginx(TLS :443 → :8090)atryum-gw→callout.atryumgw.netA10.0.0.2; gatewaynetworkConfig.dnsPeeringConfigreasoningEngines/1876979048555479040(Gemini 3.1, agent identity,agent_gateway_configset)legacy-dms+ Agent Registry mcpServer (toolssearch_documents,get_document)gateway-vpc, proxy-only + PSC subnets, network attachmentagent-gateway-na, Cloud NAT, firewallatryum-allow-gateway/atryum-allow-sshGoogleCloudPlatform/cloud-networking-solutionsdemos/agent-gateway; state in GCSsingular-range-486318-b0-tfstate/agent-gateway/state; trimmedterraform.tfvarsAdmin API / rules: the callout serves the Atryum admin API on the VM at
:8080(in-VPC only). Rules were seeded viaPOST /api/v1/admin/ruleswith a staticX-API-Key/X-API-Secret(values in the VM's/opt/atryum/atryum.toml).auth_debug.skip_verify=trueon the VM — see security must-fixes; do not carry this to any shared/prod deployment.Access the VM:
gcloud compute ssh atryum-callout --zone=us-central1-a --project=singular-range-486318-b0 --tunnel-through-iap.How to reproduce / verify the live gate
POST http://localhost:8080/api/v1/admin/rules(on the VM){action:"auto_approve", server_patterns:["*"], tool_patterns:["mcp_tool_call"], enabled:true}. Query the agent (vertexai.agent_engines.get(RESOURCE).stream_query(...), seedriver/run_scenarios.py) → the agent retrieves the document.auto_denyformcp_tool_call, re-query → the agent's tool call is blocked.resource.type="networkservices.googleapis.com/Gateway" httpRequest.status=403→ expandauthzPolicyInfo→atryum-policy: DENIED.Note the parking behavior: with NO matching rule, an identified tool parks as
pending_approvaland fail-closes at the ~10s timeout (see issue on the policy provider). You must seed an explicitauto_approverule to allow; thealways_approvepolicy provider does not auto-approve this path.Walls hit and how they were resolved
CERTIFICATE_VERIFY_FAILED. The gateway TLS-inspects the agent's own egress and presents its inspection CA; ADK's registry client (httpx/certifi) rejected it. Fix: inject the gateway root CA (agentGatewayCard.rootCertificates) into the agent's trust store (prepend to the certifi bundle + monkeypatchcertifi.where, imported first). The reference codelab does not do this — a real gotcha for any non-Google agent client.targetNetworkmust exactly string-match the network-attachment's network format:projects/P/global/networks/NAME(NOT thehttps://…self-link, NOT the short name). Also the peered domain can't be under reserved.internal; usedatryumgw.net..fail_open=falsefor the real gate.compute.vmExternalIpAccessblocks VM external IPs (used--no-address+ IAP SSH); Cloud Shell GCS writes trip an org trust-boundary (ran Terraform locally on the Mac instead).Known issues & suggested fixes
From a senior review (Fable) — full text with file:line refs in
REVIEW.md. The architecture (dep-free core + isolated gRPC adapter, reuse of the invocation path) is sound; the issues are at the seams between the code's assumptions and what the live gateway does.Ranked correctness issues
CallIDcomes fromx-request-id/contextExtensions(parse.go:34), both empty at REQUEST_AUTHZ, so every Check mints a new invocation; human approvals never reattach to the retry; parked pendings accumulate with no TTL. Fix: stable CallID (mcp-session-id+JSON-RPC id under ext_proc; bounded-window content hash until then) + a TTL/reaper for external pendings; stop documenting resume-on-retry until it works.DecisionTimeout30s exceeds the 10s gateway cap (types.go:62,README.md:64), so the callout's deny-with-reason branch is unreachable. Fix: default 8s; validate/clamp>=10sas a hard error in config.TestCheck_Unattributable_FailsClosed; the fail-closed default was inverted to pass-through (server.go:111-117) without a test update. Fix: make the semantics explicit config + update the test (see B3).parse.goattributes non-tools/callJSON-RPC methods as tool calls (usesparams.namewithout checkingmethod); batch arrays silently fail to parse. Harmless today (no body) but this is exactly the code CONTENT_AUTHZ will exercise.x-serverless-authorizationused as AgentID (parse.go:22) — that's a credential, not a label; it rotates per request and gets persisted into the audit DB. Drop it or verify the JWT and usesub/email.main.go:367-371) — if the gRPC port dies while the extension is failOpen/DRY_RUN, traffic flows ungated silently. Crash the process (or flip health + add readiness).server.go:78-91) logs headers/body at Info; harmless while empty, leaks args/auth headers once CONTENT_AUTHZ lands. Remove before merge (logHost+Attributes.Destinationonce first — see B2)./mcp/variants (should be config, not a hardcoded suffix); duplicateListenAddrsource of truth; no gRPC message-size/keepalive limits.Submit(invocation/service.go:919-1115) has no policy fallback (unlikeInvoke), so[policy] provider=always_approveis ignored for the callout path. The fail-closed park is safe but the config is a lie here. Fix: wires.policyinto Submit, or fail/warn at startup; ship a starter rule pack so the out-of-box UX isn't "hang 10s → generic 403 → orphaned pending".On the
mcp_tool_callfallback (sound or hack?): sound as an explicitly-scoped surface-level egress gate, currently framed as more than it is. (B1) it gates the whole JSON-RPC surface as one synthetic tool — name/document it as surface-level, not tool-level. (B2)ServerName="mcp"throws away the target backend — key it offHost/:authorityso rules distinguish backends. (B3) the pass-through is the real hack — make it explicitnon_mcp_egress = deny|allowconfig (default deny), log at Info with a counter.Top 5 recommendations (do in this order):
The next build: ext_proc CONTENT_AUTHZ
Per-tool-name / per-argument gating (and LLM-judge on the payload) requires the body. Build a sibling adapter
internal/googlegateway/extproc/implementingenvoy.service.ext_proc.v3.ExternalProcessor, reusinggooglegateway.Service.Decideunchanged (the dep-free split pays off here). TheProcessbidi-stream handler must::authority; if not MCP, CONTINUE +ModeOverrideto skip the rest; if MCP, requestrequest_body_mode: BUFFEREDand skip response-side processing (never buffer a streamable-HTTP/SSE MCP response).tools/call→Decide(real name + args +mcp-session-id);initialize/tools/list/notifications pass through; handle batch arrays; explicit body-size cap, fail closed above it (a truncated body must not become a silent allow).ImmediateResponse403 with a well-formed JSON-RPC error body (so MCP clients surface the reason). Same improvement applies to the ext_authz plain-text deny.mcp-session-id+ JSON-RPCid— this is what fixes A1 properly.Verify first: whether GEAP's CONTENT_AUTHZ profile is ext_proc wire format or ext_authz-with-body. Design for ext_proc (strictly more capable) but confirm on a project where the gateway provisions. Before writing it, do one deploy that logs
Host+Attributes.Destinationfrom the existing callout to settle exactly what REQUEST_AUTHZ carries beyond method+path.The ADK plugin (second plane) —
examples/adk-plugin/In-process gating for code-first ADK agents:
atryum_gate(abefore_tool_callback) andAtryumPlugin(a Runner Plugin to cover all agents under a runner). Config via env/args (ATRYUM_URL, source, agent_id), fail-closed. Sees the full tool args (unlike the network plane) and returns a denial to the model. Status: authored by a sub-agent, syntax-checked, unit test included; not yet run against a livegoogle-adk— validate imports/Plugin API against the installed ADK version and runtest_plugin.pybefore relying on it. This is the "deep per-agent coverage" complement to the gateway's "enforced floor for all agents (incl. no-code)".Security must-fixes before any shared/prod demo
(Full matrix in
REVIEW.md§d.) The urgent one:auth_debug.skip_verify=trueon the callout VM makesAdminMiddlewarepass every request — anyone with VPC reach can mint anauto_approverule and neuter the gate. Set itfalse, put the admin API behind OIDC, and firewall/mTLS both:443and the admin port. Also: distroless:nonrootimage, private-CA cert (or mTLS client-cert so only the gateway can reachCheck— the open Check port is a policy-decision oracle and a DB-write flood vector), remove the TEMP dump, fix the credential-as-AgentID, and alert on callout health + on the policy sitting in DRY_RUN (a canary "must-be-denied" probe is the cheap mitigation).Teardown (when done)
Not included / TODO
driver/run_scenarios.pyis the runner but itsscenarios.jsonis TODO.server.gocleanups from the review (A2/A3/A7/B2/B3) + the failing test.