Models, tools and agents#

Guard is the one path from your data to any model. It has three properties worth relying on:

  • encode, check, then call. Text is encoded, then checked independently for any value the conversation holds; a leak raises LeakError before your function is called.

  • no network of its own. The guard owns no client, socket or key. You pass the function that talks to the model, so it works with every provider.

  • one vault per guard. A value hidden once is hidden everywhere in that conversation, however it is written.

A gate in front of any client#

from scikitplot.cleanprompt import FluentCleanPrompt

guard = FluentCleanPrompt().profile("strict").guard()
reply = guard.ask(
    "Mail ada@example.com",
    call=lambda safe: "Queued " + safe.split()[-1],
)
reply
# 'Queued ada@example.com'

call receives only the safe text and returns the model’s answer, which the guard decodes. Guard.chat does the same for a list of chat messages, and Guard.aask / Guard.achat are the asynchronous forms. For the lower-level steps, Guard.outgoing encodes and checks one text and Guard.incoming decodes one reply.

Streamed replies#

A placeholder can arrive split across chunks. Guard.stream returns a StreamDecoder whose output, for every possible chunking, equals decoding the whole reply at once:

decoder = guard.stream()
text = "".join(decoder.feed(chunk) for chunk in model_chunks) + decoder.flush()

Tool calls are an exit#

When a model asks to call a tool, its arguments may contain placeholders. Restoring every value into a tool call would hand any tool — or an injected instruction — every value in the conversation. So arguments are decoded only through Guard.decode_tool_arguments, with the kinds that tool may receive:

args = guard.decode_tool_arguments(call.arguments, allow=["EMAIL"])

A call naming a kind it was not allowed is refused. Guard.chat returns tool calls still encoded, so nothing is restored by accident.

Tool results are documents#

A tool’s JSON result is guarded as one document, in the form it will be sent: keys, numbers and strings, with prose inside JSON strings read as prose. Guard.encode_object does that; Guard.decode_object reverses it.

Command-line models#

For a model you run from a terminal, ask performs the same boundary without a shell and keeps the values in memory:

python -m scikitplot.cleanprompt ask \
    --via "ollama run llama3" \
    "Summarize the request from ada@example.com"

The command is run directly (never through a shell), receives the safe text on standard input, and is stopped if CleanPrompt exits early. --show-sent prints what was actually sent; --timeout bounds the wait.

Agents without code: MCP#

For agents that speak the Model Context Protocol, mcp serves guarded file tools over standard input and output, confined to the roots you name:

python -m scikitplot.cleanprompt mcp --root ./project

Whatever a tool returns, the model reads — so no MCP tool returns a removed value. There is deliberately no “decode” tool: reading goes through the gate, and writing restores values on disk, where the model never sees them. Paths are shown relative to a root.

skill prints, or installs with --write, instructions that make an AI coding assistant use the same boundary; plan lets a team pin the packs and rules those integrations are expected to use (Files, records, packs and plans).

Logging without leaking#

Nothing that was removed is ever logged. Log records carry counts, kinds, labels, offsets, durations and capability decisions — what happened, never what was found — and a filter on the whole scikitplot.cleanprompt logger namespace scrubs held values as a second line of defence. Nothing is emitted until you configure logging (--log-level, --log-format, or configure_logging).

What a guard does not do#

A guard keeps the values it detects out of what the model sees. It does not make a model’s instructions safe: it is not a defence against prompt injection, and it does not judge what a model does with the text it is given. See What it protects, and what it does not.