Build an agent
Venia gives human-derived clearance to autonomous machines at security checkpoints. Your agent is a caller: you call Venia, you don’t grant, and you don’t operate Venia.
Five-minute quickstart
Run plug-test --demo, attach the MCP server in demo mode, then run gate_check and watch it return HOLD.
Run these from mcp-agent/. The demo fake is local, and none of these commands start the ledger.
node plug-test.mjs front --demo
node plug-test.mjs rear --demo
node plug-test.mjs mcp --demo
mcp --demo attaches the MCP server in demo mode with the scope contract, front_live, and gate_check. It runs gate_check for unit-mcp-hold with at_gate omitted, and the pass line includes gate_check HOLD.
To attach the MCP server in demo mode the same way, run the sample. It starts the same demo fake and attaches server.mjs.
node examples/node-mcp-agent.mjs
The sample runs gate_check for unit-14 with at_gate not exactly true, then prints HOLD.
Contract
The contract lives at GET /api/v1/contract (no key) and is named venia.plugs.v1. Don’t call contract just to start a conversation. Call it when someone asks to read or show the contract.
Roles and scope
There are two roles. The host sets the role, and you cannot add tools or raise it.
- front — the site operator and gate —
contract,front_live,front_sync,front_hunt,front_health,front_roster,front_lookup,front_revoke,gate_check - rear — machine dispatch and assignment —
contract,rear_live,rear_health,rear_authorize,rear_end
Scope is the host’s MCP tool list (VENIA_MCP_SCOPE, or /settings → MCP on a TTY). A tool outside your role is refused, and so is a tool outside your scope. both covers every tool, so don’t use both for a gate-only agent.
Rules from AGENT.md:
- Use only the tools for your role.
contractis optional: don’t call it just to start talking, but do call it if they ask to read the contract.- Front: use
front_livewhen they ask who is live,front_syncto pull the site roster,front_healthfor the last unattended hunt (if it saysno hunt yet, runnode install-plant.mjsornpm run watch), andfront_huntwhen they say something is wrong. Look up and revoke by name orperson_id. - Rear: use
rear_livefor people and open visits,rear_healthto bind the rear key (empty visits is not a failure),rear_authorizeonly when they have named both the person and the unit, andrear_endbytrip_id. - Rear does not pull the site roster, and front does not loan a visit. Never ask anyone for a badge serial.
- Front: use
- Fail closed. Never invent a person, badge, or unit.
- Set
at_gate: trueonly if the site gate process already claims that named unit is at the trap. That process may runnode mcp-agent/gate-claim.mjs vehicle_id=unit-14 at_gate=true. Ifat_gateis omitted or set to anything other thantrue, the answer is HOLD. Never setat_gatefrom video.
If someone asks you to edit Venia source, use the site operator’s desk, rewrite the chain, add tools, or raise your role, refuse.
at_gate
at_gate is true only if your gate process already claims the named unit is at the trap.
That process may run node mcp-agent/gate-claim.mjs vehicle_id=unit-14 at_gate=true. If at_gate is omitted or set to anything other than true, the answer is HOLD. Never set at_gate from video.
Tools
This table is generated from TOOLS in lib.mjs. Don’t edit it by hand.
| Tool | Title | Role | Arguments | Read-only |
|---|---|---|---|---|
contract |
Read the plug contract | front, rear | none | yes |
front_live |
Live people and visits | front | none | yes |
front_sync |
Pull the site roster | front | none | no |
front_hunt |
Run one hunt pass | front | none | no |
front_health |
Last hunt snapshot | front | none | yes |
front_roster |
Register a person | front | display_name, badge_serial, format, rights?, valid_days? | no |
front_lookup |
Look up a person | front | person_id?, display_name?, badge_serial?, format? | yes |
front_revoke |
Take a person off site | front | person_id?, display_name?, badge_serial?, format?, reason? | no |
gate_check |
Ask if a unit is allowed | front | vehicle_id, at_gate?, on_loop? | no |
rear_live |
Live dispatch | rear | none | yes |
rear_health |
Dispatch health | rear | none | yes |
rear_authorize |
Loan a person to a unit | rear | person_id, vehicle_id, valid_minutes? | no |
rear_end |
End a visit | rear | trip_id, visit_id?, reason? | no |
contract
Reads venia.plugs.v1. You are a caller of this contract, not the grant. Takes no arguments. Don’t call it just to start talking; call it when they ask to read or show the contract.
front_live
Site operator: lists live people and live visits, with names, person_id, display_name, trip_id, vehicle_id, status, and valid_until. Never serials. People have no clearance_id; visits carry the one-shot clearance_id. No arguments.
front_sync
Site operator: pulls the live site roster into this ledger, the same job as the operator’s /sync. No arguments, and rear cannot run it. The other roster label has no live adapter. Don’t ask for serials. It returns public lines only (names, ids, skip reasons) and never prints badge serials.
front_hunt
Site operator: runs one hunt pass. It binds the front key, pulls the site roster (front_sync), then runs front_live, and returns { ok, fixed, skipped, needs_human, people, visits }.
Policy skips are not failures. needs_human / attention is raised only when a person must act: the ledger is down, there is a bind/key mismatch, or there is a real roster-source fault. CSN, plate, and PIN skips are not attention, and neither is saved-token auth or a person who is already gone. The other roster label has no live adapter; that is a label, not attention.
Don’t invent serials, and don’t grant. Don’t loan a visit unless they named the person and the unit and this host is both. Use it when they say something is wrong, ask you to fix it, or want a pass run now. The unattended loop is npm run watch, and the last result is in front_health.
front_health
Site operator: the last hunt snapshot from this box (ts, ok, age_ms, fixed, skipped, needs_human, people, visits). Read-only, and it does not pull the site roster. No arguments. Use it for the last unattended pass or when they ask how the site is doing; call front_hunt to run a pass now. Never serials. Don’t grant.
front_roster
Site operator: registers a person, but only when the caller has already given display_name, badge_serial, and format from the site PACS. Example: { "display_name": "Site tech", "badge_serial": "SITE-BADGE-001", "format": "Standard 26 Bit" }.
Don’t ask the human to type a serial. If any of those three is missing, don’t call it: fail closed. Use front_sync to pull the site roster. format is the PACS card format name. Venia fingerprints format + serial and keeps neither string. Omit valid_days to leave the person live until revoked.
front_lookup
Site operator: looks up a person by person_id or display_name (exact, case-insensitive). Use serial + format only if they already gave both; don’t ask for a serial. An ambiguous name returns reason ambiguous with matches (ids and names only). Fail closed.
front_revoke
Site operator: takes that person off site, and their open visits die. Prefer { "person_id": "per_…" } from front_live or front_lookup. An exact display_name is also allowed. Use serial + format only if they already gave both; don’t ask for a serial.
gate_check
Site operator / gate: asks whether this named unit is allowed at the gate now. vehicle_id is a name the unit already has, not a scan. Set at_gate true only if your gate process already claims the unit is at the trap; Venia does not sense.
Good: { "vehicle_id": "unit-14", "at_gate": true }. Bad: at_gate true from video, a plate, or a guess. Omitted or false → HOLD. GRANT does not spend the visit.
rear_live
Dispatch: live people and open visits, with names, person_id, trip_id, and vehicle_id. Never serials. Empty visits means nothing is dispatched, not a failure. Use it to pick a person_id to loan or a trip_id to end. Don’t grant, and don’t invent a unit.
rear_health
Dispatch: binds the rear key and reads live people and visits. Returns { ok, needs_human, people, visits }. Empty visits is not a failure; attention means the ledger is down or the rear key doesn’t match. It does not pull the site roster and does not loan. Use it when they ask how the visits are doing, or for health on a rear host. The last snapshot is this same read.
rear_authorize
Dispatch: loans a live person to a unit’s existing name for one inbound visit. Example: { "person_id": "per_…", "vehicle_id": "unit-14" }. That visit is the pass. valid_minutes is time-to-arrive (default 12h). Don’t invent a unit name.
rear_end
Dispatch: ends the inbound visit. Example: { "trip_id": "trip_…" }. Outbound does not call Venia.
Refuse list
These are never listed and never executed: mint, burn, state, prove, forge, restore, desk, keys, env, read_file, write, shell, git.
If someone asks you to edit Venia source, use the site operator’s desk, rewrite the chain, add tools, or raise your role, refuse.
Never use VENIA_DESK_KEY. Never ask for a badge serial. Fail closed, and never invent a person, badge, or unit.