This repository is the build system: everything needed to configure, generate, and run a Basalt deployment. What Basalt is, the architecture and the services, is described in BASALT.md. This document is the operating manual: starting the stack, stopping it, and connecting your people and your agents.
Basalt and Readymacs are the recommended, supported development environments for the Gendl and GDL engines, and the way to put those engines in front of AI agents over MCP.
A standard deployment runs an interactive console (Emacs, with
MCP and HTTP endpoints), two engine services (Gendl on CCL and on
SBCL: Common Lisp computation and geometry, reachable over HTTP,
MCP, and SLIME/Swank), and a monitor (autoheal). Users and agents
address any service by its hostname. The whole set is declared in one
file, basalt.sexp, and every other configuration file is generated
from it.
Requirements: Git and Docker.
git clone https://github.com/genworks/basalt
cd basalt
./basalt upImages are pulled as needed, services start and are health-validated
one by one, and the deployment comes up under a generated name, a
fresh one at every full up, recorded in the file .ship and used
as the name of the Docker network all services join, with the
previous name retired into the log file .ships-log. Containers
likewise carry generated instance names, recorded in the file
.muster; up <service> and restart act on the standing
deployment without renaming anything. Read the current names from
those files rather than remembering them.
If you have a ~/projects/ directory it is bind-mounted at
/projects in every container, and created if missing. To mount a
different directory (never edit .env by hand; it is generated by
./generate-env.sh):
PROJECTS_DIR=/path/to/projects ./basalt upA first up also makes a few deliberate changes on the host:
rmacsandgrmacsbecome shell commands that connect you to the console (see Connecting, below; the long-servingeskewandegskewremain as aliases).- Placeholder credential files are created under
~/.claude/,~/.gemini/,~/.codex/and~/.grok/where missing, so the console's file mounts resolve cleanly. - If a
~/.claude.jsonalready routes MCP through somemcp/mcp-execthat no longer exists on disk (a moved or deleted clone), that entry is re-pointed at this clone's copy; entries routing through another live clone are left alone, so two deployments can share the file (a backup is kept; nothing else in the file is touched).
./basalt <command>| command | what it does |
|---|---|
up [service...] |
start the deployment, or named services only; --pull fetches fresh images |
down |
stop and remove all services |
restart [service...] |
restart one service, or all |
stop [service...] |
stop without removing containers |
status, ps |
container status |
logs [service] |
follow a service's logs |
emacs |
attach an emacsclient terminal to the console |
pull |
pull missing images (PULL_ALWAYS=1 for latest) |
config |
show the merged compose configuration |
install-shell-functions |
(re)write the rmacs/grmacs helpers |
clean |
remove containers, networks, and images; asks first |
Variant switches (--lite, --default, --tui, --gui, --full)
pick the console image variant for the invocation; the dev stack
defaults to full.
For a deployment that should survive host reboots unattended, hand it to systemd:
cd systemd && sudo ./install
sudo systemctl start basalt # likewise stop, restartThe units are named basalt.*, so they can be installed beside a
sibling deployment stack's units on the same host.
Stop the deployment before pulling the repository, in case the pull changes files a running stack would refuse to stop cleanly against:
cd ~/projects/basalt
./basalt down
git pull
./basalt up --pullBASILISK_INSTANCE=alpha BASILISK_PORT_OFFSET=100 ./basalt upEach instance gets its own generated name, network, and ports.
Within any instance the services keep their declared hostnames
(console resolves to that instance's console), so nothing configured
against a service name needs to know which instance it runs in. To
reach a second instance's console from the host, name it with a
leading @-argument: rmacs @alpha.
Two deployments from different clones (a Basalt beside a sibling
stack built with the same tooling, say) need no instance name, since
each clone is its own compose project; they need only distinct
published ports. The offset is read from the
environment at every up and written into .env afresh, so pin it
in systemd/host.env (BASILISK_PORT_OFFSET=100) to make it stick.
Each clone's mcp/install-claude-code-config and
mcp/install-claude-desktop-config add only that deployment's
servers to the client registries and leave the other's in place.
Ways in for a human user:
| way in | how |
|---|---|
rmacs |
the console, in your current terminal |
grmacs |
the console in a graphical frame (falls back to the terminal without a display) |
| the web terminal | a terminal in your browser: http://localhost:6942 |
./basalt emacs |
the console, without the shell helpers |
Detach from rmacs/grmacs with ctrl-^ (eskew/egskew remain
as aliases for both).
./basalt up generates client registry files into mcp/, detecting
the platform as it goes:
| file | for |
|---|---|
mcp/claude_desktop_config.json |
Claude Desktop on this or another machine; mcp/install-claude-desktop-config splices it into Claude Desktop's config; walkthrough in docs/CLAUDE_DESKTOP.md |
mcp/claude-code-mcp.json |
Claude Code on the host; mcp/install-claude-code-config splices it into ~/.claude.json |
mcp/mcp.toml, mcp/mcp-container.json, mcp/mcp-windows.json |
other clients, and the agents inside the console |
Agents in the console. The full console variant ships terminal
agents (Claude Code, Gemini CLI, Codex, Grok) pre-configured with
the same registries, so an agent running at the console reaches the
same services an outside client would. The lite variant carries
none; outside clients work identically either way. Which agents, and
where each keeps its configuration, are documented with the image:
see
readymacs/docker/README.md.
Every entry in these registries connects directly to the service it names, with no intermediary gate: treat issuing them as issuing an invitation. An authenticating front door for uninvited clients is a supported pattern that the standard deployment does not configure.
Once a client is connected,
mcp/opening-prompt.md is a suitable
ready-made first message.
Requiring a shared secret. Put a secret in .secrets/lisply-secret
in the projects directory (beside basalt/, mode 600) before
./basalt up, and the engines and the console answer their Lisply
endpoints only to a client that sends it; every MCP launcher this
deployment writes sends it, and a changed secret takes effect on the
next request, with no restart. Without the file the endpoints are open
as before, which is safe only while no service's port is reachable
beyond your own machine.
The deployment's configuration is basalt.sexp, the one file
written by hand. Its companion glossary.sexp defines the
configuration vocabulary and the display strings; editing either
requires no change to any shipped code. The generator reads the configuration file named
for its own directory and no other: configuration written for a
differently-named build system is not read, and the error names both
files.
After editing the configuration, regenerate:
(load-file "/projects/basalt/generate-configs.el")
(skewed-generate-configs "/projects/basalt/")This is commonly run from the console of an already-running deployment, in preparation for starting another.
Generated outputs include, among others:
| generated file | what it is |
|---|---|
docker-compose.yml |
the compose configuration the deployment runs from |
mcp/mcp*.json, mcp/mcp.toml |
MCP client registries |
generated/services-generated.el |
the console's copy of the service inventory |
generated/crew.env |
the service ledger: each role, the hostname of the service filling it, and that service's declared ports |
generated/vocabulary.env |
display strings for startup and status output |
The generated files are outputs. Never edit them directly; edit the configuration and regenerate.
Everything beyond the standard set, such as licensed engine variants, an ingress (reverse proxy) or a monitoring dashboard, arrives by stack repository: a repository beside this one, one per deployment design, carrying its own additional configuration. Stack configuration is sparse: it states only what deviates from or adds to the base, and inherits the rest.
mkdir ~/projects/my-stack && cd ~/projects/my-stack
$EDITOR basalt.sexp # your deviations and additionsA stack repository may be named anything; its configuration file is
always named for the build system it feeds, hence basalt.sexp
here.
Generate the stack's output files, then install them and start:
(load-file "/projects/basalt/generate-configs.el")
(skewed-generate-configs "/projects/my-stack/")./install # copies the outputs into ../basalt/
cd ../basalt && ./basalt upSet BASILISK_DIR= if your Basalt clone is not the sibling
directory. Re-run both the generation and ./install after every
configuration edit.
Configuration that must name other services, such as an ingress's
routing rules or a services-init hook, names a role and a port:
${BASILISK_POST_CAPTAIN_ROOM} is the hostname of the service
filling the console role, ${BASILISK_POST_CAPTAIN_HTTP_PORT} that
service's declared HTTP port, and so on per role and port name. Both
resolve against the service ledger at start time, so routing rules
survive service renames and the configuration's declared ports stay
the single source of truth; no literal hostnames or ports belong in
any template.
Bringing your own image is not a special case: a stack repository
whose configuration adds one more service. It joins the deployment's
network and resolves by name from every container. An image deployed
with no role is entered on the roster as unassigned
(unassigned-<repo>): running and monitored, assigned to nothing.
A stack repository can only add. For a deployment that runs only what you declare, fork this repository, rename it for your own system, name the configuration file to match, and cut the service list to taste. The generator derives everything from the directory name, so the rename requires no code changes.
Startup will warn and proceed for every standard role you omit: a console is recommended, not required, and a deployment without one is a standalone service group rather than an interactive one.
Copyright © 2026 Gornskew Enterprises. Basalt modifications copyright © 2026 Genworks International.
Basalt is free software, released under the GNU Affero General Public License, version 3 or later; the full text is in LICENSE. The images a deployment pulls carry their own terms, stated at their own registries. Provenance and the upstream relationship are recorded in UPSTREAM.md.
The origin of record for this repository is maintained by Genworks International, and mirrored to github.com/genworks/basalt. Public issues and pull requests may be lodged against the github mirror.