Skip to content

Latest commit

 

History

History
208 lines (167 loc) · 9.79 KB

README.md

File metadata and controls

208 lines (167 loc) · 9.79 KB

Rite

Iris transports things. Rite reacts to things.

Rite is a self-hostable, minimal event-to-action runtime. It receives authenticated events, normalizes them, matches TOML-configured handlers, and forwards matching events to actions.

GitHub webhook      ──> Rite ──> configured HTTP action
Uptime Kuma webhook ──> Rite ──> configured HTTP action
Iris event          ──> Rite ──> configured HTTP action

Quick start

Create rite.toml:

[[rites]]
name = "github-pr-opened"
source = "github"
match = { event_type = "pull_request", action = "opened" }
action = { type = "http_post", url = "https://example.test/hooks/pr" }

[sources.iris]
enabled = true
base_url = "http://127.0.0.1:3000"

# Required when this ingress source is enabled.
[sources.uptime_kuma]
enabled = true
secret = "change-me"

Run it:

RITE_GITHUB_WEBHOOK_SECRET=change-me cargo run -p rite-cli -- --config rite.toml

Endpoints:

  • GET /health — returns ok
  • GET /status — live process-local event/action counters (HTTP-only)
  • GET /sources — configured source adapters
  • POST /event/github — authenticated GitHub webhook ingress using X-Hub-Signature-256
  • POST /event/uptime_kuma — authenticated Uptime Kuma webhook ingress using base64 Signature HMAC-SHA256

Process status

GET /status returns a JSON snapshot from the running Rite process with exactly these fields: events_received, events_matched, actions_succeeded, actions_failed, uptime_seconds, and handlers_loaded. The counters and uptime are process-local and reset when Rite restarts; they are not persisted, aggregated, or a view into another running server.

Status is intentionally HTTP-only. rite-cli reads local configuration rather than connecting to a running server, so it does not expose potentially misleading live metrics; the MCP manifest likewise has no status tool.

CLI and MCP

Rite's agent-facing read operations are generated from api/operations.yaml and run through the same execute_operation dispatch as HTTP. They load the local configuration and print JSON, so no HTTP server or webhook secret is required:

cargo run -p rite-cli -- list-sources --config rite.example.toml
cargo run -p rite-cli -- list-handlers --config rite.example.toml
cargo run -p rite-cli -- get-handler github-pr-opened --config rite.example.toml

rite serve --config rite.toml --github-webhook-secret "$RITE_GITHUB_WEBHOOK_SECRET" explicitly starts the server; omitting the subcommand remains equivalent for compatibility. The generated generated/mcp.json declares the same read operations for MCP hosts over the existing HTTP endpoints: GET /sources, GET /handlers, and GET /handlers/{handler_id}. No separate server surface is needed.

When enabled, the Iris source subscribes to GET /v1/events in the background. Set sources.iris.api_token (or RITE_IRIS_API_TOKEN in Docker) when Iris requires bearer authentication; Rite sends it only as the subscription's Authorization: Bearer header, including on reconnects. Without a token Rite warns at startup so legacy unauthenticated Iris deployments remain supported. It reconnects with exponential backoff (1–60 seconds), so Rite remains healthy while Iris is unavailable. Iris messages become source = "iris" events. event_type is the Iris message kind and metadata contains provider, source_id, sender, kind, iris_metadata, and the complete original Iris message under message for future matching needs.

Self-hosting with Docker

Published images are available from GitHub Container Registry after a release tag: ghcr.io/techgodhq/rite:<version> (or :latest). Supply a webhook secret and, when Rite should subscribe to Iris events, its private Iris URL:

docker run --rm \
  --name rite \
  --publish 127.0.0.1:8080:8080 \
  --env RITE_GITHUB_WEBHOOK_SECRET="${RITE_GITHUB_WEBHOOK_SECRET}" \
  --env RITE_IRIS_BASE_URL="http://iris.internal:9876" \
  --env RITE_IRIS_API_TOKEN="${RITE_IRIS_API_TOKEN}" \
  ghcr.io/techgodhq/rite:latest

RITE_GITHUB_WEBHOOK_SECRET is required for authenticated GitHub webhook ingress. Omit RITE_IRIS_BASE_URL when you do not want an Iris subscription. Set RITE_IRIS_API_TOKEN whenever that Iris instance has IRIS_API_TOKEN configured; it is not written to logs. For a complete private-network example with both services, use the Iris repository's deploy/docker-compose.yml.

HTTP actions

