@aroha-sdk/credentials
stable · v1.2.0

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

01Install02Issue mandate03Hybrid mandate04Human gates05Attenuate06Enforce07Receipt08Taxonomy

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
Spending mandate docs →

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
01

Install the credentials package

Task mandate functions live alongside spending mandates in @aroha-sdk/credentials.

npm install @aroha-sdk/credentials @noble/ed25519 @noble/hashes
02

Issue 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 body
03

Issue 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"],
    },
  }
);
04

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.
05

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)
06

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}`);
  }
}
07

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 body
08

Use 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.