Hallvi agent reference

How a coding agent, such as Codex, operates an existing Hallvi application through the hallvi CLI or the Hallvi MCP adapter. This file describes Hallvi 0.1.1-alpha.12. Read the rules first, then the section you need.

View Markdown

This page is rendered from its Markdown source.

To point an agent here, add this line to its instructions, such as the AGENTS.md in your repository:

Read https://hallvi.com/docs/agents/index.md before you operate Hallvi.

Rules

  1. Work only on an application that the owner already set up in Hallvi, and only by sending requests to its main operator, Pi. The CLI and the adapter create no applications and connect no accounts.
  2. Name the controller with a loopback address, such as http://127.0.0.1:4747. Hallvi refuses any other address and never guesses one.
  3. Run inspect before you send work, and check main and attention. A missing worker or interrupted work needs the owner, not another request.
  4. Keep the handle and the requestKey that exec returns. A timeout stops only your watching. Pi keeps working.
  5. If you do not know whether Pi accepted a request, send the identical text again with the same --request-key. Never send it again under a new key, because that can duplicate the work.
  6. Never try to get past a wait. You cannot approve a command, answer an input card, change the permission mode or continue interrupted work. Send the owner to attention.page, then follow the same handle.
  7. completed means Pi finished answering. It does not mean the work succeeded. Read the answer and the evidence of the operation that took your request key.
  8. Put no credentials, secrets or contributor instructions in a request. Send Pi only the work and its scope.
  9. Report what you observed, when you observed it, and what failed or remains unknown.

Quick start

The owner has installed Hallvi, set up an application and given you the controller address. On the computer running Hallvi:

export HALLVI_CONTROLLER_URL=http://127.0.0.1:4747
hallvi apps --json                     # pick the application's id
APP_ID='<id from applications[]>'
hallvi inspect "$APP_ID" --json        # confirm it; check main and attention
REQUEST_KEY=$(uuidgen | tr '[:upper:]' '[:lower:]')
hallvi exec "$APP_ID" - --request-key "$REQUEST_KEY" --background --json < request.txt > accepted.json
HANDLE='<handle from accepted.json>'
hallvi wait "$HANDLE" --timeout 120 --json > outcome.json

On Linux without uuidgen, use cat /proc/sys/kernel/random/uuid. Write the request to request.txt first. A bounded, read-only request looks like this:

Read-only check: GET <application URL>/api with the header
X-Hallvi-Verification: <unique marker>. Report the HTTP status, the application
name against <expected name>, and whether that same marker was echoed.
Report the running commit or immutable image identity if it can be established,
and which evidence establishes it. Do not redeploy, restart, change configuration,
create resources or alter application data. Report missing or failed checks.

Then read outcome.json as Evidence describes. Keep request and output files in a task folder of their own, because output can contain private application data.

Prerequisites

RequirementDetail
A computer running HallviHallvi runs on an Apple-silicon Mac or on Ubuntu 24.04 x64. Its controller answers only on that computer.
A running controllerhallvi status shows the service and the Pi worker as running, and prints the controller address.
An applicationThe owner creates it, connects its accounts and deploys it in Hallvi's browser page. The CLI and the adapter cannot.
Your processYour agent runs on the computer running Hallvi. To work from another computer, use the MCP adapter over SSH.
For the MCP adapter onlyYou need Node.js 22 and a source checkout of the Hallvi release in use. The standard installer does not install the adapter.

Install and connect

The owner installs Hallvi once, on the computer that will run it, as their normal user and without sudo:

curl -fsSLo ./install-hallvi.sh https://github.com/lustoykov/hallvi/releases/latest/download/install-hallvi.sh && sh ./install-hallvi.sh

The installer puts the hallvi command in ~/.local/bin and brings the Node.js it needs, so the CLI needs no source checkout. If your shell cannot find hallvi, use ~/.local/bin/hallvi. Check the service:

