Skip to main content

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.

Library, standalone package
npm install arp-guard
Library, direct from the SDK that implements it
npm install @opena2a/aim-sdk
CLI (the runtime subcommands load ARP from hackmyagent)
npm install -g opena2a hackmyagent

Quick 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.

GroupHow it observesDefault
monitorsPolls system state on an interval. Sees the whole machine, and can miss anything that starts and finishes between two samples.On
interceptorsHooks the Node module in-process. No sampling gap, but only sees the process ARP is loaded into.Off
aiLayerScanners 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.

KeyDetects
monitors.processSpawned processes, shell escapes, unexpected executables.
monitors.networkOutbound connections; hosts outside allowedHosts.
monitors.filesystemCreates, modifies, and deletes under watchPaths.
interceptors.processchild_process calls, at the call site.
interceptors.networknet.Socket connections, at the call site.
interceptors.filesystemfs operations, at the call site.
aiLayer.promptPrompt injection, jailbreak, and data-leak patterns.
aiLayer.mcpMCP parameter injection, path traversal, SSRF; tools outside allowedTools.
aiLayer.a2aAgent 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: kill

Each 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.

ActionEffect
logRecord the event. No intervention.
alertRecord and raise an alert.
pauseSuspend the agent process pending review.
killTerminate 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.

LayerMethodCost
L0 rulesPattern matching on every event.Free, local
L1 statisticalZ-score anomaly detection against the observed baseline.Free, local
L2 model-assistedShort 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.

ChannelDefaultTurn it off with
L2 model callsOn, when a model is reachable.intelligence.enabled: false
OpenA2A telemetryOff.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

CommandDescription
opena2a runtime initGenerate arp.yaml from the detected project type. --force overwrites an existing file.
opena2a runtime startStart monitoring in the foreground. Ctrl+C stops it.
opena2a runtime statusShow the config in use, active monitors, event count, and budget.
opena2a runtime tailPrint 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:

ToolWhen it runsWhat it reads
HackMyAgentBefore deploy, in CIConfig and code at rest.
ARPWhile the agent runsBehavior 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.