Appearance
ahpd 0.14
The good stuff
Agents, model providers and webhooks are records a client edits, and plugins load while the daemon runs.
- Agents are records: add, edit and remove an agent from a client, and a running session picks up the edit.
- Providers are connectors: add OpenRouter, Anthropic or OpenAI with no restart, and each lists its own models.
- Webhooks take real senders: GitHub, Stripe, Slack, Meta and more, each with its own signature, and any body.
- Plugins load live: enable, disable, configure and install a plugin while the daemon runs.
ahpd doctor: one command that checks this machine and says how to fix what is wrong.
0.14.0 - 2026-10-26
Upgrading
- A webhook endpoint is now a
webhookconnector, and a hand-writtenwebhooks/<id>.mdfile is no longer read. A call to an id that no open connector answers for answers 404. Make each endpoint again withx-ahpd/connectors/put, giving it the same id, the bot or the automation, and the token a caller already sends:
json
{
"id": "ci",
"adapter": "webhook",
"owner": "host",
"config": { "bot": "bot://motion", "secret": "the token a caller sends", "reply": "sync" }
}The host writes the token to the vault as host:connector/ci/secret, so the caller's header does not change. A host:<name> secret the old record named can be taken out with ahpd vault delete host:<name>. See docs/plugins/WEBHOOKS.md.
- A plugin registers adapters, not connector kinds:
registerConnectorisregisterAdapter,ConnectorKindisAdapter,connectorKindsisadapters, and the contract keyconnectorsisadapters. A stored connector that still sayskindand notadapterreads asunavailable. A plugin whose manifest declaresconnectorsno longer loads. x-ahpd-adapterslists the adapters, andx-ahpd-connectorslists only connectors. A connector row no longer carries an adapter row beside it, and itsadapterfield names the adapter it is one of.x-ahpd-plugins,x-ahpd-adaptersandx-ahpd-connectorstake their own grants rather thanconfig. A role that read the plugins withconfig:readnow needsplugin:list, and one that read the connectors now needsconnector:list. One that enabled, disabled or edited a plugin withconfig:writenow needsplugin:enable,plugin:disableorplugin:options. One that made or removed a connector now needsconnector:putorconnector:remove. Over HTTP,plugin config,enableanddisabletake the same grants and are no longer the deployment token's alone; install, update and remove still are.- Every connector action takes a
connector:grant, the caller's own connector included. A person needsconnector:putto make their own connector andconnector:removeto remove it.connector:listreads the caller's own connectors, their teams' and the host's, and no other person's; a role that reads every connector needs the newconnector:all. A connector owned byhostalso needs the newconnector:hostto be made or removed. A team connector no longer takesteam:write; it takesconnector:putorconnector:removeand a membership of the team. x-ahpd-connectorsopens withconnector:list, and a role holdingconnector:allsubscribes without it and reads every row. A client that read the channel's grant fromahpd.channelsnow readsconnector:list.- A role that made or removed a host connector with
connector:putorconnector:removealone now also needsconnector:host. - A webhook connector's
automationis now the id of the automation a call starts, nottrue. A connector that names neitherautomationnorbotfires thewebhooktrigger. A storedautomation: trueopens asinvalid, andx-ahpd/connectors/putrefuses it until the id is given. - A
webhooktrigger'sconfigfield isconnector, notendpoint. An automation that still saysendpointis refused when it is saved, and a stored one does not fire. - A run reads
{{event.body.<field>}},{{event.headers.<field>}}and{{event.query.<field>}}.{{event.<field>}}is empty, so an automation written for the old event data needs its placeholders rewritten. - The default
replyisnone, notcallback. A bot connector made with onlybotandsecretanswers 202, areplyofcallbackneeds acallbackPrefix, andreply,callbackPrefixandsyncTimeoutSecondsare refused on a connector with nobot. - Node 22.18 is the floor, because a
.tsplugin entry loads only where Node strips types.engines, the install scripts andahpd configuresay 22.18. - A model provider is a connector.
modelProxy.providers, the three built-in providers and their environment keys are removed, and a file that still hasmodelProxy.providersis refused at start. Add each provider as a connector of theopenrouter,anthropic,openaioropenai-compatibleadapter, with its key in the vault or{ "fromEnv": "<NAME>" }. Give it the old provider's name as its id, somodelProxy.modelsreads as before. See docs/server/PROXY.md. - An agent is a record, not a preset. At the first start, the
presetsin config.json become host agent records, and a plaintext key moves to the vault. The file is backed up toconfig.json.bak-<stamp>before it is rewritten. Presets set with--pluginor--plugin-optionare not moved. - A plugin's options schema is
ahpd.optionsSchemain its package.json. A module that exportsoptionsSchemaormanifestno longer loads. A bare.tsor.jsfile needs aplugin.json, orid,name,versionandsourcein its config entry. See docs/plugins/PLUGINS.md. - A plugin entry in the config names its module with
source, andnameis the display name. An entry that still names its module withnameloads, and is written back withsourceat its next change. ahpd proxy listprints the model names only.
Webhooks
- A webhook connector names one
authscheme:bearer(the default),header,query,hmac,github,stripe,slack,standard,metaorgraph. A timestamped scheme refuses a call more than 300 seconds from the host's clock, and every value is compared in constant time. A token in the URL can land in a proxy log. - A call carries any body: JSON of any shape, a form, text or nothing, up to
maxBodyBytes(5 MiB by default). A body over the limit answers 413. - A run reads
event.connector,event.method,event.body,event.headersandevent.query. The proof header,authorization,cookieand the URLtokenare kept out. matchon the connector selects the calls that reach its target, by a dotted path of the event data. A call it does not take answers 202 with{ "skipped": true }.- A connector names an
automation, and a call starts that one automation. A connector with neitherbotnorautomationfires thewebhooktrigger, and every automation whose trigger config names the connector runs if the owner rule passes. metaandgraphanswer their URL handshakes, andslackanswersurl_verification. Nothing fires on a handshake.- A connector links to an automation only when both have the same owner or the connector is the host's, checked on save and again at a fire.
PluginHost.runAutomation(id, data, owner?)starts one automation by its id, andfireTriggertakes the owner of the connector the event came from.- An adapter schema marks a connector field with
x-ahpd-connectorand an automation field withx-ahpd-automation; the host checks both on save.
Plugins and connectors
PluginHost.startSession(wanted, named)takes a second argument: a function the host calls once, with the session's URI, before the tree is isolated and the backend spawns. A plugin that answers for that URI can answer from the start, so a bot's connector actions are in its session's first tool list. Anamedthat throws is logged and the start goes on.PluginHost.startChat(session, { provider, model, config, title, prompt })starts a chat inside a session on a backend the plugin names, and answers the chat's URI.ToolCall.createChattakes the sameproviderandconfigbeside its other options. The session keeps its own provider and its other chats; the chat's backend and config are recorded with it, so a session restart,restartChat, a rewind and a daemon restart start it on that backend again. The session'scomputerand contributed session keys reach the chat; the session backend's own keys do not. The session owner'schat:createis asked first, and an unknown provider is refused. A client reads the chat's backend in_meta['ahpd.provider'], on the chat's state and nowhere on the session's own chats.- A plugin declares a manifest: a description, categories, links and contracts. A registration its contracts do not list fails that plugin, with a line naming the method and the name. A plugin with no contracts loads as before, and is listed as
undeclared. - A plugin registers adapters, each with a JSON Schema a client draws the form from and the capabilities a connector of that adapter has.
x-ahpd-pluginslists the configured plugins, andx-ahpd/plugins/enable,disableandoptionschange the file the daemon loads.- A plugin row carries
version,loaded,optionsSchemaandalerts. The daemon reads every plugin's manifest with no import. A disabled plugin, and an enabled one that failed to load, still show what they provide and which options they take. - A plugin raises an alert with
host.alert({ level, text, ref? })and removes one withhost.clearAlert(ref). The levels arenotice,info,warninganderror. A failed load is anerroralert, andproblem(line)and the loader's other lines arewarningalerts. An alert with the samerefreplaces the old one, a plugin keeps at most 50, and a reload or unload clears them. An alert raised after the load sends the row again. Any URL userinfo and anywriteOnlyoption value an alert quotes is answered<set>. - A plugin installs an npm package into its own
installed/folder withhost.install(spec), which answers{ version }or{ error }. npm runs with--ignore-scripts, one install at a time per plugin. A spec that starts with-or holds whitespace, a quote or%is refused. - Enable, disable, options and install take effect while the daemon runs, and only a plugin with a port still asks for a restart.
ahpd plugin reload <name>imports a plugin's code again, andahpd plugin updatereloads each package it moved. A reload ends the sessions of the agent it moves. A live change of adapters keeps the old adapter tools until a restart. - A role holding
plugin:installinstalls, removes and writes plugins from a client.x-ahpd/plugins/install {source}takes every source the CLI takes,x-ahpd/plugins/remove {id, keep?}takes the entry out, andx-ahpd/plugins/write {id, source, packageJson}writes a one-file TypeScript plugin thatx-ahpd/plugins/source {id}reads back.plugin:installis in no group, so only admin holds it by default. A source that starts with-is refused before npm runs. - A plugin manifest and an adapter name icons, with a light and a dark theme. The host sends each icon as a
data:URI of at most 32 KB and animage/type. It reads the icon from inside the package or fetches it fromhttps:at load, so a client never contacts a plugin's server. Each first-party plugin and the webhook adapter ship an icon. - An adapter may have
check(connector), which answers ok, denied, unreachable or failed, andx-ahpd/connectors/checkruns it withconnector:put. A connector row carrieshealth: the last check, the last call and the last failure. - A connector the host started with a valid config is
ready.ConnectorStateisready,invalidorunavailable. - A host connector's secret can be
{ "fromEnv": "NAME" }. A connector owned by a person or a team that names a variable is refused at put and opensinvalid, so it never reads the daemon's environment. enable,disableandoptionsmarkahpd.restartNeededon the root state for every connection, and only a restart clears it. A connection watching the root is sentx-ahpd/restartNeededwith{ channel: "ahp-root://" }when the mark is set, once, so a client holding the state does not have to read it again.x-ahpd-adapterslists the adapters a host serves, keyed by adapter name, and read-only.x-ahpd-connectorslists the connectors, andx-ahpd/connectors/putandremovemake and take them away. A field an adapter's schema markswriteOnlygoes to the vault under the owner's scope, and is in no record and no snapshot.- A
receivecapability may carrypath, where its connectors receive on the daemon's HTTP root, with{id}for the connector id.x-ahpd-adapterssends it, and the@ahpd/webhookcapability names/plugins/webhook/{id}. @ahpd/webhookregisters the first adapter, so a client makes a webhook endpoint from the connector form.@ahpd/activepiecesoffers each Activepieces piece as an adapter, namedap-<piece>: thepiecesoption names the piece packages, a connector is one account, and each action runs as a host tool in a child process that pinsNODE_TLS_REJECT_UNAUTHORIZED. The pieces are Resend, Telegram Bot, Slack, GitHub, Linear, Jira Cloud, Discord, WhatsApp Business, Notion, Airtable, SMTP, SendGrid, Stripe and HubSpot. See docs/plugins/activepieces.md.- An Activepieces piece may carry a version, as in
@activepieces/piece-postgres@0.5.1. A piece that is missing, or not at the written version, installs when the options are saved. A written version runs frominstalled/even when the plugin ships the piece. Each install is an alert with refpiece:<package>:infowith the version, orerrorwith npm's reason. - An adapter's
actioncapability runs as a host tool: one<adapter>_<action>tool for every connector of an adapter that hasrun, offered only to a session that binds one of them. A session names its connectors by alias in the newconnectorsconfig key, set when it starts, or a bot names them in its record and the bot plugin answers them live through the newregisterBindings. The host resolves the alias, checks the connector and its owner at every call, and never hands the model an id or a secret. See docs/plugins/PLUGINS.md.
Model proxy
- The host ships four provider adapters,
openrouter,anthropic,openaiandopenai-compatible, each with a form, a key check and an icon. A provider is added in a client with no restart. - A model entry's
providernames an adapter or a connector id. A call uses the caller's own connector first, then the scope team's, then the host's, and the usage record names the connector that paid. - A ready provider connector lists its own models, read again each hour, and a model is callable as
<prefix>/<id>with no line in the proxy file. A fetched model takes the same policy and owner order as a table entry, and its fetched price is charged. /modelscarries OpenRouter's name, context length and pricing, and the OpenAI paths also answer at the root.
Bots
- A bot chat that nothing runs carries its bot's mark in
_meta, so a client draws it as a bot chat from the first frame.Listedtakes an optional_meta, sent while nothing runs the session. - A session a bot starts has the bot's connector actions from its first tool list. The host names the session's URI before the tree is made and before the backend spawns, the bot keeps the URI from then on, and its reader answers for it. A start that fails after the URI is named leaves no entry, so nothing binds a session that does not exist.
- A message sent in a bot chat while a turn runs shows as a turn of its own. The running turn ends
completewith what it held, and the back session's answer after the steer lands under the new turn. - A bot record has a
connectorsfield: the connectors its sessions may use, by alias, such as{ "work": "connectors://ap-resend/account-123" }. Each session the bot started reads it live, so an edit reaches a session that is already running, and a bad alias or value is refused-32602when the bot is written.
Agents
- An agent is a record on
x-ahpd-agents, added, edited and removed the way a connector is. It is owned by the host, a team or a user, with the same owner rules. A caller sees only the agents it may use. A record is read live, so an edit reaches a running session at its next turn. - An agent record names its engine in
engine.x-ahpd-engineslists the engines, and needsagent:list. A row has the engine's name, title, plugin, icon, templates and the schema a record'sconfigfollows. It also lists the fields only a host record may set, and the key variables any record may set. agent-claude, agent-acp, agent-cofold and agent-pi registerclaude,acp,cofoldandpiwithregisterEngineand the contract keyengines. - An engine's key fields and key variables go to the vault on put, and no row or log holds their values. Only a host record may set other env variables or
extraArgs. ahpd configurewrites anacprecord instead of a preset.- The four agent plugins are titled Agent Claude, Agent ACP, Agent Cofold and Agent pi in the plugin list.
- cofold: a transcript read while a rewind removes a run answers the turns that remain instead of failing. The removed run's turn carries no usage, ending or duration.
- A background task has one shape from every backend. A backend reports it through
Start.backgroundTasks, and the host draws the row; achat/backgroundWork*action a backend sends itself is dropped. - Claude's background shells show their output in a terminal of their own, read once a second, and stop with
vscode/stopBackgroundWork, which needschat:cancel. - An ACP terminal that still runs when its call ends is listed as a background shell, and its stop kills the process. A nested host lists its inner host's tasks.
Sessions and VS Code parity
- A session row's counts move while a turn runs, at each finished tool call, rather than waiting for the turn to end. The reads are coalesced per session, so a burst of tool calls costs at most one read running and one waiting.
- A worktree session counts from the commit its tree was cut from, kept with the tree's record. A commit that was already on the local base and never pushed is no longer counted as the session's work. A record written before this keeps its old answer, counting from the base's name.