Skip to main content
Applications that run on a host with the Toolkit can send events to AO through the Toolkit, without an HMAC secret and without knowing any AO identifiers. Examples include a health checker opening and closing incidents, a configuration-management run reporting its result, or a deploy job announcing a rollout. The Toolkit is already enrolled, already queues and retries, and already knows which host it is on. It accepts the event locally, attaches it to the host, and delivers it to AO. There are three local entry points. All of them use the same ao.webhook.v1 records as the AO Webhook: For systems that don’t run on a Toolkit host, such as a SaaS CI service, use the AO Webhook instead. Records are identical, so an emitter can switch transports without changing them.

The local socket

The Toolkit listens on a Unix stream socket and speaks plain HTTP/1.1, so curl and every language’s standard HTTP client can use it:
The host name in the URL is ignored. Any value works. The socket is created with mode 0660. Whether a process may connect is an ordinary file-permission decision: add the emitting service’s user to the socket’s group, or run the emitter as root. The Toolkit never opens a TCP listener for local events. A loopback port can’t tell callers apart, and every local process can reach it. A minimal event body:
event.json
With no affected_entities, the event attaches to the Toolkit’s own host. A successful request returns 200:
source is how the Toolkit identified your process (see Who sent it). The Toolkit answers only after the event is written to its on-disk delivery queue. An acknowledged event survives a Toolkit restart or upgrade and is delivered once the Toolkit reconnects.

Toolkit in a container

When the Toolkit itself runs in a container, two things are required:
  1. The host PID namespace (--pid host, pid: host, or hostPID: true). The Toolkit identifies callers by their process ID. Without the host PID namespace it cannot see processes in other containers and rejects them with unidentified_peer (or labels them only by user ID, as your policy allows).
  2. A shared socket directory. Bind-mount the host’s /run/ao-toolkit into the Toolkit container, and the same directory into every emitter container.
The emitter container’s user must be allowed to open the socket (mode 0660): run it as root, or give it the socket group’s GID.

Who sent it

On every connection the Toolkit reads the caller’s process ID and user ID from the kernel and resolves them to the systemd unit or container the process belongs to. It records that on every event as tags the emitter cannot override: Events are also stored with that source, so events from different services never overwrite each other, even if they use the same source_event_id.

Policy: who may send what

Without a policy file, any process that can open the socket may send any namespaced event kind about its own host and that host’s services. To restrict sources, point AO_EVENTS_POLICY at a YAML file. Once a policy file exists, everything it does not allow is denied:
/etc/ao-toolkit/events-policy.yaml
A request with a kind outside the source’s list is rejected with forbidden_kind, and a reference outside its entities with forbidden_entity. Nothing from a rejected request is stored.

Attach events to the host and its services

You never need AO identifiers. Use self and the Toolkit fills in its own host:
Entity kinds (Host, HostService, Service) are matched case-insensitively and stored in that spelling.

Events about other hosts

An orchestrator such as a configuration-management master or a deploy runner reports on other machines that it knows by name. A source whose policy allows any_host can reference a host by any one of these keys:
AO resolves the reference to the one host in your organization with that value and stores the canonical host reference. A reference that matches no host, or several, is kept as sent and listed as unresolved, so you can see it instead of the event silently attaching to nothing.

Updates, closing, and retries

Events are identified by source_event_id within a source on a host. Sending the same ID again updates the event, which makes retries safe:
  • To close an event, send the same source_event_id with closed_at set.
  • AO keeps the version with the latest observed_at, so set observed_at to the time of each update.
The socket exists only while the Toolkit runs. If it is missing (for example during an upgrade), retry with backoff, or write to the spool.

The spool directory

For cron jobs and scripts, drop a file instead of making a request:
  1. Write the ao.webhook.v1 body to <anything>.json.tmp in the spool directory.
  2. Rename it to <anything>.json. Renaming is atomic, so the Toolkit never reads a half-written file.
The Toolkit picks up *.json files, validates them, queues the events, and deletes the files. A file that fails validation moves to spool/rejected/ with a matching .error file explaining why. Files written while the Toolkit is down are processed when it starts. Spooled events are attributed to the file’s owner (uid:<n>). Policy rules can match on uid.

ao-toolkit emit

For shell scripts and configuration-management states, the Toolkit binary sends an event for you. It uses the socket and falls back to the spool when the Toolkit isn’t running:
--entity HostService:<unit> attaches the event to one of the host’s systemd services.

Limits

Each source has its own rate limit (default 50 events per second with a burst of 500, configurable with AO_EVENTS_RATE and AO_EVENTS_BURST). A source over its limit receives rate_limited and should back off. One noisy source never delays other sources or the Toolkit’s own monitoring.

Check the Toolkit

ao-toolkit status shows the host ID, the socket and spool paths, and per-source accepted and rejected counts.

Verify with your AI assistant

If your assistant is connected to the AO MCP server, it can do the whole integration from there. These are discovery tools: ask the assistant to call find_tools if it doesn’t see them.
  1. integration_guide: step-by-step instructions for your task, for example “emit events from a containerized app on a host”.
  2. host_identity: the host’s ID and the exact affected_entities references for it and its services.
  3. validate_event: a dry run that checks your records and resolves every reference against your graph, for example “attaches to Host 847cdd30-…” or “orphaned: no Service with name=checkout”. Nothing is stored.
  4. Send the event.
  5. event_search with an affectedSelector on the host finds it.
This documentation is also available to the assistant through the docs_search and docs_get tools.

Reference

Event record (ao.webhook.v1)

Envelope: {"schema": "ao.webhook.v1", "records": [ … ]}. Ingested record types: event. Recognized but not ingested yet (unsupported_type): graph.attribute_affirm, graph.attribute_remove, graph.edge, graph.edge_remove, graph.node, graph.node_remove, telemetry. Each affected_entities entry: kind must match ^[a-z][a-z0-9_-]*[:.][a-z][a-z0-9_-]+$.

Severity

Set tags.severity. These labels are recognized (case-insensitive); anything else leaves the event without a severity.

Limits

Error codes: AO Webhook

Errors answer {"error": {"code": "…", "message": "…"}}.

Error codes: toolkit socket

Errors answer {"error": {"code": "…", "message": "…"}}.

Host references

Cross-host keys (Local sources whose policy allows any_host. On the webhook, reference a host by host_id (host_identity returns it).): hostname, fqdn, instance_name, toolkit_id. Resolved at ingest to the one host with that value in the organization and stored as {“kind”:“Host”,“identity_tags”:{“host_id”:…}}. Zero or several matches: stored as sent and listed by integration_unresolved_refs (reason no_match / ambiguous).

Provenance tags

The toolkit sets these on every local event: