ARP
Agent Runtime Protection. Observe and enforce security policy on an AI agent while it runs.
ARP watches what an agent actually does at execution time: the processes it spawns, the hosts it connects to, the files it touches, and the prompts, MCP tool calls, and agent-to-agent messages that pass through it. Events are scored by three detection layers and written to a local event log. Rules you define decide whether a matching event is logged, alerted on, paused, or killed.
What ARP requires
ARP runs standalone. It does not require an OpenA2A account, a registry, or a login:
- No AIM server and no credentials. Nothing is registered and nothing is verified remotely.
- Events are written to a local file, and both OpenA2A telemetry channels are off unless you turn them on. See Telemetry.
- It is not fully offline by default: the third detection layer calls a model API when one is configured. See What leaves the machine.
- Node.js 18 or later.
The implementation ships inside the AIM agent-side SDK, so @opena2a/aim-sdk appears in your dependency tree. That is a packaging fact, not a runtime dependency on AIM. See Where ARP fits.
Install
Use ARP as a library inside your agent process, or drive it from the CLI. The library path is the one to prefer: interceptors hook the Node module system in-process, so they only see what the process they are loaded into does.
npm install arp-guardnpm install @opena2a/aim-sdknpm install -g opena2a hackmyagentQuick start: library
Start ARP inside the process you want protected, before the agent does its work.
import { AgentRuntimeProtection } from 'arp-guard';
// or: import { AgentRuntimeProtection } from '@opena2a/aim-sdk/arp';
const arp = new AgentRuntimeProtection({
agentName: 'support-agent',
interceptors: {
process: { enabled: true },
network: { enabled: true, allowedHosts: ['api.example.com'] },
},
aiLayer: {
prompt: { enabled: true },
},
});
await arp.start();
// ... your agent runs here; ARP observes in the background ...
await arp.stop();The constructor also accepts a path to a config file: new AgentRuntimeProtection('./arp.yaml'). With no argument it loads the first config it finds (see Configuration).
Pattern scanning is also available on its own, without starting a monitor:
import { scanText, ALL_PATTERNS } from 'arp-guard';
const result = scanText(untrustedInput, ALL_PATTERNS);
if (result.detected) {
console.log(result.matches.map((m) => m.pattern.id));
}Quick start: CLI
# Generate arp.yaml from the detected project type opena2a runtime init # Start monitoring in the foreground; Ctrl+C stops it opena2a runtime start # In another shell: show config, monitors, event count, budget opena2a runtime status # Read the last 50 events from the event log opena2a runtime tail --count 50
runtime start holds the foreground and stops on Ctrl+C. There is no runtime stop subcommand.
What ARP watches
Detection is organised in three groups. They differ in how they observe, not just in what they observe, and they have different defaults.
| Group | How it observes | Default |
|---|---|---|
monitors | Polls system state on an interval. Sees the whole machine, and can miss anything that starts and finishes between two samples. | On |
interceptors | Hooks the Node module in-process. No sampling gap, but only sees the process ARP is loaded into. | Off |
aiLayer | Scanners for prompt, MCP, and A2A payloads. Config arms them; your code calls them. Nothing is hooked automatically. | Off |
Two consequences worth knowing before you enable anything. Interceptors patch the Node module registry of the process ARP is started in, so through the CLI they would only cover the CLI process, not your agent: to get them, start ARP as a library inside the agent. And the AI-layer scanners expose methods rather than hooks, so enabling them in arp.yaml alone produces no events. See Using the AI-layer scanners.
| Key | Detects |
|---|---|
monitors.process | Spawned processes, shell escapes, unexpected executables. |
monitors.network | Outbound connections; hosts outside allowedHosts. |
monitors.filesystem | Creates, modifies, and deletes under watchPaths. |
interceptors.process | child_process calls, at the call site. |
interceptors.network | net.Socket connections, at the call site. |
interceptors.filesystem | fs operations, at the call site. |
aiLayer.prompt | Prompt injection, jailbreak, and data-leak patterns. |
aiLayer.mcp | MCP parameter injection, path traversal, SSRF; tools outside allowedTools. |
aiLayer.a2a | Agent identity spoofing and delegation abuse; senders outside trustedAgents. |
A skill capability monitor is also published as a library export (SkillCapabilityMonitor), which compares a skill's runtime behavior against its declared capabilities. It is wired up directly in code rather than through arp.yaml.
Using the AI-layer scanners
aiLayer entries arm a scanner; they do not intercept traffic on their own. Build the scanner against the running event engine and call it at the point your agent handles untrusted content. A detection emits an ARP event, so it flows through the same rules, log, and enforcement path as everything else.
import { AgentRuntimeProtection, PromptInterceptor } from 'arp-guard';
const arp = new AgentRuntimeProtection({ agentName: 'support-agent' });
await arp.start();
const prompt = new PromptInterceptor(arp.getEngine());
await prompt.start();
const result = prompt.scanInput(userMessage);
if (result.detected) {
// e.g. ['PI-001', 'DE-001']
console.log(result.matches.map((m) => m.pattern.id));
}The other two follow the same shape: MCPProtocolInterceptor.scanToolCall(toolName, args) and A2AProtocolInterceptor.scanMessage(from, to, content). PromptInterceptor also exposes scanOutput for model responses.
Configuration
ARP loads the first file it finds, in this order: arp.yaml, arp.yml, arp.json, then the same three under .opena2a/. Pass --config <path> to override. YAML parsing requires js-yaml. With no config file, ARP starts with the defaults below.
# arp.yaml
agentName: support-agent
agentDescription: Handles customer tickets with tool access
dataDir: .opena2a/arp
# Polling monitors. Enabled unless set to false.
monitors:
process: { enabled: true, intervalMs: 5000 }
network: { enabled: true, intervalMs: 10000, allowedHosts: ['api.example.com'] }
filesystem: { enabled: true, watchPaths: ['./src'], allowedPaths: ['./data'] }
# In-process interceptors. Disabled unless enabled is true.
interceptors:
process: { enabled: true }
network: { enabled: true, allowedHosts: ['api.example.com'] }
filesystem: { enabled: true, allowedPaths: ['./data'] }
# AI-layer scanners. Armed here, invoked from your code (see below).
aiLayer:
prompt: { enabled: true }
mcp: { enabled: true, allowedTools: ['search', 'read_file'] }
a2a: { enabled: true, trustedAgents: ['billing-agent'] }
# Detection layers (see below)
intelligence:
enabled: true
budgetUsd: 5.0
minSeverityForLlm: medium
# What to do about a matching event
rules:
- name: Kill on high-severity filesystem access
condition:
source: filesystem
minSeverity: high
action: killEach block takes the exact shape shown. interceptors and aiLayer entries are objects with an enabled field, not bare booleans, and the AI-layer keys are prompt, mcp, and a2a.
Rules and enforcement
There is no global enforcement mode. Enforcement is per rule: each entry in rules pairs a condition with one action.
Omitting rules is not passive. ARP falls back to a built-in rule set, and one of those rules is kill on any event with category threat at critical severity. The rest raise alerts: high-severity violations, a burst of ten anomalies in sixty seconds, and the AI-layer detections. Write rules: [] to run in pure observation mode.
Enforcement is synchronous and fails closed: when a rule matches, its action runs immediately. requireLlmConfirmation: true does not hold the action back. It tags the event for an L2 assessment that happens afterwards and can raise the severity of what is recorded. Treat it as review after the fact, not as an approval gate.
| Action | Effect |
|---|---|
log | Record the event. No intervention. |
alert | Record and raise an alert. |
pause | Suspend the agent process pending review. |
kill | Terminate the agent process. |
A condition can match on source (the monitor that produced the event), category, minSeverity, a fieldMatch map, or a threshold of count events within windowMs.
Detection layers
Every event passes through the cheap layers first. Only ambiguous events reach the paid one.
| Layer | Method | Cost |
|---|---|---|
| L0 rules | Pattern matching on every event. | Free, local |
| L1 statistical | Z-score anomaly detection against the observed baseline. | Free, local |
| L2 model-assisted | Short prompts for events L0 and L1 cannot resolve. | Budget-controlled |
L2 is bounded by intelligence: budgetUsd (default 5.0), maxTokensPerCall (300), maxCallsPerHour (20), and minSeverityForLlm (medium). Set intelligence.enabled: false to run on L0 and L1 alone, with no external calls.
Events
Events are appended as JSON Lines to events.jsonl inside dataDir, which defaults to .opena2a/arp/ in the working directory. Each line carries a timestamp, the monitor that produced it, a severity, and event-specific metadata. Read them with opena2a runtime tail, or consume the file directly.
opena2a runtime tail --count 50 opena2a runtime status --json
What leaves the machine
Two separate things can produce outbound traffic, and only one of them is off by default.
| Channel | Default | Turn it off with |
|---|---|---|
| L2 model calls | On, when a model is reachable. | intelligence.enabled: false |
| OpenA2A telemetry | Off. | Already off; stays off. |
L2 model calls. intelligence.enabled defaults to true and the default adapter picks a destination from the environment: ANTHROPIC_API_KEY if it is set, otherwise OPENAI_API_KEY, otherwise a local Ollama at localhost:11434. So on a workstation that already exports a model key, events at or above minSeverityForLlm (default medium) are sent to that vendor's API, inside the configured budget. The request carries the agent name, its description and declared capabilities if you set them, and the event itself, which for a process event includes the command line and for a prompt event includes the scanned content. Set intelligence.enabled: false to run on L0 and L1 only, with no outbound calls, or pin intelligence.adapter: ollama to keep inference local.
Telemetry
The two OpenA2A reporting channels are separate from the above and both opt-in: structural signature telemetry (signatureTelemetry.enabled: true, or AIM_TELEMETRY=1) and the threat intelligence network (gtin.enabled: true). Setting signatureTelemetry.enabled: false opts out of every OpenA2A channel and overrides any opt-in.
CLI reference
| Command | Description |
|---|---|
opena2a runtime init | Generate arp.yaml from the detected project type. --force overwrites an existing file. |
opena2a runtime start | Start monitoring in the foreground. Ctrl+C stops it. |
opena2a runtime status | Show the config in use, active monitors, event count, and budget. |
opena2a runtime tail | Print the last N events. --count <n>, default 20. |
All four take --dir <path> and the global --json flag for machine-readable output. --config <path> is read by runtime start; the other three locate the config themselves, by the same search order described above.
Where ARP fits
The split across the OpenA2A tools is by time, not by feature:
| Tool | When it runs | What it reads |
|---|---|---|
| HackMyAgent | Before deploy, in CI | Config and code at rest. |
| ARP | While the agent runs | Behavior at execution time. |
ARP was originally distributed inside HackMyAgent and now lives in the AIM agent-side SDK, which is what arp-guard and hackmyagent/arp both re-export. Scan with HackMyAgent; protect at runtime with ARP. Neither requires the other, and neither requires an AIM account.