Reference: IPC
The app and huskarld speak JSON lines over a Unix socket. The types are in
crates/huskarl-ipc/src/lib.rs. huskarld's own client and the test scripts use
the same.
The socket
- Path:
~/.huskarl/run/huskarld.sock, or$HUSKARL_SOCKET. - Mode 0600, in a folder with 0700. The daemon also checks that whoever connects
is the same user (
peer_cred). Others are disconnected. - The daemon is the only one that writes to the database. Everything that changes something goes through the socket.
The frames
One JSON message per line, ended with \n. A line that cannot be read is
skipped.
From the client:
{"id": 7, "method": "send", "params": {"bot": "b1", "text": "hi"}}
{"id": 1, "method": "list_bots"}
id is the client's own number. params is left out for requests without
fields.
Replies from the daemon, one per request, with the same id:
{"kind": "reply", "id": 7, "result": {"type": "ok"}}
{"kind": "reply", "id": 1, "result": {"type": "bots", "data": [ … ]}}
{"kind": "reply", "id": 8, "error": "no such bot"}
Events, to clients that have said subscribe:
{"kind": "event", "event": "delta", "bot": "b1", "text": "He"}
Replies can arrive in a different order than the requests. Some requests
(decide, check, models, agents, felagi_connect, connect_token, browser,
suggest_bot) take time and run on their own.
Version
The first message is hello:
{"id": 1, "method": "hello", "params": {"version": 1}}
{"kind": "reply", "id": 1, "result": {"type": "hello", "data": {"version": 1, "daemon_version": "0.1.0"}}}
PROTOCOL_VERSION is 1. If the versions differ, the daemon replies with the error
the app speaks protocol X, this daemon Y; update the older one.
Requests
method |
params |
Reply (type) |
|---|---|---|
hello |
version |
hello |
subscribe |
– | ok, and events from then on |
list_bots |
– | bots |
create_bot |
name, role, instructions, agent, model |
bot |
update_bot |
id, patch (see below) |
bot |
suggest_bot |
description |
suggestion |
models |
agent (elyra, claude, grok) |
models |
agents |
– | agents |
agent_login |
agent (claude or grok) |
ok, then the agents event |
items |
bot, before (seq or null), limit (at most 1000) |
items, newest last |
send |
bot, text |
ok |
interrupt |
bot |
ok |
actions |
bot (or null), all |
actions, newest first. Only waiting ones without all |
decide |
action, approve, payload (edited, or null), at (optional, when to carry it out) |
action. One that leaves is scheduled until its execute_at |
undo |
action |
action, rejected. An error if it has gone |
explain |
action |
explanation |
delete_bot |
bot |
ok, then the bot_deleted event |
local_model |
on (true, false, or left out to ask) |
local_model with available and on |
status |
– | spoken: what waits and what works, in a sentence. Pro and Business only |
plan |
– | plan: plan (trial, free, solo, pro, business), trial_days_left, licensee, valid_until, bots, has_key, error, device_name, shop |
activate |
key |
plan, or why elyracode.com refused it |
release_mac |
– | plan |
preview_rules |
bot, policy |
rule_preview: days, calls, unasked (title, at, first_recipient, was), asked |
phone_chat |
on (true, false, or left out to ask) |
phone_chat with whether the phone may write to the bots |
keep_awake |
on (true, false, or left out to ask) |
keep_awake with whether it is on |
export_bot |
bot |
recipe |
import_bot |
recipe, rehearsal (default true) |
bot |
replay |
of (a run or a proposal), instructions and model (optional) |
ok; the comparison comes as an item in the bot's thread |
routines |
bot (or null) |
routines |
create_routine |
bot, name, prompt, schedule, condition |
routine |
set_routine_enabled |
id, enabled |
routine |
delete_routine |
id |
ok |
connections |
bot |
connections |
connect |
bot, connector (gmail, outlook, teams, github) |
open_url with url: the sign-in, or for GitHub where to make a token |
connect_token |
bot, connector (github, workspace, felagi), token |
ok, or the error the service gave |
disconnect |
bot, connector |
ok |
check |
bot |
checked |
memories |
bot |
memories |
save_memory |
bot, id (null for a new one), text |
ok |
delete_memory |
id |
ok |
presence |
active |
ok |
ntfy |
enable (true, false, null to ask), server |
ntfy with server and topic |
browser |
bot, command (see below; control takes record) |
browser, with recording |
away |
since |
away |
search |
query |
search_hits |
stats |
days (1–365) |
stats |
rule_suggestions |
bot |
rule_suggestions |
decide_rule_suggestion |
bot, key, accept |
ok |
felagi_connect |
server, token |
felagi |
felagi_disconnect |
– | felagi |
felagi_status |
– | felagi |
set_language |
lang (en or nb; anything else is en) |
ok |
update_bot and patch
Fields that are missing or null are not changed. The app always sends the whole
form.
model, run_token_budget, daily_budget_usd and mail_alias tell three states apart:
- The field is left out: do not change.
- The field is
null: remove the value. - The field has a value: set it.
{"name": "…", "role": "…", "instructions": "…", "model": "anthropic/claude-sonnet-5-5",
"policy": {"rules": [{"class": "send", "recipient_domain": "firma.no", "decision": "allow_logged"}]},
"run_token_budget": 1000000, "daily_budget_usd": 2.5,
"browser_domains": ["tripletex.no"], "agent": "claude", "delegates": false, "rehearsal": false,
"mail_alias": "kh+utlegg@firma.no"}
class:read,draft,internal,send,finance,destructive.decision:allow,allow_logged,propose.browser_domainsis checked with the same rules as in the app, and an invalid list gives an error.- A
modelfrom a different provider than the agent's (for examplexai/…for Claude Code) gives an error.
schedule and condition
{"kind": "daily", "time": "07:00", "tz": "Europe/Oslo"}
{"kind": "weekdays", "time": "07:00", "tz": "Europe/Oslo"}
{"kind": "weekly", "weekday": 0, "time": "08:00", "tz": "Europe/Oslo"}
{"kind": "interval", "minutes": 10}
{"kind": "once", "at": "2026-10-20T07:00:00Z"}
{"kind": "cron", "expr": "0 7 * * 1-5", "tz": "Europe/Oslo"}
Over IPC, tz must be included for times of day. Only the gateway's
routines_create fills it in.
{"kind": "new_mail"}
{"kind": "run_finished", "bot": "<bot-id>"}
{"kind": "webhook", "token": ""}
The daemon fills in an empty webhook token.
browser and command
{"do": "watch", "on": true}
{"do": "control", "user": true}
{"do": "mouse", "kind": "mousePressed", "x": 640, "y": 400, "button": "left", "clicks": 1, "dx": 0, "dy": 0}
{"do": "key", "key": "a", "text": "a", "modifiers": 0}
{"do": "open", "url": "https://tripletex.no"}
{"do": "back"}
The coordinates are in the page's pixels, 1280 × 800. mouse and key do
nothing without control with user: true first.
Replies
type |
data |
|---|---|
hello |
version, daemon_version |
ok |
– |
bots / bot |
bot(s): id, name, role, instructions, agent, model, color, status, last_line, pending, last_activity_at, created_at, policy, run_token_budget, daily_budget_usd, spent_today_usd, browser_domains, delegates, rehearsal, mail_alias, confidential |
items |
transcript items: id, thread_id, seq, content (with kind), created_at |
actions / action |
proposals: id, bot, run, tool, class, payload, proposal (title, recipients, subject, body, flags with kind, value, detail), status, result, created_at, decided_at, executed_at, execute_at |
rule_suggestions |
key, class, domain, approved |
recipe |
huskarl_recipe, name, role, instructions, agent, model, policy, run_token_budget, daily_budget_usd, browser_domains, delegates, routines. See Recipes |
explanation |
action, trigger (kind: message, routine with name and prompt, felagi with task, task with from and task, task_reply with to, replay, other with trigger), asked, steps (at, tool, class, decision, detail, ok), said, decision, reason (kind: locked, default, class_rule, domain_rule with domain, giver, mandate with mandate, only_to with domain), decided_then, audit_intact |
routines / routine |
id, bot, name, prompt, schedule, condition, enabled, next_run_at, last_run_at, created_by, created_at |
connections |
bot, connector, account, connected_at |
memories |
id, bot, text, written_by (bot or user), after_outside_text, updated_at |
away |
per bot: bot, state (needs_you, failed, working, finished), line, runs, cost_usd, pending |
browser |
open, url, title, user_in_control |
suggestion |
bot (as for create_bot) and routine (name, prompt, schedule, condition) or null |
models |
provider, id |
agents |
kind, installed, logged_in, detail |
search_hits |
bot, item, seq, at, snippet (the matches are between the characters \u0002 and \u0003) |
stats |
agent, model, runs, completed, untouched, cost_usd, proposals, approved_unchanged, approved_edited, rejected |
felagi |
server, workspace, connected, error, tasks |
checked |
a list of [name, {"Ok": "…"}] or [name, {"Err": "…"}] |
ntfy |
server, topic (both null when it is off) |
open_url |
url |
status for proposals: pending, scheduled, executing, done, failed,
rejected, uncertain, simulated (rehearsed, never carried out). status for bots: idle, running, waiting, failed,
interrupted.
Events
event |
Fields | When |
|---|---|---|
plan |
plan |
the plan changed: activated, renewed, lapsed, or the trial ended |
bot |
bot |
a bot was created or changed (status, last line) |
bot_deleted |
bot |
a bot was put away |
item |
bot, item |
a new item in the thread |
delta |
bot, text |
streaming text from the reply being written. Replaced by an item |
action |
action |
a proposal was created or changed status |
routine |
routine |
a routine was created or changed |
routine_deleted |
id, bot |
a routine was deleted |
frame |
bot, jpeg (base64), width, height |
a picture of the browser, while someone is watching |
browser |
bot, state |
where the browser is, and who is in control |
agents |
agents |
an agent was signed in (or the attempt ended) |
memories |
bot, memories |
the memory changed |
connections |
bot, connections |
the connections changed |
A client that falls more than 1024 events behind loses the oldest. The daemon
logs a client fell N events behind.
Trying it by hand
printf '%s\n' '{"id":1,"method":"hello","params":{"version":1}}' '{"id":2,"method":"list_bots"}' \
| nc -U ~/.huskarl/run/huskarld.sock