Skip to content

The Workspace Boundary Your Agent Runtime Was Never Told About

9 min read Fullmakt Team

  • governance
  • data-ownership
  • agentic-actions
  • traceability
  • agents
  • credentials

Ask a platform team how their AI agent handles multiple customers and you’ll usually hear about cost: one pooled connection to the LLM, one warm process per tool integration, one shared cache in front of the slow calls. That’s sound engineering for a stateless API. It’s a much riskier default for an agent, because the thing separating customer A’s data from customer B’s isn’t a network boundary or a process boundary — it’s a business concept, a workspace or tenant ID, that lives in your application’s data model and nowhere in the runtime doing the pooling.

The failure: efficiency built the boundary, not identity

A connection pool keyed by tool name, a response cache keyed by request shape, a warm subprocess reused across the next ten calls — every one of these exists to make an agent cheaper and faster to run, and every one of them is, by construction, indifferent to which customer’s data passed through it last. That’s fine for a pool of database connections to your own single-tenant database. It’s a live hazard for a pool of MCP connections, retrieval caches, or credential lookups sitting in front of other people’s systems, because the pool was never given a workspace ID to key on — only whatever the tool call, the cache key, or the process reuse logic happened to consider a match.

The bleed doesn’t require a bug in the traditional sense. A cache that memoizes a tool result by arguments alone will happily serve customer B the answer computed for customer A if the arguments look the same. A connection pool that reuses an authenticated MCP session across requests will carry that session’s authority into the next call unless something explicitly checks that the next call belongs to the same tenant the session was opened for. Nothing crashes, nothing throws — the response just came from, or went to, the wrong side of a boundary nobody coded into the runtime.

Why this is a data-ownership problem, not a performance decision

The instinct is to treat this as a caching bug: fix the key, ship the patch, move on. That undersells what actually happened. When an agent process serves several workspaces without the runtime layer knowing they’re distinct, “who owns this record” stops being answerable from the transcript alone. A support agent that mixes context between two customers’ tickets, a retrieval agent that surfaces one tenant’s document in another tenant’s answer, an MCP session reused across a delegation chain that spans two workspaces — each of these is a data-ownership violation the moment it happens, not a violation created by the eventual patch. The same GDPR reasoning that applies to cross-border A2A delegation applies here just as directly: an agent handling personal data on behalf of workspace A has no lawful basis to expose it to workspace B, and “our connection pool didn’t know the difference” isn’t a basis anyone accepts after the fact.

It’s also a governance problem that specifically resists the fixes teams already have in place. Access control answers “is this user allowed to open this workspace.” It has nothing to say about a shared execution layer underneath that check — a cache, a pool, a warm process — that was never asked whose workspace it’s currently serving, because access control runs once at the door and the pooling logic runs on every request after that.

Why per-request auth doesn’t catch it

This is the same category of gap this series keeps finding at different layers: a control that’s airtight at the point it was designed for and silent everywhere the runtime does something clever underneath it. A request arrives, the auth middleware confirms the caller belongs to workspace A, the handler runs — and if the handler then reaches into a pool or cache that was populated by a request from workspace B, the auth check that already ran has nothing left to say about it. The cross-user token bleed this series covered before is one specific shape of this same failure; a pooled connection or a memoized tool result is the same shape at a different layer, and just as invisible to a policy engine that only ever sees the request that came in the front door.

Incident review inherits the same blind spot. The access log shows every request authenticated correctly, because it did — the boundary that failed sits below the layer anyone’s monitoring, in code paths built for throughput rather than for the question “whose data is this connection currently allowed to see.”

Where the fix actually has to live

  • Key every cache and pool by tenant, not by request shape. A cache entry, a warm connection, a reusable session — none of them are safe to share across a workspace boundary unless the workspace ID is literally part of what makes two requests “the same” for pooling purposes.
  • Make the workspace ID travel with the request, not just the initial auth check. If a tool call, a retrieval lookup, or an MCP session can outlive the request that opened it, the workspace it was opened for has to outlive it too, checked again at reuse — not assumed from whichever request happened to populate the pool first.
  • Treat a pooled resource that outlives a single request as a privileged object, the same discipline this series has argued for scoped credentials: mint it narrow, attribute it to one tenant, and don’t let convenience turn it into a standing grant nobody re-checks.
  • Log which workspace a resource was populated for and which workspace reused it. An audit trail that shows requests but not which shared resource served them can’t answer the one question that matters after an incident: did this response ever cross a tenant boundary, or did it start and end in the same one.
  • Don’t let “multi-tenant” mean “one process, many customers” by default. Pooling for cost is a legitimate choice; pooling that was never asked to respect a workspace boundary is a cost decision that quietly became a data-ownership decision nobody signed off on.

