An environment module runs the code clients submit through
POST /processes. It owns:
- Code execution. Transpiling, sandboxing, and running submitted code.
- Runtime bindings. Exposing the
cyrnel.* API (or whatever shape it
prefers) so user code can discover and invoke tools.
- Output capture. Streaming stdout, stderr, and structured output back
to the host through callbacks.
- Cancellation and timeouts. Honouring
kill(eid) and respecting
options.timeoutMs.
Environment modules implement the SDK
EnvironmentModule interface.
Lifecycle
interface EnvironmentModule extends Module {
setup(context: EnvironmentSetupContext): Promise<void>;
execute(input: ExecutionInput): Promise<ExecutionExitState>;
kill(eid: number): Promise<void>;
generateDocs(): Promise<string>;
generateToolDocs(input: ToolDocsInput): Promise<string>;
teardown(): Promise<void>;
}
setup({ bindings }) is called when the environment is activated.
bindings is the EnvironmentBindings callback
surface back into the host (discovery, invocation, state, output).
execute(input) is called for each process. input.eid is the
process id; the environment must use it on every callback so the host
can route stdout/stderr/output to the right process.
kill(eid) interrupts a running or queued execution. Should
return promptly.
generateDocs() returns a Markdown string describing the
environment’s globals. Surfaced by GET /environment/docs.
generateToolDocs(input) returns Markdown describing how to call a
single tool inside this environment. Surfaced by
GET /tools/:serviceId/:toolId/docs.
teardown() is called when the environment is deactivated.
Active environment vs draining
Only one environment is active at a time, the one whose modules row has
enabled = true. Switching environments or disabling the active module
moves it to a draining state: no new executions are dispatched to it,
but in-flight ones run to completion. Once the last execution finishes, cyrnel
calls teardown() on it.
This means:
- An environment’s
execute may be called after another environment is
active. Implementations must not assume “active” and “currently running
executions” are the same set.
kill may be called during draining. Honour it.
Security Model
Environment modules own the security boundary user code runs inside. Their
job, more than anything else, is to make sure user code cannot do
anything the environment didn’t intend to expose.
For the bundled typescript-ivm:
- Code runs in an
isolated-vm isolate with a hard memory limit.
- The isolate has no module loader, no filesystem, no network, no Node
built-ins. The only escape hatches are the references cyrnel installs.
- Timeouts terminate the isolate; the worker slot is recreated rather than
reused.
If you write a custom environment, the relevant questions are:
- What can user code reach? (Network? Filesystem? Native code?)
- Can a malicious process exhaust resources? (CPU loops, memory, file
descriptors.)
- Can one execution affect another? (Shared globals, persisted state.)
- How are credentials exposed? (Adapter modules see secrets; environments
should not.)
See Security for the broader picture.
Authoring
See Writing a custom environment module
for the file layout, a skeleton, and a full working example.
EnvironmentModule covers the interface
contract in full, and EnvironmentBindings
documents the callback surface. Last modified on June 19, 2026