http_post forwards the normalized event as JSON by default. A handler may instead set body_template to render a deterministic text body using {{event_type}}, {{action}}, {{title}}, {{body}}, or nested event metadata such as {{metadata.provider}}; missing fields render empty. Optional headers are forwarded verbatim. Templated bodies default to Content-Type: text/plain unless that header is explicitly set.

For a JSON API that needs a custom object, use body_json instead of body_template (the two modes are mutually exclusive). It accepts nested TOML tables and arrays, strings, booleans, integers, finite floats, and scalar roots. TOML datetimes and non-finite floats are rejected; TOML has no null value, and Rite does not invent a null sentinel. Only configured string leaves are interpolated. The supported placeholders are the same event fields above; malformed or unknown placeholders are rejected while loading configuration. Each inserted value is treated as opaque string data and is never rescanned or parsed as JSON. A missing or null referenced value fails that action without sending it; non-string metadata is represented as JSON text within the string leaf. Static object keys, URLs, headers, and credentials remain configuration owned.

Structured mode sets Content-Type: application/json by default. An explicit content type may be application/json or application/json; charset=utf-8 (case-insensitive; quoted UTF-8 is also accepted). Other media types, parameters, charsets, or duplicate case-insensitive content-type headers are rejected during configuration loading. These rules apply only to body_json; legacy text and no-template request behavior is unchanged.

[[rites]]
name = "deploy-alert"
source = "iris"
match = { body_contains = "URGENT" }
action = { type = "http_post", url = "https://hooks.example.test/alerts", body_template = "{{title}}: {{body}}" }

[[rites]]
name = "structured-iris-message"
source = "iris"
match = { event_type = "text" }
action = { type = "http_post", url = "https://api.example.test/messages/synthetic-thread", body_json = { body = "{{body}}", provider = "telegram.synthetic" } }

The second example keeps the provider instance fixed in configuration while preserving arbitrary message text as a JSON string. Rite remains a generic HTTP action runner and does not add provider-specific action types.

Compound matching

Flat match tables AND every entry. For OR or negation, compose conditions with all_of, any_of, and not — arbitrarily nestable, with leaf keys keeping exactly the semantics above:

[[rites]]
name = "page-on-critical"
source = "iris"
match = { any_of = [{ severity = "critical" }, { body_contains = "URGENT" }] }
action = { type = "http_post", url = "https://hooks.example.test/pages" }

[[rites]]
name = "real-prs-only"
source = "github"
match = { all_of = [{ event_type = "pull_request" }, { not = { draft = true } }] }
action = { type = "http_post", url = "https://hooks.example.test/reviews" }

[[rites]]
name = "inbox-attention"
source = "iris"
match = { all_of = [{ path_equals = { path = ["metadata", "message", "source"], value = "agent_hook" } }, { path_equals = { path = ["metadata", "message", "metadata", "schema_version"], value = 1 } }, { path_equals = { path = ["metadata", "message", "metadata", "installation_id"], value = "synthetic-alpha" } }, { path_equals = { path = ["metadata", "message", "metadata", "event_kind"], value = "attention_required" } }, { path_equals = { path = ["metadata", "message", "metadata", "direction"], value = "inbound" } }, { path_equals = { path = ["metadata", "message", "metadata", "content_opt_in"], value = true } }] }
action = { type = "http_post", url = "https://hooks.example.test/inbox" }

path_equals takes a literal array of object keys rooted at the normalized RiteEvent JSON shape and a scalar string, integer, or boolean value. It never splits keys on dots, traverses arrays, or infers a source/provider. A missing path, null, or wrong JSON type is a non-match; malformed paths and non-scalar expected values are rejected while loading configuration.

all_of and any_of take nonempty arrays of condition tables; not takes one condition table. A table mixing operators with other keys — or using two operators as siblings — is rejected at config load with an actionable error. Structured values must use their declared grammar: an inline table under all_of/any_of, an array under not, or any table/array under an ordinary leaf key is rejected rather than silently becoming a non-matching leaf. A scalar under an operator-named key (for example, not = "draft") remains an ordinary metadata lookup exactly as before, so existing scalar configurations never change meaning.

Development

cargo build --all-targets
cargo test --all-targets
cargo clippy --all-targets -- -D warnings
cargo fmt --all -- --check

Rite normalizes GitHub push and pull_request payloads plus Uptime Kuma heartbeats, matches configured handlers, and executes http_post actions with the normalized event as JSON. Uptime Kuma heartbeats require a configured secret; status maps deterministically to up (info), down (critical), pending (warning), or maintenance (info). Matching-safe metadata includes monitor_name, monitor_id, status, plus present URL/timing fields; Kuma's msg becomes the optional event body.