# Steve's desktop CAD, read: an OpenCascade kernel, a Python feature API, and an optional Blender

> Satyajit Ghana — Head of Engineering @ Inkers Technology
> canonical: https://ai.thesatyajit.com/articles/steve-desktop-cad
> date: 2026-11-01
> tags: cad, open-source, blender, agents

On 31 October, [@10_X_eng](https://x.com/10_X_eng/status/2116619322177253449) posted: "We are
going to just let this rip. Steve's Desktop CAD is now FOSS. No more excuses. Go make
something." Nine minutes later came a second post: "I need everyone to remember I am just
one dude working on this in my off time. So stay sane please." Both are **reported**: I read
them through the fxtwitter mirror, which also showed 238 likes and 13 replies. It would not
return the replies, so I could not read the licensing exchange about Blender that prompted this
article. What follows is what the repository itself says.

Two questions. What is Steve's CAD, in the sense of what computes the geometry? And what does
it do with Blender, which is GPL software, from inside an Apache-2.0 repository?

I cloned [`stevecad/steve`](https://github.com/stevecad/steve) shallow at commit `b9709ca`
(dated 1 November 2026, 57 MB, 1,541 tracked files) and read it. Labels used below:
**measured** is something I counted or computed here, **reported** is what a file or page
says, **reasoned** is my inference from the two.

<RepoCard repo="stevecad/steve" />

## What I did not run

I did not run Steve. The README's development route needs a pinned CadQuery fork from GitHub and
`cadquery-ocp`, a 436 MB (unpacked) wheel. I downloaded that wheel to read its metadata and
tried to install it with the fork into a throwaway virtual environment. The sandbox's permission
layer refused, and then refused to run the repository's own `unittest` modules. I did not try
to get around either refusal. So every statement about what Steve computes is from reading
code; where I measured something, it is my own re-implementation, named as such.

## What it is

The README describes it in one sentence: "Local desktop CAD with the Studio-compatible modeling
runtime, a PySide6 desktop host, and an assistant powered by Pydantic AI. CAD builds run on this
machine." It adds: "This is pre-release software. Known coverage limits remain documented; not
every CAD operation is certified."

The size of each part, **measured** by counting lines in tracked files:

| Part | Files | Lines |
|---|---|---|
| `src/steve_desktop` (Python host, API, assistant) | 64 | 10,000 |
| `vendor/` Python, excluding Blender and the solver (feature API, drawings, designs) | 62 | 32,330 |
| `vendor/blender` | 2 | 1,293 |
| `cad-web/src` (TypeScript, TSX) | 587 | 110,930 |
| `vendor/solver/source` (C++) | 25 | 42,848 |
| `tests` | 45 | 4,653 |

Most of it is a web front end and a large vendored modeling runtime. The desktop application
itself, the part this repository is for, is about a tenth of the Python. `docs/UPSTREAM.txt` says
why: "The CAD frontend, modeling runtime, parser and shipped sketch-solver WASM are synchronized
from STEVE Studio", a hosted product. The desktop adds "Python loopback API and document store;
desktop host; Steve assistant bridge; local process lifecycle; WASM solver transport." In
other words, this is a local adapter around a modeling runtime that existed first as a service.

<ProcessMap />

## The kernel is OpenCascade, not Blender

A Steve document is Python source. `cad_document.validate` accepts three kinds: `part`,
`design` and `blender`. A part looks like this, from `cad_document.bracket()`:

```python
from steve import Model

model = Model()
with model.sketch("base_profile", on="XY", name="Base profile") as s:
    s.rect("outline", (0, 0), (width, depth))
model.extrude("base", "base_profile", distance=thickness, name="Base plate")
model.box("upright", width, thickness, height, at=(0, depth-thickness, 0),
          centered=False, op="join", name="Upright")
model.hole("base_holes", on="base.end", diameter=hole_diameter,
           points=[((width-hole_spacing)/2, 20), ((width+hole_spacing)/2, 20)])
result = model.result()
```

That is a parametric feature history written as readable calls with names: `"base.end"` is a
face reference that survives edits, which is the hard part of any parametric modeler. Top-level
numeric assignments become editable parameters; `change_parameters` rewrites them in the source
by character span. `vendor/steve/model.py` is 3,756 lines (**measured**) and defines the
operations: sketch, extrude, revolve, fillet, chamfer, shell, loft, sweep, patterns, mirror,
draft, hole, boolean combine and split, STEP import, sheet metal through a mixin.

Underneath, `model.py` imports `cadquery as cq` and, directly, OCP classes such as
`BRepAlgoAPI_Fuse`, `BRepFilletAPI_MakeFillet`, `BRepOffsetAPI_MakeThickSolid` and
`ShapeUpgrade_UnifySameDomain`. OCP is the Python wrapper of OpenCascade Technology (OCCT), the
open B-rep kernel. `requirements.txt` pins `cadquery-ocp==8.0.1.1.0`, and `cad_kernel.py` records
`occt_version='8.0.1'` in every build report (**reported**). A CadQuery fork at commit
`6a8cf4fb` supplies the rest.

So the kernel is OCCT. It stores a solid as boundary faces on exact surfaces, which is why the
bracket's hole is a cylinder with a radius and not a ring of triangles. `cad_kernel.build` calls
`shape.isValid()`, exports `model.brep`, and takes per-face type, area and centre from the B-rep
(`face.geomType()`, `surface_area(face)`). The mesh in `preview.glb` is produced later, for
display, by OCCT's native mesher run across threads.

The 2D constraint solver is a third engine. Sketch constraints go to FreeCAD's `planegcs`,
compiled to WebAssembly. `vendor/steve_sketch.py` starts a Node process running `bridge.mjs`
and talks to it over a pipe. `vendor/solver/NOTICE` lists the licence as LGPL-2.1 or later and
documents each STEVE change to the source. The source and build scripts are in the repository,
which is what the LGPL asks of someone who ships the binary.

### What one build costs the machine

`docs/LOCAL-COMPUTE-LIMITS.txt` is a policy note and worth reading before you point an agent
at this. It says local builds have no default wall-clock timeout, only explicit cancellation.
CPU use follows a percentage of logical cores, 80 by default and settable to 100. Worker
admission "reserves half of physical RAM and estimates 1 GiB per worker", and the note itself
calls that "an estimate, not measured peak-memory scheduling". Booleans stay serial "as upstream
requires for reproducible geometry", while meshing runs on up to the job's share of cores.
On Windows the fork-based topology checks stay serial. All of this is **reported** text; I
checked it against `cad_kernel.local_runtime`, which sets the worker counts and replaces the
hosted deadline check with `float('inf')`.

## Where Blender comes in

Only a document of kind `blender`. `cad_server.py` sends it to `local_blender.scene_jobs`, a
single-worker executor, while every other kind goes to the CAD executor. The command line is in
`local_blender.py`:

```text
<blender> --background --factory-startup --disable-autoexec --offline-mode -noaudio
          --threads N --python-exit-code 1 --python vendor/blender/blender_run.py -- <mode> 0
```

`blender_run.py` (1,186 lines, **measured**) starts with `import bpy`. It runs the document's
Python on an empty scene whose unit is one millimetre, evaluates modifiers, measures the result,
writes `preview.glb` and, on a full build, `result.blend`, four views rendered by Cycles on the CPU
at 16 samples, and an MP4 if the scene has a camera and an animation. The bundled assistant skill
for these documents says the same: "CAD links are imported meshes, not editable CAD solids."

A Blender scene can link a CAD part. `LocalBlender.links` builds the part with the CAD worker
first and hands Blender its `preview.glb` under a content hash. Geometry crosses the boundary as
a glTF mesh, "never as code", in the words of the runner's own docstring. It is one way: the CAD
side never receives Blender geometry back.

What follows from this is **reasoned**: in Steve, Blender is the presentation and organic-shape
side. The bundled skill agrees, in effect, when it says to model things to be manufactured as
parts or designs, with "STEP, drawings, simulation, CAM, checks", and to use Blender for the
rest. The parametric kernel is OpenCascade; Blender never touches a bracket's fillet.

### Mesh volume and exact volume

That boundary has a measurable consequence. The Blender path reports numbers computed from
triangles, in `vendor/blender/blender_measure.py` (107 lines of numpy, no `bpy`). Its volume is a
sum of signed tetrahedra from the origin, $V = \tfrac16 \sum_i \mathbf a_i \cdot (\mathbf b_i \times \mathbf c_i)$, and a mesh
counts as closed when every edge is shared by exactly two triangles. A B-rep kernel gets a
sphere's volume from an equation.

I re-implemented the three formulas (area, signed volume, closed test) rather than
import the repository's file, and ran them in numpy 2.5.3 on a UV sphere of radius 10 mm.
Exact values are 4,188.790 mm3 and 1,256.637 mm2. **Measured**:

| Longitudes | Triangles | Volume error | Area error |
|---|---|---|---|
| 8 | 48 | −23.15% | −12.36% |
| 16 | 224 | −6.26% | −3.18% |
| 32 | 960 | −1.60% | −0.80% |
| 64 | 3,968 | −0.40% | −0.20% |
| 128 | 16,128 | −0.10% | −0.05% |

An inscribed mesh always under-measures, and the error falls about fourfold per doubling.
That is a property of tessellation, not a defect in the code, but it means a Blender-kind
document's `volume_mm3` is a render-resolution quantity while a part's is exact. The widget
recomputes this table in your browser and shows two more behaviours I read in `summarize`:

<MeshVsExact />

Delete one triangle and the closed test fails, so the object's area still counts but its volume
does not, and the scene gets an "Open (not watertight) meshes have no volume" warning (**read**
in the file, **reproduced** by my copy of the test). Duplicate the sphere and the volume doubles:
`summarize` warns when the extents of closed objects overlap but does not subtract the overlap.

One trap the repository avoids deserves a line, because it is easy to hit. Blender hands over
vertices as `float32`, and `blender_run.py` line 377 casts them to `float64` before measuring.
I tested what happens otherwise. A 32-longitude sphere moved along (1, 1, 1) and summed in
float32 is off by 1.85% at a 1,000 mm offset and gives a negative volume at 10,000 mm; cast to
float64 after the float32 store, the error stays under 0.005% at every offset I tried, up to 100,000 mm.

<FloatTrap />

## The assistant

A language-model agent drives all of this. `agent_features.py` imports Pydantic AI and its
harness for file system and shell capabilities. `cad_service.cad_tools` defines the tool
surface: `cad_read`, `cad_build`, `cad_inspect`, `cad_set_parameters`, `cad_export`,
`cad_restore`, `cad_revisions`, `cad_find_parts`, `cad_import_package`,
`cad_import_catalog_part`, `cad_api_reference`, `cad_source_package` and `blender_render`
(**read**). `local_mcp.py` can expose the same tools over MCP. Edits use revision pinning, so a
stale edit is rejected, and `cad_inspect` returns named references so the model does not edit by
position.

The security statement is blunt, and I quote it because it is the most useful sentence in the
repository for anyone running a model against it: "All CAD execution is local. CAD source is
Python with the current user's permissions, not a security sandbox. Enabled agent tools execute
directly." `cad_process.py` filters the worker's environment to a short list (`PATH`, `HOME`,
temp directories) so API keys in the session do not leak to document code. That is process
isolation and cancellation, not containment.

## The licence question, as the documents word it

I am not a lawyer and this is not legal advice. What is checkable:

**What the repository says.**

- `LICENSE` is the Apache License 2.0 text. `NOTICE` reads "Steve / Copyright 2026 10-X-eng and
  Steve contributors / This product includes third-party software; see THIRD-PARTY-LICENSES/."
- `vendor/blender/UPSTREAM.txt`: "Source: synchronized from STEVE Studio's Blender runtime. Runtime
  reused with project owner permission."
- `blender_run.py` has no licence header of its own, and `import bpy` is on line 29.

**What the repository does with Blender's binary.** `blender_runtime.install` downloads a pinned
archive of Blender 4.5.9 from `https://download.blender.org/release/Blender4.5/`, compares its
SHA-256 against a constant in the file, and unpacks it under the user's own runtime folder; it
refuses anything past 2 GB downloaded or 8 GB unpacked. `discover` also accepts a Blender the
user installed, found through a setting, the `STEVE_BLENDER` variable, `PATH`, or the usual
install folders. `probe` runs `blender --version` and rejects anything that is not 4.5. No Blender
binary is among the 1,541 tracked files (**measured**: the only paths containing "blender" outside
the web front end are Python, a workflow, a skill and a text file), and nothing under `packaging/`
or `tools/` mentions it. The CI workflows download it to test with. I did not check what the
published installer contains, since I could not unpack it here; the Linux manifest I fetched from
`dl.stevecad.studio` listed version 0.4.1 while the repository carries notes up to 0.4.3.

**What Blender says.** On [blender.org/about/license](https://www.blender.org/about/license/):
Blender "is released under the GNU General Public License", its source "by default being
licensed as GNU GPL Version 2 or later", and "That is also the license to use for any
distribution of Blender binaries" refers to the newer GPL 3 or later. On scripts: "Blender's
Python API is an integral part of the software ... The GNU GPL license therefore requires that
such scripts (if published) are being shared under a GPL compliant license." The same page
also says: "Under certain conditions, it is possible to make Blender, or scripts that make
use of the Blender Python API (bpy), work with proprietary or commercial software", and offers
the Foundation's feedback on compliance. It adds that what you create with Blender is yours.

Put those side by side and the shape of the question is visible, without an answer:

1. Steve does not, by the evidence I read, redistribute Blender itself. The user's machine
   fetches it from the Blender Foundation. That is a statement about the binary.
2. `blender_run.py` is a published script that uses `bpy`, in an Apache-2.0 repository. Blender's
   page says published scripts of that kind should be under a GPL-compliant licence. Whether
   Apache-2.0 qualifies, and whether a script that runs only inside a separately installed
   process is covered the way an add-on loaded into the UI is, is the part I cannot settle.
   The usual position I remember is that Apache-2.0 is compatible with GPL version 3 and not
   version 2 only, and Blender's binaries are described as version 3 or later; I could not
   reach the FSF's list from here to confirm the wording, so treat that as unverified.
3. The Foundation's page says it opens a conversation first in a reported violation.

If you plan to build on the Blender half, the actionable route is the one the Blender page
offers, which is to ask. If you build only on the part/design half, Blender never enters.

A smaller observation from the same folder, **measured** and not a conclusion: the 461-file
`THIRD-PARTY-LICENSES/index.json` has no entry naming OCP or OpenCascade, while the
wrapper wheel's own metadata states Apache-2.0. OCCT itself is a separate project with its own
licence; I did not verify what ships in the installer, so I make no claim about it.

## What I would check next

- Install the pinned fork and wheel on a machine that allows it, then run
  `python -m unittest tests.test_cad_local` and `python -m steve_desktop --smoke-test`.
  The README names that smoke test as the offline check.
- Build the bracket, change `thickness`, and confirm `base.end` still resolves. That is the
  claim the whole feature API rests on.
- Compare a Blender-kind document's `volume_mm3` with the same shape from a part; the table
  above predicts the gap.
- Unpack the 0.4.x installer and list its contents against `THIRD-PARTY-LICENSES`.

## What this does and does not tell you

Steve is a real CAD stack, not a Blender wrapper: OpenCascade for solids, planegcs for
sketches, Python for the authoring surface, a browser viewer on three.js. It is also young.
The README calls it pre-release; the author calls himself one person working in spare time;
and its modeling runtime arrives from a hosted product, so some decisions here predate the
desktop. I read it; I did not run it.

Blender is a plug-in for scenes and renders. It is fetched, pinned by hash, and kept at arm's
length by a process boundary, a glTF handoff and the rule that no code crosses. Whether that
arm's length satisfies the GPL for a script that does `import bpy` is the open item, and it is
Blender's and the author's to answer, not mine.

Related: [Blender 5.3's Gaussian splats](/articles/blender-5-3-gaussian-splats) reads another part
of Blender's source, with its licence note.
