The Wire Contract
One JSON Schema, generating TS types, Zod validators, and Go structs.
Every type that crosses a process boundary in code-runner — request bodies, events,
job specs, Redis payloads — is defined once in
packages/contract/schema/wire.schema.json. From that single source, codegen produces:
- TypeScript types (consumed by the API and the SDKs),
- Zod validators (the API validates request bodies with these),
- Go structs (the worker imports
packages/contract/gen/go/wire).
A CI drift check (make contract-check) regenerates the artifacts and fails if
they differ from what's committed — so the polyglot seam can never silently drift.
Never hand-edit anything under packages/contract/gen/**. To change the wire
format, edit wire.schema.json and run make contract (or pnpm contract).
Shared names: keys, channels, events
The contract package also exports the canonical Redis keys, channel builders, and
event names from @teovilla/code-runner-contract. The Go worker mirrors these in
internal/keys.
import { keys, channelForJob, stdinChannel, controlChannel, events } from '@teovilla/code-runner-contract';
channelForJob('abc'); // "private-run-abc" (soketi output channel)
stdinChannel('abc'); // "stdin:abc" (Redis pub-sub: stdin)
controlChannel('abc'); // "ctrl:abc" (Redis pub-sub: start/kill/stdin_close)
keys.jobQueue; // "jobs:queue" (Redis list)
keys.jobSpec('abc'); // "job:abc:spec"
keys.jobStatus('abc'); // "job:abc:status"
keys.jobOutput('abc'); // "job:abc:output"
events; // { stage, stdout, stderr, compileOutput, result, artifact }Core types
These are exported from @teovilla/code-runner-contract and re-exported by the SDKs.
| Type | Shape |
|---|---|
ExecuteRequest | { language, version?, files: FileInput[], limits?: LimitsOverride, collectOutput? } |
ExecuteResponse | { jobId, channel, status: "queued" } |
FileInput | { name: string, content?: string, encoding?: "utf8" | "base64", ref?: string } — exactly one of content or ref |
JobStatus | { jobId, channel, language, version, state: JobState, updatedAtMs } |
JobState | "queued" | "starting" | "running" | "done" | "killed" | "error" |
Limits | { wallTimeMs, idleMs, cpuMs, memoryMb, pids, outputKb, maxArtifacts, maxArtifactBytes } |
LimitsOverride | all Limits fields optional |
LanguageInfo | { language, version, aliases, interactive } |
Manifest | { language, version, aliases, image, entrypoint, compile?, run, interactive, defaultLimits } |
A FileInput carries exactly one of content (inline bytes, interpreted per
encoding — "utf8" text by default or "base64" for arbitrary bytes) or ref
("sha256:<64-hex>", a pre-uploaded content-addressed blob).
The XOR is not expressible in JSON Schema in a way that survives codegen — both
fields generate as optional — so it is enforced at runtime in both the API (zod
refinement) and the worker (validateFileInput); never trust one side. ref and
encoding are purely additive: existing inline { name, content } callers are
unaffected. See Multi-file input.
Blob message types
The content-addressed blob store (CAS) is driven by two request/response pairs. See Large & shared files.
| Type | Shape | Endpoint |
|---|---|---|
BlobCheckRequest | { hashes: string[] } | POST /v1/blobs/check request |
BlobUpload | { hash: string, uploadUrl: string } | one missing-blob entry |
BlobCheckResponse | { missing: BlobUpload[], present: string[] } | POST /v1/blobs/check response |
BlobFinalizeRequest | { hashes: string[] } | POST /v1/blobs/finalize request |
BlobFinalizeResponse | { finalized: string[] } | POST /v1/blobs/finalize response |
All hashes are "sha256:<64 lowercase hex>". uploadUrl is a presigned PUT URL the
SDK uploads bytes to directly (client → store); the bytes never transit the API.
Event & message types
| Type | Shape | Carried on |
|---|---|---|
StageEvent | { phase: "queued" | "compiling" | "running" } | soketi stage |
OutputChunkEvent | { chunk: string, seq: number } | soketi stdout / stderr / compile_output |
ResultEvent | { exitCode, signal, timedOut, idleTimedOut, truncated, durationMs } | soketi result |
Artifact | { name, mimeType, bytes, url } | soketi artifact / RunResult |
CompileResult | { exitCode: number | null, signal: string | null, stdout, stderr, output, durationMs } | RunResult.compile |
RunResult | ResultEvent + { stdout, stderr, compile?: CompileResult, artifacts: Artifact[], artifactsTruncated } | GET /v1/jobs/:id/output |
StdinMessage | { chunk: string } | Redis stdin:<id> |
ControlMessage | { type: "start" | "kill" | "stdin_close" } | Redis ctrl:<id> |
RunResult.compile is present only for compiled languages that actually ran a
compile step — it's absent for interpreted languages (e.g. Python) or when no
compile ran. Build logs live in compile and are kept distinct from the run
stdout / stderr (which cover the run stage only). CompileResult.output is the
interleaved compile stdout + stderr in emission order (mirroring Piston's
compile.output). A non-zero compile.exitCode means compilation failed and the
run stage did not execute: the run stdout / stderr are empty and the
top-level exitCode reflects the compile failure.
Regenerating the contract
pnpm contract # or: make contract — regenerate TS + Zod + Go
make contract-check # CI gate: fail if generated artifacts driftedThe codegen uses go-jsonschema for the
Go structs and a TypeScript/Zod generator for the JS side.