Skip to content
genworksPublic

About

Genworks Basalt Workstation

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Basalt

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.

Starting a deployment

Requirements: Git and Docker.

git clone https://github.com/genworks/basalt
cd basalt
./basalt up

Images 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 up

A first up also makes a few deliberate changes on the host:

  • rmacs and grmacs become shell commands that connect you to the console (see Connecting, below; the long-serving eskew and egskew remain 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.json already routes MCP through some mcp/mcp-exec that 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).

Commands

./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, restart

The units are named basalt.*, so they can be installed beside a sibling deployment stack's units on the same host.

Updating

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 --pull

Several deployments on one host

BASILISK_INSTANCE=alpha BASILISK_PORT_OFFSET=100 ./basalt up

Each 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.

Connecting

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).

MCP clients

./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 configuration, and the generated files

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.

Adding services

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 additions

A 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 up

Set 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 configuration of your own

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.

License

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.

Origin of record

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.

About

Genworks Basalt Workstation

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages