> ## Documentation Index
> Fetch the complete documentation index at: https://docs.humanos.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent Governance

> Put an AI agent under your organization's policy, with the Agent SDK or over MCP: every tool call verified before it runs, mandates a person grants, and a signed record of each decision.

An agent under Humanos has a key, declares the tools it can call, and asks Humanos before every call. Your organization's policy decides each call; a tool the policy leaves to a person runs only under a **mandate** that person grants. Every decision and every outcome is a signed event on the agent's audit trail.

Pick how your agent connects in the selector at the top of the page:

* **Agent SDK**: an agent you write in TypeScript, holding its own key. It can also govern MCP servers you did not write.
* **MCP**: an MCP client, such as Claude Code, Claude Desktop or Cursor, acting as the agent. Humanos holds its key.

## How a call is decided

Humanos decides each call in this order:

1. **The agent.** A suspended or retired agent, or a revoked key, is refused whatever the tool.
2. **The policy.** The tool's access, set on the dashboard's **Policy** page:

| Tool access | `access` | The call |
| - | - | - |
| **Allow** | `allow` | Runs. |
| **Restrict** | `rules` | Runs if it passes the rules of every control on the tool. |
| **Delegate** | `mandate` | Runs only under a mandate a person granted, within its limits and rules. |
| **Deny** | `deny` | Never runs (`policy_denied`). |
| Not set | `not_set` | Follows the policy's default, which allows it unless you change it. |

A **control** is a set of rules over a call's inputs, chosen for a tool on the **Policy** page. A control of type Mandate also declares the limits a person sets when granting a mandate under it, such as a refund cap.

**Agent SDK**