The business case: workspace as the scoping primitive, not an afterthought

Fullmakt treats the workspace as a first-class boundary in the data model itself, not a filter applied after the fact. Every row that can carry a customer’s data or a customer’s access to a downstream system — Variable (where credential references live), ApiEnvironment, Collection, HistoryEntry, ActivityLog, OAuthAgentClient — carries its own WorkspaceId in src/Fullmakt.Api/Entities. Which workspaces a given user or agent can even see is resolved once, explicitly, by IWorkspaceAccessService.GetAccessibleWorkspaceIdsAsync — every listing and lookup in src/Fullmakt.Api/Endpoints/WorkspaceEndpoints.cs filters through that accessible-ID set rather than trusting a request to name the right workspace on its own.

That matters for exactly the failure this post describes:

  • A credential reference only ever resolves from the workspace’s own stored Variable rows. An agent operating in workspace A doesn’t reach workspace B’s credentials by having its tool call happen to look similar — the reference it resolves comes from the environment attached to the workspace it’s actually scoped to.
  • Every brokered call is logged with the workspace it belongs to. HistoryEntry and ActivityLog both carry WorkspaceId on every row, so an audit trail review can answer “which workspace did this call touch” directly from the log, not by inferring it from which cache happened to be warm.
  • Agent OAuth clients are workspace-scoped at creation, not reassigned at request time. OAuthAgentClient carries its own WorkspaceId, so an agent’s standing identity is tied to one workspace from the moment it’s provisioned — there’s no pooled, workspace-agnostic agent identity for a request to accidentally borrow.

The commercial logic follows the same discipline this series keeps describing at every other layer of the broker: efficiency and isolation aren’t a tradeoff Fullmakt asks a platform team to manage themselves by remembering to key every cache correctly. The workspace boundary is built into what a credential resolves from, what an agent identity is attached to, and what an audit row records — which means the runtime doesn’t need to be told where one customer ends and the next begins, because the boundary was never separated from the data in the first place.

FAQ

Isn’t this just a caching bug, fixable by adding a tenant ID to the cache key? That fixes the specific cache. It doesn’t fix the pattern, because the same gap reappears in the next pool, the next warm process, or the next in-memory map someone adds for performance — unless the workspace boundary is part of the data model those structures are built on, not a key someone has to remember to add each time.

Does this only affect large multi-tenant SaaS platforms? No — any deployment where one agent process, one MCP connection, or one retrieval index serves more than one logically separate customer, team, or data owner has the same exposure, whether that separation is enforced by a subscription plan or just an internal convention between teams.

How is this different from the cross-user token-bleed post? That post covered a bearer token surviving in a cache past the session it was issued for. This is the broader version of the same failure — any pooled or cached resource, credential or otherwise, that can outlive the workspace boundary it was created inside.

Does workspace-scoping data solve multi-agent delegation across workspaces too? It solves the common case, where an agent should never see a workspace it wasn’t provisioned for. Deliberate cross-workspace delegation is a different, narrower problem — the same one this series covered for A2A delegation chains — and needs its own explicit grant, not an accidental one from a shared cache.

What’s the one control that closes this without auditing every pool and cache by hand? Make the workspace ID part of the data every credential, identity, and log row is attached to at creation, not a filter applied at read time. A resource that was never separable from its workspace can’t leak across one just because a cache or pool reused it.

A login handshake proves who’s calling. Access control proves they’re allowed to open the workspace they asked for. Neither one was ever asked what happens to the connection, the cache entry, or the warm process underneath that request after it’s served — and until a platform’s pooling layer is built on the same workspace boundary as its data model, that’s the layer where “multi-tenant” quietly means “shared,” whether anyone signed off on that or not.