Threads · 13 / 18 · Sep 7, 2026 · Apache-2.0
The model may supply search terms, never identifiers
If a model can emit an id, a hostile message can decide which record you act on. If it can only emit search terms, the worst an injection achieves is finding nothing — and the version that would let it through does not compile.
With identifiers
A customer writes something hostile into their own conversation. The model reads it and emits an id that came from the message rather than from your database:
forgiveDebtByIdUnchecked("cli_999");
expect(RECORDS[2].balance).toBe("0.00"); // 8,400 forgiven — on a customer of a different company
The id "came from us" in the sense that our own model produced it. It came from the message.
The usual fix is a tenant check inside the tool, and it works:
expect(forgiveDebtById("cli_999", "acme").ok).toBe(false);
It is also a rule that has to be remembered in every tool, forever, by everyone — and the day somebody writes the next one and forgets, the failure is the first block again.
With search terms
const refs = resolve("Carla Ruiz", "acme"); // the model asks for her by name
expect(refs).toHaveLength(0);
The scope lives where the search is written, once, instead of being checked where it is used, every time. The model can ask for anything it likes; it is asking inside its own tenant.
And it still does the job:
const refs = resolve("ana", "acme");
forgiveDebt(refs[0]); // { ok: true, who: "Ana Pérez" }
Ambiguity comes back as a list, which is a better answer than a guess:
expect(resolve("a", "acme").length).toBeGreaterThan(1); // two matches: ask
A tool that takes an id has no ambiguity to report, so it never asks. It resolves to exactly one thing, always, and nobody sees which one.
The compiler refuses the shortcut
const fromTheModel = "cli_999";
forgiveDebt({ id: fromTheModel }); // ← does not compile
expect(r.exitCode).not.toBe(0);
expect(out).toContain("ClientRef");
ClientRef is opaque: it carries a property whose key is a unique symbol that is not exported, so there
is no literal of that type. The only way to obtain one is resolve, and resolve is scoped.
That test runs tsc on a fixture, because a property about code that cannot be written has nothing to
execute. And the test next to it compiles the honest version and asserts it passes — otherwise the red above
would only prove that something, somewhere, was broken.
Why this beats remembering
A tenant check is a rule enforced by attention. This is enforced by the shape of the argument: the unsafe call is not a violation, it is a type error, and it appears in the editor of whoever writes the next tool before they have finished the line.
It also degrades well. Get it wrong with identifiers and a stranger's balance is zero; get it wrong with search terms and somebody's assistant says "I could not find that customer".
Where this comes from
The agent in niiko reads messages written by strangers and can act on customer records. Every tool it can call takes terms, filters, or a reference the server built — and none of them takes an id the model produced.
Related: the-wall-is-in-the-data-layer, for the half of this that lives underneath — a wall the prompt cannot reach.
License
Apache-2.0 — see LICENSE. This is a demonstration, not a package. Copy what you need.
Built by Vorluno — a software studio from Panamá.
// next threadTwo SDKs, one schema — and where the drift goes next