Skip to content
Known risks

What is still wrong with it

A capability matrix says what is built. The provenance page says where a number came from. Neither answers the question a safety engineer actually asks, so this page answers that one.

Everything below came out of adversarial review of this repository — twenty-four passes, each trying to make the software command a motion it could not justify. The defects those passes found are fixed and regression-tested; the git history is that work. This is what is left over.

The most valuable bug report

If you find a way to get motion authorized that should not be, that is the most useful thing anyone can send this project. Open an issue and say plainly that it is safety-relevant. Plans are content-addressed precisely so a report can be reproduced exactly.

01

Nothing here is certified, and the gate is not a safety controller

Scope

The safety gate is a software authorization boundary: it stops this software from issuing a motion command it cannot justify. That is a real property and it is not certification.

Your protective stop, e-stop, light curtains, interlocks and performance-level requirements live in hardware and in a rated safety controller, assessed against ISO 10218, ISO/TS 15066, ISO 13849 and IEC 61508/62061 where the cell is installed.

No adapter in this repository has ever run against a physical arm. The UR10e profile is COMMUNITY and the FANUC and KUKA entries are PROFILE — cited specs with no adapter. There is now a Universal Robots adapter speaking the controller's real protocols, and `palletizer robot verify --host` runs the whole contract suite against a live controller — which closes most of the distance to a VALIDATED tier and none of the distance to certification.

02

The paths that used to sit outside the gate now refuse

Structural

Until 0.5.0, “nothing reaches a robot command except through the gate” was true of palletizer_full.hardware and false of the repository. An older control stack, an OPC UA gateway, a ROS 2 bridge, a legacy URScript sender, an AI correction path that re-armed ExecuteMove, and the supported UR adapter's own perform_step could all command motion without authorization.

Each now raises UnsupportedMotionPath before any write, and names the supported path. A test scans the source tree for new motion senders and fails the build when one appears outside the supported adapter and the quarantine register.

What is left: the scan is a pattern match and would not catch a protocol it does not know (RTDE registers, a vendor SDK, Modbus). The dashboard port — stop, unlock protective stop, power off — is outside the gate by design, so anything on your network that can reach it can do those things too.

If you are wiring a robot SDK to this project, implement RobotAdapter. The contract suite exists to make that the easier route.

03

What survives a restart, and what does not

Operational

The pallet survives. Hardware adapters write a durable record of every pallet in progress to a SQLite file before and after each step. A restarted process, or a rebuilt adapter object, adopts an unfinished record and refuses a fresh permit at step 0 until a person resumes or discards it. A test kills a real child process mid-pallet to prove it.

It is one file on one machine, keyed on the adapter's cell_id. A cell driven from two machines has two ledgers. An adapter whose cell_id changes from call to call opts out of it, and nothing detects that.

Permits do not survive a restart, on purpose: the grant cursor and capability key are per process. The in-memory grant ledger holds 4096 grants; an evicted grant is retired, refused rather than restarted. Simulation adapters keep their state in memory only.

04

The capability key is a signing key

Operational

A token is a keyed MAC minted with a per-process secret, so it cannot be forged by anything that is not already executing code in that process. An LLM, a web request, an MCP client and a browser are all data to that process.

PALLETIZER_CAPABILITY_KEY shares one key across processes. Every process holding it can mint authorizations, so it is exactly as sensitive as the ability to command the robot.

With a shared key, single use is not enforced across processes: the cursor that makes a grant one-use lives in one ledger. Keys under 32 bytes are rejected at startup.

05

What “an operator acknowledged this” is worth

Structural

When a run stops short the cell holds a partly-built pallet, and the software refuses to start anything else until a person says what happened — resume or discard, each requiring a name.

This software cannot see a pallet leave a cell. A repeat order is byte-identical, so an acknowledgement about the pallet in front of a technician is structurally indistinguishable from one about the next identical order. The defence is a time bound plus expiry when the cell is demonstrably serviced, and the time bound is a heuristic. We are naming it as one.

One minute. On the reference arm a nine-case pallet is 27 seconds of commanded motion, so the window is about two pallets.

06

The retry budget is two budgets, and the adapter influences one

Operational

A failed step is retried three times when the cell was able to move, and eight more when the cell reports it was not. The second budget exists so an operator retrying a latched protective stop does not burn the permit; it is separately bounded because “the cell would not move” is a value the adapter supplies.

One step can be commanded twelve times in the worst case. The halt message says which budget ran out.

07

Name mangling stops accidents, not intent

Structural

Seven review passes each found a defect of one shape: a subclass supplying a value the framework trusted. Framework state now lives off the adapter, keyed weakly, with mangled accessors read unbound — and two later passes threw 42 adapter classes at it without moving it.

What that buys is precise. It stops accidents: an integrator picking a name the framework was already using, which is what all seven findings were. It does not stop intent, and it does not claim to — someone inside the process already owns the method that moves the arm.

Some guarantees come from the contract suite rather than the type system. Run it. It is not decoration.

08

Two arms of the same model are distinguished by one thing

Structural

The gate compares a capability declaration fingerprint — vendor, model, reach, payload — which is identical for two arms of the same model. What separates them is the framework-issued adapter identity, and a permit is bound to the adapter that first claimed a step on it.

If you run more than one cell in one process, that binding is what keeps them apart. Verify it yourself before you rely on it.

09

A known race, disclosed rather than fixed

Disclosed

A ledger row is created before the first claim binds it. Under a widened scheduling window, two adapters presenting the same permit can interleave such that the row's resume cursor is set by the wrong one.

It is bounded — the row binds on creation, so the second adapter is refused — and it is listed because a free-threaded build widens the window, and this project's own standard is that a race the interpreter currently hides is still a defect.

0 in 789 trials at default CPython. 37 in 3000 at a 2 µs switch interval.

10

What the engine does not model

Scope

Crush strength, unless you supply per-SKU limits — and when you do not, the validator reports that the check did not run and the plan is not crush-validated. A green tick that silently omits a check is a lie by omission, and it is the failure mode most likely to hurt someone.

Interlocking is geometric, not mechanical. The pattern search alternates layers so cases bridge the seams below them — what a hand-built pallet does, and why it survives a corner — but what the software checks afterwards is still a static score. It knows the cases overlap the joints beneath them; it does not know the friction between two corrugated surfaces, the stretch of the wrap, or the lateral acceleration of the trailer.

Pallet flex and deck condition. Transport dynamics — stability is a static geometric score. Wrapping, strapping, slip sheets, top frames. Conveyor timing, changeover and fault recovery.

And your cell layout: the gate checks the robot's reach envelope, not your building. Feed it the wrong layout and it will authorize motion into a wall.

11

The published figures are a property of the baseline, not only of the engine

Disclosed

The density uplift on this site moved from 9.1% to 11.1% to 21.9% to 16.2% during this work, and optimized density never changed — it was 0.4710 throughout. What moved was the yardstick: the baseline stopped being allowed to stack boxes on air, then stopped depending on the order rows arrived in.

Each correction was right on its own. The cumulative effect on a headline number is exactly the drift the provenance page exists to make visible rather than hide.

Density moved too, and for a better reason than the baseline corrections: the engine got better. It had one packer and now runs three and keeps the best, which on a uniform 400x300 case pallet takes volume density from 0.8305 to 0.9689 and drops a whole layer off the same order. Every plan publishes what all three achieved and which rule picked the winner, because “we chose the best one” without the losers’ figures is an assertion.

Throughput moved the same way and further. It was a flat 7.5 s per case, publishing as roughly 470 cases/hr. It is now a per-case kinematic model, and the same reference pallet publishes as roughly 350 cases/hr — a 25% cut to a headline figure, made deliberately. The constant costed a case in the far corner of the top layer exactly what it costed one dropped in the middle of the bottom layer, which is the first assumption a technical buyer checks.

The uplift is published as a median of 32 orderings with its 10th–90th percentile spread, because a single number here would be a choice of seed.

12

The site's accounts, limits and model budget

Operational

Daily quotas — API-key requests and model answers — are counted in PostgreSQL, atomically. The per-minute rate is an in-memory window per serverless instance, keyed for anonymous callers on a header a client can set. It raises the cost of abuse; it is not a guarantee.

The model is never called for an anonymous visitor, and never without a durable budget. Accounts are free to create, so the site-wide daily cap is what bounds spend.

The SQL is tested on a real PostgreSQL and the gate's logic against an in-memory store with the same semantics. The supabase-js layer between them, and the Stripe webhook against Stripe itself, are not exercised in CI.

No plan authorizes motion. A tier changes what the site computes, stores and exports; the safety gate does not read it.

What the reviews could not break

Stated because a list of risks without it is not a fair picture. Across twenty-four passes, all of the following were attempted and refused: hand-built, flipped and subclassed authorizations; an authorization lying about its own fields; duck-typed and subclassed execution steps overriding the hash they are authenticated by; a plan subclass answering its own integrity check with a lie; plans reversed, tripled, stripped of every pick, and zero-length; cross-plan and cross-step replay; tokens minted about a 999999 mm arm presented to a 3.2 m one; a simulation token presented to a hardware adapter; adapters with no declared tool and no declared mode; replay through freshly constructed adapters, after completion, and after eviction; cursor walking; retry-budget exhaustion; protective-stop latch clearing; and a resealed plan artifact with a forged verdict over 54 interpenetrations.

Every concurrency probe aimed at the grant ledger came back clean — 0 double-claims in 300 barrier-synchronised trials, 0 duplicated motions in 3750 two-cell trials.

The architecture holds against everything that starts from untrusted data, which is the threat model it claims. The risks above are the ones that do not.