key.store agent dispatch service
  • Go 94.4%
  • Shell 5.6%
Find a file
2026-06-06 12:18:07 -07:00
cmd Prepare gated meet calendar integration 2026-05-26 03:35:39 -07:00
internal/agentdispatcher Prepare gated meet calendar integration 2026-05-26 03:35:39 -07:00
dispatcher-smoke.sh Add programmable dispatcher topology 2026-05-24 20:27:50 -07:00
go.mod infra: land redmine and authentik control-plane hardening 2026-03-22 22:42:03 -07:00
go.sum infra: land redmine and authentik control-plane hardening 2026-03-22 22:42:03 -07:00
identity-placement-smoke.sh Run assistant supervisor on HQ central queue 2026-05-23 05:31:20 -07:00
identity-routing-smoke.sh Add programmable dispatcher topology 2026-05-24 20:27:50 -07:00
README.md Use recovery access naming 2026-06-06 12:18:07 -07:00
topology.toml Add programmable dispatcher topology 2026-05-24 20:27:50 -07:00

key.store agent-dispatcher

Unified intake service for inbound operations events, role routing, and resident identity steers.

This directory is the formal dispatcher codebase for key.store. Webhook topology is source-controlled in topology.toml; Nix passes that file to the API service and local callers can inspect the active, secret-free view at GET /topology.

Inbound webhook paths

  • POST /webhook/redmine
  • POST /webhook/forgejo
  • POST /webhook/zulip
  • POST /webhook/forwardemail
  • POST /webhook/sms
  • POST /webhook/stable
  • POST /webhook/minecraft
  • POST /webhook/grafana
  • GET /healthz
  • GET /topology (loopback only)

All webhook traffic is expected to come through https://dispatch.key.store.

Communications routing

  • Forward Email aliases can post directly to /webhook/forwardemail; the dispatcher dedupes on identity + message-id.
  • Email intake (agent-dispatcher mail) can still poll IMAP, sharing the same dedupe path so webhook-first and mailbox-fetch flows do not double-enqueue.
  • Mail intake first enqueues a resident-thread steer. The receiving identity must absorb the steer before any side effect; mail intake has no SMS sender.
  • Redmine should be searched before creating duplicate todos from mail, Forgejo, SMS, or wake notifications.
  • Mail sender policy is fail-closed for execution. Trusted senders can steer the owning role; untrusted mail routes only to the configured review role identity, keeps the requested role in payload context, and must not trigger repository, secret, DNS, or production changes without later human approval.
  • Zulip outgoing webhooks post to /webhook/zulip through the Agent Dispatcher outgoing-webhook bot. Channel messages should mention @Agent Dispatcher plus a role alias such as @ios; direct messages to the bot can route through the receiving role identity.
  • Zulip replies are relayed from completed resident-thread output. The identity absorbs the steer first, then the supervisor posts the final assistant answer back to the originating stream/topic or direct message as the mapped Zulip identity.
  • SMS/calls (/webhook/sms) and physical mail (stable) can be delivered via signed webhooks.
  • SMS-compatible sources default to the assistant role and assistant identity, and stable defaults to the coo role and manager identity, unless an explicit mention overrides routing.
  • SMS does not use a completion relay. The assistant must decide in the resident thread whether the inbound text needs a response and call communications_sms_send before ending the turn when it should text back.
  • Forgejo webhooks route explicit role mentions and identity-specific issue assignments or pull request review requests. Assigned/review-requested identities wake their resident role thread even when the Forgejo body has no @role/... or alias mention.
  • Grafana Alerting posts to /webhook/grafana through the provisioned key.store dispatcher contact point. Grafana signs the raw body with HMAC-SHA256 and includes the configured timestamp header; labels or annotations can route with key_store_role, role_id, key_store_identity, or role aliases such as team=infra. Alerts default to the infra identity.
  • The resident-agent shape is one long-running Codex thread per identity. Incoming notifications become steers into that identity thread; the dispatcher no longer exposes a separate task executor path.

Resident Steer Execution

  • Resident identity threads use the configured role model, reasoning effort, sandbox, and approval policy.
  • Queue state lives in forge-local PostgreSQL via AGENT_DISPATCHER_DATABASE_URL; legacy sqlite/task imports are no longer part of the live path.
  • Mail-to-work links live beside the steer queue: mail_redmine_links records JMAP/RFC message ids, dispatcher event keys, steer ids, and Redmine issue ids without duplicating message bodies.
  • Resident environments receive role-scoped Redmine, Forgejo, JMAP, Zulip, communications, physical-mail, and MCP credentials so primary-session tools can read/update the same event the dispatcher routed from.
  • Resident environments project org/organization.toml agent runtime policy into the app-server thread. The supervisor prewarms every mapped identity for an identity-mode role; thread persistence, app-server clients, identity-bound credentials, and multi-identity work roots are scoped by role and identity so identities that share a role, such as agent and assistant, do not replace each other or inherit each other's tokens. Role-specific environment variables still override model and provider defaults for recovery changes.
  • Resident environments also project the per-identity workspace placement from org/organization.toml. Every identity has an explicit host/surface/profile/tooling contract: iOS on macOS Namespace, Android on the Android Namespace image, Windows on the Windows lane, assistant on HQ, and the remaining identities on appropriate Linux Namespace or forge surfaces.
  • Minecraft and office presence are primary-session MCP tools. The dispatcher no longer requires a dedicated Minecraft sub-agent.

