In a regulated environment, the question that decides an AI engagement is almost never "how good is the model." It is "where does our data go, and how would you prove it."

Most answers to that question are a paragraph. The paragraph sits in a master services agreement, it uses the word "appropriate" twice, and everyone signs it. It is a promise, and a promise is a fine thing. But a promise does not execute at runtime. Configuration does.

I have been building a delivery-planning agent called Planwright. It reads an engagement's documentation and source code and drafts assessments, decision records, and roadmaps for a human to review. It is the kind of system that is trivially useful and trivially dangerous: to be useful at all, it has to read the client's sensitive material. That makes the boundary a central requirement of the design, and it only counts if it is there before anybody asks for it. Retrofitting one after a security review has already said no is how these projects die.

Here is what I think a defensible boundary looks like, at the three points where it can fail.

Intake: secrets never travel

Before anything is indexed, the corpus is scanned. Credentials, keys, and connection strings that turn up are quarantined; they're never indexed, never sent to any model, and reported back to the client.

That last clause is the one that matters. A scan of a working codebase usually turns something up, and the client should hear it from me rather than from an incident report.

Processing: fail closed, before the run starts

The agent's own instructions tell it not to send sensitive material to third parties. That is worth something, and it is not worth much. Prompt text is a policy. The provider setting is what actually decides where the bytes go.

So the guardrail lives in settings validation, before the process finishes coming up. PW_CORPUS_SENSITIVE defaults to true, which means a corpus is sensitive until a person deliberately says otherwise. Select a hosted embeddings backend for a corpus still marked sensitive and the run hard-fails in that validation pass: the process refuses to start at all. It is the same posture as the cost meter, which aborts a run before it can exceed a USD ceiling instead of reporting the overrun once the money is gone.

Putting the check there rather than at the call site is the entire point. By the time a first hosted call goes out, the corpus has already been read, chunked, and embedded, and there is an index on disk built under the assumption that the run was permitted. A check at that moment stops one request. A check at startup means there is no state in which the agent is running, warm, and one bad flag away from egress.

Selecting a backend that can send data off the machine, for a corpus marked sensitive, raises an error before the run starts. Not a warning. Not a check at the moment of the call. The process refuses to come up.

Every index and cache is derived from the engagement's own directory. There is no shared location, so two engagements cannot contaminate each other. Not "should not." Cannot.

I considered a runtime warning and rejected it, because a warning is easy to ignore and one misconfiguration leaks the corpus. I considered trusting the operator and rejected that too, which is less a design decision than an admission about what the control is for.

The consequence is that where processing happens becomes a contract term instead of an implementation detail. The client picks one of three postures, and each one states its trade-off in plain language, including the unflattering half.

All three share a floor. Local embeddings mean the corpus is indexed and searched on the machine, so the files themselves never move: no bulk upload, no third-party index. What crosses a boundary is narrower. The passages retrieval selects for a given run, wrapped in a prompt, and the drafted artifact that comes back.

That distinction is smaller than "the corpus leaves" and larger than it first sounds. Retrieval selects for relevance, so the passages that transit are the ones most on point, which in a sensitive corpus means the sensitive ones. And over a long engagement the union of everything retrieved across many runs is a real fraction of the whole.

Which is why the JSONL trace is a deliverable rather than a debugging artifact. Every model turn and tool call is recorded locally, so the set of passages that crossed the boundary is not a thing the client has to take on faith or reconstruct from my invoice. It is a file, it is in their workspace, and it is named in the engagement documents before the first run. A reviewer asking which passages moved and to whose infrastructure gets an answer they can read rather than an assurance they have to accept.

Zero-egress local. A local model, nothing leaving the machine. The strongest boundary and the simplest approval story. It also means smaller models, which means some deliverables run under a stricter mandatory review tier and heavier jobs batch overnight. The reasoning and the sequencing are in ADR_006.

Client-managed cloud. Frontier models on Bedrock inside the client's own AWS account. This is the path most regulated clients take: the capability ceiling is the same as hosted SaaS, and the prompts stay in a tenancy they already hold controls, logging, and audit evidence for. It is still egress. It is egress inside a perimeter they already govern.

Hosted SaaS. Anthropic, OpenAI, or Google directly. Fastest to stand up and the least to negotiate. It also sends those passages to a vendor perimeter the client neither controls nor audits, which makes the provider's retention terms and training commitments the whole of the argument — and in a regulated environment that is usually the sentence that ends the conversation.

That is the trade-off, written down before anyone has to discover it.

Exit: the part nobody writes about

Everyone in this field talks about how an engagement starts. Almost nobody publishes what happens when it ends.

Close-out is a command, it enumerates every store the engagement created and, on explicit confirmation, returns them and destroys them. The specifics are the whole thing:

  • Dry run is the default. Destruction requires an explicit confirmation flag.
  • Archive before destroy, so the "returned" half of the promise is a real artifact rather than a gesture.
  • Reserved names are refused outright, so a mistyped argument cannot take out client/references/, the shared reference library that serves every engagement.
  • A symbolic link that escapes the workspace is refused rather than followed.
  • A receipt is written outside the tree that was just destroyed, recording every path, every size, every action, and every warning.

And one more, which is the part I am most confident about. It does not rewrite git history, and the receipt says so. If a contract requires that, it requires a separate, deliberate act by a person who knows what they are doing. A destruction tool that quietly rewrites history is worse than no tool at all.

Where this stops

Configuration is a control, and a control is still not a proof. Everything above assumes my code does what I believe it does. A path-handling bug, a log line that captures more than it should, telemetry left switched on, a dependency that phones home. A startup check prevents none of those, and any one of them would be a fair finding in a real review.

What the boundary buys is that the remaining exposure is defect risk rather than design risk. That is a meaningful downgrade, because defects are found by testing and review while design gaps are found by incident. It is a downgrade and not an exemption. Anyone who tells you their guardrail eliminates the category is selling you the paragraph from the MSA again, with better vocabulary.

Approvable comes first

None of this makes the model better. It does not improve a single draft. What it buys is that the engagement is approvable, and in a regulated environment approvability sits upstream of everything else. Model quality is irrelevant if the security review says no.

So the differentiator is not the model. It is the boundary you can hand to counsel, and the receipt you can hand to an auditor.

The decision records behind all of this are public. They are numbered, they carry the alternatives I rejected and the reasons, and they are the closest thing I have to showing my work. The data-boundary guardrail is ADR_003. The review gate and provenance are ADR_005. Workspace isolation and close-out are ADR_012 and ADR_022. The full log is in the repository.

Planwright on GitHub