The problem

Enterprise AI agents run for minutes, touch production, and hand back a verdict. Everything in between is a black box to the people the workflow serves. Traces exist — for developers. The approver and the auditor get nothing in their language, so they either rubber-stamp the agent or refuse to adopt it.

And every team solving this builds the same four things by hand, once per workflow: a status UI, an approval flow, credential glue, an audit trail. Four teams, four workflows, four incompatible half-solutions — and an auditor who has to learn all of them.

One platform, every client app

DevPortal is the platform those client apps are built on. Any app that fronts an agentic workflow — an incident console, a claims queue, a migration dashboard — is built on the same shell and inherits the same four things, plus one fence every agent acts through.

That means one place where a run is watched, one approval gesture, one identity model, one audit trail — across every workflow in the organization, no matter which team wrote the agent or which department the app serves.

Written fresh for a pack: detection rules, a bounded action vocabulary, domain views, an app.yaml. Everything else — the fence, the identity model, the run stream, the shell — is platform.

Agents stay external, deployed on Google Cloud with their own lifecycle. The platform never runs, retrains, or locks in an agent. ADK and adk web serve the developer; DevPortal serves the people the agent's work lands on.

How it works: two arrows

Inbound, the platform is a client of the agent. It reads runs, card, health and memory like any external system. The agent reports every step to POST /_platform/agent-runs/events, so the app shows runs it never started, streamed to the browser as they arrive.

Outbound, the platform is the agent's policy enforcement point. Every call the agent makes crosses the Capability Broker carrying the agent's own identity. Reads execute. Mutations become pending_approval intents carrying a payload-bound HMAC digest; a human approves that digest, the agent re-presents it, and the broker commits exactly once. Payload drift invalidates the approval.

App Packs are the unit of delivery: a full-stack app that declares the capabilities it needs and renders the agent's work as the user's content. Its API runs in a workerd V8 isolate inside the Cloud Run container — no keys, loopback-only egress.

Fortified Enterprise Fleet — how it maps

Capability What it is here
Agent registry An A2A-style card built by ADK from the live tree, rendered as the app's Agents view. Releases signed Ed25519 over the manifest digest with a CycloneDX SBOM. A cross-department catalog is designed, not built.
Agent runtime A long-running autonomous ADK tree on Cloud Run, polling and acting with no session holding it open.
Memory bank Vertex AI Agent Engine Memory Bank, four scoped kinds, server-side consolidation. Context outlives the process.
Agent identity Workload tokens minted per deployment, identity bound from the presented token at every fence — never from the request body.
Agent gateway The broker is the single ingress: default-deny, method allowlists with teeth, single-use digest intents with nonce and expiry.
Model Armor Inline on both sides of the planner's model call, with two failure policies — a filter match fails closed, an unreachable guardrail fails open and says so on the run.
Observability An append-only run-event log carrying the whole reasoning chain, attributed per sub-agent, streamed live and mirrored to Cloud Storage so history outlives the container.

Six ADK surfaces do that work as integration points: AgentCardBuilder (the Agents view is the card), BasePlugin (hooks become run events; every hook returns None, so narration cannot alter behavior), VertexAiMemoryBankService, BaseAgent/LlmAgent, FunctionTool, InMemoryRunner.

Six modules do it and none of them know what an incident is — agent-card introspection (97 lines), the reporting client (145), the narration plugin (72), the Model Armor guardrail (184), the Memory Bank wrapper (507), the EmbeddingGemma tier (194). Point the Agents and Memory views at a different ADK deployment and they render it.

The reference App Pack: Cloud Incident Commander

An operator console for a 12-service Online Boutique fleet on GKE. No chat box, and nobody starts a run — the operator's job is to decide, not to prompt.

Inject a fault (an 8 MB Node heap cap on paymentservice) and every step below appears as the agent reports it:

  1. It finds the crash-loop on its own poll and opens a run.
  2. It recalls what it knows about this service. EmbeddingGemma scores how closely each known-benign pattern restates this symptom.
  3. It investigates with read-only tools — topology, deployment state, logs, metrics — each call narrated, attributed to the sub-agent that made it. Log entries are ranked against the symptom before the prompt budget is spent.
  4. Model Armor checks the evidence in and the answer out.
  5. Gemini 3.5 Flash names the cause and proposes exactly one bounded action, with the method, target, payload, the broker's diff and its reasoning.
  6. The human decides, in one click. That is the only click in the workflow.
  7. The agent executes the approved payload, waits for two healthy polls, closes the run, and writes a lesson to memory.

