Ship a multi-tenant agent in five minutes, not a sprint.
Install, add @agentAction to one service method, invoke. Tenant scope, permissions, audit, and reversibility gating come for free — no parallel schema, no plumbing.
class JobService {
@agentAction({
description: "Get a technician's schedule for a given date",
reversibility: "idempotent",
requiredPermissions: ["schedule:read"],
audienceRoles: ["dispatcher", "owner", audience.self((ctx, p) => ctx.userId === p.techId)],
costWeight: 1,
})
async getTechSchedule(ctx: AgentContext, params: { techId: string; date: Date }) {
// ctx.tenantId is injected and validated. Permissions pre-checked.
// Audit log written. Tool schema derived from these types — no parallel copy.
return this.db.schedule.find({ tenantId: ctx.tenantId, ...params });
}
}One decorator. Five things you don't hand-roll.
ctx.tenantId is injected and every call is scoped to it. A cross-tenant leak through the agent path is structurally impossible, not a check you have to remember.
requiredPermissions and audienceRoles are enforced against your role registry before the method executes. A role typo is a compile error, not a production bug.
invoke returns a typed Result. Failures are values you handle, not exceptions that unwind your request. Check isOk and the success type is narrowed for you.
Tag a method reversible, idempotent, or irreversible. Irreversible calls pause for in-product approval instead of firing — a runtime invariant, not a prompt you hope the model obeys.
const result = await invoke("JobService.getTechSchedule", ctx, params);
if (isOk(result)) {
result.value; // typed, scoped to ctx.tenantId
} else {
result.error; // a value, never a throw
}Every action writes a paired PROPOSED and COMPLETED record — actionKey, tenantId, userId, role — in one uniform shape across your whole app. You write no audit code.
Every agent SDK assumes you're starting from nothing. You already shipped the product.
OpenAI's Agents SDK has no concept of tenant context, permission checks, audit logging, or reversibility — so every SaaS team rebuilds them by hand, around every method. That's the parallel schema that drifts, the copy-pasted permission check, the tenant ID threaded by hand. Miss it once and you have a cross-tenant leak: a P0 in any multi-tenant product.
We don't say that as a take — we built the alternative and shipped it on npm as @hypergaas/core. Tenant scope and permissions are enforced at runtime before your method body runs, irreversible calls pause for approval, and every action writes a paired audit record. TypeScript-strict, no any in the public surface, 49 passing tests.
From a service method to a tenant-scoped, audited agent action.
Three steps: install, decorate a method you already have, invoke. By the last line you have a permission-checked, tenant-scoped call with a full audit trail — and you wrote none of that plumbing.
- 01 — install one package, one dependency
npm install @hypergaas/core - 02 — decorate a method you already have — no parallel schema, no handler
// 1. Bind your roles to the registry once, at module scope. import { defineRoles, audience, createActionRegistry, type AgentContext } from "@hypergaas/core"; const roles = defineRoles({ owner: { displayName: "Owner", seniority: 4, canApproveIrreversibleUpTo: "high" }, dispatcher: { displayName: "Dispatcher", seniority: 3, canApproveIrreversibleUpTo: "medium" }, technician: { displayName: "Technician", seniority: 1 }, }); const { agentAction, invoke } = createActionRegistry(roles); // 2. Decorate a service method you already have. No parallel schema, no handler. class JobService { @agentAction({ description: "Get a technician's schedule for a given date", reversibility: "idempotent", requiredPermissions: ["schedule:read"], // 'dispatcher' | 'owner' are checked against your role registry — a typo is a // compile error. audience.self(...) infers params from the method signature. audienceRoles: ["dispatcher", "owner", audience.self((ctx, p) => ctx.userId === p.techId)], costWeight: 1, }) async getTechSchedule(ctx: AgentContext, params: { techId: string; date: Date }) { return this.db.schedule.find({ tenantId: ctx.tenantId, ...params }); } } - 03 — invoke tenant scope, permissions, and audit enforced before the body runs
import { createAgentContext, isOk } from "@hypergaas/core"; // 3. Build one context per request. It is the only source of tenant identity — // set it once at your request layer and never thread tenantId by hand again. const ctx = createAgentContext({ tenantId: "acme-hvac", userId: "u_marcus", role: "dispatcher", permissions: ["schedule:read"], autonomyLevel: "medium", }); // 4. Invoke. Permissions and tenant scope are enforced before the body runs; // the result is typed, not thrown. const result = await invoke("JobService.getTechSchedule", ctx, { techId: "u_marcus", date: new Date("2026-05-25"), }); if (isOk(result)) { console.log(result.value); // the schedule, scoped to tenant "acme-hvac" } // The default in-memory audit logger now holds a paired PROPOSED + COMPLETED // record for this call — actionKey, tenantId, userId, role — without you writing // a line of audit code. Swap InMemoryAuditLogger for a durable backend behind the // same interface when you go to production.
Read the source before you install.
The whole v0.1 public surface is on GitHub — types, the action registry, the worked example. No black box: read it, paste the quickstart, see it run. If it's useful, a star helps other SaaS teams find it.