Skip to content

Python and TypeScript modules

A module in Python or TypeScript is loaded by the host like any built-in one. You write the tools; the bindings serve the protocol. Installed while pcx runs, it becomes a custom module, which runs once a person approves its code.

What a custom module is for, how it is approved and what it may not do: Custom modules.

There is no SDK of the wire protocol for other languages, on purpose. You write against the bindings, pcx-module on PyPI and @post-cortex/module on npm: PyO3 for Python and napi-rs for TypeScript. They are the same Rust code the built-in modules use; they own the server side and hand your handlers protobuf messages. One implementation of the protocol means one set of bugs, fixed once.

from pcx_module import Module, Effect
from greet.v1 import greet_pb2 as pb
module = Module(id="greet-py", version="0.1.0", proto="greet/v1/greet.proto",
description="Says hello.", uses=["post-cortex.memory"])
@module.tool("/example.greet.v1.Greeter/Greet", name="greet", effect=Effect.WRITE)
async def greet(ctx, request: pb.GreetRequest) -> pb.GreetResponse:
return pb.GreetResponse(text=f"Hello, {request.name}")
module.run() # serves on the socket the host gives
  • A tool’s effect says what it does: READ, WRITE, or EXTERNAL (it acts outside). A dry run of a workflow calls a read for real and simulates the others.
  • A handler is an async def (TypeScript: an async function). It fails a call with ModuleError(kind, message); any other exception fails it as internal.
  • ctx is the call: who asked, the workspace, the time left, whether the caller gave up. ctx.host.call(path, request) calls a module listed in uses; ctx.publish emits a declared event; ctx.log writes to the module’s log.
  • concurrency is how many calls run at once, 8 by default.

A module is a folder:

File What
module.py or module.ts the module
proto/ its messages and services; an RPC’s comment is its tool’s description
tests/ its tests: unittest for Python, deno test for TypeScript
requirements.lock or deno.lock every package it uses, each with its hash

Do not add generated message code: it is generated from the proto when the module is prepared, so what runs is what the proto says. Do not name the bindings in the lock file: the host gives the matching one.

Limits: 16 MB packed, 64 MB unpacked, 1,000 files; no links, absolute paths or ...

Packages are installed only from the lock file, checked by hash, and no package’s install script runs.

  • Python: uv pip compile --generate-hashes -o requirements.lock requirements.in.
  • TypeScript: deno.lock, from deno cache of the module.

Native packages (numpy, pandas, scipy, pillow, lxml, cryptography, pydantic-core and a few more) come in one version for every module, so they are built once for every platform. A lock file that names another version is refused, and the refusal says which version to lock. A native package not on that list is refused, by name.

Measured through the real host:

  • Work that waits on I/O (an API call, a call of another module) is within 1.3 times a Rust module, in both.
  • Pure computation in Python is about 57 times slower than Rust; in TypeScript about 1.6 times, but on one thread. Hot loops belong in numpy, or in a Rust module.
  • Python runs without the GIL (CPython 3.15, free-threaded), so its calls use several cores. A package that does not declare itself safe without the GIL (lxml, for one) turns it on for the whole module; the module’s page then says so.
  • TypeScript runs on Deno. For a module whose calls are CPU-bound, workers runs the handlers on Web Workers: 8 calls at once went from 141 ms to 25 ms on an 8-core Mac.

The module-builder agent and skill write and install a module with you. An agent with file access names the folder, and its files are read from there, so what is installed is exactly what it tested. A script can pass the files themselves instead.

Installing runs the module’s code once, locked down (no network, no other modules, 60 s), to read what it declares and run its tests. A failed test refuses the version. Then the version waits for a person on the board.

To change a module, install it again with the changed files, as a new version. The approved version keeps running until the new one is approved.

A custom module never gets the network, processes of its own, the file system, settings of its own, or another module’s install rights. It reaches outside only through the HTTP module, and keeps a credential in one of its connections.