plimsoll trainer · 04 / client contract
There are two ways to plug plimsoll into a product: embed its Go package, or call the daemon from a client in Go, Python or TypeScript. In Go your code cannot tell the two apart: the same interface, the same request and result types, the same floor, meaning the weakest isolation tier (how strong the wall around a run is) that a request accepts. What changes is which process owns the provider, the backend that runs the code; the proof that it is ready; and the permissions a run carries.
Embed the sandbox package and your process owns everything. Call
plimsolld over Connect, an RPC framework that runs over ordinary
HTTP, and a daemon owns everything, behind authentication, with an audit line per run.
Pick a plug: the code and the list of who owns what follow.
Two more official clients call the same daemon and make the Go client's checks: each states the protocol number (the version of the request format it speaks), recomputes every run record (the daemon's statement of what was sent, what came back and where it ran), compares the returned tier with the floor it sent, follows the chain of records in a session (one sandbox kept open for many calls), and reads the mark that says nothing ran (section 03).
pip install ./clients/python (the package is plimsoll-client).
Standard library only, Python 3.10 or later,
with AsyncClient for programs built on Python's asyncio.npm install @plimsollmark/client. No runtime
dependencies, Node 22.18 or later. Its executeCode tool for chat agents
is covered in Agent products (INTERNAL · trainer site →).None of them follows a redirect. A 307 or 308 answer would otherwise make a client
send the request again, the code included, to wherever it pointed, so a redirect
is an error. A Go program that passes its own HTTP client with
client.WithHTTPClient has to refuse redirects too.
What a snippet run carries in and what it hands back, on either plug, laid out like a connector's pinout. The two highlighted fields are the ones a product must set itself; every other field has a safe default or is evidence the run produces. A grant, field 3, is permission for a run's code to call listed routes of one API.
ErrInvalidRequest before any provider is touched.HostAPIGrant you assemble in memory; nil means no network. Remote: must be nil; a named profile, a grant stored on the daemon under a name, is selected on the client instead.ErrInsufficientIsolation and nothing runs.StdoutTruncated and StderrTruncated say so; no marker is ever added to the bytes themselves.ErrIsolationEvidenceMismatch.A project run has the same shape with different fields: Files (path
and content, canonical relative paths only), Steps (shell commands, in
order, at most 20 steps), Artifacts (the relative paths to bring back,
at most 100 artifact paths), the same Timeout, Grant and
MinimumIsolation; and back: one StepResult per command that
ran, the artifacts that existed, and a typed Outcome. Section 04 runs
one. The whole request may hold 200 files and 4 MiB of content.
Every returned error answers two questions, and the two answers are independent.
Tap an error to see its Connect code and what a product should do with it. The
sentinel errors, fixed error values a caller compares with errors.Is,
survive the trip over the network, so errors.Is works on the remote plug
too.
Read "nothing ran" from the mark, not from the error kind. Every refusal
raised before dispatch, the moment a checked request is handed to the provider
to run, is marked, with a reason, and
sandbox.NotDispatchedReason(err) reads it the same on either plug (on
the wire it is the NotDispatched error detail). An error without the
mark may have run, whatever its kind: the one ErrInvalidRequest a
simulation worker returns after its container started is unmarked, and so is every
error in the third box.
Files are written into the sandbox, steps run in order and stop at the first failure, and only the artifacts you named that actually exist come back. Choose what happens.
A failing step is a result: Outcome is completed,
the failure sits in that step's ExitCode, and later steps simply never ran.
Only a run the provider could not carry out gets a different outcome:
setup_failed, timed_out, or protocol_error, each
with human context in Detail. Refusals before dispatch and infrastructure
failures are returned errors (03), never an outcome.
Startup checks state an intent. The request floor and the result check are what hold on every call, including the one after the daemon was reconfigured.