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 daemon
  • sekia-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:

  1. /etc/sekia/
  2. ~/.config/sekia/
  3. 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

FlagDescription
--configPath to config file (overrides search paths)
--versionShow version and exit

Startup sequence

  1. Start embedded NATS with JetStream
  2. Create agent registry (subscribes to sekia.registry and sekia.heartbeat.>)
  3. Start workflow engine, load .lua files, optionally start file watcher
  4. Start HTTP API on Unix socket
  5. Start web dashboard on TCP port (if configured)
  6. Block on OS signal or stop channel
  7. Shutdown in reverse order

Daemon Configuration

Config file: sekia.toml. Environment variable prefix: SEKIA_.

[server]

OptionTypeDefaultEnv VarDescription
socketstring~/.config/sekia/sekiad.sockSEKIA_SERVER_SOCKETUnix socket path for the API (or $XDG_RUNTIME_DIR/sekia/sekiad.sock when set)

[nats]

OptionTypeDefaultEnv VarDescription
embeddedbooltrueSEKIA_NATS_EMBEDDEDRun embedded NATS server (vs connecting to remote)
data_dirstring~/.local/share/sekia/natsSEKIA_NATS_DATA_DIRJetStream data directory
hoststringSEKIA_NATS_HOSTNATS hostname (if not embedded)
portintSEKIA_NATS_PORTNATS port (if not embedded)
tokenstringSEKIA_NATS_TOKENNATS authentication token

[workflows]

OptionTypeDefaultEnv VarDescription
dirstring~/.config/sekia/workflowsSEKIA_WORKFLOWS_DIRDirectory containing .lua workflow files
hot_reloadbooltrueSEKIA_WORKFLOWS_HOT_RELOADWatch for file changes and auto-reload
handler_timeoutduration30sSEKIA_WORKFLOWS_HANDLER_TIMEOUTMax execution time for a Lua handler
verify_integrityboolfalseSEKIA_WORKFLOWS_VERIFY_INTEGRITYVerify .lua files against SHA256 manifest before loading

[web]

OptionTypeDefaultEnv VarDescription
listenstringSEKIA_WEB_LISTENTCP address for web dashboard (e.g. :8080). Empty = disabled
usernamestringSEKIA_WEB_USERNAMEHTTP Basic Auth username. Empty = no auth
passwordstringSEKIA_WEB_PASSWORDHTTP Basic Auth password. Empty = no auth

[ai]

OptionTypeDefaultEnv VarDescription
providerstringanthropicSEKIA_AI_PROVIDERLLM provider (currently only anthropic)
api_keystringSEKIA_AI_API_KEYAnthropic API key for sekia.ai() calls
modelstringclaude-sonnet-4-20250514SEKIA_AI_MODELDefault Claude model
max_tokensint1024SEKIA_AI_MAX_TOKENSDefault max tokens for responses
temperaturefloat0.0SEKIA_AI_TEMPERATUREDefault temperature (0.0–1.0)
system_promptstringSEKIA_AI_SYSTEM_PROMPTDefault system prompt

[security]

OptionTypeDefaultEnv VarDescription
command_secretstringSEKIA_COMMAND_SECRETHMAC-SHA256 secret for command authentication

[secrets]

OptionTypeDefaultEnv VarDescription
identitystring~/.config/sekia/age.keySEKIA_AGE_KEY_FILEPath to age identity (private key) file for ENC[...] decryption
aws_regionstringAWS region override for KMS[...] and ASM[...] resolution
kms_key_idstringSEKIA_KMS_KEY_IDDefault 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

FlagDefaultDescription
--socket~/.config/sekia/sekiad.sockPath to sekiad Unix socket
--versionShow 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

AgentReloadable settingsRequires restart
github-agentpoll.interval, poll.repos, poll.labels, poll.per_tick, poll.stategithub.token, webhook.listen, webhook.secret
slack-agentsecurity.command_secretslack.bot_token, slack.app_token
linear-agentpoll.interval, poll.team_filterlinear.api_key
google-agentgmail.poll_interval, gmail.query, gmail.max_messages, calendar.poll_interval, calendar.upcoming_minsgoogle.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:

FormatBackendDescription
ENC[<base64>]ageDecrypt with local age identity
KMS[<base64>]AWS KMSDecrypt via KMS API (key ID embedded in cipherblob)
ASM[<name-or-arn>]AWS Secrets ManagerFetch 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

  1. The daemon loads every .lua file from the workflows directory
  2. Each workflow gets its own Lua state and goroutine (thread-safe)
  3. Workflows register handlers with sekia.on(pattern, fn)
  4. When an event matches a pattern, the handler function is called
  5. 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 (minus dofile, loadfile, load)
  • table — table manipulation
  • string — string operations
  • math — 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.

ParameterTypeDescription
patternstringNATS subject pattern. Supports * (single segment) and > (all remaining segments)
handlerfunctionCallback 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:

FieldTypeDescription
idstringUnique event ID (UUID)
typestringEvent type (e.g. "github.issue.opened")
sourcestringEvent source (e.g. "github", "workflow:my-flow")
timestampnumberUnix timestamp
payloadtableEvent-specific data (varies by event type)

sekia.publish(subject, event_type, payload)

Emit a new event onto the NATS bus.

ParameterTypeDescription
subjectstringNATS subject (e.g. "sekia.events.custom")
event_typestringEvent type string
payloadtableArbitrary 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>.

ParameterTypeDescription
agentstringTarget agent name (e.g. "github-agent", "slack-agent")
commandstringCommand name (e.g. "add_label", "send_message")
payloadtableCommand-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.

ParameterTypeDescription
levelstringLog level: "debug", "info", "warn", "error"
messagestringLog 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.

ParameterTypeDescription
promptstringThe prompt to send to the LLM
optstable (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 history
  • conv: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=...} entries
  • conv: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 --config overrides)
  • NATS registration uses the instance name (e.g., github-work instead of github-agent)
  • Command subject becomes sekia.commands.{name} — target instances from workflows via sekia.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

CommandDescription
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 listList all managed sekia services

Create flags

FlagDescription
--name <instance>Instance name (required)
--config <path>Explicit config file path (optional)
--env KEY=VALUEEnvironment variable (optional, repeatable). Stored in plaintext — prefer ENC[...] config values

Platform details

PlatformService file locationBackend
macOS~/Library/LaunchAgents/com.sekia.{name}.plistlaunchd
Linux~/.config/systemd/user/sekia-{name}.servicesystemd (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]

OptionTypeDefaultEnv VarDescription
urlstringnats://127.0.0.1:4222SEKIA_NATS_URLNATS server URL
tokenstringSEKIA_NATS_TOKENNATS auth token

[github]

OptionTypeDefaultEnv VarDescription
tokenstringGITHUB_TOKENGitHub Personal Access Token (required)

[webhook]

OptionTypeDefaultEnv VarDescription
listenstring:8080SEKIA_WEBHOOK_LISTENHTTP listen address. Empty = disabled
secretstringGITHUB_WEBHOOK_SECRETHMAC-SHA256 secret for webhook verification
pathstring/webhookSEKIA_WEBHOOK_PATHHTTP path for the webhook endpoint

[poll]

OptionTypeDefaultEnv VarDescription
enabledboolfalseSEKIA_POLL_ENABLEDEnable REST API polling
intervalduration30sSEKIA_POLL_INTERVALPolling interval
repos[]stringSEKIA_POLL_REPOSList of "owner/repo" to poll (required when enabled)
per_tickint100SEKIA_POLL_PER_TICKMax items fetched per tick (1–100)
labels[]stringSEKIA_POLL_LABELSFilter issues by labels (enables label-filtered mode)
statestringopenSEKIA_POLL_STATEIssue state filter: open, closed, or all

[security]

OptionTypeDefaultEnv VarDescription
command_secretstringSEKIA_COMMAND_SECRETHMAC-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 TypeSourcePayload Fields
github.issue.openedWebhook + Pollingowner, repo, number, title, body, author, url, labels
github.issue.closedWebhook + Pollingowner, repo, number, title, body, author, url, labels
github.issue.reopenedWebhook onlyowner, repo, number, title, body, author, url, labels
github.issue.labeledWebhook onlyowner, repo, number, title, body, author, url, labels, label
github.issue.assignedWebhook onlyowner, repo, number, title, body, author, url, labels, assignee
github.issue.updatedPolling onlyowner, repo, number, title, body, author, url, labels
github.issue.matchedLabel-filtered pollingowner, repo, number, title, body, author, url, state, labels
github.pr.openedWebhook + Pollingowner, repo, number, title, body, author, head_branch, base_branch, url
github.pr.closedWebhook + Pollingowner, repo, number, title, body, author, head_branch, base_branch, url
github.pr.mergedWebhook + Pollingowner, repo, number, title, body, author, head_branch, base_branch, url, merge_commit
github.pr.review_requestedWebhook onlyowner, repo, number, title, body, author, head_branch, base_branch, url, reviewer
github.pr.updatedPolling onlyowner, repo, number, title, body, author, head_branch, base_branch, url
github.pushWebhook onlyowner, repo, ref, before, after, commits_count, pusher, head_commit_message
github.comment.createdWebhook onlyowner, repo, issue_number, comment_id, body, author, url

Commands

Send commands to "github-agent" via sekia.command().