The SDK steps need the [Quick Start](https://docs.humanos.tech/essentials/quick-start.md) setup: the SDK installed, and an API key with its Signature Secret.

With `npx humanos init` and a framework adapter, or `via()`, registering, declaring and guarding are done for you: see [Agent SDK](https://docs.humanos.tech/essentials/integrate/agent-sdk.md#frameworks). The steps below build each piece yourself, for an agent that needs to.

## 1. Register the agent

The agent's key is its identity. `registerAgent` proves the agent holds the key and returns its DID; every call after is signed with that key.

```typescript theme={"dark"}
import { createViaMcpClient, registerAgent, storedSoftwareKey } from "@humanos-ai/agent-sdk";

const client = createViaMcpClient({
  url: "https://mcp.humanos.tech/mcp",
  apiKey: process.env.HUMANOS_API_KEY!,
  signatureSecret: process.env.HUMANOS_SIGNATURE_SECRET!,
});
await client.initialize();

const agentKey = await storedSoftwareKey(".humanos/agent-key.json");
const { did, granted, existing } = await registerAgent(client, agentKey);
// granted: { assurance: "pop", binding: "self" } for a software key
// existing: true when this key was registered before
```

Humanos grades how the key is held and returns the grade as `granted`. `storedSoftwareKey` creates the key in the file on the first run and loads it on every run after, so registering again returns the same DID: a new key is a new agent, and its mandates stay with the old one. Keep the file like the API key, out of the repository. Where no file persists, `softwareKeyFromJson` reads the file's content from a variable; `softwareKey()` creates a key that lasts only as long as the process. Any key store works through the `ViaAgentKey` interface (`publicJwk`, `sign`, `evidence`), so a key that never leaves its KMS or enclave signs the same way.

## 2. Declare its tools

Humanos governs the tools the agent declares. Declare each one as its server describes it: name, description, `inputSchema`, `outputSchema` and annotations, grouped by `service`.

```typescript theme={"dark"}
import { declareTools } from "@humanos-ai/agent-sdk";

await declareTools(client, did, tools); // one call per service
```

Declare again whenever the tools change. A tool left out of a service's new declaration is kept and marked as no longer declared; `{ emptyServices: ["Files"] }` marks every tool of a service the agent stopped using. A service takes at most 500 tools per declaration.

## 3. Set the policy

Until you set the policy, nothing is enforced: every tool follows the default, which allows it.

1. In the [dashboard](https://app.humanos.tech), open **Policy**, under **Agents** in the sidebar. The tools the agent declared are listed there.
2. Choose **Allow**, **Restrict**, **Delegate** or **Deny** for each tool.
3. For a tool set to **Restrict**, choose the control whose rules it runs within. A tool set to **Delegate** runs under any mandate a person grants, a plain authorization included; choose a mandate control to require one under it.
4. To refuse every tool you have not set, change the policy's default to deny.
5. Publish the policy. Nothing changes for your agents until you do. The dashboard asks you to confirm with your passkey or a code sent to your email; only a manager or an owner can publish.

`getPolicy(client, did)` returns the policy as the agent meets it: each declared tool's `access`, its meaning in one sentence, and the controls behind it.

## 4. Guard every call

```typescript theme={"dark"}
import { McpGuardVerifier, policyMandates, ViaDeniedError, ViaGuard } from "@humanos-ai/agent-sdk";

const guard = new ViaGuard({
  agentKey,
  agentDid: did,
  verifier: new McpGuardVerifier(client),
  mandates: policyMandates(client, did),
});

try {
  const out = await guard.call("issue_refund", { order_id: "A-1", amount_eur: 40 }, issueRefund);
  console.log(out.result);
} catch (e) {
  if (e instanceof ViaDeniedError) console.log(`blocked: ${e.reason}`); // the tool never ran
  else throw e;
}
```

For each call the guard:

1. Picks the mandate the call presents, if any. `policyMandates` reads the policy and the agent's mandates, and presents one only for a tool set to **Delegate**.
2. Gets a single-use challenge, signs the tool and its parameters with the agent's key, and asks Humanos to verify.
3. Runs the tool on **allow**, then reports the outcome: `completed`, or `failed` with the error, which it rethrows.
4. Throws `ViaDeniedError` on **deny**, with `reason` and the rule `evaluations`. The tool never runs.

To guard an existing tools object without changing its call sites, `guard.interceptTools(tools)` replaces each function in place; a guarded tool returns its own result and throws `ViaDeniedError` on a deny.

`mode: "observe"` runs the whole check and records every decision, but runs the tool even when the decision is deny. Use it to see what a policy would block before enforcing it.

## 5. When a tool needs a person's mandate

A call to a tool set to **Delegate** with no mandate covering it is denied `mandate_required`, and `ViaDeniedError.request` says what to ask for:

* **A guardrail** (`kind: "guardrail"`): a mandate under one of the controls in `actionIds`. `getAction` returns the control's limits; your agent fills in their values.
* **An authorization** (`kind: "authorization"`): with no control for the tool, a mandate in plain words. It allows one call and ends in an hour, unless `maxExecutions` and `validUntil` say otherwise.

Whom to ask, and the values of the limits, come from your agent's own context.

```typescript theme={"dark"}
import { requestMandate, waitForMandate, ViaDeniedError } from "@humanos-ai/agent-sdk";

try {
  await guard.call("issue_refund", args, issueRefund);
} catch (e) {
  if (!(e instanceof ViaDeniedError) || e.reason !== "mandate_required" || !e.request) throw e;

  const req =
    e.request.kind === "guardrail"
      ? await requestMandate(client, did, {
          kind: "guardrail",
          actionId: e.request.actionIds[0]!,
          userParams: { max_refund_eur: 100 }, // the limits getAction(client, actionId) describes
          contact: "person@example.com",
        })
      : await requestMandate(client, did, {
          kind: "authorization",
          name: "Refund A-1",
          terms: "Refund order A-1, 40 euros",
          contact: "person@example.com",
        });

  console.log(req.consentCard.link); // the person approves on their own device

  const status = await waitForMandate(client, req);
  if (status.phase === "ACTIVE") await guard.call("issue_refund", args, issueRefund);
}
```

`waitForMandate` returns once the person decides (`ACTIVE`, `REJECTED` or `INACTIVE`), or still `PENDING` at the earlier of the consent link's expiry and ten minutes; `until` changes the deadline and `signal` stops the wait. The call after an approval presents the new mandate on its own. `mandateStatus` checks once, for an agent that waits its own way.

## 6. Govern MCP servers you did not write

`startViaMcpProxy` puts the guard in front of MCP servers you connect to but did not write. It merges their tools, declares them to Humanos by service, and verifies every call before it reaches the server.

```typescript theme={"dark"}
import { connectUpstream, createStdioTransport, startViaMcpProxy } from "@humanos-ai/agent-sdk";

const proxy = await startViaMcpProxy({
  upstreams: [
    await connectUpstream({
      service: "Slack",
      url: "https://mcp.slack.com/mcp",
      headers: { Authorization: `Bearer ${slackToken}` },
    }),
  ],
  humanos: client,
  agentKey,
  did,
});
createStdioTransport().run(proxy);

// When a server's tools change, declare them again.
await proxy.refresh("Slack");
```

An upstream is a remote server (`url`, `headers`) or a local one (`command`, `args`, `env`). Your MCP host connects to the proxy as it would to any MCP server. The proxy presents a mandate only for a tool set to **Delegate**: the one whose control lists the tool, or whose person allowed it. Every other call is decided on the policy alone.

For a remote server, an allowed call also carries its call proof: the proxy sends it with the call, in `_meta` and in the `Humanos-Call-Proof` and `Humanos-Agent-Record` headers, so the server can check that Humanos verified the call.

## 7. Test without a platform

`createTestVerifier` decides calls in process, with the same rules and policy the connector applies.

```typescript theme={"dark"}
import { softwareKey, ViaGuard } from "@humanos-ai/agent-sdk";
import { createTestVerifier } from "@humanos-ai/agent-sdk/testing";

const agentKey = await softwareKey();
const t = createTestVerifier({
  rules: [{ name: "cap", expression: "executionParams.tool != 'pay' || executionParams.amount <= userParams.limit" }],
  userParams: { limit: 100 },
  agentKey,
  policy: { tools: { pay: "mandate", search: "allow" }, default: "deny" },
});
const guard = new ViaGuard({ mandate: t.mandate, compiled: t.compiled, agentKey, agentDid: "did:web:agent.test", verifier: t.verifier });

await guard.call("pay", { amount: 500 }, pay); // throws ViaDeniedError: rule_failed
```

**MCP**

The MCP client connects to the [Humanos MCP connector](https://docs.humanos.tech/essentials/integrate/mcp.md) and signs in with OAuth. Humanos holds the agent's key and signs for it, so the client needs no SDK.

## 1. Connect the client

Add `https://mcp.humanos.tech/mcp` to your client and complete the OAuth sign-in. The [MCP Server](https://docs.humanos.tech/essentials/integrate/mcp.md#setup) page has the steps for Claude Code, Claude Desktop, Cursor, VS Code, Windsurf and Codex CLI.

## 2. Register and declare tools

The connector tells the client to call `onboard` first. It walks the client through setup, one step per call:

1. **Register:** `register` with no arguments creates the agent, its key held by Humanos, and `declare_client` records the model and host.
2. **Declare tools:** `declare_tools` with every tool of the services connected to the client, copied as the host describes them.

The client's built-in tools (code, files, web search and the like) are not declared: they are not the organization's to govern.

## 3. Set the policy

Until you set the policy, nothing is enforced: every tool follows the default, which allows it.

1. In the [dashboard](https://app.humanos.tech), open **Policy**, under **Agents** in the sidebar. The tools the client declared are listed there.
2. Choose **Allow**, **Restrict**, **Delegate** or **Deny** for each tool.
3. For a tool set to **Restrict**, choose the control whose rules it runs within. A tool set to **Delegate** runs under any mandate a person grants, a plain authorization included; choose a mandate control to require one under it.
4. To refuse every tool you have not set, change the policy's default to deny.
5. Publish the policy. Nothing changes for your agents until you do. The dashboard asks you to confirm with your passkey or a code sent to your email; only a manager or an owner can publish.

`get_policy` returns the policy as the agent meets it: each declared tool's `access`, its meaning in one sentence, and the controls behind it.

## 4. Verify every call

Before any call with a side effect, the client calls `verify` with the tool and its full parameters:

* **allow**: it runs the tool, then calls `report_outcome` with `completed` or `failed`.
* **deny**: it stops and tells the person why. It does not retry.

The connector's instructions tell the client to do this on every call; most clients ask you to confirm a tool call first, so keep that confirmation on.

## 5. When a tool needs a person's mandate

A call to a tool set to **Delegate** with no mandate covering it is denied `mandate_required`. The client then:

1. Reads what to ask for in `get_policy`'s `request`: a guardrail under a control (`get_action` returns its limits), or, with no control, an authorization in plain words.
2. Asks for it with `request_mandate`, naming the person who grants it.
3. Shows the person the consent link, and polls `mandate_status` until they decide.
4. Calls `verify` again with the new `mandateId`.

## An agent with its own key

An agent that holds its own key can use the connector the same way with the organization's API key instead of OAuth: it registers with `register_challenge` and `register`, and calls `challenge` before each `verify` and `report_outcome` to sign the call. The Agent SDK does exactly this.

Every tool and its parameters are listed on the [MCP Server](https://docs.humanos.tech/essentials/integrate/mcp.md#tools) page.

## Deny reasons

| `reason` | Meaning |
| - | - |
| `policy_denied` | The policy does not allow the tool, or the call breaks a Restrict control's rule, named in `evaluations`. Do not retry. |
| `mandate_required` | The tool needs a person's mandate and none covers the call. |
| `rule_failed` | The call breaks a rule of the mandate's control; check `evaluations`. |
| `tool_not_covered` | The mandate does not cover this tool. |
| `required_param_missing` | A value a rule checks was left out of the call. |
| `stepup_required` | The call needs an approval nobody is named to give. |
| `actor_suspended` | The agent is suspended; an operator resumes it in the dashboard. |
| `actor_retired` | The agent has been retired. |
| `agent_key_revoked` | The agent's key has been revoked. |

A deny is a decision, not an error: the agent asked for something it is not allowed to do.

## Audit trail

Every decision, every outcome and every mandate a person grants or revokes is a signed event, hash-chained to the agent's record. The dashboard shows it on the agent's **Audit trail**.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.