Good Backends Are Agent Backends
You’re already agent-ready
Introduction
Our previous post detailed how our backend was structured to support lightweight Temporal workflows as thin coordination layers (following the Saga pattern). Business logic doesn’t happen inside of workflows, but rather inside RPC endpoints that the workflows interact with.
AI Agents are workflows where the orchestrator happens to be a language model rather than a static script. A backend designed to support versatile lightweight workflows is already a backend designed to support AI agents. Whether those workflows are deterministic or stochastic in nature shouldn’t matter from your backend’s perspective.
Agents as Workflow Orchestrators
If your backend supports Temporal well, it means you have a system that can answer a large number of business questions with well-scoped endpoints that handle the gathering of data end-to-end, and you support chaining those questions together to handle complex business and analytics needs. Doesn’t that sound exactly like an agent workflow?
What does make an AI different than Temporal is trust. Your workflows were written by a human - or at least code reviewed by one. AI are not (yet) accountable or predictable like a human is, so they can’t have the same access scope that your workflows have. Agents represent a novel security threat; a highly capable and potentially hazardous actor that you deploy within your own infrastructure. Your agent will leak everything and break things. Build first with the concept that AI agents are maximally hostile, and add trust from there.
Agents are operating on your developer’s machine, potentially with a developer’s internet access and permissions. You don’t want it firing off an email containing customer’s PII, and you don’t want it deleting production customer records because it thinks they’re causing a bug. Your backend access layer needs to make this structurally impossible. Prompt engineering is not enough.
This is why giving your agent unrestricted access to a tool like https://github.com/fullstorydev/grpcurl is a mistake. Our solution was to deploy an MCP proxy, https://github.com/Basic-Capital/grpcmcp, which we stood up with kubernetes. Many MCPs can be hosted locally on a developer’s machine, but our MCP server is acting both as a standard tool use helper and as access control, so it needs to be hosted in an environment that an agent can’t modify. This proxy reflects a subset of our endpoints that we’ve demarcated as ai-accessible using just a single protobuf field and one protobuf annotation:
enum AiAccessLevel {
AI_ACCESS_LEVEL_BLOCKED = 0;
AI_ACCESS_LEVEL_ALLOWED = 1; // both internal and customer-facing
AI_ACCESS_LEVEL_INTERNAL_ONLY = 2; // internal tools only
}
extend google.protobuf.MethodOptions {
optional AiAccessLevel ai_access_level = 50003;
}rpc GetDataAboutSomething(...) returns (...) {
option (bcoptions.ai_access_level) = AI_ACCESS_LEVEL_ALLOWED;
}This is the access control surface for AI in our codebase. Any method is BLOCKED by default. This makes it very easy to adjust and interpret access control in-code.
grpcmcp picks up the annotation through --require-method-option, a flag that filters which gRPC methods are exposed as MCP tools by proto method-option tag and value.
Enforcement lives in a single gRPC interceptor, which looks to the X509 certificate used against our MTLS encrypted RPC endpoints to determine the actor type and then enforces the access level as follows:
class AiAccessLevelInterceptor : ServerInterceptor {
override fun <ReqT, RespT> interceptCall(...): Listener<ReqT> {
val level = call.methodDescriptor.schemaDescriptor
?.let { it as? ProtoMethodDescriptorSupplier }
?.methodDescriptor?.options
?.getExtension(CustomOptions.aiAccessLevel)
?: AiAccessLevel.AI_ACCESS_LEVEL_BLOCKED
val isExternalAgent = requestContext?.actorType ==
ActorType.EXTERNAL_AI_AGENT
val allowed = when (level) {
AI_ACCESS_LEVEL_ALLOWED -> true
AI_ACCESS_LEVEL_INTERNAL_ONLY -> !isExternalAgent
else -> false
}
if (!allowed) {
call.close(Status.PERMISSION_DENIED, Metadata())
return NoOpListener()
}
return next.startCall(piiRedactingCall, headers)
}
}Every PII field in our protos carries a debug_redact annotation — a built-in protobuf field option that we dual-purposed as the source of truth for "this field should never leave the wire to an agent". These annotations allow our RPC interceptor to trivially enforce redacting by reading the annotation and stripping the field before the response leaves the interceptor.
message Human {
string uuid = 1;
string given_name = 2 [debug_redact = true];
optional string middle_name = 14 [debug_redact = true];
string surname = 3 [debug_redact = true];
Date date_of_birth = 4 [debug_redact = true];
string email = 5 [debug_redact = true];
optional string phone_number = 6 [debug_redact = true];
}This gave us jarvis, the in-house name for our internal AI assistant. Rather than laboriously chaining together RPC calls by hand to solve production issues, Claude Code (or any other agentic harness) can talk to the jarvis MCP and one-shot diagnose and fix a lot of production problems using natural language.
Agents surface issues when they talk to your backend, and descriptive errors will let the agent know why a call failed or what an alternative endpoint to use would be. Well-named methods are easier for the agent to retrieve from context. All of these principles are table stakes in traditional software engineering, but in our new world they also happen to reduce query time and hallucinations. Agents shouldn’t be an excuse to fire off the slop cannons but an additive reward for good design.
The next step was rolling out a customer-facing version of this tool. A key insight is that there’s not a large distinction between a power user and a debugging developer; both want to chain together business logic to answer novel questions.
This gave us the idea for Vision, the customer-facing version of jarvis. Rather than living in a standalone proxy, Vision lives in our application code, and effectively defines an alternative backend interface to the user. Rather than the traditional approach of building out extensive, bespoke, feature-creeped visualizations for power users, an agent allows our clever customers to answer novel questions that we didn’t predict.
Agents as Frontends
In the same way that jarvis is a stochastic analogue to Temporal, Vision is a stochastic analogue to a user dashboard. It reuses the user’s auth to provide multi-tenancy, structurally preventing data leaks between users.
Vision runs as an in-process gRPC server inside the application backend, so the model’s tool calls flow through the same interceptor chain as production traffic from our user-facing dashboards; auth, PII redaction, request-context propagation, tracing, etc:
private val server: Server =
InProcessServerBuilder.forName(serverName)
.intercept(AiAccessLevelInterceptor())
.intercept(AssistantRequestContextInterceptor(auditDao, env, jwtManager))
.addAuthInterceptors(systemSecurity)
.intercept(GrpcServerExceptionInterceptor(env, includeDebugInfo = true))
.apply { services.forEach { addService(it) } }
.build()Every tool call the model issues is wrapped in call credentials that carry the customer’s real auth token, with the request context tagged as agent-driven.
internal class BearerTokenCredentials(
private val token: String,
private val identity: Identity,
private val userUuid: String,
private val parentContext: RequestContext? = null,
) : CallCredentials() {
override fun applyRequestMetadata(...) {
val requestContext = RequestContext(
actorType = ActorType.EXTERNAL_AI_AGENT,
actor = "Vision",
actingFor = userUuid,
parent = parentContext,
)
val metadata = Metadata()
metadata.put(GrpcMetadataKeys.Authorization, "Bearer $token")
metadata.put(GrpcMetadataKeys.IdentityType, identity.type.name)
metadata.put(GrpcMetadataKeys.IdentityUuid, identity.uuid)
applier.apply(metadata)
}
}Vision was powerful out-of-the-box because it was dogfooding flows that our engineering and operations teams had already been using. Developers are heavily incentivized to fix their own pain, so extending these developer tools to users gives us naturally battle-tested products that are agent-ready.