CommandRequired PayloadDescription
add_labelowner, repo, number, labelAdd a label to an issue or PR
remove_labelowner, repo, number, labelRemove a label from an issue or PR
create_commentowner, repo, number, bodyCreate a comment on an issue or PR
close_issueowner, repo, numberClose an issue
reopen_issueowner, repo, numberReopen 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]

OptionTypeDefaultEnv VarDescription
urlstringnats://127.0.0.1:4222SEKIA_NATS_URLNATS server URL
tokenstringSEKIA_NATS_TOKENNATS auth token

[slack]

OptionTypeDefaultEnv VarDescription
bot_tokenstringSLACK_BOT_TOKENSlack bot token (required, starts with xoxb-)
app_tokenstringSLACK_APP_TOKENSlack app token for Socket Mode (required, starts with xapp-)

[security]

OptionTypeDefaultEnv VarDescription
command_secretstringSEKIA_COMMAND_SECRETHMAC-SHA256 secret for command auth

Setup

  1. Create a Slack app at api.slack.com/apps
  2. Enable Socket Mode in Settings → Socket Mode
  3. Add bot token scopes: chat:write, reactions:write, channels:history, groups:history, im:history, mpim:history
  4. Subscribe to events: message.channels, message.groups, message.im, reaction_added, channel_created, app_mention
  5. Install the app to your workspace
  6. Copy the Bot Token (xoxb-...) and App Token (xapp-...)
  7. Enable Interactivity in Settings → Interactivity & Shortcuts (no Request URL needed with Socket Mode)

Event types

All events are published to sekia.events.slack.

Event TypePayload Fields
slack.message.receivedchannel, user, text, timestamp, thread_ts (if threaded)
slack.mentionchannel, user, text, timestamp, thread_ts (if threaded)
slack.reaction.addeduser, reaction, channel, timestamp
slack.channel.createdchannel_id, channel_name, creator
slack.action.button_clickedaction_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".

CommandRequired PayloadDescription
send_messagechannel, 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_reactionchannel, timestamp, emojiAdd an emoji reaction to a message
send_replychannel, thread_ts, textSend a threaded reply
update_messagechannel, 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]

OptionTypeDefaultEnv VarDescription
urlstringnats://127.0.0.1:4222SEKIA_NATS_URLNATS server URL
tokenstringSEKIA_NATS_TOKENNATS auth token

[linear]

OptionTypeDefaultEnv VarDescription
api_keystringLINEAR_API_KEYLinear API key (required)

[poll]

OptionTypeDefaultEnv VarDescription
intervalduration30sSEKIA_POLL_INTERVALGraphQL polling interval
team_filterstringSEKIA_POLL_TEAM_FILTEROptional team name to filter by

[security]

OptionTypeDefaultEnv VarDescription
command_secretstringSEKIA_COMMAND_SECRETHMAC-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 TypePayload Fields
linear.issue.createdid, identifier, title, state, priority, team, url, description, assignee, labels
linear.issue.updatedid, identifier, title, state, priority, team, url, description, assignee, labels
linear.issue.completedid, identifier, title, state, priority, team, url, description, assignee, labels
linear.comment.createdid, body, author, issue_id, issue_identifier

Commands

Send commands to "linear-agent".

CommandRequired PayloadOptional PayloadDescription
create_issueteam_id, titledescriptionCreate a new issue
update_issueissue_idstate_id, assignee_id, priorityUpdate an issue (at least one optional field required)
create_commentissue_id, bodyAdd comment to an issue
add_labelissue_id, label_idAdd 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

  1. Create a project in Google Cloud Console
  2. Enable the Gmail API and Google Calendar API
  3. Create OAuth2 credentials (Desktop application type)
  4. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET
  5. Run sekia-google auth — this opens your browser, you grant permissions, and the token is saved to disk
  6. Run sekia-google to 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]

OptionTypeDefaultEnv VarDescription
urlstringnats://127.0.0.1:4222SEKIA_NATS_URLNATS server URL
tokenstringSEKIA_NATS_TOKENNATS auth token

[google]

OptionTypeDefaultEnv VarDescription
client_idstringGOOGLE_CLIENT_IDOAuth2 client ID (required)
client_secretstringGOOGLE_CLIENT_SECRETOAuth2 client secret (required)
token_pathstring~/.config/sekia/google-token.jsonGOOGLE_TOKEN_PATHPath to store the OAuth2 token
auth_portint0 (random)Fixed port for OAuth2 callback server. Useful for SSH port forwarding when running sekia-google auth on a remote machine

[gmail]

