Aex Brain
Customize and operate

Write an environment

Connect your own runtime or workspace so Brain can run tools there.

An environment lets Brain run code in a service you control, such as a sandbox or a browser. Most of the time an existing environment is enough. Write your own when you need a different runtime or resource lifecycle.

This guide assumes you can run an HTTP service and control its deployment. If your request handler only needs to return a tool result, use the existing HTTP tool extension instead.

Run the JavaScript example

The lazy workspace example is a small HTTP service that echoes tool input and keeps a shared in-memory workspace. With Node.js 22 or newer:

git clone https://github.com/aexhq/brain.git
cd brain
npm ci
npm run build
REFERENCE_ENV_TOKEN=workspace-local node examples/lazy-environment.mjs

It listens on 127.0.0.1:8090. For this local example, run Brain on the same host from a source build:

BRAIN_DATA_DIR="$PWD/brain-data" \
BRAIN_API_TOKEN=quickstart \
BRAIN_ENV_WORKER="$PWD/target/debug/brain-env-worker" \
./target/debug/brain --listen 127.0.0.1:8080

These are Bash commands; on Windows, use PowerShell environment assignments and the .exe binaries. A Brain running in Docker can't reach this service through its own 127.0.0.1.

Connect a tool

In the quickstart app, import environment and replace the order tool with:

import { environment } from "@aexhq/brain";

const workspace = environment({
  url: () => "http://127.0.0.1:8090",
  credential: () => "workspace-local",
});
const echo = tool({
  name: "echo",
  description: "Echo a message in the workspace.",
  input: z.object({ message: z.string() }),
  implementation: { type: "reference_echo" },
});

// In sessions.create:
// tools: [echo({ env: workspace({ name: "workspace" }) })],

Send "Call echo with the message hello." and run the app. The transcript shows the echoed input and a workspace entry count. JavaScript needs no build step; for a TypeScript service, use your normal compiler.

Edit the example's execute handler to run your own fixed operation. It calls the supplied finish callback before returning a receipt, which tells Brain the tool has completed.

The SDK's environmentProtocol validates commands and receipts against Brain's generated contract, and serveEnvironment authenticates and serves them over HTTP:

import { environmentProtocol, serveEnvironment } from "@aexhq/brain/runtime";

const handle = environmentProtocol(async operation => provider.run(operation));
const server = await serveEnvironment(handle, { token: process.env.ENVIRONMENT_TOKEN, port: 8090 });

Here provider.run implements the operations for your runtime. serveEnvironment requires a token or an authenticate function, limits request body size and validates commands before your provider runs. A few helpers cover the rest:

  • finishExecution(operation, receipt) commits completion for a synchronous Tool.
  • callExecution calls other granted services.
  • reportEnvironment sends binding observations.

Each callback is sent once, and a lost completion acknowledgement returns an unknown outcome. Returning a receipt alone doesn't finish a Tool that's still running in the background.

Add typed methods and observations

environmentHandler validates provider options and named method inputs, just like a Tool. Your service authenticates Brain's request, then passes the decoded command to handler.run(). This excerpt replaces lifecycle handling inside your service and assumes provider is your integration with attach, health, release and resource-loss callbacks:

import { environmentHandler } from "@aexhq/brain";
import { z } from "zod";

const handler = environmentHandler({
  options: z.object({ region: z.string() }),
  async setup(ctx) {
    await provider.attach(ctx.sessionId, ctx.environment.name, ctx.options.region);
    const reporter = ctx.reporter;
    provider.onResourceLost(ctx.environment.name, async resource => {
      await reporter.emitResult({
        scope: "resource", resource, code: "lost", message: "The resource is no longer available.",
      });
    });
  },
  methods: {
    inspect: {
      description: "Read current provider health.",
      input: z.object({ resource: z.string() }),
      run: (input, ctx) => provider.inspect(ctx.environment.name, input.resource),
    },
  },
  teardown: ctx => provider.release(ctx.environment.name),
});

Hand the provider's watchers only ctx.reporter, and keep operation services inside the handler. The reporter can outlive setup and carries no control authority. An operation can call another authorized environment through ctx.environments, with grants separate from those of any Tool or Tool it hosts. Export handler.methods on the environment(...) factory so the application can grant the methods it chooses.

Use effect: "replace" for a method that replaces the whole execution environment, and ordinary methods to manage resources inside it. Brain handles references, grants, journaling and activation; your Env implements the provider behavior. See Control environments.

Python and other languages

An environment service speaks HTTP and JSON, so you can write it in any language with an HTTP server. Brain ships tested JavaScript services and a Python project runner. The runner itself is JavaScript; it prepares and runs Python tools with uv and their lockfiles.

If your Python code only needs computation and Brain callbacks, the smaller packaged Python tool is simpler. For services in other languages, implement the Environment protocol; Brain doesn't provide a separate environment SDK for each language.

Deploy and use it

Deploy the service somewhere your Brain server can reach. Set its URL and credential in the environment(...) factory, then create a new session with that environment. Prepare code and dependencies before running the tool, and decide when workspaces are kept and released.

The local example doesn't isolate untrusted code. A deployed service must authenticate requests, validate operations and limit the code's access to files, secrets and the network. The protocol reference covers setup, execution, cancellation and cleanup.

For supported hosted integrations, see the Aex docs.

On this page