> ## 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/module-specs/writing-custom-modules). For the full
interface reference see [`EnvironmentModule`](/cyrnel/module-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;
  private logger: EnvironmentSetupContext["logger"] | null = null;
  private config: Record<string, unknown> = {};

  async setup({ bindings, config, logger }: EnvironmentSetupContext) {
    this.bindings = bindings;
    this.config = config;
    // Configure reduction for yourself from your own config field, then scope
    // a base phase. The host manages the rest of the correlation metadata.
    const patterns = (this.config.redactionPatterns as string[] | undefined) ?? [];
    this.logger = logger.redact(patterns).child({ phase: "setup" });
  }

  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;

    const execLogger = this.logger?.child({ executionId: eid, phase: "execution" });
    execLogger?.info({ event: "execution-start" }, "Execution starting");

    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));
      execLogger?.error({ event: "execution-failed", err }, "Execution failed");
      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: {
    redactionPatterns: {
      type: "array",
      items: { type: "string" },
      description:
        "Path patterns (dot/bracket notation) merged additively with the host-enforced baseline for this module's logs.",
    },
  },
} as const;
const secretsSchema = { type: "null" } as const;

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

## Logging

The host owns all logging - your module never creates a logger. The logger it
receives in `setup` already carries your module's identity (`moduleId`,
`moduleType`, `environmentId`), and every entry is tagged `type: "module"`.

* Call `context.logger.redact(patterns)` to opt into **self-managed
  reduction**. Declare `redactionPatterns` in your own `configSchema` and read
  it back from `context.config`: the host never supplies patterns for you.
  Your patterns merge additively on top of a non-disableable baseline
  (secrets / tokens / passwords / authorization).
* Use `logger.child({ ... })` to scope a logger for a phase or a specific
  execution. `child` accepts only `phase`/`event`; the host-owned correlation
  fields (`executionId`, `dispatchId`, `toolId`, …) are merged in by the host
  and cannot be forged.
* Emit structured logs with `logger.info({ event, ...payload }, "message")`.
  All six levels are available: `trace`, `debug`, `info`, `warn`, `error`,
  `fatal`.

## `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/module-specs/environment-bindings) for the callback
surface and [Execution](/cyrnel/module-specs/environment-execution) for the state machine
and timeout contracts.
