| Crates.io | greentic-runner-host |
| lib.rs | greentic-runner-host |
| version | 0.4.45 |
| created_at | 2025-11-09 08:33:43.965878+00 |
| updated_at | 2026-01-25 22:25:09.978492+00 |
| description | Host runtime shim for Greentic runner: config, pack loading, activity handling |
| homepage | https://github.com/greentic-ai/greentic-runner |
| repository | https://github.com/greentic-ai/greentic-runner |
| max_upload_size | |
| id | 1923811 |
| size | 829,772 |
greentic-runner-host packages the Greentic runner as a standalone crate. It owns tenant bindings, the pack watcher, Wasmtime glue, canonical ingress adapters (Telegram, Teams, Slack, WebChat, Webex, WhatsApp, generic webhook, timers), the state machine (pause/resume, session/state persistence), and admin/health endpoints. Binaries such as greentic-runner or greentic-demo embed this crate instead of vendoring runtime internals.
Provider execution is provider-core only: packs declare provider runtimes via the greentic.ext.provider extension and invoke them with the provider.invoke node, instantiating greentic:provider/schema-core@1.0.0 components. Legacy typed provider worlds are not supported.
runner-core, verifies signatures/digests (PACK_PUBLIC_KEY, PACK_VERIFY_STRICT), caches artifacts under PACK_CACHE_DIR, and supports ordered overlays per tenant.PACK_REFRESH_INTERVAL drives a watcher that resolves the index, preloads packs, and swaps tenant runtimes atomically; /admin/packs/reload triggers the same path on demand. Overlays can be added/removed without touching the base pack.{
"tenant": "demo",
"provider": "slack",
"provider_ids": {
"workspace_id": "T123",
"channel_id": "C789",
"thread_id": "1731315600.000100",
"user_id": "U456",
"message_id": "1731315600.000100",
"event_id": "Ev01ABC"
},
"session": {
"key": "demo:slack:1731315600.000100:U456",
"scopes": ["chat","attachments","buttons"]
},
"timestamp": "2025-11-11T09:00:00Z",
"text": "Hi",
"attachments": [],
"buttons": [],
"entities": { "mentions": [], "urls": [] },
"metadata": { "raw_headers": {}, "ip": null },
"channel_data": { "type": "message" },
"raw": { "...": "original provider payload" }
}
Canonical session keys follow {tenant}:{provider}:{conversation-or-thread-or-channel}:{user}, ensuring pause/resume and dedupe behave consistently per adapter.greentic-session/greentic-state. Multi-turn flows pause via session.wait; the runtime stores FlowSnapshots keyed by the canonical session, resumes on the next ingress event, and clears the entry on completion. Packs can access greentic:state/store@1.0.0 when the component declares state capability and the policy allows it.greentic-telemetry), /healthz, and bearer-protected /admin endpoints (loopback-only when ADMIN_TOKEN is unset).Pack ingestion is driven by a JSON index (see examples/index.json). Each tenant entry declares a main_pack plus optional overlays, letting you layer overrides without rebuilding the base artifact:
{
"tenants": {
"demo": {
"main_pack": {
"reference": { "name": "demo-pack", "version": "1.2.3" },
"locator": "fs:///packs/demo.gtpack",
"digest": "sha256:abcd...",
"signature": "ed25519:...",
"path": "./packs/demo.gtpack"
},
"overlays": [
{
"reference": { "name": "demo-overlay", "version": "1.2.3" },
"path": "./packs/demo_overlay.gtpack",
"digest": "sha256:efgh..."
}
]
}
}
}
During a reload the watcher resolves each locator (filesystem, HTTPS, OCI, S3, GCS, Azure blob), verifies digests/signatures, caches artifacts, and constructs a TenantRuntime that loads the main pack plus overlays in order. Overlay changes are safe to deploy independently—crates/tests/tests/host_integration.rs includes regression coverage.
Runner can also execute a materialized pack directory (contains manifest.cbor, flows/templates, and components/<id>.wasm) or a .gtpack paired with local component files. Component resolution now prefers explicit overrides, then the materialized directory, and finally embedded archive entries; missing components raise a clear error. The desktop CLI exposes --components-dir / --components-map so distributor-produced layouts can run without the runner fetching OCI components itself.
The runner exposes a cache module for compiled component artifacts. Each cache entry is scoped by an EngineProfile (Wasmtime version, target triple, CPU policy, and a config fingerprint) and an ArtifactKey (engine_profile_id + wasm_digest). Disk entries are namespaced under <cache_root>/v1/<engine_profile_id>/... to prevent cross-version contamination.
The cache manager is wired into component loading with disk + memory tiers. Disk entries are serialized Wasmtime components with metadata; memory entries hold Arc<Component> with bounded eviction. The API surface (CacheManager::get_component, warmup, doctor, prune_disk) is available for future CLI extensions and diagnostics.
Packs can pause mid-flow by emitting the session.wait component. The host persists the FlowSnapshot (current node pointer + execution state) into greentic-session. The next inbound activity for the same canonical session key (tenant:provider:channel:conversation:user) automatically resumes the stored snapshot, continues execution, and clears the entry when the flow completes. This makes multi-message LLM flows and human-in-the-loop approvals idempotent without bespoke session wiring.
Each tenant bindings file may optionally declare an oauth block:
oauth:
http_base_url: https://oauth.api.greentic.net/
nats_url: nats://oauth-broker:4222
provider: greentic.oauth.default
env: prod # optional, falls back to GREENTIC_ENV/local
team: platform # optional logical scope
When present, the host wires greentic-oauth-host, keeps a tenant-scoped
client configuration, and registers the greentic:oauth-broker@1.0.0/world broker WIT world in Wasmtime. Packs that import the world can ask the host for
consent URLs, exchange codes for tokens, or fetch stored tokens, while
environments without the block behave exactly as before (no broker world is
wired).
use greentic_runner_host::{Activity, HostBuilder, HostConfig};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let config = HostConfig::load_from_path("./bindings/customera.yaml")?;
let host = HostBuilder::new().with_config(config).build()?;
host.start().await?;
host.load_pack("customera", "./packs/customera/index.ygtc".as_ref()).await?;
let activity = Activity::text("hello")
.with_tenant("customera")
.from_user("u-1");
let replies = host.handle_activity("customera", activity).await?;
for reply in replies {
println!("reply: {}", reply.payload());
}
host.stop().await?;
Ok(())
}
verify (default) – validate pack files exist before loading.mcp – enable tool invocation through the mcp-exec bridge.telemetry – wire OTLP export via greentic-telemetry.| Variable | Description | Default |
|---|---|---|
PACK_SOURCE |
Default resolver scheme when the index omits one (fs, http, oci, s3, gcs, azblob) |
fs |
PACK_INDEX_URL |
URI or path to the pack index consumed by the watcher | required |
PACK_CACHE_DIR |
Location for the content-addressed pack cache | .packs |
PACK_PUBLIC_KEY |
Optional Ed25519 key (ed25519:BASE64...) to enforce signature checks |
unset |
PACK_VERIFY_STRICT |
Force signature verification even when no PACK_PUBLIC_KEY is provided (1/true), or opt out when the key is present (0/false) |
PACK_PUBLIC_KEY driven |
PACK_REFRESH_INTERVAL |
Interval used by the background watcher (30s, 5m, etc.) |
30s |
TENANT_RESOLVER |
Router mode for HTTP requests (host, header, jwt, env) |
env |
DEFAULT_TENANT |
Fallback tenant identifier when the resolver cannot infer one | demo |
SECRETS_BACKEND |
Secrets provider to initialise (env, aws, gcp, azure) |
env |
OTEL_SERVICE_NAME |
Overrides the OTLP service name advertised to the collector | greentic-runner-host |
OTEL_EXPORTER_OTLP_ENDPOINT |
Explicit OTLP collector endpoint | provider preset / unset |
ADMIN_TOKEN |
Bearer token required for /admin endpoints (loopback-only access if unset) |
unset |
SLACK_SIGNING_SECRET |
HMAC secret for Slack Events/Interactive adapters | unset |
WEBEX_WEBHOOK_SECRET |
Signature key for Cisco Webex webhook validation | unset |
WHATSAPP_VERIFY_TOKEN / WHATSAPP_APP_SECRET |
Verification + signature secrets for WhatsApp Cloud API | unset |
PACK_VERIFY_STRICT |
Enforce signature checks even without a public key | driven by key |
| Method | Path | Description |
|---|---|---|
GET |
/healthz |
Liveness check (telemetry, secrets, active packs) |
GET |
/admin/packs/status |
Lists loaded tenants, versions, and digests plus last reload info |
POST |
/admin/packs/reload |
Triggers an immediate pack refresh via the watcher |
If ADMIN_TOKEN is set, clients must send Authorization: Bearer <token>; otherwise, admin endpoints are limited to loopback connections.
| Adapter | Route | Session anchor | Notes / Env |
|---|---|---|---|
| Telegram Bot API | POST /messaging/telegram/webhook |
chat.id:user.id (fallback to user.id) |
Uses update_id for dedupe; outbound path relies on TELEGRAM_BOT_TOKEN |
| Microsoft Teams (Bot Framework) | POST /teams/activities |
replyToId → conversation.id → channel |
Accepts Activities JSON (channelData, attachments) |
| Slack Events API | POST /slack/events |
thread_ts → channel |
Requires SLACK_SIGNING_SECRET, dedupes via event_id, handles retries |
| Slack Interactive | POST /slack/interactive |
channel/thread from payload |
Same signing secret; parses payload= form body |
| WebChat / Direct Line | POST /webchat/activities |
conversation.id |
Mirrors Bot Framework schema; attachments mapped 1:1 |
| Cisco Webex | POST /webex/webhook |
parentId → roomId |
Optional WEBEX_WEBHOOK_SECRET; keeps requires_auth metadata for file URLs |
| WhatsApp Cloud API | GET/POST /whatsapp/webhook |
messages[].from |
WHATSAPP_VERIFY_TOKEN (challenge) + WHATSAPP_APP_SECRET (signature); interactive/list replies → canonical buttons |
| Generic Webhook | ANY /webhook/:flow_id |
Idempotency-Key header (if present) |
Wraps method/path/headers/body into canonical payload |
| Timers / Cron | Defined in bindings.yaml |
schedule_id |
Schedules flows with normalized cron (seconds field injected) |
Each adapter injects the canonical payload (tenant, provider, provider_ids, session, timestamp, text, attachments, buttons, entities, metadata, channel_data, raw) and uses the same session-key policy {tenant}:{provider}:{conversation-or-thread-or-channel}:{user} enforced everywhere. Custom adapters can follow the same pattern by translating incoming payloads into an IngressEnvelope.
This project is licensed under the MIT License.