Security model

The 16 defence layers that keep one mistake from being catastrophic.

agent_api runs in your live FiveM server's process and can move money in ESX, kick players, run SQL, and rewrite resources. Every part of the design assumes the token might leak; layered defences keep one mistake from being catastrophic.

Defence layers

LayerEnforced byWhat it stops
1 — Tokenx-agent-token header check on every request except GET /healthRandom scanners on :30120
2 — Body limit5 MB cap, returns BODY_TOO_LARGEDenial via oversized POST
3 — Rate limitToken bucket, 120 req/min default per token hash, returns RATE_LIMITEDRunaway agent loops
4 — JSON parseThrows INVALID_INPUT on malformed JSONGarbage payloads
5 — zod schemaPer-tool .strict() schemaUnknown fields, wrong types
6 — Readonly modeagent_api_readonly (defaults to true) blocks every mutating toolProduction lock-down; opt into writes with false
7 — Path sandboxsandbox.ts + per-tool checks.., absolute paths, txData/.env/database/cache segments, writes outside configured roots
8 — Extension allowlistbroad text-file set in sandbox.ts (read shares the write set)Writing/reading .exe, .dll, binary blobs
9 — Verb allowlistparseAllowedCommand for run_commandBanned console verbs (quit, exec, add_ace, rcon_password, …)
10 — ACEFiveM's own ACL on command.*Agent calling lifecycle verbs without server permission
11 — Per-resource lockwithLock(name, fn)Two mutators racing on the same resource
12 — Self-target guardrunLifecycle refuses if resource = agent_apiA known FiveM Mono SIGSEGV
13 — Subject opt-in/agent_test_optin chat command + TTL sessionActing on non-consenting players
14 — Native blocklistsPer-tool blocklists + a built-in danger listDropPlayer, ExecuteCommand, StopResource via reflective natives
15 — Plugin gatesPer-plugin convars (e.g. agent_api_plugin_oxmysql_readonly)Open SQL access by default
16 — Audit logAppend-only dist/audit.log JSONLForensics after the fact

Token handling

  • Storage. dist/.agent_token, mode 0600 on POSIX. Gitignored. It is never listed in the fxmanifest files {} block — doing so would stream it to every client (cfx-nui-agent_api/...); the server reads it from disk via GetResourcePath, so it never needs to be a client file.
  • In transit. x-agent-token header. We do not log the full token anywhere — the audit log records sha256(token).slice(0, 12) only.
  • Rotation. rm dist/.agent_token && restart agent_api. The console prints a new MCP config block with the new token.

Network exposure

  • Default bind: 127.0.0.1. The FiveM HTTP handler does not expose agent_api/* paths externally unless you've opened 30120 to the internet (don't).
  • For remote dev, SSH-tunnel 30120 to your dev machine.

Reflective dispatch — what makes it safe

Reflective tools (server_call_native, client_call_native, esx_call_player, oxlib_call) can in principle call anything. They survive review because of three layered checks:

  1. Built-in danger blocklist (server only) — DropPlayer, ExecuteCommand, etc. always refused regardless of readonly.
  2. Convar blocklist — operator-controlled, applies to every reflective call.
  3. Readonly verb heuristic — when readonly=true, only method names starting with Get/Has/Is/Does/Can/Will/Network pass. Anything else (Set, Add, Delete, Drop, Spawn, …) refused.

These layers compose. Even with readonly=false, DropPlayer is still blocked.

What's intentionally NOT defended

  • A compromised host machine. If an attacker can read dist/.agent_token, they can act as the agent.
  • A malicious server operator. The operator controls every convar. If they turn off rate limit, sandbox, and blocklist, then hand the token to a third party, that's on them.
  • A malicious agent. A misbehaving LLM agent can still spam allowed calls. We have rate limit + audit log, not behavioural detection.

Responsible disclosure

Sandbox bypasses, gate-circumvention paths, or anything that lets a caller escape the readonly/blocklist surfaces should be reported privately to fx.frame009@gmail.com before any public issue. See CONTRIBUTING.md.