Files
simplexcalc/README.md
T
clawbot 3a8f1cc334
check / check (push) Successful in 1m14s
Exact remainders; refuse numbers held rounded (closes #3)
x % y is now y times the fractional part of x/y. The old form, x minus
y times the whole part of x/y, passed through a product that go/constant
could hold only rounded even when both operands and the remainder were
exact, and then replied 0.

A number whose numerator or denominator reaches 4096 bits, which
go/constant holds rounded, is now refused as too large wherever it
appears, literals included. A sum of such numbers could lose the answer,
and a rounded exponent near a whole number was computed as an exact
power. This replaces the separate check on a negative base's exponent.

Model: opus-5-5
2026-09-29 00:42:19 +00:00

9.9 KiB

simplexcalc

simplexcalc is a Go chat bot by @sneak for the SimpleX Chat network: it accepts every contact request and answers arithmetic such as 2 + 2 with the result.

Send it 2 + 2 and it replies 4; send 5 * 5/2 and it replies 12.5. It understands decimal numbers, + - * /, powers written 2^10 or 2**10, remainders written 7 % 3, signs and parentheses, and computes exactly, so 0.1 + 0.2 is 0.3. Anything else gets a short explanation instead of a result.

Getting Started

Build the image and run the bot, with its SimpleX profile on a named volume:

git clone git@git.eeqj.de:clawbot/simplexcalc.git
cd simplexcalc
make docker
docker run -d --name simplexcalc --restart unless-stopped \
    -v simplexcalc-data:/var/lib/simplexcalc simplexcalc
docker logs simplexcalc 2>&1 | grep '"msg":"ready"'

The ready log line carries the bot's contact address: address is the short link to share, and full_address is the same address in the long form that older SimpleX clients need. Open the link in any SimpleX Chat app, or paste it into its "connect via link" screen; the bot accepts at once, greets you, and answers every message you send it.

The address is stored on the simplexcalc-data volume. It survives restarts, image upgrades and recreating the container, and is lost only with the volume.

Messaging it from a terminal

The image carries the SimpleX Chat command-line client, so a throwaway second client can talk to the bot without installing anything. Replace ADDRESS with the bot's address:

docker run --rm -v simplexcalc-tester:/var/lib/simplexcalc simplexcalc \
    simplex-chat -d /var/lib/simplexcalc/tester --user-display-name tester \
    -e "/c ADDRESS" -t 30 --execute-log all
docker run --rm -v simplexcalc-tester:/var/lib/simplexcalc simplexcalc \
    simplex-chat -d /var/lib/simplexcalc/tester \
    -e "@calc 5 * 5/2" -t 20 --execute-log messages
docker volume rm simplexcalc-tester

The first command connects and prints the bot's greeting; the second sends 5 * 5/2 and prints the reply, 12.5.

Configuration

All configuration is environment variables, read once at startup. A .env file in the working directory is loaded automatically for development.

A variable that is set to something unparseable aborts startup. It is never quietly replaced by the default. Defaults apply only to variables that are absent.

  • DATA_DIR — where the SimpleX database lives: the bot's profile, its keys, its address and its contacts. Default ./data; the image sets /var/lib/simplexcalc.
  • DEBUG — true or false, default false. true logs every event the chat client sends.

Entrypoints

This repo adheres to the Scripts to Rule Them All standard. The script/ entrypoints, each with a thin make shim:

  • script/bootstrap (make bootstrap) — install build dependencies (git, make, go) idempotently. Linting additionally needs docker; markdown formatting needs docker or a local prettier.
  • script/setup (make setup) — bootstrap plus the git pre-commit hook
  • script/test (make test) — go test with the race detector and coverage, -count=1, 90 s timeout; quiet on success, verbose rerun on failure
  • script/lint (make lint) — golangci-lint via docker only, against the digest-pinned image, then script/assert-step-ran and script/assert-context-complete over the build log. Nothing is installed locally and nothing runs on the host.
  • script/fmt (make fmt) — format Go with gofmt and everything else with prettier (writes)
  • script/fmt-check (make fmt-check) — the same scope, read-only
  • script/check (make check) — test + lint + fmt-check. What the pre-commit hook runs. Never modifies files.
  • script/docker (make docker) — build the image, tagged with script/projectname
  • script/cibuild (make cibuild) — the CI gate: docker build --progress=plain --no-cache-filter=lint --no-cache-filter=builder . followed by assertions that the lint step and the test step really ran, and that both stages really received the whole repository. The Gitea workflow runs this on every push.
  • script/precommit — the hook body: go mod tidy must not change go.mod/go.sum, then check
  • script/install-precommit (make hooks) — installs the hook
  • script/projectname — prints the project name; other scripts call it so they can stay identical across repos
  • script/prettier, script/assert-step-ran, script/assert-context-complete, script/repo-source-manifest — helpers, not entrypoints

make build, make run, make dev, make deps and make clean are the ordinary conveniences. make run and make dev run the bot on the host, which needs the simplex-chat command-line client on PATH.

Why the assertions exist

A green docker build is not evidence that the checks ran. On an unchanged tree every layer comes from cache and the build exits 0 having executed nothing; BuildKit silently ignores a --no-cache-filter naming a stage that no longer exists; and a .dockerignore entry can remove a package from the build context, after which the linter genuinely runs, genuinely examines what it was handed, and genuinely reports 0 issues. over a repository with a violation in it.

So the build is not trusted. script/assert-step-ran requires the log to show the step executing and the tool's own success line coming out of it. script/assert-context-complete requires the file inventory each stage emitted to match the git index — an expectation .dockerignore cannot reach. Read the comments in those two scripts before changing either; they document what they still cannot see.

Rationale

SimpleX Chat is an end-to-end encrypted messenger with no user identifiers: people connect through addresses they choose to share. simplexcalc is a calculator anyone can reach that way, and a small, complete example of a SimpleX bot in Go: profile and address set-up, automatic acceptance, and replies, with the chat client in the same container.

Design

  • One process tree, one container. simplexcalc run starts the SimpleX Chat command-line client, simplex-chat, as a child process, with its database under $DATA_DIR/simplex and its WebSocket API on 127.0.0.1:5225. The API has no authentication, which is why it is never exposed outside the container.
  • The protocol (internal/simplex) is JSON over that WebSocket: a command carries a correlation id, its response carries the same id, and anything without one is an event. Only the fields the bot reads are decoded, so records that grow new fields in a later client release still decode.
  • Set-up on every start (internal/bot): read the bot's profile, create its long-term address if it has none, and set the address to accept every contact request and to greet each new contact. The first start creates the profile itself, a bot profile named calc. The address is logged in the ready line.
  • Replies: for each text message a contact sends in a direct chat, the bot sends back the result, as a reply quoting the message. Group messages, files and the bot's own messages are ignored.
  • Arithmetic (internal/calc): a small parser of its own reads numbers, + - * / % ^, signs and parentheses, and refuses anything else. go/constant computes with exact rationals. ^, also written **, is a power: it binds tighter than *, /, % and a sign on its left, and groups to the right, so 2^3^2 is 512, -2^2 is -4, (-2)^2 is 4 and 2^-1 is 0.5. % is the remainder and ranks with * and /; its result takes the sign of the divisor, as in Python, so 7 % 3 is 1, -7 % 3 is 2 and 7.5 % 2 is 1.5. A power with a whole exponent is exact, so 0.1^2 is 0.01, unless its numerator and denominator together could pass 4096 bits; that power, and one with a fractional exponent, is computed as a double, so 2^0.5 is 1.4142135623730951. A negative number to a fractional power is refused, as having no real result. Numbers are read as decimal, so 010 is ten. Input over 256 bytes is refused and exact powers are capped, so a message cannot make the bot do unbounded work. Whole numbers below 1021 are written exactly; other results in the shortest form that reads back as the same double, in exponent notation from 1021 up and below 10-6. A result beyond the range of a double is refused as too large, and so is any number, even a small one such as 1e-1300 or one inside a longer expression, whose numerator or denominator reaches 4096 bits: go/constant could hold it only rounded.
  • Failure is an exit. If the chat client exits or the connection to it drops, the bot exits with an error and the container's restart policy starts both again. SIGTERM stops the bot, which stops the chat client with SIGTERM and kills it if it has not exited within 10 seconds.
  • The image: a Go build stage, then an Ubuntu 22.04 runtime — the release the chat client is built for, which already has every library it links — with the simplex-chat v7.0.2 binary downloaded by an ADD whose checksum BuildKit verifies. It runs as an unprivileged user and is x86_64 only.

Operating it

Backup. Everything durable is the SimpleX database on the volume: simplex_chat.db and simplex_agent.db. Stop the container before copying them, since a copy taken from under the running client can be inconsistent. The database holds the bot's keys, so a copy lets its holder answer as the bot; keep it as private as the running instance.

Upgrade. Rebuild the image and recreate the container with the same volume. The chat client migrates its database on start. A newer simplex-chat release is a change to the URL and the checksum on the ADD line in the Dockerfile.

TODO

See docs/TODO.md and the repository's issue tracker.

Author

@sneak