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.
The bindings
Section titled “The bindings”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, Effectfrom 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, orEXTERNAL(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 withModuleError(kind, message); any other exception fails it asinternal. ctxis the call: who asked, the workspace, the time left, whether the caller gave up.ctx.host.call(path, request)calls a module listed inuses;ctx.publishemits a declared event;ctx.logwrites to the module’s log.concurrencyis how many calls run at once, 8 by default.
The files
Section titled “The files”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 ...
The lock file
Section titled “The lock file”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, fromdeno cacheof 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.
Python or TypeScript
Section titled “Python or TypeScript”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,
workersruns the handlers on Web Workers: 8 calls at once went from 141 ms to 25 ms on an 8-core Mac.
Install
Section titled “Install”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.
Not allowed
Section titled “Not allowed”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.