hallvi status
Hallvi <installed version> (<revision>)
  service    running; starts when you log in to this Mac
  interface  http://127.0.0.1:4747
  Pi worker  running
  state      /Users/you/.local/share/hallvi
  logs       hallvi logs

The interface line is the controller address. The owner finishes setup in a browser at that address: the accounts, then an application and its first deployment. The people docs cover those steps in Install Hallvi and Deploy your first application.

Choose the controller

Every request command needs a controller, and Hallvi never looks for one, starts one or tries another. A command uses the first of these:

  1. --controller <url> on the command line.
  2. The HALLVI_CONTROLLER_URL environment variable.
  3. Nothing. The command contacts nothing and fails with controller-missing.

The address must be a plain http origin at localhost, 127.0.0.1 or [::1], with a port and nothing else, such as http://127.0.0.1:4747. The CLI refuses anything else before it connects (controller-invalid), including https, a path and credentials. It also refuses a redirect instead of following it (redirected).

Use the address the installation reports. hallvi status prints it on its interface line, and hallvi url prints it alone:

export HALLVI_CONTROLLER_URL=$(hallvi url)

An installation set up for use from another computer answers at port 5747, not 4747. Spell the address the same way everywhere, because the CLI compares it with the controller inside a handle.

Every result names its controller in controller. Check it, especially when a development checkout and an installation run side by side. A handle names its own controller, and a --controller passed beside a handle must be the same one (controller-conflict).

Loopback is a local trust assumption, not a login. Any process on that computer can reach the controller, as it can reach Hallvi's page. A request's origin label says where it was written and grants nothing. Never expose the controller or the adapter on a public address or through an unauthenticated tunnel.

CLI commands

The request commands are apps, inspect, exec and wait. Every one takes --json, and then stdout carries exactly one JSON object and stderr stays empty. Without --json, progress goes to stderr and a compact result to stdout. Read the JSON even when a command exits nonzero. The service commands, such as start, stop, status and logs, are separate and belong to the owner.

hallvi apps

hallvi apps --json

Returns { controller, applications }. Each application has id, name, source, condition, stack, address, permissionMode and mainChatId. It reads recorded state only. It calls no model and looks at no server.

hallvi inspect

hallvi inspect <app> --json
hallvi inspect <app> --execution <execution-id> --json

Returns { controller, applicationId, application, condition, conversations, main, attention, deployment, records, recordsOmitted, executions, executionsOmitted }. The result holds the 25 records Pi presented most recently and the 20 newest executions, with counts of the rest. It reads recorded state only, so it is not a fresh check of the server.

With --execution, it returns { controller, applicationId, execution }: one execution of that application in full, as far as the executor kept it. That is the last 100,000 characters, redacted.

hallvi exec

hallvi exec <app> "<request>" --json
hallvi exec <app> - --request-key <uuid> --background --json < request.txt
ArgumentMeaning
<app>An application id that hallvi apps printed, a lowercase UUID. There is no lookup by name.
"<request>" or -The request text, or - to read it from stdin. It can be up to 5,000 characters.
--request-key <uuid>The request's key, a lowercase UUID. Without it, exec makes one and returns it as requestKey. Reuse it only to retry the same text.
--background, --bgReturn once Pi has durably taken the request, without watching it. Combining it with --timeout is a usage error.
--timeout <seconds>Stop watching after this long. 0 reads the state once. Without it, exec watches until the request settles. It never limits the work.

exec sends the request to the application's main conversation as a follow-up (next), never as a steer, so Pi reads it when the current work ends. Hallvi labels it CLI instead of You in the conversation. The request meets the same Pi, permission mode, approvals and evidence as a message typed in the page. accepted turns true only once Pi has durably taken it. If the controller's answer is lost, exec sends the same request under the same key up to three times.

hallvi wait

hallvi wait '<handle>' --timeout 120 --json
hallvi wait '<handle>' --timeout 0 --json      # read the state once

wait only reads. It never sends, retries or continues work, and it can follow a request that another process sent. A handle is the address of the request's outcome:

