A Python runtime with no user interface

Code in.
An answer out.

Your assistant can write Python. It cannot run it. Point it at one MCP endpoint and it can — in a container with no network, a memory limit and a clock, and what comes back is structured JSON rather than a wall of console output.

asks“Here’s the export — which region grew fastest last quarter?”
runsexecute({ operation: 'run.python', params: {
  code: "df = pd.read_csv('q3.csv'); df.groupby('region').growth.mean().idxmax()",
  files: [{ name: 'q3.csv', content: '…' }] } })
getsanswer: "north-east"
result: { by_region: { … } } · display: [ table, 4×3 ]
took: { duration_ms: 412, cpu_ms: 380, max_rss_kb: 61204 }
when it breakserror: { type: "ModuleNotFoundError", line: 1, source: "import pandas as pd",
  hint: "not installed in this runtime — a run has no network, so it cannot pip install" }
next: [ runtime.add_packages({ packages: ['pandas'], build: true }) ]
2tools
12tables
274operations
0environments to install
The difference

Console output is not an answer.

Most ways of running code hand back a string and leave you to parse it. That is fine for a person at a terminal and useless to a program — and an assistant that has to regex its way out of stdout will get it wrong eventually, quietly, on the number that mattered. Crucible answers in fields.

The last line is the answer

A snippet ending in an expression needs no print. Its value comes back as answer, typed — a number is a number, a dict is an object, a DataFrame is rows and columns.

  • emit(name, value) for anything else worth naming
  • show(x) to put a table or a figure in display
  • artifact(name, data) for a file to carry out

A failure is data too

The exception comes back with your line numbers — not the harness’s — the source line that raised, the frames between, and a hint that names the actual problem.

  • Missing package, no network, no such file, out of memory — each has a different fix, and it says which
  • next carries the call that fixes it, arguments filled in
  • Anything printed before it died is still there

Charts come back as charts

Any matplotlib figure left open is rendered and carried out as a PNG. Nobody remembers to call savefig into a container that is about to be destroyed, so it is done for them.

Files come back as files

Anything written to the workspace is collected before the sandbox dies. Fetch one at a time with artifact.get, so a listing of fifty runs is not fifty megabytes of base64.

What contains it

A container, and every reason it is one.

An assistant runs code somebody else wrote from a prompt somebody else typed. The interesting question is not whether it works but what it can reach when it doesn’t.

No network

--network none, by default, always. A runtime can be given egress deliberately, and then only to the hosts it names. Data gets in by being sent with the call.

Nothing writable

Read-only root filesystem. Two tmpfs mounts — the workspace and /tmp — and both vanish with the container.

No privileges

Every capability dropped, no-new-privileges, running as nobody. Memory, swap, CPU, PIDs and file descriptors all capped, so a fork bomb is a failed run rather than an incident.

Two clocks

The sandbox stops itself just before the outer one kills it — which is why a run that overruns still tells you what it printed and where it got to, instead of just disappearing.

It says how it ran

Every run records its own isolation. If a deployment has no engine and has been told to fall back to a plain process, every reply says so, in the record and in the words. It is never a silent downgrade.

Nothing leaks in

The server’s own environment — its database URL, its API keys — is scrubbed before the code starts. What a run gets is its runtime’s variables and the secrets it is entitled to, by name.

The shape of it

An organisation, its teams, and what they run.

A run always has an owner, so “who ran this, in what, and what did it cost” is a question with an answer. Everything else exists to shape a run or to explain one.

runs

One execution: what was sent, what it got, what it printed, what it returned, what it cost, how it ended.

  • Written before the code runs, so a run that kills the sandbox is still on the record
  • Never edited — only read
  • Output ages out; the fact of the run stays

runtimes

A named environment: image, packages, network policy, limits. Building bakes the packages in, because a run has no network to install from.

  • Resolved down a chain — the call, you, your team, the organisation
  • runtime.probe says what is really installed
  • Every run records the digest it actually used

programs

Code worth keeping, with declared inputs checked before anything starts and a revision history behind it.

  • “Run the weekly reconciliation” is a call, not a paste
  • Run an old revision when it worked last week
  • Private, team, or the whole organisation

sessions

A sandbox that stays alive, so variables, imports and files survive between calls.

  • One interpreter, many calls — load the file once
  • defined lists what is in there, by name and type
  • Expires on its own; a restart is reported, never faked

secrets

A value a run needs and nobody can read back. Encrypted, injected as an environment variable, and returned by no operation at all.

  • Scoped to the organisation, a team, or one person
  • A run records the names it got, never the values
  • An expiry date is rotation you do not have to remember

quotas

How much anyone may spend, per day or per month. They stack, and the tightest one decides.

  • Count limits refuse; per-run limits shorten instead
  • A refusal names which quota bit, and when it resets
  • Runs, CPU seconds, concurrency, seconds and memory per run
The whole API

Two tools. One of them runs Python.

No REST surface, no SDK, no client library to keep in step. An assistant learns the API at runtime and then calls it — and there is nothing to relearn when it changes, because it describes itself.

discover — learn the API

Tables, fields, allowed values, the filter language, and all 274 operations with their exact signatures and a runnable example each. Nine scopes, including a fuzzy search across the lot.

execute — do everything

Run Python. Build a runtime. Save a program. Open a session. Set a quota. Read a report. Every operation takes the same envelope, and dry_run, idempotency_key and actor work on all of them.

Being straight about it

What it deliberately is not.

Not a notebook

There is no cell you scroll back to and no hidden state you forgot you set — except in a session, where that is the point and the state is listed for you.

Not a job queue

A run is synchronous and bounded by a clock. If the work takes an hour, it is not this shape of work.

Not a deployment target

Nothing here serves traffic or listens on a port. Code runs, answers, and the container is destroyed.

Not a package registry

Packages come from PyPI at build time. A run never installs anything, because it has no network to install from.

Not an app to live in

There is a records browser and a run log for when you want to look. Nobody is meant to open it daily.

Not a REST API

/mcp is the only door into the data. The web pages sign you in, show you your own records, and manage keys.

Underneath

Boring where it should be.

PostgreSQL, and only PostgreSQL

Real timestamptz, jsonb with GIN indexes, and partial indexes that match each ORDER BY exactly. The schema is generated from one file; there is no migration directory to fall out of step with it.

Any Docker engine

A local socket, or a remote one over DOCKER_HOST. Nothing is mounted and no file is staged on the host — the job goes in on stdin and the answer comes out on stdout, which is what makes a remote engine work at all.

OAuth 2.1, or a key

Dynamic client registration, PKCE, rotating refresh tokens. Or issue an API key from your account page and paste it into a header.

Mail through Plinth

Invitations and password resets, in one house style. Not configured? The link comes back in the reply so you can send it yourself.

Models through konsole

assist.write turns a request into code that will actually run here; assist.explain and assist.fix read a failed run and answer from it. All optional — nothing else needs a model.

Multi-tenant by construction

The organisation fence, row visibility and secret redaction all live in the one path every read and write goes through. A query that forgets one is impossible rather than merely unlikely.

Getting started

One line, then ask it something.

claude mcp add --transport http crucible https://your-deployment/mcp

Nothing needs setting up before the first run. With no runtime configured it runs plain Python and tells you so — and tells you what to call when you want pandas.

Connect an assistant Read the API What runs the code