Measured on the live deployment (INC-7367C1D0, 24 events): fault → case 15 s, detection → proposal 20 s, approval → verified 57 s. The 2 m 25 s in the middle was a person reading.

Memory changes later behavior. A finding a human rejected becomes a known-benign pattern, and a recurrence returns marked advisory instead of paging anyone.

Features and functionality

Everything below runs on Google Cloud today.

  • An app shell that hosts App Packs. A pack declares the capabilities it needs; an absent optional capability answers 503 naming the port rather than faking a reading.
  • Pack APIs in a workerd V8 isolate, holding no keys, loopback-only egress. Every response discloses who served it.
  • One fence for everything an agent does. Default deny; an undeclared method is refused outright, never routed into an approval nobody should be asked for.
  • Approval bound to an exact payload by HMAC digest, single-use via nonce and expiry.
  • Workload identity instead of standing keys, bound from the presented token at every fence.
  • A client for external agents — runs, card, health and memory read from any ADK deployment that speaks the contract.
  • Live narration, streamed to the browser as each step arrives.
  • A run log that outlives the container, mirrored to Cloud Storage and replayed at boot.
  • Inline guardrails around any model call, with two failure policies.
  • A second, smaller model that scores rather than decides. EmbeddingGemma-300M, int4 ONNX on the agent's own CPU — no GPU, no endpoint, no hop on the poll path. Fails open: without it, advisories arrive unscored and log clipping is positional.
  • Signed, versioned releases — Ed25519 over the manifest digest, CycloneDX SBOM from the lockfile.

467 vitest and 52 pytest.

Technologies used

Google Agent Development Kit (custom BaseAgents + LlmAgents, InMemoryRunner, FunctionTool, BasePlugin narration, AgentCardBuilder, memory services) · Gemini 3.5 Flash on Vertex AI · EmbeddingGemma-300M (int4 ONNX, on-CPU) · Vertex AI Agent Engine Memory Bank · Model Armor · GKE · Cloud Run · Cloud Storage · Cloud Logging & Monitoring · Cloudflare workerd · TypeScript + Cordis · Python/FastAPI · React with server-sent events.

Other data sources used

No external datasets, no training data. Everything the agent reasons over is read live, through the broker, with the agent's own identity:

  • GKE cluster state — pod topology and deployment state, cluster CA pinned.
  • Cloud Logging — recent container log entries.
  • Cloud Monitoring — container CPU and memory time series.
  • Vertex AI Memory Bank — the agent's own prior incidents, lessons, decisions and known-benign patterns.
  • The pack's manifests — parsed and validated live.
  • The fleet — Google's Online Boutique, deployed unmodified.

Findings and learnings

  • Moving the agent out made the product honest. With the loop inside the platform, every governance claim was really our code calling itself. Externalizing it forced the two arrows, and the fence stopped being decorative.
  • ADK's plugin hooks were the right seam. Seventy-two lines turn before_model / before_tool / after_tool into events a user watches — and because the plugin only observes, narration can never change what the agents do.
  • A human's "no" has to be terminal on both sides of the fence. The broker drops a rejected intent, so the agent read the missing digest as a transient denial and would have re-proposed the same fix every poll, forever.
  • A guardrail needs two failure policies. A filter match failing closed is the guardrail working. An API error failing closed is a guardrail halting production because it is sick.
  • A small model should score, not decide. EmbeddingGemma first suppressed re-runs of a finding a human had declined — and silenced a genuinely changed one, which a test caught. It now scores advisories and ranks evidence, and decides nothing.
  • Two smaller ones. Identity needs a lifetime: a one-hour default failed the agent closed after an hour, correctly and uselessly. And ADK resolves an instruction once per invocation, so every model step needs its own session or they all reason over the first one's evidence.

What's next

Many packs in one shell. A host mounts one pack today. The router already namespaces routes by group and capabilities are scoped per pack, so the work is a pack registry in the shell and a mount loop — after which one deployment fronts every agentic workflow a department runs, rather than one deployment each.

Packs that bring their own design system. The manifest already validates a theme.tokens slot; nothing consumes it yet. Honoring it lets a pack carry a team's brand into the shared shell without forking it.

User-authenticated approvals — a signed-in principal carried across the agent hop and bound into the digest, so an approval commits to the payload and the approver.

More app packs. One pack ships today and two more are scaffolded, neither of them watching a fleet. Each brings its own detection rules, action vocabulary and views; the fence, the identity model and the run stream come from the platform.

An app pack registry — verify a signed release before install, and put the capabilities a pack asks for in front of a human. The same fence, pointed at installing software.

Built With

Share this project:

Updates