http://127.0.0.1:4747/api/applications/<app>/chats/<conversation>/requests/<key>

It names the controller, the application, the conversation and the request key, and holds no credential. Following is a poll about once a second. If a read fails, wait never takes that for idle, finished or cancelled. It asks again for about fifteen seconds while a controller or worker restarts, then stops with the state not known and exit code 1.

Output

exec and wait return this object with --json:

{
  "controller": "http://127.0.0.1:4747",
  "applicationId": "...",
  "chatId": "...",           // the main conversation
  "requestKey": "...",
  "handle": "http://127.0.0.1:4747/api/applications/.../requests/...",
  "accepted": true,          // false: refused; null: not known
  "status": "completed",     // null when not known, and with --background
  "timedOut": false,
  "stoppedByUser": false,    // Ctrl-C
  "background": false,
  "operation": {
    "id": "...",             // Pi's own; the request key when this request began it
    "status": "completed",   // open, completed, failed, aborted
    "startedAt": "...",
    "endedAt": "...",
    "requestKeys": ["..."]   // every request this operation took
  },
  "answer": "...",           // Pi's final words, once the operation ended
  "answerTruncated": false,
  "failure": null,           // a bounded, redacted reason and advice
  "attention": null,         // { kind, reason, page, executionId? }
  "evidence": [],            // the operation's calls, see Evidence
  "evidenceOmitted": 0,
  "error": null              // { code, message } when the command failed
}

With --background, accepted: true and status: null mean Pi has the request, not that anything passed. answer holds the first 20,000 characters, and answerTruncated says when Hallvi cut it. Here attention.page is an absolute address on the controller. operation.id is usually a request key, but it is queue:<id> when the operation began by reading Pi's queue after Continue. When exec or wait fails after the request is identified, this object carries error beside whatever was known. Any other failure returns { controller, applicationId, error: { code, message } }, with null for what was not chosen yet.

Request lifecycle

exec --> queued --> working --+--> completed             exit 0
                              +--> failed                exit 1
                              +--> waiting-for-approval  exit 2
                              +--> waiting-for-input     exit 2
                              +--> interrupted           exit 4
queued or working ---------------> cancelled             exit 4

waiting-for-approval --> working   after the owner approves or declines
StatusMeaningExitNext step
queuedPi holds the request and has not read it yet.3 on timeoutFollow the handle again.
workingThe operation that took the request is running.3 on timeoutFollow the handle again.
waiting-for-approvalA call waits for the owner's decision and has not run.2Send the owner to attention.page, then follow the same handle.
waiting-for-inputThe operation ended by asking the owner for something through one of Hallvi's cards, and the card is still open.2Send the owner to attention.page. The answer starts new work.
completedPi finished answering.0Read the answer and the evidence before you report anything.
failedPi could not finish.1Read failure and the evidence, and report them.
cancelledSomeone stopped it in Hallvi, or Stop dropped it before Pi read it.4Report it. Do not send it again without the owner.
interruptedThe worker went away mid-operation, or before reading the request. Nothing runs until someone chooses Continue or Stop in Hallvi.4Send the owner to attention.page, then follow the same handle.

Other exit codes: 0 for --background once Pi accepted the request and for apps and inspect, 1 for a validation, transport or refusal failure or for acceptance not known, and 130 for Ctrl-C.

When Pi reads several waiting messages in one operation, each of those requests gets that operation's whole result, and operation.requestKeys lists them all. A later request, or another application's, never becomes part of it.

Approvals and input

Retries

The request key makes a retry safe. Pi never takes one key twice, and it refuses the same key with different text.

SituationWhat to do
accepted: null, or error acceptance-unknownSend the unchanged request with the same --request-key. That settles acceptance without duplicating work.
The wait timed out, or you pressed Ctrl-CPi still has the request. Run wait on the same handle.
Your MCP session disconnectedPi still has the request. Call hallvi_wait with the same handle.
You want different workSend a new request with a new key.
# The retry is the same command, with the same file and the same key.
hallvi exec "$APP_ID" - --request-key "$REQUEST_KEY" --background --json < request.txt

