Taking it to a real target¶
The lab hands you a paused message. A real engagement does not. Almost all of the difficulty outside the lab is in two places the lab skips entirely: getting in path, and knowing what your result is worth.
Use AIT only against systems you own or are explicitly authorized to assess.
Can you even get in path?¶
Answer this before anything else, because it decides whether the engagement is possible with this tool at all.
AIT is a reverse proxy with an explicit upstream. Someone has to point the sending agent at it. There is no transparent interception through ARP, DNS manipulation, or proxy autoconfiguration. Explicit placement is auditable, and the target's operators can see exactly what was in the path.
The envelope, stated plainly:
| Requirement | Why |
|---|---|
| You can repoint the sender at AIT | Nothing redirects it for you; placement is explicit by design. |
| The sender is reachable from your host, or runs on it | The listener binds loopback unless you give it a routable address. |
| The upstream may speak TLS, including behind a private CA or requiring mTLS | Configure it on the connection; see below. |
A sender on another host¶
Give the connection a routable listen address and the data plane binds it:
ait connections add --protocol a2a_jsonrpc --upstream https://agent.internal/ \
--listen 192.0.2.10:8080
There is no separate confirmation flag. Writing a non-loopback address is the decision, and a second opt-in elsewhere is a flag people copy from a doc without reading. An address that cannot be parsed is treated as remote rather than assumed safe.
The bind takes the one interface you name, not 0.0.0.0. On a client network
that difference is the whole of the exposure, so it is asserted rather than
assumed: tests/test_placement_rehearsal.py binds to this host's outbound
address, drives a sender at it, and checks that loopback still refuses.
The control plane stays on loopback either way: only the data plane moves. It carries the target's traffic, so treat the port as you would any other listener on a client network.
Reaching a TLS upstream¶
Store the trust material on the connection and it is applied when the session starts:
ait connections add --protocol a2a_jsonrpc --upstream https://agent.internal/ \
--tls-ref ca_file=/path/to/internal-ca.pem
Keys are ca_file, client_cert_file, client_key_file, server_name and
insecure_skip_verify. A pinned CA is added to the system roots rather than
replacing them, so an internal host behind a public load balancer still verifies.
Paths are checked when the session starts, not when the connection is saved, so a
bundle that has moved is reported while you can still fix it.
Prefer ca_file to insecure_skip_verify. They answer different questions: one
is "I have a private CA", the other is "I accept an unauthenticated upstream",
and only the first keeps your own traffic verified.
A sender that will only speak HTTPS¶
Supply the certificate the listener should present:
ait connections add --protocol a2a_jsonrpc --upstream https://agent.internal/ \
--tls-ref listen_cert_file=/path/to/seam.crt \
--tls-ref listen_key_file=/path/to/seam.key
AIT does not mint one for you. The sender has to trust what is presented, and only you know what it will trust: your own CA, a certificate the target already trusts, or a client you can configure. Generating one here would produce a listener the sender rejects and a failure that looks like the tool being broken.
The certificate must name the address the sender dials. A cert for
localhost is the one you are likely to already have and is exactly what a
remote sender rejects. It connects to an IP or an internal hostname. That
failure lands on the sender. It refuses to connect, and what you see is a
target that has gone quiet, not a certificate problem. Include the address in
the SAN.
Interception works through the TLS listener exactly as it does in plaintext:
pause, attack arm, close control, and a scored finding. That is rehearsed end to
end in tests/test_placement_rehearsal.py rather than assumed.
For the parts one machine cannot show, such as a real link, host firewall, and a sender you trigger rather than own, see Two-Host Field Rehearsal. Run it before an engagement, not during one.
What is still out of reach¶
Transparent interception. AIT is a reverse proxy with an explicit upstream, and nothing here does ARP, DNS or proxy-autoconfig tricks. If you cannot influence where the sender points, this tool cannot be placed: that is a deliberate design choice, not a gap.
Where placement is most direct: MCP stdio. You replace the server command in the
client's config with the wrapper AIT prints, and you are in path with no network
changes at all. The repository's direct proof for this placement is
tests/test_against_a_third_party_server.py, which drives a
published third-party server this way. For A2A, the equivalent proof is
tests/test_against_a_cross_implementation_agent.py, which intercepts an agent
built on the JavaScript SDK.
ait intercept start --protocol mcp_stdio --command /path/to/server
# then paste the printed wrapper command into the MCP client's config
Scope before you place¶
Write down, before the listener exists:
- the exact locators in scope, and what is explicitly out;
- effects you must not cause: a real payment, a real email, a real deletion;
- who to call when something breaks, because in-path means you can break it;
- retention: transcripts contain full request bodies and, deliberately, credentials.
That last one is not boilerplate. Credential masking was removed so token
forging, stripping, downgrade and relay can be tested. Artifacts are written
0600, and export defaults to --redacted, but the raw capture exists on disk
from the moment you start.
Take a baseline first¶
Put AIT in path with interception off and let normal traffic through.
You are looking for whether your presence alone changed anything: framing, TLS negotiation, streaming behaviour, timeouts. If the inert path already behaves differently, stop and diagnose that. Every finding afterwards is contaminated otherwise, and "the tool broke it" is indistinguishable from "the attack worked".
Aim narrowly¶
A session with no break conditions pauses every decoded message and holds each sender's connection open until you decide. On a live system that is an outage you caused before attacking anything.
ait intercept start --connection CONNECTION_ID --direction request --operation tools/call
ait intercept start --connection CONNECTION_ID \
--where 'json.params.message.parts.0.text:contains:refund' \
--pending-timeout 60 --pending-timeout-action forward_original
Set a pending timeout on anything unattended. A paused message holds a real connection; the timeout decides for you and records that it did.
Retarget the catalogue¶
The shipped primitives carry values pinned to the lab's field names. Against a
real target mcp.tool-argument-escalation will not find /params/arguments/account.
ait intercept attack --target MESSAGE_ID
ait intercept attack mcp.tool-argument-escalation --target MESSAGE_ID \
--set /params/arguments/beneficiary_iban='"DE99OPERATOR"'
intercept attack --target lists what applies and what does not, with the
reason, so a short list tells you which paths are wrong. For anything you will
run more than once, write a primitive into <workspace>/.ait/primitives/. See
Authoring Attack Primitives. Your own primitive keeps
the hypothesis, control and limitation attached to the finding, which a hand edit
does not.
Decide what your oracle is, before you attack¶
The tiers above receiver_processed need something the target does not control.
On an engagement that is usually one of:
- a shadow endpoint you stood up, for anything that redirects;
- a controlled account, mailbox, file or ledger row the action would touch;
- a state snapshot taken either side of the arm.
If you cannot name your oracle before running the attack, you will be writing a
finding that stops at receiver_processed and arguing the rest in prose.
Run the control. Every time.¶
Same field, same visibility, a value that does not cross the boundary. Both arms in the same session, because attribution compares what the receiver did on each one.
Attribution comes back as attributable, not_isolated, no_control, or
undetermined. Report what it says. not_isolated is a real result: the target
reacts to change rather than to authority, and undetermined means you did not
measure, which is not the same as a negative.
Tear down like it matters¶
ait intercept export SESSION_ID --format jsonl --redacted --out session.jsonl
ait intercept evidence --primitive PRIMITIVE_ID --out finding.json
ait intercept stop SESSION_ID --pending forward
ait intercept shadow list
stop asks what to do with anything still paused, because a message dropped at
teardown is a message the target never received and that belongs in the record.
Check shadow list: a shadow left running is an authenticated listener on a
client network after you said you were done.
Scoring survives teardown, so you are not forced to write the finding in the field. Verify the chain when you hand it over:
agentic-redteam/seam/seam transcript verify \
--schema agentic-redteam/schema/transcript.schema.json \
--transcript .ait/runs/RUN_ID/transcript.json
What to expect it to be bad at¶
- Instruction injection. The lab's consumers are deterministic; real ones are not. Treat a single positive as a signal and repeat it.
- Transparent interception. Someone has to repoint the sender. TLS and a remote bind are configuration (see above); influencing routing is not, and no amount of it is coming. This is the ceiling.
- Deciding what to attack. Nothing here does reconnaissance for you.
The rule the tool is built on¶
Agent narration is never evidence. A target saying it did something is not proof it did. The evidence ledger enforces that in code. It determines two tiers from the transcript and refuses to infer the other two, and your report should hold the same line. A finding that claims less than you hoped is doing its job.