> ## Documentation Index
> Fetch the complete documentation index at: https://actelos.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Writing a Custom Environment Module

> Skeleton and example for building a custom environment

This page covers the environment-specific parts of writing a custom module.
For the shared mechanics (file layout, registration, lifecycle) see
[Writing a custom module](/cyrnel/specs/writing-custom-modules). For the full
interface reference see [`EnvironmentModule`](/cyrnel/specs/environment-module).

## Minimal Skeleton

```ts theme={null}
import type {
  EnvironmentBindings,
  EnvironmentModule,
  EnvironmentSetupContext,
  ExecutionExitState,
  ExecutionInput,
  ToolDocsInput,
} from "@cyrnel/sdk";

class MyEnvironment implements EnvironmentModule {
  private bindings!: EnvironmentBindings;

  async setup({ bindings }: EnvironmentSetupContext) {
    this.bindings = bindings;
  }

  async teardown() {}

  async execute(input: ExecutionInput): Promise<ExecutionExitState> {
    const eid = input.eid;

    // input.envConfig contains environment-level configuration the host
    // resolved from process defaults. At minimum it guarantees timeoutMs.
    const timeoutMs =
      (input.envConfig?.timeoutMs as number | undefined) ?? 30_000;

    this.bindings.setState(eid, "running");
    try {
      // ...execute input.code in your runtime, emitting stdout/stderr/output
      // Use timeoutMs to enforce the deadline.
      return "success";
    } catch (err) {
      this.bindings.setError(eid, String(err));
      return "failed";
    }
  }

  async kill(_eid: number) {}

  async generateDocs() {
    return "# My Environment\n\nDocument the runtime here.";
  }

  async generateToolDocs(_input: ToolDocsInput) {
    return "# Tool\n\nDocument how to call this tool here.";
  }
}

const configSchema = { type: "object", properties: {} } as const;
const secretsSchema = { type: "null" } as const;

export default { configSchema, secretsSchema, instantiate: () => new MyEnvironment() };
```

## `envConfig`

The host passes per-execution configuration via `input.envConfig` (a
`Record<string, unknown>`). The keys are caller-provided; each
environment module documents which keys it recognises and falls back to
sensible defaults for any missing values:

```ts theme={null}
# validate and coerce at runtime
const raw = input.envConfig?.timeoutMs;
const timeoutMs = Number.isInteger(raw) && (raw as number) >= 1
  ? (raw as number)
  : 30_000;
```

Formerly this configuration lived in a separate `ExecutionOptions` type
and an `options` field on `ExecutionInput`. Both have been removed in
favour of `envConfig`. Update any existing module code accordingly.

## Full Example

See the [Shell environment example](https://github.com/actelos/mci/tree/main/examples/environment-module)
in the repository for a complete, working environment that executes
submitted code as shell commands via `sh -c`. It streams stdout and stderr
back through the host bindings and reports the exit code on completion.

See [`EnvironmentBindings`](/cyrnel/specs/environment-bindings) for the callback
surface and [Execution](/cyrnel/specs/environment-execution) for the state machine
and timeout contracts.