MCP and search posture

  • dispatch.key.store/mcp is the shared Authentik-backed MCP hub for contributor clients and the per-identity loopback bearer hub for forge-resident agents.
  • The public plugin metadata should mention the broad tool surface so Codex tool search can discover the hub for mail, Redmine, Forgejo, SMS, office/Minecraft, and observability work.
  • Stalwart stores message data, blobs, lookup, and full-text search in PostgreSQL. The stalwart and agent_dispatcher databases are part of the daily PostgreSQL backup set.
  • The mail MCP namespace exposes JMAP-native mailbox state, search, snippets, fetch, thread context, draft/submit/send, calendar event search/create, contact search, and readiness tools rather than inventing a separate message-body index first.
  • Meeting-backed calendar events use the Stalwart/JMAP calendar create tool with meet.key.store Jitsi URLs embedded in the event metadata; Stalwart remains the source of truth for calendar state and scheduling mail.
  • JMAP search, fetch, and thread results are enriched with dispatcher steer links and stored Redmine issue links. Use mail_jmap_link_redmine_issue after an identity decides that an email needs durable Redmine follow-up.

Helper CLIs

  • key-store-redmine whoami
  • redmine_issue_search is available through MCP for duplicate checks before creating durable work.
  • key-store-redmine issue show <id>
  • key-store-redmine issue create --subject "..." --description "..."
  • key-store-redmine issue note <id> --message "..." or note text on stdin
  • key-store-redmine issue update <id> --status-id <n> [--assigned-to-id <n>] [--notes "..."]
  • key-store-zulip send --message "..." for office-channel updates.
  • communications_sms_send, communications_sms_messages, communications_phone_numbers, and communications_voice_calls are available through MCP for assistant-owned communications.
  • stable_mail_items is available through MCP for COO-owned physical-mail lookup.

Provider scripts

  • Scripts/communications-fetch-events.sh
  • Scripts/communications-send-sms.sh
  • Scripts/communications-provision-webhook.sh
  • Scripts/stable-fetch-mail.sh
  • Scripts/stable-provision-webhook.sh

These scripts assume communications and Stable API keys are managed with agenix:

  • secrets/communications/api-key.age
  • secrets/stable/api-key.age

Current provider defaults:

  • SMS/voice provider API: https://api.openphone.com
  • Stable API: https://api.usestable.com
  • Stable webhook endpoints are configured in the Stable Dashboard; the local helper validates API access and prints the target/events for manual dashboard setup.

Smoke checks

  • services/redmine/redmine-smoke.sh
  • services/redmine/redmine-api-smoke.sh
  • services/agent-dispatcher/dispatcher-smoke.sh
  • services/agent-dispatcher/identity-placement-smoke.sh
  • services/mcp-hub/mcp-hub-smoke.sh

dispatcher-smoke.sh proves the dispatcher HTTP surface, including the Zulip webhook path. identity-placement-smoke.sh fails if forge regains a dedicated iOS supervisor or stops excluding ios and assistant from the forge-owned supervisor.

HQ assistant resident supervisor

  • nixos/keystore-hq.nix runs the assistant resident supervisor on HQ. It keeps the Codex app-server and communications-capable assistant runtime on the failsafe node while using an SSH tunnel to forge PostgreSQL for the shared steer queue.
  • Forge excludes assistant, so signed SMS and HQ watchdog steers have one canonical assistant consumer instead of competing forge and HQ sessions.

macOS iOS resident supervisor

  • Scripts/run-ios-macos-agent-supervisor.sh runs the tech_lead_apple / ios supervisor locally on macOS. It starts a local Codex app-server and uses an SSH tunnel to forge PostgreSQL only for queue access when AGENT_DISPATCHER_DATABASE_URL is not set.
  • Scripts/install-ios-macos-agent-launchagent.sh installs the no-sudo per-user LaunchAgent for the local Mac.
  • Scripts/install-ios-macos-agent-launchd.sh installs the system LaunchDaemon variant for managed macOS hosts where root installation is available.