@cheela/runtime
Everything that runs on your infrastructure: the registry of capabilities, the handlers that verify an incoming Cheela request, and the HMAC primitive underneath them.
Install
npm install @cheela/runtime @cheela/sdkRuntime
import { Runtime } from "@cheela/runtime";
const runtime = new Runtime();
const guarded = new Runtime({ permissions: ["orders:read", "orders:write"] });options.permissionsreadonly string[]Permissions this runtime holds. An action requiring one that is absent throws before its handler runs.
| METHOD | RETURNS |
|---|---|
| register(capability, action) | void |
| execute(name, input, options?) | Promise<RuntimeExecutionResult> |
| getCapabilities() | readonly Capability[] |
| getRegistrations() | readonly RuntimeRegistration[] |
getRegistrations() exists for tooling — the CLI uses it to compile a manifest. Both getters return snapshots; the registry itself stays private.
register()
runtime.register(
{
name: "order-status",
description: "Looks up the status of one order by its id",
version: "1.0.0",
input: z.object({ orderId: z.string() }),
},
{
name: "lookup",
async handler(context, input) {
return db.orders.findById(input.orderId);
},
},
);Throws immediately when:
- The name is invalid. The error names the offending character and suggests a replacement.
- The name is already registered. Names are unique per runtime.
A name the model cannot be given is a capability that can never be invoked. Left to surface later it becomes an opaque provider 400 on the first real execution — long after the name has been published in a manifest strangers may already have cached.
execute()
Runs a capability in-process. The handlers below call this for you; call it directly in tests, or to reuse a capability from your own code.
const result = await runtime.execute("order-status", { orderId: "8812" }, {
endUserToken: "session_abc",
executionId: "exec_...",
});
// { executionId, output, startedAt, completedAt }options.endUserTokenstringThe caller’s credential, forwarded to context.endUserToken. Required for capabilities marked requiresEndUser.
options.executionIdstringCheela’s id for this execution, so a handler that logs it can be joined to the trace. Absent for a direct in-process call, where a fresh UUID is minted.
In order, execute():
1. look up the registration → throws if unknown
2. enforce requiresEndUser → throws if no credential
3. check the action's permissions → throws if missing
4. validate input against the schema → throws ValidationError
5. run the handler
6. validate output against the schemaRequest handlers
createCheelaHandler
For anything built on Request/Response: Next.js route handlers, Hono, Deno, Bun, Cloudflare Workers.
import { createCheelaHandler } from "@cheela/runtime";
import runtime from "../../../.cheela/runtime";
export const POST = createCheelaHandler({
runtime,
secret: process.env.CHEELA_RUNTIME_SECRET!,
runtimeId: process.env.CHEELA_RUNTIME_ID,
});createCheelaExpressHandler
app.post(
"/cheela/execute",
express.raw({ type: "*/*" }),
createCheelaExpressHandler({ runtime, secret }),
);Mount it with a raw body parser. Given a parsed object it answers 400 raw_body_required with an explanation rather than failing every request as signature_mismatch — which reads as a wrong secret and sends people off rotating a credential that was fine.
HandlerOptions
runtimeRuntimerequiredThe runtime to dispatch into.
secretstringrequiredThe runtime secret, shown once at registration. Never default this to an empty string.
runtimeIdstringWhen given, a signature issued for another runtime is rejected. Cheap, and worth setting.
toleranceMsnumberdefault 300000Clock-skew allowance, matching the server’s default of five minutes.
nonceStoreNonceStoreDefaults to a shared in-process store. Supply your own when running more than one instance.
Responses
| STATUS | BODY |
|---|---|
| 200 | { output } |
| 200 | { output: null, error } |
| 400 | { error: "invalid_json" } |
| 400 | { error: "missing_capability" } |
| 401 | { error: reason } |
A capability that throws is reported, not raised — Cheela turns it into a tool_result error so one bad call does not abort the agent run.
verifyCheelaSignature
The primitive, for frameworks neither handler covers.
import { verifyCheelaSignature, MemoryNonceStore } from "@cheela/runtime";
const result = await verifyCheelaSignature({
secret: process.env.CHEELA_RUNTIME_SECRET!,
headers: request.headers,
rawBody: await request.text(),
runtimeId: process.env.CHEELA_RUNTIME_ID,
nonceStore: new MemoryNonceStore(),
});
if (!result.valid) {
return new Response(result.reason, { status: 401 });
}headers accepts a Headers object or a lower-cased record. rawBody must be the exact bytes received.
Checks run in this order, and the order is deliberate:
1. all four x-cheela-* headers present → missing_headers
2. runtime matches (if pinned) → runtime_mismatch
3. timestamp is a number → timestamp_invalid
4. |now - sentAt| <= tolerance → timestamp_outside_tolerance
5. HMAC matches, constant-time → signature_mismatch
6. nonce unused → nonce_replayed- The cheap structural checks come first, so malformed traffic is rejected before any HMAC work happens.
- The timestamp comparison is absolute, so a request from a clock running fast is rejected too — a future-dated timestamp would otherwise extend its own replay window.
- The nonce is claimed after the signature verifies, so an attacker cannot burn nonces with forged requests.
DEFAULT_TOLERANCE_MS is exported, and is five minutes.
NonceStore
interface NonceStore {
claim(nonce: string, expiresAt: number): boolean | Promise<boolean>;
}claim returns false when the nonce has been seen. MemoryNonceStore implements it in-process and drops entries past the tolerance window.
Behind a load balancer, a captured request can be replayed once per instance until each has seen the nonce. Back the interface with Redis or your database if you run more than one.
const redisNonceStore: NonceStore = {
async claim(nonce, expiresAt) {
const claimed = await redis.set(`cheela:nonce:${nonce}`, "1", {
NX: true,
PXAT: expiresAt,
});
return claimed !== null;
},
};Types
interface RuntimeRegistration<TInput, TOutput> {
capability: Capability<TInput, TOutput>;
action: Action<TInput, TOutput>;
}
interface RuntimeExecutionResult<TOutput> {
executionId: string;
output: TOutput;
startedAt: number;
completedAt: number;
}
interface CapabilityRequestBody {
executionId?: string;
capability?: string;
input?: unknown;
metadata?: Record<string, string>;
endUserToken?: string; // never logged
}
type VerifyFailureReason =
| "missing_headers"
| "runtime_mismatch"
| "timestamp_invalid"
| "timestamp_outside_tolerance"
| "nonce_replayed"
| "signature_mismatch";Practical guidance in Serve capability calls.