A module is a directory.
Five files, three contracts, zero plumbing. The platform auto-discovers your module at startup and the rest writes itself.
What's in a module.
Every module has the same shape, so every engineer reads the next module fluently.
{
"code": "pm_monitor",
"name": "PM Performance Monitor",
"version": "1.4.0",
"description": "Tracks PC and workstation fleet health.",
"table_prefix": "pm_",
"roles": ["operator", "supervisor", "admin"],
"ui_panel": "module-ui.tsx",
"depends_on": [],
"agent_skills": ["pm.health_briefing"],
"sandbox_demo": "sandbox/pm_demo_data.json"
}
$ platform modules scaffold pm_monitor
→ wrote modules/pm_monitor/
→ added module.json, tools.py, routes.py, repository.py
→ added migrations/0001_initial.py
→ added frontend/modules/pm_monitor/module-ui.tsx
$ platform modules disable pm_monitor --tenant acme-ops
→ unsubscribed acme-ops
→ tools deregistered from agent context
→ UI panel hidden in tenant console
From manifest to deployed feature.
Scaffold
platform modules scaffold pm_monitor — generates manifest, tools.py, routes.py, repository.py, ui panel stub, and initial migration.
Write tools
Decorate functions with @platform_tool. The platform turns them into agent tools, REST endpoints, and CLI verbs.
Sandbox preview
platform modules preview pm_monitor — runs in the sandbox tenant with seeded demo data. Agent included.
Pipeline
platform modules promote pm_monitor — runs the module through validation, migration dry-run, security checks, and tenant subscription.
Subscribe a tenant
platform tenants subscribe acme-ops pm_monitor — migrations apply, tools register, UI mounts on the next page load.
# tools.py — the only file that matters
from platform.sdk import platform_tool, ToolContext
@platform_tool(
name="pm.machines.list",
description="List monitored machines for the tenant.",
roles=["operator", "supervisor"],
)
async def list_machines(ctx: ToolContext) -> list[dict]:
repo = MachineRepo(ctx.tenant_db)
machines = await repo.all()
return [m.to_dict() for m in machines]
@platform_tool(name="pm.machine.restart", roles=["supervisor"])
async def restart_machine(ctx: ToolContext, machine_id: str) -> dict:
# bridge tool — runs on customer site
result = await ctx.bridge.dispatch("pm.machine.restart", machine_id)
await ctx.audit.write("machine.restart", machine_id=machine_id)
return result
What "@platform_tool" earns you for free.
Agent registration
Tool appears in the executor's registry on next request, scoped to the declared roles.
REST endpoint
Mirror endpoint mounted at /api/v1/{module_code}/tools/{name}. Same signature, same RBAC.
CLI verb
platform call pm.machines.list --tenant acme-ops — for ops and runbooks.
Audit row
Every invocation written to the platform audit log with the agent session id.
Tenant routing
ctx.tenant_db is pre-bound to the caller's tenant DB. You cannot accidentally cross.
RBAC enforcement
Roles checked before your function runs. Unauthorized callers get a 403, not a stack trace.
Bridge dispatch
Mark a tool 'bridge' and it runs at the customer site, not the cloud. Same decorator.
Typed schema
Function signature becomes the agent's JSON schema. Pydantic for complex returns.