Orchestrator
harn orchestrator serve is the long-running process entry point for
manifest-driven trigger ingestion and connector activation.
Today, the command:
- load
harn.tomlthrough the existing manifest loader - boot the selected orchestrator role
- initialize the shared EventLog under
--state-dir - initialize the configured secret-provider chain
- resolve and register manifest triggers
- activate connectors for the manifest’s providers
- bind an HTTP listener for
webhookanda2a-pushtriggers - write a state snapshot and stay up until shutdown
Current limitations:
multi-tenantreturns a clear not-implemented error that points atO-12 #190inspect,replay,dlq, andqueueare placeholders forO-08 #185
Command
harn orchestrator serve \
--config harn.toml \
--state-dir ./.harn/orchestrator \
--bind 0.0.0.0:8080 \
--cert certs/dev.pem \
--key certs/dev-key.pem \
--role single-tenant
Omit --cert and --key to serve plain HTTP. When both are present,
the listener serves HTTPS and terminates TLS with rustls.
On startup, the command logs the active secret-provider chain, loaded
triggers, registered connectors, and the actual bound listener URL. On
SIGTERM, it stops accepting new requests, lets in-flight requests drain,
appends lifecycle events to the EventLog, and persists a final
orchestrator-state.json snapshot under --state-dir.
--manifest is an alias for --config, and --listen is an alias for
--bind. Container deployments can also configure those through
HARN_ORCHESTRATOR_MANIFEST, HARN_ORCHESTRATOR_LISTEN,
HARN_ORCHESTRATOR_STATE_DIR, HARN_ORCHESTRATOR_CERT, and
HARN_ORCHESTRATOR_KEY.
On Unix, SIGHUP reloads manifest-backed HTTP trigger bindings without
rebinding the socket. The orchestrator reparses harn.toml,
re-collects manifest triggers, installs a new manifest binding version
for changed webhook / a2a-push entries, and swaps the live listener
route table in place. Requests already in flight keep the binding
version they started with; new requests route to the newest active
binding version. The orchestrator records reload_succeeded /
reload_failed events on orchestrator.manifest and refreshes
orchestrator-state.json after a successful reload.
Current reload scope is intentionally narrow: listener-wide settings
such as --bind, TLS files, allowed_origins, max_body_bytes, and
connector-managed trigger changes still require a full restart.
HTTP Listener
The orchestrator listener assembles routes from [[triggers]] entries
with kind = "webhook" or kind = "a2a-push".
- If a trigger declares
path = "/github/issues", that path is used. - Otherwise the route defaults to
/triggers/<id>. /health,/healthz, and/readyzare reserved listener endpoints; useGET /healthfor container health checks.
Accepted deliveries are normalized into TriggerEvent records and
appended to the shared orchestrator.triggers.pending queue in the
event log for downstream dispatch.
Hot reload uses the trigger registry’s versioned manifest bindings. A modified trigger id drains the old binding version, activates a new version, and keeps terminated versions around for a short retention window so operators can inspect the handoff without the registry growing unbounded.
Listener controls
Listener-wide controls live under [orchestrator] in harn.toml.
[orchestrator]
allowed_origins = ["https://app.example.com"]
max_body_bytes = 10485760
allowed_originsdefaults to["*"]semantics when omitted or empty. Requests with anOriginheader outside the allowlist are rejected with403 Forbidden.max_body_bytesdefaults to10485760bytes (10 MiB). Larger requests are rejected with413 Payload Too Large.
Listener auth
Health probes stay public:
GET /healthGET /healthzGET /readyz
Webhook routes keep using their provider-specific signature checks.
a2a-push routes require either a bearer API key or a shared-secret
HMAC authorization header.
Configure the auth material with environment variables:
export HARN_ORCHESTRATOR_API_KEYS="dev-key-1,dev-key-2"
export HARN_ORCHESTRATOR_HMAC_SECRET="replace-me"
Bearer requests use:
Authorization: Bearer <api-key>
HMAC requests use:
Authorization: HMAC-SHA256 timestamp=<unix>,signature=<base64>
The canonical string is:
METHOD
PATH
TIMESTAMP
SHA256(BODY)
METHOD is uppercased, PATH is the request path without the query
string, TIMESTAMP is a Unix epoch seconds value, and SHA256(BODY) is
the lowercase hex digest of the raw request body. Timestamps outside the
5-minute replay window are rejected with 401 Unauthorized.
Deployment
Release tags publish a distroless container image to
ghcr.io/burin-labs/harn for both linux/amd64 and linux/arm64.
docker run \
-p 8080:8080 \
-v "$PWD/triggers.toml:/etc/harn/triggers.toml:ro" \
-e HARN_ORCHESTRATOR_API_KEYS=xxx \
-e HARN_ORCHESTRATOR_HMAC_SECRET=replace-me \
-e RUST_LOG=info \
ghcr.io/burin-labs/harn
The image runs as UID 10001 and stores orchestrator state under
/var/lib/harn/state by default. Override the startup contract with
environment variables instead of replacing the entrypoint:
HARN_ORCHESTRATOR_MANIFESTdefaults to/etc/harn/triggers.tomlHARN_ORCHESTRATOR_LISTENdefaults to0.0.0.0:8080HARN_ORCHESTRATOR_STATE_DIRdefaults to/var/lib/harn/stateHARN_ORCHESTRATOR_API_KEYSsupplies bearer credentials for authenticateda2a-pushroutesHARN_ORCHESTRATOR_HMAC_SECRETsupplies the shared secret for canonical-request HMAC auth ona2a-pushroutesHARN_SECRET_*, provider API-key env vars, and deployment-specificHARN_PROVIDER_*values are passed through to connector/provider codeRUST_LOGcontrols runtime log verbosity
The image healthcheck issues GET /health against the local listener, so
it works with Docker, BuildKit smoke tests, and most container platforms
without requiring curl inside the distroless runtime.
Trigger examples
[[triggers]]
id = "github-new-issue"
kind = "webhook"
provider = "github"
path = "/triggers/github-new-issue"
match = { events = ["issues.opened"] }
handler = "handlers::on_new_issue"
secrets = { signing_secret = "github/webhook-secret" }
[[triggers]]
id = "incoming-review-task"
kind = "a2a-push"
provider = "a2a-push"
path = "/a2a/review"
match = { events = ["a2a.task.received"] }
handler = "a2a://reviewer.prod/triage"
GitHub webhook triggers verify the X-Hub-Signature-256 HMAC against
secrets.signing_secret before enqueueing. Generic provider = "webhook"
triggers use the shared Standard Webhooks verifier. a2a-push routes
require either Authorization: Bearer <api-key> or a valid
Authorization: HMAC-SHA256 ... header before enqueueing.