@pleach/rbac-contract
Zero-dependency type contract for role-based access control — the RbacRuntime interface @pleach/core types its rbacRuntime? slot against, plus the plugin-hook adapter that wires it in.
A latch with no gate built to hang it on yet: @pleach/rbac-contract
defines the shape of a permission check before any host has wired
one in. It's the second package to use the
cycle-break contract pattern —
same reasoning as @pleach/compliance-contract, applied to
authorization instead of PII/PHI compliance. @pleach/core types
SessionRuntimeConfig.rbacRuntime?: against this contract so it takes
no type-graph edge on whatever concrete authorization service a host
plugs in.
What it provides
- The
RbacRuntimeinterface — two methods,checkPermissionandgetRolesForActor. Deliberately smaller thanComplianceRuntime's five: this contract was authored fresh, with no shipped implementer yet, so it stays scoped to what a pre-execution authorization gate actually needs rather than speculating ahead of a real consumer. CheckPermissionInput—actorId, optionalactorType,action, optionalresource, optional free-formcontext.PermissionDecision—allowed, optionalreason, optionalroles.RBAC_RUNTIME_UNCONFIGURED_SENTINEL— the one runtime export (a literal string), for hosts that want a grep-able marker on a decision made with noRbacRuntimeconfigured.
interface RbacRuntime {
checkPermission(input: CheckPermissionInput): Promise<PermissionDecision>;
getRolesForActor(actorId: string): Promise<readonly string[]>;
}Ships zero runtime and zero peer dependencies. Every export is a TypeScript type or the one sentinel constant.
Install
npm install @pleach/rbac-contractMost consumers never install this directly — @pleach/core depends
on it and re-exports the type for slot-typing purposes. Install it
explicitly only when authoring an RbacRuntime implementation without
pulling in a specific implementer package.
The adapter — and what it doesn't do yet
@pleach/core/rbac ships createRbacApprovalFlow(rbacRuntime, resolveActor),
which adapts an RbacRuntime into ApprovalFlowContribution — the
existing pre-execution checkApprovalNeeded plugin hook, the same one
@pleach/core already reserves for cost/risk gating. No new hook was
added for this.
Read this before wiring it into anything that needs to actually
enforce a boundary: checkApprovalNeeded has no shipped consumer in
the reference host today. Registering createRbacApprovalFlow's
output via contributeApprovalFlow() wires a plugin hook that nothing
currently calls — on its own, it does not gate tool execution. It
demonstrates the intended integration shape for the day a host opts
into the hook-routed gateway, or for calling rbacRuntime.checkPermission()
directly from your own enforcement path in the meantime.
There's a second, structural reason not to treat it as a hard
denial today: ApprovalRequest (the hook's non-null return type) has
no verdict field — it's a pre-execution question a human resolves,
not an answer. Every resolution path lets a human edit the arguments
and approve anyway, so a denial routed through this adapter is
human-overridable by construction. resolveActor classifies every
call into one of three outcomes rather than a bare actor | null, so
"intentionally ungated" and "couldn't identify the caller" don't
collapse into the same auto-approve:
type RbacActorResolution =
| { kind: "actor"; actor: { actorId: string; actorType?: string } }
| { kind: "skip" } // no end-user in the loop — auto-approves
| { kind: "unresolvable" }; // should be identifiable, resolution failed — DENIESA policy-check failure denies too — checkPermission throwing is not
treated as an implicit grant.
import { createRbacApprovalFlow } from "@pleach/core/rbac"
import { definePleachPlugin } from "@pleach/core/plugins"
import type { RbacRuntime } from "@pleach/rbac-contract"
const rbac: RbacRuntime = {
async checkPermission({ actorId, action, resource }) {
const allowed = !(action === "tool:execute" && resource === "delete_database")
return { allowed, reason: allowed ? undefined : `${actorId} lacks ${action} on ${resource}` }
},
async getRolesForActor() {
return ["member"]
},
}
const rbacPlugin = definePleachPlugin({
name: "rbac",
version: "1.0.0",
contributeApprovalFlow: () =>
createRbacApprovalFlow(rbac, async (toolCall) => {
const actorId = await resolveCurrentRequestActorId() // your own lookup
return actorId
? { kind: "actor", actor: { actorId, actorType: "user" } }
: { kind: "unresolvable" }
}),
})Where to go next
@pleach/base-tools
Domain-agnostic tool primitives — math, datetime, scratchpad, unit_convert, text_search, json_query, memory, and opt-in web_search, web_fetch, code_exec, ask_user, filesystem, and recall.
@pleach/sandbox
Vendor-neutral SandboxProvider contract plus four endpoint-shape adapters that ship inside the package — the seam @pleach/coding-agent reads.