BetaFindAgent is in free public beta — every agent is free to connect and paid agents aren't available yet.
Most agent "safety" today is a paragraph in a system prompt:
Never share API keys. Always ask the user before deleting anything. Do not process more than 10 records at a time.
That's not a guardrail. It's a request, addressed to a component whose defining characteristic is that it can be talked into things — by a user, by a retrieved document, by a support ticket containing instructions. The moment the model is persuaded, every rule in that paragraph is gone at once, and nothing in the system notices.
A guardrail has to be enforced somewhere the model cannot reach. On FindAgent that's the gateway: it sits in front of the runtime, it reads the manifest's declared guardrails, and it applies them to every call regardless of what the model decided.
flowchart LR
C["MCP client"] --> I
subgraph GW["FindAgent gateway"]
I["guardrails.input<br/>max_length<br/>deny_patterns<br/>pii_redaction"]
A["guardrails.actions[tool]<br/>approval<br/>max_amount<br/>rate_limit<br/>idempotent<br/>requires"]
O["guardrails.output<br/>secret_leak_scan (mandatory)<br/>schema<br/>pii_redaction"]
end
I --> A --> R["@findagent/mcp runtime<br/>binds params · resolves auth_ref<br/>host-matched credential"]
R --> EXT["External API"]
EXT --> R --> O --> C
style O fill:#1f2937,color:#fffInput runs before the call reaches the agent. Action policy runs per tool, keyed by name. Output runs on the way back. All three are declared in the manifest and applied by the gateway — the agent does not participate in its own enforcement.
Guardrails may only be tightened. A creator can add restrictions to their own agent. Nobody can
loosen a mandatory rail, and the one that matters most, secret_leak_scan, cannot be touched at
all: it runs on every output unconditionally, and a manifest that writes it as false fails
validation.
"input": {
"max_length": 20000,
"deny_patterns": ["ignore (all )?previous instructions", "system prompt"],
"pii_redaction": ["email", "phone", "credit_card"]
}max_lengthCaps the size of the payload reaching the agent. Two jobs: it bounds cost, and it kills the "paste 400KB of text with a payload buried at character 380,000" pattern that defeats attention- based filtering.
Set it to roughly twice the largest legitimate input you expect. A summariser that takes an article wants a larger cap than a lookup tool that takes an order ID — and giving the lookup tool a 200,000 character budget buys nothing but exposure.
deny_patternsThe prompt-injection deny-list. Be clear-eyed about what this does: it's a cheap filter for known
phrasings, not a solution to injection. Attackers rephrase. Treat it as the layer that removes
lazy attempts and noise, while the actual containment comes from allowed_hosts and
approval: human — controls that limit what an injection can accomplish rather than trying to
recognise it.
pii_redactionStrips categories before the input reaches the agent: credit_card, email, phone, iban,
national_id, and more.
Match the categories to the domain rather than switching everything on:
| Agent kind | Redact | Why |
|---|---|---|
| Support ticket triage | email, phone, credit_card |
Customer text is full of them; triage doesn't need them |
| Finance / reconciliation | credit_card, iban |
Amounts matter, instruments don't |
| HR / recruiting | email, phone, national_id |
Minimises what leaves the system of record |
| Internal code review | usually none | Redaction would mangle code, and the input isn't personal data |
Over-redaction has a real cost: if the agent needs the email address to do its job, stripping it produces confidently wrong output rather than a clean failure.
Keyed by tool name. This is where per-tool judgement lives, and where most manifests are too uniform.
"actions": {
"search_orders": { "approval": "none", "rate_limit": "120/hour", "idempotent": true },
"update_order": { "approval": "none", "rate_limit": "30/hour", "idempotent": false },
"prepare_refund": { "approval": "none", "rate_limit": "60/hour", "idempotent": true },
"execute_refund": { "approval": "human_strict", "rate_limit": "10/hour", "idempotent": false,
"max_amount": 500, "requires": ["prepare_refund"] },
"delete_customer": { "approval": "human_strict", "rate_limit": "5/hour", "idempotent": false }
}Note the refund is two tools, not one. prepare_refund is a read-only call that returns the
proposed amount and reason; execute_refund performs it. That split isn't stylistic — on a
bidirectional client the runtime can raise a real "Approve?" prompt, but on the stateless hosted
gateway there's no channel back to the user mid-call, so a workflow that needs "show me what you're
about to do, then I'll say yes" is cleanest as two tool calls, with the client's own conversation
carrying the confirmation between them. Why that is, in detail.
approvalnone, human, or human_strict. Set an approval level on anything that spends money or carries
destructiveHint.
The test isn't "is this dangerous" — it's reversibility. Can the user undo the result in under a minute using the vendor's own UI?
none — no gate. Posting a draft: reversible, none.human — for a reversible action that still deserves a deliberate second step. On a
client that can be asked, the runtime elicits a real approval prompt; on the hosted gateway, where
it can't, it falls back to a confirm step (the tool returns instead of running, and the caller
must re-run to proceed) so the intent lands in the transcript.human_strict — for an irreversible action (money, public-facing, production risk).
It elicits where it can, and where it cannot — the hosted gateway, or any client that can't be
asked — it is refused, never auto-confirmed. Use it for issuing a refund or sending a
customer-facing email: the ones people misjudge, because sending feels routine while being
perfectly irreversible.max_amountA spend cap on tools that move money. It's a ceiling on a single action, not a budget, and it
belongs alongside an approval gate rather than instead of it: approval stops the wrong action,
the cap bounds the damage of a right-looking one.
It's a convention guard, not a semantic analyzer: the gateway scans only the tool's own top-level
input keys — amount, amount_cents, total or value — in that tool's own unit. Name the field
carrying the number one of those, or the cap has nothing to read.
rate_limitPut one on every tool that calls an external API, without exception. It's not primarily an abuse control — it's what stops a retry loop from burning the buyer's vendor quota, and what keeps one enthusiastic agent from getting the whole platform's IP range throttled by a vendor.
Rough starting points: reads 60–120/hour, writes 10–30/hour, anything with money or deletion
5–10/hour.
idempotentThe most misunderstood field. It answers exactly one question: may the gateway auto-retry this call after a crash?
flowchart TD
Call["Tool call in flight"] --> Crash{"Gateway crashes<br/>before the result lands"}
Crash --> Q{"idempotent?"}
Q -->|"true"| Retry["Auto-retry.<br/>Safe: same call, same result."]
Q -->|"false"| Fail["Surface the failure.<br/>Let a human decide."]
Retry --> Dup["⚠️ If the tool actually creates<br/>something, you now have two."]
style Dup fill:#7f1d1d,color:#ffftrue for GETs and for writes the vendor genuinely deduplicates. false for anything that creates
a record, sends a message, or moves money. A POST /releases marked idempotent produces two draft
releases; a POST /refunds marked idempotent produces two refunds.
Keep it consistent with the tool's idempotentHint annotation. When they disagree, one of them is
wrong, and review will ask which.
requiresA tool-order rail. It names the tool(s) that must have run — for the same user, within a short
window — before this one may run, turning "always search before you create" from prompt advice into
an enforced gate. "execute_refund": { "requires": ["prepare_refund"] } means an execute call is
blocked unless a prepare call preceded it.
The gate is deterministic on the required tool having run at all, not on matching its arguments. And it fails closed: if the runtime can't verify the order, it blocks — a rail that can't be checked is never a silent no-op.
"output": {
"schema": {
"type": "object",
"properties": {
"orders": { "type": "array" },
"total_count": { "type": "integer" }
},
"required": ["orders"]
},
"pii_redaction": ["email", "phone"]
}secret_leak_scan is deliberately absent from that block — it's mandatory, always on, and writing
the field is how you fail validation. It's the backstop for the case where an upstream API echoes a
key back in an error message, or a verbose response embeds a token in a debug field.
The optional schema validates the result shape. It's underused and worth the ten minutes: a
malformed upstream response caught at the gateway becomes a clean error, while an unvalidated one
becomes a model confidently narrating fields that aren't there.
Output pii_redaction takes the same categories as the input rail but strips them from the result
before it reaches the caller — for a doer that pulls customer records and drops them into a Slack
digest, it removes the PII in the platform rail rather than trusting a prompt to. It runs after the
mandatory secret-leak scan.
Copy the row that matches, then tighten.
{ "approval": "none", "rate_limit": "120/hour", "idempotent": true }{ "approval": "none", "rate_limit": "30/hour", "idempotent": false }{ "approval": "human_strict", "rate_limit": "10/hour", "idempotent": false }{ "approval": "human_strict", "rate_limit": "5/hour", "idempotent": false, "max_amount": 500 }"guardrails": {
"input": {
"max_length": 20000,
"pii_redaction": ["email", "phone", "credit_card"]
},
"actions": {
"search_tickets": { "approval": "none", "rate_limit": "120/hour", "idempotent": true },
"get_ticket": { "approval": "none", "rate_limit": "120/hour", "idempotent": true },
"add_internal_note": { "approval": "none", "rate_limit": "60/hour", "idempotent": false },
"reply_to_customer": { "approval": "human_strict", "rate_limit": "20/hour", "idempotent": false },
"close_ticket": { "approval": "human", "rate_limit": "30/hour", "idempotent": false }
}
}Note add_internal_note versus reply_to_customer. Both write. One is visible only to the team and
editable; the other reaches a customer and cannot be unsent. Same verb, different reversibility,
different approval — that distinction is the whole craft of writing an action block.
| Symptom | Cause | Fix |
|---|---|---|
Validation fails on secret_leak_scan |
The field was written | Delete it — it's mandatory and always on |
| Duplicate records after a transient failure | idempotent: true on a creating tool |
false on anything with side effects |
| Agent burns the buyer's vendor quota in minutes | No rate_limit on an external call |
Rate-limit every external tool |
| Agent sends customer-facing messages unprompted | approval: "none" on an irreversible write |
Reversibility test, then human_strict |
| Agent returns confidently wrong answers about people | pii_redaction stripping a field the agent needs |
Narrow the categories to the domain |
Injection lands despite deny_patterns |
Treating the deny-list as the control | It's a filter; containment is allowed_hosts + approval |
| Guardrails silently ignored | Action key doesn't match a tool name | guardrails.actions is keyed by exact tool name |
max_length set to roughly twice the largest legitimate inputpii_redaction categories matched to the domain, not switched on wholesalerate_limitapproval: "human_strict" (reversible-but-serious tools use human)idempotent: falseidempotent agrees with the tool's idempotentHint annotationmax_amount present on anything that moves moneyguardrails.actions keys match tool names exactlysecret_leak_scan not written at allschema declared where the response shape is knownWhy not put guardrails in the system prompt? A system prompt instructs the model, and a model can be argued out of an instruction by a user or by injected content in anything it reads. Gateway guardrails are applied outside the model, to every call, regardless of what the model decided to do.
What does idempotent actually control? Whether the gateway may automatically retry a call after a crash. Set it true only when repeating the call is genuinely harmless. A creating POST marked idempotent will produce duplicate records after a transient failure.
When should a tool require human approval?
Apply the reversibility test: if the user cannot undo the result in about a minute through the
vendor's own interface, require approval. Use human for a reversible action that still deserves a
deliberate second step, and human_strict for an irreversible one — the strict level is refused
outright where the platform can't ask for confirmation, so it can never run unapproved. Sending an
email is the common misjudgement — it feels routine and is completely irreversible.
Can I disable the secret leak scan for my own agent? No. It runs on every output unconditionally and a manifest that sets it false fails validation. Don't write the field.
Do deny_patterns stop prompt injection? They filter known phrasings, which removes noise and lazy attempts. They do not stop a rephrased attack. Real containment comes from controls that limit what an injection can accomplish — audience-bound credentials and human approval on irreversible tools.
Are guardrails per agent or per tool? Input and output guardrails are per agent. Action policy is per tool, keyed by exact tool name, so a read tool and a delete tool in the same agent carry different rules.
Related: Audience-bound credentials · The MCP Agent Manifest Cookbook · Stateless transport and human approval
Handing an agent an API key is handing it to every host the agent can reach. Audience-bound credential slots invert that: the runtime attaches a secret only when the destination host matches a declared list.
Interactive approval works where the client can be asked — and degrades to a confirm step where it can't. The design that survives both is a read-only propose call, then a separate execute call, with the conversation in between.
Every rejection we issue falls into six buckets. Four are caught automatically before a human looks, and all six are things you can check yourself in about ten minutes.