sekia Documentation
Complete reference for the multi-agent event bus for workflow automation.
What is sekia?
sekia is an event bus that connects external services (GitHub, Slack, Linear, Gmail, Google Calendar) to Lua workflows via embedded NATS messaging. Events from agents flow into the bus, Lua handlers react to them, and commands flow back out to agents.
The system follows Unix philosophy: small, composable tools. Seven standalone binaries communicate over NATS:
sekiad— the daemon (embedded NATS, workflow engine, API)sekiactl— CLI to inspect and control the daemonsekia-github— GitHub agent (webhooks + polling)sekia-slack— Slack agent (Socket Mode)sekia-linear— Linear agent (GraphQL polling)sekia-google— Google agent (Gmail + Calendar)sekia-mcp— MCP server for AI assistants
Installation
Homebrew
Each component is a separate formula. Install the daemon/CLI first, then any agents you need:
# Daemon + CLI (required)
brew install sekia-ai/tap/sekia
# Agents (install the ones you need)
brew install sekia-ai/tap/sekia-github
brew install sekia-ai/tap/sekia-slack
brew install sekia-ai/tap/sekia-linear
brew install sekia-ai/tap/sekia-google
brew install sekia-ai/tap/sekia-mcp
Each formula (except sekia-mcp) includes a launchd service definition. Use brew services to run them as background services:
# Start the daemon and agents as background services
brew services start sekia
brew services start sekia-github
brew services start sekia-slack
# Check running services
brew services list
# Stop a service
brew services stop sekia-github
Logs are written to $(brew --prefix)/var/log/<formula>.log (e.g. sekiad.log, sekia-github.log).
Go
go install github.com/sekia-ai/sekia/cmd/sekiad@latest
go install github.com/sekia-ai/sekia/cmd/sekiactl@latest
go install github.com/sekia-ai/sekia/cmd/sekia-github@latest
go install github.com/sekia-ai/sekia/cmd/sekia-slack@latest
go install github.com/sekia-ai/sekia/cmd/sekia-linear@latest
go install github.com/sekia-ai/sekia/cmd/sekia-google@latest
go install github.com/sekia-ai/sekia/cmd/sekia-mcp@latest
Docker
git clone https://github.com/sekia-ai/sekia.git
cd sekia
docker compose up
Build from source
git clone https://github.com/sekia-ai/sekia.git
cd sekia
go build ./cmd/sekiad ./cmd/sekiactl ./cmd/sekia-github \
./cmd/sekia-slack ./cmd/sekia-linear ./cmd/sekia-google \
./cmd/sekia-mcp
Quick Start
Get sekia running in four steps.
1. Start the daemon
sekiad
This starts the embedded NATS server, workflow engine, and Unix socket API. No configuration file needed for defaults.
Or run it as a background service with Homebrew:
brew services start sekia
2. Write a workflow
Create a Lua file in ~/.config/sekia/workflows/:
-- ~/.config/sekia/workflows/auto-label.lua
sekia.on("sekia.events.github", function(event)
if event.type ~= "github.issue.opened" then return end
local title = string.lower(event.payload.title or "")
if string.find(title, "bug") then
sekia.command("github-agent", "add_label", {
owner = event.payload.owner,
repo = event.payload.repo,
number = event.payload.number,
label = "bug",
})
end
end)
3. Connect an agent
export GITHUB_TOKEN=ghp_...
sekia-github
4. Check status
sekiactl status
sekiactl agents
sekiactl workflows
Architecture
Event flow
The core pattern is: Agent → NATS event → Lua workflow → NATS command → Agent
External Service (GitHub, Slack, etc.)
|
v
Agent binary (sekia-github, sekia-slack, etc.)
| publishes to sekia.events.<source>
v
Embedded NATS (inside sekiad)
|
v
Lua workflow engine (pattern matching)
| sekia.command("agent-name", "cmd", payload)
v
NATS sekia.commands.<agent-name>
|
v
Agent binary executes command via external API
NATS runs in-process
By default, NATS is embedded inside sekiad with DontListen: true — no TCP port is opened. Agents connect using nats.InProcessServer() when running locally, or via a NATS URL when running remotely.
Config file search paths
All binaries search for their config file in this order:
/etc/sekia/~/.config/sekia/- Current directory (
./)
Override with the --config flag on any binary. Environment variables always take precedence over config file values.
sekiad (Daemon)
The daemon is the central process. It runs the embedded NATS server, agent registry, Lua workflow engine, Unix socket API, and optional web dashboard.
Usage
sekiad [--config /path/to/sekia.toml]
Flags
| Flag | Description |
|---|---|
--config | Path to config file (overrides search paths) |
--version | Show version and exit |
Startup sequence
- Start embedded NATS with JetStream
- Create agent registry (subscribes to
sekia.registryandsekia.heartbeat.>) - Start workflow engine, load
.luafiles, optionally start file watcher - Start HTTP API on Unix socket
- Start web dashboard on TCP port (if configured)
- Block on OS signal or stop channel
- Shutdown in reverse order
Daemon Configuration
Config file: sekia.toml. Environment variable prefix: SEKIA_.
[server]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| socket | string | ~/.config/sekia/sekiad.sock | SEKIA_SERVER_SOCKET | Unix socket path for the API (or $XDG_RUNTIME_DIR/sekia/sekiad.sock when set) |
[nats]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| embedded | bool | true | SEKIA_NATS_EMBEDDED | Run embedded NATS server (vs connecting to remote) |
| data_dir | string | ~/.local/share/sekia/nats | SEKIA_NATS_DATA_DIR | JetStream data directory |
| host | string | — | SEKIA_NATS_HOST | NATS hostname (if not embedded) |
| port | int | — | SEKIA_NATS_PORT | NATS port (if not embedded) |
| token | string | — | SEKIA_NATS_TOKEN | NATS authentication token |
[workflows]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| dir | string | ~/.config/sekia/workflows | SEKIA_WORKFLOWS_DIR | Directory containing .lua workflow files |
| hot_reload | bool | true | SEKIA_WORKFLOWS_HOT_RELOAD | Watch for file changes and auto-reload |
| handler_timeout | duration | 30s | SEKIA_WORKFLOWS_HANDLER_TIMEOUT | Max execution time for a Lua handler |
| verify_integrity | bool | false | SEKIA_WORKFLOWS_VERIFY_INTEGRITY | Verify .lua files against SHA256 manifest before loading |
[web]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| listen | string | — | SEKIA_WEB_LISTEN | TCP address for web dashboard (e.g. :8080). Empty = disabled |
| username | string | — | SEKIA_WEB_USERNAME | HTTP Basic Auth username. Empty = no auth |
| password | string | — | SEKIA_WEB_PASSWORD | HTTP Basic Auth password. Empty = no auth |
[ai]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| provider | string | anthropic | SEKIA_AI_PROVIDER | LLM provider (currently only anthropic) |
| api_key | string | — | SEKIA_AI_API_KEY | Anthropic API key for sekia.ai() calls |
| model | string | claude-sonnet-4-20250514 | SEKIA_AI_MODEL | Default Claude model |
| max_tokens | int | 1024 | SEKIA_AI_MAX_TOKENS | Default max tokens for responses |
| temperature | float | 0.0 | SEKIA_AI_TEMPERATURE | Default temperature (0.0–1.0) |
| system_prompt | string | — | SEKIA_AI_SYSTEM_PROMPT | Default system prompt |
[security]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| command_secret | string | — | SEKIA_COMMAND_SECRET | HMAC-SHA256 secret for command authentication |
[secrets]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| identity | string | ~/.config/sekia/age.key | SEKIA_AGE_KEY_FILE | Path to age identity (private key) file for ENC[...] decryption |
| aws_region | string | — | — | AWS region override for KMS[...] and ASM[...] resolution |
| kms_key_id | string | — | SEKIA_KMS_KEY_ID | Default KMS key ID/ARN/alias for sekiactl secrets kms-encrypt |
Additional env vars: SEKIA_AGE_KEY (raw age secret key string). AWS credentials use the standard SDK v2 default chain (env vars, shared credentials file, IAM instance role, ECS task role).
Example config file
# ~/.config/sekia/sekia.toml
[server]
# socket defaults to ~/.config/sekia/sekiad.sock (or $XDG_RUNTIME_DIR/sekia/sekiad.sock)
# socket = "/tmp/sekiad.sock" # override for Docker or custom setups
[nats]
embedded = true
data_dir = "~/.local/share/sekia/nats"
[workflows]
dir = "~/.config/sekia/workflows"
hot_reload = true
handler_timeout = "30s"
verify_integrity = false
[web]
listen = ":8080"
[ai]
api_key = "sk-ant-..."
model = "claude-sonnet-4-20250514"
# [secrets]
# identity = "~/.config/sekia/age.key" # age key file (default)
# aws_region = "us-east-1" # for KMS[...] and ASM[...] values
# kms_key_id = "alias/sekia" # default KMS key for CLI encrypt
sekiactl (CLI)
Command-line tool to inspect and control the sekiad daemon via Unix socket.
Global flags
| Flag | Default | Description |
|---|---|---|
--socket | ~/.config/sekia/sekiad.sock | Path to sekiad Unix socket |
--version | — | Show version and exit |
sekiactl status
Show daemon health and uptime.
$ sekiactl status
Status: ok
Uptime: 15m30s
NATS Running: true
Started At: 2026-02-15 10:30:45
Agents: 3
sekiactl agents
List all registered agents.
$ sekiactl agents
NAME VERSION STATUS EVENTS ERRORS LAST HEARTBEAT
github-agent v0.2.0 alive 42 0 2s ago
slack-agent v0.2.0 alive 18 0 5s ago
linear-agent v0.2.0 alive 7 0 12s ago
sekiactl workflows
List loaded workflows. Alias for sekiactl workflows list.
$ sekiactl workflows
NAME HANDLERS PATTERNS EVENTS ERRORS LOADED AT
auto-label 1 sekia.events.github 15 0 10m ago
ai-classifier 1 sekia.events.github 8 0 10m ago
sekiactl workflows reload
Trigger hot-reload of all .lua workflow files from disk.
$ sekiactl workflows reload
Workflows reloaded successfully
sekiactl workflows sign
Generate or update the SHA256 manifest (workflows.sha256) for workflow files. Required when verify_integrity is enabled.
$ sekiactl workflows sign
Signed 3 workflow(s) in /Users/me/.config/sekia/workflows
a1b2c3d4e5... github-labeler.lua
f6a7b8c9d0... slack-responder.lua
1234567890... linear-triage.lua
# Custom directory
$ sekiactl workflows sign --dir /path/to/workflows
sekiactl config reload
Broadcast a config reload signal via NATS. All agents re-read their config files and apply settings that are safe to change at runtime. Credentials and connection settings (tokens, API keys, listen addresses) require a restart.
# Reload all agents and daemon
$ sekiactl config reload
# Reload a specific agent
$ sekiactl config reload --target github-agent
# Reload daemon only
$ sekiactl config reload --target sekiad
Hot-reloadable settings per agent
| Agent | Reloadable settings | Requires restart |
|---|---|---|
| github-agent | poll.interval, poll.repos, poll.labels, poll.per_tick, poll.state | github.token, webhook.listen, webhook.secret |
| slack-agent | security.command_secret | slack.bot_token, slack.app_token |
| linear-agent | poll.interval, poll.team_filter | linear.api_key |
| google-agent | gmail.poll_interval, gmail.query, gmail.max_messages, calendar.poll_interval, calendar.upcoming_mins | google.client_id, google.client_secret, google.token_path |
All agents also hot-reload security.command_secret.
Secret value formats
Three inline formats are supported in TOML config files. Backends are lazily initialized — only when values with that prefix are detected:
| Format | Backend | Description |
|---|---|---|
ENC[<base64>] | age | Decrypt with local age identity |
KMS[<base64>] | AWS KMS | Decrypt via KMS API (key ID embedded in cipherblob) |
ASM[<name-or-arn>] | AWS Secrets Manager | Fetch plaintext secret by name or ARN |
All formats can be mixed within the same config file. For example:
# sekia-github.toml
[github]
token = "ENC[YWdlLWVuY3J5cHRpb24...]" # age-encrypted
[webhook]
secret = "KMS[AQIDAHhB...]" # KMS-encrypted
[nats]
token = "ASM[prod/sekia/nats-token]" # Secrets Manager reference
sekiactl secrets keygen
Generate a new age X25519 keypair for encrypting config secrets. The private key is written to a file (default ~/.config/sekia/age.key with mode 0600) and the public key is printed to stdout.
$ sekiactl secrets keygen
Key file written to: /home/user/.config/sekia/age.key
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
# Custom path
$ sekiactl secrets keygen --output /path/to/key.age
sekiactl secrets encrypt
Encrypt a plaintext value and output an ENC[...] string for pasting into a TOML config file.
# Uses the default key file's public key
$ sekiactl secrets encrypt "ghp_mytoken123"
ENC[YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IF...]
# Explicit recipient
$ sekiactl secrets encrypt --recipient age1abc... "xoxb-slack-token"
sekiactl secrets decrypt
Decrypt an ENC[...] value using the configured identity. Useful for debugging.
$ sekiactl secrets decrypt "ENC[YWdlLWVuY3J5cHRpb24...]"
ghp_mytoken123
sekiactl secrets kms-encrypt
Encrypt a plaintext value using an AWS KMS key and output a KMS[...] string. The key ID can be an ARN, alias, or key ID.
# Explicit key ID
$ sekiactl secrets kms-encrypt --key-id alias/sekia-config "ghp_mytoken123"
KMS[AQIDAHhBm4p0...]
# Key ID from env or config
$ export SEKIA_KMS_KEY_ID="arn:aws:kms:us-east-1:123456789:key/abc-def"
$ sekiactl secrets kms-encrypt "xoxb-slack-token"
sekiactl secrets kms-decrypt
Decrypt a KMS[...] value using the AWS KMS API. No key ID needed — it is embedded in the ciphertext blob.
$ sekiactl secrets kms-decrypt "KMS[AQIDAHhBm4p0...]"
ghp_mytoken123
sekiactl secrets asm-get
Fetch a plaintext secret from AWS Secrets Manager. Only plaintext secrets are supported; binary secrets return an error.
$ sekiactl secrets asm-get prod/sekia/github-token
ghp_mytoken123
AWS configuration
AWS credentials are resolved via the standard SDK v2 default chain (environment variables, shared credentials file, IAM instance role, ECS task role, etc.). An optional region override can be set:
# In sekia.toml (or any agent config)
[secrets]
aws_region = "us-east-1" # optional region override
kms_key_id = "alias/sekia" # default KMS key for kms-encrypt CLI
KMS auto-rotation is fully supported — AWS preserves all previous key material versions, so existing ciphertexts remain decryptable after rotation.
sekiactl service
Manage named agent instances as background services (launchd on macOS, systemd on Linux). See Named Instances for full documentation.
$ sekiactl service create sekia-github --name github-work
$ sekiactl service start github-work
$ sekiactl service stop github-work
$ sekiactl service restart github-work
$ sekiactl service remove github-work
$ sekiactl service list
sekiactl skills list
List loaded skills.
$ sekiactl skills list
Workflows
Workflows are Lua scripts that react to events and send commands. They live in the workflows directory (default: ~/.config/sekia/workflows/) and are loaded automatically when the daemon starts.
How it works
- The daemon loads every
.luafile from the workflows directory - Each workflow gets its own Lua state and goroutine (thread-safe)
- Workflows register handlers with
sekia.on(pattern, fn) - When an event matches a pattern, the handler function is called
- Handlers can send commands to agents, publish new events, or call LLMs
Sandboxing
Workflows run in a restricted Lua environment. Only these standard libraries are available:
base— core functions (minusdofile,loadfile,load)table— table manipulationstring— string operationsmath— math functions
The os, io, and debug libraries are not available.
Self-event guard
When a workflow publishes an event, the event source is set to workflow:<name>. Events from workflow:<name> are automatically skipped by handlers in the same workflow, preventing infinite loops.
Hot-reload
When hot_reload = true (the default), the daemon uses fsnotify to watch the workflows directory. File changes trigger a full reload with a 500ms debounce. You can also trigger a manual reload via sekiactl workflows reload or the HTTP API.
Integrity verification
When verify_integrity = true, the daemon verifies each .lua file against a SHA256 manifest (workflows.sha256) before loading it. Files not in the manifest or with a hash mismatch are rejected.
Generate the manifest with the CLI:
sekiactl workflows sign [--dir <path>]
The manifest uses sha256sum-compatible format (<64-hex> <filename>). When hot-reload is enabled, changes to workflows.sha256 automatically trigger a full reload of all workflows.
Workflow: edit .lua file → load rejected (hash mismatch) → run sekiactl workflows sign → manifest updated → fsnotify triggers reload → all files pass verification.
Lua API Reference
The sekia global table is available in all workflows.
sekia.on(pattern, handler)
Register an event handler for a NATS subject pattern.
| Parameter | Type | Description |
|---|---|---|
| pattern | string | NATS subject pattern. Supports * (single segment) and > (all remaining segments) |
| handler | function | Callback receiving an event table |
-- Match all GitHub events
sekia.on("sekia.events.github", function(event)
sekia.log("info", "Got event: " .. event.type)
end)
-- Match all events from any source
sekia.on("sekia.events.>", function(event)
-- handles github, slack, linear, google, etc.
end)
-- Match events from a specific wildcard source
sekia.on("sekia.events.*", function(event)
-- matches sekia.events.github, sekia.events.slack, etc.
end)
Event object shape
The event table passed to handlers has these fields:
| Field | Type | Description |
|---|---|---|
| id | string | Unique event ID (UUID) |
| type | string | Event type (e.g. "github.issue.opened") |
| source | string | Event source (e.g. "github", "workflow:my-flow") |
| timestamp | number | Unix timestamp |
| payload | table | Event-specific data (varies by event type) |
sekia.publish(subject, event_type, payload)
Emit a new event onto the NATS bus.
| Parameter | Type | Description |
|---|---|---|
| subject | string | NATS subject (e.g. "sekia.events.custom") |
| event_type | string | Event type string |
| payload | table | Arbitrary event data |
Raises a Lua error on failure.
sekia.publish("sekia.events.custom", "deploy.started", {
environment = "production",
version = "v1.2.3",
})
sekia.command(agent, command, payload)
Send a command to a connected agent. The command is published to sekia.commands.<agent>.
| Parameter | Type | Description |
|---|---|---|
| agent | string | Target agent name (e.g. "github-agent", "slack-agent") |
| command | string | Command name (e.g. "add_label", "send_message") |
| payload | table | Command-specific data |
Raises a Lua error on failure.
sekia.command("github-agent", "create_comment", {
owner = "myorg",
repo = "myrepo",
number = 42,
body = "Automated comment from sekia",
})
sekia.log(level, message)
Log a message via zerolog.
| Parameter | Type | Description |
|---|---|---|
| level | string | Log level: "debug", "info", "warn", "error" |
| message | string | Log message |
sekia.log("info", "Processing issue #" .. num)
sekia.ai(prompt [, opts])
Call an LLM synchronously. Returns the response text. Each workflow has its own goroutine, so this blocks only the current workflow.
| Parameter | Type | Description |
|---|---|---|
| prompt | string | The prompt to send to the LLM |
| opts | table (optional) | Options: model, max_tokens, temperature, system |
Returns: (result, err) — result is a string or nil, err is nil or a string. Never raises a Lua error. Returns (nil, "AI not configured") if no API key is set. Timeout: 120 seconds.
local result, err = sekia.ai("Summarize this text: " .. text)
if err then
sekia.log("error", "AI failed: " .. err)
return
end
-- With options
local result, err = sekia.ai("Classify this issue", {
model = "claude-sonnet-4-20250514",
max_tokens = 100,
temperature = 0,
system = "Reply with one word only.",
})
sekia.ai_json(prompt [, opts])
Like sekia.ai() but requests a JSON response and parses it into a Lua table.
Returns: (table, err) — result is a Lua table or nil.
local data, err = sekia.ai_json("Return JSON with keys: label, confidence")
if data then
sekia.log("info", "Label: " .. data.label .. ", Confidence: " .. data.confidence)
end
sekia.skill(name)
Returns the full instructions for a named skill (from SKILL.md files). Returns an empty string if the skill is not found or skills are not configured.
local instructions = sekia.skill("pr-review")
sekia.log("info", "Skill instructions: " .. instructions)
sekia.conversation(platform, channel [, thread])
Returns a conversation handle for multi-turn interactions. Conversations are keyed by (platform, channel, thread) and stored in memory with TTL eviction.
The returned table has these methods:
conv:append(role, content)— add a message to the conversation historyconv:reply(prompt)— append the prompt as a user message, send full history to the LLM, store the response. Returns(response, err)conv:history()— returns a table of{role=..., content=...}entriesconv:metadata(key [, value])— get or set per-conversation metadata
local conv = sekia.conversation("slack", event.payload.channel, event.payload.thread_ts)
conv:append("user", "hello")
local response, err = conv:reply("What can you help with?")
if response then
sekia.log("info", "AI said: " .. response)
end
-- Per-conversation metadata
conv:metadata("mood", "happy")
local mood = conv:metadata("mood") -- "happy"
sekia.schedule(interval_seconds, handler)
Registers a timer-driven handler that fires at the given interval (minimum 1 second). Enables autonomous agent behavior without external triggers.
sekia.schedule(300, function()
local result, err = sekia.ai("Any PRs needing attention?")
if err then return end
sekia.command("slack-agent", "send_message", {
channel = "#engineering",
text = result,
})
end)
sekia.name
A string containing the current workflow's name (derived from the filename without .lua extension).
sekia.log("info", "Running workflow: " .. sekia.name)
Workflow Examples
Auto-label GitHub issues
Labels issues based on keywords in the title and posts a welcome comment.
-- github-auto-label.lua
sekia.on("sekia.events.github", function(event)
if event.type ~= "github.issue.opened" then return end
local title = string.lower(event.payload.title or "")
local owner = event.payload.owner
local repo = event.payload.repo
local num = event.payload.number
if string.find(title, "bug") or string.find(title, "crash") then
sekia.command("github-agent", "add_label", {
owner = owner, repo = repo, number = num, label = "bug",
})
end
if string.find(title, "feature") or string.find(title, "request") then
sekia.command("github-agent", "add_label", {
owner = owner, repo = repo, number = num, label = "enhancement",
})
end
sekia.command("github-agent", "create_comment", {
owner = owner, repo = repo, number = num,
body = "Thanks for opening this issue! A maintainer will review it shortly.",
})
end)
AI-powered issue classifier
Uses an LLM to classify issues and apply the appropriate label.
-- ai-issue-classifier.lua
sekia.on("sekia.events.github", function(event)
if event.type ~= "github.issue.opened" then return end
local prompt = "Classify this GitHub issue. Reply with exactly one word: bug, feature, question, or docs.\n\n"
.. "Title: " .. (event.payload.title or "") .. "\n"
.. "Body: " .. (event.payload.body or "")
local result, err = sekia.ai(prompt, { max_tokens = 16, temperature = 0 })
if err then
sekia.log("error", "AI classification failed: " .. err)
return
end
local label = string.lower(string.gsub(result, "%s+", ""))
sekia.command("github-agent", "add_label", {
owner = event.payload.owner,
repo = event.payload.repo,
number = event.payload.number,
label = label,
})
end)
Auto-reply to urgent emails
-- gmail-auto-reply.lua
-- Requires: sekia-google with gmail.enabled = true
sekia.on("sekia.events.google", function(event)
if event.type ~= "gmail.message.received" then return end
local subject = string.lower(event.payload.subject or "")
if string.find(subject, "urgent") or string.find(subject, "critical") then
sekia.command("google-agent", "reply_email", {
thread_id = event.payload.thread_id,
in_reply_to = event.payload.message_id,
to = event.payload.from,
subject = "Re: " .. (event.payload.subject or ""),
body = "Thank you. This has been flagged as urgent and will be reviewed promptly.",
})
end
end)
Calendar event notifications
-- google-calendar-notify.lua
-- Requires: sekia-google with calendar.enabled = true
sekia.on("sekia.events.google", function(event)
if event.type == "google.calendar.event.created" then
sekia.log("info", "New event: " .. (event.payload.summary or "untitled"))
-- Notify via Slack
sekia.command("slack-agent", "send_message", {
channel = "#calendar",
text = "New event: " .. event.payload.summary .. " at " .. event.payload.start,
})
end
if event.type == "google.calendar.event.upcoming" then
sekia.log("info", "Starting in " .. event.payload.minutes_until .. "m: " .. (event.payload.summary or "untitled"))
end
end)
Persona — Agent Identity
A markdown file defines your agent's personality, communication style, values, and boundaries. This content is automatically prepended to every AI system prompt (both sekia.ai() and conv:reply()).
Default path: ~/.config/sekia/persona.md. Configure with ai.persona_path in sekia.toml.
Prompt layering order: [JSON mode prefix] + [persona.md] + [skills index] + [per-call system prompt]
# ~/.config/sekia/persona.md
You are a helpful engineering assistant for the Acme team.
## Communication style
- Be concise, technical, and direct
- When in doubt, ask for clarification rather than guessing
- Use code examples when explaining concepts
## Boundaries
- Never approve PRs without human review
- Escalate security issues immediately
Skills — Capability Definitions
Skills are directory-based capability definitions with YAML frontmatter and natural language instructions. Place them in the skills directory (default ~/.config/sekia/skills/).
Each skill is a directory containing a SKILL.md file and an optional handler.lua workflow.
# Directory structure
~/.config/sekia/skills/
pr-review/
SKILL.md # YAML frontmatter + instructions
handler.lua # optional, auto-loaded as workflow
triage/
SKILL.md
SKILL.md format:
---
name: pr-review
description: Reviews GitHub PRs for code quality
triggers:
- github.pr.opened
version: "1.0"
---
When reviewing a PR:
1. Check for test coverage
2. Look for security issues
3. Approve if only minor style issues
Skills metadata is automatically injected as a compact index into every AI prompt, so the LLM knows what capabilities are available. Use sekia.skill(name) to get the full instructions for a specific skill.
Config: [skills] section — dir, hot_reload.
CLI: sekiactl skills list. API: GET /api/v1/skills.
Conversations — Multi-Turn State
The conversation store provides in-memory multi-turn conversation state keyed by (platform, channel, thread), with configurable history limits and TTL eviction.
-- Conversational Slack bot
sekia.on("sekia.events.slack", function(event)
if event.type ~= "slack.mention" then return end
local conv = sekia.conversation("slack", event.payload.channel,
event.payload.thread_ts or event.payload.timestamp)
local text = event.payload.text:gsub("<@[^>]+>%s*", "")
local response, err = conv:reply(text)
if err then return end
sekia.command("slack-agent", "send_reply", {
channel = event.payload.channel,
thread_ts = event.payload.thread_ts or event.payload.timestamp,
text = response,
})
end)
Config: [conversation] section — max_history (default 50), ttl (default 1h).
Scheduled Workflows
Use sekia.schedule() to register handlers that fire on a timer, enabling autonomous agent behavior without external triggers. Handlers run in the same goroutine as event handlers, ensuring thread safety.
-- Autonomous PR reviewer (every 5 minutes)
sekia.schedule(300, function()
local result, err = sekia.ai_json("Review open PRs needing attention", {
system = sekia.skill("pr-review")
})
if err then return end
for _, pr in ipairs(result.prs or {}) do
sekia.command("github-agent", "create_comment", {
owner = pr.owner, repo = pr.repo,
number = pr.number, body = pr.review_comment,
})
end
end)
Sentinel — Proactive AI Checks
Sentinel reads a markdown checklist on a configurable interval, gathers system context (connected agents, loaded workflows), and asks the AI if anything needs attention. Unlike cron, sentinel thinks about whether something matters.
Checklist example (~/.config/sekia/sentinel.md):
- Are there GitHub PRs open >3 days without review?
- Are there urgent Linear tickets with no assignee?
- Have any agents gone offline since last check?
- Are there unread emails from important contacts?
Events are published to sekia.events.sentinel with types sentinel.action.required and sentinel.check.complete. Handle them in workflows like any other event:
-- React to sentinel findings
sekia.on("sekia.events.sentinel", function(event)
if event.type == "sentinel.action.required" then
sekia.command("slack-agent", "send_message", {
channel = "#ops",
text = event.payload.reasoning,
})
end
end)
Config: [sentinel] section — enabled (default false), interval (default 5m), checklist_path (default ~/.config/sekia/sentinel.md).
Named Instances (Multi-Tenancy)
All agent binaries support a --name flag for running multiple instances of the same agent type with different configurations.
# Two GitHub agents for different accounts
sekia-github --name github-personal # reads sekia-github-personal.toml
sekia-github --name github-work # reads sekia-github-work.toml
When --name is set:
- Config file becomes
sekia-{agent}-{name}.toml(unless--configoverrides) - NATS registration uses the instance name (e.g.,
github-workinstead ofgithub-agent) - Command subject becomes
sekia.commands.{name}— target instances from workflows viasekia.command("github-work", "add_label", payload)
Without --name, behavior is unchanged (default names: github-agent, slack-agent, etc.).
Background Services
Use sekiactl service to manage named instances as background services (launchd on macOS, systemd on Linux):
# Create a service
sekiactl service create sekia-github --name github-work
# Start / stop / restart
sekiactl service start github-work
sekiactl service stop github-work
sekiactl service restart github-work
# List all managed services
sekiactl service list
# Remove (stops and deletes the service file)
sekiactl service remove github-work
CLI Reference
| Command | Description |
|---|---|
sekiactl service create <binary> --name <instance> | Generate a service file for a named instance |
sekiactl service start <name> | Start a named service |
sekiactl service stop <name> | Stop a named service |
sekiactl service restart <name> | Restart a named service |
sekiactl service remove <name> | Stop and delete the service file |
sekiactl service list | List all managed sekia services |
Create flags
| Flag | Description |
|---|---|
--name <instance> | Instance name (required) |
--config <path> | Explicit config file path (optional) |
--env KEY=VALUE | Environment variable (optional, repeatable). Stored in plaintext — prefer ENC[...] config values |
Platform details
| Platform | Service file location | Backend |
|---|---|---|
| macOS | ~/Library/LaunchAgents/com.sekia.{name}.plist | launchd |
| Linux | ~/.config/systemd/user/sekia-{name}.service | systemd (user units) |
Logs are written to ~/.config/sekia/logs/{name}.log. The default brew services-managed instance is not affected.
GitHub Agent
Standalone binary (sekia-github) that bridges GitHub to the NATS event bus via webhooks and/or REST API polling.
Usage
sekia-github [--name <instance>] [--config /path/to/sekia-github.toml]
Configuration
Config file: sekia-github.toml. At least one of webhook or polling must be enabled.
[nats]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| url | string | nats://127.0.0.1:4222 | SEKIA_NATS_URL | NATS server URL |
| token | string | — | SEKIA_NATS_TOKEN | NATS auth token |
[github]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| token | string | — | GITHUB_TOKEN | GitHub Personal Access Token (required) |
[webhook]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| listen | string | :8080 | SEKIA_WEBHOOK_LISTEN | HTTP listen address. Empty = disabled |
| secret | string | — | GITHUB_WEBHOOK_SECRET | HMAC-SHA256 secret for webhook verification |
| path | string | /webhook | SEKIA_WEBHOOK_PATH | HTTP path for the webhook endpoint |
[poll]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| enabled | bool | false | SEKIA_POLL_ENABLED | Enable REST API polling |
| interval | duration | 30s | SEKIA_POLL_INTERVAL | Polling interval |
| repos | []string | — | SEKIA_POLL_REPOS | List of "owner/repo" to poll (required when enabled) |
| per_tick | int | 100 | SEKIA_POLL_PER_TICK | Max items fetched per tick (1–100) |
| labels | []string | — | SEKIA_POLL_LABELS | Filter issues by labels (enables label-filtered mode) |
| state | string | open | SEKIA_POLL_STATE | Issue state filter: open, closed, or all |
[security]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| command_secret | string | — | SEKIA_COMMAND_SECRET | HMAC-SHA256 secret for command auth |
Webhook vs Polling
Webhooks provide real-time, fine-grained events (labeled, assigned, reopened) but require a publicly accessible URL. Polling works behind firewalls but can only detect opened/closed/updated states. Both can run simultaneously; polled events include payload.polled = true.
Label-filtered mode
When poll.labels is set, the poller switches to label-filtered mode: it queries issues by label and state instead of time, skips PRs/comments, and emits github.issue.matched events instead of the standard event types.
Rate limits
At startup, the agent logs a warning if the estimated API call rate (3 calls/repo/cycle) exceeds 80% of GitHub's 5,000 requests/hour limit.
Event types
All events are published to sekia.events.github. Filter by event.type in your workflow.
| Event Type | Source | Payload Fields |
|---|---|---|
github.issue.opened | Webhook + Polling | owner, repo, number, title, body, author, url, labels |
github.issue.closed | Webhook + Polling | owner, repo, number, title, body, author, url, labels |
github.issue.reopened | Webhook only | owner, repo, number, title, body, author, url, labels |
github.issue.labeled | Webhook only | owner, repo, number, title, body, author, url, labels, label |
github.issue.assigned | Webhook only | owner, repo, number, title, body, author, url, labels, assignee |
github.issue.updated | Polling only | owner, repo, number, title, body, author, url, labels |
github.issue.matched | Label-filtered polling | owner, repo, number, title, body, author, url, state, labels |
github.pr.opened | Webhook + Polling | owner, repo, number, title, body, author, head_branch, base_branch, url |
github.pr.closed | Webhook + Polling | owner, repo, number, title, body, author, head_branch, base_branch, url |
github.pr.merged | Webhook + Polling | owner, repo, number, title, body, author, head_branch, base_branch, url, merge_commit |
github.pr.review_requested | Webhook only | owner, repo, number, title, body, author, head_branch, base_branch, url, reviewer |
github.pr.updated | Polling only | owner, repo, number, title, body, author, head_branch, base_branch, url |
github.push | Webhook only | owner, repo, ref, before, after, commits_count, pusher, head_commit_message |
github.comment.created | Webhook only | owner, repo, issue_number, comment_id, body, author, url |
Commands
Send commands to "github-agent" via sekia.command().
| Command | Required Payload | Description |
|---|---|---|
add_label | owner, repo, number, label | Add a label to an issue or PR |
remove_label | owner, repo, number, label | Remove a label from an issue or PR |
create_comment | owner, repo, number, body | Create a comment on an issue or PR |
close_issue | owner, repo, number | Close an issue |
reopen_issue | owner, repo, number | Reopen an issue |
Example config
# sekia-github.toml
[github]
token = "ghp_..." # or set GITHUB_TOKEN env var
[webhook]
listen = ":8080"
secret = "my-webhook-secret"
[poll]
enabled = true
interval = "30s"
repos = ["myorg/myrepo", "myorg/other-repo"]
Slack Agent
Standalone binary (sekia-slack) that connects to Slack via Socket Mode (WebSocket). No public URL needed.
Usage
sekia-slack [--name <instance>] [--config /path/to/sekia-slack.toml]
Configuration
Config file: sekia-slack.toml.
[nats]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| url | string | nats://127.0.0.1:4222 | SEKIA_NATS_URL | NATS server URL |
| token | string | — | SEKIA_NATS_TOKEN | NATS auth token |
[slack]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| bot_token | string | — | SLACK_BOT_TOKEN | Slack bot token (required, starts with xoxb-) |
| app_token | string | — | SLACK_APP_TOKEN | Slack app token for Socket Mode (required, starts with xapp-) |
[security]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| command_secret | string | — | SEKIA_COMMAND_SECRET | HMAC-SHA256 secret for command auth |
Setup
- Create a Slack app at api.slack.com/apps
- Enable Socket Mode in Settings → Socket Mode
- Add bot token scopes:
chat:write,reactions:write,channels:history,groups:history,im:history,mpim:history - Subscribe to events:
message.channels,message.groups,message.im,reaction_added,channel_created,app_mention - Install the app to your workspace
- Copy the Bot Token (
xoxb-...) and App Token (xapp-...) - Enable Interactivity in Settings → Interactivity & Shortcuts (no Request URL needed with Socket Mode)
Event types
All events are published to sekia.events.slack.
| Event Type | Payload Fields |
|---|---|
slack.message.received | channel, user, text, timestamp, thread_ts (if threaded) |
slack.mention | channel, user, text, timestamp, thread_ts (if threaded) |
slack.reaction.added | user, reaction, channel, timestamp |
slack.channel.created | channel_id, channel_name, creator |
slack.action.button_clicked | action_id, value, block_id, action_type, user, user_name, channel, message_ts, message_text, trigger_id |
The agent automatically filters out messages from its own bot user. Interactive events (button clicks) require Interactivity to be enabled in the Slack app settings.
Commands
Send commands to "slack-agent".
| Command | Required Payload | Description |
|---|---|---|
send_message | channel, text, blocks (optional) | Send a message to a channel. When blocks is provided (array of Block Kit objects), sends a rich message with text as notification fallback |
add_reaction | channel, timestamp, emoji | Add an emoji reaction to a message |
send_reply | channel, thread_ts, text | Send a threaded reply |
update_message | channel, timestamp, text, blocks (optional) | Update an existing message. When blocks is provided, updates with rich Block Kit content |
Example config
# sekia-slack.toml
[slack]
bot_token = "xoxb-..." # or set SLACK_BOT_TOKEN
app_token = "xapp-..." # or set SLACK_APP_TOKEN
Linear Agent
Standalone binary (sekia-linear) that polls the Linear GraphQL API for changes and executes commands.
Usage
sekia-linear [--name <instance>] [--config /path/to/sekia-linear.toml]
Configuration
Config file: sekia-linear.toml.
[nats]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| url | string | nats://127.0.0.1:4222 | SEKIA_NATS_URL | NATS server URL |
| token | string | — | SEKIA_NATS_TOKEN | NATS auth token |
[linear]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| api_key | string | — | LINEAR_API_KEY | Linear API key (required) |
[poll]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| interval | duration | 30s | SEKIA_POLL_INTERVAL | GraphQL polling interval |
| team_filter | string | — | SEKIA_POLL_TEAM_FILTER | Optional team name to filter by |
[security]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| command_secret | string | — | SEKIA_COMMAND_SECRET | HMAC-SHA256 secret for command auth |
Event types
All events are published to sekia.events.linear. Event classification is based on createdAt vs the last sync time, and the issue state name.
| Event Type | Payload Fields |
|---|---|
linear.issue.created | id, identifier, title, state, priority, team, url, description, assignee, labels |
linear.issue.updated | id, identifier, title, state, priority, team, url, description, assignee, labels |
linear.issue.completed | id, identifier, title, state, priority, team, url, description, assignee, labels |
linear.comment.created | id, body, author, issue_id, issue_identifier |
Commands
Send commands to "linear-agent".
| Command | Required Payload | Optional Payload | Description |
|---|---|---|---|
create_issue | team_id, title | description | Create a new issue |
update_issue | issue_id | state_id, assignee_id, priority | Update an issue (at least one optional field required) |
create_comment | issue_id, body | — | Add comment to an issue |
add_label | issue_id, label_id | — | Add label to an issue |
Example config
# sekia-linear.toml
[linear]
api_key = "lin_api_..." # or set LINEAR_API_KEY
[poll]
interval = "30s"
team_filter = "Engineering"
Google Agent
Standalone binary (sekia-google) that bridges Gmail and Google Calendar to the NATS event bus via OAuth2-authenticated REST APIs.
Usage
# Authorize (one-time setup)
sekia-google auth [--name <instance>] [--config /path/to/sekia-google.toml]
# Run the agent
sekia-google [--name <instance>] [--config /path/to/sekia-google.toml]
OAuth2 Setup
- Create a project in Google Cloud Console
- Enable the Gmail API and Google Calendar API
- Create OAuth2 credentials (Desktop application type)
- Set
GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRET - Run
sekia-google auth— this opens your browser, you grant permissions, and the token is saved to disk - Run
sekia-googleto start the agent (token auto-refreshes)
Remote auth via SSH
When running sekia-google auth on a headless server (e.g. EC2), the OAuth2 callback needs to reach the remote machine from your local browser. Set a fixed callback port and use SSH port forwarding:
# In sekia-google.toml on the remote machine
[google]
auth_port = 44557
# From your local machine, forward the callback port
ssh -L 44557:localhost:44557 your-server
# Then run auth on the remote machine (via the SSH session)
sekia-google auth
The auth URL will open in your local browser. After granting permissions, the callback redirects to localhost:44557, which SSH tunnels back to the remote server. Without auth_port, the callback port is random and changes each run.
Configuration
Config file: sekia-google.toml.
[nats]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| url | string | nats://127.0.0.1:4222 | SEKIA_NATS_URL | NATS server URL |
| token | string | — | SEKIA_NATS_TOKEN | NATS auth token |
[google]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| client_id | string | — | GOOGLE_CLIENT_ID | OAuth2 client ID (required) |
| client_secret | string | — | GOOGLE_CLIENT_SECRET | OAuth2 client secret (required) |
| token_path | string | ~/.config/sekia/google-token.json | GOOGLE_TOKEN_PATH | Path to store the OAuth2 token |
| auth_port | int | 0 (random) | — | Fixed port for OAuth2 callback server. Useful for SSH port forwarding when running sekia-google auth on a remote machine |
[gmail]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| enabled | bool | true | SEKIA_GMAIL_ENABLED | Enable Gmail polling |
| poll_interval | duration | 30s | SEKIA_GMAIL_POLL_INTERVAL | Gmail polling interval |
| user_id | string | me | SEKIA_GMAIL_USER_ID | Gmail user ID |
| query | string | — | SEKIA_GMAIL_QUERY | Gmail search query filter (e.g. "is:unread") |
| max_messages | int | 20 | SEKIA_GMAIL_MAX_MESSAGES | Max messages to fetch per poll |
[calendar]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| enabled | bool | false | SEKIA_CALENDAR_ENABLED | Enable Calendar polling |
| poll_interval | duration | 60s | SEKIA_CALENDAR_POLL_INTERVAL | Calendar polling interval |
| calendar_id | string | primary | SEKIA_CALENDAR_CALENDAR_ID | Calendar ID |
| upcoming_mins | int | 0 | SEKIA_CALENDAR_UPCOMING_MINS | If > 0, emit upcoming events for events starting within N minutes |
[security]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| command_secret | string | — | SEKIA_COMMAND_SECRET | HMAC-SHA256 secret for command auth |
Gmail event types
Published to sekia.events.google. Uses the Gmail History API for efficient incremental sync via historyId.
| Event Type | Payload Fields |
|---|---|
gmail.message.received | id, thread_id, message_id, from, to, subject, body, date, labels |
Gmail commands
Send commands to "google-agent".
| Command | Required Payload | Optional Payload | Description |
|---|---|---|---|
send_email | to, subject, body | — | Send a new email |
reply_email | thread_id, to, subject, body | in_reply_to | Reply to an email thread |
add_label | message_id, label | — | Add label to a message |
remove_label | message_id, label | — | Remove label from a message |
archive | message_id | — | Archive a message |
trash | message_id | — | Move message to trash |
untrash | message_id | — | Move a trashed message back to inbox |
delete | message_id | — | Permanently delete a message |
Calendar event types
Published to sekia.events.google. Uses syncToken for incremental sync. Handles 410 Gone (expired token) with automatic reseed.
| Event Type | Payload Fields |
|---|---|
google.calendar.event.created | id, summary, description, location, start, end, status, organizer, attendees, html_link, calendar_id |
google.calendar.event.updated | id, summary, description, location, start, end, status, organizer, attendees, html_link, calendar_id |
google.calendar.event.deleted | id, summary, description, location, start, end, status, organizer, attendees, html_link, calendar_id |
google.calendar.event.upcoming | id, summary, start, end, minutes_until, location, html_link, calendar_id |
Calendar commands
| Command | Required Payload | Optional Payload | Description |
|---|---|---|---|
create_event | summary, start, end | description, location, attendees | Create a calendar event. Dates: RFC3339 or YYYY-MM-DD |
update_event | event_id | summary, description, location, start, end | Update an event (at least one optional field required) |
delete_event | event_id | — | Delete a calendar event |
Example config
# sekia-google.toml
[google]
client_id = "..." # or GOOGLE_CLIENT_ID
client_secret = "..." # or GOOGLE_CLIENT_SECRET
token_path = "~/.config/sekia/google-token.json"
[gmail]
enabled = true
poll_interval = "30s"
query = "is:unread"
[calendar]
enabled = true
poll_interval = "60s"
upcoming_mins = 15
MCP Server
Standalone binary (sekia-mcp) that exposes sekia capabilities to AI assistants via the Model Context Protocol. Uses stdio transport — MCP clients launch it as a subprocess.
Usage
sekia-mcp [--config /path/to/sekia-mcp.toml]
Configuration
Config file: sekia-mcp.toml.
[nats]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| url | string | nats://127.0.0.1:4222 | SEKIA_NATS_URL | NATS server URL |
| token | string | — | SEKIA_NATS_TOKEN | NATS auth token |
[daemon]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| socket | string | ~/.config/sekia/sekiad.sock | SEKIA_DAEMON_SOCKET | Unix socket path to sekiad |
[security]
| Option | Type | Default | Env Var | Description |
|---|---|---|---|---|
| command_secret | string | — | SEKIA_COMMAND_SECRET | HMAC-SHA256 secret for command auth |
Setup for Claude Desktop
Add to your Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"sekia": {
"command": "sekia-mcp"
}
}
}
Setup for Claude Code
Add to your project's .mcp.json:
{
"mcpServers": {
"sekia": {
"command": "sekia-mcp"
}
}
}
MCP Tools
| Tool | Parameters | Description |
|---|---|---|
get_status | (none) | Daemon health, uptime, NATS status, agent/workflow counts |
list_agents | (none) | All connected agents with capabilities, commands, heartbeat data |
list_workflows | (none) | All loaded Lua workflows with handler patterns, event/error counts |
reload_workflows | (none) | Hot-reload all .lua files from disk |
publish_event | source (string), event_type (string), payload (object, optional) | Emit a synthetic event onto the NATS bus |
send_command | agent (string), command (string), payload (object) | Send a command to a connected agent |
HTTP API
The daemon serves a JSON API over a Unix socket (default: ~/.config/sekia/sekiad.sock). No authentication is required — the socket file permissions (0600) and parent directory ownership verification provide implicit security.
Accessing the API
# Using curl
curl --unix-socket ~/.config/sekia/sekiad.sock http://localhost/api/v1/status
# Using sekiactl (recommended)
sekiactl status
GET /api/v1/status
Returns daemon health and metadata.
{
"status": "ok",
"uptime": "15m30s",
"nats_running": true,
"started_at": "2026-02-15T10:30:45Z",
"agent_count": 3,
"workflow_count": 2
}
GET /api/v1/agents
Returns all registered agents.
{
"agents": [
{
"name": "github-agent",
"version": "v0.2.0",
"status": "alive",
"commands": ["add_label", "remove_label", "create_comment", "close_issue", "reopen_issue"],
"events_processed": 42,
"errors": 0,
"last_heartbeat": "2026-02-15T10:45:30Z"
}
]
}
GET /api/v1/workflows
Returns all loaded workflows.
{
"workflows": [
{
"name": "github-auto-label",
"file_path": "/home/user/.config/sekia/workflows/github-auto-label.lua",
"handlers": 1,
"patterns": ["sekia.events.github"],
"events": 15,
"errors": 0,
"loaded_at": "2026-02-15T10:30:50Z"
}
]
}
POST /api/v1/workflows/reload
Triggers hot-reload of all workflow files.
{"status": "reloaded"}
POST /api/v1/config/reload
Broadcasts a config reload signal via NATS. Optional query parameter: target (agent name, "sekiad", or "*" for all).
# Reload all
curl --unix-socket ~/.config/sekia/sekiad.sock -X POST http://localhost/api/v1/config/reload
# Reload specific agent
curl --unix-socket ~/.config/sekia/sekiad.sock -X POST "http://localhost/api/v1/config/reload?target=github-agent"
{"status": "reload_requested", "target": "*"}
Web Dashboard
An embedded web UI served on a configurable TCP port. Disabled by default.
Enable the dashboard
# In sekia.toml
[web]
listen = ":8080"
# Optional authentication
username = "admin"
password = "secret"
Or via environment: SEKIA_WEB_LISTEN=:8080 sekiad
Features
- System status, agent table, and workflow table with auto-refresh every 5 seconds (htmx)
- Live event stream via Server-Sent Events (50-event ring buffer for initial load)
- Dark mode toggle (persisted in localStorage)
- All assets embedded in the binary (no CDN dependency)
- HTTP Basic Auth support (optional)
- Security headers on every response (CSP, X-Frame-Options, X-Content-Type-Options, HSTS)
- SSE connection limit (max 50 concurrent clients) to prevent DoS
- CSRF protection via double-submit cookie for state-changing requests
Routes
| Route | Description |
|---|---|
GET /web | Full dashboard page |
GET /web/partials/status | Status card fragment (htmx) |
GET /web/partials/agents | Agents table fragment (htmx) |
GET /web/partials/workflows | Workflows table fragment (htmx) |
GET /web/events/stream | SSE endpoint for live events |
GET /web/static/* | Vendored CSS/JS assets |
NATS Subjects
All communication between the daemon, agents, and workflows uses these NATS subject patterns.
| Subject | Purpose | Publisher |
|---|---|---|
sekia.registry | Agent registration announcements | Agents (on startup) |
sekia.heartbeat.<name> | Agent heartbeats (30s interval) | Agents (periodic) |
sekia.events.github | GitHub events | sekia-github |
sekia.events.slack | Slack events | sekia-slack |
sekia.events.linear | Linear events | sekia-linear |
sekia.events.google | Gmail and Calendar events | sekia-google |
sekia.events.<custom> | Custom events from workflows or MCP | Workflows / sekia-mcp |
sekia.commands.github-agent | Commands for GitHub agent | Workflows / sekia-mcp |
sekia.commands.slack-agent | Commands for Slack agent | Workflows / sekia-mcp |
sekia.commands.linear-agent | Commands for Linear agent | Workflows / sekia-mcp |
sekia.commands.google-agent | Commands for Google agent | Workflows / sekia-mcp |
Pattern matching
NATS supports two wildcard tokens for subscriptions:
*— matches a single token. Example:sekia.events.*matchessekia.events.githubbut notsekia.events.github.sub>— matches one or more tokens (must be last). Example:sekia.events.>matches all events from any source
Security
Command authentication
When command_secret is set on both the daemon and an agent, commands are signed with HMAC-SHA256. The agent verifies the signature before executing any command. This prevents unauthorized command injection via NATS.
Set the same secret on the daemon and all agents:
# In sekia.toml, sekia-github.toml, sekia-slack.toml, etc.
[security]
command_secret = "your-shared-secret"
# Or via environment
export SEKIA_COMMAND_SECRET=your-shared-secret
NATS authentication
When using a remote NATS server (not embedded), you can configure a token:
[nats]
token = "your-nats-token"
Web dashboard authentication
The web dashboard supports HTTP Basic Auth:
[web]
listen = ":8080"
username = "admin"
password = "secret"
Web dashboard hardening
The dashboard sets the following security headers on every response:
Content-Security-Policy— restricts scripts, styles, images, and connections to same-origin only ('unsafe-eval'is allowed for Alpine.js reactive expressions)X-Frame-Options: DENY— prevents clickjacking via iframesX-Content-Type-Options: nosniff— prevents MIME-type sniffingStrict-Transport-Security— enforces HTTPS when accessed over TLS
SSE connection limit: The live event stream endpoint caps concurrent SSE connections at 50. Additional connections receive 503 Service Unavailable. Slots are freed when clients disconnect.
CSRF protection: A double-submit cookie (sekia_csrf) is set on every response. State-changing requests (POST, PUT, DELETE, PATCH) must include a matching X-CSRF-Token header. The cookie uses SameSite=Strict and Secure when TLS is active. For htmx, configure the CSRF header globally:
document.body.addEventListener('htmx:configRequest', function(e) {
e.detail.headers['X-CSRF-Token'] = document.cookie
.split('; ').find(c => c.startsWith('sekia_csrf='))?.split('=')[1] || '';
});
GitHub webhook verification
When webhook.secret is configured, the GitHub agent verifies X-Hub-Signature-256 headers using HMAC-SHA256.
Secrets encryption
sekia supports native encryption of secret values in config files using age (X25519). Encrypted values are stored inline as ENC[...] strings and decrypted transparently at startup by every binary.
# Generate a keypair
sekiactl secrets keygen
# Key file written to: ~/.config/sekia/age.key
# Public key: age1abc...
# Encrypt a secret
sekiactl secrets encrypt "ghp_mytoken123"
# ENC[YWdlLWVuY3J5cHRpb24...]
Use the ENC[...] value in any config file:
[github]
token = "ENC[YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IF...]"
The decryption identity is resolved from (in order): SEKIA_AGE_KEY env var (raw key string), SEKIA_AGE_KEY_FILE env var (path), secrets.identity config key, or the default ~/.config/sekia/age.key.
Off-machine keys: The decryption key does not need to reside on the same machine. Inject it via SEKIA_AGE_KEY from your secrets manager (Vault, AWS Secrets Manager, etc.), CI/CD pipeline, or use age-plugin-yubikey for hardware-backed keys.
Unix socket security
The daemon API uses a Unix socket (default: ~/.config/sekia/sekiad.sock), which inherits file system permissions. The daemon creates the parent directory with mode 0700, verifies it is owned by the current user, rejects symlinks at the socket path, and sets the socket file to mode 0600. This prevents symlink attacks that could occur with world-writable directories like /tmp.
CI security scanning
Every push and pull request runs three layers of automated security analysis:
- govulncheck — the official Go vulnerability checker. Scans all dependencies against the Go vulnerability database and reports only vulnerabilities that affect code paths actually used by sekia.
- gosec — static analysis for Go security issues. Detects common mistakes like SQL injection, command injection, hardcoded credentials, weak crypto, and unhandled errors in security-sensitive code.
- Trivy — container image vulnerability scanner. Scans the Docker image for known CVEs in OS packages and application dependencies. The CI pipeline fails on any CRITICAL or HIGH severity finding.
These checks run as separate parallel jobs in CI, so security findings surface immediately without slowing down the test suite.
Release signing
All release checksums are signed with cosign (Sigstore) using keyless signing via GitHub Actions OIDC. This proves that artifacts were built by the official CI pipeline — no private key management required.
Each release includes checksums.txt, checksums.txt.sig (signature), and checksums.txt.pem (certificate). To verify:
# Verify the checksum signature
cosign verify-blob \
--certificate checksums.txt.pem \
--signature checksums.txt.sig \
--certificate-identity-regexp "^https://github\.com/sekia-ai/sekia/" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
checksums.txt
# Verify your archive against the signed checksums
sha256sum --check checksums.txt --ignore-missing
SBOM generation
Every release archive includes a CycloneDX JSON Software Bill of Materials (.sbom.json) generated by syft. SBOMs list all Go module dependencies, enabling vulnerability scanning and license compliance auditing.
Hardening measures
In addition to runtime security features, the codebase includes several hardening measures identified and enforced by static analysis:
- Slowloris protection — all HTTP servers set
ReadHeaderTimeoutto prevent slow-read denial-of-service attacks. - Tight directory permissions — the workflow directory is created with mode
0750and the socket directory with0700, limiting access to the owner and group. - Safe integer conversions — UID comparisons include bounds checks to prevent integer overflow on 32-bit platforms.