OptionTypeDefaultEnv VarDescription
enabledbooltrueSEKIA_GMAIL_ENABLEDEnable Gmail polling
poll_intervalduration30sSEKIA_GMAIL_POLL_INTERVALGmail polling interval
user_idstringmeSEKIA_GMAIL_USER_IDGmail user ID
querystringSEKIA_GMAIL_QUERYGmail search query filter (e.g. "is:unread")
max_messagesint20SEKIA_GMAIL_MAX_MESSAGESMax messages to fetch per poll

[calendar]

OptionTypeDefaultEnv VarDescription
enabledboolfalseSEKIA_CALENDAR_ENABLEDEnable Calendar polling
poll_intervalduration60sSEKIA_CALENDAR_POLL_INTERVALCalendar polling interval
calendar_idstringprimarySEKIA_CALENDAR_CALENDAR_IDCalendar ID
upcoming_minsint0SEKIA_CALENDAR_UPCOMING_MINSIf > 0, emit upcoming events for events starting within N minutes

[security]

OptionTypeDefaultEnv VarDescription
command_secretstringSEKIA_COMMAND_SECRETHMAC-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 TypePayload Fields
gmail.message.receivedid, thread_id, message_id, from, to, subject, body, date, labels

Gmail commands

Send commands to "google-agent".

CommandRequired PayloadOptional PayloadDescription
send_emailto, subject, bodySend a new email
reply_emailthread_id, to, subject, bodyin_reply_toReply to an email thread
add_labelmessage_id, labelAdd label to a message
remove_labelmessage_id, labelRemove label from a message
archivemessage_idArchive a message
trashmessage_idMove message to trash
untrashmessage_idMove a trashed message back to inbox
deletemessage_idPermanently 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 TypePayload Fields
google.calendar.event.createdid, summary, description, location, start, end, status, organizer, attendees, html_link, calendar_id
google.calendar.event.updatedid, summary, description, location, start, end, status, organizer, attendees, html_link, calendar_id
google.calendar.event.deletedid, summary, description, location, start, end, status, organizer, attendees, html_link, calendar_id
google.calendar.event.upcomingid, summary, start, end, minutes_until, location, html_link, calendar_id

Calendar commands

CommandRequired PayloadOptional PayloadDescription
create_eventsummary, start, enddescription, location, attendeesCreate a calendar event. Dates: RFC3339 or YYYY-MM-DD
update_eventevent_idsummary, description, location, start, endUpdate an event (at least one optional field required)
delete_eventevent_idDelete 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]

OptionTypeDefaultEnv VarDescription
urlstringnats://127.0.0.1:4222SEKIA_NATS_URLNATS server URL
tokenstringSEKIA_NATS_TOKENNATS auth token

[daemon]

OptionTypeDefaultEnv VarDescription
socketstring~/.config/sekia/sekiad.sockSEKIA_DAEMON_SOCKETUnix socket path to sekiad

[security]

OptionTypeDefaultEnv VarDescription
command_secretstringSEKIA_COMMAND_SECRETHMAC-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

ToolParametersDescription
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_eventsource (string), event_type (string), payload (object, optional)Emit a synthetic event onto the NATS bus
send_commandagent (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

RouteDescription
GET /webFull dashboard page
GET /web/partials/statusStatus card fragment (htmx)
GET /web/partials/agentsAgents table fragment (htmx)
GET /web/partials/workflowsWorkflows table fragment (htmx)
GET /web/events/streamSSE 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.

SubjectPurposePublisher
sekia.registryAgent registration announcementsAgents (on startup)
sekia.heartbeat.<name>Agent heartbeats (30s interval)Agents (periodic)
sekia.events.githubGitHub eventssekia-github
sekia.events.slackSlack eventssekia-slack
sekia.events.linearLinear eventssekia-linear
sekia.events.googleGmail and Calendar eventssekia-google
sekia.events.<custom>Custom events from workflows or MCPWorkflows / sekia-mcp
sekia.commands.github-agentCommands for GitHub agentWorkflows / sekia-mcp
sekia.commands.slack-agentCommands for Slack agentWorkflows / sekia-mcp
sekia.commands.linear-agentCommands for Linear agentWorkflows / sekia-mcp
sekia.commands.google-agentCommands for Google agentWorkflows / sekia-mcp

Pattern matching

NATS supports two wildcard tokens for subscriptions:

  • * — matches a single token. Example: sekia.events.* matches sekia.events.github but not sekia.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 iframes
  • X-Content-Type-Options: nosniff — prevents MIME-type sniffing
  • Strict-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 ReadHeaderTimeout to prevent slow-read denial-of-service attacks.
  • Tight directory permissions — the workflow directory is created with mode 0750 and the socket directory with 0700, limiting access to the owner and group.
  • Safe integer conversions — UID comparisons include bounds checks to prevent integer overflow on 32-bit platforms.