agentwave is an embeddable agent assistant for CRM systems, admin panels, dashboards and internal tools. It turns a short request and the current screen route into one validated suggestion. The suggestion proposes exactly one action. The assistant never performs the action. Your application receives the suggestion and stays in control.
The module has zero external dependencies and contains three packages.
| Package | Purpose |
|---|---|
agentwave |
Policy engine and bounded job store. |
server |
HTTP handler and private Unix socket listener. |
codex |
Generation backend that runs the OpenAI Codex CLI (codex exec). |
- An admin panel shows an assistant that suggests the next action for the current page.
- A CRM contact list asks for the best next step. The assistant returns a
lookupsuggestion with a search term. - A dashboard explains its own features when a user asks for help.
- An internal tool routes a question to the correct module.
The application decides what to do with each suggestion. You can show it, log it, or apply it after your own permission checks.
browser or application
|
v
your backend
|
v
private Unix socket /run/agentwave/agentwave.sock
|
v
Codex CLI (read-only sandbox, all tools disabled)
|
v
model
The service listens only on a Unix socket. A process can connect only when it has access to the socket file. Your backend is the only client. The Codex CLI runs in a read-only sandbox with a fixed environment and every tool feature disabled. The model receives the request and route as untrusted data. Every result must match the policy. A result outside the policy becomes a failed job.
Create a policy file. The example below lists three actions and five modules.
{
"actions": ["help", "navigate", "lookup"],
"modules": ["none", "home", "contacts", "deals", "reports"],
"routePrefix": "/admin",
"queryActions": {
"lookup": "contacts"
}
}Build the binary and run it. The socket path must be absolute.
go build -o /tmp/agentwave ./cmd/agentwave
/tmp/agentwave \
--socket /run/agentwave/agentwave.sock \
--policy /etc/agentwave/policy.json \
--model gpt-5.6-solThe command agentwave --version prints the version and exits. The flags are
--socket (required), --policy (required), --model (required), --codex
(default codex) and --home (optional).
Then call the routes:
curl --unix-socket /run/agentwave/agentwave.sock http://localhost/healthz
curl --unix-socket /run/agentwave/agentwave.sock \
-H 'Content-Type: application/json' \
-H 'X-Actor-Id: 11111111-1111-4111-8111-111111111111' \
-d '{"prompt":"Where do I find the contact Acme?","route":"/admin/contacts"}' \
http://localhost/jobs
curl --unix-socket /run/agentwave/agentwave.sock \
-H 'X-Actor-Id: 11111111-1111-4111-8111-111111111111' \
http://localhost/jobPOST /jobs returns a job in the working status. Poll GET /job until the
status is ready or failed. See HTTP API for the status codes.
You can also read the recent history and follow one job as server-sent events:
curl --unix-socket /run/agentwave/agentwave.sock \
-H 'X-Actor-Id: 11111111-1111-4111-8111-111111111111' \
'http://localhost/jobs/history?limit=10'
curl --no-buffer --unix-socket /run/agentwave/agentwave.sock \
-H 'X-Actor-Id: 11111111-1111-4111-8111-111111111111' \
http://localhost/jobs/JOB_ID/eventsThe binary is a single static file. You can cross-compile it for Linux.
CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o agentwave ./cmd/agentwavePrebuilt images are published for linux/amd64 and linux/arm64 when a v*
tag is pushed:
ghcr.io/m1chlcz/agentwave
m1chl/agentwave
The image tag matches the Git tag (for example v0.1.0). The latest tag
follows stable releases only. Add --policy, --socket and --model like the
local binary.
Build the image yourself and run the container. The policy file is read-only. The socket lives in a named volume.
docker build -t agentwave .
docker run --rm \
--read-only \
--cap-drop ALL \
--security-opt no-new-privileges:true \
--tmpfs /tmp \
-v "$PWD/deploy/policy.json:/etc/agentwave/policy.json:ro" \
-v agentwave-run:/run/agentwave \
agentwaveThe Codex CLI keeps its credentials in CODEX_HOME (/home/agent/.codex).
With a read-only root file system, mount that directory when the model needs
credentials.
deploy/docker-compose.yml sets the same limits. Put your policy at
deploy/policy.json and start the service:
docker compose -f deploy/docker-compose.yml up --builddeploy/agentwave.service runs the binary as the agentwave user with a strict
file system policy.
go build -trimpath -ldflags="-s -w" -o agentwave ./cmd/agentwave
install -m 0755 agentwave /usr/local/bin/agentwave
useradd --system --user-group --home-dir /var/lib/agentwave --shell /usr/sbin/nologin agentwave
install -d -m 0755 /etc/agentwave
install -m 0644 cmd/agentwave/policy.example.json /etc/agentwave/policy.json
install -m 0644 deploy/agentwave.service /etc/systemd/system/agentwave.service
systemctl enable --now agentwaveA browser cannot open a Unix socket. Your backend proxies the routes and adds the actor id. Use one actor id per user or per session. Never accept the actor id from the browser.
The next example is a small Node.js proxy. It forwards /assistant/* to the
socket.
import { createServer, request } from "node:http";
const socketPath = "/run/agentwave/agentwave.sock";
const actorId = "11111111-1111-4111-8111-111111111111";
createServer((incoming, outgoing) => {
const path = "/" + incoming.url.replace(/^\/assistant\/?/, "");
const upstream = request(
{
socketPath,
path,
method: incoming.method,
headers: {
"X-Actor-Id": actorId,
"Content-Type": incoming.headers["content-type"] ?? "application/json",
},
},
(reply) => {
outgoing.writeHead(reply.statusCode, reply.headers);
reply.pipe(outgoing);
},
);
upstream.on("error", () => {
outgoing.writeHead(503).end();
});
incoming.pipe(upstream);
}).listen(3000);The browser calls GET /assistant/healthz, POST /assistant/jobs,
GET /assistant/job, GET /assistant/jobs/history and
GET /assistant/jobs/{id}/events.
| Field | Required | Meaning |
|---|---|---|
actions |
Yes | Allowed action names. Must contain help. |
modules |
Yes | Allowed module names. Must contain none. |
routePrefix |
No | Route scope. The default is /. |
queryActions |
No | Maps an action to the module that a non-empty query requires. |
working |
No | Job message during generation. |
failed |
No | Job message after a failed generation. |
templates |
No | Maps a template name to text with {{name}} placeholders. |
The policy fixes the reserved action rules:
helprequires an empty query.navigaterequires a module other thannoneand an empty query.- Each action in
queryActionsrequires its configured module and a non-empty query.
The service rejects a suggestion outside the policy. The model cannot invent a new action or module.
Templates only shape the prompt text. {{name}} placeholders use lowercase
names, every placeholder must be provided exactly once through variables, and
unused variables are rejected. A template cannot set actions, modules or
queries, and it cannot inject commands: it becomes ordinary prompt text before
validation.
| Route | Method | Purpose | Responses |
|---|---|---|---|
/healthz |
GET |
Liveness check. | 200 |
/metrics |
GET |
Prometheus metrics for operations. | 200, 405 |
/jobs |
POST |
Accept one prompt or template request. | 202, 401, 409, 415, 422, 503 |
/job |
GET |
Return the current job of the actor. | 200, 204, 401, 405, 422 |
/jobs/history |
GET |
Return completed jobs of the actor, most recent first. | 200, 401, 405, 422 |
/jobs/{id}/events |
GET |
Stream the state of one job as server-sent events. | 200, 401, 404, 405, 422 |
Every route except /healthz and /metrics requires exactly one X-Actor-Id
header with a lowercase UUID. The body of a POST /jobs request is
{"prompt":"...","route":"..."} or
{"template":"...","variables":{"name":"..."},"route":"..."} with
Content-Type: application/json. The service marks each response as
Cache-Control: no-store.
GET /jobs/history returns a JSON array of the actor's completed jobs, most
recent first. Each entry carries the same fields as a job response plus
finishedAt, the time when the job reached a terminal status. The history
never contains jobs of other actors and never exposes the actor id.
The optional limit query parameter selects 1 to 100 entries and defaults to
20; any other query is rejected with 422. The store keeps at most 64
completed jobs and drops the oldest entry first. An embedded store changes the
capacity with agentwave.NewStoreWithOptions and
Options.HistoryCapacity. History entries stay visible after the live entry
expires.
GET /jobs/{id}/events streams the job as server-sent events. The stream
starts with the current state and then reports every transition. The event
name is the job status (working, ready or failed), and the data line is
the same JSON object that GET /job returns. The stream closes when the job
reaches a terminal status or when the client disconnects. A comment line
: keep-alive is sent every 15 seconds.
Every subscriber has a small bounded buffer. A slow reader is dropped instead
of blocking the generation worker. With once=1 the endpoint writes the
current state as a single event and closes the stream. A job id without a live
entry yields 404.
Send SIGHUP to reload the policy file without a restart. The store and the
runner validate the new policy first; on a parse or validation error the
previous policy stays active and the error is logged. Running generations
finish under the policy they started with.
GET /metrics returns Prometheus text exposition format: job counters
(started, completed, failed), in-flight generations, live entries, response
counts by status class, build info and uptime. The endpoint needs no actor
header because it exposes counters only, never prompts, routes or suggestions;
the socket permissions still restrict access to the service group.
- The service listens on a private Unix socket with mode
0660. It opens no TCP port. - The Codex CLI runs with a read-only sandbox, an empty tool set and a fixed environment. It cannot change files or run commands.
- The policy constrains every suggestion. A result outside the policy becomes a failed job.
- The service has no access to application data, credentials or mutation endpoints. It stores only the prompts, routes and suggestions that callers send.
- The container and the systemd unit run as a non-root user.
- The store keeps at most one job per actor, at most 32 entries for 15 minutes, and at most two concurrent generations with a 120 second timeout. The history keeps at most 64 completed jobs and returns only the calling actor's entries.
gofmt -l .
go vet ./...
go test -race ./...
go build ./cmd/agentwaveMIT. See LICENSE.