> ## Content Index
> Fetch the complete content index at: https://www.adventuresintech.org/llms.txt
> Use this file to discover other available public pages before exploring further.

# What it takes to build a software factory: part 2 - access without trust
- URL: https://www.adventuresintech.org/what-it-takes-to-build-a-software-factory-part-2-access-without-trust/
- Published: 2026-11-12T17:19:15.000Z
- Updated: 2026-11-12T17:26:51.000Z
- Author: Andrew Eacott
- Tags: easel, software-factory

When Miro rolled out AI coding tools to engineers, we moved fast. In [an earlier post](https://miro.com/ai/ai-governance-best-practices/?ref=adventuresintech.org), I wrote about how \~600 people changed their ways of working and adopted daily use of local agents in a little over two weeks. We deliberately chose speed over governance in those first few weeks, because we felt it was essential to embrace AI transformation for the company.

Easel came later, when our security posture was more mature and this time around, we started from a position of secure-by-default. Miro holds SOC 2 and ISO 27001 certification and customer data comes first. As such, least privilege was the design from week one. 

The common assumption is that agents running on infrastructure create security headaches that are difficult to manage. In practice, we found that a managed and hosted agent platform was *far* easier to control and secure than hundreds of agents configured on hundreds of laptops.

![](https://digitalpress.fra1.cdn.digitaloceanspaces.com/cimdb8w/2026/11/02-laptops-vs-platform-1.svg)

## A laptop agent and a hosted agent are different things

An agent on your laptop acts as you. It uses your credentials, your permissions and your network, and for the most part you're watching it. If it does something odd, you see it happen (you're watching it, right ? 😅).

A hosted agent is in a very different position. It acts for many people, often with nobody watching, and it's triggered by text that someone else wrote: a GitHub issue, a Slack message or a comment on a pull request. Any of that text could be wrong, careless or deliberately hostile. Prompt injection isn't a theoretical concern when your agent reads every bug report that lands in a busy channel.

We took the view that a hosted agent's authority cannot come from the person who asked, and it certainly can't come from the prompt. It has to come from somewhere that has been reviewed and version controlled. In Easel, that's the "preset".

A preset is reviewed configuration, stored in code, and it defines what a task needs: the instructions, skills, tools, repositories, network destinations and credentials. A simplified one looks like this:

```yaml
security-fix:
  description: Fix a reported vulnerability and open a draft pull request
  owner: platform-team
  required_skills: [code-changes, gh]
  tools: [shell, github]
  runtime_policy:
    # read the org, write one repo
    github_credential_access: org_read_repo_write 
    sandbox_network:
      
      profile: restricted                           
      direct_exceptions: []
```

When a run starts, the platform resolves the preset into a run specification and freezes it. From that point on, nothing the agent reads can widen what it's allowed to do. A skill can't grant access to a repository. A comment on a pull request can't add a network destination. If a task needs more authority, someone opens a pull request against the preset and someone else reviews it.

## The agent doesn't act as you

One of the earliest decisions we made was that an Easel agent never uses the authority of the person who started it. It uses system accounts, scoped by the preset.

Every run is still *attributed* to whoever launched it, so we always know who asked for what and where from. But everyone gets the same access through the agent. An engineer with admin rights on a repository and a new starter with none get exactly the same agent.

This sounds like a limitation, and in some ways it is. It also made the platform much simpler to build and for a small team, that matters a lot. If agents acted with each user's own permissions, we'd need to keep each user's credentials separate, inside a system running thousands of jobs a week. With system accounts, most of those multi-tenancy problems disappear.

It does leave an obvious question: if everyone gets the same access through the agent, can someone use it to reach something they couldn't reach themselves?

## The data we didn't connect

The answer is that we don't connect sources where per-person access matters.

Snowflake is the clearest example. People regularly ask for it, and technically it would be just another integration. But access to our data warehouse is granted to individuals based on their role and there's no way to give an agent general access without exposing data that not everyone is allowed to see. The team that owns our data platform didn't want agents using it that way, and they were right, so Easel doesn't have it.

The proper fix would be delegated permissions, where agents act with the requester's own rights. This has been on our backlog since the early days of the project - it's fiddly, but not too hard to achieve. However, we never got around to building it because the limitations from shared system accounts were just not that noticeable.

Easel has always grown by demand: when people ask for something and it makes sense, we extend the platform. Most of what makes an agent useful here turned out to be things everyone can already see, like code, CI results, logs, tickets and company context.

## Who gets to see a run

Access to data is one problem but access to *context* is another, and this one caught us out.

When you tag Easel in a Slack thread, the agent gets the whole thread as context. This is what makes it useful in a conversation: it knows what's already been said, and what previous agents have done. It also means a run's record contains everything in that thread.

Shopify faced the same question with River, their Slack-native agent, and chose one direction: ["River only works in the open. No direct messages."](https://shopify.engineering/under-the-river?ref=adventuresintech.org) 

Every conversation is visible to everyone, because learning compounds. As they put it, "One person's hard-won fix becomes the next person's starting point." It's a good argument, and when we made the opposite call, one of our own colleagues made almost exactly the same point.

We went the other way at first. In August we shipped "private runs": anything launched from a DM or a private channel would be visible only to the person who launched it, with an audited break-glass option for operators. Our reasoning was that we can't control where people tag the agent. Someone might use it in a sensitive channel without realising the whole thread would end up in a record that anyone could read.

That decision lasted three days - the problem was obvious as soon as teams used it. A team could work on a Slack thread together in their own private channel, but if one of them started an agent run from it, nobody else on the team could see the run. Collaborating on the conversation but not on the work that arose from it just felt wrong.

Of course, there were ways to fix that awkwardness, but in the end we landed on a compromise. Runs started from a DM are private. Everything else, including runs from private channels, is visible in the platform.

This is much closer to Shopify's position than where we started, with one exception - for things that are genuinely personal.

## Following one git push

Listing guardrails doesn't tell you much about how the system works, so let's try and make it more real. Here's what happens when an agent does something ordinary: pushing a branch to GitHub.

![One path in, one policy, one record. The agent never holds a real credential.](https://digitalpress.fra1.cdn.digitaloceanspaces.com/cimdb8w/2026/11/02-life-of-a-github-action.svg)

One path in, one policy, one record. The agent never holds a real credential.

1. **The token is a placeholder.**  
The agent has a `GH_TOKEN` like any developer would, but its value is a dummy token. If the agent leaked it, or an instruction hidden in a bug report tricked it into sending it somewhere, nothing would be lost.
2. **The connection is captured.**  
`git` connects out over HTTPS. Inside the sandbox, outbound connections are redirected to a small local shim. It reads only the destination hostname, then opens a tunnel to the gateway outside the sandbox and presents the run's own capability. Nothing in the sandbox decrypts traffic, and there's no direct route out. If the gateway can't be reached, the push fails.
3. **The gateway checks the run's policy.**  
Each run has its own capability, which the shim presents on every connection. It tells the gateway which run the connection belongs to, and what that run may do: the repositories it may read or write to, the permitted egress routes and which credentials should be substituted for each etc.  
    
For GitHub, the gateway terminates TLS and inspects the request, so it can see that this is a push to a particular repository.
4. **The real credential is added on the way out.**  
If the push is allowed, the gateway swaps in a short-lived token from our GitHub App and forwards the request. This is also where the platform can do things GitHub's own tokens can't.  
    
A GitHub App token carries one set of permissions across all the repositories it covers, so there's no single token for "read across the organisation but write to this one repository". The gateway can hold multiple credentials instead of just one and can pick the right one for each request. The agent never sees them.
5. **The decision is recorded.**  
The request, the decision and the outcome are stored against the run.

Now suppose the same agent, following an instruction someone hid in a bug report, tries to send what it found to a paste site. The connection is captured in exactly the same way but the gateway finds no matching destination in the run's policy. It refuses that connection attempt and records the denial against the run, so it's there for anyone who looks at what the agent did.

The rest of the design sits around that path:

- **Run specification**  
The preset is frozen when the run starts.  
Retries and follow-ups inherit it rather than re-reading anything.
- **Sandbox**  
A fresh microVM for every run, destroyed afterwards, in a cluster with only mediated paths to internal services.
- **Output**  
Code changes leave as pull requests, still reviewed by a person before anything merges (although, this is also changing for some types of change).
- **Record**  
Every turn and tool call is stored, alongside the network record.

## The cost of isolation

None of this isolation is free, but the cost is maybe not where you'd expect.

The expensive part is every hole you open. Every time an agent genuinely needs to reach something new, we have to design that path carefully, so it does the one job it's meant for without weakening anything else.

We recently shipped a new capability to allow Easel agents to use browsers so they can deliver evidence to users about a UI fix. In order to check a UI fix in our product, an agent needs to log in to a test environment and that means getting a test account from the service that hands them out. 

That service isn't production, but because of how our networks are laid out, the agent simply cannot reach it. Instead we built trusted bridges into our infrastructure so that Easel agents can reach only the internal capabilities they need, and nothing more.

The easy answer would have been to say, "It's an internal environment, just give it network access to the places it needs." Every one of those decisions would have been reasonable on its own. However, taken together, they would slowly erode the isolation that makes the rest of the design trustworthy. 

It makes some features slower to build, and some conversations longer, but I think it's the right trade-off. It's allowed us to deliver something with a lot more value and integration than many folks would have been comfortable with otherwise.

## So, is it safe ?

The honest answer is that it's as safe as we can make it, and safer than the alternative. 

The question I'd ask anyone worried about hosted agents is what they're comparing them against. For us, the comparison isn't a hosted agent against no agent at all. It's one platform with reviewed configuration, scoped accounts, a single network policy and a complete record, against hundreds of laptops each configured slightly differently by the person using them.

My advice is: don't try to make the agent trustworthy. If we've learnt anything from the [recent security disclosures](https://en.wikipedia.org/wiki/OpenAI%5Frogue%5Fagent%5Fbreach%5Fof%5FHugging%5FFace?ref=adventuresintech.org) from frontier labs, it's that agents can be tricked or subverted, and can actively decide to evade protections. 

💡

Instead of putting your effort into preventing rogue agents, make sure they have nothing to give away and nowhere to go.

*Next time:* how we know whether any of this is actually any good.

---

*This is part 2 of a series:*

1. [*Starting with a toy*](https://www.adventuresintech.org/what-it-takes-to-build-a-software-factory/)
2. *Access without trust (this post)*
3. *How do you know it's any good?*
4. *When agents build the agent platform*