Task Mandates
Delegate non-monetary work — research, email drafting, scheduling, code execution — with cryptographic action limits, compute budgets, human gates, and full audit trails. The same chain-of-custody guarantees as spending mandates, but for any agent capability.
Quick reference — task mandate lifecycle
Core pattern: issueTaskMandate() → attenuateTaskMandate() → enforceTaskConstraints() → TaskReceiptBuilder.complete()
Spending vs Task mandates
Spending Mandate
- ✓ Controls monetary spend (USD limit)
- ✓ Merchant allow-list
- ✓ Intent → Cart → Payment chain
- ✗ No action or compute limits
- ✗ No reversibility enforcement
Task Mandate
- ✓ Controls capability invocations
- ✓ Compute budgets (tokens, time, requests)
- ✓ Reversibility enforcement
- ✓ Human gates for irreversible actions
- ✓ TaskReceipt audit trail
- ✓ Optional: hybrid with spend limit
Install the credentials package
Task mandate functions live alongside spending mandates in @aroha-sdk/credentials.
npm install @aroha-sdk/credentials @noble/ed25519 @noble/hashesIssue a Task Mandate
issueTaskMandate() creates a signed token authorising a vendor agent to invoke specific capabilities within your defined limits. The allowed list is exhaustive — anything not listed is denied.
import { issueTaskMandate } from "@aroha-sdk/credentials";
const mandate = await issueTaskMandate(
"did:aroha:human:alice", // grantor — you (or your personal agent)
"did:aroha:vendor:researcher", // grantee — the vendor agent receiving authority
["research", "web-search", "web-read", "summarise"], // allowed capabilities
{
computeLimit: {
llmTokens: 50_000, // max LLM tokens across all calls
webRequests: 100, // max outbound HTTP fetches
executionTimeMs: 60_000, // hard deadline — 60 seconds
},
reversibleOnly: true, // block send-email, publish-post, etc.
actionLimits: {
"web-search": 20, // at most 20 search calls
},
},
myPrivateKey,
3_600_000 // TTL in ms (1 hour)
);
// mandate.token is a base64url blob — pass it in the Aroha message bodyIssue a hybrid mandate (task + spend)
For tasks that also involve money — book a flight with research — set mandateType to 'hybrid' and provide both taskConstraints and spendingConstraints.
const hybrid = await issueTaskMandate(
myDID, flightAgentDID,
["research", "check-availability", "book-flight"],
{
reversibleOnly: false, // allow the irreversible book-flight action
humanGates: ["book-flight"], // pause here for human approval before booking
actionLimits: { "book-flight": 1 },
},
myPrivateKey,
3_600_000,
{
spendingConstraints: {
spendLimitUsd: 600,
currency: "USD",
allowedMerchants: ["did:aroha:provider:airnz"],
},
}
);Add a human gate
humanGates lists capabilities that require the user to approve before the agent executes them. The SDK emits an approval_required SSE event; the personal agent must call POST /v1/resume with the approval before execution continues.
// Issue the mandate with humanGates on irreversible actions
const mandate = await issueTaskMandate(
myDID, emailAgentDID,
["draft-email", "send-email"],
{
reversibleOnly: false,
humanGates: ["send-email"], // pause before this action
actionLimits: { "send-email": 3 },
},
myPrivateKey,
);
// On the vendor agent side — the SDK emits:
// { type: "approval_required", id: "gate-xyz", action: "send-email", draft: { ... } }
// Personal agent then calls:
// POST /v1/resume
// { "approvalId": "gate-xyz", "approved": true }
// Only then does the send-email execute.
// If no response within approvalTimeoutMs, the action is auto-cancelled.Attenuate to a child mandate
A vendor agent that receives a task mandate can sub-delegate to a specialist agent — but it can only narrow the constraints. Child's allowed list must be a subset of parent's. computeLimit cannot increase on any dimension.
import { attenuateTaskMandate } from "@aroha-sdk/credentials";
// Parent mandate: allowed ["research", "web-search", "summarise"], 50k tokens
const child = await attenuateTaskMandate(
parentMandate, // the SignedTaskMandate received from the caller
"did:aroha:vendor:summariser", // specialist sub-agent
["web-search", "summarise"], // subset of parent's allowed list
{
computeLimit: { llmTokens: 10_000 }, // narrower than parent's 50k
reversibleOnly: true,
},
agentPrivKey, // vendor agent signs this attenuation
);
// Attempting to widen throws immediately:
// attenuateTaskMandate(parent, ..., { computeLimit: { llmTokens: 99_999 } }, ...)
// → Error: child llmTokens (99999) exceeds parent (50000)Enforce at the capability gate
Call enforceTaskConstraints() before executing any capability. It verifies the mandate signature, checks expiry, validates the grantee, and asserts the requested action is within all constraints. Throws TaskConstraintViolation with a typed code on any breach.
import { enforceTaskConstraints, TaskConstraintViolation } from "@aroha-sdk/credentials";
// Called inside your vendor agent, before executing a capability:
try {
const mandate = await enforceTaskConstraints(
token, // base64url token from the incoming message
callerPublicKey, // public key of whoever issued this mandate
{
expectedGrantee: myDID, // my own DID
requestedCapability: "web-search", // what I'm about to do
currentActionCounts: { "web-search": 7 }, // consumed so far this session
currentCompute: { llmTokens: 12_000, webRequests: 30 },
capabilityReversibility: "full", // from capability taxonomy
}
);
// mandate.taskConstraints is the verified, authorized constraint set
// Safe to execute the capability
} catch (err) {
if (err instanceof TaskConstraintViolation) {
// err.code: one of:
// "MANDATE_INVALID" — bad signature, expired, wrong grantee
// "CAPABILITY_NOT_PERMITTED" — not in allowed list, or in blocked list
// "REVERSIBILITY_VIOLATION" — irreversible cap blocked by reversibleOnly
// "HUMAN_GATE_REQUIRED" — capability needs approval before running
// "ACTION_LIMIT_EXCEEDED" — invocation count cap hit
// "COMPUTE_LIMIT_EXCEEDED" — tokens, requests, or time budget exhausted
console.error(`Rejected [${err.code}]: ${err.message}`);
}
}Build a task receipt
Use TaskReceiptBuilder to record every action as the task runs. Return the completed receipt to the personal agent as the task output — it's the auditable proof of exactly what happened.
import { TaskReceiptBuilder } from "@aroha-sdk/credentials";
const receipt = new TaskReceiptBuilder(
mandate.mandateId,
correlationId,
myAgentDID, // executedBy
mandate.grantor // issuedBy
);
// As each action executes, record it:
receipt.addAction({
capability: "web-search",
reversibility: "full",
startedAt: new Date().toISOString(),
durationMs: 240,
status: "success",
humanGateTriggered: false,
});
receipt.recordLlmTokens(1_842);
receipt.recordWebRequest();
// Add output artefacts:
receipt.addOutput({ type: "data", summary: "Found 8 relevant results for query" });
// Finalise and return:
const final = receipt.complete("complete");
// final.mandateBoundsCheck.withinBounds === true (if no violations were recorded)
// Return final in the Aroha response envelope bodyUse the capability taxonomy
The taxonomy maps canonical capability IDs to reversibility and risk tier. Use it at your capability gate to populate capabilityReversibility without hardcoding it everywhere.
import { getReversibility, getRiskTier, isPermitted } from "@aroha-sdk/core";
// Look up before calling enforceTaskConstraints:
const rev = getReversibility("send-email"); // → "irreversible"
const tier = getRiskTier("send-email"); // → 3
// Or use isPermitted for a combined pre-flight check:
const check = isPermitted(
"send-email",
mandate.allowed,
mandate.blocked,
mandate.taskConstraints.reversibleOnly,
);
if (!check.permitted) {
throw new Error(check.reason);
}
// Full taxonomy covers 35+ canonical capabilities across 5 risk tiers:
// Tier 1 — read-only: "web-search", "read-file", "check-balance", "research"
// Tier 2 — artefacts: "draft-email", "create-calendar-event", "write-file"
// Tier 3 — irreversible non-financial: "send-email", "publish-post", "submit-form"
// Tier 4 — financial: "book-flight", "pay-bill", "charge-card", "transfer-funds"
// Tier 5 — system: "deploy-code", "run-code", "modify-permissions"What the protocol enforces vs. what you enforce
The protocol cryptographically ensures that child mandates cannot exceed parent constraints. It does not automatically intercept capability calls — you must call enforceTaskConstraints() at your capability gate before executing any side-effecting action. Human gates work only if the personal agent implements the approval_required SSE event handler and the POST /v1/resume flow.