Saturn is a moderator bot for Hack.Chat.
It supports moderation commands, user utilities, room replicas, embedded H2 persistence, and Docker-based deployment.
- JDK 23+
- Docker (optional, for containerized runs)
Check your Java version:
java --versiongit clone https://github.com/ArteomBalanuta/saturn.git
cd saturn
cp config.example.toml config.toml
make fresh-db
./mvnw package
java -Dlog4j.configurationFile=./log4j2.xml -jar target/saturn.jarMain runtime configuration lives in config.toml.
Start from the template:
cp config.example.toml config.tomlExample:
dbPath = "database/database"
wsUrl = "wss://hack.chat/chat-ws"
cmdPrefix = "*"
channel = "programming"
nick = "alphaBot"
trip = "YOUR_BOT_TRIP_HERE"
userTrips = ""
adminTrips = "YOUR_ADMIN_TRIP_HERE"
autoReconnect = true
healthCheckInterval = 5
autorunCommands = "replica lounge, say hello lads!!"
[agent]
enabled = false
# Set SATURN_AGENT_ENDPOINT in the environment before enabling the agent.
apiKeyEnv = "SATURN_AGENT_API_KEY"
timeoutSeconds = 30
maxCompletionTokens = 1024
thinkingEnabled = false
maxConcurrentRequests = 2
maxSteps = 5
maxToolCallsPerTurn = 4
toolTimeoutMillis = 10000
maxToolCalls = 4
memoryTurns = 30
memoryTtlHours = 168
creatorTrip = "595754"
ambientEnabled = false
ambientEveryMessages = 8
quietMinutes = 15
contextMessageLimit = 60
moderationEnabled = true
dynamicSqlEnabled = true
dynamicSqlMaxSqlChars = 4000
dynamicSqlMaxRows = 50
dynamicSqlMaxColumns = 32
dynamicSqlMaxCellChars = 2000
dynamicSqlMaxResultChars = 32000
dynamicSqlTimeoutMillis = 1000Important fields:
dbPath: H2 database stem; H2 stores data at<dbPath>.mv.dbcmdPrefix: command prefix used in chatchannel: room Saturn joins on startuptrip: bot trip passworduserTrips: comma-separated trips with basic accessadminTrips: comma-separated admin tripsautoReconnect: enables health-check restart behaviorautorunCommands: commands executed after startupagent.endpoint: TOML fallback for the OpenAI-compatible router base URL; Saturn calls/v1/chat/completionsSATURN_AGENT_ENDPOINT: environment-first provider URL; use this instead of committing a deployed endpointagent.apiKeyEnv: name of the environment variable containing an optional bearer tokenSATURN_AGENT_API_KEY: standard bearer-token environment variableSATURN_AGENT_MODEL: optional environment-first model overrideagent.maxCompletionTokens: provider-side output-token limit for each completionagent.thinkingEnabled: enables model thinking mode; disabled by default for predictable latencyagent.maxConcurrentRequests: maximum accepted active and queued agent requests per engineagent.maxSteps: maximum model/tool loop iterations for one requestagent.maxToolCallsPerTurn: total tool-call budget for one request;agent.maxToolCallsremains a compatible legacy aliasagent.toolTimeoutMillis: deadline for one tool invocation unless that tool declares a shorter or longer overrideagent.memoryTurnsandagent.memoryTtlHours: bounded embedded database conversation memoryagent.creatorTrip: trusted creator identity; keep it authorized inadminTripsor the roles tableagent.ambientEnabled: enables periodic participation in unaddressed public chat; defaults tofalseagent.ambientEveryMessages: when ambient mode is enabled, routes every Nth eligible messageagent.quietMinutes: per-user, per-room ambient suppression after a polite quiet requestagent.contextMessageLimit: recent public room messages automatically supplied to all public agent turnsagent.moderationEnabled: enables deterministic spam and raid responses independently of ambient chatagent.dynamicSqlEnabled: enables admin-only schema inspection and generated read-only SQLagent.dynamicSqlMax*: bounds SQL length, rows, columns, cells, and serialized resultsagent.dynamicSqlTimeoutMillis: interrupts generated queries after the configured deadline
Environment variables take precedence over TOML for the provider, router limits, memory bounds, and
dynamic-SQL bounds that expose SATURN_AGENT_* overrides. Their names use the SATURN_AGENT_ prefix
and upper snake case; for example agent.maxSteps becomes SATURN_AGENT_MAX_STEPS. Copy
.env.example to the ignored .env file only when you want environment-specific
agent overrides. Participation and moderation thresholds are configured in [agent] TOML. The
endpoint selects the model by default, so agent.model may remain empty. Set SATURN_AGENT_API_KEY
only when the endpoint requires authentication. Keep the agent disabled until an explicit endpoint is
configured; use HTTPS or a trusted private network for all prompts and tool results.
For production, choose one of these supported paths:
- Keep the current real agent values in ignored
config.toml; runmake runor Compose without a.envfile and Saturn uses those values unchanged. - Create ignored
.envfrom.env.example, replace its placeholders with the real endpoint and credentials, and setSATURN_AGENT_ENABLED=true; those environment values override TOML.
Maintainers should start with docs/AGENTIC_BEHAVIOR.md for the
implementation-grounded behavior contract and docs/ARCHITECTURE.md for the
current package map and lifecycle. The section below summarizes operator-visible behavior and
deployment constraints.
*l how many users are in the room right now?
@alphaBot can you check what sun discussed recently?
Vaelen answers *l and exact @<bot-nick> mentions immediately. Ambient participation is disabled
by default, so unaddressed public messages receive no agent response. If explicitly enabled, every
eighth eligible public message is evaluated by default. A polite request such as please be silent
produces no acknowledgement and suppresses ambient replies to that user in that room for 15 minutes.
The agent can inspect live users in any Saturn-managed room, retrieve bounded public message history
for a named user across all rooms or within one named room (up to 500 messages), and run named
read-only database queries.
By default, public direct, mention, and ambient turns automatically receive the latest 20 public
messages from their room; agent.contextMessageLimit can change that bound.
Informational Saturn commands are available to all agent callers. The configured creator, configured
admin trips, persisted admins, and persisted moderators receive moderation commands when their
invocation mode is eligible. Only a direct creator invocation receives permanent-ban and admin
commands. Recursive l, raw SQL commands, shutdown, unban-all, and unrelated admin commands are
never exposed.
Public conversation memory is shared by everyone in the same room, so another participant can continue an earlier exchange. Each engine has one FIFO agent worker, so accepted work across its rooms is serialized in submission order. Whispers use private per-user memory and are never added to the public room session. Rooms remain separate, and memory expires according to the configured TTL.
New message audit rows carry an explicit PUBLIC or WHISPER visibility. Regular agent history
tools only read PUBLIC rows. Existing rows from before this migration remain unclassified and are
excluded because Saturn cannot safely infer whether they came from public chat or a whisper.
Configured admin trips and users with a persisted ADMIN role also receive a generated-SQL
fallback. The agent must inspect the schema first and may then run one bounded, AST-validated
SELECT on a dedicated read-only H2 connection. This admin capability can read every Saturn
application table and column, including cross-room messages, trip/hash identity data, mail, notes,
moderation data, command history, agent memory, whispers, and unclassified legacy message rows.
Database metadata tables, writes, schema changes, and administrative statements remain blocked.
Logs contain only a query fingerprint, duration, row count, and outcome; raw
generated SQL is not logged.
The optional agent is a bounded orchestration layer, not an unrestricted chatbot or replacement for
Saturn's command, persistence, or moderation systems. An OpenAI-compatible provider proposes text or
structured tool calls; Saturn owns the immutable request context, tool definitions, authorization,
argument/result validation, ordering, timeouts, persistence, and final room output. The provider is
called at the configured endpoint's /v1/chat/completions path.
Agent invocations are created in four modes:
DIRECT: an explicit*l <prompt>command or whisper command; a user-visible answer is expected.MENTION: an exact public@<bot-nick>mention; a user-visible answer is expected.AMBIENT: an eligible sampled public message; no acknowledgement is required and silence is normal.MODERATION: a semantic moderation candidate; output is intentionally silent and the action path is restricted to approved moderation commands.
Contextual tools can read room users, retrieve bounded public message history, inspect the schema,
run bounded read-only SQL for authorized admins, and execute approved Saturn commands. Tool visibility
is calculated for the current caller and mode. Informational tools are broadly available; moderation
requires the configured creator/admin/persisted roles where eligible; permanent bans and admin commands
require a direct creator invocation. The agent cannot bypass SaturnCommandGateway, protected-principal
rules, or command authorization, and RPC/MCP/plugin/arbitrary skill execution is not part of this
in-process boundary.
Every proposed call is checked against its contextual descriptor, JSON arguments, capability policy,
prerequisites, duplicate ledger, per-turn budgets, deadlines, and cancellation state before execution.
Failures, denials, invalid results, timeouts, and cancellations remain explicit failures; they are never
reported as successful actions. A request is limited by agent.maxSteps, agent.maxToolCallsPerTurn
(with legacy agent.maxToolCalls compatibility), per-tool/failure limits, prompt/output bounds, and
tool deadlines. Safe independent read-only calls may run in contiguous parallel segments, but barriers,
side effects, prerequisites, and resource conflicts remain ordered; this is not an unbounded loop or a
general dependency DAG.
memoryKey is both the session lock and durable-memory namespace. Public turns share ordered memory per
room; whispers use a private per-user namespace and are not added to the public session. H2 memory is
bounded by turns and TTL. Public room context and regular history tools use only classified PUBLIC
rows; WHISPER rows are private, and pre-migration unclassified rows are excluded from regular history
queries. Tool evidence records what was actually attempted and whether it succeeded, failed, or was
unavailable. Durable history is not mutated by provider-side trimming or projection, and non-silent
conversation persistence occurs only after final response validation.
ToolResultMode controls result visibility independently of whether the agent replies: MODEL_DATA is
sent to the model and may become reusable evidence; ROOM_DELIVERY is delivered to the room without a
normal model-data observation and is not reusable; ROOM_DELIVERY_AND_MODEL_DATA uses both immediate
channels but is not reusable cross-turn. AgentResult.silent intentionally suppresses conversational
output for ambient/moderation cases.
With agent.moderationEnabled = true, Saturn watches bounded in-memory windows for message floods,
repeated text, join bursts, same-hash nickname variants, and suspicious nickname clusters. The
default escalation ceiling is warning, captcha, mute, kick, then shadow-ban. Autonomous permanent
bans do not exist; ban is available only through a direct creator invocation. Host, replica, creator,
and configured admin identities are excluded from automatic targeting.
Captcha is enabled after a detected raid and remains enabled until an authorized user runs
*captcha off. Every moderation count, window, and cooldown is documented in
config.example.toml and can be tuned under [agent].
config.example.toml is tracked in git. Your local config.toml is intentionally ignored.
make fresh-dbThis stops the Docker container and removes database/database.mv.db. Saturn recreates its H2
schema automatically on the next startup.
Linux/macOS:
./mvnw packageWindows:
mvnw.cmd packageThe image build copies VERSION into the build context so the shaded jar and runtime /VERSION
resource report the same release artifact version. The Dockerfile currently builds with JDK 24 while the
Maven source/target level is Java 23; local development requires JDK 23+ and the container supplies its
own JDK.
Run the focused test class while iterating, then the normal quality gates:
./mvnw -Dtest=<TestClass> test
./mvnw spotless:check
./mvnw test
./mvnw packagepackage produces the shaded target/saturn.jar. The Maven project remains 1.0-SNAPSHOT; the
release artifact version is read from the root VERSION file and is embedded in the jar.
The repository's release process should update VERSION deliberately; do not infer a release
version from the Maven snapshot string.
java -Dlog4j.configurationFile=./log4j2.xml -jar target/saturn.jarWhen running outside Docker, keep these files available together:
target/saturn.jarconfig.tomlconfig.example.toml(optional, as a template)log4j2.xmldatabase/database.mv.db
The easiest Docker workflow uses the included Makefile.
# Optional environment-override mode:
# cp .env.example .env
make fresh-db
make rebuild
make logsmake build # build the image
make run # recreate and run the container
make rebuild # clean, build, and run
make stop # stop the container
make rm # remove the container
make rmi # remove the image
make clean # remove both container and image
make db-check # stop Saturn and verify the H2 database file
make backup-db # stop Saturn and copy the H2 database file
make logs # follow container logs
make shell # open a shell in the container
make status # show container status
make help # list all targetsmake run and make rebuild mount:
./config.tomlto/app/config.toml./databaseto/app/database
They preserve the ignored config.toml as the runtime fallback and, when present, pass the ignored
.env file to Docker with --env-file. This lets production deployments retain their current TOML
endpoint/model settings or override them through SATURN_AGENT_* without putting real values in
tracked files. Compose forwards the same optional variables and also works with no .env file,
using config.toml fallbacks.
Container removal is graceful: Saturn receives up to 30 seconds to close its WebSocket, replicas, agent executor, and H2 connections before Docker removes it.
Do not open the bind-mounted database concurrently from another process while Saturn is running.
H2 stores data in a .mv.db file; AUTO_SERVER=TRUE coordinates local JVMs, but one Saturn
container should own a mounted database. Use make db-check or make backup-db; both
stop Saturn before touching the database. Run make start afterward to resume the existing
container.
docker build -t saturn .
docker run -d \
--name saturn \
-v $(pwd)/config.toml:/app/config.toml \
-v $(pwd)/database:/app/database \
saturnAdd --env-file .env before the volume flags only when you intentionally use the environment-
override mode.
docker compose up --build -dIf you use the local bind-mounted setup, the container will use your local config.toml and database/.
Agent tools expose a contextual AgentToolDescriptor contract. Each descriptor documents the
tool's label, category, access level, side effect, result delivery mode, usage guidance, examples,
capabilities, prerequisites, and JSON parameter schema. The registry publishes this contract in
the provider definitions, and the runtime prompt treats it as authoritative over persona prose.
The provider may emit multiple tool calls in one response. Saturn remains sequential-first:
run_command, moderation, room delivery, and dependency chains execute in provider order. Only
contiguous, independent tools marked both read-only and idempotent may run concurrently; their
observations still return to the provider in original call order. This reduces provider round trips
for compound read requests without reordering Saturn commands. See docs/ARCHITECTURE.md for the full lifecycle and extension guide.
Existing tools remain compatible: tools that do not override descriptor(context) receive safe
read-only defaults from AgentTool. Add an explicit descriptor for new tools, especially when a
tool changes the room, moderates users, writes persistence, or requires another tool first.
Agent-facing instructions and tool copy are externalized under src/main/resources/agent/; Java
code supplies only runtime values and orchestration.
- H2 is used for embedded persistence. On first startup Saturn migrates a sibling legacy
database.dbSQLite file todatabase.mv.dband archives the source asdatabase.db.bak. - Startup adds the message-visibility column and matching agent query indexes to legacy databases.
- The local database is not recreated automatically unless you run
make fresh-db. - Saturn creates the embedded H2 schema at startup; the mounted
database/directory holds the active file.