Agent Zero channel

The PrivacyFlow Agent Zero plugin — a pure channel extension that polls inbound, forwards to an agent context, and auto-replies back through the send API.

The PrivacyFlow channel is an Agent Zero plugin that gives an Agent Zero instance bi-directional encrypted chat. It polls PrivacyFlow’s public API on a 3-second loop, hands each inbound message to an Agent Zero context, and when the agent finishes its reasoning chain the channel ships the reply back through PrivacyFlow’s send endpoint. It plugs straight into the A0 lifecycle via two extension hooks.

What Agent Zero is

Agent Zero is an autonomous-AI agent framework. Its extension system lets plugins register code on lifecycle hooks — job_loop (called each tick) and process_chain_end (called when an agent finishes a reasoning chain). The PrivacyFlow plugin uses those two hooks.

How the plugin wires in

  • job_loop (PfPoller): on the first tick, if env vars are set, it starts a single background asyncio.Task polling PrivacyFlow every 3 seconds. Each polled message is forwarded to (or creates) an Agent Zero context via context.communicate(UserMessage(...)).
  • process_chain_end (PfAutoReply): when the agent finishes, fires only for the root agent (agent.number == 0) and only if pf_routing was set on the context. Extracts the last response-type log entry, splits it for messenger char limits, and sends each chunk via POST /api/v1/messages/send.

The poller and the reply hook share routing metadata through context.data['pf_routing'], which carries contact_id, group_id, messenger, and a mapping_key (groupId when present, otherwise contactId) so DMs and groups get separate Agent Zero contexts.

Install

  1. Prerequisites. A running Agent Zero instance with requests importable in its environment. Python 3.12.
  2. Drop the plugin in. Copy the plugin folder into Agent Zero’s plugins/ directory so the path becomes plugins/privacyflow_channel/. The code imports under the plugins.privacyflow_channel namespace, so the directory name matters.
  3. Configure credentials. Three env vars, set in Agent Zero’s .env, in the Settings → Secrets panel, or via the plugin web UI form (config.html). All three end up as the same env vars at runtime.
VariableRequiredPurpose
PF_API_BASEyesPublic API base URL. .env.example ships http://localhost:3004; production is https://privacyflow.app.
PF_API_KEYyesBearer API key.
PF_APP_IDyesThe single app this plugin instance handles. One appId per instance.
  1. Restart Agent Zero. On the first job_loop tick, PfPoller.execute() checks env vars are set and starts the background poller. If they’re not set the plugin idles — it won’t crash A0.

Configure via the web UI

Alternatively, open the plugin’s settings in the A0 web UI (Alpine.js form at webui/config.html). Three fields: API Base URL, API Key, App ID. The form stores values as plugin config and A0 injects them as the same three env vars when the plugin runs. Useful for operators who don’t want to edit .env directly.

One appId per instance

This plugin is deliberately narrow: one PF_APP_ID per running instance. If you want multiple apps to feed the same agent, run one plugin instance per app (or one A0 process per app). Polling across multiple apps is possible at the API level (a single key’s appIds[] round-robins), but the reply path hardcodes PF_APP_ID, so mixing apps in one instance would route replies to the wrong app.

Backend endpoints used

Exactly the four public-API endpoints, via helpers/pf_client.py:

Plugin functionEndpointMethod
health_check()/api/v1/healthGET
verify_auth()/api/v1/auth/verifyGET
poll_messages(limit=10)/api/v1/messages/poll?limit=10GET
send_message(contact_id, message, messenger, group_id=None)/api/v1/messages/sendPOST

Auth is Authorization: Bearer <PF_API_KEY> on every authenticated call.

Per-messenger message splitting

Agent replies can be long. Each messenger has its own display limit; the plugin splits the response text into chunks before sending.

MessengerChar limit per chunk
signal2,000
simplex8,000
session2,000

The splitter goes paragraph → line → word boundaries, falling back to hard-limit splitting. Each chunk is sent via a separate send_message call. The same contact_id, messenger, and group_id are propagated across all chunks so the reply lands in the same thread.

Example end-to-end trace

A Signal user sends “What’s the weather in Berlin?” to your app.

  1. PrivacyFlow backend enqueues the message on poll:app_123.
  2. The poller wakes, poll_messages(10) returns the message with messenger: "signal", contactId: "+12025550104", isCommand: false, groupId: null.
  3. isCommand is false, so it’s not skipped. mappingKey = "c_+12025550104", no existing context matches — a new Agent Zero context is created via initialize_agent().
  4. context.data['pf_routing'] = { contact_id, group_id: null, messenger: "signal", mapping_key } is set.
  5. context.communicate(UserMessage("What's the weather in Berlin?")) runs the agent.
  6. Agent Zero’s reasoning chain finishes. A0 fires process_chain_end. PfAutoReply.execute() checks agent.number == 0 and that pf_routing is set.
  7. _extract_last_response reads the last type == "response" log entry — e.g., “In Berlin it’s 18°C and clear.” (well under the 2,000-char limit, so one chunk).
  8. send_message("+12025550104", "In Berlin it's 18°C and clear.", "signal", null) POSTs to /api/v1/messages/send.
  9. context.data['pf_routing'] is cleared so the context won’t auto-reply again on a later chain end.

For a group message the polled payload carries groupId: "g_xyz" and isGroupMessage: true; the agent’s input is prefixed [c_+12025550104]: <content> so it knows who spoke in the group, and the reply carries group_id="g_xyz".

Health and auth helpers

pf_client.py exposes health_check() and verify_auth() for operational use — neither is called by the poll loop. Wire them into a startup check, a /health route on your A0 host, or a diagnostic command. They return the parsed JSON from the corresponding public-API endpoints; see health and verify.

Note

The poll interval is hardcoded at 3 seconds. That’s 20 requests/minute against the 100/minute poll budget — comfortable headroom even with multiple plugin instances per host. If you need to change the interval, edit POLL_INTERVAL_SEC in extensions/python/job_loop/_10_pf_poll.py.

Last updated: