Version 0.4 April 2026

Service Providers

A HAP Service Provider (SP) is the cryptographic authority and accountability layer of the protocol. It signs attestations that prove a human authorized a specific scope of action, and it signs receipts that prove each action stayed within those bounds.

In v0.4, the SP gains two new responsibilities beyond v0.3:

  1. Issuing execution receipts for every authorized action
  2. Tracking cumulative state against human-declared bounds

This makes the SP a runtime dependency for execution. v0.3 SPs were touched only at attestation time and for initial key resolution. v0.4 SPs are touched on every tool call.

SPs do not validate truth. They validate Profile compliance and bounds adherence. SPs do not trust executors. They enable users to enforce boundaries.

Reference implementation: humanagencyprotocol.com


Core Principles

Profile-Centric

SPs validate requests against specific Profiles. Each v0.4 Profile defines:

Profiles in v0.4 do not define executionPaths, requiredDomains, or gateQuestions. Domain requirements and execution paths are gone; gate questions are universal in the gateway UI.

Trustless by Design

Anyone may run an SP — on a phone, server, or embedded device. No central registry. No approval. No committee.

Privacy-Preserving

SPs receive only the bounds (in plaintext for enforcement) and hashes for everything else:

SPs never receive context content, intent text, or any other semantic content. Context content stays on the gateway, encrypted at rest.

Per-Group Governance

In v0.4, organizational governance moves from the profile to the SP. Each group on the SP defines:

This makes profiles universal — the same charge@0.4 profile works for personal mode and for a 500-person enterprise.


Service Provider Responsibilities

A v0.4 SP has five primary responsibilities:

  1. Attestation issuance — sign authorizations after verifying profile compliance, identity, and (in group mode) domain authority
  2. Receipt issuance — sign execution receipts after checking per-transaction bounds and cumulative limits
  3. Cumulative state tracking — maintain running totals (daily, monthly) per attestation per action type
  4. Revocation — maintain a revocation list and refuse to issue receipts against revoked attestations
  5. Retention — store attestations and receipts for at least the profile-defined retention_minimum

Attestation Issuance

SP Request Schema

{
  "profile_id": "charge@0.4",
  "bounds": {
    "profile": "charge@0.4",
    "amount_max": 100,
    "amount_daily_max": 500,
    "amount_monthly_max": 5000,
    "transaction_count_daily_max": 20
  },
  "bounds_hash": "sha256:...",
  "context_hash": "sha256:...",
  "execution_context_hash": "sha256:...",
  "domain": "finance",
  "did": "did:key:...",
  "gate_content_hashes": {
    "intent": "sha256:..."
  },
  "commitment_mode": "automatic",
  "ttl": 86400,
  "title": "Daily refund processing",
  "group_id": "acme-corp"
}

Context fields (currency, action_type) live in the contextSchema and are hashed into context_hash by the gateway. They are not part of the bounds sent to the SP — the SP only sees the context hash, and the Gatekeeper is the sole enforcer of context enum/subset constraints.

FieldDescription
profile_idThe profile this authorization references
boundsThe full bounds object (sent in plaintext for SP enforcement)
bounds_hashHash of the canonical bounds, computed by the gateway
context_hashHash of the canonical context (gateway only, sha256 of empty string when context is empty)
execution_context_hashHash of the resolved execution context schema
domainThe domain the attester claims authority for
didThe verified DID of the attester
gate_content_hashesAt minimum: { "intent": "sha256:..." }
commitment_mode"automatic" or "review"
ttlRequested TTL in seconds (clamped to profile max)
titleOptional human-readable label, stored as SP metadata
group_idGroup identifier (omit or null for personal mode)

The SP receives the bounds in plaintext because it must enforce them at receipt time. It does not receive the context content — only context_hash.

Each attestation request covers a single domain. Multi-domain decisions require separate attestation requests — one per domain owner.

Validation Rules

The SP MUST reject the attestation request if:

SP Authorization Responsibilities

Before signing an attestation, the SP MUST:

  1. Verify identity — Validate the attester’s authentication. Resolve to a verified DID.
  2. Resolve domain requirements — In group mode: look up profileDomains for the group and the requested profile.
  3. Check membership — In group mode: verify that the authenticated DID holds the required domain in the group.
  4. Validate bounds — Recompute bounds_hash from the submitted bounds and compare to the provided value.
  5. Validate against group limits — In group mode: if the group has limit ceilings configured, verify the bounds do not exceed them.
  6. Reject or sign — Only sign the attestation if all checks pass.

Issued Attestation

On valid request, return:

{
  "header": { "typ": "HAP-attestation", "alg": "EdDSA" },
  "payload": {
    "attestation_id": "uuid",
    "version": "0.4",
    "profile_id": "charge@0.4",
    "bounds_hash": "sha256:...",
    "context_hash": "sha256:...",
    "execution_context_hash": "sha256:...",
    "resolved_domains": [
      {
        "domain": "finance",
        "did": "did:key:..."
      }
    ],
    "gate_content_hashes": {
      "intent": "sha256:..."
    },
    "commitment_mode": "automatic",
    "issued_at": 1735888000,
    "expires_at": 1735974400
  },
  "signature": "base64url..."
}

Attestation properties:

The SP also stores per-attestation metadata that is not part of the signed payload:


Receipt Issuance

v0.4 introduces execution receipts. Every authorized action produces exactly one signed receipt before it executes.

Receipt Request Schema

The Gatekeeper sends the following to the SP for every execution attempt:

{
  "boundsHash": "sha256:...",
  "profileId": "charge@0.4",
  "action": "create_payment_link",
  "actionType": "charge",
  "executionContext": {
    "amount": 5,
    "currency": "EUR"
  }
}
FieldDescription
boundsHashThe bounds_hash of the attestation being exercised. This is the sole lookup key for receipt requests.
profileIdThe profile this attestation is bound to
actionThe downstream tool/action name (audit metadata only)
actionTypeThe bounds-level action category — drives cumulative state partitioning and bounds dispatch
executionContextSpecific values for this call, including the fields referenced by boundType.of for per-transaction and cumulative_sum bounds

The SP derives userId from the authenticated request context. It is not supplied in the request body.

SP Validation for Receipt Issuance

The SP MUST reject the receipt request if:

Bounds Checking

For every field in the profile’s boundsSchema.fields, the SP looks up the field’s declared boundType and dispatches on boundType.kind:

boundType.kindCheck
per_transactionexecution[boundType.of] MUST be ≤ the bound value
cumulative_sumrunning_sum(boundType.of, boundType.window) + execution[boundType.of] MUST be ≤ the bound value
cumulative_countrunning_count(boundType.window) + 1 MUST be ≤ the bound value
enumCapability flag — the stored bound value MUST be in boundType.values. Enforced at attest time (the SP rejects bounds whose values are not in the allowed set).

The SP MUST NOT attempt to enforce context constraints (enum/subset on currency, allowed_recipients, etc.). Context is hashed; the SP only sees the hash. The Gatekeeper is the sole enforcer of context constraints and MUST check them before requesting a receipt.

Cumulative State Tracking

The SP MUST maintain cumulative state per (cumulative group, profile, actionType). The key is:

key: {cumGroupId}:{profileId}:{actionType}
value: {
  daily_amount: <number>,
  daily_count: <number>,
  monthly_amount: <number>,
  monthly_count: <number>,
  daily_reset: "YYYY-MM-DD",
  monthly_reset: "YYYY-MM"
}

cumGroupId is defined as:

cumGroupId = groupId || "personal:" + userId

In group mode, cumGroupId is the group ID. In personal mode (no group), cumGroupId is the string personal:{userId}. This keeps personal and group accounting in the same keyspace while guaranteeing they cannot collide.

Important: the key uses actionType (the semantic category — charge, write, post, delete), not action (the downstream tool name). Two different tools that share the same actionType under the same profile share a bucket; this is the intended behavior because an authorization scoped to “charges up to €500/day” should cap the total across all charge-producing tools, not give each tool its own allowance.

Cumulative state resets at calendar boundaries (daily and monthly). The SP is authoritative for cumulative state.

Worked Example: Multi-Group + Personal

A single user (Alice) is a member of two groups (acme-corp as finance, widgets-inc as operations) and also has a personal workspace. She has three separate charge@0.4 authorizations — one per context. Her cumulative state has three independent buckets:

BucketcumGroupIdprofileIdactionTypeSemantics
Bucket 1acme-corpcharge@0.4chargeAcme’s finance spend against Acme limits
Bucket 2widgets-inccharge@0.4chargeWidgets’ operations spend against Widgets limits
Bucket 3personal:alice-123charge@0.4chargeAlice’s personal spend against her own limits

When Alice makes a charge via her Acme authorization, only Bucket 1’s counters move. Her Widgets and personal buckets are unaffected. This keeps accounting auditable per-attestation, per-group, and prevents cross-contamination.

action vs actionType — normative rule

The receipt request carries both action (the downstream tool name, e.g., create_payment_link) and actionType (the semantic category, e.g., charge). The SP MUST:

  1. Partition cumulative state by actionType, not by action.
  2. Dispatch bounds enforcement by looking up boundType entries in the profile’s boundsSchema.fields — the boundType.kind and any boundType.of/boundType.window fields determine how each bound is checked. actionType is not a dispatch key; it is a cumulative-bucket key.
  3. Record action in the receipt for audit purposes. action MUST NOT affect cumulative state partitioning or bounds dispatch.

Issued Receipt

On valid request, return a signed receipt:

{
  "id": "uuid",
  "groupId": "acme-corp",
  "userId": "user-123",
  "boundsHash": "sha256:...",
  "profileId": "charge@0.4",
  "action": "create_payment_link",
  "actionType": "charge",
  "executionContext": { "amount": 5, "currency": "EUR" },
  "cumulativeState": {
    "daily":   { "amount": 45,  "count": 8 },
    "monthly": { "amount": 320, "count": 47 }
  },
  "limits": {
    "amount_max": 100,
    "amount_daily_max": 500,
    "amount_monthly_max": 5000,
    "transaction_count_daily_max": 20
  },
  "timestamp": 1735888050,
  "signature": "base64url..."
}

The limits object is a snapshot of the effective numeric bounds in force at receipt issuance time: the lesser of (the attestation’s bounds, the group ceiling if configured). It is included in the receipt so auditors can reconstruct exactly which limits were being enforced without having to re-fetch the attestation and the group configuration.

The signature is over the canonical JSON serialization of the receipt body (excluding the signature field itself). See “Receipt Canonicalization” in protocol.md for the canonical serialization rules.

Error Response

{
  "approved": false,
  "errors": [
    {
      "code": "CUMULATIVE_LIMIT_EXCEEDED",
      "field": "amount_daily",
      "message": "Daily spend would be 95, exceeding limit of 80",
      "limit": 80,
      "current": 40,
      "requested": 55
    }
  ]
}

Receipt Error Codes

Error codes are canonical across protocol.md, service.md, and gatekeeper.md. See the Error Codes section in protocol.md for the single authoritative list. Codes that may be returned from a receipt request include:

CodeMeaning
ATTESTATION_NOT_FOUNDUnknown boundsHash
ATTESTATION_EXPIREDAttestation TTL has elapsed
ATTESTATION_REVOKEDAttestation has been revoked
BOUND_EXCEEDEDPer-transaction bound violated
CUMULATIVE_LIMIT_EXCEEDEDCumulative limit would be exceeded
PROFILE_NOT_FOUNDReferenced profile unknown
PROPOSAL_REQUIREDAttestation is in review mode and no matching proposalId was supplied
PROPOSAL_NOT_APPROVEDThe named proposal has not been committed yet
PROPOSAL_REJECTEDThe named proposal was rejected
PROPOSAL_MISMATCHThe receipt request does not match the stored proposal (tool, args, or context differ)
PROPOSAL_ALREADY_EXECUTEDA receipt has already been issued for this proposal

Revocation

The SP maintains a revocation list. Revocation can be initiated at any time after attestation, before TTL expiry.

Revocation Operations

The SP MUST support:

Revocation Authorization

By default, the SP MUST allow revocation by:

The SP MAY define additional revocation policies (time windows, multi-party revocation, etc.).

Revocation and Audit

Revocation does not invalidate the attestation cryptographically. The attestation’s signature remains valid for audit purposes. Revocation only affects the SP’s willingness to issue new receipts. Existing receipts (issued before revocation) remain valid proof of past executions.


Group Configuration

In v0.4, organizational governance lives on the SP, configured per group.

Profile Domains

For each group, the admin defines which profiles are enabled and which domains must attest:

{
  "group": "acme-corp",
  "profileDomains": {
    "charge@0.4": ["finance"],
    "purchase@0.4": ["finance", "compliance"],
    "publish@0.4": ["marketing", "legal"]
  }
}

The profile defines what bounds exist. The group config defines who must attest.

