Cheela Labs
REFERENCE

@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

Terminal
npm install @cheela/runtime @cheela/sdk

Runtime

TypeScript
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.

METHODRETURNS
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()

TypeScript
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.
Names are rejected here, not sanitised later

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.

TypeScript
const result = await runtime.execute("order-status", { orderId: "8812" }, {
  endUserToken: "session_abc",
  executionId: "exec_...",
});

// { executionId, output, startedAt, completedAt }
options.endUserTokenstring

The caller’s credential, forwarded to context.endUserToken. Required for capabilities marked requiresEndUser.

options.executionIdstring

Cheela’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():

Order
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 schema

Request handlers

createCheelaHandler

For anything built on Request/Response: Next.js route handlers, Hono, Deno, Bun, Cloudflare Workers.

app/cheela/execute/route.ts
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

TypeScript
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

runtimeRuntimerequired

The runtime to dispatch into.

secretstringrequired

The runtime secret, shown once at registration. Never default this to an empty string.

runtimeIdstring

When given, a signature issued for another runtime is rejected. Cheap, and worth setting.

toleranceMsnumberdefault 300000

Clock-skew allowance, matching the server’s default of five minutes.

nonceStoreNonceStore

Defaults to a shared in-process store. Supply your own when running more than one instance.

Responses

STATUSBODY
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.

TypeScript
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:

Order
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

TypeScript
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.

One process, one view

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.

TypeScript
const redisNonceStore: NonceStore = {
  async claim(nonce, expiresAt) {
    const claimed = await redis.set(`cheela:nonce:${nonce}`, "1", {
      NX: true,
      PXAT: expiresAt,
    });
    return claimed !== null;
  },
};

Types

TypeScript
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.