exec already sends the same request again, under the same key, up to three times when the controller's answer is lost. A refusal settles acceptance only when exec sent nothing before it. After a lost answer, a refused retry says nothing about the first send. Never send a request again under a new key to find out.

Evidence

Find your request key in operation.requestKeys, then use that operation's answer and evidence. Do not use the newest execution from inspect, because it can belong to other work.

Each evidence item is one tool call as Hallvi recorded it, never interpreted:

FieldMeaning
toolCallIdPi's id for the call.
executionIdThe execution record to read in full, or null when the executor keeps no record of that call.
tool, targetWhat was called, and on what.
inputThe call's input, redacted, first 2,000 characters.
statusHow the call stands: awaiting-approval, running, succeeded, failed, declined or interrupted.
exitCodeThe exit code, present only where a server command reported one. A missing exit code is not zero.
startedAt, finishedAtWhen the call ran.
outputThe call's output, redacted, last 2,000 characters.
inputTruncated, outputTruncatedTrue when Hallvi cut the text above.

An outcome carries the operation's last 40 calls, and evidenceOmitted counts the rest. Omitted evidence is not a clean run. When a relevant excerpt is truncated and has an executionId, read the full record:

hallvi inspect "$APP_ID" --execution '<executionId from the evidence>' --json

There is no verified flag. Design the request so that the evidence can prove the result, for example a unique marker that the application must echo, and check it. An HTTP 200 alone proves little. Read answer, answerTruncated, failure, attention and evidenceOmitted even when the status is completed, because a completed answer can say the objective was not achieved. Report what was observed, when, and what failed or remains unknown.

Failures and recovery

Error codeMeaningWhat to do
usageThe command line is wrong.Fix the arguments. The CLI sent nothing.
controller-missingYou named no controller.Pass --controller or set HALLVI_CONTROLLER_URL. The CLI contacted nothing.
controller-invalidThe address is not a loopback origin.Use the interface address from hallvi status. The CLI contacted nothing.
controller-conflict--controller differs from the handle's controller.Pass the handle alone.
handle-invalidThe handle is not a request address.Pass the handle from the exec output unchanged.
unreachableNothing answered at the address in time.Run hallvi status on that computer. If the service is stopped, tell the owner.
redirectedThe address answered with a redirect.Use the exact controller origin. The CLI never follows a redirect.
not-hallviSomething other than a Hallvi controller answered.Check the port against hallvi status.
controller-failedThe controller answered with a server error.For a read, run wait again. For a send, the CLI already retried, and reports acceptance-unknown if every attempt failed.
not-foundThis controller has no such application.Run hallvi apps and use an id it prints.
request-not-foundThis controller has no request under the handle's key.Pi never accepted it, or Stop dropped it while it waited. Ask the owner before you send it again.
refusedThe controller refused the request. The messages below are the common ones.Read message, then act on it.
worker-unavailableNo Pi worker is running. At a send, the CLI sent nothing and accepted is false. During a read, the CLI asks again for about fifteen seconds first.Tell the owner. hallvi status shows the worker, and hallvi restart starts it again.
acceptance-unknownWhether Pi has the request is not known.Send the identical request with the same key.
stoppedCtrl-C came before the CLI handed anything over.The CLI sent nothing.
failedAny other failure of the command.Read message.

These refused messages come from the controller:

When the request itself ends badly:

MCP adapter

The adapter is a proof of concept that gives an MCP host, such as Codex, tools for an existing controller. It calls the same client as the CLI and keeps the CLI's origin label. The standard Hallvi installer does not install it, so build it from the source of the Hallvi release you run. hallvi status prints that version on its first line.

Build

git clone --depth 1 --branch v0.1.1-alpha.12 https://github.com/lustoykov/hallvi.git
cd hallvi
npm ci
npm run plugin:build -- --controller http://127.0.0.1:4747