Normative rules:

  1. A profile with no domain configuration is not available to that group.
  2. The group admin MUST assign at least one domain to each profile they enable.
  3. Personal users (no group) bypass profileDomains entirely.

Group Limit Ceilings

Optionally, the group admin MAY define ceiling limits that constrain what humans in the group can authorize:

{
  "group": "acme-corp",
  "limits": {
    "charge@0.4": {
      "amount_max_ceiling": 1000,
      "amount_daily_max_ceiling": 5000
    }
  }
}

If a user attempts to attest with bounds that exceed a group ceiling, the SP MUST reject the attestation request.

Group ceilings constrain what humans can authorize — they do not override human decisions within those constraints.


Personal Mode vs Group Mode

Personal Mode

A user with no group is in personal mode. Characteristics:

Group Mode

A user attesting through a group operates in group mode. Characteristics:

The same SP runs both modes. The same profile schemas apply to both modes. The difference is purely in the SP’s enforcement of group governance.


Retention

The SP MUST retain attestations and receipts for at least the profile-defined retention_minimum. Records MUST be:

Storage mechanism is implementation-specific (the reference implementation uses Redis with optional persistent backups).

Receipts Outlive Attestations

Receipts remain cryptographically valid and retrievable after their parent attestation has expired or been revoked. The attestation’s TTL and revocation status affect only the SP’s willingness to issue new receipts against that attestation — they do not affect previously-issued receipts.

Concretely:

The receipt is a permanent record of what happened under a specific authorization at a specific time. Expiring or revoking the authorization does not erase that history.


Public Key Publication

SP identity = its public key (e.g., did:key:z6Mk...).

Applications and gateways whitelist SP keys they trust. There is no global trust anchor.

The SP MUST publish:


SP Workflow in Practice

Human (in gateway UI)
  | (defines bounds, writes intent, picks commitment mode, signs)
  v
Gateway (Local App)
  | (sends attestation request to SP)
  v
Service Provider
  | - validates profile compliance
  | - verifies identity and (in group mode) domain authority
  | - validates bounds against group limits
  | - signs and returns attestation
  v
Gateway
  | (caches attestation, stores context content locally)
  v
Agent calls tool
  v
Gateway
  | - verifies attestation locally (signature, TTL, hashes)
  | - requests execution receipt from SP
  v
Service Provider
  | - validates attestation is current and not revoked
  | - checks per-transaction bounds and cumulative limits
  | - records execution and signs receipt
  v
Gateway
  | (stores receipt locally, executes the tool call)
  v
Tool runs

What SPs Are NOT

MisconceptionReality
Ethics enforcerSPs validate structure and bounds — not morality or legality
Global authorityNo SP can block others. No hierarchy exists
Content inspectorSPs never see semantic content (intent, context content, problem narratives)
Stateless oraclev0.4 SPs maintain cumulative state and a revocation list. They are stateful by design.

Security Guarantees

Fraud Prevention

Privacy by Construction

SPs receive only:

SPs never receive:

Profile Isolation

A compromised personal SP cannot issue attestations for profiles it doesn’t support. Each Profile defines its own validation rules.

No Executor Trust

Executors are not required to “do the right thing.” If an executor ignores the receipt requirement, it acts outside HAP — and is liable.


Implementation Checklist


Example: Multi-Domain Attestation

Profile: purchase@0.4 (group acme-corp requires finance + compliance)

  1. The user starts an authorization for purchase@0.4 in the gateway.
  2. The gateway sends the attestation request to the SP. The SP checks profileDomains and sees purchase@0.4 requires both finance and compliance.
  3. The Finance Manager attests via SP — SP verifies their identity and domain authority, then signs an attestation with resolved_domains: [{domain: "finance", did: ...}].
  4. The Compliance Officer attests via SP — SP signs a second attestation with resolved_domains: [{domain: "compliance", did: ...}].
  5. Both attestations share the same bounds_hash, context_hash, and gate_content_hashes.intent.
  6. The Gatekeeper requests a receipt from the SP. The SP validates that the union of attested domains covers the required set, then issues the receipt.
  7. The agent executes the tool call.

If any required domain attestation is missing → the SP refuses to issue receipts → the agent cannot act.


Summary

Service Providers in HAP v0.4:

HAP’s power isn’t in its providers — it’s in its proof. Run your own SP. Trust your own keys. Own your direction.

No receipt, no execution.

Next: Gatekeeper →