Documentation menu▾
Documentation
Concepts and model
MakerChecker models an agent the way a regulated firm models an employee: an identity that holds one role, a key ring of versioned skill grants, and hard limits on its authority. Every action runs through one authorization check before it happens. This page is the developer-depth account of the entities, the check, and the codes it returns.
Companion reading
The model: agent, role, skill, grant
Four entities carry the structure. An agent holds exactly one role. A role is granted specific skill versions. Everything not granted is denied. Nothing is edited in place: skills, grants, and roles are versioned, so the record of what was allowed at a point in time stays true.
| Entity | Shape | Rule it enforces |
|---|---|---|
agent | A name, a status (active, suspended, retired), a model config, and exactly one role. | One identity, one role. A suspended or retired agent acts on nothing. |
role | A name and a limits JSONB object. Skills are granted to roles, never to agents directly. | Authority is a property of the role, so it is reviewable and shared. |
skill | A canonical name@version (for example txn-match@1), a risk tier, a status (published or deprecated), and input and output schemas. | A capability is addressable, typed, and pinned to an exact version. |
role_skill_grant | A versioned, revocable link between one role and one skill version. | Deny by default. No unrevoked grant means no execution, with no bypass. |
A fifth entity, sod_constraint, is a symmetric pair of roles that cannot both act in the same run or proxy session. It is stored once with role_a_id < role_b_id so the pair has a single canonical form. Segregation of duties is covered in its own section below.
Skills and risk tiers
A skill is the unit an agent proposes to run. It is addressed by its canonical name@version, carries input and output schemas, and is either published or deprecated. Deprecating a version stops new executions without rewriting history: the audit of what ran under the old version stays intact.
Every skill has a risk tier that decides what happens after the authorization checks pass:
- low and medium run immediately once the checks pass, and the outcome is recorded.
- high stops. In a flow it requires a preceding approval gate. Through the proxy it is categorically denied, because the proxy path has no gates.
The risk tier is read from the skill itself, so the same skill is governed the same way wherever it is called. A high-risk skill cannot quietly become low-risk by being invoked from a different place.
Grants: deny by default, versioned
A grant ties one skill version to one role. The check looks for an exact, unrevoked grant for the agent's role and the specific version proposed. There is no inheritance and no wildcard: a grant of txn-match@1 does not cover txn-match@2.
Grants are revocable, and revocation is a timestamp rather than a delete. A revoked grant still exists in the record; it simply no longer authorizes anything. This is why the absence of a grant, and the presence of a revoked one, are both denials with the same outcome: the skill does not run.
server/src/engine/enforcement.ts
enforce()
// enforce() runs before any action, at both decision time and// invocation time. Deny by default, in this order://// 1. agent exists and is active// 2. each skill@version exists and is published// 3. an unrevoked grant ties that skill to the agent's role// 4. high-risk skills require a preceding approval gate// 5. segregation of duties: no conflicting role already acted//// Any failed check throws an EnforcementError with a code. The// action never runs, and the denial is appended to the audit chain.
The same check runs twice in a flow: once at decision time, when the step is scheduled, and again at invocation time, immediately before the skill executes. The proxy runs the identical agent, skill, grant, and segregation-of-duties checks before letting an external orchestrator run the tool. Deny by default, on both paths.
Segregation of duties
A segregation-of-duties constraint names two roles that must not both act in the same context. The context is a flow run or a proxy session. When a role acts, the check records a frozen snapshot of which role it was. The next action checks that snapshot set: if any role that already acted forms an active constraint pair with the candidate role, the action is denied with sod_violation.
The core rule
Two details make this hold under pressure. Denied attempts never join the actor set: an action that was blocked did not act, so it cannot be used to provoke a later violation or to consume a slot. And on the proxy, concurrent checks in one session serialize on the session row, so two calls cannot race past the actor set at the same instant.
Approval gates
An approval gate is a first-class workflow step, not a wrapper around a skill. It requires a quorum: n of m named approvers must sign before the flow advances. The gate is where a high-risk step waits for human sign-off, and where that sign-off is recorded as part of the run.
- Quorum. The gate defines who may approve and how many of them are required. Fewer than the quorum, and the flow does not advance.
- No self-approval. The requester can never approve their own step.
forbid_requesterdefaults to true, so the agent or person that proposed the action is excluded from the quorum by default. - Fails closed. If the gate cannot be satisfied, the flow stops at it. Nothing downstream runs on an unmet approval.
Gates exist only on the flow path. A high-risk skill called through the proxy has no gate to wait at, so the proxy denies it outright and tells the caller to run it in a governed flow instead.
Role limits
A role's limits field is a JSONB contract, checked immediately before every skill invocation. Limits are split into per-skill caps and per-run budgets. The limits enforced against a scheduled step are the frozen copy taken when the step was scheduled, so an admin editing the live role mid-run cannot change what an already-scheduled run enforces.
JSONB
roles.limits
{"skills": {"txn-match@1": {"maxInvocationsPerRun": 50,"maxAmountPerInvocation": 10000,"amountField": "amount","allowlist": { "field": "destination", "values": ["acct-001", "acct-002"] },"pathScope": { "field": "path", "prefix": "/data/recon" }}},"run": {"maxSkillInvocations": 200,"maxTokens": 100000}}
Per-skill caps gate the arguments of the call, not just which skill may run:
maxInvocationsPerRun: how many times this skill may run in the scope (a flow run or a proxy session).maxAmountPerInvocationwithamountField: the numeric value in that field may not exceed the cap, and a negative amount is rejected.allowlist(field,values): the value in that field must be one of the listed strings. Used for a recipient or destination allowlist.pathScope(field,prefix): the path in that field must sit under the prefix, with traversal out of it refused.
Per-run budgets cap the whole run: maxSkillInvocations across all skills, and maxTokens across all model calls. Both are counted conservatively from the audit trail, so every attempt, including errors, counts toward the cap.
Unreadable config fails closed
Denial-code reference
Every denial returns a stable code, so a caller can branch on it and a reviewer can read the audit without guessing. Authorization codes come from the enforcement check; limit codes come from the role-limits contract. Per-skill limit checks also emit an unreadable-config variant when a configured amount, allowlist, or path field is missing or the wrong type (limit_amount_unreadable, limit_allowlist_unreadable, limit_path_unreadable). The proxy enforces the authorization and per-skill codes; the run-level budgets, limit_tokens and limit_run_invocations, are enforced on the flow path.
| Code | Meaning |
|---|---|
agent_not_found | No agent exists with that name. |
agent_not_active | The agent is suspended or retired, not active. |
skill_not_found | No skill exists at that name@version, or the reference is not canonical. |
skill_deprecated | The skill version exists but is no longer published. |
skill_not_granted | No unrevoked grant ties that skill version to the agent's role. |
high_risk_requires_gate | A high-risk skill ran without a preceding gate, or was called through the proxy, which has none. |
sod_violation | A role paired by an active constraint already acted in this run or session. |
limit_invocations | This skill reached its per-scope invocation cap. |
limit_amount | The amount field exceeds maxAmountPerInvocation, or is negative. |
limit_tokens | The run reached its token budget. |
limit_run_invocations | The run reached its total skill-invocation budget. |
limit_allowlist | The field value is not on the configured allowlist. |
limit_path | The path field is outside the allowed prefix. |
With the model in hand, the next step is to put it in front of an agent you already have. Wrap your agent covers the LangChain connector, the Claude Agent SDK, the generic wrapper, and Python. To see the chain these checks write, read the audit trail.