# Reply on cluster binding: E accepted, with one amendment

Written 2026-08-03, responding to the proposal of option E (target declared in the template
definition) against `DESIGN-NOTE-cluster-binding.md`.

Short version: E is better than the B I recommended, for the two reasons given, and both of them
are things I got wrong rather than differences of taste. I have one objection that the proposal
does not cover, and it leads to a small amendment rather than a different option. I also checked
the fact left open as question 1, and the answer is more useful than expected.

## Conceded

**The B-over-A argument depended on D, and I listed D as unverified in the same document.** My
case for B was that B and D share a tool surface, so B is a step toward D while A is a side road.
That argument is worth nothing if D is not reachable, and I wrote "I have not verified that D is
buildable" four paragraphs later without noticing the two were connected. That is a straightforward
error and the proposal is right to lead with it.

**The credential-scope objection to D is correct and I never priced it.** D requires the server to
mint SQL users for whatever cluster arrives at connect time, so it holds management scope over an
open set for its lifetime. The current templates deployment holds one fixed SQL user and provably
cannot reach anything else. I called D "the product answer" while proposing to trade away the
strongest security property this deployment has, in a product whose entire pitch to a reviewer is
that the agent is confined. That was a bad trade and I did not see it as a trade at all.

The separation of `--cluster-allow` into boundary-only, with the destination coming from the
template, is also right. My option A used an allowlist as an implicit target, which overloads a
security flag with a routing job.

## Question 1 answered: per-target credential resolution already exists, with one catch

`CredentialManager` is already keyed per cluster, not per process:

```
/** key = `${clusterId}::${role}` -> cached credential */
readonly #cache = new Map<string, CacheEntry>()
```

It mints, caches, sweeps and cleans up per cluster on demand. So for a deployment using minted
credentials, E's "resolve a credential per declared target" is close to free.

The catch is the bring-your-own path, which is what the current templates deployment uses:

```js
async getCredential(clusterId, access) {
  if (this.#byo) return this.#byo
```

With a fixed SQL user the cluster id is ignored entirely and every target gets the same
credential. So under `byoUser`, a template file declaring two clusters would not fail at boot the
way E promises. It would resolve "successfully" for both and then fail at query time, when the
second cluster refuses a user that does not exist on it.

That does not block E. It means E's fail-fast property holds for minted credentials and needs
either a per-target BYO map or an explicit boot-time refusal (BYO plus more than one distinct
target is a configuration error) to hold everywhere. For our deployment, with one target, neither
is needed today.

## The objection: the template file stops being portable, and two transports read it

The same template file is loaded by both transports. This is deliberate and the code says so:

```js
// The same file MCP loads, so a template defined once is published on both transports and the
// two cannot describe different SQL.
const restTemplates = baseConfig.templatesPath ? loadTemplates(baseConfig.templatesPath) : []
```

The REST transport binds its target through deployment configuration (host, port, database) and
never looks at a cluster id. So a required `cluster:` field in the template definition creates a
field that one transport obeys and the other ignores.

This matters more than it looks, because the original note's argument for changing anything was
that two transports over one definition were disagreeing about whether the target is part of the
request. E removes that disagreement from the tool surface and reintroduces it in the file format.
A field only one reader honours is the kind of thing that rots: someone copies the file to another
deployment, the cluster id travels with it, MCP quietly targets the old cluster and REST quietly
targets the new one, and both look fine.

## Amendment: make `cluster` optional, resolved from deployment configuration when absent

Everything else in E stands. The change is:

- `cluster` is an optional field on a template definition.
- When absent, the server resolves the target from deployment configuration exactly as B would,
  and refuses to start if it cannot.
- When present, it is validated against `--cluster-allow` at boot and used as E describes.
- `clusterId` leaves the model-facing schema either way, which is the part that actually fixes the
  bug.

This keeps the file portable in the common single-target case, which is every deployment we have
and probably most we will have. It also means our own file needs no edit: two transports keep
reading one file and neither ignores a field the other obeys. The field earns its place when a
deployment genuinely spans clusters, and until then it is absent rather than redundant.

The cost is that "where does this template run" then has two possible answers depending on whether
the field is set. I think that is acceptable because the answer is unambiguous per deployment and
discoverable at boot, but it is a real loss of the single-answer property E was reaching for, and
it is the part of this amendment most worth arguing with.

## The remaining questions

**2, mismatch between a supplied `clusterId` and a declared target.** Error, agreed. Since the
field is out of the schema, only a code caller can produce a mismatch, and a code caller passing a
disagreeing value has a bug worth surfacing.

**3, database in the target or in the SQL.** Leave it in the SQL, agreed. Lifting it out creates
two sources of truth for one fact, and the SQL is the one that cannot be wrong.

**4, a target that becomes unresolvable after boot.** Fail the tools bound to it, not the server.
Killing the process takes down healthy targets to punish a sick one, and on a single-target
deployment the two are identical anyway, so the difference only ever costs something in the case
where it matters.

**5, size of the change.** Small for us. Credential resolution is already per cluster, the target
set is one, and the tool surface change is the removal of a schema field. The work that is not
small is multi-target under BYO credentials, which we do not need and should probably refuse at
boot rather than implement.

## Where this reply is weakest

**The portability objection assumes the file gets copied.** If template files are always generated
or always deployment-specific, the objection is theoretical and the amendment buys nothing at the
cost of an optional field with two behaviours. I do not have evidence about how these files will
actually be managed, and if the answer is "always per deployment", E as originally written is
cleaner than my amendment.

**I am proposing optionality right after being told my last position was too weak.** Option A was
rejected for keeping a field visible when the fix was to remove it, and there is a version of this
amendment that is the same mistake in a different place: keeping a field configurable when the fix
is to decide. The difference I would claim is that A's optionality was in the model-facing
contract and this one is in operator configuration, where two behaviours are normal. That
distinction is doing a lot of work and it is fair to push on it.

**Two of my three concessions were logic errors rather than judgement calls**, which is a reason
to weight this reply's remaining judgement calls lower than its checked facts. The credential
manager finding is verified. The portability objection is an argument.
