Skip to content

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 webhook connector, and a hand-written webhooks/<id>.md file is no longer read. A call to an id that no open connector answers for answers 404. Make each endpoint again with x-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: registerConnector is registerAdapter, ConnectorKind is Adapter, connectorKinds is adapters, and the contract key connectors is adapters. A stored connector that still says kind and not adapter reads as unavailable. A plugin whose manifest declares connectors no longer loads.
  • x-ahpd-adapters lists the adapters, and x-ahpd-connectors lists only connectors. A connector row no longer carries an adapter row beside it, and its adapter field names the adapter it is one of.
  • x-ahpd-plugins, x-ahpd-adapters and x-ahpd-connectors take their own grants rather than config. A role that read the plugins with config:read now needs plugin:list, and one that read the connectors now needs connector:list. One that enabled, disabled or edited a plugin with config:write now needs plugin:enable, plugin:disable or plugin:options. One that made or removed a connector now needs connector:put or connector:remove. Over HTTP, plugin config, enable and disable take 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 needs connector:put to make their own connector and connector:remove to remove it. connector:list reads the caller's own connectors, their teams' and the host's, and no other person's; a role that reads every connector needs the new connector:all. A connector owned by host also needs the new connector:host to be made or removed. A team connector no longer takes team:write; it takes connector:put or connector:remove and a membership of the team.
  • x-ahpd-connectors opens with connector:list, and a role holding connector:all subscribes without it and reads every row. A client that read the channel's grant from ahpd.channels now reads connector:list.
  • A role that made or removed a host connector with connector:put or connector:remove alone now also needs connector:host.
  • A webhook connector's automation is now the id of the automation a call starts, not true. A connector that names neither automation nor bot fires the webhook trigger. A stored automation: true opens as invalid, and x-ahpd/connectors/put refuses it until the id is given.
  • A webhook trigger's config field is connector, not endpoint. An automation that still says endpoint is 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 reply is none, not callback. A bot connector made with only bot and secret answers 202, a reply of callback needs a callbackPrefix, and reply, callbackPrefix and syncTimeoutSeconds are refused on a connector with no bot.
  • Node 22.18 is the floor, because a .ts plugin entry loads only where Node strips types. engines, the install scripts and ahpd configure say 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 has modelProxy.providers is refused at start. Add each provider as a connector of the openrouter, anthropic, openai or openai-compatible adapter, with its key in the vault or { "fromEnv": "<NAME>" }. Give it the old provider's name as its id, so modelProxy.models reads as before. See docs/server/PROXY.md.
  • An agent is a record, not a preset. At the first start, the presets in config.json become host agent records, and a plaintext key moves to the vault. The file is backed up to config.json.bak-<stamp> before it is rewritten. Presets set with --plugin or --plugin-option are not moved.
  • A plugin's options schema is ahpd.optionsSchema in its package.json. A module that exports optionsSchema or manifest no longer loads. A bare .ts or .js file needs a plugin.json, or id, name, version and source in its config entry. See docs/plugins/PLUGINS.md.
  • A plugin entry in the config names its module with source, and name is the display name. An entry that still names its module with name loads, and is written back with source at its next change.
  • ahpd proxy list prints the model names only.

Webhooks ​

  • A webhook connector names one auth scheme: bearer (the default), header, query, hmac, github, stripe, slack, standard, meta or graph. 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.headers and event.query. The proof header, authorization, cookie and the URL token are kept out.
  • match on 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 neither bot nor automation fires the webhook trigger, and every automation whose trigger config names the connector runs if the owner rule passes.
  • meta and graph answer their URL handshakes, and slack answers url_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, and fireTrigger takes the owner of the connector the event came from.
  • An adapter schema marks a connector field with x-ahpd-connector and an automation field with x-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. A named that 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.createChat takes the same provider and config beside 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's computer and contributed session keys reach the chat; the session backend's own keys do not. The session owner's chat:create is 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-plugins lists the configured plugins, and x-ahpd/plugins/enable, disable and options change the file the daemon loads.
  • A plugin row carries version, loaded, optionsSchema and alerts. 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 with host.clearAlert(ref). The levels are notice, info, warning and error. A failed load is an error alert, and problem(line) and the loader's other lines are warning alerts. An alert with the same ref replaces 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 any writeOnly option value an alert quotes is answered <set>.
  • A plugin installs an npm package into its own installed/ folder with host.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, and ahpd plugin update reloads 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:install installs, 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, and x-ahpd/plugins/write {id, source, packageJson} writes a one-file TypeScript plugin that x-ahpd/plugins/source {id} reads back. plugin:install is 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 an image/ type. It reads the icon from inside the package or fetches it from https: 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, and x-ahpd/connectors/check runs it with connector:put. A connector row carries health: the last check, the last call and the last failure.
  • A connector the host started with a valid config is ready. ConnectorState is ready, invalid or unavailable.
  • 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 opens invalid, so it never reads the daemon's environment.
  • enable, disable and options mark ahpd.restartNeeded on the root state for every connection, and only a restart clears it. A connection watching the root is sent x-ahpd/restartNeeded with { channel: "ahp-root://" } when the mark is set, once, so a client holding the state does not have to read it again.
  • x-ahpd-adapters lists the adapters a host serves, keyed by adapter name, and read-only. x-ahpd-connectors lists the connectors, and x-ahpd/connectors/put and remove make and take them away. A field an adapter's schema marks writeOnly goes to the vault under the owner's scope, and is in no record and no snapshot.
  • A receive capability may carry path, where its connectors receive on the daemon's HTTP root, with {id} for the connector id. x-ahpd-adapters sends it, and the @ahpd/webhook capability names /plugins/webhook/{id}.
  • @ahpd/webhook registers the first adapter, so a client makes a webhook endpoint from the connector form.
  • @ahpd/activepieces offers each Activepieces piece as an adapter, named ap-<piece>: the pieces option names the piece packages, a connector is one account, and each action runs as a host tool in a child process that pins NODE_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 from installed/ even when the plugin ships the piece. Each install is an alert with ref piece:<package>: info with the version, or error with npm's reason.
  • An adapter's action capability runs as a host tool: one <adapter>_<action> tool for every connector of an adapter that has run, offered only to a session that binds one of them. A session names its connectors by alias in the new connectors config key, set when it starts, or a bot names them in its record and the bot plugin answers them live through the new registerBindings. 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, openai and openai-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 provider names 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.
  • /models carries 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. Listed takes 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 complete with what it held, and the back session's answer after the steer lands under the new turn.
  • A bot record has a connectors field: 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 -32602 when 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-engines lists the engines, and needs agent:list. A row has the engine's name, title, plugin, icon, templates and the schema a record's config follows. 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 register claude, acp, cofold and pi with registerEngine and the contract key engines.
  • 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 configure writes an acp record 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; a chat/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 needs chat: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.