Python runtime
The opt-in CPython lane — off-tick, behind the python build tag
Sulfur’s second plugin runtime embeds CPython 3.14 (via gopy, cgo). It exists for work that doesn’t belong on the tick: heavy aggregation, logging pipelines, external IO, ML — anything where you want real Python and its ecosystem, and where “eventually, off the tick” is the right latency contract.
The build tag
All cgo code lives behind the python build tag. The exported package surface is byte-identical
across both builds, so the same server code compiles either way:
The default binary’s import graph carries zero cgo — the pure-Go static build is preserved. The plugin host holds the Python runtime strictly by interface; nothing in the host or server packages imports gopy.
Writing a Python plugin
Same manifest, same register API — only the runtime selector and the file extension change:
Module-level state (like counts above) persists across hook calls — Python module globals are
not frozen the way Starlark’s are.
The hot-path / off-tick split
This is the load-bearing difference between the two runtimes:
When a discrete event fires, the host offers it to every loaded Python plugin by submitting the hook (with the event’s payload marshalled to plain scalars) to the plugin pool and returning immediately. A Python hook never runs on the tick goroutine. Hooks the plugin never registered are cheap no-ops.
Because a saturated pool drops dispatches rather than queueing unboundedly, Python hooks must tolerate missed events under load. Use them for statistics, logging, and side channels — not for gameplay-critical logic. Gameplay logic belongs in the Starlark lane.
World access: the request bridge
A Python plugin gets four world builtins injected alongside register:
Writes are asynchronous request producers — the owning tick goroutine applies them through the same seams the Starlark handles use, with the plugin’s manifest capabilities enforced on apply. No live tick-owned reference ever crosses into Python; only plain scalars and the request/apply indirection. Unknown capability strings still abort the load, exactly as in the Starlark lane.
Semantics summary
- The plugin’s
.pyruns once at load; top-levelregister(event, fn)calls capture hooks. Re-registering the same event overwrites (last registration wins). - CPython is initialized once per process; every hook call acquires the GIL on its pinned worker goroutine for the duration of the call.
- Unloading a Python plugin releases its captured callables and module globals.
- On a server without the Python lane (default build), the plugin is skipped at load — a missing optional runtime is never a server-down condition.