Use Node.js 22 and the real controller port. The build writes dist/hallvi-plugin/ with a bundled server.mjs, a self-contained panel.html, a product skill and local plugin metadata. It contacts no controller and copies no state, keys or logins. At runtime the adapter needs Node.js 22, but no node_modules and no checkout.

Register it in Codex

The build prints an exact codex mcp add command with your Node path and the bundle's absolute path. Run it, or add the equivalent to Codex's config.toml:

[mcp_servers.hallvi]
command = "/absolute/path/to/node22"
args = ["/absolute/path/to/dist/hallvi-plugin/server.mjs", "--controller", "http://127.0.0.1:4747"]
startup_timeout_sec = 20
tool_timeout_sec = 60

Start a new Codex chat to load the tools. Keep the bundle directory while the connection is configured. Remove the connection with codex mcp remove hallvi. That leaves Hallvi's service, applications and history intact.

Hallvi on another computer

Copy only server.mjs and panel.html to the computer running Hallvi, for example to /opt/hallvi-plugin/, with Node.js 22 there. Confirm that noninteractive SSH to it works with its established host key, then let SSH start the adapter:

[mcp_servers.hallvi]
command = "ssh"
args = ["-T", "-o", "BatchMode=yes", "hallvi-host", "/usr/bin/node /opt/hallvi-plugin/server.mjs --controller http://127.0.0.1:4747 --ui-url http://127.0.0.1:8474"]
startup_timeout_sec = 30
tool_timeout_sec = 60

Keep host-key checking on, and keep the remote shell's stdout clean for MCP messages. --ui-url changes only the browser links. The adapter still talks to the controller on that computer, so handles contain its loopback address. Pass them back to hallvi_wait, never to a browser or an HTTP client on your laptop. Use a different MCP server name for a second controller.

Approvals still need Hallvi's page. The owner keeps an SSH forward open on the laptop and opens http://127.0.0.1:8474:

ssh -N -o ExitOnForwardFailure=yes -L 127.0.0.1:8474:127.0.0.1:4747 hallvi-host

Tools

IDs and request keys are lowercase UUIDs.

ToolInputWhat it does
hallvi_appsNone.Lists applications and their recorded condition, with a Hallvi page for each. It probes no server.
hallvi_inspectapplication_id, and optionally execution_id.Reads identity, permission mode, attention, bounded evidence and the last day's stored traffic totals. With execution_id, it returns that execution's fields at the top level, not under execution as the CLI does.
hallvi_conversationapplication_id, turns from 1 to 20 (default 3), and optionally known.Reads the latest turns of the main conversation. With a previous revision as known, it only says whether anything changed.
hallvi_execapplication_id, message of 1 to 5,000 characters, and request_key.Sends one request to Pi and returns its handle, with accepted true, false or null. It does not follow the request.
hallvi_waithandle, and timeout_seconds from 0 to 20 (default 0, one read).Reads the handle's outcome, waiting up to that long. It never sends or restarts work.
hallvi_openNone.Lists the applications and attaches the optional panel for hosts that support MCP Apps.
hallvi_reload_uiNone.Rereads the installed panel.html and notifies the connection. It runs no Pi work and restarts nothing.

hallvi_exec makes one send attempt and watches for acceptance for up to 40 seconds. Retrying is your job. If acceptance is unknown (acceptance-unknown), keep the handle and send the exact message again with the same request_key. In hallvi_wait results, the top-level page is an absolute address for the browser, and attention.page is a path on the controller. A failed tool call returns error with a code: the CLI's codes, adapter-error or read-stopped. Disconnecting Codex or stopping a wait does not stop Pi. The controller, not the adapter, keeps the work and decides its outcome. Outcomes follow the same statuses and evidence rules as the CLI.

Adapter limits

Limits

Hallvi is an alpha, and it runs commands with the access the owner gave it. The owner's precautions are in beta precautions. Working on Hallvi's own source is a different job. Contributors use the repository's development setup and verification workflow, not this reference.