Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

JACS: JSON Agent Communication Standard

JACS is an open source provenance layer for agent systems. Use it when an output, tool call, file, image, email, or agent handoff crosses a trust boundary and logs alone are not enough.

Start With The Boundary

Most deployments start in one of four places:

  • Core signing: sign JSON, files, Markdown/text, images, or Rust email payloads directly.
  • MCP: run jacs mcp as a local tool server, or add signing around an existing MCP transport.
  • Frameworks: add provenance at LangChain, LangGraph, FastAPI, Express, Koa, or Vercel AI SDK boundaries.
  • A2A and agreements: exchange signed artifacts or require multiple agents to sign off.

What JACS Gives You

  • Persistent agent identity with encrypted private keys
  • Tamper-evident signed JSON and file envelopes
  • Inline Markdown/text signatures that stay readable in place
  • Embedded PNG/JPEG/WebP signatures for media provenance
  • Rust email signing and verification with field-level content hashes
  • Trust policies and local trust-store workflows
  • Cross-language verification across Rust, Python, Node.js, and Go

For platform workflows around verified documents, agent behavior, benchmarks, and hosted JACS identity flows, see HumanAssisted/haiai.

Best Entry Points

  1. Which Integration?
  2. Quick Start
  3. Use Cases
  4. MCP Overview
  5. Inline Text Signatures
  6. Image and Media Signatures
  7. Email Signing and Verification

Install

Rust CLI and MCP server

cargo install jacs-cli
jacs quickstart --name my-agent --domain my-agent.example.com
jacs mcp

Python

pip install jacs

Node.js

Registry status (observed 2026-07-11): npm serves @hai.ai/jacs@0.10.1, while these pages describe source 0.11.4. The registry package does not expose every method shown here. Pin the version and inspect its exports; use a source build only when that is an explicit deployment choice.

npm install @hai.ai/jacs

Go

go get alone cannot link the CGo wrapper, and no matching native-library release existed at the 2026-07-09 baseline. Build the full repository by following Go Installation and Quick Start.

What This Book Does Not Claim

  • MCP and A2A are different boundaries: MCP is model-to-tool; A2A is agent-to-agent discovery and exchange.
  • JACS does not require a registry, blockchain, or central server.
  • Email signing is currently documented as a Rust core API, not a CLI or Python/Node/Go binding surface.

Community

What is JACS?

JACS is a cryptographic provenance layer for AI agents and the artifacts they produce. It gives an agent a persistent signing identity, wraps important outputs in tamper-evident signatures, and lets other systems verify origin and integrity later.

Use JACS when data crosses a trust boundary: another service, another agent, another organization, a user-facing audit trail, or a file that must remain verifiable after it leaves the process that created it.

The Problem

Agent systems increasingly produce artifacts that other systems act on:

  • JSON tool results, reports, memories, and configs
  • Markdown plans, design docs, and release notes
  • Images, screenshots, charts, and generated media
  • Email messages and attachments
  • A2A artifacts, MCP tool calls, and multi-agent approvals

Logs can say what happened, but they are usually controlled by the same system that produced the data. JACS attaches proof to the artifact itself.

Core Ideas

ConceptMeaning
AgentA named identity with signing keys and metadata.
Signed documentA JSON envelope with payload, hash, signer metadata, and signature.
Artifact signatureA JACS signature attached to non-JSON content, such as text, images, or email.
Trust storeLocal public keys and trust decisions used during verification.
AgreementA signed multi-party approval with quorum, timeout, and algorithm constraints.

What Verification Proves

Verification answers two practical questions:

  1. Did the signer with this public key sign these canonical bytes?
  2. Has the signed content changed since it was signed?

Verification does not prove first creation, copyright ownership, human authorship, or that a real-world statement is true. It gives you cryptographic accountability for the artifact.

Where JACS Fits

flowchart LR
    A["Agent or service"] --> B["Create artifact"]
    B --> C["Sign with JACS"]
    C --> D["Share or store"]
    D --> E["Verify before trust"]

JACS does not replace your application protocol. It works with CLI jobs, Rust services, Python and Node frameworks, MCP tools, A2A exchange, and file-based workflows.

When To Use It

  • A downstream system should verify who produced an artifact.
  • Multiple agents or teams need an auditable handoff.
  • A document, image, or email must stay verifiable outside your database.
  • You need open source signing primitives without a mandatory central registry.

When Not To Use It

  • Everything stays inside one trusted process and logs are enough.
  • You only need accidental-corruption detection.
  • There is no signer identity or audit requirement.

Next Steps

Which JACS Path Should I Use?

Choose the smallest supported integration that matches the boundary you need to secure.

Start Here

If you need...Start hereWhy
Sign and verify JSON, files, or one-off artifactsQuick StartEstablish the agent identity and trust model first.
A ready-made local MCP server for Claude, Codex, or another MCP clientMCP Overview and jacs mcpFastest full server path; stdio only, no HTTP port.
Signed tool outputs inside LangChain / LangGraph on PythonPython Framework AdaptersAdd provenance without changing the rest of the app.
Signed tool outputs inside LangChain.js / LangGraph on NodeNode.js LangChain.jsSame pattern for TypeScript.
Secure existing MCP server/client codePython MCP or Node.js MCPWrap the transport or register JACS tools.
Sign Markdown or text in placeInline Text SignaturesSignature stays with the readable file; supports multi-signer review.
Attach provenance to PNG, JPEG, or WebP imagesImage and Media SignaturesSignature is embedded in the image; no sidecar JSON.
Sign and verify raw emailEmail Signing and VerificationRust API for .eml messages with field-level content hashes.
Cross-organization agent discovery and artifact exchangeA2A InteroperabilityUse A2A for agent-to-agent boundaries.
Multi-party approval or quorum workflowsMulti-Agent AgreementsAgreements are the right primitive for sign-off.

When You Probably Do Not Need JACS

  • Everything stays inside one service you control and logs are enough.
  • You only need integrity, not signer identity or third-party verification.
  • A plain checksum or database audit log already satisfies the requirement.
  1. Prototype with quickstart and simple sign/verify calls.
  2. Attach provenance at the boundary that already exists: file, API response, framework tool, MCP call, image, email, or A2A artifact.
  3. Add trust policy when other agents, services, or organizations enter the workflow.
  4. Add agreements, DNS, or attestations only when the deployment needs them.

Start with the boundary you need to secure now.

Use Cases

These are current JACS use cases, not speculative integrations.

1. Keep JSON and files verifiable

Use JACS when a report, memory, config, policy, or audit artifact needs to stay verifiable after it leaves the process that created it.

Start with:

What JACS adds:

  • Self-contained signed envelopes
  • Re-verification at read time
  • Cross-language compatibility across Rust, Python, Node.js, and Go

2. Sign Markdown in place for review

Use JACS when a README, design doc, policy, or release note needs visible content and verifiable sign-off in the same file.

Start with:

JACS_CONFIG=./reviewer-a.config.json jacs sign-text DESIGN.md
JACS_CONFIG=./reviewer-b.config.json jacs sign-text DESIGN.md
jacs verify-text DESIGN.md

What JACS adds:

  • A readable signature block appended at the end
  • Multi-signer review without sidecar JSON
  • Permissive or strict verification modes

3. Embed provenance in images

Use JACS when a photo, chart, screenshot, or AI-generated image needs verifiable signer identity without a separate metadata file.

Start with:

jacs sign-image photo.png --out signed.png
jacs verify-image signed.png
jacs extract-media-signature signed.png

What JACS adds:

  • Embedded PNG iTXt, JPEG APP11, or WebP XMP signatures
  • Pixel-content integrity checks
  • Optional robust mode for PNG/JPEG metadata-strip threat models

4. Sign email with Rust

Use JACS when raw RFC 5322 email needs a verifiable signature attachment and field-level content verification.

Start with:

What JACS adds:

  • jacs-signature.json MIME attachment
  • Header, body, and attachment hashes
  • Forwarding chains through parent signature hashes

Email signing is currently documented as a Rust core API via jacs::email, not as a CLI or Python/Node/Go binding surface.

5. Secure a local MCP tool server

Use JACS when an MCP client needs local document signing or supplied-key integrity checks.

Start with:

For keyless verification, install the CLI and start the explicit verification profile:

cargo install jacs-cli
jacs mcp --profile verify-only

For document signing, configure a password source before creating an identity, or use an existing signing config. Then start the explicit signing profile:

jacs quickstart --name mcp-agent --domain mcp-agent.example
jacs mcp --profile local-sign --config ./jacs.config.json

To also enable scoped text/image tools, prefix the final command with JACS_MCP_BASE_DIR=/path/to/content. Each server command runs until the MCP client closes it; verification and signing are alternative startup choices.

What JACS adds:

  • A ready-made stdio MCP server
  • Explicit-key document verification by default; JSON/Agreement tools with local-sign
  • Five text/image tools only with local-sign, a signing config and JACS_MCP_BASE_DIR
  • Local private-key custody; no HTTP listener

Signatures establish key-backed provenance, not per-action human approval, truth or authority. Loading a config alone does not grant signing. Trust/admin capabilities remain unavailable through these profiles; inspect tools/list for the running process's actual inventory.

6. Add provenance to framework boundaries

Use JACS when an existing app already has a boundary such as LangChain tool output, FastAPI response, Express middleware, or Koa middleware.

Start with:

What JACS adds:

  • Signed tool results and responses
  • Optional strict verification at adapter boundaries
  • Minimal changes to existing framework code

7. Exchange artifacts across agents or organizations

Use JACS when one agent produces work that another agent, service, or organization must verify before acting on it.

Start with:

What JACS adds:

  • Signed A2A artifacts
  • Trust policies for admission control
  • Agreement-v2 inspection of mathematical signature coverage; M-of-N, role, status and algorithm policy claims require separate application evaluation

Agreement-v2 math is not policy acceptance: its native valid and policyAccepted stay false. Quorum counts, human/notary labels and final status cannot establish human approval or authority. See the Agreement-v2 guide for the inspection/report distinction.

8. Publish identity without a central auth service

Use JACS when external systems need to verify an agent identity without putting a shared server in the middle.

Start with:

What JACS adds:

  • Public key fingerprint anchoring
  • Local trust-store workflows
  • Local private-key custody

9. Add hosted verification with HAI.AI

Use HumanAssisted/haiai when you want platform workflows for verified documents, agent behavior, benchmarks, and hosted JACS identity flows.

JACS remains the local open source signing and verification layer. HAI.AI is the hosted platform path around those identities and artifacts.

Core Concepts

JACS is a portable signing and verification system for JSON documents. Its main primitive is a verifiable JSON envelope: canonical bytes, schema-declared structure, content hash, signer identity, signing algorithm, and cryptographic signature.

Agents

An Agent is an identity that can create, sign, and verify documents.

{
  "jacsId": "550e8400-e29b-41d4-a716-446655440000",
  "jacsVersion": "123e4567-e89b-12d3-a456-426614174000",
  "jacsType": "agent",
  "jacsAgentType": "ai",
  "jacsAgentDomain": "agent.example.com"
}

Agents have stable IDs, versioned identity documents, and cryptographic keys. A2A capabilities are represented through A2A Agent Cards.

Documents

A Document is any JSON object signed with the JACS header/signature wrapper.

{
  "$schema": "https://hai.ai/schemas/header/v1/header.schema.json",
  "jacsId": "doc-uuid-here",
  "jacsVersion": "version-uuid-here",
  "jacsType": "document",
  "jacsVersionDate": "2024-01-15T10:30:00Z",
  "jacsLevel": "artifact",
  "content": {
    "title": "Analyze Q4 Sales Data",
    "description": "Generate insights from sales data"
  },
  "jacsSha256": "hash-of-document-content",
  "jacsSignature": {
    "agentID": "agent-uuid",
    "agentVersion": "agent-version-uuid",
    "signature": "base64-signature",
    "signingAlgorithm": "ring-Ed25519",
    "publicKeyHash": "hash-of-public-key",
    "date": "2024-01-15T10:30:00Z",
    "fields": ["$schema", "jacsId", "jacsVersion", "jacsType", "content"]
  }
}

Application-specific meaning belongs inside the payload or in a custom schema that extends the header.

Agreements

Agreements collect multiple signatures over the same document payload.

{
  "jacsAgreement": {
    "agentIDs": ["agent-1-uuid", "agent-2-uuid"],
    "question": "Do you approve this change?",
    "context": "Deployment approval",
    "quorum": 2,
    "signatures": []
  },
  "jacsAgreementHash": "hash-of-agreement-content"
}

An agreement is complete when the required signatures and quorum rules are satisfied.

Verification

When consuming signed documents, verify in one of two ways:

  • With a loaded agent: call verify(signedDocument) using the configured trust store and key resolution.
  • Without loading an agent: call verify_standalone(signedDocument, options) with explicit key-resolution options.

Cryptographic Security

JACS signs canonical JSON bytes. Verification checks the content hash, signature fields, signer identity, algorithm, and public key.

Supported algorithms:

AlgorithmDescription
ring-Ed25519Fast elliptic curve signatures
pq2025Post-quantum ML-DSA-87 signatures

Quick Start Guide

Get a persistent agent identity, sign data, and verify it.

Let your agent sign documents with MCP

The unified jacs binary includes a stdio-only MCP server. After installing it and creating an identity with the CLI setup, or selecting an existing signed config, explicitly enable local signing:

jacs mcp --profile local-sign --config /absolute/path/to/jacs.config.json

Configure your MCP client with the same arguments:

{
  "mcpServers": {
    "jacs": {
      "command": "jacs",
      "args": ["mcp", "--profile", "local-sign", "--config", "/absolute/path/to/jacs.config.json"]
    }
  }
}

Local signing uses the selected config's persistent encrypted keys and its keychain or password source. It grants agent signing authority; a signature does not establish a person's approval of the content. Key and trust administration are not enabled by this profile.

To verify documents without creating an identity or unlocking private keys, use the default verification-only configuration instead:

{
  "mcpServers": {
    "jacs": {
      "command": "jacs",
      "args": ["mcp"]
    }
  }
}

The default verify-only profile stays verification-only when a config or password is available; selecting local-sign is explicit. Set JACS_MCP_BASE_DIR only when you intend to add scoped text/image file tools to local-sign; no content directory is granted implicitly. See the canonical MCP guide for file scope, overwrite behavior and the actual tool inventory.

CLI

Install the unified CLI/MCP binary:

cargo install jacs-cli

Create or load an agent, then sign and verify a JSON file:

export JACS_PRIVATE_KEY_PASSWORD='use-a-strong-password'

jacs quickstart --name my-agent --domain my-agent.example.com
jacs document create -f mydata.json --output signed-document.json
jacs verify jacs_data/signed-document.json

Homebrew is also supported:

brew tap HumanAssisted/homebrew-jacs
brew install jacs

Python

pip install jacs
import jacs.simple as jacs

info = jacs.quickstart(name="my-agent", domain="my-agent.example.com")
signed = jacs.sign_message({"action": "approve", "amount": 100})
result = jacs.verify(signed.raw)
print(f"Valid: {result.valid}, Signer: {result.signer_id}")

Node.js

Registry status (observed 2026-07-11): npm serves @hai.ai/jacs@0.10.1, while these pages describe source 0.11.4. The registry package does not expose every method shown here. Pin the version and inspect its exports; use a source build only when that is an explicit deployment choice.

npm install @hai.ai/jacs
const jacs = require('@hai.ai/jacs/simple');

await jacs.quickstart({
  name: 'my-agent',
  domain: 'my-agent.example.com',
});

const signed = await jacs.signMessage({ action: 'approve', amount: 100 });
const result = await jacs.verify(signed.raw);
console.log(`Valid: ${result.valid}, Signer: ${result.signerId}`);

Sign Markdown or text

jacs sign-text README.md
jacs verify-text README.md
jacs verify-text --strict README.md

sign-text appends a -----BEGIN JACS SIGNATURE----- block at the end of the file. The original content remains readable and can be counter-signed by another agent.

See Inline Text Signatures for multi-signer flows, strict mode, and --key-dir.

Sign images

jacs sign-image photo.png --out signed.png
jacs verify-image signed.png
jacs extract-media-signature signed.png

JACS embeds the signature in PNG, JPEG, or WebP metadata. See Image and Media Signatures for overwrite policy, robust mode, and verification details.

Password bootstrap

The Rust CLI needs a password source before it can sign:

# CI/server
export JACS_PRIVATE_KEY_PASSWORD='use-a-strong-password'

# Developer workstation
jacs keychain set --agent-id <YOUR_AGENT_UUID>

# File-based secret
export JACS_PASSWORD_FILE=/secure/path/jacs-password.txt

If both JACS_PRIVATE_KEY_PASSWORD and JACS_PASSWORD_FILE are set, the CLI fails fast to avoid ambiguity. The OS keychain is only consulted when neither environment source is set.

Python and Node quickstart can auto-generate a secure password if JACS_PRIVATE_KEY_PASSWORD is unset. In production, set JACS_PRIVATE_KEY_PASSWORD explicitly.

Algorithm

The default signing algorithm is pq2025 (ML-DSA-87 / FIPS-204). Use the user-facing ed25519 choice if you need a smaller classical signature. JACS stores and emits the canonical Ed25519 wire label as ring-Ed25519:

jacs quickstart --name my-agent --domain my-agent.example.com --algorithm ed25519

Next steps

Multi-Agent Agreements

Agreement v2 records terms and agent consent signatures in a standalone jacsType: "agreement" document. Its verifier inspects mathematical and structural checks. It does not accept the full authorization policy or prove human approval, even when the supplied quorum produces a final status.

Use this walkthrough to inspect agent provenance. An application must separately establish the authority and any exact-action human approval required before acting.

The Lifecycle

Create agreement -> append transcript refs -> collect signer/notary signatures -> inspect math and policy scope

Python

All three demo identities are controlled by this code, including the agent labeled notary; no person or external notary approves anything here.

The three agents are persistent and share one data_directory so each can resolve the others' public keys when verifying. Independent ephemeral() agents keep keys in memory only and cannot verify each other's signatures.

import os
import secrets
import tempfile
from pathlib import Path

from jacs import SimpleAgent

os.environ["JACS_KEYCHAIN_BACKEND"] = "disabled"  # throwaway demo only
PASSWORD = secrets.token_urlsafe(32)
workspace = Path(tempfile.mkdtemp(prefix="jacs_multi_agent_"))
shared_data = workspace / "shared_data"  # all agents share this so keys resolve


def make_agent(name, agent_type="ai"):
    agent, info = SimpleAgent.create_agent(
        name=name,
        password=PASSWORD,
        algorithm="ring-Ed25519",
        data_directory=str(shared_data),
        key_directory=str(workspace / f"{name}_keys"),
        config_path=str(workspace / f"{name}.config.json"),
        agent_type=agent_type,
    )
    return agent, info


agent_a, a = make_agent("agent-a")
agent_b, b = make_agent("agent-b")
notary, n = make_agent("demo-notary")

agreement = agent_a.create_agreement_v2({
    "title": "Bounded refund authorization",
    "description": "Demo refund terms for agent signature inspection; no refund is authorized.",
    "terms": "Agent B may issue a refund up to $25 for order 123 after Agent A approval.",
    "termsFormat": "text/markdown",
    "status": "proposed",
    "parties": [
        {"agentId": a["agent_id"], "agentType": "ai", "role": "signer"},
        {"agentId": b["agent_id"], "agentType": "ai", "role": "signer"},
        {"agentId": n["agent_id"], "agentType": "ai", "role": "notary"},
    ],
    "signaturePolicy": {
        "partyQuorum": "all",
        "witnessRequired": 0,
        "notaryRequired": 1,
        "minimumStrength": "classical",
    },
    "controllers": [a["agent_id"], b["agent_id"], n["agent_id"]],
})

agreement = agent_a.sign_agreement_v2(agreement, "signer")
agreement = agent_b.sign_agreement_v2(agreement, "signer")
agreement = notary.sign_agreement_v2(agreement, "notary")

# The demo notary resolves the signers' public keys from shared storage.
report = notary.verify_agreement_v2(agreement)
assert report["mathematicalChecksValid"] is True
assert report["valid"] is False
assert report["policyAccepted"] is False
assert report["overallScope"] == "consent_signatures_only"
assert report["expectedStatus"] == "final"  # structural diagnostic only

This abbreviated snippet leaves its temporary workspace for inspection; remove it when finished. The runnable version includes transcript references and adversarial checks, and cleans up its temporary keys:

python examples/agreement_v2_three_party.py

What the Report Means

  • mathematicalChecksValid covers the native hash, signature and structural checks. Inspect errors for failures and notes, verifiedChainDepth and chainFullyVerified for lineage coverage limits.
  • Consent signatures bind the agreement identity and consent hash; signatures made with a nonempty transcript also bind its prefix at signing. Later unsigned transcript entries are not retroactively covered.
  • Signer counts, quorum, witness/notary requirements and expectedStatus are diagnostics against the supplied document. Neither a count nor status: "final" is an accepted policy verdict.
  • Helpers check membership, role and controller lists. These checks do not establish portable role/controller authority or a person's approval. Changing an agent's label to human does not add human approval evidence.
  • valid and policyAccepted remain false, including for the successful mathematical inspection above. Do not turn either field into true or treat mathematicalChecksValid as permission to act.

Legacy Sidecar Agreements

The older create_agreement() / sign_agreement() / check_agreement() API still exists for adding jacsAgreement metadata to an arbitrary signed document. It supports signature inspection of an existing payload; those signatures alone do not establish human approval or the complete application policy.

V2 adds standalone terms, transcript-prefix evidence and branch helpers. Moving to v2 does not create an actionable authorization.

Next Steps

Verifying Signed Documents

Verify a JACS-signed document in under 2 minutes. Verification confirms two things: the document was signed by the claimed agent, and the content has not been modified since signing.

Verification does NOT require creating an agent. You only need the signed document (and optionally access to the signer's public key).

Raw JSON is decoded strictly before signing, canonicalization, or verification. Duplicate object names are rejected at every nesting level, including names that become equal after JSON escape decoding (for example, agentID and agent\u0049D). This prevents two consumers from interpreting the same signed bytes with different first-key/last-key rules. Repeated values in arrays are ordinary JSON and remain supported.

Strict vs permissive verification

verify-text and verify-image use a permissive default: a missing signature is a typed status, not an error. Strict mode opts in to error-on-missing. The choice maps onto every binding the same way.

SurfacePermissive (default)Strict (--strict / strict=True / { strict: true } / Strict: true)
jacs verify-text (CLI)Exit 0 valid · 2 missing-sig · 1 invalidExit 0 valid · 1 missing OR invalid (stderr: no JACS signature found)
jacs verify-image (CLI)Same as aboveSame as above
Python (jacs.verify_text, jacs.verify_image)Returns result with status == "missing_signature"Raises MissingSignatureError
Node (jacs.verifyText, jacs.verifyImage)Returns result with status === "missing_signature"Promise rejects with MissingSignature-bearing message
Go (jacs.VerifyText, jacs.VerifyImage)Returns Status == "missing_signature"Returns error matching errors.Is(err, jacs.ErrMissingSignature)
Rust (jacs::text, jacs::media)Ok(Status::MissingSignature)Err(ErrorKind::MissingSignature)

Pre-existing verify surfaces (jacs verify, jacs document verify, jacs.verify) are unaffected — they keep their existing exit codes and return shapes.

For hands-on examples see Inline Text Signatures and Image and Media Signatures.

CLI: jacs verify

The fastest way to verify a document from the command line. No config file, no agent setup.

# Verify a local file
jacs verify signed-document.json

# Verify with JSON output (for scripting)
jacs verify signed-document.json --json

# Verify a remote document by URL
jacs verify --remote https://example.com/signed-doc.json

# Specify a directory containing public keys
jacs verify signed-document.json --key-dir ./trusted-keys/

Output on success:

Status:    VALID
Signer:    550e8400-e29b-41d4-a716-446655440000
Signed at: 2026-02-10T12:00:00Z

JSON output (--json):

{
  "valid": true,
  "signerId": "550e8400-e29b-41d4-a716-446655440000",
  "timestamp": "2026-02-10T12:00:00Z"
}

The exit code is 0 for valid, 1 for invalid or error. Use this in CI/CD pipelines:

if jacs verify artifact.json --json; then
  echo "Artifact is authentic"
else
  echo "Verification failed" >&2
  exit 1
fi

If a jacs.config.json and agent keys exist in the current directory, the CLI uses them automatically. Otherwise, it creates a temporary ephemeral verifier internally.

Python

With an agent loaded

import jacs.simple as jacs

jacs.load("./jacs.config.json")

result = jacs.verify(signed_json)
if result.valid:
    print(f"Signed by: {result.signer_id}")
else:
    print(f"Errors: {result.errors}")

Without an agent (standalone)

import jacs.simple as jacs

result = jacs.verify_standalone(
    signed_json,
    key_resolution="local",
    key_directory="./trusted-keys/"
)
print(f"Valid: {result.valid}, Signer: {result.signer_id}")

verify_standalone does not use a global agent. Pass the key resolution strategy and directories explicitly.

Verify by document ID

If the document is in local storage and you know its ID:

result = jacs.verify_by_id("550e8400-e29b-41d4:1")

Node.js

With an agent loaded

import * as jacs from '@hai.ai/jacs/simple';

await jacs.load('./jacs.config.json');

const result = await jacs.verify(signedJson);
console.log(`Valid: ${result.valid}, Signer: ${result.signerId}`);

Without an agent (standalone)

import { verifyStandalone } from '@hai.ai/jacs/simple';

const result = verifyStandalone(signedJson, {
  keyResolution: 'local',
  keyDirectory: './trusted-keys/',
});
console.log(`Valid: ${result.valid}, Signer: ${result.signerId}`);

Verify by document ID

const result = await jacs.verifyById('550e8400-e29b-41d4:1');

DNS Verification

DNS verification checks that an agent's public key hash matches a DNS TXT record published at _v1.agent.jacs.<domain>. This provides a decentralized trust anchor: anyone can look up the agent's expected key fingerprint via DNS without contacting a central server.

Publishing a DNS record

jacs agent dns --domain example.com --provider plain

This outputs the TXT record to add to your DNS zone. Provider options: plain, aws, azure, cloudflare.

Looking up an agent by domain

jacs agent lookup example.com

This fetches the agent's public key from https://example.com/.well-known/jacs-pubkey.json and checks the DNS TXT record at _v1.agent.jacs.example.com.

CLI verification with DNS

# Require DNS validation (fail if no DNS record)
jacs agent verify --require-dns

# Require strict DNSSEC validation
jacs agent verify --require-strict-dns

For full DNS setup instructions, see DNS-Based Verification and DNS Trust Anchoring.

Cross-Language Verification

JACS signatures are language-agnostic. A document signed by a Rust agent verifies identically in Python and Node.js, and vice versa. This holds for both Ed25519 and post-quantum (ML-DSA-87/pq2025) algorithms.

Legacy-v1 signatures that lack signatureContentVersion are denied by default because their signer, timestamp, nonce, and algorithm metadata was never covered by the signature. Use the migration API to re-sign them as v2. For a narrowly audited archive workflow, JACS_ALLOW_LEGACY_SIGNATURE_CONTENT=true enables payload-only compatibility; verification results deliberately leave signer, agent-version, and timestamp fields empty. Do not use that mode for authorization or identity attribution. The historical JACS_REJECT_LEGACY_SIGNATURE_CONTENT=true setting remains supported and takes precedence.

This is tested on every commit: current v2 fixtures verify by default across bindings, while committed v1 fixtures prove both default rejection and explicit payload-only compatibility. Each binding also countersigns the fixture with a different algorithm, proving round-trip interoperability.

Test sources:

  • Rust fixture generator: jacs/tests/cross_language/mod.rs
  • Python consumer: jacspy/tests/test_cross_language.py
  • Node.js consumer: jacsnpm/test/cross-language.test.js

Key Resolution Order

When verifying a document, JACS resolves the signer's public key in a configurable order. Set JACS_KEY_RESOLUTION to control this:

ValueSource
localLocal trust store (added via trust_agent)
dnsDNS TXT record lookup
haiHAI key distribution service

Default: local,hai. Example: JACS_KEY_RESOLUTION=local,dns,hai.

What Is an Attestation?

Signing says WHO. Attestation says WHO plus WHY.

A JACS attestation is a cryptographically signed document that goes beyond proving who signed something. It records why a piece of data should be trusted -- the evidence, the claims, and the reasoning behind that trust.

Signing vs. Attestation

sign_message()create_attestation()
ProvesWho signed itWho signed it + why it's trustworthy
ContainsSignature + hashSignature + hash + subject + claims + evidence
Use caseData integrityTrust decisions, audit trails, compliance
VerificationWas it tampered with?Was it tampered with? Are the claims valid? Is the evidence fresh?

Key Concepts

Subject

What is being attested. Every attestation targets a specific subject -- an artifact, agent, workflow, or identity. The subject is identified by type, ID, and cryptographic digests.

Claims

What you assert about the subject. Claims are structured statements with a name, value, optional confidence score (0.0-1.0), and assurance level (self-asserted, verified, or independently-attested).

Evidence

What supports the claims. Evidence references link to external proofs (A2A messages, email headers, JWT tokens, TLS notary sessions) with their own digests and timestamps.

Derivation Chain

How the attestation was produced. When one attestation builds on another -- for example, a review attestation that references an earlier scan attestation -- the derivation chain captures the full transformation history.

Architecture Layers

Layer 2: Adapters (A2A, email, JWT, TLSNotary)
           |
Layer 1: Attestation Engine (create, verify, lift, DSSE export)
           |
Layer 0: JACS Core (sign, verify, agreements, storage)

Attestations build on top of existing JACS signing. Every attestation is also a valid signed JACS document. You can verify an attestation with verify() for signature checks, or use verify_attestation() for the full trust evaluation.

Quick Example

from jacs.client import JacsClient

client = JacsClient.ephemeral(algorithm="ed25519")

# Sign a document (Layer 0)
signed = client.sign_message({"action": "approve", "amount": 100})

# Attest WHY it's trustworthy (Layer 1)
att = client.create_attestation(
    subject={"type": "artifact", "id": signed.document_id,
             "digests": {"sha256": "..."}},
    claims=[{"name": "reviewed", "value": True, "confidence": 0.95}],
)

# Verify the full trust chain
result = client.verify_attestation(att.raw_json, full=True)
print(f"Valid: {result['valid']}")

Attestation vs. A2A Trust Policy

Attestation (Layer C) provides trust context: claims, evidence, and derivation chains. It answers "why should this data be trusted?" A2A trust policy (Layer B) handles agent admission: "is this agent allowed to communicate?"

For transport trust decisions, see A2A Interoperability. For how attestation and A2A compose, see A2A + Attestation Composition. For the full three-layer model, see Trust Layers.

When to Use Attestations

Use attestations when you need to answer questions like:

  • Why should I trust this data? (claims + evidence)
  • Who reviewed it and when? (issuer, timestamps, assurance level)
  • How was it produced? (derivation chain)
  • Can I independently verify the trust chain? (DSSE export, evidence verification)

If you only need to prove who signed something and that it hasn't been tampered with, sign_message() is sufficient. See Sign vs. Attest for a detailed decision guide.

JACS Trust Layers

JACS organizes trust into three distinct layers. Each layer has a clear scope and its own vocabulary. Understanding which layer you need prevents confusion between identity, transport policy, and evidentiary trust.

The Three Layers

Layer A: Identity + Integrity (JACS Core)

Scope: Who signed what, and has it been tampered with?

APIs: sign_message(), verify(), verify_standalone()

This is the foundation. A valid JACS signature links the covered content to the signing key and detects changes to that content. Binding the key to an agent or person requires trusted identity evidence; the signature alone does not establish who created the content, whether its claims are true, or whether a person approved it. Layer A answers: "Is this signature valid?"

Crypto status values: Verified · SelfSigned · Unverified · Invalid

  • Verified: Signature is valid and signer's key was resolved from a trusted source.
  • SelfSigned: Signature is valid but signer is the same as verifier (no third-party trust).
  • Unverified: Signature could not be checked because the signer's key was not available.
  • Invalid: Signature check failed — the content was tampered with or the wrong key was used.

Layer B: Exchange + Discovery (A2A Integration)

Scope: Is this agent allowed to communicate with me?

APIs: sign_artifact(), verify_wrapped_artifact(), assess_remote_agent(), discover_agent()

Layer B handles cross-boundary exchange between agents using the A2A protocol. It adds trust policy on top of Layer A's cryptographic status. Layer B answers: "Should I accept artifacts from this agent?"

Policy status values: allowed · blocked · not_assessed

Trust policies (open, verified, strict) control admission:

PolicyRequirement
openAccept all agents without identity assurance
verifiedValid same-origin Agent Card JWS plus a durable ES256 TOFU pin; proves origin/key continuity, not native identity
strictExplicitly trusted native root plus a valid binding for the exact identity, version, ES256 key, scope, and binding lifecycle

See A2A Interoperability for full details.

Layer C: Trust Context (Attestation)

Scope: Why should this data be trusted?

APIs: create_attestation(), verify_attestation(), lift_to_attestation(), export_attestation_dsse()

Layer C records the reasoning behind trust: claims, evidence, derivation chains, and assurance levels. Layer C answers: "What evidence supports this data?"

Attestation status values: local_valid · full_valid

  • local_valid: Signature and hash are correct; claims are structurally valid.
  • full_valid: All of the above, plus evidence digests verified and derivation chain intact.

See What Is an Attestation? for full details.

Terminology Glossary

TermLayerMeaning
Crypto statusAOutcome of signature verification: Verified, SelfSigned, Unverified, Invalid
Policy statusBOutcome of trust policy check: allowed, blocked, not_assessed
Attestation statusCOutcome of attestation verification: local_valid, full_valid
VerifiedASignature is valid and signer key was resolved
SelfSignedASignature is valid but signer is the verifier
UnverifiedAKey not available — cannot check signature
InvalidASignature check failed
AllowedBAgent passes the configured trust policy
BlockedBAgent does not pass the trust policy
Not assessedBNo agent card provided — trust not evaluated

Quick Decision Flow

"Which layer do I need?"

  1. I just need to prove data hasn't been tampered with → Layer A. Use sign_message() and verify().
  2. I need to exchange signed data with other agents → Layer B. Use sign_artifact() and A2A discovery. See the A2A Quickstart.
  3. I need to record WHY data should be trusted → Layer C. Use create_attestation(). See the Attestation Tutorial.
  4. I need both exchange AND trust evidence → Layer B + C. See A2A + Attestation Composition.

Common Misconceptions

  • "Unverified" does not mean "Invalid." Unverified means the signer's key wasn't available. Invalid means the signature check actively failed. These have very different security implications.
  • A2A trust policy is not attestation verification. A2A policy (Layer B) answers "should I talk to this agent?" Attestation (Layer C) answers "why should I trust this data?" They compose but are not interchangeable.
  • "Trusted" is not the same as "Verified." In JACS, "trusted" refers to trust store membership (Layer B). "Verified" refers to cryptographic signature validation (Layer A).

Deployment Compatibility

JACS has several distribution surfaces, and they do not currently ship at the same version or for the same targets. This page distinguishes four different claims:

  • Source-compilable: the repository has a build path for the target. The consumer may still need Rust, a C toolchain, system headers, and network access to dependency registries.
  • Prebuilt: the public registry or GitHub release contained a native artifact for the target at the observation date.
  • CI-tested: project CI compiled or tested source on that runner. A cross-compiled binary is only a build result, not a runtime test.
  • Runtime-smoked: CI executed or imported the packaged artifact on that target. This is narrower than full feature testing.

The shipped rows below come from release/shipped-artifacts.json, observed 2026-09-10 against source 0.13.0 at 743df757. Registry contents can change; run python3 scripts/check-release-matrix.py --online or inspect the registry before choosing a deployment target.

What users could install at the review baseline

SurfaceShipped version/statusPrebuilt targetsCI/runtime evidence and limits
Rust library (jacs)crates.io 0.13.0; publishedsource only; no prebuilt library artifactFull source suites run on Ubuntu and macOS release runners; other Rust targets are not implied. The four storage crates (jacs-duckdb, jacs-redb, jacs-postgresql 0.1.20; jacs-surrealdb 0.1.21) are published against jacs 0.13.0.
CLI (jacs-cli)crates.io and GitHub Releases 0.13.0; publishedmacOS arm64; macOS x86_64; Linux x86_64 glibc; Linux arm64 glibc; Windows x86_64Release jobs build each listed target, run a launch smoke, and install the staged archive through the Python and Node CLI installers (sign/verify smoke on Unix only: the trust model refuses to create key files on Windows). cargo install remains a source build.
Python (jacs)PyPI 0.13.0; publishedmacOS arm64; macOS x86_64; Linux x86_64 manylinux_2_28; Linux arm64 manylinux_2_28; Linux x86_64 musllinuxglibc wheels are built inside the pinned manylinux_2_28 images; release candidate smokes install and sign/verify on Ubuntu x86_64 and arm64, macOS x86_64 and arm64, and Alpine x86_64. No Windows or arm64 musllinux wheel is recorded.
Node (@hai.ai/jacs)npm 0.10.1; published behind sourcemacOS arm64; macOS x86_64; Linux x86_64 glibc; Linux x86_64 musl; Linux arm64 glibcSource CI runtime-smokes Node 20 on Ubuntu x86_64. Other listed native binaries are not runtime-smoked here.
Browser (@jacs/wasm)npm unpublished; source only not publishedbrowser source build; release package unavailableSource CI builds wasm32 and runs Firefox plus Chromium/Playwright tests; Safari and Windows-hosted browsers are unverified.
Go (github.com/HumanAssisted/JACS/jacsgo)Go module proxy v0.13.0; publishedmacOS arm64; macOS x86_64; Linux x86_64 glibc; Linux arm64 glibcRelease publishes libjacsgo for each listed target with SHA-256 digests and GitHub attestations; consumer smokes passed on Ubuntu x86_64 and arm64 and macOS x86_64. The macOS arm64 consumer smoke failed only because its Go 1.21 toolchain cannot run on macOS 26 (fixed for the next release).

Declared toolchain baselines

SurfaceDeclared or tested baseline
Rust library, CLI, and WASM sourceRust 1.97; Edition 2024
PythonPython 3.10–3.14 is declared by pyproject.toml; the source release workflow smoke-tests the exact wheel candidate on each version before publication.
Nodenpm 0.10.1 does not declare an engines.node constraint. Current source and release workflows use Node 20, so the previous generic “Node 18+” statement was not a tested support guarantee.
Gogo.mod declares Go 1.21; CGo and a platform C toolchain are also required

Platform decisions

Windows

The observed Windows prebuilt is the x86_64 CLI only. Do not infer Windows Python or Node support from the CLI artifact: PyPI 0.13.0 has no Windows wheel, npm 0.10.1 has no Windows .node binary, and the Go bindings have no Windows CGo linker directive or native asset. The browser/WASM source may be portable at the WebAssembly level, but no Windows browser runtime lane was recorded.

Linux libc and architecture

“Linux” is not one ABI:

  • Python glibc wheels are tagged manylinux_2_28; systems with glibc older than 2.28 cannot use those wheels. The observed musllinux wheel is x86_64 only.
  • Node 0.10.1 ships x86_64 glibc and musl binaries, plus an arm64 glibc binary. It does not ship arm64 musl.
  • CLI 0.13.0 ships glibc Linux x86_64 and arm64 archives. No musl CLI archive is recorded in the shipped inventory.
  • A source build is a separate claim. It may work on additional targets when the required compiler, linker, libc headers, Rust dependencies, and system libraries are available, but that is not prebuilt support.

Containers, Lambda, and serverless platforms

A container orchestrator does not remove the native ABI requirement. Match the image architecture and libc to an actual wheel, .node file, CLI archive, or Go shared library. For example, the observed Python musllinux wheel supports an x86_64 Alpine-style image, while its glibc wheels require glibc 2.28 or newer.

This x86_64 example deliberately requires a prebuilt musllinux wheel instead of silently falling back to an undocumented source build:

FROM --platform=linux/amd64 python:3.12-alpine
RUN python -m pip install --only-binary=:all: "jacs==0.13.0"
ENV JACS_KEYCHAIN_BACKEND=disabled
WORKDIR /app
COPY . /app
CMD ["python", "main.py"]

AWS Lambda, Vercel, Cloudflare Workers, Deno, and Bun do not have dedicated runtime-smoke lanes in the recorded matrix. Treat them as unverified. For Lambda or another native serverless runtime, build and test a layer against the exact runtime image and architecture; do not assume a generic Linux artifact is ABI-compatible. Browser-only runtimes require the WASM package, which was not published at the observation date.

For headless native environments, set JACS_KEYCHAIN_BACKEND=disabled and supply key passwords through the deployment platform's secret mechanism. Do not bake passwords or private keys into an image.

Browser/WASM capability limits

The source-built WASM surface can create both Ed25519 and pq2025 identities, sign and verify JSON, and use encrypted browser persistence. It does not expose native filesystem storage, DNS resolution, MCP, or the native CLI. Browser WebAssembly memory is JavaScript-visible; source compatibility is not a claim of hardware-backed key isolation. See the WASM package security caveats before using it for long-lived signing keys.

Building from source

Source builds require the full repository because the language bindings use path dependencies on the Rust crates:

git clone https://github.com/HumanAssisted/JACS.git
cd JACS

# Python development build
python -m pip install maturin
(cd jacspy && maturin develop --release)

# Node development build
(cd jacsnpm && npm ci && npm run build)

# Go native library + module
make -C jacsgo build

# Browser package
make build-wasm

Rust 1.97 or newer is required. Native language bindings also require their language toolchain, a C linker, and platform system dependencies. A successful source build on one machine does not add that target to the published-artifact matrix.

Troubleshooting

Common issues and solutions when installing or using JACS.

Installation Issues

pip install fails

Check your Python version (3.10+ required). If no pre-built wheel exists for your platform, install the Rust toolchain and build from source:

pip install maturin
cd jacspy && maturin develop --release

npm install fails

Pre-built binaries are available for Linux/macOS/Windows x64 and ARM64 macOS. If no pre-built binary matches your platform, you need the Rust toolchain installed so the native addon can compile during npm install.

Alpine Linux / musl libc

The default wheels and binaries target glibc. On Alpine or other musl-based systems, build from source with the Rust toolchain, or use a Debian-based container image instead.

Configuration Issues

Config not found

Run jacs quickstart --name my-agent --domain my-agent.example.com to auto-create a config, or copy the example:

cp jacs.config.example.json jacs.config.json

Private key decryption failed

Wrong or missing password. Check JACS_PRIVATE_KEY_PASSWORD. For CLI, you may also set JACS_PASSWORD_FILE to a file that contains only the password, or use jacs keychain set to store the password in the OS keychain. Set exactly one explicit source; if both env var and password file are set, CLI fails by design. The OS keychain is only consulted when neither env var nor password file is present.

Algorithm detection failed

Set jacs_agent_key_algorithm in the config, or pass the algorithm explicitly to quickstart(...) / create(...). User-facing values are pq2025 and ed25519; the legacy input alias and canonical Ed25519 wire label are ring-Ed25519.

Runtime Issues

Agent creation fails

Ensure the data and key directories exist and are writable. By default these are ./jacs_data and ./jacs_keys.

Signature verification fails

Ensure the signer's public key is accessible. If verifying a document from another agent, you may need to import their public key or use the trust store.

Documents not found

Check the jacs_data_directory path in your config. Documents are stored as JSON files in that directory.

Building from Source

git clone https://github.com/HumanAssisted/JACS.git
cd JACS

# Rust core + CLI
cargo build --release
cargo install --path jacs --features cli

# Python binding
cd jacspy && maturin develop --release

# Node.js binding
cd jacsnpm && npm run build

Requires Rust 1.97+ (install via rustup).

Getting Help

Installation

This guide covers installing the JACS Rust CLI and library.

Requirements

  • Rust: Version 1.97 or later (Edition 2024)
  • Cargo: Included with Rust installation

Verify Rust Version

rustc --version
# Should show rustc 1.97.0 or later

If you need to update Rust:

rustup update stable

Installing the CLI

cargo install jacs-cli

At the 2026-07-09 shipped-artifact snapshot, crates.io resolves jacs-cli 0.11.3; the documentation source is 0.13.0. Check the resolved version instead of assuming the source branch has already been published.

From Homebrew (macOS)

brew tap HumanAssisted/homebrew-jacs
brew install jacs

From Source

git clone https://github.com/HumanAssisted/JACS
cd JACS
cargo install --path jacs-cli

Verify Installation

jacs --help

MCP Server

The MCP server is built into the jacs binary. No separate install step needed.

# Start the MCP server (stdio transport)
JACS_CONFIG="$PWD/jacs.config.json" jacs mcp

Using as a Library

Add JACS to your Cargo.toml:

[dependencies]
jacs = "0.11.3"

0.11.3 is the published crate recorded in release/shipped-artifacts.json; source checkout 0.13.0 is not installable from crates.io until its release completes.

With Optional Features

JACS supports several optional features for observability and integrations:

[dependencies]
# Basic library usage
jacs = "0.11.3"

# With OpenTelemetry logging
jacs = { version = "0.11.3", features = ["otlp-logs"] }

# With OpenTelemetry metrics
jacs = { version = "0.11.3", features = ["otlp-metrics"] }

# With OpenTelemetry tracing
jacs = { version = "0.11.3", features = ["otlp-tracing"] }

# With all observability features
jacs = { version = "0.11.3", features = ["otlp-logs", "otlp-metrics", "otlp-tracing"] }

Available Features

FeatureDescription
cli(Deprecated -- use cargo install jacs-cli instead)
otlp-logsOpenTelemetry Protocol logging backend
otlp-metricsOpenTelemetry Protocol metrics backend
otlp-tracingOpenTelemetry Protocol distributed tracing
sqliteLightweight sync SQLite backend (default)
sqlx-sqliteAsync SQLite backend via sqlx (requires tokio)
s3AWS S3 storage; opt-in because it adds cloud HTTP/XML dependencies
agreementsAgreement lifecycle support
a2aAgent-to-Agent protocol support
attestationAttestation support

Platform Support

The Rust crate and the CLI have different distribution models. The jacs crate is source compiled by Cargo; a successful build depends on the Rust target, linker, and system dependencies available to the consumer. The CLI is also published as platform archives.

Observed public artifacts on 2026-07-09:

SurfaceVersionPrebuilt targetsTest evidence
Rust library cratejacs 0.11.3None; crates.io distributes sourceFull source suites run in release CI on Ubuntu and macOS. This does not imply every Rust-supported target.
CLI cratejacs-cli 0.11.3cargo install builds from sourceSource compilation follows the consumer's Rust target and native dependencies.
CLI archivesjacs-cli 0.11.3macOS arm64/x86_64; glibc Linux arm64/x86_64; Windows x86_64The release job builds each target and executes jacs --version on its runner. Full feature suites are not run separately on every archive target.
Browser WASMsource 0.13.0 onlyNo published @jacs/wasm packageSource CI runs Firefox wasm-pack tests and a Chromium/Playwright package smoke on Ubuntu.

There is no basis for a blanket “full support” claim across operating systems. In particular, the Windows x86_64 CLI archive does not imply Windows wheels or native modules for Python, Node, or Go. See Deployment Compatibility for the exact cross-language, architecture, and libc matrix.

WebAssembly Notes

The source-built jacs-wasm package supports both Ed25519 and post-quantum pq2025 signing and verification. The previous claim that WebAssembly lacked post-quantum support was incorrect.

The WASM surface remains browser-specific and omits native filesystem storage, DNS resolution, MCP, and the CLI. It uses encrypted browser persistence, and its private keys live in JavaScript-visible WebAssembly memory. Most importantly for installation, @jacs/wasm was not published to npm at the shipped-artifact observation date; build it from source rather than running the unqualified npm install command until a registry version exists.

Configuration

After installation, initialize JACS:

# Create configuration and agent in one step
jacs init

This creates:

  • ./jacs.config.json - Configuration file
  • Cryptographic keys for your agent
  • Initial agent document

Manual Configuration

Alternatively, create configuration and agent separately:

# Create configuration only
jacs config create

# Create agent with keys
jacs agent create --create-keys true

Environment Variables

JACS respects the following environment variables:

VariableDescriptionDefault
JACS_CONFIG_PATHPath to configuration file./jacs.config.json
JACS_USE_SECURITYEnable/disable security featurestrue
JACS_DATA_DIRECTORYDirectory for document storage./jacs_data
JACS_KEY_DIRECTORYDirectory for cryptographic keys./jacs_keys
JACS_DEFAULT_STORAGEStorage backend (fs, memory)fs
JACS_AGENT_KEY_ALGORITHMKey algorithm (ed25519, pq2025; legacy alias ring-Ed25519)pq2025
JACS_ALLOW_LEGACY_SIGNATURE_CONTENTExplicitly allow payload-only verification of legacy v1 signatures; signer/version/time metadata stays untrusted and emptyfalse
JACS_REJECT_LEGACY_SIGNATURE_CONTENTForce legacy-v1 rejection even if compatibility was requestedfalse
JACS_PAYLOAD_MAX_REPLAY_SECONDSFreshness/nonce retention window for HTTP/RPC payload proofs300
JACS_REQUIRE_SHARED_REPLAY_STOREFail closed unless an application-installed ReplayStore is shared across replicasfalse

Troubleshooting

Build Errors

"edition 2024 is required" Update Rust to version 1.97 or later:

rustup update stable

Missing dependencies on Linux Install build essentials:

# Debian/Ubuntu
sudo apt-get install build-essential pkg-config libssl-dev

# Fedora
sudo dnf install gcc openssl-devel

Runtime Errors

"Configuration file not found" Run jacs init or set JACS_CONFIG_PATH environment variable.

"Key directory does not exist" Create the key directory or run jacs init:

mkdir -p ./jacs_keys

"Permission denied" Ensure you have write permissions to the data and key directories.

Next Steps

CLI Tutorial

This page walks through common CLI workflows. For a complete command reference, see the CLI Command Reference. For practical scripting examples, see CLI Examples.

The JACS CLI provides a command-line interface for managing agents, documents, and agreements.

Getting Help

# General help
jacs --help

# Command-specific help
jacs agent --help
jacs document --help

Commands Overview

CommandDescription
jacs initInitialize JACS (create config and agent with keys)
jacs versionPrint version information
jacs configManage configuration
jacs agentManage agents
jacs documentManage documents
jacs mcpStart the built-in MCP server (stdio transport)

Initialization

Quick Start

# Initialize everything in one step
jacs init

This command:

  1. Creates a configuration file (jacs.config.json)
  2. Generates cryptographic keys
  3. Creates an initial agent document

MCP Server

The MCP server is built into the jacs binary. No separate install step needed.

# Start the MCP server (stdio transport)
JACS_CONFIG="$PWD/jacs.config.json" jacs mcp

Configuration Commands

Create Configuration

jacs config create

Creates a new jacs.config.json file in the current directory with default settings.

Read Configuration

jacs config read

Displays the current configuration, including values from both the config file and environment variables.

Agent Commands

Create Agent

jacs agent create --create-keys true

# With a custom agent definition file
jacs agent create --create-keys true -f my-agent.json

# Without creating new keys (use existing)
jacs agent create --create-keys false -f my-agent.json

Options:

OptionShortRequiredDescription
--create-keysYesWhether to create new cryptographic keys
-fNoPath to JSON file with agent definition

Verify Agent

# Verify agent from config
jacs agent verify

# Verify specific agent file
jacs agent verify -a ./path/to/agent.json

# With DNS validation options
jacs agent verify --require-dns
jacs agent verify --require-strict-dns
jacs agent verify --no-dns
jacs agent verify --ignore-dns

Options:

OptionShortDescription
-a--agent-filePath to agent file (optional)
--no-dnsDisable DNS validation
--require-dnsRequire DNS validation (not strict)
--require-strict-dnsRequire DNSSEC validation
--ignore-dnsIgnore DNS validation entirely

DNS Commands

# Generate DNS TXT record commands for agent publishing
jacs agent dns --domain example.com --agent-id [uuid]

# With different output formats
jacs agent dns --domain example.com --encoding hex
jacs agent dns --domain example.com --provider aws

# With custom TTL
jacs agent dns --domain example.com --ttl 7200

Options:

OptionDefaultDescription
--domainDomain for DNS record
--agent-idAgent UUID (optional, uses config if not provided)
--ttl3600Time-to-live in seconds
--encodingbase64Encoding format (base64, hex)
--providerplainOutput format (plain, aws, azure, cloudflare)

Lookup Agent

# Look up another agent's public key from their domain
jacs agent lookup agent.example.com

# With strict DNSSEC validation
jacs agent lookup agent.example.com --strict

# Skip DNS lookup
jacs agent lookup agent.example.com --no-dns

Document Commands

Create Document

# Create from a JSON file
jacs document create -f ./document.json

# Create from a directory of files
jacs document create -d ./documents/

# With custom schema
jacs document create -f ./document.json -s ./custom-schema.json

# With file attachments
jacs document create -f ./document.json --attach ./attachment.pdf

# Embed attachments in document
jacs document create -f ./document.json --attach ./files/ --embed true

# Output to specific file
jacs document create -f ./document.json -o ./output.json

# Print to stdout instead of saving
jacs document create -f ./document.json --no-save

Options:

OptionShortDescription
-f--filenamePath to input JSON file
-d--directoryPath to directory of JSON files
-o--outputOutput filename
-s--schemaPath to custom JSON schema
--attachPath to file/directory for attachments
--embed-eEmbed documents (true/false)
--no-save-nPrint to stdout instead of saving
-v--verboseEnable verbose output
-a--agent-filePath to agent file

Update Document

# Update an existing document with new content
jacs document update -f ./original.json -n ./updated.json

# With output file
jacs document update -f ./original.json -n ./updated.json -o ./result.json

# With file attachments
jacs document update -f ./original.json -n ./updated.json --attach ./new-file.pdf

Options:

OptionShortRequiredDescription
-f--filenameYesPath to original document
-n--newYesPath to new version
-o--outputNoOutput filename
--attachNoPath to file attachments
--embed-eNoEmbed documents (true/false)

Verify Document

# Verify a document
jacs document verify -f ./document.json

# Verify all documents in a directory
jacs document verify -d ./documents/

# With custom schema
jacs document verify -f ./document.json -s ./schema.json

# Verbose output
jacs document verify -f ./document.json -v

Options:

OptionShortDescription
-f--filenamePath to document file
-d--directoryPath to directory of documents
-s--schemaPath to JSON schema for validation
-v--verboseEnable verbose output
-a--agent-filePath to agent file

Extract Embedded Content

# Extract embedded content from a document
jacs document extract -f ./document.json

# Extract from all documents in directory
jacs document extract -d ./documents/

Agreement Commands

# Create an agreement requiring signatures from specified agents
jacs document create-agreement -f ./document.json -i agent1-uuid,agent2-uuid

# Check agreement status
jacs document check-agreement -f ./document.json

# Sign an agreement
jacs document sign-agreement -f ./document.json

Create Agreement Options:

OptionShortRequiredDescription
-f--filenameYesPath to document
-i--agentidsYesComma-separated list of agent UUIDs
-o--outputNoOutput filename
--no-save-nNoPrint to stdout

Environment Variables

The CLI respects the following environment variables:

# Use a specific configuration file
JACS_CONFIG_PATH=./custom-config.json jacs agent verify

# Override settings
JACS_DATA_DIRECTORY=./data jacs document create -f ./doc.json
JACS_KEY_DIRECTORY=./keys jacs agent create --create-keys true

Common Workflows

Create and Sign a Document

# 1. Initialize (if not done)
jacs init

# 2. Create document
jacs document create -f ./my-document.json

# 3. Verify the signed document
jacs document verify -f ./jacs_data/[document-id].json

Multi-Agent Agreement

# 1. Create agreement on a document
jacs document create-agreement -f ./document.json -i agent1-id,agent2-id

# 2. First agent signs
jacs document sign-agreement -f ./document.json

# 3. Second agent signs (using their config)
JACS_CONFIG_PATH=./agent2.config.json jacs document sign-agreement -f ./document.json

# 4. Check agreement is complete
jacs document check-agreement -f ./document.json

Verify Another Agent

# Look up agent by domain
jacs agent lookup other-agent.example.com

# Verify with strict DNS
jacs agent verify -a ./other-agent.json --require-strict-dns

Exit Codes

CodeMeaning
0Success
1General error
2Invalid arguments
3File not found
4Verification failed
5Signature invalid

Next Steps

Creating an Agent

An agent is the fundamental identity in JACS: an entity with signing keys that can create, sign, and verify documents.

What is an Agent?

A JACS agent is:

  • A stable UUID identity
  • A holder of cryptographic signing keys
  • A self-signed identity document
  • An optional DNS-verifiable identity

Capabilities for A2A interoperability live in A2A Agent Cards. The JACS agent document stays focused on identity and signing metadata.

Creating Your First Agent

jacs init

This creates:

  • Configuration file
  • Cryptographic key pair
  • Initial agent document

Manual Creation

jacs config create
jacs agent create --create-keys true

Custom Agent Definition

Create an agent definition file:

{
  "$schema": "https://hai.ai/schemas/agent/v1/agent.schema.json",
  "jacsAgentType": "ai",
  "jacsAgentDomain": "myagent.example.com",
  "name": "Content Creation Agent",
  "description": "AI agent specialized in content creation"
}

Then create the agent:

jacs agent create --create-keys true -f my-agent.json

Agent Types

TypeDescription
aiFully artificial intelligence
humanIndividual person
human-orgOrganization or group
hybridHuman-AI combination

Cryptographic Keys

New agents hold two key roles:

RoleAlgorithmPurpose
native_rootpq2025 (default) or ring-Ed25519Signs native JACS documents. Creation honors explicit Ed25519 selection. Rotation preserves the current algorithm unless Ed25519 explicitly upgrades to pq2025; downgrade is rejected.
ecosystem_signingES256 (ECDSA P-256)Compatibility credential for ecosystems that require classical signatures (JWKS/DID/A2A). Never signs native documents.

Use ed25519 as the user-facing creation label; configuration and signed documents use the canonical ring-Ed25519 wire label.

The compatibility key is minted eagerly at creation (skip with CreateAgentParams::builder().no_compat_key(true) or the CLI --no-compat-key). Existing agents migrate explicitly:

let compat = agent.add_compat_key()?; // role, algorithm, RFC 7638 kid, paths
let info = agent.ecosystem_key_info()?; // typed KeyNotFound when absent

Roles are recorded in jacs_keys/jacs.keyring.json (metadata only); the ES256 private key uses the same AES-256-GCM + Argon2id envelope and 0600 permissions as the native root.

Verifying Agents

jacs agent verify
jacs agent verify -a ./path/to/agent.json
jacs agent verify --require-dns
jacs agent verify --require-strict-dns

HTTP protocol helpers

SimpleAgent signs request context and complete response envelopes without exposing its internal mutex:

#![allow(unused)]
fn main() {
use std::collections::HashMap;
use jacs::{protocol::verify_signed_event_with_trusted_keys, simple::SimpleAgent};

let (agent, _) = SimpleAgent::ephemeral(Some("ed25519"))?;
let body = br#"{"action":"approve"}"#;
let authorization = agent.build_request_auth_header(
    "POST",
    "https://api.example.com/v1/jobs?mode=strict",
    body,
    "jobs-api",
)?;

let envelope = agent.sign_response(&serde_json::json!({"decision": "allow"}))?;
let signer_id = envelope["jacsSignature"]["agentID"]
    .as_str()
    .ok_or("missing signer ID")?;
let keys = HashMap::from([(signer_id.to_owned(), agent.get_public_key()?)]);
let verified = verify_signed_event_with_trusted_keys(&envelope, &keys)?;
assert!(authorization.starts_with("JACS v2."));
assert_eq!(verified.data["decision"], "allow");
}

The request header binds method, absolute URL including query, exact body bytes, audience, key, time, and nonce. The response signature covers both data and every envelope field. The verifier rejects unsigned/plain events, unknown signers, legacy payload-only envelopes, and any metadata or payload mutation. See Security for server replay-store requirements and migration from unbound headers.

The supplied key map is the trust boundary. A valid signature proves possession of that key, not a real-world identity unless the application established the signer-to-key association under its configured policy.

Agent Document Structure

{
  "$schema": "https://hai.ai/schemas/agent/v1/agent.schema.json",
  "jacsId": "550e8400-e29b-41d4-a716-446655440000",
  "jacsVersion": "123e4567-e89b-12d3-a456-426614174000",
  "jacsVersionDate": "2024-01-15T10:30:00Z",
  "jacsOriginalVersion": "123e4567-e89b-12d3-a456-426614174000",
  "jacsOriginalDate": "2024-01-15T10:30:00Z",
  "jacsType": "agent",
  "jacsLevel": "config",
  "jacsAgentType": "ai",
  "jacsAgentDomain": "myagent.example.com",
  "name": "Content Creation Agent",
  "description": "AI agent for content generation",
  "jacsSha256": "hash-of-document",
  "jacsSignature": {
    "agentID": "550e8400-e29b-41d4-a716-446655440000",
    "agentVersion": "123e4567-e89b-12d3-a456-426614174000",
    "signature": "base64-encoded-signature",
    "signingAlgorithm": "ring-Ed25519",
    "publicKeyHash": "hash-of-public-key",
    "date": "2024-01-15T10:30:00Z",
    "fields": ["jacsId", "jacsVersion", "jacsAgentType", "jacsAgentDomain", "name"]
  }
}

Best Practices

  1. Protect private keys.
  2. Prefer strong signing algorithms.
  3. Enable DNS verification for production agents.
  4. Keep A2A capabilities in the A2A Agent Card.
  5. Track agent document versions.

Next Steps

Working with Documents

Documents are the core data structure in JACS. Any JSON object can become a JACS document by adding the required header fields and a cryptographic signature.

What is a JACS Document?

A JACS document is a JSON object that includes:

  • Identity: Unique ID and version tracking
  • Metadata: Type, timestamps, and origin information
  • Signature: Cryptographic proof of authenticity
  • Hash: Integrity verification

Creating Documents

From a JSON File

Create a simple JSON document (my-document.json):

{
  "title": "Project Proposal",
  "description": "Q1 development plan",
  "budget": 50000,
  "deadline": "2024-03-31"
}

Sign it with JACS:

jacs document create -f my-document.json

This adds JACS headers and a signature, saves the document, and prints the exact path. Pass --json for a machine-readable document key/path result.

From a Directory

Process multiple documents at once:

jacs document create -d ./documents/

With Custom Schema

Validate against a custom JSON schema:

jacs document create -f my-document.json -s ./schemas/proposal.schema.json

Output Options

# Save to specific file
jacs document create -f my-document.json -o ./output/signed-doc.json

# Print to stdout instead of saving
jacs document create -f my-document.json --no-save

# Verbose output
jacs document create -f my-document.json -v

Document Structure

After signing, a document looks like:

{
  "$schema": "https://hai.ai/schemas/header/v1/header.schema.json",
  "jacsId": "doc-uuid-here",
  "jacsVersion": "version-uuid-here",
  "jacsVersionDate": "2024-01-15T10:30:00Z",
  "jacsOriginalVersion": "version-uuid-here",
  "jacsOriginalDate": "2024-01-15T10:30:00Z",
  "jacsType": "document",
  "jacsLevel": "artifact",

  "title": "Project Proposal",
  "description": "Q1 development plan",
  "budget": 50000,
  "deadline": "2024-03-31",

  "jacsSha256": "a1b2c3d4...",
  "jacsSignature": {
    "agentID": "agent-uuid",
    "agentVersion": "agent-version-uuid",
    "signature": "base64-signature",
    "signingAlgorithm": "ring-Ed25519",
    "publicKeyHash": "hash-of-public-key",
    "date": "2024-01-15T10:30:00Z",
    "fields": ["jacsId", "title", "description", "budget", "deadline"]
  }
}

Required Header Fields

FieldDescriptionAuto-generated
$schemaJSON Schema referenceYes
jacsIdPermanent document UUIDYes
jacsVersionVersion UUID (changes on update)Yes
jacsVersionDateWhen this version was createdYes
jacsOriginalVersionFirst version UUIDYes
jacsOriginalDateOriginal creation timestampYes
jacsTypeDocument typeYes
jacsLevelData level (raw, config, artifact, derived)Yes

Document Levels

The jacsLevel field indicates the document's purpose:

LevelDescriptionUse Case
rawOriginal data, should not changeSource documents
configConfiguration, meant to be updatedAgent definitions, settings
artifactGenerated outputReports, summaries
derivedComputed from other documentsAnalysis results

File Attachments

Attach Files

# Attach a single file
jacs document create -f my-document.json --attach ./report.pdf

# Attach a directory of files
jacs document create -f my-document.json --attach ./attachments/

Embed vs. Reference

# Embed files directly in the document (larger document, self-contained)
jacs document create -f my-document.json --attach ./files/ --embed true

# Reference files (smaller document, files stored separately)
jacs document create -f my-document.json --attach ./files/ --embed false

Attachment Structure

Embedded attachments appear in the jacsFiles field:

{
  "jacsFiles": [
    {
      "jacsFileName": "report.pdf",
      "jacsFileMimeType": "application/pdf",
      "jacsFileSha256": "file-hash",
      "jacsFileContent": "base64-encoded-content"
    }
  ]
}

Verifying Documents

Basic Verification

jacs document verify -f ./signed-document.json

Verification checks:

  1. Hash integrity (document hasn't been modified)
  2. Signature validity (signature matches content)
  3. Schema compliance (if schema specified)

Verify with Schema

jacs document verify -f ./document.json -s ./schema.json

Verify Directory

jacs document verify -d ./documents/

Verbose Output

jacs document verify -f ./document.json -v

Updating Documents

Updates create a new version while maintaining the same jacsId:

jacs document update -f ./original.json -n ./modified.json

The update process:

  1. Reads the original document
  2. Applies changes from the modified file
  3. Increments jacsVersion
  4. Links to previous version via jacsPreviousVersion
  5. Re-signs with agent's key

Update with Attachments

jacs document update -f ./original.json -n ./modified.json --attach ./new-file.pdf

Extracting Embedded Content

Extract attachments from a document:

jacs document extract -f ./document-with-attachments.json

Extract from multiple documents:

jacs document extract -d ./documents/

Document Payloads

Generic Documents

JACS signs arbitrary JSON payloads through the header schema:

{
  "$schema": "https://hai.ai/schemas/header/v1/header.schema.json",
  "jacsType": "document",
  "content": {
    "subject": "Status update",
    "body": "The requested work is complete."
  }
}

Custom Documents

Any JSON can be a JACS document. Create custom schemas:

{
  "$schema": "https://example.com/schemas/invoice.schema.json",
  "jacsType": "invoice",
  "invoiceNumber": "INV-001",
  "amount": 1000,
  "currency": "USD"
}

Version History

JACS tracks document history through version chains:

Version 1 (jacsOriginalVersion)
    ↓
Version 2 (jacsPreviousVersion → Version 1)
    ↓
Version 3 (jacsPreviousVersion → Version 2)
    ↓
Current Version

Each version is a complete document that can be independently verified.

Working with Multiple Agents

Different Agent Signs Document

# Use a specific agent's keys
jacs document create -f ./document.json -a ./other-agent.json

Verify Document from Unknown Agent

# Verify with strict DNS requirement
jacs document verify -f ./document.json --require-strict-dns

Best Practices

Document Design

  1. Use appropriate levels: Match jacsLevel to document purpose
  2. Include context: Add descriptive fields for human readability
  3. Version control: Keep source files in git alongside JACS documents

Security

  1. Verify before trusting: Always verify signatures
  2. Check agent identity: Verify the signing agent
  3. Validate schemas: Use custom schemas for strict validation

Performance

  1. External attachments: Use --embed false for large files
  2. Batch processing: Use directory mode for multiple documents
  3. Selective verification: Verify only when needed

Common Workflows

Create and Share Document

# 1. Create document
jacs document create -f ./proposal.json -o ./signed-proposal.json

# 2. Share the signed document
# The recipient can verify it:
jacs document verify -f ./signed-proposal.json

Track Document Changes

# 1. Create initial version
jacs document create -f ./contract-v1.json

# 2. Make changes and update
jacs document update -f ./contract-v1.json -n ./contract-v2.json

# 3. Continue updating
jacs document update -f ./contract-v2.json -n ./contract-v3.json

Process Multiple Documents

# Create all documents in a directory
jacs document create -d ./input-docs/

# Verify all documents
jacs document verify -d ./signed-docs/

Next Steps

Creating and Using Agreements

Agreement v2 is the preferred JACS agreement model. It is a standalone jacsType: "agreement" document that captures terms, parties, transcript references, signature policy, consent signatures, and version lineage.

Use Agreement v2 when the product question is: "did these agents consent to these terms, under this policy, with this process record?"

Agreement v2 Model

The JACS header still owns document identity, versioning, authorship signatures, hashes, registration, files, and visibility. The agreement body owns consent:

  • title, description, terms, termsFormat
  • effectiveFrom, expiresAt
  • parties
  • signaturePolicy
  • agreementSignatures
  • transcript
  • allPreviousVersions
  • links
  • controllers, owners

jacsAgreementHash is the consent hash. It changes when terms, parties, signature policy, or effective/expiry dates change. Transcript appends do not change it; signers bind the transcript state with signedTranscriptHash when transcript entries exist.

Roles

RoleMeaning
signerConsents to the terms and counts toward partyQuorum
witnessAttests to process, time, or identity and counts toward witnessRequired
notaryProvides notarial attestation; HAI uses this role and it counts toward notaryRequired
observerListed participant that does not sign

Minimal Input

{
  "title": "Refund approval",
  "description": "Agent A and Agent B agree on a bounded refund.",
  "terms": "Agent B may issue a refund up to $25 for order 123.",
  "termsFormat": "text/plain",
  "status": "proposed",
  "parties": [
    { "agentId": "00000000-0000-4000-8000-000000000001", "agentType": "ai", "role": "signer" },
    { "agentId": "00000000-0000-4000-8000-000000000002", "agentType": "human", "role": "signer" },
    { "agentId": "00000000-0000-4000-8000-000000000003", "agentType": "ai", "role": "notary" }
  ],
  "signaturePolicy": {
    "partyQuorum": "all",
    "witnessRequired": 0,
    "notaryRequired": 1,
    "minimumStrength": "classical"
  },
  "controllers": [
    "00000000-0000-4000-8000-000000000001",
    "00000000-0000-4000-8000-000000000002",
    "00000000-0000-4000-8000-000000000003"
  ]
}

Rust Core API

Rust core is the source of truth for Agreement v2 behavior. Binding surfaces call into the same helpers.

#![allow(unused)]
fn main() {
use jacs::agreements::v2::{
    create,
    sign,
    verify,
    AgreementV2Role,
    CreateAgreementV2,
};

let agreement = create(&agent, input)?;
let signed = sign(&agent, &agreement.raw, AgreementV2Role::Signer)?;
let report = verify(&agent, &signed.raw)?;

assert!(report.valid);
}

Use mutations for successor versions instead of editing JSON by hand. The helpers maintain jacsAgreementHash, jacsPreviousVersion, allPreviousVersions, status, and signature invalidation.

CLI

jacs agreement-v2 create --input agreement-input.json > agreement.json
jacs agreement-v2 sign --agreement agreement.json --role signer > signed.json
jacs agreement-v2 verify --agreement signed.json

Branch handling is also in core and exposed through the CLI:

jacs agreement-v2 detect-conflict --base base.json --left left.json --right right.json
jacs agreement-v2 merge-transcript --base base.json --left left.json --right right.json
jacs agreement-v2 resolve-conflict --base base.json --previous left.json --side right.json --mutation resolution.json

Transcript-only branches can auto-merge. Terms conflicts require an explicit successor mutation.

Legacy Sidecar Agreements

Legacy v1; new work should use Agreement v2. See Migrating from v1 agreements.

The older jacsAgreement field on arbitrary signed documents remains available for simple countersignature workflows:

jacs document create-agreement -f ./document.json -i agent1-uuid,agent2-uuid
jacs document sign-agreement -f ./document-with-agreement.json
jacs document check-agreement -f ./document-with-agreement.json

Use the legacy sidecar only when you need "these agents approved this existing payload." Use Agreement v2 when you need standalone terms, lifecycle, transcript evidence, notary signatures, branch handling, or portable SDK parity.

Exporting as a Verifiable Credential

With the agreements feature, SimpleAgent::export_agreement_v2_as_vc(agreement_json) projects an Agreement-v2 document into a VC 2.0 credential with an ecdsa-jcs-2019 Data Integrity proof (ES256 compatibility key, Multikey verification method). It requires the agreement-vc binding scope — content scopes are never auto-issued — and never mutates the native agreement. See the Agreement v2 Developer Guide for the full flow.

See Also

DNS-Based Agent Verification

JACS supports DNS-based agent verification using DNS TXT records and DNSSEC. This allows agents to publish their identity in a decentralized, verifiable way that doesn't require a central authority.

Overview

DNS verification in JACS works by:

  1. Publishing an agent's public key fingerprint as a DNS TXT record
  2. Using DNSSEC to cryptographically verify the DNS response
  3. Comparing the fingerprint from DNS with the agent's actual public key

This provides a secure, decentralized way to verify agent identity across the internet.

Why DNS Verification?

  • Decentralized: No central authority required
  • Existing Infrastructure: Uses established DNS infrastructure
  • DNSSEC Security: Cryptographic verification of DNS responses
  • Human-Readable: Agents can be identified by domain names
  • Widely Supported: Works with any DNS provider

DNS verification is also a practical bridge for DID-style deployments: you can publish DID metadata for discovery while using DNS TXT + JACS signature verification as the operational trust anchor. No blockchain is required for this model.

Publishing Agent Identity

Generate DNS Commands

# Generate DNS TXT record commands for your agent
jacs agent dns --domain myagent.example.com

# Specify agent ID explicitly
jacs agent dns --domain myagent.example.com --agent-id 550e8400-e29b-41d4-a716-446655440000

# Use hex encoding instead of base64
jacs agent dns --domain myagent.example.com --encoding hex

# Set custom TTL (time-to-live)
jacs agent dns --domain myagent.example.com --ttl 7200

Provider-Specific Formats

JACS can generate DNS commands for various providers:

# Plain text format (default)
jacs agent dns --domain myagent.example.com --provider plain

# AWS Route 53 format
jacs agent dns --domain myagent.example.com --provider aws

# Azure DNS format
jacs agent dns --domain myagent.example.com --provider azure

# Cloudflare DNS format
jacs agent dns --domain myagent.example.com --provider cloudflare

DNS Record Structure

The DNS TXT record follows this format:

_v1.agent.jacs.myagent.example.com. 3600 IN TXT "jacs-agent-fingerprint=<fingerprint>"

Where:

  • _v1.agent.jacs. is the JACS-specific subdomain prefix
  • <fingerprint> is the base64-encoded hash of the agent's public key

Setting Up with Route 53 (AWS)

  1. Generate the AWS-formatted command:
jacs agent dns --domain myagent.example.com --provider aws
  1. The output will include an AWS CLI command like:
aws route53 change-resource-record-sets \
  --hosted-zone-id YOUR_ZONE_ID \
  --change-batch '{
    "Changes": [{
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "_v1.agent.jacs.myagent.example.com",
        "Type": "TXT",
        "TTL": 3600,
        "ResourceRecords": [{"Value": "\"jacs-agent-fingerprint=...\""}]
      }
    }]
  }'
  1. Replace YOUR_ZONE_ID with your actual Route 53 hosted zone ID.

Setting Up with Cloudflare

  1. Generate the Cloudflare-formatted command:
jacs agent dns --domain myagent.example.com --provider cloudflare
  1. Or add manually in the Cloudflare dashboard:
    • Type: TXT
    • Name: _v1.agent.jacs
    • Content: jacs-agent-fingerprint=<your-fingerprint>
    • TTL: 3600

Setting Up with Azure DNS

  1. Generate the Azure-formatted command:
jacs agent dns --domain myagent.example.com --provider azure
  1. The output will include an Azure CLI command that you can run directly.

Verifying Agents with DNS

Look Up Another Agent

# Look up an agent by their domain
jacs agent lookup other-agent.example.com

# With strict DNSSEC validation
jacs agent lookup other-agent.example.com --strict

# Skip DNS verification (not recommended)
jacs agent lookup other-agent.example.com --no-dns

Verify Agent with DNS

When verifying an agent, you can specify DNS requirements:

# Default: Use DNS if available, but don't require it
jacs agent verify -a ./agent.json

# Require DNS validation (non-strict)
jacs agent verify -a ./agent.json --require-dns

# Require strict DNSSEC validation
jacs agent verify -a ./agent.json --require-strict-dns

# Disable DNS validation entirely
jacs agent verify -a ./agent.json --no-dns

# Ignore DNS (won't fail if DNS unavailable)
jacs agent verify -a ./agent.json --ignore-dns

DNS Validation Modes

ModeFlagBehavior
Default(none)Use DNS if available, fall back to local verification
Require DNS--require-dnsFail if DNS record not found (DNSSEC not required)
Require Strict--require-strict-dnsFail if DNSSEC validation fails
No DNS--no-dnsSkip DNS validation entirely
Ignore DNS--ignore-dnsDon't fail on DNS errors, just warn

Agent Domain Configuration

Agents can specify their domain in their agent document:

{
  "jacsId": "550e8400-e29b-41d4-a716-446655440000",
  "jacsAgentType": "ai",
  "jacsAgentDomain": "myagent.example.com"
}

The jacsAgentDomain field is optional but enables DNS-based verification.

DNSSEC Requirements

For maximum security, enable DNSSEC on your domain:

  1. Enable DNSSEC at your registrar: Most registrars support DNSSEC
  2. Configure your DNS provider: Ensure your DNS provider signs zones
  3. Use --require-strict-dns: Enforce DNSSEC validation

Checking DNSSEC Status

You can verify DNSSEC is working using standard tools:

# Check if DNSSEC is enabled
dig +dnssec _v1.agent.jacs.myagent.example.com TXT

# Verify DNSSEC validation
delv @8.8.8.8 _v1.agent.jacs.myagent.example.com TXT

Security Considerations

Trust Model

  • With DNSSEC: Full cryptographic chain of trust from root DNS servers
  • Without DNSSEC: Trust depends on DNS infrastructure security
  • Local Only: Trust is limited to having the correct public key

Best Practices

  1. Always enable DNSSEC for production agents
  2. Use strict validation when verifying unknown agents
  3. Rotate keys carefully - update DNS records before key changes
  4. Monitor DNS records for unauthorized changes
  5. Use short TTLs during transitions then increase for stability

Caching

DNS responses are cached based on TTL. Consider:

  • Short TTL (300-600s): Better for development or key rotation
  • Long TTL (3600-86400s): Better for production stability

Troubleshooting

"DNS record not found"

  1. Verify the record exists:
dig _v1.agent.jacs.myagent.example.com TXT
  1. Check DNS propagation (may take up to 48 hours for new records)

  2. Verify the domain in the agent document matches

"DNSSEC validation failed"

  1. Check DNSSEC is enabled:
dig +dnssec myagent.example.com
  1. Verify DS records at registrar

  2. Use --require-dns instead of --require-strict-dns if DNSSEC isn't available

"Fingerprint mismatch"

  1. The public key may have changed - regenerate DNS record:
jacs agent dns --domain myagent.example.com
  1. Update the DNS TXT record with the new fingerprint

  2. Wait for DNS propagation

Integration with CI/CD

Automate DNS updates in your deployment pipeline:

#!/bin/bash
# deploy-agent.sh

# 1. Create new agent keys
jacs agent create --create-keys true

# 2. Generate DNS update command
DNS_CMD=$(jacs agent dns --domain $AGENT_DOMAIN --provider aws)

# 3. Execute DNS update
eval $DNS_CMD

# 4. Wait for propagation
sleep 60

# 5. Verify DNS is working
jacs agent verify --require-dns

Next Steps

Rust Library API

JACS provides a Rust library for programmatic agent and document management. This chapter covers how to use the JACS library in your Rust applications.

Adding JACS as a Dependency

Add JACS to your Cargo.toml:

[dependencies]
jacs = "0.3"

Feature Flags

[dependencies]
jacs = { version = "0.3", features = ["cli", "observability"] }
FeatureDescription
sqliteLightweight sync SQLite backend (default)
sqlx-sqliteAsync SQLite backend via sqlx (requires tokio)
s3AWS S3 storage; opt-in because it adds cloud HTTP/XML dependencies
otlp-logsOTLP log export support
otlp-metricsOTLP metrics export support
otlp-tracingOTLP distributed tracing support
agreementsAgreement lifecycle support
a2aAgent-to-Agent protocol support
attestationAttestation support

Core Types

Agent

The Agent struct is the central type in JACS. It holds:

  • Schema validators
  • Agent identity and keys
  • Document storage
  • Configuration
use jacs::{get_empty_agent, load_agent};
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    // Create a new empty agent
    let agent = get_empty_agent();

    // Or load an existing agent
    let agent = load_agent(Some("path/to/agent.json".to_string()))?;

    Ok(())
}

JACSDocument

Documents in JACS are represented by the JACSDocument struct:

#![allow(unused)]
fn main() {
pub struct JACSDocument {
    pub id: String,
    pub version: String,
    pub value: serde_json::Value,
    pub jacs_type: String,
}
}

Key methods:

  • getkey() - Returns "id:version" identifier
  • getvalue() - Returns reference to the JSON value
  • getschema() - Returns the document's schema URL
  • signing_agent() - Returns the ID of the signing agent

Creating an Agent

Minimal Agent

use jacs::{get_empty_agent, create_minimal_blank_agent};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create agent JSON
    let agent_json = create_minimal_blank_agent(
        "ai".to_string(),                    // agent type
        Some("My service".to_string()),      // service description
        Some("Task completed".to_string()),  // success description
        Some("Task failed".to_string()),     // failure description
    )?;

    // Initialize and load the agent
    let mut agent = get_empty_agent();
    agent.create_agent_and_load(&agent_json, true, None)?;

    // Save the agent
    agent.save()?;

    Ok(())
}

Loading by Configuration

use jacs::get_empty_agent;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut agent = get_empty_agent();

    // Load from config file
    agent.load_by_config("./jacs.config.json".to_string())?;

    // Or load by agent ID
    agent.load_by_id("agent-id:version-id".to_string())?;

    Ok(())
}

DNS Strict Mode

use jacs::load_agent_with_dns_strict;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Load agent with strict DNS verification
    let agent = load_agent_with_dns_strict(
        "path/to/agent.json".to_string(),
        true  // strict mode
    )?;

    Ok(())
}

Working with Documents

Creating Documents

The DocumentTraits trait provides document operations:

use jacs::agent::document::DocumentTraits;
use jacs::get_empty_agent;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut agent = get_empty_agent();
    agent.load_by_config("./jacs.config.json".to_string())?;

    // Create a document from JSON
    let json = r#"{"title": "My Document", "content": "Hello, World!"}"#;
    let doc = agent.create_document_and_load(json, None, None)?;

    println!("Document created: {}", doc.getkey());

    Ok(())
}

Creating Documents with Attachments

#![allow(unused)]
fn main() {
use jacs::agent::document::DocumentTraits;

// With file attachments
let attachments = Some(vec!["./report.pdf".to_string()]);
let embed = Some(true);  // Embed files in document

let doc = agent.create_document_and_load(
    json,
    attachments,
    embed
)?;
}

Loading Documents

#![allow(unused)]
fn main() {
use jacs::agent::document::DocumentTraits;

// Load a document from JSON string
let doc = agent.load_document(&document_json_string)?;

// Get a stored document by key
let doc = agent.get_document("doc-id:version-id")?;

// List all document keys
let keys = agent.get_document_keys();
}

Updating Documents

#![allow(unused)]
fn main() {
use jacs::agent::document::DocumentTraits;

// Update creates a new version
let updated_doc = agent.update_document(
    "doc-id:version-id",    // original document key
    &modified_json_string,  // new content
    None,                   // optional attachments
    None,                   // embed flag
)?;
}

Verifying Documents

#![allow(unused)]
fn main() {
use jacs::agent::document::DocumentTraits;

// Verify document signature with agent's public key
agent.verify_document_signature(
    "doc-id:version-id",
    None,  // signature key (uses default)
    None,  // fields to verify
    None,  // public key (uses agent's)
    None,  // key encoding type
)?;

// Verify using external public key
agent.verify_external_document_signature("doc-id:version-id")?;
}

Saving Documents

#![allow(unused)]
fn main() {
use jacs::agent::document::DocumentTraits;

// Save document to filesystem
agent.save_document(
    "doc-id:version-id",
    Some("output.json".to_string()),  // output filename
    Some(true),                       // export embedded files
    None,                             // extract only
)?;
}

Signing and Verification

Signing Documents

The agent's signing_procedure method creates cryptographic signatures:

#![allow(unused)]
fn main() {
use serde_json::json;

let document = json!({
    "title": "Contract",
    "terms": "..."
});

// Sign the document
let signature = agent.signing_procedure(
    &document,
    None,           // fields to sign (None = all)
    "jacsSignature" // placement key
)?;
}

Verification

#![allow(unused)]
fn main() {
// Verify self-signature (agent document)
agent.verify_self_signature()?;

// Verify hash integrity
agent.verify_hash(&document)?;

// Full signature verification
agent.signature_verification_procedure(
    &document,
    None,                    // fields
    "jacsSignature",         // signature key
    public_key,              // public key bytes
    Some("ring-Ed25519".to_string()),  // algorithm
    None,                    // original public key hash
    None,                    // signature override
)?;
}

Custom Schema Validation

#![allow(unused)]
fn main() {
// Load custom schemas
agent.load_custom_schemas(&[
    "./schemas/invoice.schema.json".to_string(),
    "https://example.com/schemas/contract.schema.json".to_string(),
])?;

// Validate document against custom schema
agent.validate_document_with_custom_schema(
    "./schemas/invoice.schema.json",
    &document_value,
)?;
}

Configuration

Loading Configuration

#![allow(unused)]
fn main() {
use jacs::config::{load_config, find_config, Config};

// Load from specific path
let config = load_config("./jacs.config.json")?;

// Find config in directory
let config = find_config("./".to_string())?;

// Create programmatically
let config = Config::new(
    Some("false".to_string()),           // use_security
    Some("./jacs_data".to_string()),     // data_directory
    Some("./jacs_keys".to_string()),     // key_directory
    Some("private_key.pem".to_string()), // private key filename
    Some("public_key.pem".to_string()),  // public key filename
    Some("ring-Ed25519".to_string()),    // key algorithm
    Some("password".to_string()),        // private key password
    None,                                // agent ID and version
    Some("fs".to_string()),              // storage type
);
}

Accessing Configuration

#![allow(unused)]
fn main() {
// Get key algorithm
let algorithm = config.get_key_algorithm()?;

// Access config fields
let data_dir = config.jacs_data_directory();
let key_dir = config.jacs_key_directory();
let storage_type = config.jacs_default_storage();
}

Observability

Initialize Default Observability

use jacs::init_default_observability;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Set up file-based logging
    init_default_observability()?;

    // Your application code...

    Ok(())
}

Custom Observability Configuration

use jacs::{
    init_custom_observability,
    ObservabilityConfig,
    LogConfig,
    LogDestination,
    MetricsConfig,
    MetricsDestination,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = ObservabilityConfig {
        logs: LogConfig {
            enabled: true,
            level: "debug".to_string(),
            destination: LogDestination::Otlp {
                endpoint: "http://localhost:4317".to_string(),
                headers: None,
            },
            headers: None,
        },
        metrics: MetricsConfig {
            enabled: true,
            destination: MetricsDestination::Prometheus {
                endpoint: "http://localhost:9090".to_string(),
                headers: None,
            },
            export_interval_seconds: Some(30),
            headers: None,
        },
        tracing: None,
    };

    init_custom_observability(config)?;

    Ok(())
}

Storage Backends

JACS supports multiple storage backends:

#![allow(unused)]
fn main() {
use jacs::storage::MultiStorage;

// Filesystem storage (default)
let storage = MultiStorage::new("fs".to_string())?;

// In-memory storage
let storage = MultiStorage::new("memory".to_string())?;

// AWS object storage
let storage = MultiStorage::new("aws".to_string())?;
}

For signed document CRUD/search, prefer the unified DocumentService surface:

use jacs::document::service_from_agent;

let docs = service_from_agent(agent_handle)?;
// `fs` and `rusqlite` currently resolve in JACS core.

Error Handling

JACS functions return Result<T, Box<dyn Error>>:

use jacs::get_empty_agent;

fn main() {
    match get_empty_agent().load_by_config("./jacs.config.json".to_string()) {
        Ok(()) => println!("Agent loaded successfully"),
        Err(e) => eprintln!("Failed to load agent: {}", e),
    }
}

Thread Safety

The Agent struct uses internal mutexes for thread-safe access to:

  • Document schemas (Arc<Mutex<HashMap<String, Validator>>>)
  • Storage operations

For concurrent usage:

#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex};
use jacs::get_empty_agent;

let agent = Arc::new(Mutex::new(get_empty_agent()));

// Clone Arc for threads
let agent_clone = Arc::clone(&agent);
std::thread::spawn(move || {
    let mut agent = agent_clone.lock().unwrap();
    // Use agent...
});
}

Complete Example

use jacs::get_empty_agent;
use jacs::agent::document::DocumentTraits;
use serde_json::json;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Initialize agent
    let mut agent = get_empty_agent();
    agent.load_by_config("./jacs.config.json".to_string())?;

    // Create a document
    let doc_json = json!({
        "title": "Project Proposal",
        "description": "Q1 development plan",
        "budget": 50000
    });

    let doc = agent.create_document_and_load(
        &doc_json.to_string(),
        None,
        None
    )?;

    println!("Created document: {}", doc.getkey());

    // Verify the document
    agent.verify_document_signature(&doc.getkey(), None, None, None, None)?;
    println!("Document verified successfully");

    // Save to file
    agent.save_document(&doc.getkey(), Some("proposal.json".to_string()), None, None)?;

    Ok(())
}

Next Steps

Observability (Rust API)

This page covers the Rust-specific observability API: ObservabilityConfig, LogDestination, MetricsConfig, TracingConfig, and related types. For a cross-language guide covering structured events, OTEL collector setup, and monitoring backend integration, see the Observability & Monitoring Guide.

JACS provides comprehensive observability features including logging, metrics, and distributed tracing. This chapter covers configuring and using these features in your Rust applications.

Overview

JACS observability is built on the OpenTelemetry standard, providing:

  • Logging: Structured logging with multiple destinations
  • Metrics: Counters, gauges, and histograms for monitoring
  • Tracing: Distributed tracing for request flows

Feature Flags

Enable observability features in your Cargo.toml:

[dependencies]
jacs = { version = "0.3", features = ["observability"] }
FeatureDescription
otlp-logsOTLP log export support
otlp-metricsOTLP metrics export support
otlp-tracingOTLP distributed tracing support

Convenience helpers for recording operations are always available (no feature flag needed).

Quick Start

Default Configuration

The simplest way to enable observability:

use jacs::init_default_observability;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    init_default_observability()?;

    // Your application code...

    Ok(())
}

This sets up:

  • File-based logging to ./logs/ with daily rotation
  • Metrics disabled by default
  • Tracing disabled by default

Custom Configuration

For more control, use init_custom_observability:

use jacs::{
    init_custom_observability,
    ObservabilityConfig,
    LogConfig,
    LogDestination,
    MetricsConfig,
    MetricsDestination,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = ObservabilityConfig {
        logs: LogConfig {
            enabled: true,
            level: "info".to_string(),
            destination: LogDestination::Stderr,
            headers: None,
        },
        metrics: MetricsConfig {
            enabled: false,
            destination: MetricsDestination::Stdout,
            export_interval_seconds: None,
            headers: None,
        },
        tracing: None,
    };

    init_custom_observability(config)?;
    Ok(())
}

Logging

Log Levels

Supported log levels (from most to least verbose):

  • trace
  • debug
  • info
  • warn
  • error

Log Destinations

Stderr (Default)

#![allow(unused)]
fn main() {
LogDestination::Stderr
}

Logs to standard error. Useful for development and containerized environments.

File

#![allow(unused)]
fn main() {
LogDestination::File {
    path: "./logs".to_string(),
}
}

Logs to rotating files with daily rotation. Creates files like app.log.2024-01-15.

OTLP

#![allow(unused)]
fn main() {
LogDestination::Otlp {
    endpoint: "http://localhost:4318".to_string(),
    headers: None,
}
}

Exports logs via OpenTelemetry Protocol. Requires otlp-logs feature.

Null

#![allow(unused)]
fn main() {
LogDestination::Null
}

Disables logging completely.

Using Logs

JACS uses the tracing crate for logging:

#![allow(unused)]
fn main() {
use tracing::{info, debug, warn, error};

fn process_document() {
    info!("Processing document");
    debug!("Document details: {:?}", doc);

    if let Err(e) = verify() {
        error!("Verification failed: {}", e);
    }
}
}

Metrics

Enabling Metrics

#![allow(unused)]
fn main() {
MetricsConfig {
    enabled: true,
    destination: MetricsDestination::Otlp {
        endpoint: "http://localhost:4318".to_string(),
        headers: None,
    },
    export_interval_seconds: Some(30),
    headers: None,
}
}

Metrics Destinations

OTLP

#![allow(unused)]
fn main() {
MetricsDestination::Otlp {
    endpoint: "http://localhost:4318".to_string(),
    headers: None,
}
}

Exports to an OpenTelemetry collector. Requires otlp-metrics feature.

Prometheus (via Collector)

#![allow(unused)]
fn main() {
MetricsDestination::Prometheus {
    endpoint: "http://localhost:9090".to_string(),
    headers: None,
}
}

Note: Direct Prometheus export requires routing through an OTLP collector.

File

#![allow(unused)]
fn main() {
MetricsDestination::File {
    path: "./metrics.txt".to_string(),
}
}

Writes metrics to a file.

Stdout

#![allow(unused)]
fn main() {
MetricsDestination::Stdout
}

Prints metrics to standard output. Useful for testing.

Recording Metrics

JACS provides convenience functions for common metrics:

#![allow(unused)]
fn main() {
use jacs::observability::metrics::{increment_counter, set_gauge, record_histogram};
use std::collections::HashMap;

// Increment a counter
let mut tags = HashMap::new();
tags.insert("operation".to_string(), "sign".to_string());
increment_counter("jacs_operations_total", 1, Some(tags));

// Set a gauge value
set_gauge("jacs_documents_active", 42.0, None);

// Record a histogram value (e.g., latency)
let mut tags = HashMap::new();
tags.insert("method".to_string(), "verify".to_string());
record_histogram("jacs_operation_duration_ms", 150.0, Some(tags));
}

Built-in Metrics

JACS convenience helpers automatically record:

  • jacs_agent_operations - Count of agent operations
  • jacs_signature_verifications - Signature verification results
  • jacs_document_operations - Document create/update/verify counts

Distributed Tracing

Enabling Tracing

#![allow(unused)]
fn main() {
use jacs::{TracingConfig, TracingDestination, SamplingConfig, ResourceConfig};
use std::collections::HashMap;

let config = ObservabilityConfig {
    // ... logs and metrics config ...
    tracing: Some(TracingConfig {
        enabled: true,
        sampling: SamplingConfig {
            ratio: 1.0,           // Sample all traces
            parent_based: true,
            rate_limit: None,
        },
        resource: Some(ResourceConfig {
            service_name: "my-jacs-app".to_string(),
            service_version: Some("1.0.0".to_string()),
            environment: Some("production".to_string()),
            attributes: HashMap::new(),
        }),
        destination: Some(TracingDestination::Otlp {
            endpoint: "http://localhost:4318".to_string(),
            headers: None,
        }),
    }),
};
}

Tracing Destinations

OTLP

#![allow(unused)]
fn main() {
TracingDestination::Otlp {
    endpoint: "http://localhost:4318".to_string(),
    headers: None,
}
}

Jaeger

#![allow(unused)]
fn main() {
TracingDestination::Jaeger {
    endpoint: "http://localhost:14268/api/traces".to_string(),
    headers: None,
}
}

Sampling Configuration

Control how many traces are captured:

#![allow(unused)]
fn main() {
SamplingConfig {
    ratio: 0.1,          // Sample 10% of traces
    parent_based: true,  // Inherit parent sampling decision
    rate_limit: Some(100), // Max 100 samples per second
}
}

Using Tracing Spans

#![allow(unused)]
fn main() {
use tracing::{instrument, info_span};

#[instrument]
fn sign_document(doc: &Document) -> Result<(), Error> {
    // Automatically creates a span named "sign_document"
    // with doc as a field
}

fn manual_span() {
    let span = info_span!("verify_chain", doc_count = 5);
    let _guard = span.enter();

    // Operations within this span
}
}

Configuration File

You can configure observability via jacs.config.json:

{
  "$schema": "https://hai.ai/schemas/jacs.config.schema.json",
  "jacs_data_directory": "./jacs_data",
  "jacs_key_directory": "./jacs_keys",
  "jacs_agent_key_algorithm": "ring-Ed25519",
  "observability": {
    "logs": {
      "enabled": true,
      "level": "info",
      "destination": {
        "file": {
          "path": "./logs"
        }
      }
    },
    "metrics": {
      "enabled": true,
      "destination": {
        "otlp": {
          "endpoint": "http://localhost:4318"
        }
      },
      "export_interval_seconds": 30
    },
    "tracing": {
      "enabled": true,
      "sampling": {
        "ratio": 1.0,
        "parent_based": true
      },
      "resource": {
        "service_name": "jacs-service",
        "service_version": "1.0.0",
        "environment": "production"
      },
      "destination": {
        "otlp": {
          "endpoint": "http://localhost:4318"
        }
      }
    }
  }
}

OpenTelemetry Collector Setup

For production use, route telemetry through an OpenTelemetry Collector:

# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:

exporters:
  logging:
    loglevel: debug
  prometheus:
    endpoint: "0.0.0.0:9090"
  jaeger:
    endpoint: jaeger:14250

service:
  pipelines:
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [logging]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [prometheus]
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [jaeger]

Reset and Cleanup

For testing or reinitialization:

#![allow(unused)]
fn main() {
use jacs::observability::{reset_observability, flush_observability, force_reset_for_tests};

// Flush pending data
flush_observability();

// Reset configuration
reset_observability();

// Force reset for tests (clears all state)
force_reset_for_tests();
}

Best Practices

Development

#![allow(unused)]
fn main() {
let config = ObservabilityConfig {
    logs: LogConfig {
        enabled: true,
        level: "debug".to_string(),
        destination: LogDestination::Stderr,
        headers: None,
    },
    metrics: MetricsConfig {
        enabled: false,
        destination: MetricsDestination::Stdout,
        export_interval_seconds: None,
        headers: None,
    },
    tracing: None,
};
}

Production

#![allow(unused)]
fn main() {
let config = ObservabilityConfig {
    logs: LogConfig {
        enabled: true,
        level: "info".to_string(),
        destination: LogDestination::Otlp {
            endpoint: "http://collector:4318".to_string(),
            headers: Some(auth_headers()),
        },
        headers: None,
    },
    metrics: MetricsConfig {
        enabled: true,
        destination: MetricsDestination::Otlp {
            endpoint: "http://collector:4318".to_string(),
            headers: Some(auth_headers()),
        },
        export_interval_seconds: Some(30),
        headers: None,
    },
    tracing: Some(TracingConfig {
        enabled: true,
        sampling: SamplingConfig {
            ratio: 0.1,  // Sample 10% in production
            parent_based: true,
            rate_limit: Some(1000),
        },
        resource: Some(ResourceConfig {
            service_name: "jacs-production".to_string(),
            service_version: Some(env!("CARGO_PKG_VERSION").to_string()),
            environment: Some("production".to_string()),
            attributes: HashMap::new(),
        }),
        destination: Some(TracingDestination::Otlp {
            endpoint: "http://collector:4318".to_string(),
            headers: Some(auth_headers()),
        }),
    }),
};
}

Troubleshooting

Logs Not Appearing

  1. Check that logging is enabled: logs.enabled: true
  2. Verify log level includes your log statements
  3. For file logging, ensure the directory is writable

Metrics Not Exporting

  1. Verify otlp-metrics feature is enabled
  2. Check endpoint connectivity
  3. Confirm metrics are enabled: metrics.enabled: true

Traces Missing

  1. Verify otlp-tracing feature is enabled
  2. Check sampling ratio isn't filtering all traces
  3. Ensure spans are properly instrumented

Next Steps

Node.js Installation

Registry status (observed 2026-07-11): npm serves @hai.ai/jacs@0.10.1, while these pages describe source 0.11.4. The registry package does not expose every method shown here. Pin the version and inspect its exports; use a source build only when that is an explicit deployment choice.

The JACS Node.js package (@hai.ai/jacs) provides JavaScript/TypeScript bindings to the JACS Rust library for servers and other Node.js projects. It is a native .node package and does not run in a browser; see the source-built WASM package for browser applications.

Requirements

  • Node.js: Version 20.0 or higher
  • npm or yarn: For package management
  • Operating System: macOS, Linux, or Windows with WSL

Installation

Using npm

npm install @hai.ai/jacs

Using yarn

yarn add @hai.ai/jacs

Using pnpm

pnpm add @hai.ai/jacs

Verify Installation

Create a simple test to verify everything is working:

// test.js
import { JacsAgent } from '@hai.ai/jacs';

console.log('JACS Node.js bindings loaded successfully!');

// Test basic functionality (async API)
try {
  const agent = new JacsAgent();
  await agent.load('./jacs.config.json');
  console.log('Agent loaded successfully!');
} catch (error) {
  console.error('Error loading agent:', error);
}

Run the test:

node test.js

Package Structure

The @hai.ai/jacs package exposes these entry points:

Core and simple API

import { JacsAgent, hashString, createConfig } from '@hai.ai/jacs';
import * as jacs from '@hai.ai/jacs/simple';  // quickstart, load, signMessage, verify, etc.
import { JacsClient } from '@hai.ai/jacs/client';

MCP (@hai.ai/jacs/mcp)

import { createJACSTransportProxy, createJACSTransportProxyAsync, registerJacsTools } from '@hai.ai/jacs/mcp';

HTTP / framework adapters

import { jacsMiddleware } from '@hai.ai/jacs/express';
import { jacsKoaMiddleware } from '@hai.ai/jacs/koa';
import { JACSExpressMiddleware, JACSKoaMiddleware } from '@hai.ai/jacs/http';  // legacy

TypeScript Support

The package includes full TypeScript definitions:

import { JacsAgent, createConfig, hashString } from '@hai.ai/jacs';

// Create an agent instance
const agent: JacsAgent = new JacsAgent();

// Load configuration from file (async)
await agent.load('./jacs.config.json');

// Use utility functions
const hash: string = hashString('some data');

// Create a configuration string
const configJson: string = createConfig(
  undefined,           // jacs_use_security
  './jacs_data',       // jacs_data_directory
  './jacs_keys',       // jacs_key_directory
  undefined,           // jacs_agent_private_key_filename
  undefined,           // jacs_agent_public_key_filename
  'ring-Ed25519',      // jacs_agent_key_algorithm
  undefined,           // jacs_private_key_password
  undefined,           // jacs_agent_id_and_version
  'fs'                 // jacs_default_storage
);

Configuration

Basic Configuration

const config = {
  // Required fields
  jacs_data_directory: "./jacs_data",      // Where documents are stored
  jacs_key_directory: "./jacs_keys",       // Where keys are stored
  jacs_default_storage: "fs",              // Storage backend
  jacs_agent_key_algorithm: "ring-Ed25519",     // Signing algorithm
  
  // Optional fields
  jacs_agent_id_and_version: null,         // Existing agent to load
  jacs_agent_private_key_filename: "private.pem",
  jacs_agent_public_key_filename: "public.pem"
};

Configuration File

Create a jacs.config.json file:

{
  "$schema": "https://hai.ai/schemas/jacs.config.schema.json",
  "jacs_data_directory": "./jacs_data",
  "jacs_key_directory": "./jacs_keys",
  "jacs_default_storage": "fs",
  "jacs_agent_key_algorithm": "ring-Ed25519"
}

Load the configuration:

import { JacsAgent } from '@hai.ai/jacs';

const agent = new JacsAgent();
agent.load('./jacs.config.json');

Environment Variables

JACS reads environment variables that override configuration file settings:

export JACS_DATA_DIRECTORY="./production_data"
export JACS_KEY_DIRECTORY="./production_keys"
export JACS_AGENT_KEY_ALGORITHM="ring-Ed25519"
export JACS_DEFAULT_STORAGE="fs"

Storage Backends

Configure storage in jacs.config.json:

File System (Default)

{
  "jacs_default_storage": "fs",
  "jacs_data_directory": "./jacs_data",
  "jacs_key_directory": "./jacs_keys"
}

Local Indexed SQLite

{
  "jacs_default_storage": "rusqlite"
}

Use rusqlite when you want local full-text search and the upgraded DocumentService behavior in bindings and MCP.

AWS Storage

{
  "jacs_default_storage": "aws"
}

AWS credentials are read from standard AWS environment variables.

Memory Storage (Testing)

{
  "jacs_default_storage": "memory"
}

Cryptographic Algorithms

{
  "jacs_agent_key_algorithm": "ring-Ed25519"
}

Pros: Fast, secure, small signatures Cons: Requires elliptic curve support

pq2025 (Post-Quantum)

{
  "jacs_agent_key_algorithm": "pq2025"
}

Pros: FIPS-204 ML-DSA-87 post-quantum signatures Cons: Larger signatures than Ed25519

Development Setup

Project Structure

my-jacs-project/
├── package.json
├── jacs.config.json
├── src/
│   ├── agent.js
│   ├── tasks.js
│   └── agreements.js
├── jacs_data/
│   ├── agents/
│   ├── tasks/
│   └── documents/
└── jacs_keys/
    ├── private.pem
    └── public.pem

Package.json Setup

{
  "name": "my-jacs-app",
  "version": "1.0.0",
  "type": "module",
  "dependencies": {
    "@hai.ai/jacs": "^0.6.0",
    "express": "^4.18.0"
  },
  "scripts": {
    "start": "node src/app.js",
    "test": "node test/test.js",
    "dev": "nodemon src/app.js"
  }
}

Basic Application

// src/app.js
import { JacsAgent } from '@hai.ai/jacs';

async function main() {
  const agent = new JacsAgent();
  await agent.load('./jacs.config.json');

  const documentJson = JSON.stringify({
    title: "My First Document",
    content: "Hello from Node.js!"
  });

  const signedDoc = await agent.createDocument(documentJson);
  console.log('Document created:', signedDoc);

  const isValid = await agent.verifyDocument(signedDoc);
  console.log('Document valid:', isValid);
  console.log('JACS agent ready!');
}
main().catch(console.error);

Common Issues

Module Not Found

If you get Module not found errors:

# Check Node.js version
node --version  # Should be 20+

# Clear node_modules and reinstall
rm -rf node_modules package-lock.json
npm install

Permission Errors

If you get permission errors accessing files:

# Check directory permissions
ls -la jacs_data/ jacs_keys/

# Fix permissions
chmod 755 jacs_data/ jacs_keys/
chmod 600 jacs_keys/*.pem

Binary Compatibility

If you get binary compatibility errors:

# Rebuild native modules
npm rebuild

# Or reinstall
npm uninstall @hai.ai/jacs
npm install @hai.ai/jacs

TypeScript Issues

If TypeScript can't find definitions:

// tsconfig.json
{
  "compilerOptions": {
    "moduleResolution": "node",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true
  }
}

Next Steps

Now that you have JACS installed:

  1. Basic Usage - Learn core JACS operations
  2. MCP Integration - Add Model Context Protocol support
  3. HTTP Server - Create JACS HTTP APIs
  4. Express Middleware - Integrate with Express.js
  5. API Reference - Complete API documentation

Examples

Check out the complete examples in the examples directory:

  • Basic agent creation and task management
  • Express.js middleware integration
  • MCP server implementation

Simplified API

Registry status (observed 2026-07-11): npm serves @hai.ai/jacs@0.10.1, while these pages describe source 0.11.4. The registry package does not expose every method shown here. Pin the version and inspect its exports; use a source build only when that is an explicit deployment choice.

The simplified API (@hai.ai/jacs/simple) provides a streamlined, module-level interface for common JACS operations. It's designed to get you signing and verifying in under 2 minutes.

v0.7.0: Async-First API

All NAPI operations now return Promises by default. Sync variants are available with a Sync suffix, following the Node.js convention (like fs.readFile vs fs.readFileSync).

// Async (default, recommended -- does not block the event loop)
const signed = await jacs.signMessage({ action: 'approve' });

// Sync (blocks event loop, use in scripts or CLI tools)
const signed = jacs.signMessageSync({ action: 'approve' });

Quick Start

Quickstart -- one call (with required name/domain) to start signing:

const jacs = require('@hai.ai/jacs/simple');

const info = await jacs.quickstart({
  name: 'my-agent',
  domain: 'my-agent.example.com',
});
console.log(info.configPath, info.publicKeyPath, info.privateKeyPath);
const signed = await jacs.signMessage({ action: 'approve', amount: 100 });
const result = await jacs.verify(signed.raw);
console.log(`Valid: ${result.valid}, Signer: ${result.signerId}`);

quickstart(name, domain, ...) creates a persistent agent with keys on disk (default algorithm: pq2025). If ./jacs.config.json already exists, it loads it; otherwise it creates a new agent. Returned AgentInfo includes config/key paths (config_path/public_key_path/private_key_path in Python, configPath/publicKeyPath/privateKeyPath in Node) plus key directory metadata so you can locate key material immediately. Rust/CLI quickstart requires an explicit password source (JACS_PRIVATE_KEY_PASSWORD, or JACS_PASSWORD_FILE in CLI). Python/Node can auto-generate a password if needed; set JACS_SAVE_PASSWORD_FILE=true if you want that generated password persisted to ./jacs_keys/.jacs_password. Pass { algorithm: 'ed25519' } to override the default (pq2025).

To load an existing agent explicitly, use load() instead:

const agent = await jacs.load('./jacs.config.json');
const signed = await jacs.signMessage({ action: 'approve', amount: 100 });

When to Use the Simplified API

Simplified APIJacsAgent Class
Quick prototypingMultiple agents in one process
Scripts and CLI toolsComplex multi-document workflows
MCP tool implementationsFine-grained control
Single-agent applicationsCustom error handling

API Reference

Every function that calls into NAPI has both async (default) and sync variants:

FunctionSync VariantDescription
quickstart(options)quickstartSync(options)Create a persistent agent with keys on disk
create(options)createSync(options)Create a new agent programmatically
load(configPath)loadSync(configPath)Load agent from config file
verifySelf()verifySelfSync()Verify agent's own integrity
updateAgent(data)updateAgentSync(data)Update agent document
updateDocument(id, data)updateDocumentSync(id, data)Update existing document
signMessage(data)signMessageSync(data)Sign any JSON data
signFile(path, embed)signFileSync(path, embed)Sign a file
verify(doc)verifySync(doc)Verify signed document
verifyById(id)verifyByIdSync(id)Verify by storage ID
reencryptKey(old, new)reencryptKeySync(old, new)Re-encrypt private key
createAgreement(doc, ids, ...)createAgreementSync(doc, ids, ...)Create multi-party agreement
signAgreement(doc)signAgreementSync(doc)Sign an agreement
checkAgreement(doc)checkAgreementSync(doc)Check agreement status
audit(options?)auditSync(options?)Run a security audit

Pure sync functions (no NAPI call, no suffix needed):

FunctionDescription
verifyStandalone(doc, opts?)Verify without loading an agent
getPublicKey()Get public key
isLoaded()Check if agent is loaded
getDnsRecord(domain, ttl?)Get DNS TXT record
getWellKnownJson()Get well-known JSON
trustAgent(json)Add agent to trust store
listTrustedAgents()List trusted agent IDs
untrustAgent(id)Remove from trust store
isTrusted(id)Check if agent is trusted
getTrustedAgent(id)Get trusted agent's JSON

quickstart(options)

Create a persistent agent with keys on disk. If ./jacs.config.json already exists, loads it. Otherwise creates a new agent, saving keys and config to disk. If JACS_PRIVATE_KEY_PASSWORD is unset, Node quickstart auto-generates a secure password in-process (JACS_SAVE_PASSWORD_FILE=true persists it to ./jacs_keys/.jacs_password). Call this once before signMessage() or verify().

Parameters:

  • options (object, required fields): { name: string, domain: string, description?: string, algorithm?: string, configPath?: string }. Default algorithm: "pq2025". User-facing choices: "ed25519", "pq2025"; "ring-Ed25519" is accepted as a legacy alias.

Returns: Promise<AgentInfo> (async) or AgentInfo (sync)

const info = await jacs.quickstart({
  name: 'my-agent',
  domain: 'my-agent.example.com',
});
console.log(`Agent ID: ${info.agentId}`);
console.log(`Config path: ${info.configPath}`);
console.log(`Public key: ${info.publicKeyPath}`);
console.log(`Private key: ${info.privateKeyPath}`);

// Or with a specific algorithm
const info = await jacs.quickstart({
  name: 'my-agent',
  domain: 'my-agent.example.com',
  algorithm: 'ed25519',
});

// Sync variant (blocks event loop)
const info = jacs.quickstartSync({
  name: 'my-agent',
  domain: 'my-agent.example.com',
  algorithm: 'ed25519',
});

load(configPath?)

Load a persistent agent from a configuration file. Use this instead of quickstart(options) when you want to load a specific config file explicitly.

Parameters:

  • configPath (string, optional): Path to jacs.config.json (default: "./jacs.config.json")

Returns: Promise<AgentInfo> (async) or AgentInfo (sync)

const info = await jacs.load('./jacs.config.json');
console.log(`Agent ID: ${info.agentId}`);

// Sync variant
const info = jacs.loadSync('./jacs.config.json');

isLoaded()

Check if an agent is currently loaded.

Returns: boolean

if (!jacs.isLoaded()) {
  await jacs.load('./jacs.config.json');
}

getAgentInfo()

Get information about the currently loaded agent.

Returns: AgentInfo or null if no agent is loaded

const info = jacs.getAgentInfo();
if (info) {
  console.log(`Agent: ${info.agentId}`);
}

verifySelf()

Verify the loaded agent's own integrity (signature and hash).

Returns: Promise<VerificationResult> (async) or VerificationResult (sync)

Throws: Error if no agent is loaded

const result = await jacs.verifySelf();
if (result.valid) {
  console.log('Agent integrity verified');
} else {
  console.log('Errors:', result.errors);
}

signMessage(data)

Sign arbitrary data as a JACS document.

Parameters:

  • data (any): Object, array, string, or any JSON-serializable value

Returns: Promise<SignedDocument> (async) or SignedDocument (sync)

Throws: Error if no agent is loaded

// Async (recommended)
const signed = await jacs.signMessage({
  action: 'transfer',
  amount: 500,
  recipient: 'agent-123'
});

// Sync
const signed = jacs.signMessageSync({
  action: 'transfer',
  amount: 500,
  recipient: 'agent-123'
});

console.log(`Document ID: ${signed.documentId}`);
console.log(`Signed by: ${signed.agentId}`);

signFile(filePath, embed?)

Sign a file with optional content embedding.

Parameters:

  • filePath (string): Path to the file to sign
  • embed (boolean, optional): If true, embed file content in the document (default: false)

Returns: Promise<SignedDocument> (async) or SignedDocument (sync)

// Reference only (stores hash)
const signed = await jacs.signFile('contract.pdf', false);

// Embed content (creates portable document)
const embedded = await jacs.signFile('contract.pdf', true);

verify(signedDocument)

Verify a signed document and extract its content.

Parameters:

  • signedDocument (string): The JSON string of the signed document

Returns: Promise<VerificationResult> (async) or VerificationResult (sync)

const result = await jacs.verify(signedJson);

if (result.valid) {
  console.log(`Signed by: ${result.signerId}`);
  console.log(`Data: ${JSON.stringify(result.data)}`);
} else {
  console.log(`Invalid: ${result.errors.join(', ')}`);
}

verifyStandalone(signedDocument, options?)

Verify a signed document without loading an agent. Use when you only need to verify (e.g. a lightweight API). Does not use the global agent.

Parameters:

  • signedDocument (string): The signed JACS document JSON
  • options (object, optional): { keyResolution?, dataDirectory?, keyDirectory? }

Returns: VerificationResult (always sync -- no NAPI call)

const result = jacs.verifyStandalone(signedJson, { keyResolution: 'local', keyDirectory: './keys' });
console.log(result.valid, result.signerId);

HTTP protocol helpers (JacsSimpleAgent instances)

The module-level async API focuses on durable documents. Request-bound HTTP auth and signed event methods live on the synchronous instance class:

const { JacsSimpleAgent } = require('@hai.ai/jacs');

const agent = JacsSimpleAgent.ephemeral('ed25519');
const body = '{"action":"approve"}';
const authorization = agent.buildRequestAuthHeader(
  'POST',
  'https://api.example.com/v1/jobs?mode=strict',
  body,
  'jobs-api',
);

const envelope = agent.signResponse(JSON.stringify({ decision: 'allow' }));
const signerId = JSON.parse(envelope).jacsSignature.agentID;
const keys = JSON.stringify({ [signerId]: agent.getPublicKeyPem() });
const verified = JSON.parse(agent.unwrapSignedEvent(envelope, keys));
if (!authorization.startsWith('JACS v2.') || !verified.verified) {
  throw new Error('protocol verification failed');
}
  • buildRequestAuthHeader(method, url, body, audience) binds the normalized method, absolute URL including query, exact UTF-8 body bytes, audience, signer/key, issue time, and nonce. Stringify the final request body first and send it unchanged.
  • signResponse(payloadJson) returns a 2.0.0 envelope whose jacs-response-v2 signature covers payload and all metadata.
  • unwrapSignedEvent(eventJson, serverKeysJson) returns JSON containing verified data, signerId, timestamp, algorithm, and documentId. Plain/unsigned input, legacy payload-only envelopes, unknown signers, and any mutation throw; it never successfully returns verified: false.

The server key map is the application's configured trust decision. Signature verification proves possession of the corresponding key, not a real-world identity by itself.

Legacy no-argument buildAuthHeader() remains available for compatibility and emits a WARN. Migrate both peers to v2; strict deployments can reject the legacy method with JACS_REJECT_UNBOUND_AUTH_HEADER=true. See Security.


signText(path)

Append a YAML-bodied JACS signature block to the end of a markdown or text file. The file content (everything before the first signature block) is preserved byte-for-byte; multi-signer files are unordered. Async-first since v0.7.0 — returns Promise.

Parameters: path (string): file to sign in place

Returns: Promise<void>

Throws: MarkerCollision error if the file contains a column-zero -----BEGIN JACS SIGNATURE----- outside a valid block.

await jacs.signText('README.md');

verifyText(path, options?)

Verify all signature blocks in a text file. Permissive by default — missing signatures are a typed status, not an error.

Parameters:

  • path (string)
  • options (object, optional): { strict?: boolean, keyDir?: string }. When strict is true, the Promise rejects with a MissingSignature-bearing error if the file is unsigned. keyDir is a directory of <signer_id>.public.pem files used in addition to the local trust store.

Returns: Promise<VerifyTextResult>.status is 'signed' | 'missing_signature' | 'malformed'; .signers lists per-signer outcomes.

const result = await jacs.verifyText('README.md');
console.log(result.status);  // 'signed'

// Strict — Promise rejects on missing signature
try {
  await jacs.verifyText('UNSIGNED.md', { strict: true });
} catch (err) {
  if (/MissingSignature/.test(err.message)) {
    console.log('not signed');
  } else {
    throw err;
  }
}

See the inline text signing guide for the full feature set.


signImage(path, out, options?)

Embed a JACS signature inside a PNG, JPEG, or WebP image. The signature lives in a metadata chunk (PNG iTXt / JPEG APP11 / WebP XMP).

Parameters:

  • path (string): input image
  • out (string): output path
  • options (object, optional): { robust?: boolean, refuseOverwrite?: boolean }. robust opts in to LSB fallback for PNG/JPEG (off by default; WebP deferred). refuseOverwrite rejects with AlreadySigned if the input already carries a signature.

Returns: Promise<void>

await jacs.signImage('photo.png', 'signed.png');

verifyImage(path, options?)

Verify the embedded signature on an image. Same permissive / strict model as verifyText.

Parameters: as for verifyText.

Returns: Promise<VerifyImageResult>.status is 'valid' | 'invalid' | 'missing_signature'; .signerId, .signedAt, .algorithm populated when valid.

const v = await jacs.verifyImage('signed.png');
console.log(v.status);  // 'valid'

See the media signing guide for the full feature set.


extractMediaSignature(path, options?)

Extract the embedded signature payload without verifying it.

Parameters:

  • path (string)
  • options (object, optional): { rawPayload?: boolean } — when true, returns the base64url wire form instead of decoded JSON.

Returns: Promise<object> (decoded JSON) or Promise<string> (base64url when rawPayload: true).

const payload = await jacs.extractMediaSignature('signed.png');
console.log(payload.signedAt);

audit(options?)

Run a read-only security audit and health checks. Returns an object with risks, health_checks, summary, and overall_status. Does not require a loaded agent; does not modify state.

Parameters: options (object, optional): { configPath?, recentN? }

Returns: Promise<object> (async) or object (sync)

See Security Model -- Security Audit for full details and options.

const result = await jacs.audit();
console.log(`Risks: ${result.risks.length}, Status: ${result.overall_status}`);

updateAgent(newAgentData)

Update the agent document with new data and re-sign it.

This function expects a complete agent document (not partial updates). Use exportAgent() to get the current document, modify it, then pass it here.

Parameters:

  • newAgentData (object|string): Complete agent document as JSON string or object

Returns: Promise<string> (async) or string (sync) -- The updated and re-signed agent document

const agentDoc = JSON.parse(jacs.exportAgent());
agentDoc.jacsAgentType = 'hybrid';
const updated = await jacs.updateAgent(agentDoc);

updateDocument(documentId, newDocumentData, attachments?, embed?)

Update an existing document with new data and re-sign it.

Parameters:

  • documentId (string): The document ID (jacsId) to update
  • newDocumentData (object|string): Updated document as JSON string or object
  • attachments (string[], optional): Array of file paths to attach
  • embed (boolean, optional): If true, embed attachment contents

Returns: Promise<SignedDocument> (async) or SignedDocument (sync)

const original = await jacs.signMessage({ status: 'pending', amount: 100 });
const doc = JSON.parse(original.raw);
doc.content.status = 'approved';
const updated = await jacs.updateDocument(original.documentId, doc);

exportAgent()

Export the current agent document for sharing or inspection.

Returns: string -- The agent JSON document (pure sync, no suffix needed)

const agentDoc = jacs.exportAgent();
const agent = JSON.parse(agentDoc);
console.log(`Agent type: ${agent.jacsAgentType}`);

getDnsRecord(domain, ttl?)

Return the DNS TXT record line for the loaded agent. Pure sync, no suffix needed.

Parameters: domain (string), ttl (number, optional, default 3600)

Returns: string


getWellKnownJson()

Return the well-known JSON object for the loaded agent. Pure sync, no suffix needed.

Returns: object


getPublicKey()

Get the loaded agent's public key in PEM format. Pure sync, no suffix needed.

Returns: string -- PEM-encoded public key

const pem = jacs.getPublicKey();
console.log(pem);

Type Definitions

AgentInfo

interface AgentInfo {
  agentId: string;      // Agent's UUID
  name: string;         // Agent name from config
  publicKeyPath: string; // Path to public key file
  configPath: string;   // Path to loaded config
}

SignedDocument

interface SignedDocument {
  raw: string;          // Full JSON document with signature
  documentId: string;   // Document's UUID (jacsId)
  agentId: string;      // Signing agent's ID
  timestamp: string;    // ISO 8601 timestamp
}

VerificationResult

interface VerificationResult {
  valid: boolean;       // True if signature verified
  data?: any;           // Extracted document content
  signerId: string;     // Agent who signed
  timestamp: string;    // When it was signed
  attachments: Attachment[];  // File attachments
  errors: string[];     // Error messages if invalid
}

Attachment

interface Attachment {
  filename: string;     // Original filename
  mimeType: string;     // MIME type
  hash: string;         // SHA-256 hash
  embedded: boolean;    // True if content is embedded
  content?: Buffer;     // Embedded content (if available)
}

Complete Example

const jacs = require('@hai.ai/jacs/simple');

// Load agent
const agent = await jacs.load('./jacs.config.json');
console.log(`Loaded agent: ${agent.agentId}`);

// Verify agent integrity
const selfCheck = await jacs.verifySelf();
if (!selfCheck.valid) {
  throw new Error('Agent integrity check failed');
}

// Sign a transaction
const transaction = {
  type: 'payment',
  from: agent.agentId,
  to: 'recipient-agent-uuid',
  amount: 250.00,
  currency: 'USD',
  memo: 'Q1 Service Payment'
};

const signed = await jacs.signMessage(transaction);
console.log(`Transaction signed: ${signed.documentId}`);

// Verify the transaction (simulating recipient)
const verification = await jacs.verify(signed.raw);

if (verification.valid) {
  console.log(`Payment verified from: ${verification.signerId}`);
  console.log(`Amount: ${verification.data.amount} ${verification.data.currency}`);
} else {
  console.log(`Verification failed: ${verification.errors.join(', ')}`);
}

// Sign a file
const contractSigned = await jacs.signFile('./contract.pdf', true);
console.log(`Contract signed: ${contractSigned.documentId}`);

// Update agent metadata
const agentDoc = JSON.parse(jacs.exportAgent());
agentDoc.jacsAgentType = 'ai';
const updatedAgent = await jacs.updateAgent(agentDoc);
console.log('Agent metadata updated');

// Share public key
const publicKey = jacs.getPublicKey();
console.log('Share this public key for verification:');
console.log(publicKey);

MCP Integration

The simplified API works well with MCP tool implementations:

const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
const jacs = require('@hai.ai/jacs/simple');

// Load agent once at startup
await jacs.load('./jacs.config.json');

// Define a signed tool
server.setRequestHandler('tools/call', async (request) => {
  const { name, arguments: args } = request.params;

  if (name === 'approve_request') {
    const signed = await jacs.signMessage({
      action: 'approve',
      requestId: args.requestId,
      approvedBy: jacs.getAgentInfo().agentId
    });

    return {
      content: [{ type: 'text', text: signed.raw }]
    };
  }
});

Error Handling

const jacs = require('@hai.ai/jacs/simple');

try {
  await jacs.load('./missing-config.json');
} catch (e) {
  console.error('Config not found:', e.message);
}

try {
  // Will fail if no agent loaded
  await jacs.signMessage({ data: 'test' });
} catch (e) {
  console.error('No agent:', e.message);
}

try {
  await jacs.signFile('/nonexistent/file.pdf');
} catch (e) {
  console.error('File not found:', e.message);
}

// Verification doesn't throw - check result.valid
const result = await jacs.verify('invalid json');
if (!result.valid) {
  console.error('Verification errors:', result.errors);
}

See Also

Basic Usage

Registry status (observed 2026-07-11): npm serves @hai.ai/jacs@0.10.1, while these pages describe source 0.11.4. The registry package does not expose every method shown here. Pin the version and inspect its exports; use a source build only when that is an explicit deployment choice.

This chapter covers fundamental JACS operations in Node.js, including agent initialization, document creation, signing, and verification.

v0.7.0: Async-First API

All NAPI operations now return Promises by default. Sync variants are available with a Sync suffix, following the Node.js convention (like fs.readFile vs fs.readFileSync).

// Async (default, recommended)
await agent.load('./jacs.config.json');
const doc = await agent.createDocument(JSON.stringify(content));

// Sync (blocks event loop)
agent.loadSync('./jacs.config.json');
const doc = agent.createDocumentSync(JSON.stringify(content));

Initializing an Agent

Create and Load Agent

import { JacsAgent } from '@hai.ai/jacs';

// Create a new agent instance
const agent = new JacsAgent();

// Load configuration from file (async)
await agent.load('./jacs.config.json');

// Or use sync variant
agent.loadSync('./jacs.config.json');

Configuration File

Create jacs.config.json:

{
  "$schema": "https://hai.ai/schemas/jacs.config.schema.json",
  "jacs_data_directory": "./jacs_data",
  "jacs_key_directory": "./jacs_keys",
  "jacs_default_storage": "fs",
  "jacs_agent_key_algorithm": "ring-Ed25519",
  "jacs_agent_id_and_version": "agent-uuid:version-uuid"
}

Creating Documents

Basic Document Creation

import { JacsAgent } from '@hai.ai/jacs';

const agent = new JacsAgent();
await agent.load('./jacs.config.json');

// Create a document from JSON
const documentData = {
  title: "Project Proposal",
  content: "Quarterly development plan",
  budget: 50000
};

const signedDocument = await agent.createDocument(JSON.stringify(documentData));
console.log('Signed document:', signedDocument);

With Custom Schema

Validate against a custom JSON Schema:

const signedDocument = await agent.createDocument(
  JSON.stringify(documentData),
  './schemas/proposal.schema.json'  // custom schema path
);

With Output File

const signedDocument = await agent.createDocument(
  JSON.stringify(documentData),
  null,                    // no custom schema
  './output/proposal.json' // output filename
);

Without Saving

const signedDocument = await agent.createDocument(
  JSON.stringify(documentData),
  null,   // no custom schema
  null,   // no output filename
  true    // noSave = true
);

With Attachments

const signedDocument = await agent.createDocument(
  JSON.stringify(documentData),
  null,                      // no custom schema
  null,                      // no output filename
  false,                     // save the document
  './attachments/report.pdf', // attachment path
  true                       // embed files
);

Verifying Documents

Verify Document Signature

// Verify a document's signature and hash
const isValid = await agent.verifyDocument(signedDocumentJson);
console.log('Document valid:', isValid);

Verify Specific Signature Field

// Verify with a custom signature field
const isValid = await agent.verifySignature(
  signedDocumentJson,
  'jacsSignature'  // signature field name
);

Updating Documents

Update Existing Document

// Original document key format: "id:version"
const documentKey = 'doc-uuid:version-uuid';

// Modified document content
const updatedData = {
  jacsId: 'doc-uuid',
  jacsVersion: 'version-uuid',
  title: "Updated Proposal",
  content: "Revised quarterly plan",
  budget: 75000
};

const updatedDocument = await agent.updateDocument(
  documentKey,
  JSON.stringify(updatedData)
);

console.log('Updated document:', updatedDocument);

Update with New Attachments

const updatedDocument = await agent.updateDocument(
  documentKey,
  JSON.stringify(updatedData),
  ['./new-report.pdf'],  // new attachments
  true                   // embed files
);

Signing and Verification

Sign Arbitrary Data

// Sign any string data
const signature = await agent.signString('Important message to sign');
console.log('Signature:', signature);

Verify Arbitrary Data

// Verify a signature on string data
const isValid = await agent.verifyString(
  'Important message to sign',  // original data
  signatureBase64,              // base64 signature
  publicKeyBuffer,              // public key as Buffer
  'ring-Ed25519'                // algorithm
);

Working with Agreements

The methods in this section use the legacy jacsAgreement sidecar on an existing signed document. For new standalone consent workflows, prefer Agreement v2 through createAgreementV2(), signAgreementV2(), and verifyAgreementV2().

Create an Agreement

// Add agreement requiring multiple agent signatures
const documentWithAgreement = await agent.createAgreement(
  signedDocumentJson,
  ['agent1-uuid', 'agent2-uuid'],           // required signers
  'Do you agree to these terms?',            // question
  'Q1 2024 service contract',                // context
  'jacsAgreement'                            // field name
);

Sign an Agreement

// Sign the agreement as the current agent
const signedAgreement = await agent.signAgreement(
  documentWithAgreementJson,
  'jacsAgreement'  // agreement field name
);

Check Agreement Status

// Check which agents have signed
const status = await agent.checkAgreement(
  documentWithAgreementJson,
  'jacsAgreement'
);

console.log('Agreement status:', JSON.parse(status));

Agent Operations

Verify Agent

// Verify the loaded agent's signature
const isValid = await agent.verifyAgent();
console.log('Agent valid:', isValid);

Update Agent

// Update agent document
const updatedAgentJson = await agent.updateAgent(JSON.stringify({
  jacsId: 'agent-uuid',
  jacsVersion: 'version-uuid',
  name: 'Updated Agent Name',
  description: 'Updated description'
}));

Sign External Agent

// Sign another agent's document with registration signature
const signedAgentJson = await agent.signAgent(
  externalAgentJson,
  publicKeyBuffer,
  'ring-Ed25519'
);

Request/Response Signing

These methods remain synchronous (V8-thread-only, no Sync suffix):

Sign a Request

// Sign request parameters as a JACS document
const signedRequest = agent.signRequest({
  method: 'GET',
  path: '/api/resource',
  timestamp: new Date().toISOString(),
  body: { query: 'data' }
});

Verify a Response

// Verify a signed response
const result = agent.verifyResponse(signedResponseJson);
console.log('Response valid:', result);

// Verify and get signer's agent ID
const resultWithId = agent.verifyResponseWithAgentId(signedResponseJson);
console.log('Signer ID:', resultWithId);

Utility Functions

Hash String

import { hashString } from '@hai.ai/jacs';

// SHA-256 hash of a string
const hash = hashString('data to hash');
console.log('Hash:', hash);

Create Configuration

import { createConfig } from '@hai.ai/jacs';

// Programmatically create a config JSON string
const configJson = createConfig(
  undefined,            // jacs_use_security
  './jacs_data',        // jacs_data_directory
  './jacs_keys',        // jacs_key_directory
  undefined,            // private key filename
  undefined,            // public key filename
  'ring-Ed25519',       // key algorithm
  undefined,            // private key password
  undefined,            // agent id and version
  'fs'                  // default storage
);

console.log('Config:', configJson);

Error Handling

import { JacsAgent } from '@hai.ai/jacs';

const agent = new JacsAgent();

try {
  await agent.load('./jacs.config.json');
} catch (error) {
  console.error('Failed to load agent:', error.message);
}

try {
  const doc = await agent.createDocument(JSON.stringify({ data: 'test' }));
  console.log('Document created');
} catch (error) {
  console.error('Failed to create document:', error.message);
}

try {
  const isValid = await agent.verifyDocument(invalidJson);
} catch (error) {
  console.error('Verification failed:', error.message);
}

Complete Example

import { JacsAgent, hashString } from '@hai.ai/jacs';

async function main() {
  // Initialize agent
  const agent = new JacsAgent();
  await agent.load('./jacs.config.json');

  // Create a generic proposal document
  const proposal = {
    title: 'Project Proposal',
    description: 'Q1 development plan',
    budget: 50000
  };

  const signedProposal = await agent.createDocument(JSON.stringify(proposal));
  console.log('Document created');

  // Verify the document
  if (await agent.verifyDocument(signedProposal)) {
    console.log('Document signature valid');
  }

  // Create agreement for proposal approval
  const proposalWithAgreement = await agent.createAgreement(
    signedProposal,
    ['manager-uuid', 'developer-uuid'],
    'Do you approve this proposal?'
  );

  // Sign the agreement
  const signedAgreement = await agent.signAgreement(proposalWithAgreement);
  console.log('Agreement signed');

  // Check agreement status
  const status = await agent.checkAgreement(signedAgreement);
  console.log('Status:', status);

  // Hash some data for reference
  const proposalHash = hashString(signedProposal);
  console.log('Document hash:', proposalHash);
}

main().catch(console.error);

Next Steps

MCP Integration (Node.js)

Registry status (observed 2026-07-11): npm serves @hai.ai/jacs@0.10.1, while these pages describe source 0.11.4. The registry package does not expose every method shown here. Pin the version and inspect its exports; use a source build only when that is an explicit deployment choice.

Node has two MCP stories:

  1. Wrap an MCP transport with signing and verification
  2. Register JACS operations as MCP tools on an existing server

If you want a full out-of-the-box server instead, install jacs-cli and run the built-in jacs mcp server.

Install

npm install @hai.ai/jacs @modelcontextprotocol/sdk

1. Wrap A Transport

Use this when you already have an MCP server or client and want signed JSON-RPC messages.

With a loaded client

import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { JacsClient } from '@hai.ai/jacs/client';
import { createJACSTransportProxy } from '@hai.ai/jacs/mcp';

const client = await JacsClient.quickstart({
  name: 'mcp-agent',
  domain: 'mcp.local',
});

const transport = new StdioServerTransport();
const secureTransport = createJACSTransportProxy(transport, client, 'server');

With only a config path

import { createJACSTransportProxyAsync } from '@hai.ai/jacs/mcp';

const secureTransport = await createJACSTransportProxyAsync(
  transport,
  './jacs.config.json',
  'server',
);

createJACSTransportProxy() does not take a config path. Use the async factory when the agent is not already loaded.

2. Register JACS Tools On Your MCP Server

Use this when the model should explicitly call JACS operations such as signing, verification, agreement creation, or trust-store inspection.

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { JacsClient } from '@hai.ai/jacs/client';
import { registerJacsTools } from '@hai.ai/jacs/mcp';

const server = new Server(
  { name: 'jacs-tools', version: '1.0.0' },
  { capabilities: { tools: {} } },
);

const client = await JacsClient.quickstart({
  name: 'mcp-agent',
  domain: 'mcp.local',
});

registerJacsTools(server, client);

The registered tool set includes:

  • document signing and verification
  • agreement helpers
  • agent-info helpers
  • trust-store helpers
  • setup and registry helper stubs

For lower-level integration, use getJacsMcpToolDefinitions() plus handleJacsMcpToolCall().

Failure Behavior

The transport proxy is not permissive by default.

  • Signing or verification failures fail closed unless you explicitly pass allowUnsignedFallback: true
  • createJACSTransportProxy() expects a real JacsClient or JacsAgent, not an unloaded shell

Common Pattern

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { JacsClient } from '@hai.ai/jacs/client';
import { createJACSTransportProxy } from '@hai.ai/jacs/mcp';

const client = await JacsClient.quickstart({
  name: 'my-agent',
  domain: 'my-agent.example.com',
});

const server = new McpServer({ name: 'my-server', version: '1.0.0' });
const transport = new StdioServerTransport();
const secureTransport = createJACSTransportProxy(transport, client, 'server');

await server.connect(secureTransport);

For stdio servers, keep logs on stderr, not stdout.

Example Paths In This Repo

  • jacsnpm/examples/mcp.stdio.server.js
  • jacsnpm/examples/mcp.stdio.client.js
  • jacsnpm/examples/mcp.sse.server.js
  • jacsnpm/examples/mcp.sse.client.js

When To Use LangChain Instead

Choose LangChain.js Integration instead when:

  • the model and tools already live in the same Node.js process
  • you only need signed tool outputs, not an MCP boundary
  • you do not need other MCP clients to connect

LangChain.js Integration

Registry status (observed 2026-07-11): npm serves @hai.ai/jacs@0.10.1, while these pages describe source 0.11.4. The registry package does not expose every method shown here. Pin the version and inspect its exports; use a source build only when that is an explicit deployment choice.

Use the LangChain.js adapter when the model already runs inside your Node.js app and you want provenance at the tool boundary.

Choose The Pattern

Give The Agent JACS Tools

Use createJacsTools() when the model should explicitly ask to sign, verify, inspect trust, or create agreements.

import { JacsClient } from '@hai.ai/jacs/client';
import { createJacsTools } from '@hai.ai/jacs/langchain';

const client = await JacsClient.quickstart({
  name: 'my-agent',
  domain: 'my-agent.example.com',
});

const jacsTools = createJacsTools({ client });
const llmWithTools = model.bindTools([...myTools, ...jacsTools]);

The tool set includes 14 tools:

  • jacs_sign
  • jacs_verify
  • jacs_create_agreement
  • jacs_sign_agreement
  • jacs_check_agreement
  • jacs_verify_self
  • jacs_trust_agent
  • jacs_trust_agent_with_key
  • jacs_list_trusted
  • jacs_is_trusted
  • jacs_share_public_key
  • jacs_share_agent
  • jacs_agent_info

Auto-Sign Existing Tools

Use this when the model should keep using your existing tool set but every result needs a signature.

Wrap one tool:

import { signedTool } from '@hai.ai/jacs/langchain';

const signed = signedTool(mySearchTool, { client });

Wrap a LangGraph ToolNode:

import { jacsToolNode } from '@hai.ai/jacs/langchain';

const node = jacsToolNode([tool1, tool2], { client });

For custom graph logic:

import { jacsWrapToolCall } from '@hai.ai/jacs/langchain';

const wrapToolCall = jacsWrapToolCall({ client });

Install

npm install @hai.ai/jacs @langchain/core
npm install @langchain/langgraph

@langchain/langgraph is only required for jacsToolNode().

Strict Mode

Pass strict: true when you want wrapper failures to throw instead of returning error-shaped output:

const jacsTools = createJacsTools({ client, strict: true });

Examples In This Repo

  • jacsnpm/examples/langchain/basic-agent.ts
  • jacsnpm/examples/langchain/signing-callback.ts

When To Use MCP Instead

Choose Node.js MCP Integration instead when:

  • the model is outside your process and connects over MCP
  • you want a shared MCP server usable by multiple clients
  • you need transport-level signing in addition to signed tool outputs

Vercel AI SDK

Registry status (observed 2026-07-11): npm serves @hai.ai/jacs@0.10.1, while these pages describe source 0.11.4. The registry package does not expose every method shown here. Pin the version and inspect its exports; use a source build only when that is an explicit deployment choice.

Sign it. Prove it. -- for every AI model output.

The JACS Vercel AI SDK adapter adds cryptographic provenance to AI-generated text and tool results using the LanguageModelV3Middleware pattern. Works with generateText, streamText, and any model provider (OpenAI, Anthropic, etc.).

5-Minute Quickstart

1. Install

npm install @hai.ai/jacs ai @ai-sdk/openai

2. Create a JACS client

import { JacsClient } from '@hai.ai/jacs/client';

const client = await JacsClient.quickstart({
  name: 'my-agent',
  domain: 'my-agent.example.com',
});

3. Sign every model output

import { withProvenance } from '@hai.ai/jacs/vercel-ai';
import { openai } from '@ai-sdk/openai';
import { generateText } from 'ai';

const model = withProvenance(openai('gpt-4'), { client });
const { text, providerMetadata } = await generateText({ model, prompt: 'Hello!' });

console.log(providerMetadata?.jacs?.text?.documentId); // JACS document ID

Quick Start

import { JacsClient } from '@hai.ai/jacs/client';
import { withProvenance } from '@hai.ai/jacs/vercel-ai';
import { openai } from '@ai-sdk/openai';
import { generateText } from 'ai';

const client = await JacsClient.quickstart({
  name: 'my-agent',
  domain: 'my-agent.example.com',
});
const model = withProvenance(openai('gpt-4'), { client });

const { text, providerMetadata } = await generateText({
  model,
  prompt: 'Summarize the quarterly report.',
});

console.log(text);
console.log(providerMetadata?.jacs?.text?.documentId); // JACS document ID
console.log(providerMetadata?.jacs?.text?.signed);      // true

Every model output is signed by your JACS agent. The provenance record is attached to providerMetadata.jacs.

Installation

npm install @hai.ai/jacs ai @ai-sdk/openai  # or any provider

The ai package is a peer dependency.

Two Ways to Use

withProvenance (convenience)

Wraps a model with the JACS middleware in one call:

import { withProvenance } from '@hai.ai/jacs/vercel-ai';

const model = withProvenance(openai('gpt-4'), { client });

jacsProvenance (composable)

Returns a LanguageModelV3Middleware you can compose with other middleware:

import { jacsProvenance } from '@hai.ai/jacs/vercel-ai';
import { wrapLanguageModel } from 'ai';

const provenance = jacsProvenance({ client });

const model = wrapLanguageModel({
  model: openai('gpt-4'),
  middleware: provenance,
});

Options

interface ProvenanceOptions {
  client: JacsClient;                  // Required: initialized JacsClient
  signText?: boolean;                  // Sign generated text (default: true)
  signToolResults?: boolean;           // Sign tool call results (default: true)
  strict?: boolean;                    // Throw on signing failure (default: false)
  metadata?: Record<string, unknown>;  // Extra metadata in provenance records
}

Streaming

Streaming works automatically. Text chunks are accumulated and signed when the stream completes:

import { streamText } from 'ai';

const result = streamText({
  model: withProvenance(openai('gpt-4'), { client }),
  prompt: 'Write a haiku.',
});

for await (const chunk of result.textStream) {
  process.stdout.write(chunk);
}

// Provenance is available after stream completes
const metadata = await result.providerMetadata;
console.log(metadata?.jacs?.text?.signed); // true

Tool Call Signing

When signToolResults is true (default), tool results in the prompt are signed:

import { generateText, tool } from 'ai';
import { z } from 'zod';

const { text, providerMetadata } = await generateText({
  model: withProvenance(openai('gpt-4'), { client }),
  tools: {
    getWeather: tool({
      parameters: z.object({ city: z.string() }),
      execute: async ({ city }) => `Weather in ${city}: sunny, 72F`,
    }),
  },
  prompt: 'What is the weather in Paris?',
});

// Both text output and tool results are signed
console.log(providerMetadata?.jacs?.text?.signed);
console.log(providerMetadata?.jacs?.toolResults?.signed);

Provenance Record

Each signed output produces a ProvenanceRecord:

interface ProvenanceRecord {
  signed: boolean;       // Whether signing succeeded
  documentId: string;    // JACS document ID
  agentId: string;       // Signing agent's ID
  timestamp: string;     // ISO 8601 timestamp
  error?: string;        // Error message if signing failed
  metadata?: Record<string, unknown>;
}

Access records from providerMetadata.jacs:

const { providerMetadata } = await generateText({ model, prompt: '...' });

const textProvenance = providerMetadata?.jacs?.text;
const toolProvenance = providerMetadata?.jacs?.toolResults;

Strict Mode

By default, signing failures are logged but do not throw. Enable strict to throw on failure:

const model = withProvenance(openai('gpt-4'), {
  client,
  strict: true,  // Throws if signing fails
});

Next Steps

Express Middleware

Registry status (observed 2026-07-11): npm serves @hai.ai/jacs@0.10.1, while these pages describe source 0.11.4. The registry package does not expose every method shown here. Pin the version and inspect its exports; use a source build only when that is an explicit deployment choice.

Sign it. Prove it. -- in your Express app.

JACS provides jacsMiddleware for Express v4/v5 that verifies incoming signed request bodies and optionally auto-signs JSON responses. No body-parser gymnastics, no monkey-patching.

5-Minute Quickstart

1. Install

npm install @hai.ai/jacs express

2. Create a JACS client

import { JacsClient } from '@hai.ai/jacs/client';

const client = await JacsClient.quickstart({
  name: 'my-agent',
  domain: 'my-agent.example.com',
});

3. Add signing middleware

import express from 'express';
import { jacsMiddleware } from '@hai.ai/jacs/express';

const app = express();
app.use(express.text({ type: 'application/json' }));
app.use(jacsMiddleware({ client, verify: true }));

app.post('/api/data', (req, res) => {
  console.log(req.jacsPayload); // verified payload
  res.json({ status: 'ok' });
});

app.listen(3000);

Quick Start

import express from 'express';
import { JacsClient } from '@hai.ai/jacs/client';
import { jacsMiddleware } from '@hai.ai/jacs/express';

const client = await JacsClient.quickstart({
  name: 'my-agent',
  domain: 'my-agent.example.com',
});
const app = express();

app.use(express.text({ type: 'application/json' }));
app.use(jacsMiddleware({ client, verify: true }));

app.post('/api/data', (req, res) => {
  console.log(req.jacsPayload); // verified payload
  res.json({ status: 'ok' });
});

app.listen(3000);

Options

jacsMiddleware({
  client?: JacsClient;      // Pre-initialized client (preferred)
  configPath?: string;       // Path to jacs.config.json (if no client)
  sign?: boolean;            // Auto-sign res.json() responses (default: false)
  verify?: boolean;          // Verify incoming POST/PUT/PATCH bodies (default: true)
  optional?: boolean;        // Allow unsigned requests through (default: false)
  authReplay?: boolean | {   // Replay protection for auth-style endpoints (default: false)
    enabled?: boolean;
    maxAgeSeconds?: number;    // default: 30
    clockSkewSeconds?: number; // default: 5
    cacheTtlSeconds?: number;  // default: maxAge + skew
  };
})

If neither client nor configPath is provided, the middleware initializes a client with JacsClient.quickstart({ name: 'jacs-express', domain: 'localhost' }) on first request.

What the Middleware Does

Every request gets req.jacsClient -- a JacsClient instance you can use for manual signing/verification in route handlers.

POST/PUT/PATCH with verify: true (default): The string body is verified as a JACS document. On success, req.jacsPayload contains the extracted payload. On failure, a 401 is returned (unless optional: true).

With sign: true: res.json() is intercepted to auto-sign the response body before sending.

Verify Incoming Requests

app.use(express.text({ type: 'application/json' }));
app.use(jacsMiddleware({ client }));

app.post('/api/process', (req, res) => {
  if (!req.jacsPayload) {
    return res.status(400).json({ error: 'Missing payload' });
  }

  const { action, data } = req.jacsPayload;
  res.json({ processed: true, action });
});

With optional: true, unsigned requests pass through with req.jacsPayload unset:

app.use(jacsMiddleware({ client, optional: true }));

app.post('/api/mixed', (req, res) => {
  if (req.jacsPayload) {
    // Verified JACS request
    res.json({ verified: true, data: req.jacsPayload });
  } else {
    // Unsigned request -- handle accordingly
    res.json({ verified: false });
  }
});

Auth Replay Protection (Auth Endpoints)

Enable replay protection when signed JACS bodies are used as authentication artifacts:

app.use(
  jacsMiddleware({
    client,
    verify: true,
    authReplay: { enabled: true, maxAgeSeconds: 30, clockSkewSeconds: 5 },
  })
);

When enabled, middleware enforces:

  • signature timestamp freshness (maxAgeSeconds + clockSkewSeconds)
  • single-use (signerId, signature) dedupe inside a TTL cache

Notes:

  • Keep this mode scoped to auth-style endpoints.
  • Cache is in-memory per process; use a shared cache for multi-instance deployments.

Auto-Sign Responses

Enable sign: true to intercept res.json() calls:

app.use(jacsMiddleware({ client, sign: true }));

app.post('/api/data', (req, res) => {
  // This response will be JACS-signed automatically
  res.json({ result: 42, timestamp: new Date().toISOString() });
});

Manual Signing in Routes

Use req.jacsClient for fine-grained control:

app.use(jacsMiddleware({ client }));

app.post('/api/custom', async (req, res) => {
  const result = processData(req.jacsPayload);

  // Sign manually
  const signed = await req.jacsClient.signMessage(result);
  res.type('application/json').send(signed.raw);
});

Per-Route Middleware

Apply JACS to specific routes only:

const app = express();
const jacs = jacsMiddleware({ client });

// Public routes -- no JACS
app.get('/health', (req, res) => res.json({ status: 'ok' }));

// Protected routes
app.use('/api', express.text({ type: 'application/json' }), jacs);

app.post('/api/secure', (req, res) => {
  res.json({ data: req.jacsPayload });
});

Multiple Agents

Use different JacsClient instances per route group:

const adminClient = await JacsClient.quickstart({
  name: 'admin-agent',
  domain: 'admin.example.com',
  algorithm: 'pq2025',
});
const userClient = await JacsClient.quickstart({
  name: 'user-agent',
  domain: 'user.example.com',
  algorithm: 'ring-Ed25519',
});

app.use('/admin', express.text({ type: 'application/json' }));
app.use('/admin', jacsMiddleware({ client: adminClient }));

app.use('/user', express.text({ type: 'application/json' }));
app.use('/user', jacsMiddleware({ client: userClient }));

Migration from JACSExpressMiddleware

The legacy JACSExpressMiddleware from @hai.ai/jacs/http still works but is deprecated. To migrate:

OldNew
import { JACSExpressMiddleware } from '@hai.ai/jacs/http'import { jacsMiddleware } from '@hai.ai/jacs/express'
JACSExpressMiddleware({ configPath: '...' })jacsMiddleware({ configPath: '...' })
Per-request agent initShared client, lazy-loaded once
res.send() monkey-patchres.json() interception (opt-in)

The new middleware is simpler, faster (no per-request init), and gives you req.jacsClient for manual operations.

Next Steps

Koa Middleware

Registry status (observed 2026-07-11): npm serves @hai.ai/jacs@0.10.1, while these pages describe source 0.11.4. The registry package does not expose every method shown here. Pin the version and inspect its exports; use a source build only when that is an explicit deployment choice.

Sign it. Prove it. -- in your Koa app.

JACS provides jacsKoaMiddleware for Koa with the same design as the Express middleware -- verify incoming signed bodies, optionally auto-sign responses.

Quick Start

import Koa from 'koa';
import bodyParser from 'koa-bodyparser';
import { JacsClient } from '@hai.ai/jacs/client';
import { jacsKoaMiddleware } from '@hai.ai/jacs/koa';

const client = await JacsClient.quickstart({
  name: 'my-agent',
  domain: 'my-agent.example.com',
});
const app = new Koa();

app.use(bodyParser({ enableTypes: ['text'] }));
app.use(jacsKoaMiddleware({ client, verify: true }));

app.use(async (ctx) => {
  console.log(ctx.state.jacsPayload); // verified payload
  ctx.body = { status: 'ok' };
});

app.listen(3000);

Options

jacsKoaMiddleware({
  client?: JacsClient;      // Pre-initialized client (preferred)
  configPath?: string;       // Path to jacs.config.json (if no client)
  sign?: boolean;            // Auto-sign ctx.body after next() (default: false)
  verify?: boolean;          // Verify incoming POST/PUT/PATCH bodies (default: true)
  optional?: boolean;        // Allow unsigned requests through (default: false)
  authReplay?: boolean | {   // Replay protection for auth-style endpoints (default: false)
    enabled?: boolean;
    maxAgeSeconds?: number;    // default: 30
    clockSkewSeconds?: number; // default: 5
    cacheTtlSeconds?: number;  // default: maxAge + skew
  };
})

How It Works

Every request gets ctx.state.jacsClient for manual use.

POST/PUT/PATCH with verify: true: The string body is verified. On success, ctx.state.jacsPayload is set. On failure, 401 is returned (unless optional: true).

With sign: true: After downstream middleware runs, if ctx.body is a non-Buffer object, it is signed before the response is sent.

Auth Replay Protection (Auth Endpoints)

Enable replay protection when signed JACS bodies are used as authentication artifacts:

app.use(
  jacsKoaMiddleware({
    client,
    verify: true,
    authReplay: { enabled: true, maxAgeSeconds: 30, clockSkewSeconds: 5 },
  })
);

When enabled, middleware enforces:

  • signature timestamp freshness (maxAgeSeconds + clockSkewSeconds)
  • single-use (signerId, signature) dedupe inside a TTL cache

Notes:

  • Keep this mode scoped to auth-style endpoints.
  • Cache is in-memory per process; use a shared cache for multi-instance deployments.

Auto-Sign Responses

app.use(jacsKoaMiddleware({ client, sign: true }));

app.use(async (ctx) => {
  // This will be JACS-signed automatically after next()
  ctx.body = { result: 42, timestamp: new Date().toISOString() };
});

Manual Signing

app.use(jacsKoaMiddleware({ client }));

app.use(async (ctx) => {
  const result = processData(ctx.state.jacsPayload);
  const signed = await ctx.state.jacsClient.signMessage(result);
  ctx.type = 'application/json';
  ctx.body = signed.raw;
});

Comparison with Express

FeatureExpressKoa
ImportjacsMiddleware from @hai.ai/jacs/expressjacsKoaMiddleware from @hai.ai/jacs/koa
Client accessreq.jacsClientctx.state.jacsClient
Payloadreq.jacsPayloadctx.state.jacsPayload
Auto-sign targetres.json() interceptionctx.body after next()

Next Steps

HTTP Server

Registry status (observed 2026-07-11): npm serves @hai.ai/jacs@0.10.1, while these pages describe source 0.11.4. The registry package does not expose every method shown here. Pin the version and inspect its exports; use a source build only when that is an explicit deployment choice.

JACS provides middleware and utilities for carrying signed documents over HTTP. The legacy middleware on this page is an artifact-integrity layer, not a complete HTTP authentication protocol.

Security boundary: signRequest() / verifyResponse() do not bind the actual method, absolute URL, query, exact body bytes, or service audience. Do not use them alone to authorize an endpoint. Source 0.13.0 clients use JacsSimpleAgent.buildRequestAuthHeader(method, url, body, audience) for a JACS v2 credential. Servers verify the same request context with a trusted key and atomically consume its nonce. Signed events use signResponse() on JacsSimpleAgent and fail-closed unwrapSignedEvent() with pinned server keys. See Security.

Overview

JACS HTTP integration provides:

  • Artifact signing: Carry generic signed JACS documents in request bodies
  • Artifact verification: Verify those document signatures and hashes
  • Request-bound auth: Build a separate JACS v2 Authorization header
  • Strict events: Verify the complete v2 response envelope with pinned keys
  • Framework middleware: Ready-to-use middleware for Express and Koa

Core Concepts

Request/Response Flow

Client                          Server
  |                               |
  |-- Authorization: JACS v2 ---> |-- verify method/URL/body/audience
  |-- exact body bytes ---------> |-- atomically consume nonce
  |                               |-- process payload
  |                               |-- JacsSimpleAgent.signResponse(result)
  |<-- v2 signed envelope -------|
  |-- unwrapSignedEvent(envelope, pinnedKeys)
  |

Successful verification proves possession of the private key corresponding to the key selected by the verifier and integrity of the signed scope. It does not independently establish a person, organization, domain, or other real-world identity; that association comes from configured trust.

HTTP Client

Basic Client Usage

import jacs from '@hai.ai/jacs';
import http from 'http';

async function main() {
  // Load JACS agent
  await jacs.load('./jacs.config.json');

  // Prepare payload
  const payload = {
    message: "Hello, secure server!",
    data: { id: 123, value: "some data" },
    timestamp: new Date().toISOString()
  };

  // Sign the request
  const signedRequest = await jacs.signRequest(payload);

  // Send HTTP request
  const response = await sendRequest(signedRequest, 'localhost', 3000, '/api/echo');

  // Verify the response
  const verifiedResponse = await jacs.verifyResponse(response);
  console.log('Verified payload:', verifiedResponse.payload);
}

function sendRequest(body, host, port, path) {
  return new Promise((resolve, reject) => {
    const options = {
      hostname: host,
      port: port,
      path: path,
      method: 'POST',
      headers: {
        'Content-Type': 'text/plain',
        'Content-Length': Buffer.byteLength(body),
      },
    };

    const req = http.request(options, (res) => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => {
        if (res.statusCode >= 200 && res.statusCode < 300) {
          resolve(data);
        } else {
          reject(new Error(`HTTP ${res.statusCode}: ${data}`));
        }
      });
    });

    req.on('error', reject);
    req.write(body);
    req.end();
  });
}

main();

Using Fetch

import jacs from '@hai.ai/jacs';

async function sendJacsRequest(url, payload) {
  await jacs.load('./jacs.config.json');

  // Sign the payload
  const signedRequest = await jacs.signRequest(payload);

  // Send request
  const response = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'text/plain' },
    body: signedRequest
  });

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  // Verify response
  const responseText = await response.text();
  const verified = await jacs.verifyResponse(responseText);

  return verified.payload;
}

// Usage
const result = await sendJacsRequest('http://localhost:3000/api/data', {
  action: 'fetch',
  query: { id: 42 }
});

Express Server

Using Express Middleware

JACS provides JACSExpressMiddleware that automatically:

  • Verifies incoming JACS requests
  • Attaches the verified payload to req.jacsPayload
  • Signs outgoing responses when you call res.send() with an object
import express from 'express';
import { JACSExpressMiddleware } from '@hai.ai/jacs/http';

const app = express();
const PORT = 3000;

// IMPORTANT: Use express.text() BEFORE JACS middleware
// This ensures req.body is a string for JACS verification
app.use('/api', express.text({ type: '*/*' }));

// Apply JACS middleware to API routes
app.use('/api', JACSExpressMiddleware({
  configPath: './jacs.server.config.json'
}));

// Route handler
app.post('/api/echo', (req, res) => {
  // Access verified payload from req.jacsPayload
  const payload = req.jacsPayload;

  if (!payload) {
    return res.status(400).send('JACS payload missing');
  }

  console.log('Received verified payload:', payload);

  // Send response object - middleware will sign it automatically
  res.send({
    echo: "Server says hello!",
    received: payload,
    timestamp: new Date().toISOString()
  });
});

app.listen(PORT, () => {
  console.log(`JACS Express server listening on port ${PORT}`);
});

Middleware Configuration

JACSExpressMiddleware({
  configPath: './jacs.config.json'  // Required: path to JACS config
})

Manual Request/Response Handling

For more control, you can handle signing manually:

import express from 'express';
import jacs from '@hai.ai/jacs';

const app = express();

// Initialize JACS once at startup
await jacs.load('./jacs.config.json');

app.use(express.text({ type: '*/*' }));

app.post('/api/process', async (req, res) => {
  try {
    // Manually verify incoming request
    const verified = await jacs.verifyResponse(req.body);
    const payload = verified.payload;

    // Process the request
    const result = {
      success: true,
      data: processData(payload),
      timestamp: new Date().toISOString()
    };

    // Manually sign the response
    const signedResponse = await jacs.signResponse(result);
    res.type('text/plain').send(signedResponse);

  } catch (error) {
    console.error('JACS verification failed:', error);
    res.status(400).send('Invalid JACS request');
  }
});

Koa Server

Using Koa Middleware

import Koa from 'koa';
import { JACSKoaMiddleware } from '@hai.ai/jacs/http';

const app = new Koa();
const PORT = 3000;

// Apply JACS Koa middleware
// Handles raw body reading, verification, and response signing
app.use(JACSKoaMiddleware({
  configPath: './jacs.server.config.json'
}));

// Route handler
app.use(async (ctx) => {
  if (ctx.path === '/api/echo' && ctx.method === 'POST') {
    // Access verified payload from ctx.state.jacsPayload or ctx.jacsPayload
    const payload = ctx.state.jacsPayload || ctx.jacsPayload;

    if (!payload) {
      ctx.status = 400;
      ctx.body = 'JACS payload missing';
      return;
    }

    console.log('Received verified payload:', payload);

    // Set response object - middleware will sign it automatically
    ctx.body = {
      echo: "Koa server says hello!",
      received: payload,
      timestamp: new Date().toISOString()
    };
  } else {
    ctx.status = 404;
    ctx.body = 'Not Found. Try POST to /api/echo';
  }
});

app.listen(PORT, () => {
  console.log(`JACS Koa server listening on port ${PORT}`);
});

API Reference

jacs.signRequest(payload)

Sign an object as a JACS request.

This is a generic signed document, not a request-bound HTTP credential. Use JacsSimpleAgent.buildAuthHeader(...) for authorization.

const signedRequest = await jacs.signRequest({
  method: 'getData',
  params: { id: 123 }
});
// Returns: JACS-signed JSON string

jacs.verifyResponse(responseString)

Verify a JACS-signed response and extract the payload.

This is legacy generic-document verification, not strict v2 event unwrapping. Use JacsSimpleAgent.unwrapSignedEvent(...) with pinned server keys.

const result = await jacs.verifyResponse(jacsResponseString);
// Returns: { payload: {...}, jacsId: '...', ... }

const payload = result.payload;

jacs.signResponse(payload)

Sign an object as a JACS response.

For the fully bound 2.0.0 response envelope, call the instance method on JacsSimpleAgent; verify it with unwrapSignedEvent() rather than parsing data after an error.

const signedResponse = await jacs.signResponse({
  success: true,
  data: { result: 42 }
});
// Returns: JACS-signed JSON string

JACSExpressMiddleware(options)

Express middleware for JACS request/response handling.

import { JACSExpressMiddleware } from '@hai.ai/jacs/http';

app.use(JACSExpressMiddleware({
  configPath: './jacs.config.json'  // Required
}));

Request Processing:

  • Reads req.body as a JACS string
  • Verifies the signature
  • Attaches payload to req.jacsPayload
  • On failure, sends 400 response

Response Processing:

  • Intercepts res.send() calls
  • If body is an object, signs it as JACS
  • Sends signed string to client

JACSKoaMiddleware(options)

Koa middleware for JACS request/response handling.

import { JACSKoaMiddleware } from '@hai.ai/jacs/http';

app.use(JACSKoaMiddleware({
  configPath: './jacs.config.json'  // Required
}));

Request Processing:

  • Reads raw request body
  • Verifies JACS signature
  • Attaches payload to ctx.state.jacsPayload and ctx.jacsPayload

Response Processing:

  • Signs ctx.body if it's an object
  • Converts to JACS string before sending

Complete Example

Server (server.js)

import express from 'express';
import { JACSExpressMiddleware } from '@hai.ai/jacs/http';

const app = express();

// JACS middleware for /api routes
app.use('/api', express.text({ type: '*/*' }));
app.use('/api', JACSExpressMiddleware({
  configPath: './jacs.server.config.json'
}));

// Echo endpoint
app.post('/api/echo', (req, res) => {
  const payload = req.jacsPayload;
  res.send({
    echo: payload,
    serverTime: new Date().toISOString()
  });
});

// Calculate endpoint
app.post('/api/calculate', (req, res) => {
  const { operation, a, b } = req.jacsPayload;

  let result;
  switch (operation) {
    case 'add': result = a + b; break;
    case 'subtract': result = a - b; break;
    case 'multiply': result = a * b; break;
    case 'divide': result = a / b; break;
    default: return res.status(400).send({ error: 'Unknown operation' });
  }

  res.send({ operation, a, b, result });
});

app.listen(3000, () => console.log('Server running on port 3000'));

Client (client.js)

import jacs from '@hai.ai/jacs';

async function main() {
  await jacs.load('./jacs.client.config.json');

  // Call echo endpoint
  const echoResult = await callApi('/api/echo', {
    message: 'Hello, server!'
  });
  console.log('Echo result:', echoResult);

  // Call calculate endpoint
  const calcResult = await callApi('/api/calculate', {
    operation: 'multiply',
    a: 7,
    b: 6
  });
  console.log('Calculate result:', calcResult);
}

async function callApi(path, payload) {
  const signedRequest = await jacs.signRequest(payload);

  const response = await fetch(`http://localhost:3000${path}`, {
    method: 'POST',
    headers: { 'Content-Type': 'text/plain' },
    body: signedRequest
  });

  const responseText = await response.text();
  const verified = await jacs.verifyResponse(responseText);
  return verified.payload;
}

main().catch(console.error);

Security Considerations

Content-Type

JACS requests should use text/plain content type since they are signed JSON strings, not raw JSON.

Error Handling

Always handle verification failures gracefully:

try {
  const verified = await jacs.verifyResponse(responseText);
  return verified.payload;
} catch (error) {
  console.error('JACS verification failed:', error.message);
  // Handle invalid/tampered response
}

Agent Keys

Each server and client needs its own JACS agent with:

  • Unique agent ID
  • Private/public key pair
  • Configuration file pointing to the keys

Middleware Order

For Express, ensure express.text() comes before JACSExpressMiddleware:

// Correct order
app.use('/api', express.text({ type: '*/*' }));
app.use('/api', JACSExpressMiddleware({ configPath: '...' }));

// Wrong - JACS middleware won't receive string body
app.use('/api', JACSExpressMiddleware({ configPath: '...' }));
app.use('/api', express.text({ type: '*/*' }));

Next Steps

API Reference

Registry status (observed 2026-07-11): npm serves @hai.ai/jacs@0.10.1, while these pages describe source 0.11.4. The registry package does not expose every method shown here. Pin the version and inspect its exports; use a source build only when that is an explicit deployment choice.

Complete API documentation for the @hai.ai/jacs Node.js package.

Installation

npm install @hai.ai/jacs

v0.7.0: Async-First API

All NAPI operations now return Promises by default. Sync variants are available with a Sync suffix, following the Node.js convention (like fs.readFile vs fs.readFileSync).

// Async (default, recommended)
await agent.load('./jacs.config.json');
const doc = await agent.createDocument(JSON.stringify(content));

// Sync (blocks event loop)
agent.loadSync('./jacs.config.json');
const doc = agent.createDocumentSync(JSON.stringify(content));

Core Module

import { JacsAgent, hashString, createConfig } from '@hai.ai/jacs';

JacsAgent Class

The JacsAgent class is the primary interface for JACS operations. Each instance maintains its own state and can be used independently, allowing multiple agents in the same process.

Constructor

new JacsAgent()

Creates a new empty JacsAgent instance. Call load() or loadSync() to initialize with a configuration.

Example:

const agent = new JacsAgent();
await agent.load('./jacs.config.json');

agent.load(configPath) / agent.loadSync(configPath)

Load and initialize the agent from a configuration file.

Parameters:

  • configPath (string): Path to the JACS configuration file

Returns: Promise<string> (async) or string (sync) -- The loaded agent's JSON

Example:

const agent = new JacsAgent();

// Async (recommended)
const agentJson = await agent.load('./jacs.config.json');

// Sync
const agentJson = agent.loadSync('./jacs.config.json');

console.log('Agent loaded:', JSON.parse(agentJson).jacsId);

agent.createDocument(...) / agent.createDocumentSync(...)

Create and sign a new JACS document.

Parameters:

  • documentString (string): JSON string of the document content
  • customSchema (string, optional): Path to a custom JSON Schema for validation
  • outputFilename (string, optional): Filename to save the document
  • noSave (boolean, optional): If true, don't save to storage (default: false)
  • attachments (string, optional): Path to file attachments
  • embed (boolean, optional): If true, embed attachments in the document

Returns: Promise<string> (async) or string (sync) -- The signed document as a JSON string

Example:

// Basic document creation (async)
const doc = await agent.createDocument(JSON.stringify({
  title: 'My Document',
  content: 'Hello, World!'
}));

// Without saving (sync)
const tempDoc = agent.createDocumentSync(
  JSON.stringify({ data: 'temporary' }),
  null, null, true
);

agent.verifyDocument(...) / agent.verifyDocumentSync(...)

Verify a document's signature and hash integrity.

Parameters:

  • documentString (string): The signed document JSON string

Returns: Promise<boolean> (async) or boolean (sync) -- True if the document is valid

Example:

const isValid = await agent.verifyDocument(signedDocumentJson);
if (isValid) {
  console.log('Document signature verified');
}

agent.verifySignature(...) / agent.verifySignatureSync(...)

Verify a document's signature with an optional custom signature field.

Parameters:

  • documentString (string): The signed document JSON string
  • signatureField (string, optional): Name of the signature field (default: 'jacsSignature')

Returns: Promise<boolean> (async) or boolean (sync)


agent.updateDocument(...) / agent.updateDocumentSync(...)

Update an existing document, creating a new version.

Parameters:

  • documentKey (string): The document key in format "id:version"
  • newDocumentString (string): The modified document as JSON string
  • attachments (Array, optional): Array of attachment file paths
  • embed (boolean, optional): If true, embed attachments

Returns: Promise<string> (async) or string (sync)

Example:

const doc = JSON.parse(signedDoc);
const documentKey = `${doc.jacsId}:${doc.jacsVersion}`;
const updatedDoc = await agent.updateDocument(
  documentKey,
  JSON.stringify({ ...doc, title: 'Updated Title' })
);

agent.createAgreement(...) / agent.createAgreementSync(...)

Legacy sidecar API. Use Agreement v2 for new standalone consent workflows.

Add an agreement requiring multiple agent signatures to a document.

Parameters:

  • documentString (string): The document JSON string
  • agentIds (Array): Array of agent IDs required to sign
  • question (string, optional): The agreement question
  • context (string, optional): Additional context
  • agreementFieldName (string, optional): Field name (default: 'jacsAgreement')

Returns: Promise<string> (async) or string (sync)


agent.signAgreement(...) / agent.signAgreementSync(...)

Sign an agreement as the current agent.

Parameters:

  • documentString (string): The document with agreement JSON string
  • agreementFieldName (string, optional): Field name (default: 'jacsAgreement')

Returns: Promise<string> (async) or string (sync)


agent.checkAgreement(...) / agent.checkAgreementSync(...)

Check the status of an agreement.

Parameters:

  • documentString (string): The document with agreement JSON string
  • agreementFieldName (string, optional): Field name (default: 'jacsAgreement')

Returns: Promise<string> (async) or string (sync) -- JSON string with agreement status


agent.signArtifact(...) / agent.signArtifactSync(...)

Sign an A2A artifact with JACS provenance. This is the canonical method name.

Parameters:

  • artifactJson (string): JSON string of the artifact to sign
  • artifactType (string): Type of artifact (e.g., "artifact", "result")
  • parentSignaturesJson (string, optional): JSON string of parent signatures for chain of custody

Returns: Promise<string> (async) or string (sync) -- The signed, wrapped artifact as a JSON string

Example:

const signed = await agent.signArtifact(
  JSON.stringify({ action: 'classify', input: 'hello' }),
  'artifact'
);

agent.wrapA2aArtifact(...) / agent.wrapA2aArtifactSync(...)

Deprecated since 0.9.0. Use signArtifact() / signArtifactSync() instead. This method will be removed in 1.0.0.

Set JACS_SHOW_DEPRECATIONS=1 to see runtime warnings when deprecated methods are called.

Wraps an A2A artifact with JACS provenance signature. Identical behavior to signArtifact() / signArtifactSync().

Parameters: Same as signArtifact() / signArtifactSync().


agent.signString(...) / agent.signStringSync(...)

Sign arbitrary string data with the agent's private key.

Parameters:

  • data (string): The data to sign

Returns: Promise<string> (async) or string (sync) -- Base64-encoded signature


agent.verifyString(...) / agent.verifyStringSync(...)

Verify a signature on arbitrary string data.

Parameters:

  • data (string): The original data
  • signatureBase64 (string): The base64-encoded signature
  • publicKey (Buffer): The public key as a Buffer
  • publicKeyEncType (string): The key algorithm (e.g., 'ring-Ed25519')

Returns: Promise<boolean> (async) or boolean (sync)


agent.signRequest(params) -- V8-thread-only

Sign a request payload, wrapping it in a JACS document. This method is synchronous (no Sync suffix) because it uses V8-thread-only APIs.

Parameters:

  • params (any): The request payload object

Returns: string -- JACS-signed request as a JSON string


agent.verifyResponse(documentString) -- V8-thread-only

Verify a JACS-signed response and extract the payload. Synchronous only.

Parameters:

  • documentString (string): The JACS-signed response

Returns: object -- Object containing the verified payload


agent.verifyResponseWithAgentId(documentString) -- V8-thread-only

Verify a response and return both the payload and signer's agent ID. Synchronous only.

Parameters:

  • documentString (string): The JACS-signed response

Returns: object -- Object with payload and agent ID


agent.verifyAgent(...) / agent.verifyAgentSync(...)

Verify the agent's own signature and hash, or verify another agent file.

Parameters:

  • agentFile (string, optional): Path to an agent file to verify

Returns: Promise<boolean> (async) or boolean (sync)


agent.updateAgent(...) / agent.updateAgentSync(...)

Update the agent document with new data.

Parameters:

  • newAgentString (string): The modified agent document as JSON string

Returns: Promise<string> (async) or string (sync)


agent.signAgent(...) / agent.signAgentSync(...)

Sign another agent's document with a registration signature.

Parameters:

  • agentString (string): The agent document to sign
  • publicKey (Buffer): The public key as a Buffer
  • publicKeyEncType (string): The key algorithm

Returns: Promise<string> (async) or string (sync)


Utility Functions

hashString(data)

Hash a string using SHA-256.

Parameters:

  • data (string): The string to hash

Returns: string -- Hexadecimal hash string

import { hashString } from '@hai.ai/jacs';
const hash = hashString('data to hash');

createConfig(options)

Create a JACS configuration JSON string programmatically.

Parameters:

  • jacsUseSecurity (string, optional)
  • jacsDataDirectory (string, optional)
  • jacsKeyDirectory (string, optional)
  • jacsAgentPrivateKeyFilename (string, optional)
  • jacsAgentPublicKeyFilename (string, optional)
  • jacsAgentKeyAlgorithm (string, optional)
  • jacsPrivateKeyPassword (string, optional)
  • jacsAgentIdAndVersion (string, optional)
  • jacsDefaultStorage (string, optional)

Returns: string -- Configuration as JSON string


HTTP Module

import { JACSExpressMiddleware, JACSKoaMiddleware } from '@hai.ai/jacs/http';

JACSExpressMiddleware(options)

Express middleware for JACS request/response handling.

Parameters:

  • options.configPath (string): Path to JACS configuration file

Returns: Express middleware function


JACSKoaMiddleware(options)

Koa middleware for JACS request/response handling.

Parameters:

  • options.configPath (string): Path to JACS configuration file

Returns: Koa middleware function


MCP Module

import {
  JACSTransportProxy,
  createJACSTransportProxy,
  createJACSTransportProxyAsync
} from '@hai.ai/jacs/mcp';

createJACSTransportProxy(transport, configPath, role)

Factory function for creating a transport proxy.

Parameters:

  • transport: The underlying MCP transport
  • configPath (string): Path to JACS configuration file
  • role (string): 'server' or 'client'

Returns: JACSTransportProxy instance


createJACSTransportProxyAsync(transport, configPath, role)

Async factory that waits for JACS to be fully loaded.

Returns: Promise


TypeScript Support

The package includes full TypeScript definitions. Import types as needed:

import { JacsAgent, hashString, createConfig } from '@hai.ai/jacs';

const agent: JacsAgent = new JacsAgent();
const hash: string = hashString('data');

Deprecated Functions

The following module-level functions are deprecated. Use new JacsAgent() and instance methods instead:

  • load() -> Use agent.load() / agent.loadSync()
  • signAgent() -> Use agent.signAgent() / agent.signAgentSync()
  • verifyString() -> Use agent.verifyString() / agent.verifyStringSync()
  • signString() -> Use agent.signString() / agent.signStringSync()
  • verifyAgent() -> Use agent.verifyAgent() / agent.verifyAgentSync()
  • updateAgent() -> Use agent.updateAgent() / agent.updateAgentSync()
  • verifyDocument() -> Use agent.verifyDocument() / agent.verifyDocumentSync()
  • updateDocument() -> Use agent.updateDocument() / agent.updateDocumentSync()
  • verifySignature() -> Use agent.verifySignature() / agent.verifySignatureSync()
  • createAgreement() -> Use agent.createAgreement() / agent.createAgreementSync()
  • signAgreement() -> Use agent.signAgreement() / agent.signAgreementSync()
  • createDocument() -> Use agent.createDocument() / agent.createDocumentSync()
  • checkAgreement() -> Use agent.checkAgreement() / agent.checkAgreementSync()
  • signRequest() -> Use agent.signRequest() (V8-thread-only, sync)
  • verifyResponse() -> Use agent.verifyResponse() (V8-thread-only, sync)
  • verifyResponseWithAgentId() -> Use agent.verifyResponseWithAgentId() (V8-thread-only, sync)

Migration Example:

// Old (deprecated, v0.6.x)
import jacs from '@hai.ai/jacs';
jacs.load('./jacs.config.json');
const doc = jacs.createDocument(JSON.stringify({ data: 'test' }));

// New (v0.7.0, async)
import { JacsAgent } from '@hai.ai/jacs';
const agent = new JacsAgent();
await agent.load('./jacs.config.json');
const doc = await agent.createDocument(JSON.stringify({ data: 'test' }));

// New (v0.7.0, sync)
import { JacsAgent } from '@hai.ai/jacs';
const agent = new JacsAgent();
agent.loadSync('./jacs.config.json');
const doc = agent.createDocumentSync(JSON.stringify({ data: 'test' }));

Error Handling

All methods may throw errors. Use try/catch for error handling:

try {
  const agent = new JacsAgent();
  await agent.load('./jacs.config.json');
  const doc = await agent.createDocument(JSON.stringify({ data: 'test' }));
} catch (error) {
  console.error('JACS error:', error.message);
}

See Also

Browser WASM Package

@jacs/wasm is the browser WebAssembly package for JACS. Use it when a web page needs to create a browser-local JACS agent, sign JSON, verify signed JACS documents, or collect multi-party agreement signatures without calling a backend service.

Distribution status (observed 2026-07-11): @jacs/wasm is not published on npm. The API on this page is available from a source build; the registry install command becomes valid only after a release is published and smoke-tested.

For Node.js server applications, use the native @hai.ai/jacs package instead. @jacs/wasm is browser-only: no filesystem access, no DNS trust lookup, no MCP server, and no CLI process.

Build and install today

git clone https://github.com/HumanAssisted/JACS.git
cd JACS
make build-wasm
# From your consuming project:
npm install /absolute/path/to/JACS/jacs-wasm/pkg

Bundlers must be able to load an ES module plus the generated jacs_wasm_bg.wasm asset. The source package is smoke-tested with Vite and Chromium. After the first registry publication, the consumer command becomes npm install @jacs/wasm.

Quick Start

import { createEphemeral, initJacsWasm } from "@jacs/wasm";

await initJacsWasm();

const agent = await createEphemeral("ed25519");
const signed = agent.signMessageJson(JSON.stringify({ hello: "world" }));
const result = JSON.parse(agent.verifyJson(signed));

console.log(result.valid); // true

initJacsWasm() is idempotent. The async constructors also initialize the module, so applications can call createEphemeral, importEncryptedAgent, importEncryptedAgentFiles, or createVerifier directly.

Public API

Constructors:

createEphemeral(algorithm: "ed25519" | "pq2025"): Promise<CoreAgentHandle>
importEncryptedAgent(materialJson: string, password: string): Promise<CoreAgentHandle>
importEncryptedAgentFiles(files: EncryptedAgentFiles, password: string): Promise<CoreAgentHandle>
createVerifier(publicKeyBase64: string, algorithm: "ed25519" | "pq2025"): Promise<CoreAgentHandle>

importEncryptedAgentFiles is shaped for browser file pickers:

type EncryptedAgentFiles = {
  configText: string;
  agentText: string;
  publicKeyBytes: Uint8Array;
  encryptedPrivateKeyBytes: Uint8Array;
};

The wrapper infers the algorithm from the raw public key length: 32 bytes for ed25519 and 2592 bytes for pq2025.

Core handle methods:

agent.signMessageJson(dataJson: string): string
agent.verifyJson(signedJson: string): string
agent.verifyWithKeyJson(
  signedJson: string,
  publicKeyBase64: string,
  algorithm: "ed25519" | "pq2025",
): string
agent.exportAgent(): string
agent.getPublicKeyBase64(): string
agent.algorithm(): "ed25519" | "pq2025"
agent.isUnlocked(): boolean
agent.clearSecrets(): void
agent.metrics(): string

clearSecrets() zeroes the in-memory private key. Signing then fails with a Locked error, while verification continues to work.

Multi-Party Agreements

Browser callers can build, sign, and verify agreement documents:

import { createAgreementJson } from "@jacs/wasm";

const documentJson = JSON.stringify({ action: "deploy", version: "1.2.3" });
const agentIdsJson = JSON.stringify(["alice", "bob"]);
const agreement = createAgreementJson(
  documentJson,
  agentIdsJson,
  "Ship this release?",
  "Production release vote",
);

const signedByAlice = alice.signAgreementJson(agreement, "approver");
const outcome = JSON.parse(
  verifier.verifyAgreementJson(
    signedByAlice,
    JSON.stringify([
      {
        agentId: "alice",
        publicKeyBase64: alice.getPublicKeyBase64(),
        algorithm: alice.algorithm(),
      },
    ]),
  ),
);

Browser Persistence

localStore wraps window.localStorage and stores values under the jacs: namespace.

import { localStore } from "@jacs/wasm";

localStore.saveEncryptedAgent("alice", encryptedMaterialJson);
localStore.saveDocument("doc-1", signed);

const restored = localStore.loadEncryptedAgent("alice");
const keys = localStore.listKeys();

localStore.remove("doc-1");
localStore.clearAll();

saveEncryptedAgent refuses obvious plaintext secrets, top-level or nested password-like fields, and raw private-key-shaped payloads. The browser package should persist encrypted agent material and signed documents only.

Workers

Post-quantum key generation can block the main thread. The worker subpath moves key generation and signing to a Web Worker:

import {
  createEphemeralInWorker,
  signMessageInWorker,
} from "@jacs/wasm/worker";

const workerAgent = await createEphemeralInWorker("pq2025");
const signed = await signMessageInWorker(
  workerAgent,
  JSON.stringify({ hello: "world" }),
);

Observability

Browsers do not expose stdout or a Prometheus endpoint for package code. @jacs/wasm exposes per-handle counters instead:

const snapshot = JSON.parse(agent.metrics());
// { signCount, verifyCount, lastSignDurationMs, lastVerifyDurationMs }

For local debugging, set globalThis.JACS_WASM_DEBUG = true before a sign, verify, or clearSecrets call. Debug logging is off by default.

Security Notes

WebAssembly memory is visible to JavaScript running on the same page. That means any XSS, compromised dependency, malicious browser extension, or third-party script on the origin can potentially inspect key material while an agent is unlocked.

Use @jacs/wasm for browser-local signing when that risk is acceptable. For hardware-backed or high-assurance signing, use a hardware key, secure-enclave-backed WebCrypto flow, or server-side signer. Call clearSecrets() as soon as the user is done signing.

Local Build and Chromium Smoke Test

Install the Rust WASM target once:

rustup target add wasm32-unknown-unknown
cargo install wasm-pack

Build the package and run the browser smoke fixture:

wasm-pack build jacs-wasm --target web --out-dir pkg
jacs-wasm/scripts/finalize-pkg.sh

cd jacs-wasm/examples/vite-smoke
npm install
npx playwright install chromium
npx playwright test --project=chromium

The Chromium smoke test builds the Vite app, serves the static output, loads it in Chromium, creates an ephemeral @jacs/wasm agent, signs a JSON payload, verifies it, and waits for SMOKE OK in the page output.

CI runs the same browser path in the WASM PR and release workflows.

Python Installation

The JACS Python package (jacs) provides Python bindings to the JACS Rust library, making it easy to integrate JACS into AI/ML workflows, data science projects, and Python applications.

Requirements

  • Python: Version 3.10 or higher
  • pip: For package management
  • Operating System: Linux, macOS, or Windows with WSL
  • Architecture: x86_64 or ARM64

Installation

Using pip

pip install jacs

For framework adapters (LangChain, FastAPI, Anthropic, etc.) use optional extras, e.g. pip install jacs[langchain], jacs[fastapi], or jacs[all]. Optional: jacs[langgraph], jacs[ws]. See Framework Adapters and the package pyproject.toml.

Using conda

conda install -c conda-forge jacs

Using poetry

poetry add jacs

Development Installation

# Clone the repository
git clone https://github.com/HumanAssisted/JACS
cd JACS/jacspy

# Create virtual environment
python -m venv .venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate

# Install in development mode
pip install -e .

Verify Installation

Create a simple test to verify everything is working:

# test.py
import jacs
print('JACS Python bindings loaded successfully!')

# Quick check (no config file; in-memory agent)
from jacs.client import JacsClient
client = JacsClient.ephemeral()
signed = client.sign_message({"hello": "jacs"})
result = client.verify(signed.raw_json)
print('Sign & verify OK:', result.valid)

Or with an existing config file:

import jacs
agent = jacs.JacsAgent()
agent.load('./jacs.config.json')
print('Agent loaded successfully!')

Run the test:

python test.py

Package Structure

The jacs package provides Python bindings to the JACS Rust library:

Core Module

import jacs

# Create and load agent
agent = jacs.JacsAgent()
agent.load('./jacs.config.json')

# Utility functions
hash_value = jacs.hash_string("data to hash")
config_json = jacs.create_config(
    jacs_data_directory="./data",
    jacs_key_directory="./keys",
    jacs_agent_key_algorithm="ring-Ed25519"
)

JacsAgent Methods

# Create a new agent instance
agent = jacs.JacsAgent()

# Load configuration
agent.load('./jacs.config.json')

# Document operations
signed_doc = agent.create_document(json_string)
is_valid = agent.verify_document(document_string)

# Signing operations
signature = agent.sign_string("data to sign")

Configuration

Configuration File

Create a jacs.config.json file:

{
  "$schema": "https://hai.ai/schemas/jacs.config.schema.json",
  "jacs_data_directory": "./jacs_data",
  "jacs_key_directory": "./jacs_keys",
  "jacs_default_storage": "fs",
  "jacs_agent_key_algorithm": "ring-Ed25519",
  "jacs_agent_id_and_version": "agent-uuid:version-uuid"
}

Load Configuration in Python

import jacs

# Create agent and load configuration
agent = jacs.JacsAgent()
agent.load('./jacs.config.json')

Programmatic Configuration

import jacs

# Create a configuration JSON string programmatically
config_json = jacs.create_config(
    jacs_data_directory="./jacs_data",
    jacs_key_directory="./jacs_keys",
    jacs_agent_key_algorithm="ring-Ed25519",
    jacs_default_storage="fs"
)

Environment Variables

JACS reads environment variables that override configuration file settings:

export JACS_DATA_DIRECTORY="./production_data"
export JACS_KEY_DIRECTORY="./production_keys"
export JACS_AGENT_KEY_ALGORITHM="ring-Ed25519"
export JACS_DEFAULT_STORAGE="fs"

Storage Backends

Configure storage in jacs.config.json:

File System (Default)

{
  "jacs_default_storage": "fs",
  "jacs_data_directory": "./jacs_data",
  "jacs_key_directory": "./jacs_keys"
}

Local Indexed SQLite

{
  "jacs_default_storage": "rusqlite"
}

Use rusqlite when you want local full-text search and the upgraded DocumentService behavior in bindings and MCP.

AWS Storage

{
  "jacs_default_storage": "aws"
}

AWS credentials are read from standard AWS environment variables.

Memory Storage (Testing)

{
  "jacs_default_storage": "memory"
}

Cryptographic Algorithms

{
  "jacs_agent_key_algorithm": "ring-Ed25519"
}

Pros: Fast, secure, small signatures Cons: Requires elliptic curve support

pq2025 (Post-Quantum)

{
  "jacs_agent_key_algorithm": "pq2025"
}

Pros: FIPS-204 ML-DSA-87 post-quantum signatures Cons: Larger signatures than Ed25519

Development Setup

Project Structure

my-jacs-project/
├── requirements.txt
├── jacs.config.json
├── src/
│   ├── agent.py
│   ├── tasks.py
│   └── agreements.py
├── jacs_data/
│   ├── agents/
│   ├── tasks/
│   └── documents/
├── jacs_keys/
│   ├── private.pem
│   └── public.pem
└── tests/
    └── test_jacs.py

Requirements.txt Setup

jacs>=0.9.0
fastapi>=0.100.0  # For FastMCP integration
uvicorn>=0.23.0   # For ASGI server
pydantic>=2.0.0   # For data validation

Basic Application

# src/app.py
import jacs
import json

def main():
    # Create and load agent
    agent = jacs.JacsAgent()
    agent.load('./jacs.config.json')

    # Create a document
    document_data = {
        "title": "My First Document",
        "content": "Hello from Python!"
    }

    signed_doc = agent.create_document(json.dumps(document_data))
    print('Document created')

    # Verify the document
    is_valid = agent.verify_document(signed_doc)
    print(f'Document valid: {is_valid}')

    print('JACS agent ready!')
    return agent

if __name__ == "__main__":
    agent = main()

Virtual Environment Setup

Using venv

# Create virtual environment
python -m venv jacs-env

# Activate (Linux/macOS)
source jacs-env/bin/activate

# Activate (Windows)
jacs-env\Scripts\activate

# Install JACS
pip install jacs

Using conda

# Create conda environment
conda create -n jacs-env python=3.11

# Activate environment
conda activate jacs-env

# Install JACS
pip install jacs

Using poetry

# Initialize poetry project
poetry init

# Add JACS dependency
poetry add jacs

# Install dependencies
poetry install

# Activate shell
poetry shell

Jupyter Notebook Setup

# Install Jupyter in your JACS environment
pip install jupyter

# Start Jupyter
jupyter notebook
# In your notebook
import jacs
import json

# Create and load agent
agent = jacs.JacsAgent()
agent.load('./jacs.config.json')

# Create a simple document
doc = agent.create_document(json.dumps({
    "title": "Notebook Analysis",
    "data": [1, 2, 3, 4, 5]
}))

print("Document created!")
print("JACS ready for notebook use!")

Common Issues

Module Not Found

If you get ModuleNotFoundError: No module named 'jacs':

# Check Python version
python --version  # Should be 3.10+

# Check if jacs is installed
pip list | grep jacs

# Reinstall if needed
pip uninstall jacs
pip install jacs

Permission Errors

If you get permission errors accessing files:

# Check directory permissions
ls -la jacs_data/ jacs_keys/

# Fix permissions
chmod 755 jacs_data/ jacs_keys/
chmod 600 jacs_keys/*.pem

Binary Compatibility

If you get binary compatibility errors:

# Update pip and reinstall
pip install --upgrade pip
pip uninstall jacs
pip install jacs --no-cache-dir

Windows Issues

On Windows, you may need Visual C++ Build Tools:

# Install Visual C++ Build Tools
# Or use conda-forge
conda install -c conda-forge jacs

Type Hints and IDE Support

JACS is built with Rust and PyO3, providing Python bindings:

import jacs
import json

# Create agent instance
agent: jacs.JacsAgent = jacs.JacsAgent()
agent.load('./jacs.config.json')

# Create and verify documents
signed_doc: str = agent.create_document(json.dumps({"title": "Test"}))
is_valid: bool = agent.verify_document(signed_doc)

Testing Setup

# tests/test_jacs.py
import unittest
import jacs
import json

class TestJACS(unittest.TestCase):
    def setUp(self):
        # Requires a valid jacs.config.json file
        self.agent = jacs.JacsAgent()
        self.agent.load('./jacs.config.json')

    def test_document_creation(self):
        doc_data = {"title": "Test Document", "content": "Test content"}
        signed_doc = self.agent.create_document(json.dumps(doc_data))

        # Document should be a valid JSON string
        parsed = json.loads(signed_doc)
        self.assertIn("jacsId", parsed)
        self.assertIn("jacsSignature", parsed)

    def test_document_verification(self):
        doc_data = {"title": "Verify Test"}
        signed_doc = self.agent.create_document(json.dumps(doc_data))

        is_valid = self.agent.verify_document(signed_doc)
        self.assertTrue(is_valid)

    def test_sign_string(self):
        signature = self.agent.sign_string("test data")
        self.assertIsInstance(signature, str)
        self.assertTrue(len(signature) > 0)

if __name__ == "__main__":
    unittest.main()

Next Steps

Now that you have JACS installed:

  1. Basic Usage - Learn core JACS operations
  2. MCP Integration - Add Model Context Protocol support
  3. FastMCP Integration - Build advanced MCP servers
  4. API Reference - Complete API documentation

Examples

Check out the complete examples in the examples directory:

  • Basic agent creation and task management
  • Jupyter notebook workflows
  • FastMCP server implementation
  • AI/ML pipeline integration

Simplified API

The simplified API (jacs.simple) provides a streamlined, module-level interface for common JACS operations. It's designed to get you signing and verifying in under 2 minutes.

Quick Start

Zero-config -- one call to start signing:

import jacs.simple as jacs

info = jacs.quickstart(name="my-agent", domain="my-agent.example.com")
print(info.config_path, info.public_key_path, info.private_key_path)
signed = jacs.sign_message({"action": "approve", "amount": 100})
result = jacs.verify(signed.raw)
print(f"Valid: {result.valid}, Signer: {result.signer_id}")

quickstart(name, domain, ...) creates a persistent agent with keys on disk (default algorithm: pq2025). If ./jacs.config.json already exists, it loads it; otherwise it creates a new agent. Returned AgentInfo includes config/key paths (config_path/public_key_path/private_key_path in Python, configPath/publicKeyPath/privateKeyPath in Node) plus key directory metadata so you can locate key material immediately. Rust/CLI quickstart requires an explicit password source (JACS_PRIVATE_KEY_PASSWORD, or JACS_PASSWORD_FILE in CLI). Python/Node can auto-generate a password if needed; set JACS_SAVE_PASSWORD_FILE=true if you want that generated password persisted to ./jacs_keys/.jacs_password. Pass algorithm="ed25519" to override the default (pq2025).

To load an existing agent explicitly, use load() instead:

agent = jacs.load("./jacs.config.json")
signed = jacs.sign_message({"action": "approve", "amount": 100})

When to Use the Simplified API

Simplified APIJacsAgent Class
Quick prototypingMultiple agents in one process
Scripts and CLI toolsComplex multi-document workflows
MCP tool implementationsFine-grained control
Single-agent applicationsCustom error handling

API Reference

quickstart(name, domain, description=None, algorithm=None, config_path=None)

Create a persistent agent with keys on disk. If ./jacs.config.json already exists, loads it. Otherwise creates a new agent, saving keys and config to disk. If JACS_PRIVATE_KEY_PASSWORD is unset, Python quickstart auto-generates a secure password in-process (JACS_SAVE_PASSWORD_FILE=true persists it to ./jacs_keys/.jacs_password). Call this once before sign_message() or verify().

Parameters:

  • name (str, required): Agent name used for first-time creation.
  • domain (str, required): Agent domain used for DNS/public-key verification workflows.
  • description (str, optional): Human-readable description.
  • algorithm (str, optional): Signing algorithm. Default: "pq2025". User-facing choices: "ed25519", "pq2025"; "ring-Ed25519" is accepted as a legacy alias.
  • config_path (str, optional): Config path (default: "./jacs.config.json").

Returns: AgentInfo dataclass

info = jacs.quickstart(name="my-agent", domain="my-agent.example.com")
print(f"Agent ID: {info.agent_id}")
print(f"Config path: {info.config_path}")
print(f"Public key: {info.public_key_path}")
print(f"Private key: {info.private_key_path}")

# Or with a specific algorithm
info = jacs.quickstart(
    name="my-agent",
    domain="my-agent.example.com",
    algorithm="ed25519",
)

load(config_path=None, strict=None)

Load a persistent agent from a configuration file. Use this instead of quickstart(name=..., domain=..., ...) when you want to load a specific config file explicitly.

Parameters:

  • config_path (str, optional): Path to jacs.config.json (default: "./jacs.config.json")
  • strict (bool, optional): If True, verification failures raise; if None, uses JACS_STRICT_MODE env var

Returns: AgentInfo dataclass

Raises: JacsError if config not found or invalid

info = jacs.load("./jacs.config.json")
print(f"Agent ID: {info.agent_id}")
print(f"Config: {info.config_path}")

is_loaded()

Check if an agent is currently loaded.

Returns: bool

if not jacs.is_loaded():
    jacs.load("./jacs.config.json")

get_agent_info()

Get information about the currently loaded agent.

Returns: AgentInfo or None if no agent is loaded

info = jacs.get_agent_info()
if info:
    print(f"Agent: {info.agent_id}")

verify_self()

Verify the loaded agent's own integrity (signature and hash).

Returns: VerificationResult

Raises: AgentNotLoadedError if no agent is loaded

result = jacs.verify_self()
if result.valid:
    print("Agent integrity verified")
else:
    print(f"Errors: {result.errors}")

sign_message(data)

Sign arbitrary data as a JACS document.

Parameters:

  • data (any): Dict, list, string, or any JSON-serializable value

Returns: SignedDocument

Raises: AgentNotLoadedError if no agent is loaded

# Sign a dict
signed = jacs.sign_message({
    "action": "transfer",
    "amount": 500,
    "recipient": "agent-123"
})

print(f"Document ID: {signed.document_id}")
print(f"Signed by: {signed.agent_id}")
print(f"Timestamp: {signed.timestamp}")
print(f"Raw JSON: {signed.raw}")

sign_file(file_path, embed=False)

Sign a file with optional content embedding.

Parameters:

  • file_path (str): Path to the file to sign
  • embed (bool, optional): If True, embed file content in the document (default: False)

Returns: SignedDocument

Raises: JacsError if file not found or no agent loaded

# Reference only (stores hash)
signed = jacs.sign_file("contract.pdf", embed=False)

# Embed content (creates portable document)
embedded = jacs.sign_file("contract.pdf", embed=True)

verify(signed_document)

Verify a signed document and extract its content.

Parameters:

  • signed_document (str|dict|SignedDocument): The signed document as JSON string, dict, or SignedDocument

Returns: VerificationResult

Raises: AgentNotLoadedError if no agent is loaded

result = jacs.verify(signed_json)

if result.valid:
    print(f"Signed by: {result.signer_id}")
    print(f"Timestamp: {result.timestamp}")
else:
    print(f"Invalid: {', '.join(result.errors)}")

verify_standalone(document, key_resolution="local", data_directory=None, key_directory=None)

Verify a signed document without loading an agent. Use when you only need to verify (e.g. a lightweight API).

Parameters: document (str|dict), key_resolution (str), data_directory (str, optional), key_directory (str, optional)

Returns: VerificationResult


HTTP protocol helpers (SimpleAgent instances)

The module-level API focuses on durable documents. Request-bound HTTP auth and signed event helpers live on the instance-based native SimpleAgent:

import json
from jacs import SimpleAgent

agent, _ = SimpleAgent.ephemeral(algorithm="ed25519")
body = '{"action":"approve"}'
authorization = agent.build_request_auth_header(
    "POST",
    "https://api.example.com/v1/jobs?mode=strict",
    body,
    "jobs-api",
)

envelope = agent.sign_response(json.dumps({"decision": "allow"}))
signer_id = json.loads(envelope)["jacsSignature"]["agentID"]
keys = json.dumps({signer_id: agent.get_public_key_pem()})
verified = json.loads(agent.unwrap_signed_event(envelope, keys))
assert authorization.startswith("JACS v2.")
assert verified["verified"] is True
assert verified["data"]["decision"] == "allow"
  • build_request_auth_header(method, url, body="", audience="hai.ai") binds the normalized method, absolute URL including query, exact UTF-8 body bytes, audience, signer/key, issue time, and nonce. Serialize the final request body before calling it and transmit that string unchanged.
  • sign_response(payload_json) returns a 2.0.0 envelope whose jacs-response-v2 signature covers payload and all metadata.
  • unwrap_signed_event(event_json, server_keys_json) returns JSON containing verified data, signerId, timestamp, algorithm, and documentId. Plain/unsigned input, legacy payload-only envelopes, unknown signers, and any mutation raise; it never successfully returns verified: false.

server_keys_json is a map from signer ID to a pinned base64 key or PEM string. It represents the application's configured trust decision. Cryptographic success proves possession of that key, not a real-world identity by itself.

Legacy no-argument build_auth_header() remains available for compatibility and emits a WARN. Migrate both peers to v2; strict deployments can reject the legacy method with JACS_REJECT_UNBOUND_AUTH_HEADER=true. See Security.


sign_text(path)

Append a YAML-bodied JACS signature block to the end of a markdown or text file. The file content (everything before the first signature block) is preserved byte-for-byte; multi-signer files are unordered.

Parameters: path (str): file to sign in place

Returns: None

Raises: MarkerCollisionError if the file contains a column-zero -----BEGIN JACS SIGNATURE----- outside a valid block — see the inline-text-signing guide for documented workarounds.

jacs.sign_text("README.md")

verify_text(path, *, strict=False, key_dir=None)

Verify all signature blocks in a text file. Permissive by default — missing signatures are a typed status, not an error.

Parameters:

  • path (str)
  • strict (bool, keyword-only): when True, raises MissingSignatureError if the file is unsigned. Default False.
  • key_dir (str, keyword-only): directory of <signer_id>.public.pem files used in addition to the local trust store.

Returns: VerifyTextResult.status is 'signed' | 'missing_signature' | 'malformed'; .signers lists per-signer outcomes.

result = jacs.verify_text("README.md")
print(result.status)  # 'signed'

# Strict — missing signature raises
try:
    jacs.verify_text("UNSIGNED.md", strict=True)
except MissingSignatureError:
    print("not signed")

See the inline text signing guide for the full feature set.


sign_image(path, *, out=None, robust=False, refuse_overwrite=False)

Embed a JACS signature inside a PNG, JPEG, or WebP image. The signature lives in a metadata chunk (PNG iTXt / JPEG APP11 / WebP XMP).

Parameters:

  • path (str): input image
  • out (str, keyword-only): output path. If omitted, signs in place.
  • robust (bool, keyword-only): opt-in LSB embedding fallback for PNG/JPEG (off by default; WebP deferred).
  • refuse_overwrite (bool, keyword-only): raise AlreadySignedError if the input already carries a signature.

Returns: None

jacs.sign_image("photo.png", out="signed.png")

verify_image(path, *, strict=False, key_dir=None)

Verify the embedded signature on an image. Same permissive / strict model as verify_text.

Parameters: as for verify_text.

Returns: VerifyImageResult.status is 'valid' | 'invalid' | 'missing_signature'; .signer_id, .signed_at, .algorithm populated when valid.

v = jacs.verify_image("signed.png")
print(v.status)  # 'valid'

See the media signing guide for the full feature set.


extract_media_signature(path, *, raw_payload=False)

Extract the embedded signature payload without verifying it.

Parameters:

  • path (str)
  • raw_payload (bool, keyword-only): emit the base64url wire form instead of decoded JSON. Default False.

Returns: dict (decoded JSON) or str (base64url wire form when raw_payload=True).

payload = jacs.extract_media_signature("signed.png")
print(payload["signedAt"])

verify_by_id(document_id)

Verify a document by its storage ID (uuid:version) without passing the full JSON. Requires the document to be stored locally (e.g. in the agent's data directory).

Parameters: document_id (str): Document ID in format "uuid:version"

Returns: VerificationResult


reencrypt_key(old_password, new_password)

Re-encrypt the loaded agent's private key with a new password.

Parameters: old_password (str), new_password (str)

Raises: AgentNotLoadedError if no agent loaded; JacsError if password is wrong


audit(config_path=None, recent_n=None)

Run a read-only security audit and health checks. Returns a dict with risks, health_checks, summary, and overall_status. Does not require a loaded agent; does not modify state.

See Security Model — Security Audit for full details and options.

result = jacs.audit()
print(f"Risks: {len(result['risks'])}, Status: {result['overall_status']}")

update_agent(new_agent_data)

Update the agent document with new data and re-sign it.

This function expects a complete agent document (not partial updates). Use export_agent() to get the current document, modify it, then pass it here.

Parameters:

  • new_agent_data (dict|str): Complete agent document as JSON string or dict

Returns: str - The updated and re-signed agent document

Raises: AgentNotLoadedError if no agent loaded, JacsError if validation fails

import json

# Get current agent document
agent_doc = json.loads(jacs.export_agent())

# Modify fields
agent_doc["jacsAgentType"] = "hybrid"
agent_doc["description"] = "Hybrid signing agent"

# Update (creates new version, re-signs, re-hashes)
updated = jacs.update_agent(agent_doc)
new_doc = json.loads(updated)

print(f"New version: {new_doc['jacsVersion']}")
print(f"Previous: {new_doc['jacsPreviousVersion']}")

Valid jacsAgentType values: "human", "human-org", "hybrid", "ai"


update_document(document_id, new_document_data, attachments=None, embed=False)

Update an existing document with new data and re-sign it.

Note: The original document must have been saved to disk (created without no_save=True).

Parameters:

  • document_id (str): The document ID (jacsId) to update
  • new_document_data (dict|str): Updated document as JSON string or dict
  • attachments (list, optional): List of file paths to attach
  • embed (bool, optional): If True, embed attachment contents

Returns: SignedDocument with the updated document

Raises: JacsError if document not found, no agent loaded, or validation fails

import json

# Create a document (must be saved to disk)
original = jacs.sign_message({"status": "pending", "amount": 100})

# Later, update it
doc = json.loads(original.raw)
doc["content"]["status"] = "approved"

updated = jacs.update_document(original.document_id, doc)
new_doc = json.loads(updated.raw)

print(f"New version: {new_doc['jacsVersion']}")
print(f"Previous: {new_doc['jacsPreviousVersion']}")

export_agent()

Export the current agent document for sharing or inspection.

Returns: str - The agent JSON document

Raises: AgentNotLoadedError if no agent loaded

agent_doc = jacs.export_agent()
print(agent_doc)

# Parse to inspect
agent = json.loads(agent_doc)
print(f"Agent type: {agent['jacsAgentType']}")

get_dns_record(domain, ttl=3600)

Return the DNS TXT record line for the loaded agent (for DNS-based discovery). Format: _v1.agent.jacs.{domain}. TTL IN TXT "v=hai.ai; ...".

Returns: str


get_well_known_json()

Return the well-known JSON object for the loaded agent (e.g. for /.well-known/jacs-pubkey.json). Keys: publicKey, publicKeyHash, algorithm, agentId.

Returns: dict


get_public_key()

Get the loaded agent's public key in PEM format for sharing with others.

Returns: str - PEM-encoded public key

Raises: AgentNotLoadedError if no agent loaded

pem = jacs.get_public_key()
print(pem)
# -----BEGIN PUBLIC KEY-----
# ...
# -----END PUBLIC KEY-----

Type Definitions

All types are Python dataclasses for convenient access:

AgentInfo

@dataclass
class AgentInfo:
    agent_id: str       # Agent's UUID
    config_path: str    # Path to loaded config
    public_key_path: Optional[str] = None  # Path to public key file

SignedDocument

@dataclass
class SignedDocument:
    raw: str            # Full JSON document with signature
    document_id: str    # Document's UUID (jacsId)
    agent_id: str       # Signing agent's ID
    timestamp: str      # ISO 8601 timestamp

VerificationResult

@dataclass
class VerificationResult:
    valid: bool                  # True if signature verified
    signer_id: Optional[str]     # Agent who signed
    timestamp: Optional[str]     # When it was signed
    attachments: List[Attachment]  # File attachments
    errors: List[str]            # Error messages if invalid

Attachment

@dataclass
class Attachment:
    filename: str       # Original filename
    mime_type: str      # MIME type
    content_hash: str   # SHA-256 hash of file content
    content: Optional[str] = None  # Base64-encoded content (if embedded)
    size_bytes: int = 0 # Size of the original file

Exceptions

class JacsError(Exception):
    """Base exception for JACS errors."""
    pass

class AgentNotLoadedError(JacsError):
    """Raised when an operation requires a loaded agent."""
    pass

Complete Example

import json
import jacs.simple as jacs
from jacs.types import JacsError, AgentNotLoadedError

# Load agent
agent = jacs.load("./jacs.config.json")
print(f"Loaded agent: {agent.agent_id}")

# Verify agent integrity
self_check = jacs.verify_self()
if not self_check.valid:
    raise RuntimeError("Agent integrity check failed")

# Sign a transaction
transaction = {
    "type": "payment",
    "from": agent.agent_id,
    "to": "recipient-agent-uuid",
    "amount": 250.00,
    "currency": "USD",
    "memo": "Q1 Service Payment"
}

signed = jacs.sign_message(transaction)
print(f"Transaction signed: {signed.document_id}")

# Verify the transaction (simulating recipient)
verification = jacs.verify(signed.raw)

if verification.valid:
    doc = json.loads(signed.raw)
    print(f"Payment verified from: {verification.signer_id}")
    print(f"Amount: {doc['content']['amount']} {doc['content']['currency']}")
else:
    print(f"Verification failed: {', '.join(verification.errors)}")

# Sign a file
contract_signed = jacs.sign_file("./contract.pdf", embed=True)
print(f"Contract signed: {contract_signed.document_id}")

# Update agent metadata
agent_doc = json.loads(jacs.export_agent())
agent_doc["jacsAgentType"] = "ai"
agent_doc["description"] = "AI signing agent"
updated_agent = jacs.update_agent(agent_doc)
print("Agent metadata updated")

# Share public key
public_key = jacs.get_public_key()
print("Share this public key for verification:")
print(public_key)

MCP Integration

The simplified API works well with FastMCP tool implementations:

from fastmcp import FastMCP
import jacs.simple as jacs

mcp = FastMCP("My Server")

# Load agent once at startup
jacs.load("./jacs.config.json")

@mcp.tool()
def approve_request(request_id: str) -> dict:
    """Approve a request with cryptographic signature."""
    signed = jacs.sign_message({
        "action": "approve",
        "request_id": request_id,
        "approved_by": jacs.get_agent_info().agent_id
    })
    return {"signed_approval": signed.raw}

@mcp.tool()
def verify_approval(signed_json: str) -> dict:
    """Verify a signed approval."""
    result = jacs.verify(signed_json)
    return {
        "valid": result.valid,
        "signer": result.signer_id,
        "errors": result.errors
    }

Error Handling

import jacs.simple as jacs
from jacs.types import JacsError, AgentNotLoadedError

try:
    jacs.load("./missing-config.json")
except JacsError as e:
    print(f"Config not found: {e}")

try:
    # Will fail if no agent loaded
    jacs.sign_message({"data": "test"})
except AgentNotLoadedError as e:
    print(f"No agent: {e}")

try:
    jacs.sign_file("/nonexistent/file.pdf")
except JacsError as e:
    print(f"File not found: {e}")

# Verification doesn't throw - check result.valid
result = jacs.verify("invalid json")
if not result.valid:
    print(f"Verification errors: {result.errors}")

See Also

Basic Usage

This chapter covers fundamental JACS operations in Python. For quick signing and verification, start with the Simplified API (jacs.simple) or JacsClient; the sections below use the JacsAgent class directly for fine-grained control.

Initializing an Agent

Create and Load Agent

import jacs

# Create a new agent instance
agent = jacs.JacsAgent()

# Load configuration from file
agent.load('./jacs.config.json')

Configuration File

Create jacs.config.json:

{
  "$schema": "https://hai.ai/schemas/jacs.config.schema.json",
  "jacs_data_directory": "./jacs_data",
  "jacs_key_directory": "./jacs_keys",
  "jacs_default_storage": "fs",
  "jacs_agent_key_algorithm": "ring-Ed25519",
  "jacs_agent_id_and_version": "agent-uuid:version-uuid"
}

Creating Documents

Basic Document Creation

import jacs
import json

agent = jacs.JacsAgent()
agent.load('./jacs.config.json')

# Create a document from JSON
document_data = {
    "title": "Project Proposal",
    "content": "Quarterly development plan",
    "budget": 50000
}

signed_document = agent.create_document(json.dumps(document_data))
print('Signed document:', signed_document)

With Custom Schema

Validate against a custom JSON Schema:

signed_document = agent.create_document(
    json.dumps(document_data),
    './schemas/proposal.schema.json'  # custom schema path
)

With Output File

signed_document = agent.create_document(
    json.dumps(document_data),
    None,                     # no custom schema
    './output/proposal.json'  # output filename
)

Without Saving

signed_document = agent.create_document(
    json.dumps(document_data),
    None,   # no custom schema
    None,   # no output filename
    True    # no_save = True
)

With Attachments

signed_document = agent.create_document(
    json.dumps(document_data),
    None,                        # no custom schema
    None,                        # no output filename
    False,                       # save the document
    './attachments/report.pdf',  # attachment path
    True                         # embed files
)

Verifying Documents

Verify Document Signature

# Verify a document's signature and hash
is_valid = agent.verify_document(signed_document_json)
print('Document valid:', is_valid)

Verify Specific Signature Field

# Verify with a custom signature field
is_valid = agent.verify_signature(
    signed_document_json,
    'jacsSignature'  # signature field name
)

Updating Documents

Update Existing Document

# Original document key format: "id:version"
document_key = 'doc-uuid:version-uuid'

# Modified document content (must include jacsId and jacsVersion)
updated_data = {
    "jacsId": "doc-uuid",
    "jacsVersion": "version-uuid",
    "title": "Updated Proposal",
    "content": "Revised quarterly plan",
    "budget": 75000
}

updated_document = agent.update_document(
    document_key,
    json.dumps(updated_data)
)

print('Updated document:', updated_document)

Update with New Attachments

updated_document = agent.update_document(
    document_key,
    json.dumps(updated_data),
    ['./new-report.pdf'],  # new attachments
    True                   # embed files
)

Signing and Verification

Sign Arbitrary Data

# Sign any string data
signature = agent.sign_string('Important message to sign')
print('Signature:', signature)

Verify Arbitrary Data

# Verify a signature on string data
is_valid = agent.verify_string(
    'Important message to sign',  # original data
    signature_base64,             # base64 signature
    public_key_bytes,             # public key as bytes
    'ring-Ed25519'                # algorithm
)

Working with Agreements

The methods in this section use the legacy jacsAgreement sidecar on an existing signed document. For new standalone consent workflows, prefer Agreement v2 through SimpleAgent.create_agreement_v2(), sign_agreement_v2(), and verify_agreement_v2().

Create an Agreement

# Add agreement requiring multiple agent signatures
document_with_agreement = agent.create_agreement(
    signed_document_json,
    ['agent1-uuid', 'agent2-uuid'],           # required signers
    'Do you agree to these terms?',            # question
    'Q1 2024 service contract',                # context
    'jacsAgreement'                            # field name
)

Sign an Agreement

# Sign the agreement as the current agent
signed_agreement = agent.sign_agreement(
    document_with_agreement_json,
    'jacsAgreement'  # agreement field name
)

Check Agreement Status

# Check which agents have signed
status = agent.check_agreement(
    document_with_agreement_json,
    'jacsAgreement'
)

print('Agreement status:', json.loads(status))

Agent Operations

Verify Agent

# Verify the loaded agent's signature
is_valid = agent.verify_agent()
print('Agent valid:', is_valid)

# Verify a specific agent file
is_valid_other = agent.verify_agent('./other-agent.json')

Update Agent

# Update agent document
updated_agent_json = agent.update_agent(json.dumps({
    "jacsId": "agent-uuid",
    "jacsVersion": "version-uuid",
    "name": "Updated Agent Name",
    "description": "Updated description"
}))

Sign External Agent

# Sign another agent's document with registration signature
signed_agent_json = agent.sign_agent(
    external_agent_json,
    public_key_bytes,
    'ring-Ed25519'
)

Generic Request/Response Documents

Not HTTP authentication: JacsAgent.sign_request() and verify_response() wrap and verify a generic signed JACS document. They do not bind the HTTP method, absolute URL, query, exact body bytes, or audience. For an authorization credential use SimpleAgent.build_request_auth_header(method, url, body, audience). For v2 response envelopes use SimpleAgent.sign_response() and fail-closed SimpleAgent.unwrap_signed_event() with pinned server keys. See Security.

Sign a Request

# Sign request-like data as a durable JACS document (not an HTTP credential)
signed_request = agent.sign_request({
    "method": "GET",
    "path": "/api/resource",
    "timestamp": datetime.now().isoformat(),
    "body": {"query": "data"}
})

Verify a Response

# Verify a generic signed document (not a jacs-response-v2 event)
result = agent.verify_response(signed_response_json)
print('Response valid:', result)

# Verify and get signer's agent ID
result_with_id = agent.verify_response_with_agent_id(signed_response_json)
print('Signer ID:', result_with_id)

Utility Functions

Hash String

import jacs

# SHA-256 hash of a string
hash_value = jacs.hash_string('data to hash')
print('Hash:', hash_value)

Create Configuration

import jacs

# Programmatically create a config JSON string
config_json = jacs.create_config(
    jacs_data_directory='./jacs_data',
    jacs_key_directory='./jacs_keys',
    jacs_agent_key_algorithm='ring-Ed25519',
    jacs_default_storage='fs'
)

print('Config:', config_json)

Error Handling

import jacs

agent = jacs.JacsAgent()

try:
    agent.load('./jacs.config.json')
except Exception as error:
    print(f'Failed to load agent: {error}')

try:
    doc = agent.create_document(json.dumps({'data': 'test'}))
    print('Document created')
except Exception as error:
    print(f'Failed to create document: {error}')

try:
    is_valid = agent.verify_document(invalid_json)
except Exception as error:
    print(f'Verification failed: {error}')

Complete Example

import jacs
import json

def main():
    # Initialize agent
    agent = jacs.JacsAgent()
    agent.load('./jacs.config.json')

    # Create a generic proposal document
    proposal = {
        "title": "Project Proposal",
        "description": "Q1 development plan",
        "budget": 50000
    }

    signed_proposal = agent.create_document(json.dumps(proposal))
    print('Document created')

    # Verify the document
    if agent.verify_document(signed_proposal):
        print('Document signature valid')

    # Create agreement for proposal approval
    proposal_with_agreement = agent.create_agreement(
        signed_proposal,
        ['manager-uuid', 'developer-uuid'],
        'Do you approve this proposal?'
    )

    # Sign the agreement
    signed_agreement = agent.sign_agreement(proposal_with_agreement)
    print('Agreement signed')

    # Check agreement status
    status = agent.check_agreement(signed_agreement)
    print('Status:', status)

    # Hash some data for reference
    proposal_hash = jacs.hash_string(signed_proposal)
    print('Document hash:', proposal_hash)

if __name__ == "__main__":
    main()

Working with Document Data

Parse Signed Documents

import json

# Create and sign a document
doc_data = {"title": "My Document", "content": "Hello, World!"}
signed_doc = agent.create_document(json.dumps(doc_data))

# Parse the signed document to access JACS fields
parsed = json.loads(signed_doc)
print('Document ID:', parsed.get('jacsId'))
print('Document Version:', parsed.get('jacsVersion'))
print('Signature:', parsed.get('jacsSignature'))

Document Key Format

# Document keys combine ID and version
doc_id = parsed['jacsId']
doc_version = parsed['jacsVersion']
document_key = f"{doc_id}:{doc_version}"

# Use the key for updates
updated_doc = agent.update_document(document_key, json.dumps({
    **parsed,
    "content": "Updated content"
}))

Configuration Management

Load from File

import jacs

agent = jacs.JacsAgent()
agent.load('./jacs.config.json')

Environment Variables

JACS reads environment variables that override configuration file settings:

export JACS_DATA_DIRECTORY="./production_data"
export JACS_KEY_DIRECTORY="./production_keys"
export JACS_AGENT_KEY_ALGORITHM="ring-Ed25519"
export JACS_DEFAULT_STORAGE="fs"

Programmatic Configuration

import jacs
import json
import os

# Create config programmatically
config_json = jacs.create_config(
    jacs_data_directory='./jacs_data',
    jacs_key_directory='./jacs_keys',
    jacs_agent_key_algorithm='ring-Ed25519',
    jacs_default_storage='fs'
)

# Write to file
with open('jacs.config.json', 'w') as f:
    f.write(config_json)

# Then load it
agent = jacs.JacsAgent()
agent.load('./jacs.config.json')

Next Steps

MCP Integration (Python)

Python exposes two different MCP stories:

  1. Secure a local FastMCP transport with jacs.mcp
  2. Expose JACS operations as MCP tools with jacs.adapters.mcp

Use the first when you already have an MCP server or client. Use the second when you want the model to call JACS signing, agreement, A2A, or trust helpers as normal MCP tools.

What Is Supported

  • Local FastMCP server wrapping with JACSMCPServer
  • Local FastMCP client wrapping with JACSMCPClient
  • One-line server creation with create_jacs_mcp_server()
  • FastMCP tool registration with register_jacs_tools(), register_a2a_tools(), and register_trust_tools()

Important Constraints

  • JACSMCPClient, JACSMCPServer, and jacs_call() enforce loopback-only URLs
  • Unsigned fallback is disabled by default
  • Clients must pin the expected server agent ID; servers must allowlist client agent IDs
  • stdio is rejected because it bypasses the HTTP signing middleware
  • JSON POST and response bodies default to a 2 MiB hard limit (JACS_MCP_MAX_MESSAGE_BYTES)
  • strict=True is about config loading and failure behavior, not an opt-in to security

A valid signature proves possession of a key, not that the signer is the intended peer. The peer-ID policy below is what binds that proof to your deployment. An optional expected_peer_public_key_hash also pins one exact server key. On the server, the agent-ID allowlist is combined with JACS trust/key resolution; operators must maintain rotation and revocation policy for those allowed identities.

1. Secure A FastMCP Server

The shortest path is the factory:

from jacs.mcp import create_jacs_mcp_server

mcp = create_jacs_mcp_server(
    "My Server",
    "./jacs.config.json",
    allowed_peer_agent_ids=["CLIENT_AGENT_ID"],
)

@mcp.tool()
def hello(name: str) -> str:
    return f"Hello, {name}!"

mcp.run(transport="sse")

If you already have a FastMCP instance:

from fastmcp import FastMCP
from jacs.mcp import JACSMCPServer

mcp = JACSMCPServer(
    FastMCP("Secure Server"),
    "./jacs.config.json",
    allowed_peer_agent_ids=["CLIENT_AGENT_ID"],
)

app = mcp.http_app(transport="sse")

2. Secure A FastMCP Client

from jacs.mcp import JACSMCPClient

client = JACSMCPClient(
    "http://localhost:8000/sse",
    "./jacs.config.json",
    expected_peer_agent_id="SERVER_AGENT_ID",
    # Optional stronger rotation policy:
    # expected_peer_public_key_hash="SERVER_PUBLIC_KEY_HASH",
)

async with client:
    result = await client.call_tool("hello", {"name": "World"})

To accept any valid signer without authenticating which peer signed, use the separate dangerous compatibility switch:

client = JACSMCPClient(
    "http://localhost:8000/sse",
    "./jacs.config.json",
    allow_any_verified_peer=True,
)

This is proof-of-possession only and is not peer authentication. To allow fully unsigned fallback explicitly:

client = JACSMCPClient(
    "http://localhost:8000/sse",
    "./jacs.config.json",
    expected_peer_agent_id="SERVER_AGENT_ID",
    allow_unsigned_fallback=True,
)

3. Register JACS As MCP Tools

This is the better fit when the model should be able to ask for signatures, agreements, A2A cards, or trust-store operations directly.

from fastmcp import FastMCP
from jacs.client import JacsClient
from jacs.adapters.mcp import (
    register_jacs_tools,
    register_a2a_tools,
    register_trust_tools,
)

client = JacsClient.quickstart(name="mcp-agent", domain="mcp.local")
mcp = FastMCP("JACS Tools")

register_jacs_tools(mcp, client=client)
register_a2a_tools(mcp, client=client)
register_trust_tools(mcp, client=client)

The core tool set includes document signing, verification, agreements, audit, and agent-info helpers. The A2A and trust helpers are opt-in registrations.

Useful Helper APIs

From jacs.mcp:

  • jacs_tool to sign a specific tool's response
  • jacs_middleware() for explicit Starlette middleware
  • jacs_call() for one-off authenticated local MCP calls

jacs_tool, jacs_middleware(), and JacsSSETransport refuse unsigned output when no agent is loaded or signing fails. The dangerous allow_unsigned_fallback=True option restores legacy passthrough explicitly. Authenticated transport messages use a schema-valid JSON-RPC carrier whose params.envelope is a complete portable-v2 JACS document. Requests receive a fresh unpredictable wire ID per session, and responses with unknown IDs fail closed, preventing a signed response from being transplanted into another client session.

Example Paths In This Repo

  • jacspy/examples/mcp/server.py
  • jacspy/examples/mcp/client.py
  • jacspy/examples/mcp_server.py
  • jacspy/tests/test_adapters_mcp.py

When To Use Adapters Instead

Choose Python Framework Adapters instead of MCP when:

  • the model and tools already live in the same Python process
  • you only need signed LangChain, LangGraph, or FastAPI boundaries
  • you do not need MCP clients to connect from outside the app

Framework Adapters

Use adapters when the model already runs inside your Python app and you want provenance at the framework boundary, not a separate MCP server.

Choose The Adapter

If you need...APIStart here
Signed LangChain tool resultsjacs_signing_middleware, signed_toolLangChain / LangGraph section below
Signed LangGraph ToolNode outputsjacs_wrap_tool_call, with_jacs_signingLangChain / LangGraph section below
Signed FastAPI responses and verified inbound requestsJacsMiddleware, jacs_routeFastAPI section below
Signed Anthropic tool return valuesjacs.adapters.anthropic.signed_toolAnthropic section below

Install only the extra you need:

pip install jacs[langchain]
pip install jacs[fastapi]
pip install jacs[anthropic]

Optional: jacs[langgraph] (LangGraph ToolNode), jacs[ws] (WebSockets). See pyproject.toml for the full list.

LangChain / LangGraph

This is the smallest JACS path if your model already lives in LangChain.

LangChain middleware

from langchain.agents import create_agent
from jacs.client import JacsClient
from jacs.adapters.langchain import jacs_signing_middleware

client = JacsClient.quickstart(name="langchain-agent", domain="langchain.local")

agent = create_agent(
    model="openai:gpt-4o",
    tools=[search_tool, calc_tool],
    middleware=[jacs_signing_middleware(client=client)],
)

LangGraph ToolNode

from jacs.adapters.langchain import with_jacs_signing

tool_node = with_jacs_signing([search_tool, calc_tool], client=client)

Wrap one tool instead of the whole graph

from jacs.adapters.langchain import signed_tool

signed_search = signed_tool(search_tool, client=client)

The executable example to start from in this repo is jacspy/examples/langchain/signing_callback.py.

FastAPI / Starlette

Use this when the trust boundary is an API route instead of an MCP transport.

from fastapi import FastAPI
from jacs.client import JacsClient
from jacs.adapters.fastapi import JacsMiddleware

client = JacsClient.quickstart(name="api-agent", domain="api.local")
app = FastAPI()
app.add_middleware(JacsMiddleware, client=client)

Useful options:

  • Signing and verification failures fail closed by default
  • sign_responses=False or verify_requests=False to narrow the behavior
  • a2a=True to also expose A2A discovery routes from the same FastAPI app

Legacy compatibility options are explicit and dangerous:

  • allow_unsigned_output=True permits raw output after signing fails
  • allow_unverified_passthrough=True permits unverifiable input to continue
  • allow_plain_signature_fallback=True permits an attestation request to downgrade to a plain signature, dropping attestation claims

strict=True always disables all three fallbacks. Do not use these options at a trust boundary unless the caller separately marks the data as unsigned or unverified.

For auth-style endpoints, replay protection is available:

app.add_middleware(
    JacsMiddleware,
    client=client,
    strict=True,
    auth_replay_protection=True,
    auth_max_age_seconds=30,
    auth_clock_skew_seconds=5,
)

To sign only one route:

from jacs.adapters.fastapi import jacs_route

@app.get("/signed")
@jacs_route(client=client)
async def signed_endpoint():
    return {"ok": True}

Anthropic / Claude SDK

Use the Anthropic adapter when you want signed return values from normal Python tool functions:

from jacs.adapters.anthropic import signed_tool

@signed_tool(client=client)
def get_weather(location: str) -> str:
    return f"Weather in {location}: sunny"

When To Use MCP Instead

Choose Python MCP Integration instead of adapters when:

  • the model is outside your process and talks over MCP
  • you want an MCP tool suite for JACS operations
  • you need the same server to be usable by external MCP clients

API Reference

Complete API documentation for the jacs Python package.

For most use cases, the Simplified API (jacs.simple) and JacsClient (instance-based, multiple agents) are recommended. This page documents the lower-level JacsAgent class and module-level functions.

Installation

pip install jacs

Core Module

import jacs
from jacs import JacsAgent

JacsAgent Class

The JacsAgent class is the primary interface for JACS operations. Each instance maintains its own state and can be used independently, allowing multiple agents in the same process.

Constructor

JacsAgent()

Creates a new empty JacsAgent instance. Call load() to initialize with a configuration.

Example:

agent = jacs.JacsAgent()
agent.load('./jacs.config.json')

agent.load(config_path)

Load and initialize the agent from a configuration file.

Parameters:

  • config_path (str): Path to the JACS configuration file

Returns: str - The loaded agent's JSON

Example:

agent = jacs.JacsAgent()
agent_json = agent.load('./jacs.config.json')
print('Agent loaded:', json.loads(agent_json)['jacsId'])

agent.create_document(document_string, custom_schema=None, output_filename=None, no_save=False, attachments=None, embed=False)

Create and sign a new JACS document.

Parameters:

  • document_string (str): JSON string of the document content
  • custom_schema (str, optional): Path to a custom JSON Schema for validation
  • output_filename (str, optional): Filename to save the document
  • no_save (bool, optional): If True, don't save to storage (default: False)
  • attachments (str, optional): Path to file attachments
  • embed (bool, optional): If True, embed attachments in the document

Returns: str - The signed document as a JSON string

Example:

# Basic document creation
doc = agent.create_document(json.dumps({
    'title': 'My Document',
    'content': 'Hello, World!'
}))

# With custom schema
validated_doc = agent.create_document(
    json.dumps({'title': 'Validated', 'amount': 100}),
    custom_schema='./schemas/invoice.schema.json'
)

# Without saving
temp_doc = agent.create_document(
    json.dumps({'data': 'temporary'}),
    no_save=True
)

# With attachments
doc_with_file = agent.create_document(
    json.dumps({'report': 'Monthly Report'}),
    attachments='./report.pdf',
    embed=True
)

agent.verify_document(document_string)

Verify a document's signature and hash integrity.

Parameters:

  • document_string (str): The signed document JSON string

Returns: bool - True if the document is valid

Example:

is_valid = agent.verify_document(signed_document_json)
if is_valid:
    print('Document signature verified')
else:
    print('Document verification failed')

agent.verify_signature(document_string, signature_field=None)

Verify a document's signature with an optional custom signature field.

Parameters:

  • document_string (str): The signed document JSON string
  • signature_field (str, optional): Name of the signature field (default: 'jacsSignature')

Returns: bool - True if the signature is valid

Example:

# Verify default signature field
is_valid = agent.verify_signature(doc_json)

# Verify custom signature field
is_valid_custom = agent.verify_signature(doc_json, 'customSignature')

agent.update_document(document_key, new_document_string, attachments=None, embed=False)

Update an existing document, creating a new version.

Parameters:

  • document_key (str): The document key in format "id:version"
  • new_document_string (str): The modified document as JSON string
  • attachments (list, optional): List of attachment file paths
  • embed (bool, optional): If True, embed attachments

Returns: str - The updated document as a JSON string

Example:

# Parse existing document to get key
doc = json.loads(signed_doc)
document_key = f"{doc['jacsId']}:{doc['jacsVersion']}"

# Update the document
updated_doc = agent.update_document(
    document_key,
    json.dumps({
        **doc,
        'title': 'Updated Title',
        'content': 'Modified content'
    })
)

agent.create_agreement(document_string, agent_ids, question=None, context=None, agreement_field_name=None)

Legacy sidecar API. Use Agreement v2 for new standalone consent workflows.

Add an agreement requiring multiple agent signatures to a document.

Parameters:

  • document_string (str): The document JSON string
  • agent_ids (list): List of agent IDs required to sign
  • question (str, optional): The agreement question
  • context (str, optional): Additional context for the agreement
  • agreement_field_name (str, optional): Field name for the agreement (default: 'jacsAgreement')

Returns: str - The document with agreement as a JSON string

Example:

doc_with_agreement = agent.create_agreement(
    signed_document_json,
    ['agent-1-uuid', 'agent-2-uuid', 'agent-3-uuid'],
    question='Do you agree to these terms?',
    context='Q1 2024 Service Agreement',
    agreement_field_name='jacsAgreement'
)

agent.sign_agreement(document_string, agreement_field_name=None)

Sign an agreement as the current agent.

Parameters:

  • document_string (str): The document with agreement JSON string
  • agreement_field_name (str, optional): Field name of the agreement (default: 'jacsAgreement')

Returns: str - The document with this agent's signature added

Example:

signed_agreement = agent.sign_agreement(
    doc_with_agreement_json,
    'jacsAgreement'
)

agent.check_agreement(document_string, agreement_field_name=None)

Check the status of an agreement (which agents have signed).

Parameters:

  • document_string (str): The document with agreement JSON string
  • agreement_field_name (str, optional): Field name of the agreement (default: 'jacsAgreement')

Returns: str - JSON string with agreement status

Example:

status_json = agent.check_agreement(signed_agreement_json)
status = json.loads(status_json)

print('Required signers:', status['required'])
print('Signatures received:', status['signed'])
print('Complete:', status['complete'])

agent.sign_artifact(artifact_json, artifact_type, parent_signatures_json=None)

Sign an A2A artifact with JACS provenance. This is the canonical method name.

Parameters:

  • artifact_json (str): JSON string of the artifact to sign
  • artifact_type (str): Type of artifact (e.g., "artifact", "result")
  • parent_signatures_json (str, optional): JSON string of parent signatures for chain of custody

Returns: str - The signed, wrapped artifact as a JSON string

Example:

signed = agent.sign_artifact(
    json.dumps({"action": "classify", "input": "hello"}),
    "artifact"
)

agent.wrap_a2a_artifact(artifact_json, artifact_type, parent_signatures_json=None)

Deprecated since 0.9.0. Use sign_artifact() instead. This method will be removed in 1.0.0.

Set JACS_SHOW_DEPRECATIONS=1 to see runtime warnings when deprecated methods are called.

Wraps an A2A artifact with JACS provenance signature. Identical behavior to sign_artifact().

Parameters: Same as sign_artifact().


agent.sign_string(data)

Sign arbitrary string data with the agent's private key.

Parameters:

  • data (str): The data to sign

Returns: str - Base64-encoded signature

Example:

signature = agent.sign_string('Important message')
print('Signature:', signature)

agent.verify_string(data, signature_base64, public_key, public_key_enc_type)

Verify a signature on arbitrary string data.

Parameters:

  • data (str): The original data
  • signature_base64 (str): The base64-encoded signature
  • public_key (bytes): The public key as bytes
  • public_key_enc_type (str): The key algorithm (e.g., 'ring-Ed25519')

Returns: bool - True if the signature is valid

Example:

is_valid = agent.verify_string(
    'Important message',
    signature_base64,
    public_key_bytes,
    'ring-Ed25519'
)

agent.sign_request(params)

Sign a request payload, wrapping it in a JACS document.

This is a generic document wrapper, not an HTTP authorization credential: it does not bind the actual method, absolute URL, query, body bytes, or audience. Use SimpleAgent.build_request_auth_header(method, url, body, audience) for HTTP.

Parameters:

  • params (any): The request payload (will be JSON serialized)

Returns: str - JACS-signed request as a JSON string

Example:

signed_request = agent.sign_request({
    'method': 'GET',
    'path': '/api/data',
    'timestamp': datetime.now().isoformat(),
    'body': {'query': 'value'}
})

agent.verify_response(document_string)

Verify a JACS-signed response and extract the payload.

This verifies the legacy generic document shape. It is not the strict jacs-response-v2 event contract. New event consumers use SimpleAgent.unwrap_signed_event(event_json, server_keys_json), which rejects plain events, unknown signers, legacy envelopes, and mutations without releasing data.

Parameters:

  • document_string (str): The JACS-signed response

Returns: dict - Dictionary containing the verified payload

Example:

result = agent.verify_response(jacs_response_string)
payload = result.get('payload')
print('Verified payload:', payload)

agent.verify_response_with_agent_id(document_string)

Verify a response and return both the payload and signer's agent ID.

Parameters:

  • document_string (str): The JACS-signed response

Returns: dict - Dictionary with payload and agent ID

Example:

result = agent.verify_response_with_agent_id(jacs_response_string)
print('Payload:', result['payload'])
print('Signed by agent:', result['agentId'])

agent.verify_agent(agent_file=None)

Verify the agent's own signature and hash, or verify another agent file.

Parameters:

  • agent_file (str, optional): Path to an agent file to verify

Returns: bool - True if the agent is valid

Example:

# Verify the loaded agent
is_valid = agent.verify_agent()

# Verify another agent file
is_other_valid = agent.verify_agent('./other-agent.json')

agent.update_agent(new_agent_string)

Update the agent document with new data.

Parameters:

  • new_agent_string (str): The modified agent document as JSON string

Returns: str - The updated agent document

Example:

current_agent = json.loads(agent.load('./jacs.config.json'))
updated_agent = agent.update_agent(json.dumps({
    **current_agent,
    'description': 'Updated description'
}))

agent.sign_agent(agent_string, public_key, public_key_enc_type)

Sign another agent's document with a registration signature.

Parameters:

  • agent_string (str): The agent document to sign
  • public_key (bytes): The public key as bytes
  • public_key_enc_type (str): The key algorithm

Returns: str - The signed agent document

Example:

signed_agent = agent.sign_agent(
    external_agent_json,
    public_key_bytes,
    'ring-Ed25519'
)

Module-Level Functions

These functions operate on a global agent singleton and are maintained for backwards compatibility. New code should use the JacsAgent class instead.

jacs.load(config_path)

Load the global agent from a configuration file.

import jacs
jacs.load('./jacs.config.json')

jacs.sign_request(data)

Sign a request using the global agent.

This deprecated helper signs request-like data only; it does not authenticate HTTP context. Use instance-based SimpleAgent.build_auth_header(...) for a request-bound v2 credential.

signed = jacs.sign_request({'method': 'tools/call', 'params': {...}})

jacs.verify_request(data)

Verify an incoming request using the global agent.

payload = jacs.verify_request(incoming_request_string)

jacs.sign_response(data)

Sign a response using the global agent.

This deprecated global helper is not the fully bound response-envelope API. Use SimpleAgent.sign_response(payload_json) for jacs-response-v2.

signed = jacs.sign_response({'result': 'success'})

jacs.verify_response(data)

Verify an incoming response using the global agent.

This deprecated global helper is not strict signed-event unwrapping. Use SimpleAgent.unwrap_signed_event(event_json, server_keys_json) and treat every error as untrusted input.

result = jacs.verify_response(response_string)
payload = result.get('payload')

MCP Module

from jacs.mcp import JACSMCPServer, JACSMCPClient, create_jacs_mcp_server, jacs_call

Canonical MCP documentation lives at Python MCP Integration. This API section lists the MCP entry points only:

  • JACSMCPServer(..., allowed_peer_agent_ids=[...]) - Wrap a FastMCP HTTP server with allowlisted JACS request verification and response signing.
  • JACSMCPClient(..., expected_peer_agent_id="...") - Create a FastMCP SSE client pinned to the expected server signer.
  • create_jacs_mcp_server(..., allowed_peer_agent_ids=[...]) - One-line authenticated HTTP server factory.
  • jacs_call(..., expected_peer_agent_id="...") - One-shot authenticated local MCP call.

The wrappers reject stdio, require portable-v2 signed carriers for every POST, randomize request IDs per session, and cap buffered JSON messages. See the canonical MCP page for the dangerous compatibility flags and key-hash pin.

For examples, strict-mode behavior, and security guidance, see Python MCP Integration.


Configuration

Configuration File Format

Create a jacs.config.json file:

{
  "$schema": "https://hai.ai/schemas/jacs.config.schema.json",
  "jacs_agent_id_and_version": "your-agent-id:version",
  "jacs_agent_key_algorithm": "ring-Ed25519",
  "jacs_agent_private_key_filename": "private.pem",
  "jacs_agent_public_key_filename": "public.pem",
  "jacs_data_directory": "./jacs_data",
  "jacs_default_storage": "fs",
  "jacs_key_directory": "./jacs_keys"
}

Configuration Options

FieldTypeDescription
jacs_agent_id_and_versionstringAgent ID and version in format "id:version"
jacs_agent_key_algorithmstringSigning algorithm: "ring-Ed25519" or "pq2025".
jacs_agent_private_key_filenamestringPrivate key filename
jacs_agent_public_key_filenamestringPublic key filename
jacs_data_directorystringDirectory for data storage
jacs_key_directorystringDirectory for key storage
jacs_default_storagestringStorage backend: "fs", "s3", "memory"

Error Handling

All methods may raise exceptions. Use try/except for error handling:

try:
    agent = jacs.JacsAgent()
    agent.load('./jacs.config.json')
    doc = agent.create_document(json.dumps({'data': 'test'}))
except FileNotFoundError as e:
    print(f'Configuration file not found: {e}')
except ValueError as e:
    print(f'Invalid configuration: {e}')
except Exception as e:
    print(f'JACS error: {e}')

Common Exceptions

ExceptionDescription
FileNotFoundErrorConfiguration file or key file not found
ValueErrorInvalid configuration or document format
RuntimeErrorAgent not loaded or cryptographic operation failed

Type Hints

The package supports type hints for better IDE integration:

from jacs import JacsAgent
import json

def process_document(agent: JacsAgent, data: dict) -> str:
    """Create and return a signed document."""
    doc_string = json.dumps(data)
    return agent.create_document(doc_string)

def verify_and_extract(agent: JacsAgent, doc: str) -> dict:
    """Verify document and extract content."""
    if agent.verify_document(doc):
        return json.loads(doc)
    raise ValueError("Document verification failed")

Thread Safety

JacsAgent instances use internal locking and are thread-safe. You can safely use the same agent instance across multiple threads:

import threading
from jacs import JacsAgent

agent = JacsAgent()
agent.load('./jacs.config.json')

def worker(data):
    # Safe to call from multiple threads
    doc = agent.create_document(json.dumps(data))
    return doc

threads = [
    threading.Thread(target=worker, args=({'id': i},))
    for i in range(10)
]
for t in threads:
    t.start()
for t in threads:
    t.join()

See Also

Go (jacsgo) Installation and Quick Start

jacsgo provides Go bindings for signing and verifying JACS documents in services, APIs, and agent runtimes.

Note: Go bindings are community-maintained. Python and Node.js currently have broader framework adapter coverage. For the full MCP surface, install jacs-cli and run jacs mcp; the Go MCP examples in the repo are demo code.

Install

At the 2026-07-09 distribution baseline, the Go module had only a pseudo-version and no matching native library. go get alone therefore cannot produce a linkable consumer. Build the full repository today:

git clone https://github.com/HumanAssisted/JACS.git
cd JACS
# For reproducible use, pin a reviewed source commit before building.
make -C jacsgo build-rust
cd jacsgo
go test ./...

After a semantic jacsgo/vX.Y.Z release and its checksum manifest actually exist, use the version-matched installer documented in the jacsgo README.

Minimal Sign + Verify

Create an agent first (CLI: jacs quickstart --name my-agent --domain my-agent.example.com, or programmatically with jacs.Create() and JACS_PRIVATE_KEY_PASSWORD). Then:

package main

import (
	"fmt"
	"log"

	jacs "github.com/HumanAssisted/JACS/jacsgo"
)

func main() {
	// Load agent: nil = default ./jacs.config.json
	if err := jacs.Load(nil); err != nil {
            log.Fatal("create an agent first: jacs quickstart --name my-agent --domain my-agent.example.com")
	}

	signed, err := jacs.SignMessage(map[string]interface{}{
		"event":  "tool-result",
		"status": "ok",
	})
	if err != nil {
		log.Fatal(err)
	}

	result, err := jacs.Verify(signed.Raw)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Valid: %t signer=%s\n", result.Valid, result.SignerID)
}

Programmatic agent creation

Use jacs.Create(name, &jacs.CreateAgentOptions{...}). Password must be set in options or via JACS_PRIVATE_KEY_PASSWORD. See the jacsgo README for the full API table and options.

Concurrent use

For multiple agents in one process, use NewJacsAgent(), then agent.Load(path) and agent methods; call agent.Close() when done. Attestation, A2A (agent cards, trust policy), and protocol helpers are available on JacsAgent and as package-level wrappers (see godoc or the jacsgo README).

Request authentication and signed events

For the cross-language simple contract, use JacsSimpleAgent:

algorithm := "ed25519"
agent, _, err := jacs.EphemeralSimpleAgent(&algorithm)
if err != nil {
	log.Fatal(err)
}
defer agent.Close()

body := []byte(`{"action":"approve"}`)
authorization, err := agent.BuildAuthHeader(
	"POST",
	"https://api.example.com/v1/jobs?mode=strict",
	body,
	"jobs-api",
)
if err != nil {
	log.Fatal(err)
}

envelope, err := agent.SignResponse(`{"decision":"allow"}`)
if err != nil {
	log.Fatal(err)
}

BuildAuthHeader binds method, absolute URL including query, exact body bytes, audience, signer/key, issue time, and nonce. SignResponse creates a fully bound 2.0.0 response envelope. Verify an incoming envelope with UnwrapSignedEvent(eventJSON, serverKeysJSON), where serverKeysJSON is a JSON map from signer ID to a pinned base64 key or PEM string. Plain events, unknown signers, legacy payload-only envelopes, and mutations return errors without releasing data.

The key map is a configured trust decision. A valid signature proves key possession, not a real-world identity by itself. For legacy JacsAgent, migrate from the denied-by-default no-argument BuildAuthHeader to BuildRequestAuthHeader; JacsSimpleAgent.BuildAuthHeader already requires the full request context and emits v2. See the security guide.

Common Go Use Cases

  • Sign outbound API/MCP payloads before crossing trust boundaries
  • Verify inbound signed payloads before executing sensitive actions
  • Sign files (SignFile) for portable chain-of-custody workflows
  • Generate DNS TXT fingerprints (GetDnsRecord) for public identity verification

MCP and HTTP Patterns

The Go repository includes runnable examples for transport-level signing:

  • jacsgo/examples/mcp/main.go for MCP-style request/response signing
  • jacsgo/examples/http/ for signed HTTP client/server traffic

Identity and Trust Notes

  • JACS agent identity is key-based (jacsId + versioned signatures)
  • Verification behavior follows the configured key-resolution order in the runtime (for example local and remote resolution modes supported by the underlying JACS core)
  • DID interoperability is possible at the integration layer without requiring blockchain infrastructure

See DNS-Based Verification and DID Integration (No Blockchain Required).

JSON Schemas

JACS schemas now describe a small portable signing surface instead of many narrow workflow document types. The core model is a generic signed JSON document: canonical JSON bytes, a common header, cryptographic signatures, signer identity, hashes, optional files, and optional agreement semantics.

Application-specific payloads belong in the document body or in a custom schema layered on top of the header schema.

Preserved Schemas

Document and Agent Schemas

SchemaPurpose
header/v1/header.schema.jsonGeneric signature wrapper for signed JSON documents
agent/v1/agent.schema.jsonAgent identity and public signing metadata
agreement/v2/agreement.schema.jsonStandalone agreement document for verifiable consent to terms
a2a-verification-result.schema.jsonCross-language A2A artifact verification result

Component Schemas

SchemaPurpose
components/signature/v1/signature.schema.jsonCryptographic signatures
components/agreement/v1/agreement.schema.jsonLegacy sidecar agreement metadata and co-signatures
components/files/v1/files.schema.jsonFile attachments and content hashes

Configuration Schema

SchemaPurpose
jacs.config.schema.jsonAgent configuration file format

Schema Locations

Schemas are available as HTTPS URLs and local files:

https://hai.ai/schemas/header/v1/header.schema.json
https://hai.ai/schemas/agent/v1/agent.schema.json
https://hai.ai/schemas/agreement/v2/agreement.schema.json
https://hai.ai/schemas/components/signature/v1/signature.schema.json
https://hai.ai/schemas/components/agreement/v1/agreement.schema.json
https://hai.ai/schemas/components/files/v1/files.schema.json
https://hai.ai/schemas/jacs.config.schema.json

Generic Signed Documents

Every signed document includes a $schema field. Generic payloads use the header schema directly:

{
  "$schema": "https://hai.ai/schemas/header/v1/header.schema.json",
  "jacsId": "550e8400-e29b-41d4-a716-446655440000",
  "jacsVersion": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "jacsType": "document",
  "jacsLevel": "artifact",
  "content": {
    "invoice_id": "INV-001",
    "amount": 100.0
  }
}

Custom schemas may extend the header when an integration needs stronger payload validation:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://example.com/schemas/invoice.schema.json",
  "allOf": [
    { "$ref": "https://hai.ai/schemas/header/v1/header.schema.json" },
    {
      "type": "object",
      "properties": {
        "content": {
          "type": "object",
          "required": ["invoice_id", "amount"]
        }
      }
    }
  ]
}

Agreements

Use agreement/v2/agreement.schema.json for new agreement workflows. It is a standalone jacsType: "agreement" document with explicit terms, parties, signer/witness/notary policy, transcript references, links, controllers, and portable verification.

Use components/agreement/v1/agreement.schema.json only when a generic signed document needs simple legacy jacsAgreement countersignature metadata. V1 answers "did these agents approve this payload?" V2 answers "did these agents consent to these terms with this process record?"

HAI Extensions

JACS schemas include a custom hai property that categorizes fields:

ValueDescription
metaMetadata fields such as IDs, timestamps, and versions
baseCore cryptographic fields such as hashes and signatures
agentAgent-controlled content fields

These categories help determine which fields participate in hashing and signing.

Agent Schema

The Agent Schema defines signed identity documents for entities that can sign and verify JACS documents. Agents are cryptographic identities, not catalogs of services or contact records.

Schema Location

https://hai.ai/schemas/agent/v1/agent.schema.json

Overview

Agent documents describe:

  • Identity and versioning
  • Agent type
  • Optional DNS-based verification domain
  • Public-key and signature metadata

Capabilities for A2A interoperability are represented in A2A Agent Cards instead of the JACS identity schema.

Agent Types

The jacsAgentType field classifies the agent:

TypeDescription
humanIndividual person
human-orgOrganization or group
hybridCombination of human and AI components
aiFully artificial intelligence

Agent Fields

FieldTypeRequiredDescription
jacsIdstring (UUID)YesUnique agent identifier
jacsVersionstring (UUID)YesCurrent version identifier
jacsVersionDatestring (date-time)YesVersion timestamp
jacsTypestringYesSet to agent
jacsOriginalVersionstring (UUID)YesFirst version identifier
jacsOriginalDatestring (date-time)YesCreation timestamp
jacsLevelstringYesDocument level
jacsAgentTypestringYesAgent classification
jacsAgentDomainstringNoDomain for DNS verification
jacsSignatureobjectNoCryptographic signature
jacsSha256stringNoContent hash

DNS Verification

Agents can link to a domain for DNSSEC-validated verification:

{
  "jacsAgentDomain": "example.com"
}

The domain should have a DNS TXT record at _v1.agent.jacs.example.com. containing the agent's public key fingerprint.

Example

{
  "$schema": "https://hai.ai/schemas/agent/v1/agent.schema.json",
  "jacsId": "550e8400-e29b-41d4-a716-446655440000",
  "jacsVersion": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "jacsVersionDate": "2024-01-15T10:30:00Z",
  "jacsType": "agent",
  "jacsOriginalVersion": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "jacsOriginalDate": "2024-01-15T10:30:00Z",
  "jacsLevel": "artifact",
  "jacsAgentType": "ai",
  "jacsAgentDomain": "agent.example.com",
  "jacsSignature": {
    "agentID": "550e8400-e29b-41d4-a716-446655440000",
    "agentVersion": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "date": "2024-01-15T10:30:00Z",
    "signature": "base64-encoded-signature...",
    "publicKeyHash": "sha256-hash-of-public-key",
    "signingAlgorithm": "ring-Ed25519",
    "fields": ["jacsId", "jacsVersion", "jacsAgentType", "jacsAgentDomain"]
  },
  "jacsSha256": "document-hash..."
}

See Also

Document Schema

The Document Schema is the generic header/signature wrapper for JACS documents. It is intentionally payload-agnostic: applications can sign arbitrary JSON content while preserving the same hashing, signature, agreement, and verification contracts across languages.

Schema Location

https://hai.ai/schemas/header/v1/header.schema.json

Overview

The header schema provides:

  • Unique document and version identifiers
  • Version history through previous-version references
  • Cryptographic signatures and hashes
  • Optional file attachments
  • Optional multi-party agreements

Core Fields

FieldTypeRequiredDescription
$schemastringYesSchema URL for validation
jacsIdstring (UUID)YesUnique document identifier
jacsTypestringYesDocument type, commonly document for generic payloads
jacsVersionstring (UUID)YesCurrent version identifier
jacsVersionDatestring (date-time)YesVersion creation timestamp
jacsOriginalVersionstring (UUID)YesFirst version identifier
jacsOriginalDatestring (date-time)YesDocument creation timestamp
jacsLevelstringYesIntended document level
jacsSignatureobjectNoCreator signature
jacsSha256stringNoCanonical content hash

Document Level

LevelDescription
rawRaw data that should not change
configConfiguration meant to be updated
artifactGenerated content that may be updated
derivedComputed from other documents

Signatures

jacsSignature contains the signing agent, timestamp, algorithm, public key hash, fields signed, and signature bytes.

{
  "jacsSignature": {
    "agentID": "550e8400-e29b-41d4-a716-446655440000",
    "agentVersion": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "date": "2024-01-15T10:30:00Z",
    "signature": "base64-encoded-signature-string",
    "publicKeyHash": "sha256-hash-of-public-key",
    "signingAlgorithm": "ring-Ed25519",
    "fields": ["jacsId", "jacsVersion", "jacsType", "content"]
  }
}

Agreements

Any signed document can include jacsAgreement for multi-party approval.

{
  "jacsAgreement": {
    "agentIDs": [
      "agent-1-uuid",
      "agent-2-uuid"
    ],
    "question": "Do you approve this payload?",
    "context": "Deployment approval",
    "quorum": 2,
    "signatures": []
  },
  "jacsAgreementHash": "hash-of-content-at-agreement-time"
}

File Attachments

Documents can include file attachments using jacsFiles:

{
  "jacsFiles": [
    {
      "mimetype": "application/pdf",
      "path": "./documents/contract.pdf",
      "embed": false,
      "sha256": "file-content-hash"
    }
  ]
}

Complete Example

{
  "$schema": "https://hai.ai/schemas/header/v1/header.schema.json",
  "jacsId": "550e8400-e29b-41d4-a716-446655440000",
  "jacsType": "document",
  "jacsVersion": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "jacsVersionDate": "2024-01-15T10:30:00Z",
  "jacsOriginalVersion": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "jacsOriginalDate": "2024-01-15T10:30:00Z",
  "jacsLevel": "artifact",
  "content": {
    "title": "Sample Document",
    "body": "This is arbitrary signed JSON content."
  },
  "jacsSignature": {
    "agentID": "550e8400-e29b-41d4-a716-446655440000",
    "agentVersion": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "date": "2024-01-15T10:30:00Z",
    "signature": "signature-base64...",
    "publicKeyHash": "public-key-hash...",
    "signingAlgorithm": "ring-Ed25519",
    "fields": ["$schema", "jacsId", "jacsType", "jacsVersion", "content"]
  },
  "jacsSha256": "document-hash..."
}

Config File Schema

This page documents the jacs.config.json schema fields. For a comprehensive configuration guide including observability setup, storage backends, zero-config quickstart, and production patterns, see the Configuration Reference.

Schema Location

https://hai.ai/schemas/jacs.config.schema.json

Minimal Configuration

{
  "$schema": "https://hai.ai/schemas/jacs.config.schema.json",
  "jacs_agent_id_and_version": "YOUR_AGENT_ID:YOUR_VERSION",
  "jacs_agent_key_algorithm": "ring-Ed25519"
}

All other settings use sensible defaults (./jacs_data, ./jacs_keys, fs storage). Override only what you need.

Fields

FieldTypeDescription
jacs_data_directorystringPath to store documents and agents
jacs_key_directorystringPath to store cryptographic keys
jacs_agent_private_key_filenamestringPrivate key filename
jacs_agent_public_key_filenamestringPublic key filename
jacs_agent_key_algorithmstringSigning algorithm
jacs_default_storagestringStorage backend
jacs_keychain_backendstringOS keychain backend: "auto", "macos-keychain", "linux-secret-service", "disabled"

Configuration Options

Key Configuration

jacs_agent_key_algorithm

Specifies the cryptographic algorithm for signing:

ValueDescription
ring-Ed25519Ed25519 signatures (recommended)
pq2025FIPS-204 ML-DSA-87 post-quantum signatures
{
  "jacs_agent_key_algorithm": "ring-Ed25519"
}

jacs_agent_private_key_filename

Name of the private key file in the key directory:

{
  "jacs_agent_private_key_filename": "private.pem"
}

If the key is encrypted, it will have .enc appended automatically when loading.

jacs_agent_public_key_filename

Name of the public key file:

{
  "jacs_agent_public_key_filename": "public.pem"
}

jacs_private_key_password

Password for encrypted private keys:

{
  "jacs_private_key_password": "your-password"
}

Warning: Do not store passwords in config files for production. Use the JACS_PRIVATE_KEY_PASSWORD environment variable or jacs keychain set (OS keychain) instead. See OS Keychain Configuration.

Storage Configuration

jacs_default_storage

Specifies where documents are stored:

ValueDescription
fsLocal filesystem
awsAWS S3 storage
haiHAI cloud storage
{
  "jacs_default_storage": "fs"
}

jacs_data_directory

Path for storing documents and agents:

{
  "jacs_data_directory": "./jacs_data"
}

Agent Identity

jacs_agent_id_and_version

Load an existing agent by ID and version:

{
  "jacs_agent_id_and_version": "550e8400-e29b-41d4-a716-446655440000:f47ac10b-58cc-4372-a567-0e02b2c3d479"
}

Schema Versions

Specify which schema versions to use:

{
  "jacs_agent_schema_version": "v1",
  "jacs_header_schema_version": "v1",
  "jacs_signature_schema_version": "v1"
}

DNS Configuration

For DNSSEC-based agent verification:

jacs_agent_domain

Domain for DNS-based public key verification:

{
  "jacs_agent_domain": "example.com"
}

jacs_dns_validate

Enable DNS TXT fingerprint validation:

{
  "jacs_dns_validate": true
}

jacs_dns_strict

Require DNSSEC validation (no fallback):

{
  "jacs_dns_strict": true
}

jacs_dns_required

Require domain and DNS validation:

{
  "jacs_dns_required": true
}

Security

jacs_keychain_backend

OS keychain backend for password storage. Controls whether JACS looks up the private key password from the OS credential store.

{
  "jacs_keychain_backend": "auto"
}
ValueDescription
"auto"Detect platform default (macOS Keychain or Linux Secret Service)
"macos-keychain"macOS Keychain
"linux-secret-service"Linux D-Bus Secret Service
"disabled"Never consult OS keychain (recommended for CI/headless)

Override with env var: JACS_KEYCHAIN_BACKEND=disabled

See OS Keychain Configuration for full details.

jacs_use_security

Enable strict security features:

{
  "jacs_use_security": "1"
}

Values: "0", "1", or "false", "true"

Observability Fields

The observability object supports logs, metrics, and tracing sub-objects. For full details on all destinations, sampling options, and production patterns, see the Configuration Reference.

Environment Variables

Configuration can be overridden with environment variables:

VariableConfig Field
JACS_PRIVATE_KEY_PASSWORDjacs_private_key_password
JACS_DATA_DIRECTORYjacs_data_directory
JACS_KEY_DIRECTORYjacs_key_directory
JACS_KEYCHAIN_BACKENDjacs_keychain_backend

See Also

JACS Attestation vs. Other Standards

JACS occupies a unique position as an agent-layer attestation framework. This page compares JACS with related standards and explains when to use them together.

Comparison Table

FeatureJACSin-toto / SLSASigstore / cosignSCITTIETF RATS / EAT
Primary domainAI agent runtimeBuild provenanceArtifact signingTransparency logsHardware/platform attestation
Identity modelDecentralized (key pairs)Build system certsKeyless (OIDC)Issuer certsPlatform certs
Agent-nativeYesNoNoNoPartial
Offline verificationYesYes (with keys)No (requires Rekor)No (requires log)Depends
Multi-agent quorumYes (M-of-N)NoNoNoNo
Evidence normalizationYes (A2A, email, JWT, custom)NoNoNoPartial (EAT claims)
Transform receiptsYes (derivation chains)Yes (build steps)NoNoNo
Probabilistic claimsYes (confidence + assurance)NoNoNoNo
Post-quantumYes (ML-DSA-87)NoNoNoDepends
Central infrastructureNot requiredNot requiredRequired (Fulcio + Rekor)Required (transparency log)Depends
Schema formatJSON Schema + JCSin-toto layoutSigstore bundleSCITT receiptCBOR/COSE

JACS vs. in-toto / SLSA

Domain difference: in-toto and SLSA focus on build provenance -- proving that a software artifact was built by a specific builder from specific source code. JACS focuses on runtime agent actions -- proving that a specific agent performed a specific action with specific evidence.

Interoperability: JACS exports attestations as DSSE (Dead Simple Signing Envelope) documents, the same format used by in-toto v1.0+. This means:

  • A JACS attestation can include an in-toto predicate type URI
  • SLSA verifiers can validate the DSSE envelope structure
  • JACS and in-toto attestations can coexist in the same verification pipeline

When to use both: If your workflow includes both software builds (use SLSA/in-toto for build provenance) and AI agent actions (use JACS for runtime attestation), you can link them via derivation chains.

JACS vs. Sigstore / cosign

Domain difference: Sigstore provides signing infrastructure (Fulcio CA, Rekor transparency log) and cosign is a tool for signing container images and artifacts. JACS provides its own signing with decentralized identity.

Key difference: Sigstore's keyless signing relies on centralized OIDC identity providers and a public transparency log. JACS uses self-managed key pairs and does not require any centralized infrastructure.

When to use both: Sigstore for container image signing in CI/CD pipelines. JACS for AI agent action signing at runtime. A planned Sigstore bundle verification adapter (N+2) would let JACS attestations reference Sigstore signatures as evidence.

JACS vs. SCITT

Most overlap. SCITT (Supply Chain Integrity, Transparency and Trust) defines a centralized transparency service for recording signed statements about artifacts.

Key difference: SCITT requires a transparency log (centralized notary). JACS is fully decentralized and offline-capable. JACS verification works without contacting any server.

Complementary use: JACS signs and attests. SCITT logs. An organization could use JACS to create signed attestations and then submit them to a SCITT transparency log for auditability, getting the benefits of both decentralized creation and centralized discoverability.

JACS vs. IETF RATS / EAT

Layer difference: RATS (Remote ATtestation procedureS) and EAT (Entity Attestation Token) focus on hardware and platform attestation -- proving that a device or execution environment is in a known-good state. JACS fills the software agent layer above hardware.

Alignment opportunity: JACS claim names could align with IANA-registered EAT claim types where they overlap. A JACS attestation could reference a RATS attestation result as evidence, creating a trust chain from hardware to agent.

IETF drafts of interest:

  • draft-huang-rats-agentic-eat-cap-attest-00 -- Capability attestation for agents, directly aligned with JACS claims model
  • draft-messous-eat-ai-00 -- EAT profile for AI agents
  • draft-jiang-seat-dynamic-attestation-00 -- Dynamic attestation for runtime assertions

JACS vs. CSA Agentic Trust Framework

The Cloud Security Alliance's Agentic Trust Framework defines progressive trust levels that map directly to JACS's trust model:

CSA LevelJACS EquivalentVerification
NoneNo JACSNo signing
BasicOpenValid signature accepted
StandardVerifiedTrust store + DNS verification
EnhancedStrictAttestation-level evidence required

When to Use JACS

Use JACS when you need:

  • Agent identity that works without PKI/CA infrastructure
  • Non-repudiable action logging for AI agent workflows
  • Multi-agent authorization with quorum (M-of-N approval)
  • Offline verification without centralized services
  • Evidence-backed trust that goes beyond simple signing
  • Post-quantum readiness for long-lived agent identities

When to Use JACS Alongside Other Tools

ScenarioJACS + ...
CI/CD pipeline with AI agentsJACS (agent actions) + SLSA (build provenance)
Enterprise with compliance requirementsJACS (signing) + SCITT (transparency log)
IoT/edge with hardware attestationJACS (agent layer) + RATS/EAT (hardware layer)
Container-based agent deploymentJACS (runtime signing) + cosign (image signing)

Security Model

JACS implements a comprehensive security model designed to ensure authenticity, integrity, and non-repudiation for all agent communications and documents.

Security Model (v0.6.0+)

  • Passwords: The private key password can be provided via JACS_PRIVATE_KEY_PASSWORD environment variable, JACS_PASSWORD_FILE, or the OS keychain (macOS Keychain / Linux Secret Service). It is never stored in config files.
  • Keys: Private keys are encrypted at rest (AES-256-GCM with PBKDF2, 600k iterations). Public keys and config may be stored on disk.
  • Path validation: All paths built from untrusted input (e.g. publicKeyHash, filenames) are validated via require_relative_path_safe() to prevent directory traversal. This single validation function is used in data and key directory path builders and the trust store. It rejects empty segments, ., .., null bytes, and Windows drive-prefixed paths.
  • Trust ID canonicalization: Trust-store operations normalize canonical agent docs (jacsId + jacsVersion) into a safe UUID:VERSION_UUID identifier before filesystem use, preserving path-safety checks while supporting standard agent document layout.
  • Filesystem schema policy: Local schema loading is disabled by default and requires JACS_ALLOW_FILESYSTEM_SCHEMAS=true. When enabled, schema paths must remain within configured roots (JACS_DATA_DIRECTORY and/or JACS_SCHEMA_DIRECTORY) after normalized/canonical path checks.
  • Network endpoint policy: Registry verification requires HTTPS for JACS_REGISTRY_URL (legacy alias: HAI_API_URL). Localhost HTTP is allowed for local testing only.
  • Strict JSON ingress: Raw JSON crossing a signing, verification, hashing, schema, or protocol boundary must pass the shared jacs-core strict decoder. Duplicate decoded object names are rejected recursively before a serde_json::Value can collapse them; escape-equivalent names are duplicates, while repeated array elements remain valid.
  • No secrets in config: Config files must not contain passwords or other secrets. The example config (jacs.config.example.json) does not include jacs_private_key_password.
  • Dependency auditing: Run cargo audit (Rust), npm audit (Node.js), or pip audit (Python) to check for known vulnerabilities.

Core Security Principles

1. Cryptographic Identity

Every JACS agent has a unique cryptographic identity:

  • Key Pair: Each agent possesses a private/public key pair
  • Agent ID: Unique UUID identifying the agent
  • Public Key Hash: SHA-256 hash of the public key for verification
{
  "jacsSignature": {
    "agentID": "550e8400-e29b-41d4-a716-446655440000",
    "publicKeyHash": "sha256-of-public-key",
    "signingAlgorithm": "pq2025"
  }
}

Native signing defaults to post-quantum pq2025 (ML-DSA-87 / FIPS-204). Explicit ed25519 creation is supported and emits the canonical ring-Ed25519 label. Routine rotation preserves the current algorithm; Ed25519 can explicitly upgrade to pq2025, while PQ-to-Ed25519 downgrade is rejected.

2. Document Integrity

All documents include cryptographic guarantees:

  • Signature: Cryptographic signature over specified fields
  • Hash: SHA-256 hash of document contents
  • Version Tracking: Immutable version history

3. Non-Repudiation

Signatures provide proof of origin:

  • Agents cannot deny signing a document
  • Timestamps record when signatures were made
  • Public keys enable independent verification

Security Audit (audit())

JACS provides a read-only security audit that checks configuration, directories, secrets, trust store, storage, quarantine/failed files, and optionally re-verifies recent documents. It does not modify state.

Purpose: Surface misconfiguration, missing keys, unexpected paths, and verification failures in one report.

Options (all optional):

  • config_path: Path to jacs.config.json (default: 12-factor load)
  • data_directory / key_directory: Override paths
  • recent_verify_count: Number of recent documents to re-verify (default 10, max 100)

Return structure: AuditResult with overall_status, risks (list of AuditRisk), health_checks (list of ComponentHealth), summary, checked_at, and optional quarantine_entries / failed_entries.

Rust:

#![allow(unused)]
fn main() {
use jacs::audit::{audit, AuditOptions};

let result = audit(AuditOptions::default())?;
println!("{}", jacs::format_audit_report(&result));
}

Python:

import jacs.simple as jacs

result = jacs.audit()  # dict with risks, health_checks, summary, overall_status
print(f"Risks: {len(result['risks'])}, Status: {result['overall_status']}")

Node.js:

import * as jacs from '@hai.ai/jacs/simple';

const result = jacs.audit({ recentN: 5 });
console.log(`Risks: ${result.risks.length}, Status: ${result.overall_status}`);

Available in the language bindings for local diagnostics and automation.

Threat Model

Protected Against

ThreatProtection
TamperingContent hashes detect modifications
ImpersonationCryptographic signatures verify identity
Replay AttacksHTTP/RPC payload and W3C request proofs use timestamp validation plus atomic nonce consumption; document signatures remain durable by design
Man-in-the-MiddleDNS verification via DNSSEC; TLS certificate validation
Key CompromiseKey rotation through versioning
Weak PasswordsMinimum 28-bit entropy enforcement (35-bit for single class)

Trust Assumptions

  1. Private keys are kept secure
  2. Cryptographic algorithms are sound
  3. DNS infrastructure (when used) is trustworthy

Replay Store Deployment

The default replay backend is a bounded, atomic, process-local Moka cache. It accepts exactly one concurrent use of a nonce within one process and is suitable for local development or a genuinely single-process service.

Multi-replica production services must install an implementation of the Rust ReplayStore trait backed by a shared atomic primitive (for example Redis SET key value NX EX ttl or a database unique insert), then set JACS_REQUIRE_SHARED_REPLAY_STORE=true. With that setting JACS fails closed if the installed backend reports itself as process-local or returns an error. The library never silently falls back to memory after a shared-store failure.

Replay decisions emit structured events without nonce contents and increment jacs_replay_checks_total{backend,outcome}. Use JACS_PAYLOAD_MAX_REPLAY_SECONDS to configure the default payload window. For an accepted future-skewed payload, JACS retains its nonce through the signed timestamp plus that window, not merely for a fixed interval from verification.

Remote JWKS Retrieval

A2A Agent Card verification keeps JWKS network access disabled unless JACS_ALLOW_JWKS_FETCH=true (or the broader JACS_ALLOW_NETWORK=true) is set. Even after that opt-in, JACS accepts only HTTPS origins, resolves and validates every redirect hop, rejects private/reserved IP answers, pins the validated DNS answer into the HTTP client, disables proxy resolution, allows at most three same-origin redirects, and requires a JSON Content-Type. The whole operation is limited to five seconds and 256 KiB.

Loopback HTTP remains available for deliberate local development only when both the network capability and JACS_ALLOW_PRIVATE_JWKS=true are set. Do not enable the private-JWKS override in a service that assesses Agent Cards supplied by untrusted callers.

Request-Bound HTTP Authorization

Use the request builder exposed by your API with the actual HTTP method, absolute URL, exact body bytes (empty bytes for no body), and a service-specific audience:

APIRequest-bound builder
Rust SimpleAgentbuild_request_auth_header(method, url, body, audience)
Python SimpleAgentbuild_request_auth_header(method, url, body, audience)
Node JacsSimpleAgentbuildRequestAuthHeader(method, url, body, audience)
Go JacsSimpleAgentBuildRequestAuthHeader(method, url, body, audience)

The resulting JACS v2... header authenticates the signer/key ID, normalized scheme/authority/path/query, SHA-256 content digest, audience, timestamp, and nonce. verify_request_auth_header requires the trusted key and the actual request context; any method, endpoint, query, body, signer, or audience substitution fails before replay state is consumed.

Build the header after the final body serialization and send those same bytes. On verification, reconstruct the absolute URL from trusted server/framework configuration rather than trusting an attacker-controlled forwarding header. The standard Rust verifier atomically consumes replay state after all other checks pass. Servers with an application-owned shared store use verify_request_auth_header_with_trusted_key_without_replay, then grant access only if an atomic insert such as Redis SET ... NX EX ... accepts the returned nonce. Use request_auth_replay_ttl(&verified_claims, max_age) for the Redis expiry (or retain it longer). A fixed max_age TTL is insufficient when the signer's accepted clock skew is positive: the credential's absolute expiry is issued_at + max_age.

The older no-argument Rust build_auth_header and equivalent legacy class methods sign only identity, time, and nonce. They remain available for source compatibility and emit a WARN because a first-use credential could be moved to a different request. The additive request-bound methods require method, URL, body, and audience and emit v2. Strict deployments can reject legacy construction with JACS_REJECT_UNBOUND_AUTH_HEADER=true.

Encrypted-Key KDF Resource Policy

V2 private-key envelopes accept only the versioned Argon2id resource profile: 8,192–19,456 KiB memory, one or two passes, and parallelism one. JACS validates these values before base64 allocation or Argon2 execution, and also caps the serialized envelope/ciphertext and enforces exact salt/nonce sizes. A policy rejection emits encrypted_key_kdf_policy_rejected at WARN and increments jacs_kdf_policy_rejections_total{kdf="Argon2id",profile="v2"}; neither signal contains passwords, key bytes, or ciphertext.

Signed Response and Event Contract

sign_response emits response envelope version 2.0.0 with signature scope jacs-response-v2. Its domain-separated signature authenticates the complete envelope except for the signature bytes themselves: payload, protocol version/type, issuer, document ID, payload hash, timestamp, signer, algorithm, public-key hash, and any additional fields. This is a protocol-specific contract; do not replace it with generic document verification or verification of data alone.

Use verify_response_json_with_key in Rust or unwrap_signed_event / unwrapSignedEvent in a native binding with a trusted map from signer ID to public key. Trusted keys may be raw bytes/base64 or canonical PEM, depending on the binding. On success the bindings return only verified data plus provenance:

{
  "data": {"decision": "allow"},
  "verified": true,
  "status": "verified",
  "signerId": "agent-id:version",
  "timestamp": "2026-07-09T12:00:00Z",
  "algorithm": "pq2025",
  "documentId": "2f1e2bdb-55f4-4c99-8f42-d45249808f4b"
}

Plain events, legacy payload-only response envelopes, unknown signers, and invalid signatures are errors. There is no successful verified: false result; callers must never dispatch an event by catching that error and reparsing its untrusted input.

These protocols authenticate control of the private key corresponding to the public key that the verifier selected. Signed agentID, name, domain, and time fields are not independent proof of a person, organization, service, or DNS identity. Establish that association through a pinned key, strict local trust configuration, or another explicitly chosen verification policy before making an authorization decision.

Signature Process

Signing a Document

  1. Field Selection: Determine which fields to sign
  2. Canonicalization: Serialize fields deterministically
  3. Signature Generation: Sign with private key
  4. Hash Computation: Compute SHA-256 of signed document
import jacs
import json

agent = jacs.JacsAgent()
agent.load('./jacs.config.json')

# Create signed document
doc = agent.create_document(json.dumps({
    'title': 'Confidential Report',
    'content': 'Sensitive data here'
}))

# Document now includes jacsSignature and jacsSha256

Verifying a Document

  1. Hash Verification: Recompute hash and compare
  2. Signature Verification: Verify signature with public key
  3. Agent Verification: Optionally verify agent identity via DNS
is_valid = agent.verify_document(doc_json)
is_signature_valid = agent.verify_signature(doc_json)

Key Management

Compatibility key binding (P2)

An agent's ES256 ecosystem_signing key is authorized per-export by a compatibility key binding: a compatibilityKeyBinding JACS document signed by the current native root, persisted as one canonical-JSON file (jacs_keys/jacs.compat-binding.json). Lifecycle rules:

  • granting or widening a scope always requires the native root's signature — the ES256 holder cannot self-escalate;
  • re-issue replaces the file (latest issuedAt wins; no version chains);
  • native key rotation supersedes the binding — verification fails with a "re-issue" error until a new binding is signed by the current root;
  • an expired binding denies export; expiresAt: null is permitted in P2;
  • default issuance grants only the identity scopes (jwks, did, a2a-agent-card, w3c-agent-identity); content scopes (ap2-mandate, agreement-vc) require an explicit re-issue.

Be precise about what this is: scope checks are a locally enforced authorization policy and an auditable delegation record for relying parties — not isolation. Both private keys live in the same directory under the same password; a compromised host is outside this model. The sound property is non-self-escalation via the native-root signature requirement.

The ES256 private key is encrypted at rest with the same AES-256-GCM + Argon2id envelope as the native root key — the post-quantum library is a signing primitive, not an encryption primitive, so compatibility keys reuse the existing audited envelope rather than inventing a PQ-encrypted key format.

Targeted content exports (P2)

Content exporters (the AP2 mandate export, the Agreement-v2-as-VC export) sign ecosystem artifacts with the ES256 compatibility key through purpose-built, schema-pinned paths — there is no generic "sign this document with algorithm X" API. Two properties matter here:

  • Classical verification is not PQ trust. A stock JOSE/Data Integrity verifier can check the ES256 signature with nothing but the public key; that proves possession of the compatibility key, not the agent's native identity. Tracing the export to that root additionally requires verifying the native-root-signed compatibility key binding (jacs agent export-compat-binding).
  • Native documents are never mutated. Exports are derived views; the native jacsSignature keeps the agent's selected algorithm and the source document's bytes and verification are unchanged after every export. These are targeted ecosystem artifacts, not JACS projections — native documents never gain proof or other projection fields.

Exports are one-way in P2: JACS emits these artifacts, but verifying incoming AP2 mandates or third-party VCs is out of scope — use the target ecosystem's stock verifiers for that.

Key Generation

JACS generates cryptographic key pairs during agent creation:

# Keys are created in the configured key directory
jacs_keys/
├── private.pem    # Private key (keep secure!)
└── public.pem     # Public key (can be shared)

Key Protection

Encryption at Rest:

Private keys are encrypted using AES-256-GCM with a key derived via PBKDF2-HMAC-SHA256 (600,000 iterations). Never store the password in config files.

# Option 1: Environment variable (recommended for CI/servers)
export JACS_PRIVATE_KEY_PASSWORD="secure-password"

# Option 2: OS keychain (recommended for developer workstations)
jacs keychain set --agent-id <YOUR_AGENT_UUID>

Important: The CLI can prompt for the password during jacs init, but scripts and servers must set JACS_PRIVATE_KEY_PASSWORD as an environment variable or use the OS keychain.

OS Keychain Integration:

On macOS and Linux desktops, JACS can store and retrieve the private key password from the OS credential store, eliminating the need for environment variables or plaintext password files during day-to-day development:

  • macOS: Uses Security.framework (Keychain Access)
  • Linux: Uses the freedesktop.org D-Bus Secret Service API (GNOME Keyring, KDE Wallet, KeePassXC)

Each password is keyed by agent ID, so multiple agents can coexist on the same machine without overwriting each other. Store your password once with jacs keychain set --agent-id <ID>, and all subsequent JACS operations will find it automatically. The password resolution order is:

  1. JACS_PRIVATE_KEY_PASSWORD env var (highest priority -- explicit always wins)
  2. JACS_PASSWORD_FILE / legacy .jacs_password file
  3. OS keychain keyed by agent ID (lowest priority among explicit sources)

To disable keychain lookups (recommended for CI and headless environments):

export JACS_KEYCHAIN_BACKEND=disabled

Or in jacs.config.json:

{
  "jacs_keychain_backend": "disabled"
}

The keychain stores the password under service name jacs-private-key with user default. Multiple agents on one machine can use agent-specific passwords via the *_for_agent() API variants.

Security note: The OS keychain is encrypted at rest by the OS and unlocked by the user's login session. It is the same mechanism used by git, ssh-agent, docker, and other CLI tools. The keychain feature is optional and not compiled into the jacs core crate by default -- it is enabled by default only in jacs-cli.

Password Entropy Requirements:

JACS enforces password entropy minimums for private key encryption. Password validation is performed at encryption time, and weak passwords are rejected with helpful error messages:

  • Minimum 28-bit entropy for passwords with 2+ character classes (mixed case, numbers, symbols)
  • Minimum 35-bit entropy for single-character-class passwords (e.g., all lowercase)
  • Entropy is calculated based on character class diversity and length
  • Weak passwords result in immediate rejection during key encryption
  • Error messages guide users toward stronger password choices

Example of rejected weak passwords:

  • password - Too common and predictable
  • 12345678 - Insufficient character diversity
  • abc - Too short

File Permissions:

chmod 700 ./jacs_keys
chmod 600 ./jacs_keys/private.pem

Key Rotation

Update agent version to rotate keys:

  1. Generate new key pair
  2. Create new agent version
  3. Sign new version with old key
  4. Update configuration to use new keys

Every rotation resolves to pq2025, including rotation of an Ed25519 agent — rotation is the designated Ed25519 → post-quantum migration path. Rotation also supersedes the ES256 compatibility key binding: re-issue it under the new native root (jacs agent issue-compat-binding) before the next ecosystem export.

TLS Certificate Validation

JACS includes configurable TLS certificate validation for secure network communication.

Default Behavior (Development)

By default, JACS warns about invalid TLS certificates but accepts them to facilitate development environments with self-signed certificates:

WARNING: Invalid TLS certificate detected. Set JACS_STRICT_TLS=true for production.

Production Configuration

For production deployments, enable strict TLS validation:

export JACS_STRICT_TLS=true

When enabled, JACS will:

  • Reject connections with invalid, expired, or self-signed certificates
  • Enforce proper certificate chain validation
  • Fail fast with clear error messages for certificate issues

Implementation: Certificate validation logic is located in jacs/src/schema/utils.rs.

Security Implications

ModeBehaviorUse Case
Default (dev)Warn on invalid certs, allow connectionLocal development, testing
Strict (JACS_STRICT_TLS=true)Reject invalid certsProduction, staging

For registry verification endpoints, JACS_REGISTRY_URL (legacy HAI_API_URL) must use HTTPS. HTTP is only allowed for localhost test endpoints.

Signature Timestamp Validation

A document's signed timestamp records the signer's claimed signing time. It is not an independently trusted timestamp or proof that a later request is fresh. JACS document and agent-identity signatures are archival: the generic signature timestamp validator does not impose a maximum age.

What the Timestamp Check Establishes

validate_signature_timestamp parses an RFC 3339 timestamp and rejects a date more than five minutes in the future relative to the verifier's clock. Older dates pass this timestamp check; they do not need to fall within five minutes of the current time. The signature, content hash and applicable trust policy must still be checked separately.

JACS_MAX_SIGNATURE_AGE_SECONDS is not a supported document-signature setting in the current runtime. Applications that need an expiration or current-authority policy must evaluate it explicitly; successful archival verification alone does not authorize a new action.

Freshness and Replay Protection

A valid timestamp does not prevent copying a signed document or replaying a request. Use the request-bound HTTP authorization contract for actual HTTP actions, with the expected method, URL, body and atomic nonce consumption. The replay store must cover the service's deployment scope; a process-local store does not coordinate multiple replicas.

HTTP/RPC payload verification has a separate replay window, controlled by JACS_PAYLOAD_MAX_REPLAY_SECONDS (default 300 seconds). That window does not change archival document-signature age semantics. Strict A2A admission separately checks the compatibility binding's issued time and expiry; see Compatibility key binding.

Clock Synchronization

Keep verifier clocks synchronized. The five-minute future tolerance permits bounded clock skew; it does not establish when an artifact was actually created, prevent pre-signing, or prove that a past authorization remains current.

Verification Claims

Agents can claim a verification level that determines security requirements. This follows the principle: "If you claim it, you must prove it."

Claim Levels

ClaimRequired ConditionsBehavior
unverified (default)NoneRelaxed DNS/TLS settings allowed; self-asserted identity
verifiedDomain with DNSSECStrict TLS, strict DNS with DNSSEC validation required
verified-registryAbove + registry verificationMust be registered and verified by a JACS registry
verified-hai.ai (legacy alias)Same as verified-registryBackward-compatible alias

Setting a Verification Claim

Add the jacsVerificationClaim field to your agent definition:

{
  "jacsAgentType": "ai",
  "jacsVerificationClaim": "verified",
  "jacsAgentDomain": "myagent.example.com"
}

Claim Enforcement

When an agent claims verified, verified-registry, or legacy verified-hai.ai:

  1. Domain Required: The jacsAgentDomain field must be set
  2. Strict DNS: DNS lookup uses DNSSEC validation (no insecure fallback)
  3. DNS Required: Public key fingerprint must match DNS TXT record
  4. Strict TLS: TLS certificate validation is mandatory (no self-signed certs)

For verified-registry (or legacy verified-hai.ai) claims, additional enforcement:

  1. Registry Registration: Agent must be registered with the configured registry (for HAI-hosted registry, hai.ai)
  2. Public Key Match: Registered public key must match the agent's key
  3. Network Required: Verification fails if the registry API is unreachable

Backward Compatibility

  • Agents without jacsVerificationClaim are treated as unverified
  • Existing agents continue to work with their current DNS settings
  • No breaking changes for agents that don't opt into verified status

Error Messages

If verification fails, clear error messages explain what's wrong:

Verification claim 'verified' failed: Verified agents must have jacsAgentDomain set.
Agents claiming 'verified' must meet the required security conditions.
Verification claim 'verified-registry' failed: Agent 'uuid' is not registered with the configured registry.
Agents claiming 'verified-registry' must be registered with a reachable registry endpoint.

Security Considerations

  1. No Downgrade: Once an agent claims verified, it cannot be verified with relaxed settings
  2. Claim Changes: Changing the claim requires creating a new agent version
  3. Network Dependency: verified-registry requires network access to the registry endpoint
  4. Audit Trail: Verification claim and enforcement results are logged

DNS-Based Verification

JACS supports DNSSEC-validated identity verification:

How It Works

  1. Agent publishes public key fingerprint in DNS TXT record
  2. Verifier queries DNS for _v1.agent.jacs.<domain>.
  3. DNSSEC validates the response authenticity
  4. Fingerprint is compared against agent's public key

Configuration

{
  "jacs_agent_domain": "myagent.example.com",
  "jacs_dns_validate": true,
  "jacs_dns_strict": true
}

Security Levels

ModeDescription
jacs_dns_validate: falseNo DNS verification
jacs_dns_validate: trueAttempt DNS verification, allow fallback
jacs_dns_strict: trueRequire DNSSEC validation
jacs_dns_required: trueFail if domain not present

Trust Store Management

JACS maintains a trust store for managing trusted agent relationships.

Trusting Agents

Before trusting an agent, JACS performs public key hash verification:

# Trust an agent after verifying their public key hash
agent.trust_agent(agent_id, public_key_hash)

Untrusting Agents

The untrust_agent() method properly handles the case when an agent is not in the trust store:

try:
    agent.untrust_agent(agent_id)
except AgentNotTrusted as e:
    # Agent was not in the trust store
    print(f"Agent {agent_id} was not trusted: {e}")

Trust Store Security

OperationValidation
trust_agent()UUID format validation, path traversal rejection, public key hash verification, self-signature verification before adding
untrust_agent()UUID format validation, path containment check, returns AgentNotTrusted error if agent not found
get_trusted_agent()UUID format validation, path containment check
is_trusted()UUID format validation, safe lookup without side effects
Key cache (load_public_key_from_cache)require_relative_path_safe() rejects traversal in publicKeyHash
Key cache (save_public_key_to_cache)require_relative_path_safe() rejects traversal in publicKeyHash

Path Traversal Protection (v0.6.0): All trust store operations that construct file paths from agent IDs or key hashes use defense-in-depth:

  1. UUID format validation: Agent IDs must match UUID:UUID format (rejects special characters)
  2. Path character rejection: Explicit rejection of .., /, \, and null bytes
  3. Path containment check: For existing files, canonicalized paths are verified to stay within the trust store directory
  4. require_relative_path_safe(): Key hashes are validated to prevent traversal before constructing cache file paths

Best Practices

  1. Verify Before Trust: Always verify an agent's public key hash through an out-of-band channel before trusting
  2. Audit Trust Changes: Log all trust store modifications for security auditing
  3. Periodic Review: Regularly review and prune the trust store

Agreement Security

Multi-party agreements provide additional security:

Agreement Structure

{
  "jacsAgreement": {
    "agentIDs": ["agent-1", "agent-2", "agent-3"],
    "signatures": [
      {
        "agentID": "agent-1",
        "signature": "...",
        "responseType": "agree",
        "date": "2024-01-15T10:00:00Z"
      }
    ]
  },
  "jacsAgreementHash": "hash-at-agreement-time"
}

Agreement Guarantees

  1. Content Lock: jacsAgreementHash ensures all parties agreed to same content
  2. Individual Consent: Each signature records explicit agreement
  3. Response Types: Support for agree, disagree, or reject
  4. Timestamp: Records when each party signed

Request/Response Security

For HTTP, use the request-bound authorization and signed response/event contracts above. Generic document signing does not bind an HTTP method, URL, audience, or request body, and generic verification of only the response payload does not authenticate the v2 envelope metadata.

MCP messages that need portable artifact integrity can carry ordinary signed JACS documents. That is separate from HTTP request authentication: the MCP stdio transport has no HTTP target to bind, and authorization remains the MCP host's responsibility.

Algorithm Security

Supported Algorithms

AlgorithmTypeRole
pq2025Post-Quantum (FIPS-204 ML-DSA-87)Default native root; every rotation resolves to it
ring-Ed25519Elliptic CurveSupported native creation, signing, and verification; user-facing input alias: ed25519
ES256Elliptic Curve (P-256)Compatibility key for targeted ecosystem exports only — never a valid native jacsSignature algorithm

Algorithm Selection

Native JACS signing defaults to pq2025; explicit Ed25519 creation is supported without substitution. Rotation always converges to pq2025.

An ES256 content signature does not create native JACS trust: classical verification proves possession of the compatibility key only. The native-root-signed compatibility key binding is what ties that key to the agent — see Compatibility key binding (P2).

Security Best Practices

1. Key Storage

# Never commit keys to version control
echo "jacs_keys/" >> .gitignore

# Secure file permissions
chmod 700 ./jacs_keys
chmod 600 ./jacs_keys/private.pem

2. Password Handling

# Option A: Use environment variables (CI, servers)
export JACS_PRIVATE_KEY_PASSWORD="$(pass show jacs/key-password)"

# Option B: Use OS keychain (developer workstations)
jacs keychain set --agent-id <AGENT_UUID>  # stores password securely in OS credential store

# Option C: Disable keychain for headless/CI environments
export JACS_KEYCHAIN_BACKEND=disabled

3. Transport Security

Always use TLS for network communication:

# HTTPS for web transport
client = JACSMCPClient(
    "https://localhost:8000/sse",
    expected_peer_agent_id="SERVER_AGENT_ID",
)  # TLS plus an explicit signer pin
# Plain HTTP remains loopback-only and should not be exposed remotely.

4. Verification Policies

{
  "jacs_dns_strict": true,
  "jacs_dns_required": true,
  "jacs_enable_filesystem_quarantine": "true"
}

5. Audit Logging

Enable observability for security auditing:

{
  "observability": {
    "logs": {
      "enabled": true,
      "level": "info"
    }
  }
}

Security Checklist

Development

  • Generate unique keys for each environment
  • Never commit private keys
  • Use test keys separate from production

Production

  • Encrypt private keys at rest
  • Use environment variables or OS keychain for secrets (never store jacs_private_key_password in config)
  • Set JACS_KEYCHAIN_BACKEND=disabled on CI/headless servers
  • Enable DNS verification
  • Configure strict security mode
  • Enable audit logging
  • Use TLS for all network transport
  • Restrict key file permissions (0600 for keys, 0700 for key directory)
  • Implement key rotation policy
  • Set JACS_STRICT_TLS=true for certificate validation
  • Use strong passwords (28+ bit entropy, 35+ for single character class)
  • Enable signature timestamp validation
  • Verify public key hashes before trusting agents
  • Run cargo audit / npm audit / pip audit regularly for dependency vulnerabilities

Verification

  • Always verify documents before trusting
  • Verify agent signatures
  • Check agreement completeness
  • Validate DNS records when required

Security Considerations

Supply Chain

  • Verify JACS packages are from official sources
  • Use package checksums
  • Keep dependencies updated

Side Channels

  • Use constant-time comparison for signatures
  • Protect against timing attacks
  • Secure memory handling for keys

Recovery

  • Backup key material securely
  • Document key recovery procedures
  • Plan for key compromise scenarios

Troubleshooting Verification Claims

Common Issues and Solutions

"Verified agents must have jacsAgentDomain set"

Problem: You set jacsVerificationClaim to verified but didn't specify a domain.

Solution: Either add a domain or use unverified:

// Option 1: Add a domain (recommended for production)
{
  "jacsVerificationClaim": "verified",
  "jacsAgentDomain": "myagent.example.com"
}

// Option 2: Use unverified if DNS verification isn't needed
{
  "jacsVerificationClaim": "unverified"
}

"Agent is not registered with the registry"

Problem: You're using verified-registry (or legacy verified-hai.ai) but the agent isn't registered.

Solution:

  1. Register your agent with your configured registry (for HAI-hosted registry, hai.ai)
  2. Or use verified for DNS-only verification:
{
  "jacsVerificationClaim": "verified",
  "jacsAgentDomain": "myagent.example.com"
}

"Cannot downgrade from 'verified' to 'unverified'"

Problem: You're trying to change an existing agent's claim to a lower level.

Solution: Verification claims cannot be downgraded for security. Options:

  1. Keep the current claim level
  2. Create a new agent with the desired claim level
  3. If this is a test/development scenario, start fresh
# Create a new agent instead
jacs quickstart --name replacement-agent --domain replacement.example.com

"DNS fingerprint mismatch"

Problem: The public key hash in DNS doesn't match your agent's key.

Solution:

  1. Regenerate the DNS record with your current keys:
    jacs dns-record
    
  2. Update your DNS TXT record with the new value
  3. Wait for DNS propagation (can take up to 48 hours)

"Strict DNSSEC validation failed"

Problem: Your domain doesn't have DNSSEC enabled.

Solution:

  1. Enable DNSSEC with your domain registrar
  2. Publish DS records at the parent zone
  3. Or use verified with non-strict DNS (development only)

Claim Level Reference

ClaimSecurity LevelRequirements
unverified0 (lowest)None - self-asserted identity
verified1Domain + DNS TXT record + DNSSEC
verified-registry2 (highest)Above + registry registration
verified-hai.ai (legacy alias)2 (highest)Alias of verified-registry

Upgrade vs Downgrade Rules

  • Upgrades allowed: unverifiedverifiedverified-registry (legacy alias verified-hai.ai is same level)
  • Downgrades blocked: Cannot go from higher to lower claim
  • Same level allowed: Can update agent while keeping same claim

Quick Diagnostic Commands

# Check your agent's current claim
jacs info | grep jacsVerificationClaim

# Verify DNS record is correct
jacs dns-check

# Test verification
jacs verify --agent your-agent-id:version

See Also

Key Rotation

Key rotation is the process of replacing an agent's cryptographic keys while preserving the ability to verify documents signed with previous keys. JACS implements version-aware key management to support secure key lifecycle operations.

Why Key Rotation Matters

Key Compromise Recovery

When a private key is compromised, the agent must be able to:

  • Generate new keys and continue operating
  • Revoke trust in the compromised key
  • Maintain verifiability of documents signed before the compromise

Cryptographic Agility

Cryptographic algorithms evolve. Key rotation enables:

  • Migration from older algorithms to newer ones
  • Transition to post-quantum cryptography when needed
  • Algorithm upgrades without breaking existing signatures

Migrating Ed25519 Agents to pq2025

Rotation always resolves to pq2025 — it is the designated migration path for Ed25519 agents:

jacs agent rotate-keys
  • An Ed25519 agent that rotates (with or without an --algorithm argument) gets a pq2025 root; the config is stamped pq2025; the transition proof is signed by the old Ed25519 key, so the migration is cryptographically authorized by the previous root.
  • Rotating to Ed25519 (or any non-PQ algorithm) is a typed error.
  • Until it rotates, an Ed25519 agent keeps signing normally with Ed25519.
  • Documents signed by the old Ed25519 key remain verifiable after migration (see Cross-Algorithm Verification in the Algorithm Selection Guide).
  • Rotation supersedes the ES256 compatibility key binding: the binding is signed by the native root, so after rotation ecosystem exports fail with a "re-issue" error until jacs agent issue-compat-binding runs under the new native root.
  • Rotation applies to the native root only — rotating the ES256 compatibility key itself is out of scope in P2.

Compliance Requirements

Many security standards require periodic key rotation:

  • PCI-DSS mandates regular key changes
  • SOC 2 requires key management policies
  • NIST guidelines recommend rotation schedules

Agent Versioning

JACS uses a versioned identity model where each key rotation creates a new agent version.

Version Format

Agent identifiers follow the format: {agent_id}:{version_uuid}

  • jacsId: The stable agent identity (UUID v4) - never changes
  • jacsVersion: Current version UUID - changes on each update
  • jacsPreviousVersion: Links to the prior version
  • jacsOriginalVersion: The first version ever created
{
  "jacsId": "550e8400-e29b-41d4-a716-446655440000",
  "jacsVersion": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "jacsPreviousVersion": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "jacsOriginalVersion": "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
}

Version Chain

Each version forms a linked chain back to the original:

Original (v1) <-- Previous (v2) <-- Current (v3)
   |                 |                  |
 key-A             key-B              key-C

This chain provides an audit trail of all key changes and allows verification of any version.

Version-Aware Verification

The critical insight enabling key rotation is that signatures contain both the agent ID and the version that created them.

Signature Structure

{
  "jacsSignature": {
    "agentID": "550e8400-e29b-41d4-a716-446655440000",
    "agentVersion": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
    "publicKeyHash": "sha256-of-public-key-A",
    "signingAlgorithm": "ring-Ed25519",
    "signature": "base64-encoded-signature",
    "date": "2024-01-15T10:00:00Z"
  }
}

Key Resolution Process

When verifying a signature:

  1. Extract agentVersion and publicKeyHash from the signature
  2. Look up the public key that was active for that version
  3. Verify the signature using that historical key
#![allow(unused)]
fn main() {
// Pseudocode for version-aware verification
fn verify_signature(doc: &Document) -> Result<()> {
    let sig = &doc.jacs_signature;

    // Find the key that was active for this version
    let public_key = resolve_key_for_version(
        &sig.agent_id,
        &sig.agent_version,
        &sig.public_key_hash,
    )?;

    // Verify with the historical key
    verify_with_key(&doc, &sig, &public_key)
}
}

Key Lookup Priority

The verification system tries multiple sources:

  1. Local cache by hash - Fastest, key already stored locally
  2. Trust store by version - Most accurate for known agents
  3. Trust store by hash - Fallback for legacy entries
  4. DNS lookup - External verification, authoritative
  5. Fail - Key not found, verification impossible

Key Rotation Process

Step-by-Step Rotation

  1. Generate new key pair with the desired algorithm
  2. Create new agent version with updated key information
  3. Sign new version with old key (transition signature)
  4. Update DNS records to include new key fingerprint
  5. Store old public key for future verifications

Transition Signature

The transition signature proves the key rotation was authorized by the holder of the old key:

JACS_KEY_ROTATION:{agent_id}:{old_key_hash}:{new_key_hash}:{timestamp}

This signed message:

  • Proves continuity of ownership
  • Provides an audit trail
  • Binds old and new keys together cryptographically

CLI Commands

# Rotate keys with default algorithm
jacs agent rotate-keys

# Rotate to post-quantum algorithm
jacs agent rotate-keys --algorithm pq2025

# List key history (active and archived keys)
jacs agent keys-list

# Repair config after a crash during rotation
jacs agent repair

# Revoke a compromised key (Coming Soon)
jacs agent keys revoke <key-hash>

Transition Signature

During key rotation, JACS produces a cryptographic transition proof that binds the old key to the new key. This proof is embedded in the agent document as jacsKeyRotationProof:

{
  "jacsKeyRotationProof": {
    "transitionMessage": "JACS_KEY_ROTATION:{agent_id}:{old_key_hash}:{new_key_hash}:{timestamp}",
    "signature": "base64-encoded-signature-with-old-key",
    "signingAlgorithm": "ring-Ed25519",
    "oldPublicKeyHash": "sha256-of-old-key",
    "newPublicKeyHash": "sha256-of-new-key",
    "timestamp": "2026-04-07T10:00:00Z"
  }
}

The transition message is signed with the old private key before it is archived. This proves:

  • The rotation was authorized by the holder of the previous key
  • The old and new keys are cryptographically linked
  • An attacker cannot forge a rotation without the old private key

Agent::verify_transition_proof() validates the detached old-key signature. Registries and directories must use Agent::verify_transition_proof_for_rotation(proof, expected_agent_id, old_key, new_key) instead. That stricter API also binds the proof to the expected stable identity and exact candidate new key, preventing proof transplantation.

Crash Recovery

JACS uses a write-ahead journal to recover from crashes during key rotation. Before rotation begins, a journal file is written to {key_directory}/.jacs_rotation_journal.json. The journal tracks the rotation stage:

  1. started - Rotation initiated
  2. keys_rotated - New keys generated, old keys archived
  3. agent_saved - New agent version saved to disk
  4. config_signed - Config re-signed with new key (journal deleted on success)

If the process crashes mid-rotation, the next agent load detects the journal and automatically repairs the config by re-signing it with the current key. No manual intervention is required for the common case.

For manual recovery: jacs agent repair

Cross-Algorithm Rotation

Rotation is a one-way migration to pq2025, not an arbitrary algorithm selector. An Ed25519 root can migrate to post-quantum signing:

# Rotate from Ed25519 to post-quantum
jacs agent rotate-keys --algorithm pq2025
#![allow(unused)]
fn main() {
// Rust API
let result = advanced::rotate(&agent, Some("pq2025"))?;
}
# Python
result = client.rotate_keys(algorithm="pq2025")
// Node.js
const result = await client.rotateKeys({ algorithm: 'pq2025' });

After cross-algorithm rotation, the config file's jacs_agent_key_algorithm field is updated atomically. Documents signed before the rotation remain verifiable using the archived old key.

Example Rotation Flow

Time T0: Agent created
  - jacsId: "abc-123"
  - jacsVersion: "v1-uuid"
  - jacsCurrentKeyHash: "hash-A"

Time T1: Agent signs document D1
  - D1.jacsSignature.agentVersion: "v1-uuid"
  - D1.jacsSignature.publicKeyHash: "hash-A"

Time T2: Key rotation
  - New keys generated with hash-B
  - jacsVersion: "v2-uuid"
  - jacsKeyHistory: [{ hash: "hash-A", status: "rotated" }]
  - jacsCurrentKeyHash: "hash-B"

Time T3: Verify D1
  - Extract agentVersion "v1-uuid" and hash "hash-A"
  - Look up key: find "hash-A" with status "rotated"
  - Verification succeeds (old key still valid for old docs)

Time T4: Agent signs document D2
  - D2.jacsSignature.agentVersion: "v2-uuid"
  - D2.jacsSignature.publicKeyHash: "hash-B"

Trust Store with Version History

The trust store maintains a history of all public keys for each trusted agent.

TrustedAgent Structure

{
  "agent_id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "Example Agent",
  "trusted_at": "2024-01-15T10:00:00Z",
  "current_key_hash": "abc123...",
  "domain": "agent.example.com",
  "key_history": [
    {
      "public_key_hash": "xyz789...",
      "public_key_pem": "-----BEGIN PUBLIC KEY-----\n...",
      "signing_algorithm": "ring-Ed25519",
      "trusted_at": "2024-01-01T00:00:00Z",
      "first_version": "11111111-1111-1111-1111-111111111111",
      "last_version": "22222222-2222-2222-2222-222222222222",
      "status": "rotated"
    },
    {
      "public_key_hash": "abc123...",
      "public_key_pem": "-----BEGIN PUBLIC KEY-----\n...",
      "signing_algorithm": "ring-Ed25519",
      "trusted_at": "2024-01-15T10:00:00Z",
      "first_version": "33333333-3333-3333-3333-333333333333",
      "last_version": null,
      "status": "active"
    }
  ]
}

Key Status Values

StatusDescription
activeCurrently in use for signing
rotatedSuperseded by newer key, still valid for old signatures
revokedCompromised, signatures should not be trusted
expiredPast validity period

Looking Up Keys

#![allow(unused)]
fn main() {
impl TrustedAgent {
    /// Get the public key that was active for a specific agent version
    fn get_key_for_version(&self, version: &str) -> Option<&KeyEntry> {
        self.key_history.iter().find(|entry| {
            match (&entry.first_version, &entry.last_version) {
                (Some(first), Some(last)) => {
                    version >= first && version <= last
                }
                (Some(first), None) => {
                    version >= first  // Current key
                }
                _ => false
            }
        })
    }

    /// Get the public key by its hash
    fn get_key_by_hash(&self, hash: &str) -> Option<&KeyEntry> {
        self.key_history.iter().find(|e| e.public_key_hash == hash)
    }
}
}

DNS Support for Key Versions

DNS records can advertise multiple key versions for an agent.

Multi-Version DNS Records

Each key version gets its own TXT record:

; Current key
_v1.agent.jacs.example.com. 3600 IN TXT "v=hai.ai; jacs_agent_id={id}; ver=current; alg=SHA-256; hash={hash1}"

; Previous key (still valid for old signatures)
_v1.agent.jacs.example.com. 3600 IN TXT "v=hai.ai; jacs_agent_id={id}; ver=rotated; valid_until=2025-01-15; hash={hash2}"

DNS Record Generation

# Generate DNS records for all active keys
jacs agent dns --all-keys

Security Considerations

Key Revocation

When a key is compromised:

  1. Mark as revoked in the agent's key history
  2. Update DNS to include revocation status
  3. Signatures fail verification when made with revoked keys
  4. Notify trusted peers if possible

Overlap Period

During rotation, both old and new keys may be valid:

  • New documents should be signed with the new key
  • Old documents remain verifiable with the old key
  • DNS may advertise both keys during transition

Secure Deletion

After rotation:

  • Old private keys should be securely deleted
  • Only public keys are retained for verification
  • Key metadata must be protected from modification

Best Practices

Rotation Schedule

  • Regular rotation: Quarterly or annually for compliance
  • Algorithm upgrade: When transitioning to stronger cryptography
  • Incident response: Immediately after suspected compromise

Pre-Rotation Checklist

  • Backup current agent state
  • Verify all systems can handle new key format
  • Plan DNS propagation time
  • Notify dependent systems of upcoming change

Post-Rotation Checklist

  • Verify new key is active
  • Confirm old documents still verify
  • Update DNS records
  • Securely delete old private key
  • Test signing with new key
  • Re-issue the compatibility key binding (jacs agent issue-compat-binding) if the agent has an ES256 compatibility key — ecosystem exports fail until the new native root signs a fresh binding

See Also

Cryptographic Algorithms

JACS supports multiple cryptographic algorithms for digital signatures, providing flexibility for different security requirements and future-proofing against quantum computing threats.

pq2025 (ML-DSA-87 / FIPS-204) is the native default. Explicit ed25519 selection creates genuine Ed25519 keys and emits the canonical ring-Ed25519 wire label; JACS never silently substitutes one supported algorithm for another. Every key rotation currently resolves to pq2025. ES256 exists in JACS only as an ecosystem compatibility key for targeted exports; it is never a native jacsSignature algorithm. See the Algorithm Selection Guide.

Supported Algorithms

AlgorithmConfig ValueTypeKey SizeSignature SizeRole
PQ2025pq2025ML-DSA-872,592 bytes4,627 bytesNative root default; post-quantum
Ed25519ring-Ed25519 (input: ed25519)Elliptic Curve32 bytes64 bytesSupported native creation, signing, and verification

Ed25519 (ring-Ed25519)

Supported for creation, signing, and verification across native bindings.

Overview

Ed25519 is an elliptic curve signature scheme using Curve25519. JACS uses the ring cryptographic library implementation.

Characteristics

  • Speed: Extremely fast signing and verification
  • Key Size: 32-byte private key, 32-byte public key
  • Signature Size: 64 bytes
  • Security Level: ~128 bits (classical)

Configuration

{
  "jacs_agent_key_algorithm": "ring-Ed25519"
}

For user-facing CLI and SDK input, prefer ed25519. Configuration and signed documents store the canonical ring-Ed25519 wire label.

Use Cases

  • Bandwidth-sensitive or high-throughput signing
  • Interoperability with existing Ed25519 identities
  • Shorter-lived signatures where post-quantum resistance is not required

Example

import jacs
import json

agent, info = jacs.SimpleAgent.ephemeral(algorithm="ed25519")
assert info["algorithm"] == "ring-Ed25519"

signed = agent.sign_message({"message": "Hello, World!"})
assert json.loads(signed["raw"])["jacsSignature"]["signingAlgorithm"] == "ring-Ed25519"

PQ2025

Overview

PQ2025 uses ML-DSA-87, the FIPS-204 post-quantum signature algorithm currently supported by JACS. It is the default native root algorithm.

Characteristics

  • Speed: Moderate (slower than Ed25519)
  • Key Size: 2,592-byte public keys
  • Signature Size: 4,627 bytes
  • Security Level: NIST Level 3 (quantum-resistant)

Configuration

{
  "jacs_agent_key_algorithm": "pq2025"
}

Use Cases

  • Long-term document security
  • Protection against future quantum attacks
  • High-security applications
  • Government/defense requirements

Considerations

  • Larger signatures and keys than Ed25519
  • Use for compliance or long-lived signatures where post-quantum readiness matters
  • May be required for future compliance

ES256 (Compatibility Key — Never Native)

ES256 (ECDSA P-256) appears in JACS only as the ecosystem compatibility key used by the targeted exporters (JWKS, DID/W3C identity, A2A agent card, AP2 mandate, Agreement-v2 Verifiable Credential). It is never a valid native jacsSignature algorithm: the signature schema permits only ring-Ed25519 and pq2025, and native verification rejects ES256.

An ES256 signature on an exported artifact proves possession of the compatibility key, nothing more — it does not by itself establish native JACS trust. The native-root-signed compatibility key binding is what ties the ES256 key to the agent; see the Security Model for the binding lifecycle. Verifying incoming AP2 mandates or third-party VCs is out of scope in P2.

Algorithm Selection Guide

Choose the native algorithm at agent creation. pq2025 is the default; ed25519 is an explicit size/performance tradeoff.

Decision Matrix

RequirementAlgorithm
New agent requiring post-quantum assurancepq2025 (default)
New agent prioritizing small keys/signaturesed25519 (stored/emitted as ring-Ed25519)
Verifying Ed25519 documentsring-Ed25519 (verification is automatic)
JOSE/W3C ecosystem interop (JWKS, AP2, VC)ES256 compatibility key — export-only, never native

By Use Case

Web APIs and MCP:

{
  "jacs_agent_key_algorithm": "pq2025"
}

Native signing defaults to pq2025; explicitly selected Ed25519 agents remain Ed25519 until rotation.

Legal/Financial Documents:

{
  "jacs_agent_key_algorithm": "pq2025"
}

Long-term validity requires quantum resistance.

Enterprise Integration:

{
  "jacs_agent_key_algorithm": "pq2025"
}

Use pq2025 where cross-organization retention or compliance expectations favor post-quantum signatures.

High-Security:

{
  "jacs_agent_key_algorithm": "pq2025"
}

Belt-and-suspenders approach for maximum protection.

Key Generation

Keys are generated automatically when creating an agent:

# Directory structure after agent creation
jacs_keys/
├── private.pem    # Algorithm-specific private key
└── public.pem     # Algorithm-specific public key

Key Formats

AlgorithmPrivate Key FormatPublic Key Format
ring-Ed25519PEM (PKCS#8)PEM (SPKI)
pq2025Raw ML-DSA-87 bytesRaw ML-DSA-87 bytes

Signature Structure

Signatures in JACS documents include algorithm metadata:

{
  "jacsSignature": {
    "agentID": "550e8400-e29b-41d4-a716-446655440000",
    "agentVersion": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "date": "2024-01-15T10:30:00Z",
    "signature": "base64-encoded-signature",
    "publicKeyHash": "sha256-of-public-key",
    "signingAlgorithm": "pq2025",
    "fields": ["jacsId", "jacsVersion", "content"]
  }
}

The signingAlgorithm field enables verifiers to use the correct verification method. The native signature schema permits only ring-Ed25519 and pq2025; a native document whose signingAlgorithm is anything else (for example ES256) fails verification.

Hashing

JACS uses SHA-256 for all hash operations:

  • Document content hashing (jacsSha256)
  • Public key fingerprints (publicKeyHash)
  • Agreement content locking (jacsAgreementHash)
{
  "jacsSha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}

Algorithm Migration

Key rotation is the designated Ed25519 → pq2025 migration path. Every rotation resolves to pq2025 — with or without an explicit algorithm argument — so an Ed25519 agent becomes PQ-rooted the first time it rotates. Requesting any other rotation target is a typed error.

  1. Rotate Keys

    jacs agent rotate-keys
    
  2. Re-issue the Compatibility Key Binding (if present)

    The ES256 compatibility key binding is signed by the native root, so rotation supersedes it. Ecosystem exports fail with a "re-issue" error until a new binding is signed by the current native root:

    jacs agent issue-compat-binding
    
  3. Maintain Backward Compatibility

    • Keep old agent versions for verifying old documents
    • Old signatures remain valid with old public keys

Performance Comparison

Approximate performance (varies by hardware):

AlgorithmSign (ops/sec)Verify (ops/sec)Key Gen (ms)
ring-Ed25519~50,000~20,000<1
pq2025~4,000~8,000~2

Security Considerations

Algorithm Agility

JACS documents include the signing algorithm, enabling:

  • Verification with correct algorithm
  • Graceful algorithm transitions
  • Multi-algorithm environments

Forward Secrecy

Signatures don't provide forward secrecy. For confidentiality:

  • Use TLS for transport
  • Consider additional encryption layers

Key Compromise

If a private key is compromised:

  1. Generate new key pair
  2. Create new agent version
  3. Revoke trust in compromised version
  4. Re-sign critical documents

See Also

Algorithm Selection Guide

Choosing the right signing algorithm affects key size, signature size, verification speed, and compliance posture. This guide helps you pick the right one.

New-Agent Selection

pq2025 (ML-DSA-87 / FIPS-204) is the secure default. Pass ed25519 explicitly when its smaller keys and signatures are required. JACS returns the canonical wire label ring-Ed25519; the requested label, generated key shape, persisted configuration, and emitted signature algorithm always agree. ring-Ed25519 remains accepted as a legacy input alias. ES256 is an ecosystem compatibility key, never a native jacsSignature algorithm.

All key rotations currently resolve to pq2025, so rotation is also the supported Ed25519-to-post-quantum migration path — see Key Rotation.

Supported Algorithms

AlgorithmConfig ValuePublic KeySignatureStatus
ML-DSA-87pq20252,592 bytes4,627 bytesDefault; post-quantum (FIPS-204)
Ed25519ring-Ed25519 (input alias: ed25519)32 bytes64 bytesSupported for creation, signing, and verification

When to Choose Post-Quantum

Choose pq2025 (ML-DSA-87, FIPS-204) when:

  • Your compliance team asks about quantum readiness
  • Government or defense contracts require FIPS-204
  • You need long-lived signatures that must remain valid for 10+ years
  • You want to avoid a future algorithm migration

JACS supports ML-DSA-87 (FIPS-204) for post-quantum digital signatures. When your compliance team asks about quantum readiness, JACS already has the answer.

The tradeoff is size: ML-DSA-87 public keys are 2,592 bytes and signatures are 4,627 bytes -- roughly 80x larger than Ed25519. For most applications this is negligible, but if you're signing millions of small messages and bandwidth matters, consider Ed25519.

Cross-Algorithm Verification

JACS verification works across supported algorithms. An agreement can contain signatures from Ed25519 and ML-DSA agents and all verify correctly. This heterogeneous verification is important for cross-organization scenarios where different parties choose different supported algorithms.

Each agent uses one algorithm (chosen at creation time), but can verify signatures from all supported algorithms.

Configuration

Set the algorithm in your jacs.config.json:

{
  "jacs_agent_key_algorithm": "pq2025"
}

Or via environment variable:

export JACS_AGENT_KEY_ALGORITHM=pq2025

User-facing values for new keys: ed25519, pq2025. The historical ring-Ed25519 input remains accepted and is the canonical signature/config label emitted for Ed25519.

In Python and Node.js, pass the algorithm to quickstart(...):

from jacs.client import JacsClient
client = JacsClient.quickstart(
    name="algo-agent",
    domain="algo.example.com",
    algorithm="pq2025",
)
import { JacsClient } from "@hai.ai/jacs";
const client = await JacsClient.quickstart({
  name: "algo-agent",
  domain: "algo.example.com",
  algorithm: "pq2025",
});

Current Limitations

  • Each agent uses one algorithm, chosen at creation time. You cannot change an agent's algorithm after creation.
  • Algorithm negotiation between agents is planned but not yet implemented.
  • Use pq2025 for ML-DSA-87 post-quantum signatures and verification hints.

Storage Backends

JACS has two storage layers today:

  • Low-level file/object storage via MultiStorage
  • Signed document CRUD/search via DocumentService

Those are related, but they are not identical. The most important rule is the signed-document contract:

  • Every DocumentService read verifies the stored JACS document before returning it.
  • Every create() and update() verifies the signed document before persisting it.
  • If an update payload changes an already-signed JACS document without re-signing it, the write fails.
  • Visibility changes create a new signed version instead of mutating metadata in place.

Built-in Core Backends

BackendConfig ValueCore SurfaceNotes
FilesystemfsMultiStorage + DocumentServiceDefault. Signed JSON files on disk.
Local indexed SQLiterusqliteDocumentService + SearchProviderStores signed documents in a local SQLite DB with FTS search.
AWS object storageawsMultiStorageObject-store backend.
MemorymemoryMultiStorageNon-persistent, useful for tests and temporary flows.
Browser local storagelocalMultiStorageWASM-only.

For local indexed document search in JACS core, use rusqlite.

Filesystem (fs)

Filesystem is the default signed-document backend.

{
  "jacs_default_storage": "fs",
  "jacs_data_directory": "./jacs_data",
  "jacs_key_directory": "./jacs_keys"
}

Typical layout:

jacs_data/
├── agent/
│   └── {agent-id}:{agent-version}.json
└── documents/
    ├── {document-id}:{version}.json
    └── archive/

Use filesystem when you want the simplest possible deployment, inspectable files, and no local database dependency.

Local Indexed SQLite (rusqlite)

rusqlite is the built-in indexed document backend used by the upgraded bindings and MCP search path.

{
  "jacs_default_storage": "rusqlite",
  "jacs_data_directory": "./jacs_data",
  "jacs_key_directory": "./jacs_keys"
}

With this setting:

  • Signed documents are stored in ./jacs_data/jacs_documents.sqlite3
  • Full-text search comes from SQLite FTS
  • DocumentService reads and writes enforce verification
  • Updating visibility creates a new signed successor version

Use rusqlite when you want local full-text search, filtered document queries, and a single-machine deployment.

AWS (aws)

AWS support is an object-store backend for lower-level storage operations.

{
  "jacs_default_storage": "aws"
}

Required environment variables:

export JACS_ENABLE_AWS_BUCKET_NAME="my-jacs-bucket"
export AWS_ACCESS_KEY_ID="..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_REGION="us-west-2"

Use aws when you need remote object storage. If you also need a richer signed-document query surface, use one of the database-focused crates below.

Memory (memory)

Memory storage is non-persistent:

{
  "jacs_default_storage": "memory"
}

Use it for tests, temporary operations, and ephemeral agent flows.

Extracted Backend Crates

Several richer database backends now live outside the JACS core crate:

  • jacs-postgresql
  • jacs-duckdb
  • jacs-redb
  • jacs-surrealdb

These crates implement the same storage/search traits in their own packages. They are not built-in jacs_default_storage values for the core crate.

Choosing a Backend

ScenarioRecommendation
Default local usagefs
Local search + filteringrusqlite
Ephemeral testsmemory
Remote object storageaws
Postgres / vector / multi-model needsUse an extracted backend crate

Migration Notes

Switching backends does not migrate data automatically.

When you change jacs_default_storage:

  1. Export the signed documents you need to keep.
  2. Update the config value.
  3. Create/import the new backend’s data store.
  4. Re-run verification on imported documents as part of migration validation.

Custom Schemas

JACS allows you to define custom document schemas that extend the base header schema, enabling type-safe, validated documents for your specific use cases.

Overview

Custom schemas:

  • Inherit all JACS header fields (jacsId, jacsVersion, jacsSignature, etc.)
  • Add domain-specific fields with validation
  • Enable IDE autocompletion and type checking
  • Ensure document consistency across your application

Creating a Custom Schema

Basic Structure

Custom schemas extend the JACS header using allOf:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://example.com/schemas/invoice.schema.json",
  "title": "Invoice",
  "description": "Invoice document with JACS signing",
  "allOf": [
    { "$ref": "https://hai.ai/schemas/header/v1/header.schema.json" },
    {
      "type": "object",
      "properties": {
        "invoiceNumber": {
          "type": "string",
          "description": "Unique invoice identifier"
        },
        "amount": {
          "type": "number",
          "minimum": 0
        },
        "currency": {
          "type": "string",
          "enum": ["USD", "EUR", "GBP"]
        }
      },
      "required": ["invoiceNumber", "amount"]
    }
  ]
}

Step-by-Step Guide

  1. Create the schema file
// schemas/order.schema.json
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://mycompany.com/schemas/order.schema.json",
  "title": "Order",
  "description": "E-commerce order document",
  "allOf": [
    { "$ref": "https://hai.ai/schemas/header/v1/header.schema.json" },
    {
      "type": "object",
      "properties": {
        "orderId": {
          "type": "string",
          "pattern": "^ORD-[0-9]{6}$"
        },
        "customer": {
          "type": "object",
          "properties": {
            "name": { "type": "string" },
            "email": { "type": "string", "format": "email" }
          },
          "required": ["name", "email"]
        },
        "items": {
          "type": "array",
          "minItems": 1,
          "items": {
            "type": "object",
            "properties": {
              "sku": { "type": "string" },
              "quantity": { "type": "integer", "minimum": 1 },
              "price": { "type": "number", "minimum": 0 }
            },
            "required": ["sku", "quantity", "price"]
          }
        },
        "total": {
          "type": "number",
          "minimum": 0
        },
        "status": {
          "type": "string",
          "enum": ["pending", "processing", "shipped", "delivered", "cancelled"]
        }
      },
      "required": ["orderId", "customer", "items", "total", "status"]
    }
  ]
}
  1. Use the schema when creating documents
import jacs
import json

agent = jacs.JacsAgent()
agent.load('./jacs.config.json')

order = agent.create_document(
    json.dumps({
        'orderId': 'ORD-123456',
        'customer': {
            'name': 'Jane Smith',
            'email': 'jane@example.com'
        },
        'items': [
            {'sku': 'WIDGET-001', 'quantity': 2, 'price': 29.99}
        ],
        'total': 59.98,
        'status': 'pending'
    }),
    custom_schema='./schemas/order.schema.json'
)
import { JacsAgent } from '@hai.ai/jacs';

const agent = new JacsAgent();
agent.load('./jacs.config.json');

const order = agent.createDocument(
  JSON.stringify({
    orderId: 'ORD-123456',
    customer: {
      name: 'Jane Smith',
      email: 'jane@example.com'
    },
    items: [
      { sku: 'WIDGET-001', quantity: 2, price: 29.99 }
    ],
    total: 59.98,
    status: 'pending'
  }),
  './schemas/order.schema.json'
);

Schema Best Practices

Use Meaningful IDs

{
  "$id": "https://mycompany.com/schemas/v1/order.schema.json"
}

Include version in the path for schema evolution.

Document Everything

{
  "properties": {
    "status": {
      "type": "string",
      "description": "Current order status in the fulfillment workflow",
      "enum": ["pending", "processing", "shipped", "delivered", "cancelled"]
    }
  }
}

Use Appropriate Validation

{
  "properties": {
    "email": {
      "type": "string",
      "format": "email"
    },
    "phone": {
      "type": "string",
      "pattern": "^\\+?[1-9]\\d{1,14}$"
    },
    "quantity": {
      "type": "integer",
      "minimum": 1,
      "maximum": 1000
    }
  }
}
{
  "properties": {
    "shipping": {
      "type": "object",
      "properties": {
        "address": { "type": "string" },
        "city": { "type": "string" },
        "country": { "type": "string" },
        "postalCode": { "type": "string" }
      }
    },
    "billing": {
      "type": "object",
      "properties": {
        "address": { "type": "string" },
        "city": { "type": "string" },
        "country": { "type": "string" },
        "postalCode": { "type": "string" }
      }
    }
  }
}

Advanced Schema Features

Conditional Validation

Different requirements based on field values:

{
  "allOf": [
    { "$ref": "https://hai.ai/schemas/header/v1/header.schema.json" },
    {
      "type": "object",
      "properties": {
        "paymentMethod": {
          "type": "string",
          "enum": ["credit_card", "bank_transfer", "crypto"]
        }
      }
    }
  ],
  "if": {
    "properties": {
      "paymentMethod": { "const": "credit_card" }
    }
  },
  "then": {
    "properties": {
      "cardLastFour": {
        "type": "string",
        "pattern": "^[0-9]{4}$"
      }
    },
    "required": ["cardLastFour"]
  }
}

Reusable Definitions

{
  "$defs": {
    "address": {
      "type": "object",
      "properties": {
        "street": { "type": "string" },
        "city": { "type": "string" },
        "country": { "type": "string" },
        "postalCode": { "type": "string" }
      },
      "required": ["street", "city", "country"]
    }
  },
  "properties": {
    "shippingAddress": { "$ref": "#/$defs/address" },
    "billingAddress": { "$ref": "#/$defs/address" }
  }
}

Array Constraints

{
  "properties": {
    "tags": {
      "type": "array",
      "items": { "type": "string" },
      "minItems": 1,
      "maxItems": 10,
      "uniqueItems": true
    }
  }
}

Pattern Properties

For dynamic field names:

{
  "properties": {
    "metadata": {
      "type": "object",
      "patternProperties": {
        "^x-": { "type": "string" }
      },
      "additionalProperties": false
    }
  }
}

Schema Inheritance

Extending Custom Schemas

Create schema hierarchies:

// schemas/base-transaction.schema.json
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://example.com/schemas/base-transaction.schema.json",
  "allOf": [
    { "$ref": "https://hai.ai/schemas/header/v1/header.schema.json" },
    {
      "type": "object",
      "properties": {
        "transactionId": { "type": "string" },
        "timestamp": { "type": "string", "format": "date-time" },
        "amount": { "type": "number" }
      },
      "required": ["transactionId", "timestamp", "amount"]
    }
  ]
}
// schemas/payment.schema.json
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://example.com/schemas/payment.schema.json",
  "allOf": [
    { "$ref": "https://example.com/schemas/base-transaction.schema.json" },
    {
      "type": "object",
      "properties": {
        "paymentMethod": { "type": "string" },
        "processorId": { "type": "string" }
      },
      "required": ["paymentMethod"]
    }
  ]
}

Validation

Python Validation

import jacs
import json

agent = jacs.JacsAgent()
agent.load('./jacs.config.json')

try:
    # This will fail validation - missing required field
    doc = agent.create_document(
        json.dumps({
            'orderId': 'ORD-123456'
            # Missing: customer, items, total, status
        }),
        custom_schema='./schemas/order.schema.json'
    )
except Exception as e:
    print(f"Validation failed: {e}")

Node.js Validation

import { JacsAgent } from '@hai.ai/jacs';

const agent = new JacsAgent();
agent.load('./jacs.config.json');

try {
  // This will fail - invalid enum value
  const doc = agent.createDocument(
    JSON.stringify({
      orderId: 'ORD-123456',
      customer: { name: 'Jane', email: 'jane@example.com' },
      items: [{ sku: 'A', quantity: 1, price: 10 }],
      total: 10,
      status: 'invalid_status'  // Not in enum
    }),
    './schemas/order.schema.json'
  );
} catch (error) {
  console.error('Validation failed:', error.message);
}

Example Schemas

Medical Record

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://healthcare.example.com/schemas/medical-record.schema.json",
  "title": "Medical Record",
  "allOf": [
    { "$ref": "https://hai.ai/schemas/header/v1/header.schema.json" },
    {
      "type": "object",
      "properties": {
        "patientId": { "type": "string" },
        "recordType": {
          "type": "string",
          "enum": ["visit", "lab_result", "prescription", "diagnosis"]
        },
        "provider": {
          "type": "object",
          "properties": {
            "name": { "type": "string" },
            "npi": { "type": "string", "pattern": "^[0-9]{10}$" }
          }
        },
        "date": { "type": "string", "format": "date" },
        "notes": { "type": "string" },
        "confidential": { "type": "boolean", "default": true }
      },
      "required": ["patientId", "recordType", "provider", "date"]
    }
  ]
}
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://legal.example.com/schemas/contract.schema.json",
  "title": "Legal Contract",
  "allOf": [
    { "$ref": "https://hai.ai/schemas/header/v1/header.schema.json" },
    {
      "type": "object",
      "properties": {
        "contractNumber": { "type": "string" },
        "parties": {
          "type": "array",
          "minItems": 2,
          "items": {
            "type": "object",
            "properties": {
              "name": { "type": "string" },
              "role": { "type": "string" },
              "agentId": { "type": "string", "format": "uuid" }
            }
          }
        },
        "effectiveDate": { "type": "string", "format": "date" },
        "expirationDate": { "type": "string", "format": "date" },
        "terms": { "type": "string" },
        "jurisdiction": { "type": "string" }
      },
      "required": ["contractNumber", "parties", "effectiveDate", "terms"]
    }
  ]
}

IoT Sensor Reading

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://iot.example.com/schemas/sensor-reading.schema.json",
  "title": "Sensor Reading",
  "allOf": [
    { "$ref": "https://hai.ai/schemas/header/v1/header.schema.json" },
    {
      "type": "object",
      "properties": {
        "deviceId": { "type": "string" },
        "sensorType": {
          "type": "string",
          "enum": ["temperature", "humidity", "pressure", "motion"]
        },
        "value": { "type": "number" },
        "unit": { "type": "string" },
        "timestamp": { "type": "string", "format": "date-time" },
        "location": {
          "type": "object",
          "properties": {
            "latitude": { "type": "number" },
            "longitude": { "type": "number" }
          }
        }
      },
      "required": ["deviceId", "sensorType", "value", "timestamp"]
    }
  ]
}

Schema Versioning

Version in Path

{
  "$id": "https://example.com/schemas/v1/order.schema.json"
}

Version Field

{
  "properties": {
    "schemaVersion": {
      "type": "string",
      "const": "1.0.0"
    }
  }
}

Migration Strategy

  1. Create new schema version
  2. Update application to support both versions
  3. Migrate existing documents
  4. Deprecate old version

See Also

Trust Store Operations

The JACS trust store is a local directory of agent public keys and metadata that your agent has explicitly chosen to trust. It enables offline signature verification without a central authority -- once you trust an agent, you can verify its signatures without network access.

How it works

When you add an agent to your trust store, JACS:

  1. Parses the agent's JSON document
  2. Extracts the public key and verifies the agent's self-signature
  3. Saves the agent document, public key, and metadata to ~/.jacs/trust_store/

After that, any document signed by that agent can be verified locally using the cached public key.

API

All bindings expose five trust store functions:

FunctionDescription
trust_agent(agent_json)Add an agent to the trust store (verifies self-signature first)
list_trusted_agents()List all trusted agent IDs
is_trusted(agent_id)Check if an agent is in the trust store
get_trusted_agent(agent_id)Retrieve the full agent JSON
untrust_agent(agent_id)Remove an agent from the trust store

Python example

import jacs

# Receive an agent document from a partner organization
remote_agent_json = receive_from_partner()

# Add to trust store (self-signature is verified automatically)
agent_id = jacs.trust_agent(remote_agent_json)
print(f"Now trusting: {agent_id}")

# Later, check trust before processing a signed document
if jacs.is_trusted(sender_id):
    # Verify their signature using the cached public key
    result = jacs.verify(signed_document)

# List all trusted agents
for aid in jacs.list_trusted_agents():
    print(aid)

# Remove trust
jacs.untrust_agent(agent_id)

Node.js example

import { trustAgent, isTrusted, listTrustedAgents, untrustAgent } from '@hai.ai/jacs';

// Add a partner's agent to the trust store
const agentId = trustAgent(remoteAgentJson);

// Check trust
if (isTrusted(senderId)) {
  const result = verify(signedDocument);
}

// List and remove
const trusted = listTrustedAgents();
untrustAgent(agentId);

Cross-organization scenario

A realistic deployment involves two organizations that need to verify each other's agent signatures:

  1. Org B creates an agent and publishes its public key via DNS TXT records or a HAI key distribution endpoint
  2. Org A fetches Org B's agent document (via fetch_remote_key or direct exchange)
  3. Org A calls trust_agent() with Org B's agent JSON -- JACS verifies the self-signature and caches the public key
  4. From this point on, Org A can verify any document signed by Org B's agent offline, using only the local trust store

This is the same model as SSH known_hosts or PGP key signing: trust is established once through a verified channel, then used repeatedly without network round-trips.

Security notes

  • trust_agent() cryptographically verifies the agent's self-signature before adding it to the store. A tampered agent document will be rejected.
  • Agent IDs are validated against path traversal attacks before any filesystem operations.
  • The trust store directory (~/.jacs/trust_store/) should be protected with appropriate file permissions.
  • Revoking trust with untrust_agent() removes both the agent document and cached key material.

Infrastructure vs Tools: JACS as Middleware

Most signing libraries work as tools: the developer calls sign() and verify() manually at each point where integrity matters. JACS can work that way too, but its real value appears when it operates as infrastructure -- signing happens automatically as a side effect of normal framework usage.

The difference

ApproachDeveloper effortCoverage
ToolCall sign()/verify() at every boundaryOnly where you remember to add it
InfrastructureAdd 1-3 lines of setupEvery request/response automatically

Transport-level: MCP proxies

JACS MCP transport proxies sit between client and server. Every JSON-RPC message is signed on the way out and verified on the way in. The MCP tools themselves never call a signing function -- it happens at the transport layer.

Client --> [JACS Proxy: sign] --> Server
Client <-- [JACS Proxy: verify] <-- Server

No application code changes. The proxy handles it.

Framework-level: Express / FastAPI middleware

A single middleware line signs every HTTP response automatically:

# FastAPI -- one line of setup
app.add_middleware(JacsMiddleware, client=jacs_client)
# Every response now carries a JACS signature header
// Express -- one line of setup
app.use(jacsMiddleware({ client }));
// Every response now carries a JACS signature header

The route handlers are unchanged. Signing is invisible to the developer writing business logic.

Protocol-level: A2A agent cards

When JACS publishes an A2A agent card, the card includes the agent's public key and supported algorithms. Any other A2A-compatible agent can verify signatures without prior arrangement -- the trust bootstrapping is built into the protocol.

Why this matters

Manual signing has the same problem as manual memory management: developers forget, and the places they forget are the places attackers target. Infrastructure-level signing eliminates that gap.

  • MCP transport: every tool call and result is signed, not just the ones you thought to protect
  • HTTP middleware: every API response is signed, including error responses and health checks
  • A2A integration: every agent interaction is verifiable, including discovery

The developer adds setup code once. After that, signing happens everywhere automatically -- including in code paths the developer never explicitly considered.

When to use each approach

Use JACS as a tool when you need fine-grained control: signing specific documents, creating agreements between named parties, or building custom verification workflows.

Use JACS as infrastructure when you want blanket coverage: every message signed, every response verifiable, every agent interaction auditable. This is the recommended default for production deployments.

Both approaches use the same keys, the same signatures, and the same verification. The difference is who calls sign() -- you, or the framework.

DNS Trust Anchoring

JACS uses DNS TXT records to anchor agent identity to domain names, providing a decentralized trust layer that does not require a central certificate authority. This page explains the trust model, configuration levels, and known limitations.

How It Works

When an agent has jacsAgentDomain set, JACS publishes a TXT record at _v1.agent.jacs.<domain> containing a fingerprint of the agent's public key. During verification, JACS resolves this record and compares the fingerprint against the agent's actual key material.

The TXT record format:

v=hai.ai; id=<agent-uuid>; alg=sha256; enc=base64; fp=<digest>

If the digest matches the local public key hash, the agent's identity is confirmed through DNS.

Four Configuration Levels

dns_validatedns_requireddns_strictCLI FlagBehavior
falsefalsefalse--ignore-dnsNo DNS checks at all. Verification relies only on embedded fingerprints.
truefalsefalse--no-dnsAttempt DNS lookup; fall back to embedded fingerprint on failure.
truetruefalse--require-dnsDNS TXT record must exist and match. No fallback to embedded fingerprint.
truetruetrue--require-strict-dnsDNS TXT record must exist, match, and be DNSSEC-authenticated.

Default behavior: When no flags are set, dns_validate and dns_required are derived from whether jacsAgentDomain is present in the agent document. If a domain is set, validation and requirement default to true. dns_strict always defaults to false.

Verified claims override: Agents with jacsVerificationClaim set to a verified level automatically use validate=true, strict=true, required=true regardless of flags.

Security Model Assumptions

  1. Domain ownership implies identity: The entity controlling DNS for a domain is authorized to speak for agents on that domain.
  2. TXT records are tamper-evident with DNSSEC: When --require-strict-dns is used, the full DNSSEC chain of trust (root -> TLD -> domain -> record) provides cryptographic integrity.
  3. Embedded fingerprints are a weaker fallback: Without DNS, JACS falls back to the signature fingerprint (jacsSignature.publicKeyHash) in the signed artifact/agent material. This proves key consistency but not domain ownership.

Known Attack Vectors

AttackRisk LevelMitigated By
DNS cache poisoningMediumDNSSEC (--require-strict-dns), short TTLs
TXT record manipulation (compromised DNS credentials)HighDNSSEC, monitoring, key rotation
DNS spoofing (man-in-the-middle)MediumDNSSEC validation, DNS-over-HTTPS resolvers
Stale records after key rotationLowTTL management, re-publishing records before rotation
Downgrade to embedded-onlyMediumUse --require-dns to prevent fallback

What JACS Provides

  • Fingerprint binding: The TXT record ties a specific public key to a domain, preventing key substitution.
  • Multiple verification levels: From no-DNS (local development) to strict DNSSEC (production cross-org).
  • Fallback logic: When DNS is unavailable and not required, verification degrades gracefully to embedded fingerprint comparison.
  • Error specificity: Distinct error messages for "record missing," "fingerprint mismatch," "DNSSEC failed," and "agent ID mismatch."

What JACS Does Not Yet Provide

  • Active DNSSEC chain validation: JACS relies on the system resolver (or DoH) for DNSSEC; it does not perform independent DNSKEY/DS chain validation.
  • Certificate Transparency-style monitoring: No log of historical TXT record changes. Domain owners must monitor independently.
  • Automatic key-to-DNS synchronization: Publishing and updating TXT records is a manual step (or CI/CD-driven).

Recommendations

EnvironmentMinimum SettingReason
Local development--ignore-dns or --no-dnsNo real domain needed
Internal org--no-dnsDNS available but not critical
Cross-org production--require-dnsPrevents impersonation across trust boundaries
High-security / regulated--require-strict-dnsFull DNSSEC chain required

For production cross-organization deployments, use --require-dns at minimum. Enable DNSSEC on your domain and use --require-strict-dns when the infrastructure supports it.

See Also

Failure Modes

This page documents the error messages you will see when multi-agent agreements fail, plus the P2 compatibility and export failure table for operators. Each agreement scenario is validated by the chaos agreement tests in the JACS test suite.

Partial Signing (Agent Crash)

What happened: An agreement was created for N agents but one or more agents never signed -- they crashed, timed out, or disconnected before calling sign_agreement.

Error message:

not all agents have signed: ["<unsigned-agent-id>"] { ... agreement object ... }

What to do: Identify the unsigned agent from the error, re-establish contact, and have them call sign_agreement on the document. The partially-signed document is still valid and can accept additional signatures -- signing is additive.

Quorum Not Met

What happened: An agreement with an explicit quorum (M-of-N via AgreementOptions) received fewer than M signatures.

Error message:

Quorum not met: need 2 signatures, have 1 (unsigned: ["<agent-id>"])

What to do: Either collect more signatures to meet the quorum threshold, or create a new agreement with a lower quorum if appropriate. The unsigned agent IDs in the error tell you exactly who still needs to sign.

Tampered Signature

What happened: A signature byte was modified after an agent signed the agreement. The cryptographic verification layer detects that the signature does not match the signed content.

Error message:

The exact message comes from the crypto verification layer and varies by algorithm, but it will always fail on the signature check rather than reporting missing signatures. You will not see "not all agents have signed" for this case -- the error is a cryptographic verification failure.

What to do: This indicates data corruption in transit or deliberate tampering. Discard the document and request a fresh copy from the signing agent. Do not attempt to re-sign a document with a corrupted signature.

Tampered Document Body

What happened: The document content was modified after signatures were applied. JACS stores an integrity hash of the agreement-relevant fields at signing time, and any body modification causes a mismatch.

Error message:

Agreement verification failed: agreement hashes do not match

What to do: The document body no longer matches what the agents originally signed. Discard the modified document and go back to the last known-good version. If the modification was intentional (e.g., an amendment), create a new agreement on the updated document and collect fresh signatures from all parties.

In-Memory Consistency After Signing

What happened: sign_agreement succeeded but save() was never called -- for example, a storage backend failure or process interruption before persistence.

Error message: None. This is not an error. After sign_agreement returns successfully, the signed document is immediately retrievable and verifiable from in-memory storage.

What to do: Retry the save() call to persist to disk. The in-memory state is consistent: you can retrieve the document with get_document, verify it with check_agreement, serialize it, and transfer it to other agents for additional signatures -- all without saving first.

P2 Compatibility and Export Failures

Beyond agreements, the P2 trust model (native signing with a post-quantum default, the ES256 compatibility key, and targeted ecosystem exports) emits structured log events with matching Prometheus counters. This is the sysadmin fix-table: what fired, which metric to alert on, which part of the model is involved, and the usual fix.

Note on the Metric column: the counters exist only in builds with the otlp-metrics feature enabled — it is not in the default feature set of the jacs crate or of cargo install jacs-cli, so in a default build the counters compile to no-ops. The log events are always emitted once a tracing subscriber is installed (see Observability).

Log eventMetricFailure classLikely fix
compatibility_key_missing (WARN)jacs_compatibility_export_error_total{format,reason="missing_key"}compatibility bindingAn export was attempted (requested_export in the event; format on the metric) but the agent has no ES256 compatibility key — run jacs agent add-compat-key.
compatibility_key_unreadable (WARN)— (log event only, no counter)compatibility bindingA gate-and-enrich export (did, w3c-agent-identity) found the ES256 key state corrupt or inconsistent — jacs.keyring.json unparseable, or the keyring and the key files disagree (reason in the event). The export still succeeds in its native-only shape (a corrupt keyring must not take down DID serving), so this WARN is the only signal — inspect jacs.keyring.json and the jacs.ecosystem.* key files in the key directory. Distinct from compatibility_key_missing, which is the quiet never-migrated state.
compatibility_binding_verify_failed (WARN)jacs_compatibility_binding_verify_failed_total{reason}compatibility bindingThe native-root-signed binding no longer verifies (jacs_id + compat kid in the event) — re-issue after key rotation (jacs agent issue-compat-binding) and check expiresAt. Fixed reason values: schema_invalid (also covers a jacsSha256 content-hash mismatch — the document lies about itself), signature_invalid, rotated_root, root_kid_mismatch, compat_kid_mismatch, jwk_mismatch, identity_mismatch, version_mismatch (identity discovery safely reissues an otherwise-authentic same-root stale-version binding), superseded (an older validly-signed binding was restored over a re-issue; latest issuedAt wins), and expired (also an unparseable expiresAt, which fails closed).
content_export_scope_denied (WARN)jacs_content_export_scope_denied_total{format}ecosystem exportThe binding (binding_hash in the event) lacks the requested scope (ap2-mandate, agreement-vc are never auto-issued) — re-issue the binding with that scope; the current native root signs the grant.
compatibility_binding_created (INFO)— (log event only, no counter)compatibility bindingSuccess, not a failure — the current native root (re-)issued the compatibility key binding (jacs_id, compat kid, binding_hash, and granted scopes in the event). Expect one per jacs agent issue-compat-binding or first identity-export auto-issue; its absence after a key rotation means the required re-issue has not happened yet.
ecosystem_export_generated (INFO)jacs_compatibility_export_total{format}ecosystem exportSuccess, not a failure — six formats, exactly the six binding scopes: jwks, did, a2a-agent-card, w3c-agent-identity, ap2-mandate, agreement-vc.

Metric label values are fixed, low-cardinality strings (reason, format) — never raw error text.

Two boundary notes for dashboard authors:

  • The did and w3c-agent-identity exports are gate-and-enrich views: without a valid binding granting the scope they still succeed in their pre-P2 native-only shape and emit no event or counter. Only the authorized (compat-enriched) exports appear under jacs_compatibility_export_total. One exception to the silence: when the compat key state exists but is unreadable (corrupt keyring, missing key file), the native-only fallback emits the compatibility_key_unreadable WARN above.
  • Exporting the compatibility key binding itself (jacs agent export-compat-binding) is the trust artifact behind the six scoped exports, not one of them — it logs a plain INFO line and never appears under the format label.

See Also

Testing

This chapter covers testing strategies for applications that use JACS, including unit testing, integration testing, and mocking approaches.

Testing Fundamentals

Test Agent Setup

Create dedicated test configurations to isolate tests from production:

// jacs.test.config.json
{
  "$schema": "https://hai.ai/schemas/jacs.config.schema.json",
  "jacs_data_directory": "./test_data",
  "jacs_key_directory": "./test_keys",
  "jacs_agent_private_key_filename": "test_private.pem",
  "jacs_agent_public_key_filename": "test_public.pem",
  "jacs_agent_key_algorithm": "ring-Ed25519",
  "jacs_default_storage": "fs"
}

Test Fixtures

Set up test fixtures before running tests:

Python (pytest):

import pytest
import jacs
import tempfile
import shutil

@pytest.fixture
def test_agent():
    """Create a test agent with temporary directories."""
    temp_dir = tempfile.mkdtemp()
    data_dir = f"{temp_dir}/data"
    key_dir = f"{temp_dir}/keys"

    # Initialize directories
    import os
    os.makedirs(data_dir)
    os.makedirs(key_dir)

    # Create test config
    config = {
        "jacs_data_directory": data_dir,
        "jacs_key_directory": key_dir,
        "jacs_agent_key_algorithm": "ring-Ed25519",
        "jacs_default_storage": "fs"
    }

    config_path = f"{temp_dir}/jacs.config.json"
    with open(config_path, 'w') as f:
        import json
        json.dump(config, f)

    agent = jacs.JacsAgent()
    agent.load(config_path)

    yield agent

    # Cleanup
    shutil.rmtree(temp_dir)

def test_create_document(test_agent):
    """Test document creation."""
    import json
    doc = test_agent.create_document(json.dumps({
        'title': 'Test Document'
    }))

    assert doc is not None
    parsed = json.loads(doc)
    assert 'jacsId' in parsed
    assert 'jacsSignature' in parsed

Node.js (Jest):

import { JacsAgent } from '@hai.ai/jacs';
import fs from 'fs';
import path from 'path';
import os from 'os';

describe('JACS Document Tests', () => {
  let agent;
  let tempDir;

  beforeAll(() => {
    // Create temp directory
    tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jacs-test-'));
    const dataDir = path.join(tempDir, 'data');
    const keyDir = path.join(tempDir, 'keys');

    fs.mkdirSync(dataDir);
    fs.mkdirSync(keyDir);

    // Create test config
    const config = {
      jacs_data_directory: dataDir,
      jacs_key_directory: keyDir,
      jacs_agent_key_algorithm: 'ring-Ed25519',
      jacs_default_storage: 'fs'
    };

    const configPath = path.join(tempDir, 'jacs.config.json');
    fs.writeFileSync(configPath, JSON.stringify(config));

    agent = new JacsAgent();
    agent.load(configPath);
  });

  afterAll(() => {
    // Cleanup
    fs.rmSync(tempDir, { recursive: true });
  });

  test('creates a signed document', () => {
    const doc = agent.createDocument(JSON.stringify({
      title: 'Test Document'
    }));

    const parsed = JSON.parse(doc);
    expect(parsed.jacsId).toBeDefined();
    expect(parsed.jacsSignature).toBeDefined();
  });
});

Unit Testing

Testing Document Operations

import pytest
import jacs
import json

def test_document_verification(test_agent):
    """Test that created documents verify correctly."""
    doc = test_agent.create_document(json.dumps({
        'content': 'Test content'
    }))

    is_valid = test_agent.verify_document(doc)
    assert is_valid is True

def test_document_tampering_detected(test_agent):
    """Test that tampered documents fail verification."""
    doc = test_agent.create_document(json.dumps({
        'content': 'Original content'
    }))

    # Tamper with the document
    parsed = json.loads(doc)
    parsed['content'] = 'Tampered content'
    tampered = json.dumps(parsed)

    is_valid = test_agent.verify_document(tampered)
    assert is_valid is False

def test_signature_verification(test_agent):
    """Test signature verification."""
    doc = test_agent.create_document(json.dumps({
        'data': 'test'
    }))

    is_valid = test_agent.verify_signature(doc)
    assert is_valid is True

Testing Agreements

def test_agreement_creation(test_agent):
    """Test creating a document with agreement."""
    doc = test_agent.create_document(json.dumps({
        'contract': 'Service Agreement'
    }))

    # Add agreement
    doc_with_agreement = test_agent.create_agreement(
        doc,
        ['agent-1-id', 'agent-2-id'],
        question='Do you agree to these terms?',
        context='Test agreement'
    )

    parsed = json.loads(doc_with_agreement)
    assert 'jacsAgreement' in parsed
    assert len(parsed['jacsAgreement']['agentIDs']) == 2

def test_agreement_signing(test_agent):
    """Test signing an agreement."""
    doc = test_agent.create_document(json.dumps({
        'contract': 'Test'
    }))

    agent_json = test_agent.load('./jacs.test.config.json')
    agent_data = json.loads(agent_json)
    agent_id = agent_data['jacsId']

    doc_with_agreement = test_agent.create_agreement(
        doc,
        [agent_id],
        question='Agree?'
    )

    signed = test_agent.sign_agreement(doc_with_agreement)

    status_json = test_agent.check_agreement(signed)
    status = json.loads(status_json)

    assert status['complete'] is True

Agreement Completion Semantics (Strict)

check_agreement() is intentionally strict: it should fail until all required signers have signed. Add explicit tests for each state:

  1. Unsigned agreement: must fail.
  2. Partially signed agreement: must still fail.
  3. Fully signed agreement: must pass with complete == true.

This prevents accidental "partial approval" workflows in production.

Two-Agent Agreement Harness (Separate Agents)

For realistic interoperability tests, use two separate agents with:

  • Separate key directories (agent1/keys, agent2/keys)
  • Shared data directory (shared-data) so both public keys are locally resolvable
  • Relative paths from a temporary test working directory

Python (pytest):

def test_two_party_agreement_requires_both_signatures(tmp_path):
    import os
    import pytest
    import jacs.simple as simple

    (tmp_path / "shared-data").mkdir()
    (tmp_path / "agent1").mkdir()
    (tmp_path / "agent2").mkdir()

    original_cwd = os.getcwd()
    try:
        os.chdir(tmp_path)
        a1 = simple.create(
            name="agent-1",
            password="TestP@ss123!#",
            algorithm="ring-Ed25519",
            data_directory="shared-data",
            key_directory="agent1/keys",
            config_path="agent1/jacs.config.json",
        )
        a2 = simple.create(
            name="agent-2",
            password="TestP@ss123!#",
            algorithm="ring-Ed25519",
            data_directory="shared-data",
            key_directory="agent2/keys",
            config_path="agent2/jacs.config.json",
        )

        simple.load("agent1/jacs.config.json")
        agreement = simple.create_agreement(
            {"proposal": "two-party-approval"},
            [a1.agent_id, a2.agent_id],
            question="Approve?",
        )

        with pytest.raises(Exception):
            simple.check_agreement(agreement)
        signed_by_a1 = simple.sign_agreement(agreement)
        with pytest.raises(Exception):
            simple.check_agreement(signed_by_a1)

        simple.load("agent2/jacs.config.json")
        signed_by_both = simple.sign_agreement(signed_by_a1)
        status = simple.check_agreement(signed_by_both)
        assert status.complete is True
    finally:
        os.chdir(original_cwd)

Node.js (Mocha/Jest style):

const root = fs.mkdtempSync(path.join(os.tmpdir(), 'jacs-two-agent-'));
const originalCwd = process.cwd();
try {
  process.chdir(root);
  fs.mkdirSync('agent1', { recursive: true });
  fs.mkdirSync('agent2', { recursive: true });

  simpleA.create({
    name: 'agent-a',
    password: 'TestP@ss123!#',
    algorithm: 'ring-Ed25519',
    dataDirectory: 'shared-data',
    keyDirectory: 'agent1/keys',
    configPath: 'agent1/jacs.config.json'
  });
  simpleB.create({
    name: 'agent-b',
    password: 'TestP@ss123!#',
    algorithm: 'ring-Ed25519',
    dataDirectory: 'shared-data',
    keyDirectory: 'agent2/keys',
    configPath: 'agent2/jacs.config.json'
  });

  simpleA.load('agent1/jacs.config.json');
  simpleB.load('agent2/jacs.config.json');
  const infoA = simpleA.getAgentInfo();
  const infoB = simpleB.getAgentInfo();

  const agreement = simpleA.createAgreement(
    { proposal: 'two-party-approval' },
    [infoA.agentId, infoB.agentId]
  );

  expect(() => simpleA.checkAgreement(agreement)).to.throw();
  const signedByA = simpleA.signAgreement(agreement);
  expect(() => simpleA.checkAgreement(signedByA)).to.throw();

  const signedByBoth = simpleB.signAgreement(signedByA);
  const status = simpleB.checkAgreement(signedByBoth);
  expect(status.complete).to.equal(true);
} finally {
  process.chdir(originalCwd);
  fs.rmSync(root, { recursive: true, force: true });
}

Testing Request/Response Signing

The following tests the legacy generic document wrapper only. It is not an HTTP-authentication test because method, target, body bytes, audience, trusted key mapping, and atomic replay consumption are absent. New HTTP security tests must exercise JACS v2 and independently mutate every bound request value.

def test_request_signing(test_agent):
    """Test signing a request payload."""
    payload = {
        'method': 'tools/call',
        'params': {'name': 'test_tool'}
    }

    signed = test_agent.sign_request(payload)
    assert signed is not None

    # Verify the signed request
    result = test_agent.verify_response(signed)
    assert result is not None
    assert 'payload' in result

Integration Testing

Testing MCP Integration

Python:

import pytest
import asyncio
from jacs.mcp import JACSMCPServer, JACSMCPClient
from fastmcp import FastMCP

@pytest.fixture
def mcp_server(test_agent):
    """Create a test MCP server."""
    mcp = FastMCP("Test Server")

    @mcp.tool()
    def echo(text: str) -> str:
        return f"Echo: {text}"

    return JACSMCPServer(
        mcp,
        allowed_peer_agent_ids=["TEST_CLIENT_AGENT_ID"],
    )

@pytest.mark.asyncio
async def test_mcp_tool_call(mcp_server, test_agent):
    """Test calling an MCP tool with JACS authentication."""
    # This would require setting up actual server/client connection
    # For unit testing, test the signing/verification separately
    pass

Node.js:

import { JACSExpressMiddleware } from '@hai.ai/jacs/http';
import express from 'express';
import request from 'supertest';

describe('JACS Express Middleware', () => {
  let app;
  let agent;

  beforeAll(() => {
    app = express();
    app.use('/api', express.text({ type: '*/*' }));
    app.use('/api', JACSExpressMiddleware({
      configPath: './jacs.test.config.json'
    }));

    app.post('/api/echo', (req, res) => {
      res.send({ echo: req.jacsPayload });
    });

    agent = new JacsAgent();
    agent.load('./jacs.test.config.json');
  });

  test('accepts valid JACS requests', async () => {
    const signedRequest = agent.signRequest({
      message: 'Hello'
    });

    const response = await request(app)
      .post('/api/echo')
      .set('Content-Type', 'text/plain')
      .send(signedRequest);

    expect(response.status).toBe(200);
  });

  test('rejects invalid requests', async () => {
    const response = await request(app)
      .post('/api/echo')
      .set('Content-Type', 'text/plain')
      .send('{"invalid": "request"}');

    expect(response.status).toBe(400);
  });
});

Testing HTTP Endpoints

This historical example accepts a generic signed document. Do not treat it as an authorization test. Production endpoint tests must verify the v2 request context against a trusted key and prove that a simultaneous replay is accepted exactly once by the shared nonce store.

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
import jacs
import json

app = FastAPI()

@app.post("/api/document")
async def create_doc(request_body: str):
    agent = jacs.JacsAgent()
    agent.load('./jacs.test.config.json')

    result = agent.verify_response(request_body)
    if result:
        # Process the verified payload
        return {"status": "success", "payload": result.get("payload")}
    return {"status": "error"}

@pytest.fixture
def client():
    return TestClient(app)

def test_endpoint_accepts_signed_request(client, test_agent):
    """Test that endpoint accepts properly signed requests."""
    signed = test_agent.sign_request({
        'action': 'create',
        'data': {'title': 'Test'}
    })

    response = client.post("/api/document", content=signed)
    assert response.status_code == 200

Mocking

Mocking JACS Agent

Python:

from unittest.mock import Mock, patch
import json

def test_with_mocked_agent():
    """Test with a mocked JACS agent."""
    mock_agent = Mock()

    # Mock create_document to return a fake signed document
    mock_agent.create_document.return_value = json.dumps({
        'jacsId': 'mock-id',
        'jacsVersion': 'mock-version',
        'content': 'test',
        'jacsSignature': {'signature': 'mock-sig'}
    })

    # Mock verify_document to always return True
    mock_agent.verify_document.return_value = True

    # Use the mock in your tests
    doc = mock_agent.create_document(json.dumps({'content': 'test'}))
    assert mock_agent.verify_document(doc) is True

Node.js:

// Mock for testing
const mockAgent = {
  createDocument: jest.fn().mockReturnValue(JSON.stringify({
    jacsId: 'mock-id',
    jacsVersion: 'mock-version',
    content: 'test',
    jacsSignature: { signature: 'mock-sig' }
  })),
  verifyDocument: jest.fn().mockReturnValue(true),
  signRequest: jest.fn().mockImplementation((payload) =>
    JSON.stringify({ payload, jacsSignature: { signature: 'mock' } })
  ),
  verifyResponse: jest.fn().mockImplementation((response) =>
    ({ payload: JSON.parse(response).payload })
  )
};

test('uses mocked agent', () => {
  const doc = mockAgent.createDocument(JSON.stringify({ test: true }));
  expect(mockAgent.createDocument).toHaveBeenCalled();
  expect(mockAgent.verifyDocument(doc)).toBe(true);
});

Mocking MCP Transport

// Mock transport for MCP testing
class MockTransport {
  constructor() {
    this.messages = [];
  }

  send(message) {
    this.messages.push(message);
  }

  async receive() {
    return this.messages.shift();
  }
}

test('MCP client with mock transport', async () => {
  const mockTransport = new MockTransport();
  // Use mock transport in tests
});

Test Coverage

Rust Coverage

For Rust coverage, we recommend cargo-llvm-cov for its cross-platform support and accuracy with cryptographic code.

Installation:

cargo install cargo-llvm-cov

Running coverage:

# Print coverage summary to stdout
cargo llvm-cov

# Generate and open HTML report in browser
cargo llvm-cov --open

# With specific features enabled
cargo llvm-cov --features cli

# Export LCOV format for CI integration
cargo llvm-cov --lcov --output-path lcov.info

Why cargo-llvm-cov?

Factorcargo-llvm-covtarpaulin
Platform supportLinux, macOS, WindowsLinux primarily
AccuracyLLVM source-based (highly accurate)Ptrace-based (some inaccuracies)
Coverage typesLine, region, branchLine primarily

For CI integration, export to LCOV format and upload to Codecov or similar services.

Python Coverage

# Run tests with coverage
pytest --cov=myapp --cov-report=html tests/

# View coverage report
open htmlcov/index.html

Node.js Coverage

# Run tests with coverage
npm test -- --coverage

# Or with Jest directly
jest --coverage

CI/CD Integration

GitHub Actions

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Install dependencies
        run: npm install

      - name: Generate test keys
        run: |
          mkdir -p test_keys test_data
          # Generate test keys (implementation depends on your setup)

      - name: Run tests
        run: npm test

      - name: Upload coverage
        uses: codecov/codecov-action@v3

Test Environment Variables

# Set test environment
export JACS_TEST_MODE=1
export JACS_TEST_CONFIG=./jacs.test.config.json

RAII Test Fixtures (Rust)

For Rust tests that modify global state (environment variables, file system, etc.), use RAII guards to ensure cleanup even on panic. This pattern is essential for test isolation and reliability.

TrustTestGuard Pattern

The JACS codebase uses a TrustTestGuard pattern for tests that modify the HOME environment variable:

#![allow(unused)]
fn main() {
use std::env;
use tempfile::TempDir;

/// RAII guard for test isolation that ensures HOME is restored even on panic.
struct TrustTestGuard {
    _temp_dir: TempDir,
    original_home: Option<String>,
}

impl TrustTestGuard {
    fn new() -> Self {
        // Save original HOME before modifying
        let original_home = env::var("HOME").ok();

        let temp_dir = TempDir::new().expect("Failed to create temp directory");

        // SAFETY: Only used in #[serial] tests - no concurrent access
        unsafe {
            env::set_var("HOME", temp_dir.path().to_str().unwrap());
        }

        Self {
            _temp_dir: temp_dir,
            original_home,
        }
    }
}

impl Drop for TrustTestGuard {
    fn drop(&mut self) {
        // Restore original HOME even during panic unwinding
        unsafe {
            match &self.original_home {
                Some(home) => env::set_var("HOME", home),
                None => env::remove_var("HOME"),
            }
        }
    }
}

// Usage in tests:
#[test]
#[serial]  // Use serial_test crate to prevent parallel execution
fn test_with_isolated_home() {
    let _guard = TrustTestGuard::new();  // Setup

    // Test code here - HOME points to temp directory

    // Guard automatically restores HOME on drop, even if test panics
}
}

Key benefits:

  • Panic safety: Cleanup runs even if the test panics
  • No manual cleanup: Drop trait handles restoration automatically
  • Environment isolation: Each test gets a fresh temporary directory
  • Composable: Multiple guards can be combined for complex setups

Property-Based Testing

For cryptographic code, property-based testing helps verify invariants that hold across many random inputs. We recommend proptest for Rust.

Key Properties to Test

  1. Round-trip: Sign then verify should always succeed
  2. Tamper detection: Modified content should fail verification
  3. Key independence: Different keys produce different signatures
#![allow(unused)]
fn main() {
use proptest::prelude::*;

proptest! {
    #[test]
    fn signature_roundtrip(content in ".*") {
        let signed = sign_content(&content)?;
        prop_assert!(verify_signature(&signed).is_ok());
    }

    #[test]
    fn tamper_detection(content in ".*", tamper_pos in 0usize..1000) {
        let signed = sign_content(&content)?;
        let tampered = tamper_at_position(&signed, tamper_pos);
        prop_assert!(verify_signature(&tampered).is_err());
    }
}
}

Fuzzing

Fuzz testing is recommended for parsing and decoding functions to discover edge cases and potential security issues.

# Install
cargo install cargo-fuzz

# Create a fuzz target
cargo fuzz init
cargo fuzz add base64_decode

# Run fuzzing
cargo +nightly fuzz run base64_decode

Priority Fuzz Targets for JACS

  1. Base64 decoding - Handles untrusted input from signatures
  2. Agent JSON parsing - Complex nested structures
  3. Document validation - Schema compliance checking
  4. Timestamp parsing - Date/time format handling

Fuzzing documentation will be expanded as fuzz targets are added to the JACS test suite.

Best Practices

1. Isolate Tests

  • Use separate test configurations
  • Create temporary directories for each test run
  • Clean up after tests (use RAII guards in Rust)

2. Test Edge Cases

def test_empty_document():
    """Test handling of empty documents."""
    with pytest.raises(Exception):
        test_agent.create_document('')

def test_invalid_json():
    """Test handling of invalid JSON."""
    with pytest.raises(Exception):
        test_agent.create_document('not json')

def test_large_document():
    """Test handling of large documents."""
    large_content = 'x' * 1000000
    doc = test_agent.create_document(json.dumps({
        'content': large_content
    }))
    assert doc is not None

3. Test Security Properties

def test_signature_changes_with_content():
    """Verify different content produces different signatures."""
    doc1 = test_agent.create_document(json.dumps({'a': 1}))
    doc2 = test_agent.create_document(json.dumps({'a': 2}))

    sig1 = json.loads(doc1)['jacsSignature']['signature']
    sig2 = json.loads(doc2)['jacsSignature']['signature']

    assert sig1 != sig2

4. Test Error Handling

def test_verify_invalid_signature():
    """Test that invalid signatures are rejected."""
    doc = test_agent.create_document(json.dumps({'data': 'test'}))
    parsed = json.loads(doc)

    # Corrupt the signature
    parsed['jacsSignature']['signature'] = 'invalid'
    corrupted = json.dumps(parsed)

    assert test_agent.verify_document(corrupted) is False

See Also

MCP Overview

Use MCP when the boundary is model-to-tool inside an application or local workstation. Use A2A when the boundary is agent-to-agent across organizations or services.

Choose The MCP Path

JACS offers three MCP integration surfaces:

  1. Run jacs mcp for local document verification without signing keys. Select --profile local-sign with an existing signed config to let the local client sign as that agent.
  2. Wrap an existing MCP transport when you already have an MCP server or client and want signed JSON-RPC.
  3. Register JACS as MCP tools when you want the model to call signing, verification, agreement, A2A, or trust operations directly.

Best Fit By Runtime

RuntimeBest starting pointWhat it gives you
Rust CLIjacs mcpVerification by default; explicit local signing of documents and Agreement-v2 artifacts, with optional scoped text/image tools
Pythonjacs.mcp or jacs.adapters.mcpLocal SSE transport security or FastMCP tool registration
Node.js@hai.ai/jacs/mcpTransport proxy or MCP tool registration for existing SDK-based servers

Important Constraints

  • Python MCP wrappers are local-only. JACSMCPClient, JACSMCPServer, and jacs_call() enforce loopback URLs.
  • Unsigned fallback is off by default. Both Python and Node fail closed unless you explicitly allow unsigned fallback.
  • Node has two factories. createJACSTransportProxy() takes a loaded JacsClient or JacsAgent; createJACSTransportProxyAsync() is the config-path variant.
  • The native server's profiles do not configure the Python or Node adapters. Those integrations have their own transport, key-loading and authorization requirements; the examples below do not inherit the native server's closed local-sign inventory.
  • Signatures are provenance, not human approval. Supplied-key verification checks integrity; it does not establish truth, identity, authorization, publication permission or Agreement policy acceptance.

1. Ready-Made Server: jacs mcp

Install the unified binary and start a verification-only server. No identity creation or private-key password is required:

cargo install jacs-cli
jacs mcp

Configure a compatible MCP client with:

{
  "mcpServers": {
    "jacs": {
      "command": "jacs",
      "args": ["mcp"]
    }
  }
}

The default verify-only profile exposes only jacs_verify_document; callers supply the public key and algorithm. An explicitly supplied --config or JACS_CONFIG loads public-only configuration without unlocking private keys.

To sign, reuse an existing signed config, or run jacs init once in the directory where you want to keep the agent identity. Keep using its normal keychain/password source and select it explicitly:

jacs mcp --profile local-sign --config /absolute/path/to/jacs.config.json

Use those same arguments in the MCP client:

{
  "mcpServers": {
    "jacs": {
      "command": "jacs",
      "args": ["mcp", "--profile", "local-sign", "--config", "/absolute/path/to/jacs.config.json"]
    }
  }
}

JACS_CONFIG can supply the config path instead. Local signing requires a valid signed config and persistent encrypted keys in filesystem storage; it does not discover config implicitly or create a replacement identity. Never send a password as a tool argument or embed it in client JSON. Prefer an owner-readable JACS_PASSWORD_FILE or the configured OS keychain.

This profile exposes nine JSON/Agreement-v2 tools when Agreement tools are compiled. To add exactly five text/image tools, grant an existing content directory explicitly at process startup:

JACS_MCP_BASE_DIR=/absolute/path/to/content jacs mcp --profile local-sign --config /absolute/path/to/jacs.config.json

There is no implicit working-directory grant. File tools reject traversal and symlinks and stay beneath the selected directory. Replacing a distinct existing output requires JACS_MCP_OVERWRITE_OK=1; explicit in-place signing remains available. Normal backups contain the original plaintext file, so protect them alongside the content. A content root never expands verify-only. Signing persists ordinary JACS documents under the config directory's documents/ directory and grants agent signing authority, not human approval.

An explicit --profile overrides JACS_MCP_PROFILE; unknown values fail startup. Historical core/full recipes are obsolete; trust-admin and compatibility-only legacy-core remain unavailable. Key administration, trust management, A2A and attestation tools are not enabled by local-sign. The compiled 42-tool contract is not a runtime grant: use tools/list to see the process's actual authorized inventory.

The native server uses stdio only: stdout carries MCP JSON-RPC and diagnostics go to stderr. It uses RMCP 3.3.0, supporting date-versioned MCP 2026-07-28 and legacy 2025-11-25 initialization; these are distinct from SDK major versions such as the TypeScript SDK v2. See the crate MCP README for the complete local-signing and response-error contract.

2. Transport Security Around Your Existing MCP Code

Python

Use jacs.mcp when you already have a FastMCP server or client and want transparent signing around the SSE transport:

from fastmcp import FastMCP
from jacs.mcp import JACSMCPServer

mcp = JACSMCPServer(
    FastMCP("Secure Server"),
    "./jacs.config.json",
    allowed_peer_agent_ids=["CLIENT_AGENT_ID"],
)

For clients:

from jacs.mcp import JACSMCPClient

client = JACSMCPClient(
    "http://localhost:8000/sse",
    "./jacs.config.json",
    expected_peer_agent_id="SERVER_AGENT_ID",
)

Helpful utilities in the same module:

  • create_jacs_mcp_server() for a one-line FastMCP server
  • jacs_middleware() for explicit Starlette middleware wiring
  • jacs_call() for one-off authenticated local calls

See Python MCP Integration for the detailed patterns.

Node.js

Registry status (observed 2026-07-11): npm serves @hai.ai/jacs@0.10.1, while these pages describe source 0.11.4. The registry package does not expose every method shown here. Pin the version and inspect its exports; use a source build only when that is an explicit deployment choice.

Use the transport proxy when you already have an MCP transport:

import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { JacsClient } from '@hai.ai/jacs/client';
import { createJACSTransportProxy } from '@hai.ai/jacs/mcp';

const client = await JacsClient.quickstart({
  name: 'mcp-agent',
  domain: 'mcp.local',
});

const transport = new StdioServerTransport();
const secureTransport = createJACSTransportProxy(transport, client, 'server');

If you only have a config path:

import { createJACSTransportProxyAsync } from '@hai.ai/jacs/mcp';

const secureTransport = await createJACSTransportProxyAsync(
  transport,
  './jacs.config.json',
  'server',
);

See Node.js MCP Integration for examples and tool registration.

3. Register JACS Operations As MCP Tools

This is different from transport security. Here the model gets explicit MCP tools such as jacs_sign_document, jacs_verify_document, agreement helpers, and trust helpers.

Python

from fastmcp import FastMCP
from jacs.client import JacsClient
from jacs.adapters.mcp import (
    register_jacs_tools,
    register_a2a_tools,
    register_trust_tools,
)

client = JacsClient.quickstart(name="mcp-agent", domain="mcp.local")
mcp = FastMCP("JACS Tools")
register_jacs_tools(mcp, client=client)
register_a2a_tools(mcp, client=client)
register_trust_tools(mcp, client=client)

Node.js

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { JacsClient } from '@hai.ai/jacs/client';
import { registerJacsTools } from '@hai.ai/jacs/mcp';

const server = new Server(
  { name: 'jacs-tools', version: '1.0.0' },
  { capabilities: { tools: {} } },
);

const client = await JacsClient.quickstart({
  name: 'mcp-agent',
  domain: 'mcp.local',
});

registerJacsTools(server, client);

Choose the native jacs mcp server for its explicit local-signing boundary. Tool registration in an existing Node or Python server is a separate integration; review that server's exposed operations and authorization.

Provenance MCP tools

With local-sign and an explicit JACS_MCP_BASE_DIR, the native server adds the following tools for inline text and image provenance. They cover the same surface as the sign-text and sign-image CLI verbs:

  • jacs_sign_text — append a YAML-bodied signature block to a markdown / text file.
  • jacs_verify_text — verify all signature blocks in a file (permissive default; strict opt-in).
  • jacs_sign_image — embed a signature in PNG iTXt / JPEG APP11 / WebP XMP.
  • jacs_verify_image — verify the embedded image signature (permissive default; strict opt-in).
  • jacs_extract_media_signature — read out the embedded signature payload (decoded JSON by default; raw_payload opt-in for the base64url wire form).

Example Paths In This Repo

  • jacspy/examples/mcp/server.py
  • jacspy/examples/mcp/client.py
  • jacsnpm/examples/mcp.stdio.server.js
  • jacsnpm/examples/mcp.stdio.client.js

A2A Interoperability

The current exporter uses the legacy A2A v0.4.0 card shape. Interoperability with current released A2A peers remains unproven.

Use A2A when your agent needs to be discoverable and verifiable by another service, team, or organization. This is the cross-boundary story; MCP is the inside-the-app story.

What JACS Adds To A2A

  • Agent Cards with JACS provenance metadata
  • Signed artifacts such as a2a-task or a2a-message
  • Trust policy for deciding whether another agent is acceptable
  • Chain of custody via parent signatures

The Core Flow

1. Export An Agent Card

Python:

from jacs.client import JacsClient

client = JacsClient.quickstart(name="my-agent", domain="my-agent.example.com")
card = client.export_agent_card()

Node.js:

import { JacsClient } from '@hai.ai/jacs/client';

const client = await JacsClient.quickstart({
  name: 'my-agent',
  domain: 'my-agent.example.com',
});

const card = client.exportAgentCard();

2. Serve Discovery Documents

Python and Node provide discovery-only server helpers. These mounts publish well-known documents; they do not implement A2A message/task handling or the legacy optional /jacs/sign and /jacs/verify host endpoint examples in the extension descriptor. A host application must implement and authorize any callable interface separately. Discovery does not grant remote signing access.

Quick demo server:

from jacs.a2a import JACSA2AIntegration

JACSA2AIntegration.quickstart(
    name="my-agent",
    domain="my-agent.example.com",
).serve(port=8080)

Production FastAPI mounting:

from jacs.a2a_server import create_a2a_app, jacs_a2a_routes

app = create_a2a_app(client, title="My A2A Agent")
# or:
# app.include_router(jacs_a2a_routes(client))

Node.js has two discovery helpers:

  • client.getA2A().listen(port) for a minimal demo server
  • jacsA2AMiddleware(client, options) for mounting discovery routes in an existing Express app
import express from 'express';
import { jacsA2AMiddleware } from '@hai.ai/jacs/a2a-server';

const app = express();
app.use(jacsA2AMiddleware(client));
app.listen(3000);

The local listening port does not rewrite the signed public interface URL. Configure the advertised domain before generating the signed card and provide the host service that interface describes. A reachable discovery route alone does not prove that message/task operations exist.

3. Sign And Verify Artifacts

Python:

signed = client.sign_artifact({"artifactId": "a-1", "operation": "classify"}, "artifact")
result = client.get_a2a().verify_wrapped_artifact(signed)
assert result["valid"]

Node.js:

const signed = await client.signArtifact(
  { artifactId: 'a-1', operation: 'classify' },
  'artifact',
);

const result = await client.verifyArtifact(signed);
console.log(result.valid);

Both SDKs require the canonical native A2A verifier for an affirmative result. The generic verify_response() / verifyResponse() document API is not an A2A fallback: even a literal success boolean yields an invalid result with blank signer, version, type, timestamp, and payload provenance, and cannot elevate trust. This prevents parsed attacker-controlled wrapper fields from being presented as authenticated A2A evidence.

The Node signing wrapper also validates the native return contract before releasing it: the document must carry complete portable-v2 signature metadata, its payload and type must match the request, and its ordered parent chain must exactly match the caller's input. If no parents were requested, only an absent or empty returned chain is accepted. This prevents a malformed native result from injecting, dropping, reordering, or changing chain-of-custody evidence.

Trust Policies

Trust policy answers a different question from cryptographic verification.

  • Trust policy: should this remote agent be admitted?
  • Artifact verification: is this specific signed payload valid?

The current policy meanings are:

PolicyBehavior
openAccept all agents without verification
verifiedRequire a valid Agent Card JWS against the same-origin JWKS and a durable jacsId:jacsVersion key pin (default). This is origin/key continuity, not proof of the claimed native JACS identity.
strictRequire an explicitly trusted native JACS root. ES256 cards must also carry the fixed-path compatibility binding, whose native signature, identity/version, binding hash, compatibility JWK/kid, scope, expiry, and signed issuance time all verify. Bindings are accepted for at most seven days after issuedAt (with five minutes of future clock skew), and the verifier durably pins the latest observed hash, kid, and issuedAt; a newer root-signed binding advances the pin and older replays fail closed.

Lifecycle boundary: The absolute seven-day freshness check also applies on first contact, independently of expiresAt. Local discovery generation refreshes an authentic binding after six days under the shared issuance lock, preserving its scopes and explicit expiry. Once a newer native-root-signed binding is observed, JACS also rejects rollback to the old hash/kid. Use an earlier expiresAt when old compatibility keys must stop working sooner, and distribute the refreshed binding promptly after rotation or compromise.

verified means the card signature and a durable same-origin key pin verified. It proves origin/key continuity, not that the self-asserted jacsId is a real-world or explicitly trusted identity. strict is the identity-bound mode.

Python

a2a = client.get_a2a()
assessment = a2a.assess_remote_agent(remote_card_json, policy="strict")

if assessment["allowed"]:
    result = a2a.verify_wrapped_artifact(artifact, assess_trust=True)

Node.js

const a2a = client.getA2A();
const assessment = a2a.assessRemoteAgent(remoteCardJson);

if (assessment.allowed) {
  const result = await a2a.verifyWrappedArtifact(signedArtifact);
}

Bootstrap Patterns

Use the trust store when you want explicit admission:

  • Export the agent document with share_agent() / shareAgent()
  • Exchange the public key with share_public_key() / getPublicKey()
  • Add the remote agent with trust_agent_with_key() / trustAgentWithKey()

This is the cleanest path into strict policy.

ES256-Signed Agent Cards (P2)

Agent cards can also be exported with an ES256 signature from the agent's compatibility key (export_a2a_agent_card in the Rust core and bindings, feature a2a). This is a card-typed exporter, not a generic JWS API:

  • The JWS protected header is pinned to {"alg": "ES256", "typ": "JOSE", "kid": "<compat-key kid>"}typ is JOSE, not JWT.
  • The export is gated by the a2a-agent-card scope of the native-root-signed compatibility key binding (an identity scope, granted by default).
  • The card's metadata carries jacsCompatKid, jacsCompatBindingHash, and the fixed same-origin jacsCompatBindingPath (/.well-known/jacs-compat-binding.json).

A stock JOSE verifier can check the ES256 signature against the agent's JWKS (jacs agent export-jwks), but that proves possession of the compatibility key only. Tracing the card to the agent's native root requires verifying the native-root-signed compatibility key binding (jacs agent export-compat-binding). Native generate_well_known_documents publishes the binding at the fixed path and reuses the same persisted ES256 key across calls and restarts. An obsolete explicit ring-Ed25519 discovery-key choice is rejected rather than silently ignored — see the Security Model.

Under strict, the verifier checks the trusted native root signature, expected jacsId and jacsVersion, binding content hash, compatibility kid and JWK, the a2a-agent-card scope, expiry, and a signed issuedAt no more than seven days old or five minutes in the future. This absolute freshness check applies on first contact; local discovery generation refreshes an authentic binding at six days while preserving its scopes and explicit expiry. A self-advertised JWKS key by itself can never establish the claimed JACS identity.

Current Runtime Differences

  • Python and Node.js: the FastAPI and Express routers serve the six native discovery documents, including the signed card, JWKS and compatibility binding. They are discovery-only; serve() and listen() are convenience hosts, not complete A2A task/message services.
  • The mounted routers refresh their complete snapshots lazily at six days from binding issuance. HTTP freshness ends at renewal or earlier explicit expiry; separate fetches can still straddle replacement. Verify the related documents together and refetch on a mismatch. See Serve Your Agent Card.

Example Paths In This Repo

  • jacspy/tests/test_a2a_server.py
  • jacsnpm/src/a2a-server.js
  • jacsnpm/examples/a2a-agent-example.js
  • jacs/tests/a2a_cross_language_tests.rs

Opt-in A2A 1.0 peer example

The existing exporters and discovery adapters retain the legacy JACS-labeled 0.4.0 card format. The separate native jacs::a2a::v1 projection and examples/a2a_v1_peer.py exercise a finite A2A 1.0 JSON-RPC report exchange against official a2aproject/a2a-python 1.1.4, pinned to commit 2d4d3048b245d2af854bad804f0e722ea9febc08. This example does not enable a new production binding, alter the existing language facade APIs, or host a remote signer. The SDK version and protocol version are different numbers.

From the repository root, with Python 3.14 and uv installed:

uv venv --python python3.14 /tmp/jacs-a2a-peer-env
uv pip sync --python /tmp/jacs-a2a-peer-env/bin/python examples/a2a_v1_peer_requirements.txt
cargo build -p jacs --no-default-features --features a2a --example a2a_v1_peer
/tmp/jacs-a2a-peer-env/bin/python examples/a2a_v1_peer.py --native-bin target/debug/examples/a2a_v1_peer

The test-only lock uses the exact official source commit: PyPI had no 1.1.4 artifact when checked on September 14, 2026. It pins transitive dependencies with distribution hashes. No global package environment is needed.

A disposable native helper creates an encrypted signing identity, a card, its native-root-signed compatibility binding, and a pre-signed synthetic report. It removes its private workspace before a separate recipient starts. A finite loopback host serves the card and a real SDK JSON-RPC interface. The official client follows that returned interface and receives the report as raw application/json artifact bytes. The recipient checks exact bytes, uses the native public-key-only verifier for the document, and separately validates the native binding against an explicitly supplied trusted root before the official SDK verifies the ES256 card signature. No email or external network is used.

The new profile puts native identity and binding references in urn:jacs:provenance-v1 extension parameters, using the current card's standard signature fields. It preserves explicitly supplied optional false booleans and omits absent optional fields and ordinary proto defaults. Required strings and arrays must be nonempty. It rejects caller-supplied binding parameters, duplicate extension URIs, unsupported capability claims, non-object extension parameter roots, and empty/null or unsafe numeric extension values instead of silently changing them. Production interface URLs require HTTPS; explicit loopback HTTP is permitted for this example. The caller must actually implement the declared finite report skill.

This deliberately bounded profile records a pinned-peer limitation: its canonicalizer removes explicit optional empty strings, although the protocol's presence rules preserve them. The peer is not patched. Unsupported values are rejected by the new typed boundary, and the example does not claim general A2A conformance. It tests independent SDK card/JWS handling and message transport; native document and root-binding verification still use JACS. The existing legacy Strict discovery parser is not a v1 parser, and this stateless bridge does not establish rollback history. Key attribution, human approval, current contact authority and Agreement-policy acceptance remain separate decisions.

Verification on September 15, 2026 used macOS arm64, Rust 1.98.1, Python 3.14.7, the pinned SDK, and the reviewed Rustls 0.23.45 lock. The native profile regression and existing legacy discovery/restart regression passed. The actual peer verified three card vectors and received all 1,201 report bytes unchanged; 14 peer/native refusals and seven typed-profile refusals passed, along with private-workspace and host cleanup. This is selected-feature (a2a, without default features) example evidence, not an all-platform or general-conformance certification.

DID Integration (No Blockchain Required)

This chapter describes an integration pattern. JACS ships a lightweight did:wba export surface (jacs w3c did, jacs w3c did-document, and the W3C MCP tools) but no general-purpose DID resolver or full DID method toolchain.

The DID domain comes from --domain at agent creation (stamped into the config as jacs_agent_domain); override it per-export with --origin.

You can still use JACS in DID-oriented architectures without requiring a blockchain or ledger.

Core Position

  • JACS already gives you signed, verifiable agent identity and artifact provenance
  • DID is an interoperability format you can layer on top when needed
  • Blockchain is optional infrastructure, not a dependency of JACS

How Identity Maps

JACS identity primitives:

  • jacsId (stable agent identity)
  • agentVersion (key/version lifecycle)
  • publicKeyHash (verification anchor)
  • Optional DNS domain for public verification

Typical DID mappings:

  • did:web:agent.example.com for web-hosted identity metadata
  • did:key:<multibase> for direct key-based identity
  • Organization-specific DID methods for internal ecosystems

Practical Integration Pattern

  1. Keep JACS as the source of truth for signing and verification.
  2. Publish a DID representation that references the same public key material.
  3. Verify payload signatures with JACS runtime policy (local, dns, registry).
  4. Use DID resolution for discovery/routing metadata where useful.

This avoids duplicate trust stacks while still supporting DID-based interoperability requirements.

Why Teams Choose This Model

  • No chain operations, gas costs, or ledger finality concerns
  • Works in private networks and regulated environments
  • Easier migration path from existing PKI/DNS systems
  • Clear separation: JACS for provenance, DID for interoperable identity documents

Example: did:web + JACS DNS

  • Serve DID metadata via did:web
  • Publish JACS public key fingerprint in DNS TXT
  • Accept external artifacts only when JACS verification succeeds under your trust policy

This gives human-readable identity, standards-friendly discovery, and strong cryptographic verification without blockchain dependencies.

ES256 Compatibility Entries in DID Documents (P2)

When the agent has an ES256 compatibility key and a valid native-root-signed compatibility key binding granting the did scope, generated DID documents (jacs w3c did-document) include two verification-method entries for that key:

  • a JsonWebKey entry (publicKeyJwk) for JOSE consumers, and
  • a Multikey entry (publicKeyMultibase) — the form ecdsa-jcs-2019 Data Integrity proofs reference.

If the compatibility key is absent or the binding is missing, invalid, or lacks the did scope, the DID document is still exported — it simply omits the ES256 entries. The ES256 entries prove possession of the compatibility key only; native-root trust comes from the binding (jacs agent export-compat-binding) — see the Security Model.

JACS-Specific Request Proofs

The request-proof helpers (jacs_w3c_sign_request and jacs_w3c_verify_request in the compiled MCP inventory, backed by the Rust w3c::auth module) use an experimental JACS-specific JSON format: type: "JacsW3cRequestProof", scheme: "DIDWba". This label does not make the JSON object an ANP HTTP Message Signatures request or demonstrate interoperability with an ANP peer. Existing helper names and fields describe the current JACS contract; an ANP wire adapter is a separate integration.

The proof signs canonical request fields, including method, target, claimed creation time, nonce and an optional body digest. Verification checks the supplied DID document and signing key, timestamp policy and nonce use. To bind verification to the request actually received, provide its expected method and URL and its body; inspecting only the proof's own method and URL does not establish that match. A supplied DID document is not, by itself, trusted identity evidence or permission to act. Apply trusted resolution and current authorization policy at the receiving application boundary.

Where to Combine With Other Standards

AP2 (Agent Payments Protocol)

JACS can export the merchant/business-authorization artifact of the UCP AP2-Mandates extension (dated spec revision 2026-01-23) as a detached ES256 JWS, signed with the agent's ecosystem_signing compatibility key.

Scope is deliberately narrow:

  • Exported role: merchant/business authorization only. User-side AP2 Checkout Mandates are SD-JWT-VC and are not produced by JACS — this is not full AP2 conformance, and the docs do not claim it.
  • Outgoing only. JACS does not verify incoming AP2 mandates.
  • CLI + binding only. There is no MCP tool for this export.

Wire format (pinned)

  • Protected header: exactly {"alg":"ES256","kid":"<compat-key kid>"} — no typ, no RFC 7797 b64:false, no crit.
  • Detached compact serialization: <BASE64URL(header)>..<BASE64URL(signature)>.
  • Signing input: BASE64URL(header) + "." + BASE64URL(JCS(payload)), where the payload is the checkout object excluding the ap2 field, canonicalized per RFC 8785 (JCS).

The exporter validates its input against the named schema jacs/schemas/compatibility/ap2-mandate/v1/ap2-mandate.schema.json and rejects anything that does not conform — typed input is a schema check, not a naming convention.

The schema is deliberately open (additionalProperties: true) to match UCP checkout extensibility: extra checkout fields beyond the required core are signed as-is, so relying parties must not assume JACS validated anything beyond the schema's required core fields. Operators granting the ap2-mandate scope are authorizing signatures over extensible checkout payloads.

Usage

The ap2-mandate binding scope is a content scope: it is never auto-issued. Grant it explicitly first (this requires the native root's signature):

jacs agent issue-compat-binding \
  --scopes jwks,did,a2a-agent-card,w3c-agent-identity,ap2-mandate

# inline JSON, a file path, or '-' for stdin all work:
jacs ap2 export-mandate --input checkout.json > export.json
cat checkout.json | jacs ap2 export-mandate --input - > export.json

The export carries the detached JWS, the checkout with ap2.merchant_authorization filled in, and the binding reference as a content hash.

Verification

Any stock JOSE verifier can check the ES256 signature against the agent's JWKS — see the committed reference verifier:

jacs agent export-jwks > jwks.json
npm install jose canonicalize
node scripts/smoke/verify_ap2_jws.mjs export.json jwks.json

Classical verification is not native-root trust. The ES256 check proves possession of the compatibility key only. To trace the mandate to the agent's native root, a JACS-aware relying party additionally verifies the native-root-signed compatibility key binding (jacs agent export-compat-binding) whose content hash the export references. Native JACS documents are never touched by this export — signing a mandate does not create or modify any jacsSignature.

A2A Quickstart

Three focused mini-guides to get your JACS agent working with A2A.

GuideWhat You'll DoTime
1. ServePublish your Agent Card so other agents can find you2 min
2. Discover & TrustFind remote agents and assess their trustworthiness2 min
3. ExchangeSign and verify A2A artifacts with chain of custody3 min

Single-page version: See the A2A Quick Start at the repo root for a 10-line journey.


JACS for A2A Developers

Already using the A2A protocol? Here's what JACS adds -- and what stays the same.

What Stays the Same

  • Agent Cards currently export the legacy v0.4.0 shape, preserving fields such as name, description, skills and url. Interoperability with current released A2A peers remains unproven; this guide does not claim current wire compatibility.
  • Discovery publishes the native six-document well-known set, including /.well-known/agent-card.json. The bundled routers do not implement message/task or remote-signing endpoints; callable services require separate host implementation and authorization.
  • JSON-RPC transport is untouched. JACS works alongside A2A, not instead of it.

What JACS Adds

A2A AloneWith JACS
Agent Card has no signatureAgent Card is JWS-signed + JWKS published
Artifacts are unsigned payloadsArtifacts carry jacsSignature with signer ID, algorithm, and timestamp
Trust is transport-level (TLS)Trust is data-level -- signatures persist offline
No chain of custodyparent_signatures link artifacts into a verifiable chain
No standard trust policyopen / verified / strict policies built in

Minimal Integration (Add JACS to Existing A2A Code)

If you already serve an Agent Card, adding JACS provenance takes two steps:

Step 1: Add the JACS extension to your Agent Card's capabilities:

{
  "capabilities": {
    "extensions": [{
      "uri": "urn:jacs:provenance-v1",
      "description": "JACS cryptographic document signing",
      "required": false
    }]
  }
}

Step 2: Sign artifacts before sending them:

from jacs.client import JacsClient

client = JacsClient.quickstart(name="my-agent", domain="my-agent.example.com")
# Wrap your existing artifact payload
signed = client.sign_artifact(your_existing_artifact, "artifact")
# Send `signed` instead of the raw artifact

Receiving agents that don't understand JACS will ignore the extra fields. Receiving agents that do understand JACS can verify the signature and assess trust.

Dual Key Architecture

JACS generates two key pairs per agent:

  • A native JACS root (pq2025 by default, or ring-Ed25519) for JACS documents and compatibility-key authorization.
  • A persisted P-256 / ES256 compatibility key for RFC 8785-canonicalized Agent Card JWS signatures.

The native root signs the published compatibility binding, so strict verifiers can connect the ES256 card key to an explicitly trusted JACS identity.


Troubleshooting FAQ

Q: pip install jacs[a2a-server] fails. A: The a2a-server extra requires Python 3.10+ and adds FastAPI + uvicorn. If you only need signing (not serving), use pip install jacs with no extras.

Q: discover_and_assess returns jacs_registered: false. A: The remote agent's Agent Card does not include the urn:jacs:provenance-v1 extension. This is normal for non-JACS A2A agents. With the open trust policy, they are still allowed; with verified, they are rejected.

Q: Verification returns valid: true but trust.allowed: false. A: The card signature may be correct while identity policy still rejects it. For strict, import the full native self-signed agent document and explicit native public key through an authenticated out-of-band channel with a2a.trust_a2a_agent(agent_document_json, public_key_pem). Passing the self-advertised Agent Card is rejected.

Q: sign_artifact raises "no agent loaded". A: Call JacsClient.quickstart(name="my-agent", domain="my-agent.example.com") or JacsClient(config_path=...) before signing. The client must have a loaded agent with keys.

Q: Agent Card export returns empty skills. A: Skills are part of the signed Agent Card. Configure them on the agent before generating or mounting discovery documents; servers reject post-signing skill overrides.

Q: My existing A2A client doesn't understand the JACS fields. A: This is expected. JACS fields (jacsId, jacsSignature, jacsSha256) are additive. Non-JACS clients should ignore unknown fields per JSON convention. If a client rejects them, strip JACS fields before sending by extracting signed["payload"].

Q: How do I verify artifacts from agents I've never seen before? A: Use JACS_KEY_RESOLUTION to configure key lookup. Set JACS_KEY_RESOLUTION=local,hai to check your local cache first, then the HAI key service. For offline-only verification, set JACS_KEY_RESOLUTION=local.


Next Steps

Serve Your Agent Card

The current exporter uses the legacy A2A v0.4.0 card shape. Interoperability with current released A2A peers remains unproven.

Registry status (observed 2026-07-11): npm serves @hai.ai/jacs@0.10.1, while these pages describe source 0.11.4. The registry package does not expose every method shown here. Pin the version and inspect its exports; use a source build only when that is an explicit deployment choice.

Make your JACS agent discoverable by other A2A agents.

These helpers serve discovery documents only. They do not implement message/task handlers or the legacy optional /jacs/sign and /jacs/verify host examples in the extension descriptor. Configure the signed public interface for a service your host actually implements; a local discovery listener does not create that service or grant remote signing access.

Prerequisites: pip install jacs[a2a-server] (Python) or npm install @hai.ai/jacs express (Node.js).

from jacs.a2a import JACSA2AIntegration

JACSA2AIntegration.quickstart(name="my-agent", domain="my-agent.example.com").serve(port=8080)

Your agent is now discoverable at http://localhost:8080/.well-known/agent-card.json.

This local listener does not rewrite the card's signed public interface URL.

Production: Mount into Your Own FastAPI App

from fastapi import FastAPI
from jacs.client import JacsClient
from jacs.a2a_server import jacs_a2a_routes

app = FastAPI()
client = JacsClient.quickstart(name="my-agent", domain="my-agent.example.com")
router = jacs_a2a_routes(client)
app.include_router(router)
const express = require('express');
const { JacsClient } = require('@hai.ai/jacs/client');
const { jacsA2AMiddleware } = require('@hai.ai/jacs/a2a-server');

const client = await JacsClient.quickstart({
  name: 'my-agent',
  domain: 'my-agent.example.com',
});
const app = express();
app.use(jacsA2AMiddleware(client));
app.listen(8080);

Your agent is now discoverable at http://localhost:8080/.well-known/agent-card.json.

What Gets Served

The native generator serves six .well-known endpoints automatically:

EndpointPurpose
/.well-known/agent-card.jsonA2A Agent Card with JWS signature
/.well-known/jwks.jsonStable ES256 compatibility JWK used by the Agent Card
/.well-known/jacs-compat-binding.jsonNative-root-signed binding from the ES256 key to the claimed JACS identity
/.well-known/jacs-agent.jsonJACS agent descriptor
/.well-known/jacs-pubkey.jsonJACS public key
/.well-known/jacs-extension.jsonJACS provenance extension descriptor

The Agent Card includes the urn:jacs:provenance-v1 extension in capabilities.extensions, signaling to other JACS agents that your agent supports cryptographic provenance.

The card and JWKS reuse the persisted ES256 compatibility key across calls and restarts. The card references /.well-known/jacs-compat-binding.json by both fixed path and content hash; strict verification checks that native-root-signed artifact before treating the card as the explicitly trusted JACS identity. Existing pre-compatibility agents must run jacs agent add-compat-key once before serving. Local loopback trust tests additionally require JACS_ALLOW_PRIVATE_JWKS=true; private-address JWKS fetching is otherwise denied.

Strict verifiers accept a compatibility binding for at most seven days after its signed issuedAt, with five minutes of future clock skew. Generating the well-known set refreshes an authentic binding after six days under the shared issuance lock, preserving its scopes and any explicit expiresAt. The FastAPI and Express mounts call the generator lazily at that six-day boundary. HTTP freshness ends by renewal or earlier expiry; failed renewal can serve a still-valid snapshot with no-store, while hard expiry returns 503. Finite expiry requires renewed authorization and remounting. Separate resource fetches can straddle replacement; verify the card/JWKS/binding together and refetch if their references differ.

Next Steps

Discover & Trust Remote Agents

Find other A2A agents and decide whether to trust them.

from jacs.a2a_discovery import discover_and_assess_sync

result = discover_and_assess_sync("https://agent.example.com")
if result["allowed"]:
    print(f"Trusted: {result['card']['name']} ({result['trust_level']})")

Add to Your Trust Store

For strict policy, establish native JACS trust from an authenticated agent document and explicit native public key. An Agent Card is self-advertised and cannot create strict identity trust by itself:

from jacs.client import JacsClient
from jacs.a2a import JACSA2AIntegration

client = JacsClient.quickstart(name="my-agent", domain="my-agent.example.com")
a2a = JACSA2AIntegration(client, trust_policy="strict")

# Assess a remote agent's trustworthiness
assessment = a2a.assess_remote_agent(remote_card_json)
print(f"JACS registered: {assessment['jacs_registered']}")
print(f"Allowed: {assessment['allowed']}")

# Obtain both values through an authenticated out-of-band channel.
a2a.trust_a2a_agent(remote_native_agent_json, remote_native_public_key_pem)

Async API

from jacs.a2a_discovery import discover_agent, discover_and_assess

card = await discover_agent("https://agent.example.com")
result = await discover_and_assess("https://agent.example.com", policy="verified", client=client)
const { discoverAndAssess } = require('@hai.ai/jacs/a2a-discovery');

const result = await discoverAndAssess('https://agent.example.com');
if (result.allowed) {
  console.log(`Trusted: ${result.card.name} (${result.trustLevel})`);
}

Add to Your Trust Store

const { JacsClient } = require('@hai.ai/jacs/client');
const { JACSA2AIntegration } = require('@hai.ai/jacs/a2a');

const client = await JacsClient.quickstart({
  name: 'my-agent',
  domain: 'my-agent.example.com',
});
const a2a = new JACSA2AIntegration(client, 'strict');

// Assess a remote agent
const assessment = a2a.assessRemoteAgent(remoteCardJson);
console.log(`JACS registered: ${assessment.jacsRegistered}`);
console.log(`Allowed: ${assessment.allowed}`);

// Obtain both through an authenticated out-of-band channel. Passing an
// Agent Card here is rejected.
a2a.trustA2AAgent(remoteNativeAgentDocument, remoteNativePublicKeyPem);

Trust Policies

PolicyBehavior
openAccept all agents without verification
verifiedRequire a valid Agent Card JWS against the same-origin JWKS and a durable jacsId:jacsVersion key pin (default). This is origin/key continuity, not proof of the claimed native JACS identity.
strictRequire an explicitly trusted native JACS root. ES256 cards must also carry the fixed-path compatibility binding, whose native signature, identity/version, binding hash, compatibility JWK/kid, scope, expiry, and signed issuance time all verify. Bindings are accepted for at most seven days after issuedAt (with five minutes of future clock skew), and the verifier durably pins the latest observed hash, kid, and issuedAt; a newer root-signed binding advances the pin and older replays fail closed.

Lifecycle boundary: The absolute seven-day freshness check also applies on first contact, independently of expiresAt. Local discovery generation refreshes an authentic binding after six days under the shared issuance lock, preserving its scopes and explicit expiry. Once a newer native-root-signed binding is observed, JACS also rejects rollback to the old hash/kid. Use an earlier expiresAt when old compatibility keys must stop working sooner, and distribute the refreshed binding promptly after rotation or compromise.

How Trust Flows

1. Discover  -- Fetch /.well-known/agent-card.json from a remote URL
2. Assess    -- Check for JACS extension, verify signatures
3. Decide    -- Trust policy determines if the agent is allowed
4. Trust     -- Optionally add the agent to your local trust store

With open, all agents pass step 3 without identity assurance. With verified, the Agent Card JWS must verify against the same-origin JWKS and the ES256 key must match its durable TOFU pin; this proves origin/key continuity, not native identity. With strict, the native root must be explicitly trusted and its signed compatibility binding must authorize the exact card key, identity, version, scope, and current binding hash.

Next Steps

Exchange Signed Artifacts

Sign artifacts with cryptographic provenance and verify artifacts from other agents.

Sign and Verify

from jacs.client import JacsClient

client = JacsClient.quickstart(name="my-agent", domain="my-agent.example.com")

# Sign an artifact
signed = client.sign_artifact({"action": "classify", "input": "data"}, "artifact")

# Verify it (with trust assessment)
a2a = client.get_a2a()
result = a2a.verify_wrapped_artifact(signed, assess_trust=True)
print(f"Valid: {result['valid']}, Allowed: {result['trust']['allowed']}")

Chain of Custody

When multiple agents process data in sequence, link artifacts into a verifiable chain:

# Agent A signs step 1
step1 = client_a.sign_artifact({"step": 1, "data": "raw"}, "artifact")

# Agent B signs step 2, referencing step 1 as parent
step2 = client_b.sign_artifact(
    {"step": 2, "data": "processed"},
    "artifact",
    parent_signatures=[step1],
)

# Verify the full chain
result = a2a.verify_wrapped_artifact(step2)
assert result["valid"]
assert result["parent_signatures_valid"]

Build an Audit Trail

chain = a2a.create_chain_of_custody([step1, step2])
# chain contains: steps (ordered), signers, timestamps, validity

Sign and Verify

const { JacsClient } = require('@hai.ai/jacs/client');

const client = await JacsClient.quickstart({
  name: 'my-agent',
  domain: 'my-agent.example.com',
});

// Sign an artifact
const signed = await client.signArtifact({ action: 'classify', input: 'data' }, 'task');

// Verify it
const a2a = client.getA2A();
const result = a2a.verifyWrappedArtifact(signed);
console.log(`Valid: ${result.valid}`);

Chain of Custody

// Agent A signs step 1
const step1 = await clientA.signArtifact({ step: 1, data: 'raw' }, 'message');

// Agent B signs step 2, referencing step 1
const step2 = await clientB.signArtifact(
  { step: 2, data: 'processed' }, 'message', [step1],
);

// Verify the full chain
const result = a2a.verifyWrappedArtifact(step2);
console.log(`Chain valid: ${result.valid}`);
console.log(`Parents valid: ${result.parentSignaturesValid}`);

Artifact Types

The artifact_type parameter labels the payload for downstream processing:

TypeUse Case
taskTask assignments, work requests
messageInter-agent messages
resultTask results, responses

You can use any string -- these are conventions, not enforced types.

What Gets Signed

Every signed artifact includes:

FieldDescription
jacsIdUnique document ID
jacsSignatureSigner ID, algorithm, timestamp, and base64 signature
jacsSha256Content hash for integrity verification
jacsTypeThe artifact type label
jacsParentSignaturesParent artifacts for chain of custody (if any)
payloadThe original artifact data

Non-JACS receivers can safely ignore the jacs* fields and extract payload directly.

Next Steps

Sign vs. Attest: When to Use Which

This guide helps you choose the right JACS API for your use case.

Decision Tree

Start here: What do you need to prove?

  1. "This data hasn't been tampered with"

    • Use sign_message() / signMessage()
    • This gives you a cryptographic signature and integrity hash.
  2. "This data hasn't been tampered with AND here's why it should be trusted"

    • Use create_attestation() / createAttestation()
    • This gives you signature + integrity + claims + evidence + derivation chain.
  3. "I have an existing signed document and want to add trust context"

    • Use lift_to_attestation() / liftToAttestation()
    • This wraps an existing JACS-signed document into a new attestation.
  4. "I need to export a trust proof for external systems"

    • Use export_attestation_dsse() / exportAttestationDsse()
    • This creates an in-toto DSSE envelope compatible with SLSA and Sigstore.
  5. "I need to send signed data to another agent or service"

Quick Reference

ScenarioAPIOutput
Log an AI actionsign_message()Signed document
Record a human review decisioncreate_attestation()Attestation with claims
Attach evidence from another systemcreate_attestation() with evidenceAttestation with evidence refs
Wrap an existing signed doc with trust contextlift_to_attestation()New attestation referencing original
Export for SLSA/Sigstoreexport_attestation_dsse()DSSE envelope
Verify signature onlyverify()Valid/invalid + signer
Verify signature + claims + evidenceverify_attestation(full=True)Full verification result
Exchange artifact with another agentsign_artifact() / A2ASigned wrapped artifact

Native Signing vs. Compatibility Exports

Every API above signs natively: the output carries a JACS jacsSignature from the agent's selected native root (pq2025 by default, or ring-Ed25519 when explicitly selected). Separately, the CLI offers targeted ecosystem exporters (jacs agent export-jwks, jacs ap2 export-mandate, jacs agreement-v2 export-vc, the A2A agent card) that sign derived artifacts with the agent's ES256 compatibility key. Exports are one-way views: the native document never gains proof or projection fields, and an ES256 signature by itself proves possession of the compatibility key only — the native-root-signed compatibility key binding is what ties that key to the agent. There is no generic "sign this document with algorithm X" API, and verifying incoming AP2/VC artifacts is out of scope. See the Security Model.

Examples

Just need integrity? Use signing.

signed = client.sign_message({"action": "approve"})
result = client.verify(signed.raw_json)
# result["valid"] == True

Need trust context? Use attestation.

att = client.create_attestation(
    subject={"type": "artifact", "id": "doc-001", "digests": {"sha256": "..."}},
    claims=[{"name": "reviewed", "value": True, "confidence": 0.95}],
)
result = client.verify_attestation(att.raw_json, full=True)
# result["valid"] == True, result["evidence"] == [...]

Already signed? Lift to attestation.

signed = client.sign_message({"content": "original"})
att = client.lift_to_attestation(signed, [{"name": "approved", "value": True}])
# att now has attestation metadata referencing the original document

Common Patterns

AI Agent Action Logging

Use sign_message() for each tool call or action. The signature proves the agent took the action and the data hasn't been modified.

Human Review Attestation

Use create_attestation() with claims like reviewed_by: human and confidence: 0.95. This creates an auditable record that a human reviewed and approved the output.

Multi-step Pipeline

Use create_attestation() with a derivation field to capture input/output relationships. Each step attests to its own transformation with references to upstream attestations.

Cross-system Verification

Use export_attestation_dsse() to generate an in-toto DSSE envelope that external systems (SLSA verifiers, Sigstore) can validate independently.

Agreement v2 Developer Guide

Agreement v2 is a standalone jacsType: "agreement" document with consent-signature and signed-transcript-prefix inspection. Its party proofs do not authenticate the full role, quorum, lineage, or finalization policy. A successful mathematical check is not permission to act on the agreement.

Rust owns the protocol checks; Python, Node.js, Go, CLI, MCP, and WASM expose related JSON workflows. Native verify_agreement_v2 reports mathematicalChecksValid separately and always returns valid: false, policyAccepted: false, and overallScope: "consent_signatures_only". The portable inspection report likewise does not accept application policy. Do not interpret either result as proof of human approval or authority.

Mental Model

  • The JACS header owns document identity, versioning, authorship signatures, content hash, registration, files, and visibility.
  • The agreement body records terms and consent-signature inputs: title, description, terms, parties, signaturePolicy, agreementSignatures, transcript, links, controllers, and owners.
  • jacsAgreementHash is the consent hash. It changes when terms, parties, policy, effective dates, or expiry dates change.
  • Transcript appends do not change jacsAgreementHash. They change the transcript hash that later agreement signatures bind.
  • Links are intentionally slim: only jacsId and jacsVersion. Put relationship meaning in the successor agreement's terms and status.

Minimal Input

{
  "title": "Refund approval",
  "description": "Agent A and Agent B agree on the refund action.",
  "terms": "Agent B may issue a refund up to $25 for order 123.",
  "termsFormat": "text/plain",
  "status": "proposed",
  "parties": [
    { "agentId": "00000000-0000-4000-8000-000000000001", "agentType": "ai", "role": "signer" },
    { "agentId": "00000000-0000-4000-8000-000000000002", "agentType": "human", "role": "signer" },
    { "agentId": "00000000-0000-4000-8000-000000000003", "agentType": "ai", "role": "notary" }
  ],
  "signaturePolicy": {
    "partyQuorum": "all",
    "witnessRequired": 0,
    "notaryRequired": 1,
    "minimumStrength": "classical"
  },
  "controllers": [
    "00000000-0000-4000-8000-000000000001",
    "00000000-0000-4000-8000-000000000002",
    "00000000-0000-4000-8000-000000000003"
  ]
}

The schema labels are signer, witness, notary, and observer; observers do not sign. V2 helpers check membership against supplied roles, but that does not establish portable role authority. An agentType: "human" label is metadata: an automated signature with that label is not evidence that a person reviewed or approved the terms.

Workflow

  1. Create the agreement from the input JSON.
  2. Append transcript references as negotiation messages, statements, or evidence are emitted as separate JACS documents.
  3. Update terms only when the actual agreement language changes. This clears prior agreement signatures because jacsAgreementHash changes.
  4. Sign as signer, witness, or notary.
  5. Inspect signatures and transcript tamper evidence. Native role counts and expectedStatus are structural diagnostics over the supplied document; neither they nor stored status authorize an action.
  6. Resolve concurrent branches in Rust core: transcript-only branches auto-merge; terms conflicts require an explicit successor mutation.

Prerequisites

Before the first agreement-v2 call you need a loaded agent with keys on disk and a config:

  • CLI: run jacs quickstart --name my-agent --domain example.com once. It creates ./jacs.config.json, a key pair, and (if missing) a generated password under ./jacs_keys. Set JACS_PRIVATE_KEY_PASSWORD or let quickstart manage it.
  • Python / Node.js: load or create a SimpleAgent (SimpleAgent.create_agent(...) / JacsSimpleAgent.create(...)), or use ephemeral() for throwaway single-agent demos.

Multiple distinct agents must share a data_directory (or exchange public keys out of band) so a verifier can resolve every signer's key. Ephemeral agents keep keys in memory only and cannot verify each other's signatures.

Roles, Quorum, and Notaries

  • signaturePolicy.partyQuorum is all or an integer M (M-of-N signer parties).
  • witnessRequired and notaryRequired are separate inputs to structural status calculation; their presence does not authenticate a witness or notary role.
  • controllers is the supplied list checked by mutation helpers; the header signature does not prove that the parties authorized that controller list. owners are soft copyright claims. Keep both distinct from parties.

Mutations

All public surfaces accept the same mutation JSON:

{ "type": "appendTranscript", "entry": { "jacsId": "...", "jacsVersion": "...", "jacsSha256": "..." } }
{ "type": "updateTerms", "terms": "Updated agreement text." }
{ "type": "setStatus", "status": "proposed" }
{ "type": "addLink", "link": { "jacsId": "...", "jacsVersion": "..." } }

updateTerms also accepts optional title, description, termsFormat, effectiveFrom, and expiresAt.

Python

from jacs import SimpleAgent

agent, info = SimpleAgent.ephemeral(algorithm="ed25519")
agent_id = info["agent_id"]

agreement = agent.create_agreement_v2({
    "title": "Refund approval",
    "description": "Approval for a bounded refund.",
    "terms": "Refund up to $25 for order 123.",
    "status": "proposed",
    "parties": [{"agentId": agent_id, "agentType": "ai", "role": "signer"}],
    "signaturePolicy": {"partyQuorum": "all", "witnessRequired": 0, "notaryRequired": 0},
    "controllers": [agent_id],
})

signed = agent.sign_agreement_v2(agreement, "signer")
report = agent.verify_agreement_v2(signed)
assert report["mathematicalChecksValid"] is True
assert report["valid"] is False
assert report["policyAccepted"] is False

Node.js

This single-agent example self-signs and self-verifies, so the agent resolves its own key. For multiple distinct agents, see the multi-agent walkthrough, which shares key storage so each agent can resolve the others' public keys.

import { JacsSimpleAgent } from "@hai.ai/jacs";

async function main() {
  const agent = JacsSimpleAgent.ephemeral("ed25519");
  const agentId = agent.getAgentId();

  const agreement = await agent.createAgreementV2(JSON.stringify({
    title: "Refund approval",
    description: "Approval for a bounded refund.",
    terms: "Refund up to $25 for order 123.",
    status: "proposed",
    parties: [{ agentId, agentType: "ai", role: "signer" }],
    signaturePolicy: { partyQuorum: "all", witnessRequired: 0, notaryRequired: 0 },
    controllers: [agentId],
  }));

  const signed = await agent.signAgreementV2(agreement, "signer");
  const report = await agent.verifyAgreementV2(signed);
  console.log("mathematical checks:", report.mathematicalChecksValid);
  console.log("policy accepted:", report.policyAccepted); // always false for v2
}

main();

CLI

jacs quickstart --name agent-a --domain example.com
jacs agreement-v2 create --input agreement-input.json > agreement.json
jacs agreement-v2 sign --agreement agreement.json --role signer > signed.json
jacs agreement-v2 verify --agreement signed.json

The verify command emits the inspection report and exits non-zero because v2 does not produce an accepted policy verdict. This is the expected result even when the mathematical checks pass.

For branch handling:

jacs agreement-v2 detect-conflict --base base.json --left left.json --right right.json
jacs agreement-v2 merge-transcript --base base.json --left left.json --right right.json
jacs agreement-v2 resolve-conflict --base base.json --previous left.json --side right.json --mutation resolution.json

Branch Merge and Conflicts

Two agents can emit successor versions from the same prior version. Resolve them in core, never by editing JSON by hand:

  • Transcript-only branches (each side only appended transcript entries) auto-merge. Detect with detect-conflict, then merge-transcript.
  • Any terms, party, policy, status, signature, controller, or link divergence is a real conflict and requires an explicit successor via resolve-conflict (CLI) or resolveAgreementV2BranchConflict / resolve_agreement_v2_branch_conflict (Node/Python).
jacs agreement-v2 detect-conflict --base base.json --left left.json --right right.json
jacs agreement-v2 merge-transcript --base base.json --left left.json --right right.json
jacs agreement-v2 resolve-conflict --base base.json --previous left.json --side right.json --mutation resolution.json

Golden Example

The repository includes a three-party Python example of the v2 consent-signature workflow:

python examples/agreement_v2_three_party.py

It creates Agent A and Agent B as signer parties, a local demo agent labeled notary, and Agent X as an outsider. The example appends transcript references, rejects outsider mutation/signing, collects two signer signatures plus the demo notary signature, and inspects their mathematical validity. The resulting v2 report does not establish a portable finalization or human-approval verdict. The example checks mathematicalChecksValid: true while requiring valid: false and policyAccepted: false, and rejects transcript and signature tampering.

MCP and WASM

MCP tools mirror the CLI:

  • jacs_create_agreement_v2
  • jacs_apply_agreement_v2
  • jacs_sign_agreement_v2
  • jacs_verify_agreement_v2
  • jacs_detect_agreement_v2_branch_conflict
  • jacs_merge_agreement_v2_transcript_branches
  • jacs_resolve_agreement_v2_branch_conflict

WASM exposes the same flow as JSON-string methods: createAgreementV2Json, applyAgreementV2Json, signAgreementV2Json, verifyAgreementV2Json, detectAgreementV2BranchConflictJson, mergeAgreementV2TranscriptBranchesJson, and resolveAgreementV2BranchConflictJson.

DevEx Rules

  • Use core helpers instead of hand-editing agreement JSON. The helpers maintain jacsAgreementHash, allPreviousVersions, jacsPreviousVersion, and status transitions.
  • Keep parties, controllers, and owners separate. Party signatures record agent provenance; controllers are checked by version helpers; owners are soft copyright claims. None of those labels establishes human approval.
  • Treat stored status as a cache. V2 inspection cannot establish authority to act on that status; the application must separately establish its authorization policy.
  • Use transcript entries for process evidence and links for agreement lineage. Links stay {jacsId, jacsVersion} by design.
  • For post-final terms changes, create a successor agreement or explicit conflict resolution rather than mutating a final agreement in place.
  • Principal delegation (one agent signing on behalf of another party) is unsupported and unscheduled. The signing agent must be listed in parties with the matching role; delegatedBy and delegationChain are rejected. This does not change ES256 export-key delegation through compatibility key bindings, or SDK forwarding of calls to Rust.

Migrating from v1 agreements

JACS still ships the original v1 "sidecar" agreement: a jacsAgreement field attached to an existing signed document, with separate create_agreement / sign_agreement / check_agreement calls and an AgreementOptions struct. V1 and v2 both permit mathematical signature inspection without proving the complete authorization policy. V2 additionally supplies a standalone identity, terms hash, transcript-prefix binding, and version/branch helpers. The presence of parties, roles, and signaturePolicy fields does not mean all of those claims are authenticated by each party proof.

V1 and v2 remain available for compatibility and inspection. Moving a payload to v2 does not make it an actionable authorization.

Conceptual difference

  • v1 sidecar: signatures over a referenced document. The agreement is a field bolted onto some other JSON. No standalone identity, no roles beyond "must sign", no transcript, no notary, no branch handling.
  • v2 document: a first-class agreement document. Parties carry role (signer / witness / notary / observer), the signaturePolicy expresses quorum and witness/notary requirements, a transcript records process evidence, and successor versions / branches are reconciled by core helpers.

Operation mapping

v1 operationv2 equivalent
create_agreement (binding) / jacs document create-agreement (CLI)create_agreement_v2 / jacs agreement-v2 create
sign_agreement (binding) / jacs document sign-agreement (CLI)sign_agreement_v2 / jacs agreement-v2 sign --role signer
check_agreement (binding) / jacs document check-agreement (CLI)verify_agreement_v2 / jacs agreement-v2 verify
AgreementOptions.quorum (M-of-N)signaturePolicy.partyQuorum (all, majority, or integer M)
AgreementOptions.timeoutsignaturePolicy.timeout
AgreementOptions.required_algorithmssignaturePolicy.requiredAlgorithms
AgreementOptions.minimum_strengthsignaturePolicy.minimumStrength

There is no v1 equivalent for v2 mutations (apply_agreement_v2 / jacs agreement-v2 apply) or branch handling (jacs agreement-v2 detect-conflict / merge-transcript / resolve-conflict) - these are new in v2.

What stays the same

  • The cryptographic signing model: the same agent identity and keys sign in both versions, and signatures are JACS signatures over canonical content.
  • You still load an agent with keys on disk (or ephemeral() for single-agent demos) before any agreement call.

What is new in v2

  • Roles: signer, witness, notary, observer (v1 has only "agents that must sign").
  • Notary and witness requirements (signaturePolicy.notaryRequired, signaturePolicy.witnessRequired), counted independently of signer quorum.
  • A transcript with tamper evidence for each signature's covered prefix.
  • Branch merge / conflict resolution for concurrent successor versions.
  • A diagnostic report from verify_agreement_v2: mathematicalChecksValid, expectedStatus, errors, and chain coverage. The retained valid and policyAccepted fields are always false.
  • Fail-closed CLI: jacs agreement-v2 verify exits non-zero for v2 because no accepted policy verdict is produced.

For the full v2 walkthrough, see the Workflow, Python, Node.js, and CLI sections above and the Rust core / legacy comparison.

Verification Matrix

This is a source-test inventory, not evidence that every published SDK has been exercised together. It does not establish all-language discovery parity.

ScenarioCoverage
Create standalone Agreement v2Rust core tests, binding parity fixture, CLI/MCP/WASM tests
Structural signer-count/quorum calculation; not portable policy acceptanceRust core tests and shared parity fixture
Structural notaryRequired calculation; not authenticated notary authorityRust core tests and language parity tests
Human agentType labels; not human-approval evidenceRust core tests
Outsider cannot mutateRust core authorization tests
Outsider cannot signRust core role-membership tests
Transcript append preserves jacsAgreementHashRust core tests
Signed-prefix tamper/reorder/substitution detection; no per-entry hash chainRust core tests
Terms edit changes jacsAgreementHash and clears signaturesRust core tests
effectiveFrom and expiresAtRust core tests
allPreviousVersions chain reconciliationRust core tests
Links are only {jacsId, jacsVersion}Rust core tests and parity fixture
Transcript-only branch auto-mergeRust core, binding parity, CLI, MCP, WASM tests
Terms conflict requires explicit resolutionRust core, binding parity, CLI, MCP, WASM tests
Key rotation / agentVersion matchingRust core tests
Shared agreement workflow fixturesPython, Node.js, Go, CLI, MCP, and WASM parity tests

The fixture binding-core/tests/fixtures/agreement_v2_scenarios.json is the portable workflow source of truth. Update it when an exposed workflow changes so every binding stays aligned.

Exporting an Agreement as a Verifiable Credential

jacs agreement-v2 export-vc (or SimpleAgent::export_agreement_v2_as_vc / export_agreement_v2_as_vc_json in the bindings) projects an Agreement-v2 document into a W3C Verifiable Credential 2.0 carrying an ecdsa-jcs-2019 Data Integrity proof, signed with the agent's ES256 compatibility key:

# the agreement-vc content scope is never auto-issued — grant it first
jacs agent issue-compat-binding \
  --scopes jwks,did,a2a-agent-card,w3c-agent-identity,agreement-vc

jacs agreement-v2 export-vc --agreement agreement.json > vc_export.json

The VC's @context is ["https://www.w3.org/ns/credentials/v2", "https://hai.ai/ns/credentials/jacs-agreement/v1"], its type is ["VerifiableCredential", "JacsAgreementCredential"], and credentialSubject.jacsAgreementV2 embeds the agreement verbatim. The proof's verificationMethod references the Multikey entry of the agent's DID document (conformant ecdsa-jcs-2019 verifiers reject JWK-typed methods). The export is a derived view: the native agreement's bytes, jacsSignature (using the agent's selected native algorithm), and verification are unchanged.

A stock Data Integrity verifier can check the proof classically — see scripts/smoke/verify_di_vc.mjs. That proves possession of the ES256 key only; tracing the credential to the agent's native root requires the native-root-signed compatibility key binding (jacs agent export-compat-binding). Verifying incoming VCs is out of scope for JACS in P2.

Troubleshooting

  • report.valid and report.policyAccepted are always false for v2. To diagnose mathematical failures, inspect mathematicalChecksValid and errors; a missing signer key is one possible failure, not the reason for the unconditional policy rejection.
  • "outsider" rejection on sign/apply: the acting agent is not listed in parties with the matching role. Add it to parties (and to controllers for mutations).
  • Status looks wrong: expectedStatus is a structural diagnostic. Neither it nor stored status is an accepted authorization verdict.

Tutorial: Add Attestations to Your Workflow

This step-by-step tutorial walks you through adding attestation support to an existing JACS workflow. You'll go from basic signing to full attestation creation and verification in under 5 minutes.

Prerequisites

  • JACS installed (Python, Node.js, or CLI)
  • Attestation feature enabled (built with --features attestation)

Step 1: Create an Agent

Use an ephemeral agent for testing (no files on disk):

{{#tabs }} {{#tab name="Python" }}

from jacs.client import JacsClient

client = JacsClient.ephemeral(algorithm="ed25519")
print(f"Agent ID: {client.agent_id}")

{{#endtab }} {{#tab name="Node.js" }}

const { JacsClient } = require('@hai.ai/jacs/client');

const client = await JacsClient.ephemeral('ring-Ed25519');
console.log(`Agent ID: ${client.agentId}`);

{{#endtab }} {{#tab name="CLI" }}

export JACS_PRIVATE_KEY_PASSWORD="YourP@ssw0rd"
jacs quickstart --algorithm ed25519

{{#endtab }} {{#endtabs }}

Step 2: Sign a Document

Sign some data to establish the base document:

{{#tabs }} {{#tab name="Python" }}

signed = client.sign_message({"action": "approve", "amount": 100})
print(f"Document ID: {signed.document_id}")

{{#endtab }} {{#tab name="Node.js" }}

const signed = await client.signMessage({ action: 'approve', amount: 100 });
console.log(`Document ID: ${signed.documentId}`);

{{#endtab }} {{#endtabs }}

Step 3: Create an Attestation

Now add trust context -- why this document should be trusted:

{{#tabs }} {{#tab name="Python" }}

import hashlib
content_hash = hashlib.sha256(signed.raw_json.encode()).hexdigest()
attestation = client.create_attestation(
    subject={
        "type": "artifact",
        "id": signed.document_id,
        "digests": {"sha256": content_hash},
    },
    claims=[
        {
            "name": "reviewed_by",
            "value": "human",
            "confidence": 0.95,
            "assuranceLevel": "verified",
        }
    ],
)
print(f"Attestation ID: {attestation.document_id}")

{{#endtab }} {{#tab name="Node.js" }}

const { createHash } = require('crypto');
const contentHash = createHash('sha256').update(signed.raw).digest('hex');
const attestation = await client.createAttestation({
  subject: {
    type: 'artifact',
    id: signed.documentId,
    digests: { sha256: contentHash },
  },
  claims: [{
    name: 'reviewed_by',
    value: 'human',
    confidence: 0.95,
    assuranceLevel: 'verified',
  }],
});
console.log(`Attestation ID: ${attestation.documentId}`);

{{#endtab }} {{#endtabs }}

Step 4: Verify the Attestation

Local Verification (fast -- signature + hash only)

{{#tabs }} {{#tab name="Python" }}

result = client.verify_attestation(attestation.raw_json)
print(f"Valid: {result['valid']}")
print(f"Signature OK: {result['crypto']['signature_valid']}")
print(f"Hash OK: {result['crypto']['hash_valid']}")

{{#endtab }} {{#tab name="Node.js" }}

const result = await client.verifyAttestation(attestation.raw);
console.log(`Valid: ${result.valid}`);
console.log(`Signature OK: ${result.crypto.signature_valid}`);
console.log(`Hash OK: ${result.crypto.hash_valid}`);

{{#endtab }} {{#endtabs }}

Full Verification (thorough -- includes evidence + derivation chain)

{{#tabs }} {{#tab name="Python" }}

full = client.verify_attestation(attestation.raw_json, full=True)
print(f"Valid: {full['valid']}")
print(f"Evidence: {full.get('evidence', [])}")
print(f"Chain: {full.get('chain')}")

{{#endtab }} {{#tab name="Node.js" }}

const full = await client.verifyAttestation(attestation.raw, { full: true });
console.log(`Valid: ${full.valid}`);
console.log(`Evidence: ${JSON.stringify(full.evidence)}`);
console.log(`Chain: ${JSON.stringify(full.chain)}`);

{{#endtab }} {{#endtabs }}

Step 5: Add Evidence (Optional)

Evidence references link to external proofs that support your claims:

{{#tabs }} {{#tab name="Python" }}

attestation_with_evidence = client.create_attestation(
    subject={
        "type": "artifact",
        "id": "doc-001",
        "digests": {"sha256": "abc123..."},
    },
    claims=[{"name": "scanned", "value": True, "confidence": 1.0}],
    evidence=[
        {
            "kind": "custom",
            "digests": {"sha256": "evidence-hash..."},
            "uri": "https://scanner.example.com/results/123",
            "collectedAt": "2026-03-04T00:00:00Z",
            "verifier": {"name": "security-scanner", "version": "2.0"},
        }
    ],
)

{{#endtab }} {{#tab name="Node.js" }}

const attWithEvidence = await client.createAttestation({
  subject: {
    type: 'artifact',
    id: 'doc-001',
    digests: { sha256: 'abc123...' },
  },
  claims: [{ name: 'scanned', value: true, confidence: 1.0 }],
  evidence: [{
    kind: 'custom',
    digests: { sha256: 'evidence-hash...' },
    uri: 'https://scanner.example.com/results/123',
    collectedAt: '2026-03-04T00:00:00Z',
    verifier: { name: 'security-scanner', version: '2.0' },
  }],
});

{{#endtab }} {{#endtabs }}

Step 6: Export as DSSE (Optional)

Export your attestation as a DSSE (Dead Simple Signing Envelope) for compatibility with in-toto, SLSA, and Sigstore:

{{#tabs }} {{#tab name="Python" }}

envelope = client.export_attestation_dsse(attestation.raw_json)
print(f"Payload type: {envelope['payloadType']}")
print(f"Signatures: {len(envelope['signatures'])}")

{{#endtab }} {{#tab name="Node.js" }}

const envelope = await client.exportAttestationDsse(attestation.raw);
console.log(`Payload type: ${envelope.payloadType}`);
console.log(`Signatures: ${envelope.signatures.length}`);

{{#endtab }} {{#endtabs }}

What's Next?

Writing a Custom Evidence Adapter

Evidence adapters normalize external proof sources into JACS attestation claims and evidence references. JACS ships with A2A and Email adapters; you can add your own for JWT tokens, TLSNotary proofs, or any custom evidence source.

The built-in A2A and Email adapters are deliberately digest-only. They record and later compare the exact evidence digest, but do not verify A2A protocol signatures, DKIM, S/MIME, PGP, JACS signatures, sender identity, or signer trust. Their generated claims therefore use self-asserted assurance and names ending in evidence-digest-recorded. First verify protocol provenance with the corresponding protocol API, then create an explicit claim from that typed verification result if your application needs to attest signature validity.

What Is an EvidenceAdapter?

An EvidenceAdapter is a Rust trait with three methods:

#![allow(unused)]
fn main() {
pub trait EvidenceAdapter: Send + Sync + std::fmt::Debug {
    /// Returns the kind string (e.g., "jwt", "tlsnotary", "custom").
    fn kind(&self) -> &str;

    /// Normalize raw evidence bytes + metadata into claims + evidence reference.
    fn normalize(
        &self,
        raw: &[u8],
        metadata: &serde_json::Value,
    ) -> Result<(Vec<Claim>, EvidenceRef), Box<dyn Error>>;

    /// Verify a previously created evidence reference.
    fn verify_evidence(
        &self,
        evidence: &EvidenceRef,
    ) -> Result<EvidenceVerificationResult, Box<dyn Error>>;
}
}

The adapter lifecycle:

  1. At attestation creation time: normalize() is called with raw evidence bytes and optional metadata. It returns structured claims and an EvidenceRef that will be embedded in the attestation document.
  2. At verification time (full tier): verify_evidence() is called with the stored EvidenceRef to re-validate the evidence.

The normalize() Contract

normalize() must:

  • Compute content-addressable digests of the raw evidence using compute_digest_set_bytes()
  • Decide whether to embed the evidence (recommended for data under 64KB)
  • Extract meaningful claims from the evidence
  • Set appropriate confidence and assuranceLevel values
  • Include a collectedAt timestamp
  • Return a VerifierInfo identifying your adapter and version

normalize() must NOT:

  • Make network calls (normalization should be deterministic and fast)
  • Modify the raw evidence
  • Set confidence to 1.0 unless the evidence is self-verifying (e.g., a valid cryptographic proof)
  • Treat caller-supplied metadata or successful digest computation as proof that a protocol signature, sender identity, or trust policy was verified

The verify_evidence() Contract

verify_evidence() must:

  • Verify the digest integrity (re-hash and compare)
  • Check freshness (is the collectedAt timestamp within acceptable bounds?)
  • Return a detailed EvidenceVerificationResult with digest_valid, freshness_valid, and human-readable detail

verify_evidence() may:

  • Make network calls (for remote evidence resolution)
  • Access the file system (for local evidence files)
  • Return partial results (e.g., digest valid but freshness expired)

Step-by-Step: Building a JWT Adapter

Here is a complete example of a JWT evidence adapter:

#![allow(unused)]
fn main() {
use crate::attestation::adapters::EvidenceAdapter;
use crate::attestation::digest::compute_digest_set_bytes;
use crate::attestation::types::*;
use serde_json::Value;
use std::error::Error;

#[derive(Debug)]
pub struct JwtAdapter;

impl EvidenceAdapter for JwtAdapter {
    fn kind(&self) -> &str {
        "jwt"
    }

    fn normalize(
        &self,
        raw: &[u8],
        metadata: &Value,
    ) -> Result<(Vec<Claim>, EvidenceRef), Box<dyn Error>> {
        // 1. Parse the JWT (header.payload.signature)
        let jwt_str = std::str::from_utf8(raw)?;
        let parts: Vec<&str> = jwt_str.split('.').collect();
        if parts.len() != 3 {
            return Err("Invalid JWT: expected 3 dot-separated parts".into());
        }

        // 2. Decode the payload (base64url). These fields are UNVERIFIED until
        // a separate JWT verifier validates the signature, algorithm, issuer,
        // audience, and time claims against an explicit trust policy.
        let payload_bytes = base64::Engine::decode(
            &base64::engine::general_purpose::URL_SAFE_NO_PAD,
            parts[1],
        )?;
        let payload: Value = serde_json::from_slice(&payload_bytes)?;

        // 3. Compute content-addressable digests
        let digests = compute_digest_set_bytes(raw);

        // 4. Extract claims (only non-PII fields per TRD Decision 14)
        let mut claims = vec![];
        if let Some(iss) = payload.get("iss") {
            claims.push(Claim {
                name: "jwt-unverified-issuer".into(),
                value: iss.clone(),
                confidence: None,
                assurance_level: Some(AssuranceLevel::SelfAsserted),
                issuer: None,
                issued_at: Some(crate::time_utils::now_rfc3339()),
            });
        }
        if let Some(sub) = payload.get("sub") {
            claims.push(Claim {
                name: "jwt-unverified-subject".into(),
                value: sub.clone(),
                confidence: None,
                assurance_level: Some(AssuranceLevel::SelfAsserted),
                issuer: None,
                issued_at: None,
            });
        }

        // 5. Build the evidence reference
        let evidence = EvidenceRef {
            kind: EvidenceKind::Jwt,
            digests,
            uri: metadata.get("uri").and_then(|v| v.as_str()).map(String::from),
            embedded: raw.len() < 65536,
            embedded_data: if raw.len() < 65536 {
                Some(Value::String(jwt_str.to_string()))
            } else {
                None
            },
            collected_at: crate::time_utils::now_rfc3339(),
            resolved_at: None,
            sensitivity: EvidenceSensitivity::Restricted, // JWTs may contain PII
            verifier: VerifierInfo {
                name: "jacs-jwt-adapter".into(),
                version: env!("CARGO_PKG_VERSION").into(),
            },
        };

        Ok((claims, evidence))
    }

    fn verify_evidence(
        &self,
        evidence: &EvidenceRef,
    ) -> Result<EvidenceVerificationResult, Box<dyn Error>> {
        // Re-verify the digest
        let digest_valid = if let Some(ref data) = evidence.embedded_data {
            let raw = data.as_str().unwrap_or("").as_bytes();
            let recomputed = compute_digest_set_bytes(raw);
            recomputed.sha256 == evidence.digests.sha256
        } else {
            // Cannot verify without embedded data or fetchable URI
            false
        };

        // Check freshness (example: 5 minute max age)
        let freshness_valid = true; // Implement actual time check

        Ok(EvidenceVerificationResult {
            kind: "jwt".into(),
            digest_valid,
            freshness_valid,
            detail: if digest_valid {
                "JWT bytes match the recorded digest; JWT signature and claims were not verified".into()
            } else {
                "JWT digest mismatch or data unavailable; JWT signature and claims were not verified".into()
            },
        })
    }
}
}

Testing Your Adapter

Write tests that cover:

  1. Normal case: Valid evidence normalizes to expected claims
  2. Invalid input: Malformed evidence returns a clear error
  3. Digest verification: Round-trip through normalize + verify_evidence
  4. Empty evidence: Edge case handling
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn jwt_normalize_extracts_issuer() {
        let adapter = JwtAdapter;
        // Build a minimal JWT (header.payload.signature)
        let header = base64::Engine::encode(
            &base64::engine::general_purpose::URL_SAFE_NO_PAD,
            b"{\"alg\":\"EdDSA\"}",
        );
        let payload = base64::Engine::encode(
            &base64::engine::general_purpose::URL_SAFE_NO_PAD,
            b"{\"iss\":\"auth.example.com\",\"sub\":\"user-123\"}",
        );
        let jwt = format!("{}.{}.fake-sig", header, payload);

        let (claims, evidence) = adapter
            .normalize(jwt.as_bytes(), &json!({}))
            .expect("normalize should succeed");

        assert!(claims.iter().any(|c| c.name == "jwt-unverified-issuer"));
        assert!(claims.iter().all(|c| c.assurance_level != Some(AssuranceLevel::Verified)));
        assert_eq!(evidence.kind, EvidenceKind::Jwt);
    }
}
}

Registering Your Adapter with the Agent

Adapters are registered on the Agent struct via the evidence adapter list. To add your adapter to the default set, modify adapters/mod.rs:

#![allow(unused)]
fn main() {
pub fn default_adapters() -> Vec<Box<dyn EvidenceAdapter>> {
    vec![
        Box::new(a2a::A2aAdapter),
        Box::new(email::EmailAdapter),
        Box::new(jwt::JwtAdapter),  // Add your adapter here
    ]
}
}

For runtime registration (without modifying JACS source), use the agent's adapter API (when available in a future release).

Privacy Considerations

The EvidenceSensitivity enum controls how evidence is handled:

  • Public: Evidence can be freely shared and embedded
  • Restricted: Evidence should be handled with care; consider redacting PII
  • Confidential: Evidence should not be embedded; use content-addressable URI references only

For JWTs and other credential-based evidence, default to Restricted and only include non-PII fields (iss, sub, aud, iat, exp) in claims.

Framework Adapter Attestation Guide

JACS provides Python framework adapters for LangChain, FastAPI, and Anthropic. Each adapter can be configured to produce attestations (not just signatures) for tool calls, API requests, and agent actions.

Common Patterns

All framework adapters share these attestation patterns:

Default Claims

When attest=True is enabled on any adapter, it automatically includes these default claims:

[
    {"name": "framework", "value": "langchain", "confidence": 1.0},
    {"name": "tool_name", "value": "my_tool", "confidence": 1.0},
    {"name": "timestamp", "value": "2026-03-04T...", "confidence": 1.0},
]

Custom Claims

Add your own claims to any adapter call:

extra_claims = [
    {"name": "reviewed_by", "value": "human", "confidence": 0.95},
    {"name": "approved", "value": True, "assuranceLevel": "verified"},
]

Evidence Attachment

Attach evidence references from external systems:

evidence = [
    {
        "kind": "custom",
        "digests": {"sha256": "abc123..."},
        "uri": "https://scanner.example.com/report/456",
        "collectedAt": "2026-03-04T00:00:00Z",
        "verifier": {"name": "security-scanner", "version": "2.0"},
    }
]

LangChain

Enabling Attestation on Tool Calls

Use jacs_wrap_tool_call with attest=True:

from jacs.adapters.langchain import jacs_wrap_tool_call
from jacs.client import JacsClient

client = JacsClient.quickstart()

# Wrap a tool call with attestation
@jacs_wrap_tool_call(client, attest=True)
def my_tool(query: str) -> str:
    return f"Result for: {query}"

# The tool call now produces a signed attestation
result = my_tool("test query")
# result.attestation contains the signed attestation document

Using the signed_tool Decorator

from jacs.adapters.langchain import signed_tool

@signed_tool(client, attest=True, claims=[
    {"name": "data_source", "value": "internal_db", "confidence": 1.0}
])
def lookup_customer(customer_id: str) -> dict:
    return {"name": "Alice", "status": "active"}

With LangChain Chains

from jacs.adapters.langchain import with_jacs_signing

# Wrap an entire chain with attestation
signed_chain = with_jacs_signing(
    chain=my_chain,
    client=client,
    attest=True,
)

FastAPI

Attestation Middleware

The JacsMiddleware can be configured to produce attestations for all responses:

from fastapi import FastAPI
from jacs.adapters.fastapi import JacsMiddleware
from jacs.client import JacsClient

app = FastAPI()
client = JacsClient.quickstart()

app.add_middleware(
    JacsMiddleware,
    client=client,
    attest=True,  # Produce attestations, not just signatures
    default_claims=[
        {"name": "service", "value": "my-api", "confidence": 1.0},
    ],
)

Per-Route Attestation

Use jacs_route for route-level attestation control:

from jacs.adapters.fastapi import jacs_route

@app.post("/approve")
@jacs_route(client, attest=True, claims=[
    {"name": "action", "value": "approve", "confidence": 1.0},
    {"name": "requires_review", "value": True},
])
async def approve_request(request_id: str):
    return {"approved": True, "request_id": request_id}

The response headers will include X-JACS-Attestation-Id with the attestation document ID.

Anthropic

Tool Hook Attestation

The Anthropic adapter hooks into Claude tool calls to produce attestations:

from jacs.adapters.anthropic import signed_tool, JacsToolHook
from jacs.client import JacsClient

client = JacsClient.quickstart()

@signed_tool(client, attest=True)
def search_database(query: str) -> str:
    return "Found 3 results"

# Or use the hook class for more control
hook = JacsToolHook(
    client=client,
    attest=True,
    default_claims=[
        {"name": "model", "value": "claude-4.6"},
        {"name": "tool_use_id", "value": "auto"},  # Auto-filled from tool call
    ],
)

With the Anthropic SDK

import anthropic
from jacs.adapters.anthropic import JacsToolHook

client = anthropic.Anthropic()
jacs_client = JacsClient.quickstart()
hook = JacsToolHook(jacs_client, attest=True)

# Register tools with JACS attestation
tools = hook.wrap_tools([
    {
        "name": "get_weather",
        "description": "Get weather for a location",
        "input_schema": {"type": "object", "properties": {"location": {"type": "string"}}},
    }
])

Verifying Framework Attestations

All framework attestations use the same JACS verification API:

# Verify any attestation (from any framework adapter)
result = client.verify_attestation(attestation_json, full=True)
print(f"Valid: {result['valid']}")
print(f"Framework: {result['claims'][0]['value']}")
print(f"Evidence: {result.get('evidence', [])}")

Strict vs. Permissive Mode

All adapters respect the strict flag on JacsClient:

  • Permissive (default): Signing/attestation failures log warnings but do not block the operation
  • Strict: Signing/attestation failures raise exceptions and block the operation
# Strict mode: attestation failure = operation failure
client = JacsClient.quickstart(strict=True)

# Permissive mode: attestation failure = warning + continue
client = JacsClient.quickstart(strict=False)

A2A + Attestation: Using Both Together

A2A provenance and attestation serve different purposes. This guide explains when and how to combine them.

When You Need Both

Use A2A alone when you need to prove who sent what across agent boundaries. Use attestation alone when you need to record why data should be trusted within a single agent's workflow.

Use both when:

  • You send data to another agent AND need to explain why it's trustworthy
  • You receive data from another agent AND want to attest that you reviewed it
  • You're building a multi-agent pipeline where each step adds trust evidence

The Composition Rule

A2A chain-of-custody provides movement lineage. Attestation derivation provides claim lineage.

A2A tracks where an artifact has been (Agent A → Agent B → Agent C). Attestation tracks what trust claims have been made about it (scanned → reviewed → approved).

They compose naturally: an agent receives a signed artifact via A2A, then creates an attestation recording its analysis of that artifact.

Example Workflow

Agent A: Signs artifact with A2A provenance
    ↓ (cross-boundary exchange)
Agent B: Verifies A2A signature, attests review with evidence
    ↓ (cross-boundary exchange)
Agent C: Verifies both the A2A chain and the attestation

Python

from jacs.client import JacsClient

# --- Agent A: Sign and send ---
agent_a = JacsClient.quickstart(name="scanner", domain="scanner.example.com")
a2a_a = agent_a.get_a2a()
signed = a2a_a.sign_artifact(
    {"scan_result": "clean", "target": "file.bin"},
    "artifact",
)

# --- Agent B: Receive, verify, attest ---
agent_b = JacsClient.quickstart(name="reviewer", domain="reviewer.example.com")
a2a_b = agent_b.get_a2a()

# Verify the A2A artifact from Agent A
verify_result = a2a_b.verify_wrapped_artifact(signed)
assert verify_result["valid"]

# Now attest WHY the review is trustworthy
import hashlib, json
content_hash = hashlib.sha256(json.dumps(signed, sort_keys=True).encode()).hexdigest()
attestation = agent_b.create_attestation(
    subject={"type": "artifact", "id": signed["jacsId"], "digests": {"sha256": content_hash}},
    claims=[{"name": "reviewed", "value": True, "confidence": 0.9}],
)

# Send the attestation onward via A2A
attested_artifact = a2a_b.sign_artifact(
    {"attestation_id": attestation.document_id, "original_artifact": signed["jacsId"]},
    "artifact",
    parent_signatures=[signed],
)

Node.js

import { JacsClient } from '@hai.ai/jacs/client';

// --- Agent A: Sign and send ---
const agentA = await JacsClient.quickstart({ name: 'scanner', domain: 'scanner.example.com' });
const a2aA = agentA.getA2A();
const signed = await a2aA.signArtifact(
  { scanResult: 'clean', target: 'file.bin' },
  'artifact',
);

// --- Agent B: Receive, verify, attest ---
const agentB = await JacsClient.quickstart({ name: 'reviewer', domain: 'reviewer.example.com' });
const a2aB = agentB.getA2A();

const verifyResult = await a2aB.verifyWrappedArtifact(signed);
console.assert(verifyResult.valid);

// Attest the review
const attestation = agentB.createAttestation({
  subject: { type: 'artifact', id: signed.jacsId, digests: { sha256: '...' } },
  claims: [{ name: 'reviewed', value: true, confidence: 0.9 }],
});

What NOT to Do

  • Don't use A2A trust policy to validate attestation evidence. A2A policy (open/verified/strict) controls agent admission, not evidence quality. An allowed agent can still produce bad evidence.
  • Don't use attestation to determine transport trust. Attestation claims don't tell you whether an agent should be allowed to communicate. Use assess_remote_agent() for that.
  • Don't conflate chain-of-custody with derivation chain. A2A parent signatures track artifact movement. Attestation derivation tracks how one claim was produced from another. They are complementary, not interchangeable.

Further Reading

Observability & Monitoring Guide

JACS emits structured events at every signing, verification, and agreement lifecycle step. This guide shows you how to capture those events and route them to your monitoring stack.

For Rust-specific API details (ObservabilityConfig, LogDestination, MetricsConfig, etc.), see the Observability (Rust API).

Structured Event Reference

Every event includes an event field for filtering. The table below is derived directly from the source code.

Signing Events

EventLevelFieldsSource
document_signedinfoalgorithm, duration_mscrypt/mod.rs
batch_signedinfoalgorithm, batch_size, duration_mscrypt/mod.rs
signing_procedure_completeinfoagent_id, algorithm, timestamp, placement_keyagent/mod.rs

Verification Events

EventLevelFieldsSource
signature_verifiedinfoalgorithm, valid, duration_mscrypt/mod.rs
verification_completeinfo / errordocument_id, signer_id, algorithm, timestamp, valid, duration_msagent/mod.rs

verification_complete emits at info when valid=true and at error when valid=false.

Agreement Events

EventLevelFieldsSource
agreement_createdinfodocument_id, agent_count, quorum, has_timeoutagent/agreement.rs
signature_addedinfodocument_id, signer_id, current, total, requiredagent/agreement.rs
quorum_reachedinfodocument_id, signatures, required, totalagent/agreement.rs
agreement_expiredwarndocument_id, deadlineagent/agreement.rs

Enabling OTEL Export

JACS ships with three optional feature flags for OpenTelemetry backends. By default, only stderr and file logging are available.

# Enable all three OTEL pipelines
cargo build --features otlp-logs,otlp-metrics,otlp-tracing

# Or enable just tracing
cargo build --features otlp-tracing
FeatureWhat it adds
otlp-logsOTLP log export (opentelemetry, opentelemetry-otlp, opentelemetry-appender-tracing, tokio)
otlp-metricsOTLP metrics export (opentelemetry, opentelemetry-otlp, opentelemetry_sdk, tokio)
otlp-tracingDistributed tracing (opentelemetry, opentelemetry-otlp, tracing-opentelemetry, tokio)

Convenience helpers for automatic counter/gauge recording for sign and verify operations are always available without any feature flag.

OTEL Collector Configuration

Route JACS events through an OpenTelemetry Collector. This configuration receives OTLP over HTTP, batches events, and exports to common backends.

# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 5s
    send_batch_size: 512
  filter/jacs:
    logs:
      include:
        match_type: regexp
        record_attributes:
          - key: event
            value: "document_signed|signature_verified|verification_complete|agreement_.*|batch_signed|signing_procedure_complete|quorum_reached|signature_added"

exporters:
  # Debug: print to collector stdout
  debug:
    verbosity: detailed

  # Datadog
  datadog:
    api:
      key: "${DD_API_KEY}"
      site: datadoghq.com

  # Splunk HEC
  splunkhec:
    token: "${SPLUNK_HEC_TOKEN}"
    endpoint: "https://splunk-hec:8088/services/collector"
    source: "jacs"
    sourcetype: "jacs:events"

  # Generic OTLP (Grafana Cloud, Honeycomb, etc.)
  otlphttp:
    endpoint: "${OTLP_ENDPOINT}"
    headers:
      Authorization: "Bearer ${OTLP_API_KEY}"

service:
  pipelines:
    logs:
      receivers: [otlp]
      processors: [batch, filter/jacs]
      exporters: [debug]          # Replace with your exporter
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug]
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug]

Pointing JACS at the Collector

In jacs.config.json:

{
  "observability": {
    "logs": {
      "enabled": true,
      "level": "info",
      "destination": {
        "otlp": { "endpoint": "http://localhost:4318" }
      }
    },
    "metrics": {
      "enabled": true,
      "destination": {
        "otlp": { "endpoint": "http://localhost:4318" }
      },
      "export_interval_seconds": 30
    },
    "tracing": {
      "enabled": true,
      "sampling": { "ratio": 1.0, "parent_based": true },
      "resource": {
        "service_name": "my-jacs-service",
        "environment": "production"
      },
      "destination": {
        "otlp": { "endpoint": "http://localhost:4318" }
      }
    }
  }
}

Or via environment variables (useful in containers):

export OTEL_EXPORTER_OTLP_ENDPOINT="http://collector:4318"
export OTEL_SERVICE_NAME="jacs-production"
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production"

Feeding Events to Datadog

  1. Deploy the OTEL Collector with the datadog exporter (see config above).
  2. Set DD_API_KEY in the collector's environment.
  3. In Datadog, JACS events appear under Logs > Search with source:opentelemetry.
  4. Create a monitor on event:verification_complete AND valid:false to alert on verification failures.

Alternatively, use the Datadog Agent's built-in OTLP receiver:

# datadog.yaml
otlp_config:
  receiver:
    protocols:
      http:
        endpoint: 0.0.0.0:4318

Feeding Events to Splunk

  1. Deploy the OTEL Collector with the splunkhec exporter.
  2. Set SPLUNK_HEC_TOKEN in the collector's environment.
  3. Events arrive in Splunk with sourcetype=jacs:events.
  4. Search: sourcetype="jacs:events" event="verification_complete" valid=false

Agreement Monitoring

Agreement events give you a complete lifecycle view: creation, each signature, quorum, and expiry. Here are practical queries.

Agreements Approaching Timeout

Filter for agreement_created events where has_timeout=true, then correlate with quorum_reached. Any agreement_created without a matching quorum_reached within the timeout window is at risk.

Failed Quorum Detection

event="signature_added" | stats max(current) as sigs, max(required) as needed by document_id
| where sigs < needed

Signature Velocity

Track signature_added events over time to see how quickly agents sign after agreement creation:

event="signature_added" | timechart count by document_id

Expiry Alerts

The agreement_expired event (level warn) fires when an agent attempts to sign or verify an expired agreement. Alert on this directly:

event="agreement_expired" | alert

Latency Tracking

Both document_signed and signature_verified include duration_ms. Use these to track signing and verification performance:

event="document_signed" | stats avg(duration_ms) as avg_sign_ms, p99(duration_ms) as p99_sign_ms by algorithm
event="signature_verified" | stats avg(duration_ms) as avg_verify_ms, p99(duration_ms) as p99_verify_ms by algorithm

Post-quantum signing with pq2025 will show higher latency than ring-Ed25519. Use these metrics to decide whether the security/performance tradeoff is acceptable for your workload.

Next Steps

Email Signing and Verification

JACS signs selected canonicalized fields from an RFC 5322 .eml input and returns email bytes with a jacs-signature.json MIME attachment. That attachment is an ordinary signed JACS document containing the email hash payload. The recipient verifies it with an explicitly supplied public key, then compares the received email's covered fields and attachments against the signed hashes. This does not assert byte-identical preservation of the entire .eml, mailbox ownership, truth, human approval or permission to contact a recipient.

JACS also exposes migration helpers for HAI's HTML-inline signed email transport. In that mode, the signature material travels in the HTML body and inline logo instead of as user-visible signature attachments. Core signing and verification still stay in JACS; SDKs and servers should call these helpers rather than reimplementing email hashing, MIME parsing, or media extraction.

The primary attachment-mode APIs are:

ActionFunctionWhat you supplyWhat you get back
Signjacs::email::sign_email()raw .eml bytes + a JacsSigner.eml bytes with jacs-signature.json
Verifyjacs::email::verify_email()signed .eml bytes + sender's public key + verifierContentVerificationResult (pass/fail per field)

Runnable local example

From a source checkout, run:

JACS_KEYCHAIN_BACKEND=disabled JACS_ALLOW_NETWORK=false cargo run --locked -p jacs --no-default-features --example email_signing

The example creates two disposable SimpleAgent::ephemeral instances and a synthetic multipart email with one attachment. It passes the sender's public key directly to the recipient, signs and verifies the email, prints the actual JACS attachment, checks body tampering and a wrong key, and inspects a forwarding chain. It sends no mail, reads no user keys and writes no files. jacs::email is available without an optional email feature. This is a local JACS-to-JACS example, not an independent implementation or mailbox-identity test.

//! Inspect real attachment-mode email signing with disposable in-memory keys.
//! No config files, mail delivery, remote key lookup or user keys are used.

use jacs::email::{FieldStatus, get_jacs_attachment, sign_email, verify_email};
use jacs::simple::SimpleAgent;
use serde_json::Value;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let (sender, _) = SimpleAgent::ephemeral(Some("ed25519"))?;
    let (recipient, _) = SimpleAgent::ephemeral(Some("ed25519"))?;
    // In this local example the key is passed directly. A real recipient must
    // establish the key's attribution separately from signature verification.
    let public_key = sender.get_public_key()?;
    let raw_email = concat!(
        "From: sender@example.test\r\n",
        "To: recipient@example.test\r\n",
        "Subject: Synthetic email provenance\r\n",
        "Date: Mon, 14 Sep 2026 12:00:00 +0000\r\n",
        "Message-ID: <synthetic-provenance@example.test>\r\n",
        "MIME-Version: 1.0\r\n",
        "Content-Type: multipart/mixed; boundary=synthetic-boundary\r\n\r\n",
        "--synthetic-boundary\r\n",
        "Content-Type: text/plain; charset=utf-8\r\n\r\n",
        "Synthetic body for envelope inspection.\r\n",
        "--synthetic-boundary\r\n",
        "Content-Type: application/octet-stream; name=receipt.txt\r\n",
        "Content-Disposition: attachment; filename=receipt.txt\r\n\r\n",
        "Synthetic receipt\r\n",
        "--synthetic-boundary--\r\n",
    );

    let signed_email = sign_email(raw_email.as_bytes(), &sender)?;
    let result = verify_email(&signed_email, &recipient, &public_key)?;
    assert!(result.valid, "synthetic email must verify: {result:?}");
    let attachment = get_jacs_attachment(&signed_email)?;
    let document: Value = serde_json::from_slice(&attachment)?;
    assert!(document["content"]["headers"].is_object());
    assert_eq!(
        document["content"]["attachments"][0]["filename"],
        "receipt.txt"
    );
    assert!(document["jacsSignature"]["signature"].is_string());
    assert!(document.get("payload").is_none());
    println!("Actual signed attachment (synthetic data only):");
    println!("{}", std::str::from_utf8(&attachment)?);
    println!(
        "Field verification: {}",
        serde_json::to_string_pretty(&result)?
    );

    let tampered = std::str::from_utf8(&signed_email)?.replacen(
        "Synthetic body for envelope inspection.",
        "Changed body for envelope inspection.",
        1,
    );
    assert_ne!(tampered.as_bytes(), signed_email.as_slice());
    let tampered_result = verify_email(tampered.as_bytes(), &recipient, &public_key)?;
    assert!(!tampered_result.valid);
    assert!(
        tampered_result
            .field_results
            .iter()
            .any(|field| { field.field == "body_plain" && field.status == FieldStatus::Fail })
    );
    assert!(verify_email(&signed_email, &recipient, &recipient.get_public_key()?).is_err());

    // Re-signing links the previous attachment bytes. The one-key email API
    // does not verify ancestors, even when this example reuses the same sender.
    let forwarded = sign_email(&signed_email, &sender)?;
    let forwarding_result = verify_email(&forwarded, &recipient, &public_key)?;
    assert!(!forwarding_result.valid);
    assert_eq!(forwarding_result.chain.len(), 2);
    assert!(forwarding_result.chain[0].valid);
    assert!(!forwarding_result.chain[1].valid);
    assert!(
        forwarding_result
            .field_results
            .iter()
            .all(|field| field.status != FieldStatus::Fail)
    );
    println!(
        "Forwarding inspection: {}",
        serde_json::to_string_pretty(&forwarding_result)?
    );
    println!("Signature checks do not establish mailbox identity, truth or human approval.");
    Ok(())
}

Signing an email

#![allow(unused)]
fn main() {
use jacs::email::sign_email;

// 1. Load raw email bytes (RFC 5322 format)
let raw_eml = std::fs::read("outgoing.eml")?;

// 2. Sign — SimpleAgent implements JacsSigner
let signed_eml = sign_email(&raw_eml, &my_agent)?;

// 3. Send signed_eml — it is a valid .eml with the JACS attachment
std::fs::write("outgoing_signed.eml", &signed_eml)?;
}

The JacsSigner trait

sign_email accepts any type that implements JacsSigner. SimpleAgent implements it out of the box.

#![allow(unused)]
fn main() {
pub trait JacsSigner {
    /// Create a signed JACS document from the email hash payload.
    fn sign_message(&self, data: &serde_json::Value) -> Result<SignedDocument, JacsError>;

    /// Verify a signed JACS document using the sender's public key.
    fn verify_with_key(
        &self,
        signed_document: &str,
        public_key: Vec<u8>,
    ) -> Result<VerificationResult, JacsError>;
}
}

The signing algorithm is read from your JACS agent at runtime and recorded in the jacs-signature.json document. Do not hardcode it in email code.

What sign_email does internally

  1. Parses and canonicalizes the email headers and body
  2. Computes SHA-256 hashes for each header, body part, and attachment
  3. Builds the JACS email signature payload
  4. Places the hash payload in content via sign_message()
  5. Uses the normal JACS document canonicalization, hashing and signing pipeline
  6. Attaches the result as jacs-signature.json

You do not need to know any of this to use it — it is a single function call.

Forwarding (re-signing)

If the email already has a jacs-signature.json (it was previously signed by another agent), sign_email automatically:

  1. Renames the existing signature to jacs-signature-0.json (or -1, -2, ...)
  2. Computes a parent_signature_hash linking to the previous signature
  3. Signs the email with a new jacs-signature.json

The new signature covers the hash link and the retained attachment bytes. It does not verify the earlier signers' cryptographic signatures. The current one-key verifier records ancestors with chain[].valid: false because their keys were not supplied; this also keeps the overall result false even if the current signature and all covered fields pass. Treat those ancestors as unchecked, not as proven tampering. Verify their signatures with independently selected keys before an application relies on the chain; do not turn these flags true based only on a matching hash or claimed signer label.

Verifying an email

#![allow(unused)]
fn main() {
use jacs::email::verify_email;
use jacs::simple::SimpleAgent;

let signed_eml = std::fs::read("incoming_signed.eml")?;
let sender_public_key: Vec<u8> = /* fetch from local trust, DNS, or another trusted source */;

// Any agent can verify — the sender's public key is passed explicitly
let (agent, _) = SimpleAgent::ephemeral(Some("ed25519"))?;
let result = verify_email(&signed_eml, &agent, &sender_public_key)?;

if result.valid {
    println!("Covered email fields match a JACS signature checked with the supplied key");
} else {
    // Inspect field failures and any unchecked forwarding ancestors
    for field in &result.field_results {
        println!("{}: {:?}", field.field, field.status);
    }
    println!("Chain inspection: {:?}", result.chain);
}
}

verify_email does everything in one call:

  1. Extracts jacs-signature.json from the email
  2. Removes it (the signature covers the email without itself)
  3. Verifies the JACS document signature against the sender's public key
  4. Compares covered canonicalized headers, body parts, attachments and MIME hashes
  5. Returns per-field and forwarding-chain results; Message-ID is stored but not verified

Two-step API (when you need the JACS document)

If you need a parsed email DTO and JACS metadata (issuer, timestamps) before doing the content comparison:

#![allow(unused)]
fn main() {
use jacs::email::{verify_email_document, verify_email_content};
use jacs::simple::SimpleAgent;

let (agent, _) = SimpleAgent::ephemeral(Some("ed25519"))?;

// Step 1: Verify the current JACS signature, then project its email DTO
let (doc, parts) = verify_email_document(&signed_eml, &agent, &sender_public_key)?;

// Inspect the document
println!("Signed by: {}", doc.metadata.issuer);
println!("Created at: {}", doc.metadata.created_at);

// Step 2: Compare content hashes
let result = verify_email_content(&doc, &parts);
assert!(result.valid);
}

All cryptographic operations are handled by the JACS agent via SimpleAgent::verify_with_key(). The agent's own key is not used -- the sender's public key is passed explicitly.

Field-level results

The ContentVerificationResult contains a field_results vector with one entry per field:

StatusMeaning
PassCovered hash matches the signed value; no independent mailbox attribution
ModifiedHash mismatch but case-insensitive email address match (address headers only)
FailContent does not match the signed hash
UnverifiableField absent or not verifiable (e.g. Message-ID may change in transit)

Headers covered include from, to, subject, date and, when present, cc, in_reply_to and references. message_id is stored in the signed payload but always reported Unverifiable, since transit can change it. Inspect every field status; overall valid does not mean every field was checked.

Body hashing uses decoded/canonicalized text, including charset, line-ending and trailing-whitespace normalization. Attachment hashes include the normalized filename, lowercased content type and extracted content bytes; body and attachment MIME structural headers have separate hashes. Equivalent normalized content can verify despite changes to its wire representation. Uncovered headers and transport metadata are outside this claim.

HTML-inline signed email migration helpers

The HTML-inline transport is being added for HAI email while attachment mode remains compatible. Use verify_signed_email when a caller may receive either transport:

#![allow(unused)]
fn main() {
use jacs::email::{verify_signed_email, VerificationMode};

let result = verify_signed_email(
    &raw_eml,
    &verifier_agent,
    &sender_public_key,
    VerificationMode::Strict,
)?;
}

The verifier detects SignedEmailTransport::AttachmentJacs or SignedEmailTransport::HtmlInline and returns SignedEmailVerificationResult with Verified, PartiallyVerified, or Failed.

HTML-inline helpers include:

  • build_html_inline_email_signature_payload for the inline signed pre-image. It signs the existing email header scope, the text body, and user attachments. Generated HTML and signature artifacts are excluded.
  • embed_jacs_header_in_logo_png and extract_jacs_header_from_logo_png for the signed inline PNG logo transport.
  • extract_topmost_inline_jacs_envelope for reply-safe hidden envelope selection.
  • remove_inline_signature_artifacts, strip_inline_signature_artifacts_from_html, and html_bodies_equivalent for parser-based artifact removal and HTML presentation checks.
  • verify_html_inline_email_content for content-hash verification after removing inline transport artifacts while keeping user attachments signed.

Attachment mode remains available through sign_email, verify_email, and the verify_email_* compatibility APIs.

The JACS signature document

The primary signed attachment uses the normal JACS envelope. The runnable example above prints its actual generated JSON; do not construct a signature by filling in a sample object.

Path in the signed attachmentMeaning
content.headersSelected canonicalized header values and hashes
content.body_plain, content.body_htmlPresent body parts' content and MIME-header hashes
content.attachmentsAttachment filenames, content hashes and MIME-header hashes
content.parent_signature_hashWhen forwarding, hash of the previous signature attachment bytes
jacsId, jacsVersion, jacsSha256Normal JACS document identifiers and document hash
jacsSignatureNormal signature metadata, including agentID, date, signingAlgorithm and signature

verify_email_document() first verifies this envelope, then returns a JacsEmailSignatureDocument DTO with payload, metadata and signature fields for compatibility. That DTO is not the serialized attachment format: payload comes from content; metadata.issuer comes from jacsSignature.agentID; metadata.hash comes from jacsSha256. Its legacy signature.key_id, signature.algorithm and signature.signature fields are empty in this projection. Inspect the original attachment with get_jacs_attachment() for the actual signature metadata or re-verification.

The historical version: "1.0" / payload / metadata / signature layout is not what current sign_email() emits. Parsing such a layout in an ancestor is compatibility inspection, not cryptographic verification. Neither the DTO nor the attachment establishes that a key belongs to the address in From; that attribution and any human/contact authority require separate evidence.

Public API summary

All items are re-exported from jacs::email:

#![allow(unused)]
fn main() {
// Signing
jacs::email::sign_email(raw_email: &[u8], signer: &impl JacsSigner) -> Result<Vec<u8>, EmailError>
jacs::email::build_html_inline_email_signature_payload(raw_email: &[u8])
jacs::email::JacsSigner                   // trait implemented by SimpleAgent

// Verification
jacs::email::verify_email(raw, &agent, pubkey)       // one-call: crypto + content check
jacs::email::verify_signed_email(raw, &agent, pubkey, mode)
jacs::email::verify_email_document(raw, &agent, pk)  // step 1: crypto only
jacs::email::verify_email_content(&doc, &parts)      // step 2: content hash comparison
jacs::email::verify_html_inline_email_content(&doc, &parts)
jacs::email::normalize_algorithm(...)                 // algorithm name normalization

// Types
jacs::email::ContentVerificationResult    // overall result with field_results
jacs::email::SignedEmailVerificationResult
jacs::email::SignedEmailTransport         // AttachmentJacs | HtmlInline
jacs::email::VerificationMode             // Strict | Degraded
jacs::email::FieldResult                  // per-field status
jacs::email::FieldStatus                  // Pass | Modified | Fail | Unverifiable
jacs::email::JacsEmailSignatureDocument   // parsed compatibility DTO, not raw attachment JSON
jacs::email::EmailError                   // error type

// Attachment helpers (for advanced use)
jacs::email::get_jacs_attachment(...)     // extract jacs-signature.json bytes
jacs::email::remove_jacs_attachment(...)  // strip jacs-signature.json from email
jacs::email::add_jacs_attachment(...)     // inject jacs-signature.json into email
}

Streaming Signing

JACS uses a buffer-then-sign pattern for streaming outputs. Token streams from LLMs are accumulated in memory and signed once the stream completes. This is the correct approach for LLM outputs because:

  1. LLM responses are small. A typical response is under 100KB of text. Buffering this costs nothing.
  2. Signatures cover the complete output. A partial signature over incomplete text is useless for verification.
  3. Framework adapters handle this automatically. If you use a JACS adapter, streaming signing just works.

How It Works by Framework

Vercel AI SDK (streamText)

The wrapStream middleware accumulates text-delta chunks via a TransformStream. When the stream flushes, it signs the complete text and emits a provider-metadata chunk containing the provenance record.

import { withProvenance } from '@hai.ai/jacs/vercel-ai';
import { streamText } from 'ai';

const model = withProvenance(openai('gpt-4o'), { client });
const result = await streamText({ model, prompt: 'Explain trust.' });

for await (const chunk of result.textStream) {
  process.stdout.write(chunk); // stream to user in real time
}
// provenance is available after stream completes

LangChain / LangGraph

LangChain tools execute synchronously (or await async results) before returning to the model. JACS signs each tool result individually via wrap_tool_call or signed_tool. No special streaming handling is needed because the signing happens at the tool output boundary, not the token stream.

from jacs.adapters.langchain import jacs_signing_middleware

agent = create_agent(
    model="openai:gpt-4o",
    tools=tools,
    middleware=[jacs_signing_middleware(client=jacs_client)],
)
# Tool results are auto-signed before the model sees them

Express / Koa / FastAPI

HTTP middleware signs the response body before it is sent. For streaming HTTP responses (SSE, chunked encoding), sign the complete message content before streaming, or sign each event individually.

# FastAPI: middleware signs JSON responses automatically
from jacs.adapters.fastapi import JacsMiddleware
app.add_middleware(JacsMiddleware)

Raw LLM APIs (No Framework Adapter)

If you're calling an LLM API directly without a framework adapter, accumulate the response yourself and sign it when complete:

import jacs.simple as jacs

jacs.quickstart(name="my-agent", domain="my-agent.example.com")

# Accumulate streamed response
chunks = []
async for chunk in llm_stream("What is trust?"):
    chunks.append(chunk)
    print(chunk, end="")  # stream to user

# Sign the complete response
complete_text = "".join(chunks)
signed = jacs.sign_message({"response": complete_text, "model": "gpt-4o"})
const jacs = require('@hai.ai/jacs/simple');
await jacs.quickstart({ name: 'my-agent', domain: 'my-agent.example.com' });

const chunks = [];
for await (const chunk of llmStream('What is trust?')) {
  chunks.push(chunk);
  process.stdout.write(chunk);
}

const signed = await jacs.signMessage({
  response: chunks.join(''),
  model: 'gpt-4o',
});

When NOT to Buffer

The buffer-then-sign pattern assumes the full content fits in memory. This is always true for LLM text responses. If you need to sign very large data (multi-GB files, video streams), use sign_file instead, which hashes the file on disk without loading it into memory.

See Also

Inline Text Signatures

Why this matters

You have a shared README that five agents review and counter-sign before a release; today that workflow requires a separate JACS JSON next to the markdown, which is easy to misplace and impossible to read on GitHub alongside the file. With jacs sign-text the signature sits at the end of the markdown in a YAML-bodied block; the file still renders as markdown on GitHub; every signer's identity and timestamp are attached to the exact canonical bytes. The signature proves who signed what bytes at the agent's claimed time — it does not prove first creation or legal ownership.

At a glance

ActionCLI verbPythonNodeGoRust
Signjacs sign-text <file>jacs.sign_text(path)jacs.signText(path)jacs.SignText(path, nil)jacs::text::sign_text_file(&agent, path)
Verifyjacs verify-text <file>jacs.verify_text(path)jacs.verifyText(path)jacs.VerifyText(path, nil)jacs::text::verify_text_file(&agent, path, opts)

All bindings are async-first on Node (returns Promise); CLI is synchronous; Python and Rust are synchronous; Go is synchronous.

The signature block

sign-text appends a YAML footer at the end of the file. The markers stay small and stable; the body is a full signed JACS document so markdown can carry the same jacsId / jacsVersion chain as JSON-envelope documents:

-----BEGIN JACS SIGNATURE-----
jacsId: 5f7e...
jacsVersion: 7c6d...
jacsVersionDate: "2026-05-01T12:00:00Z"
jacsType: inline-md
jacsLevel: artifact
content:
  inlineSignatureVersion: 1
  canonicalization: jacs-text-v1
  hashAlgorithm: sha256
  signedContentHash: no5d...
jacsSignature:
  agentID: agent-abc123
  signingAlgorithm: ring-Ed25519
  publicKeyHash: 3b2f...
  signature: ...
jacsSha256: 9a8b...
-----END JACS SIGNATURE-----

Multi-signer files contain multiple back-to-back blocks. The signers are unordered (matches the existing JACS agreement model) — you can reorder the blocks on disk and verification still passes. The file content (the bytes before the first block) is preserved byte-for-byte. When the same signer re-signs edited content, JACS replaces that signer's footer with a new version of the same inline document: jacsId stays the same, jacsVersion changes, and jacsPreviousVersion points to the prior version.

Field reference

FieldWhat it is
jacsIdStable identity for this signer's inline markdown document across edits.
jacsVersionVersion ID for this footer. Changes whenever the signer re-signs edited content.
jacsPreviousVersionPrevious jacsVersion for same-ID updates; absent or null on first signature.
jacsVersionDateRFC 3339 / ISO 8601 timestamp the signer claims. This is a claim, not a notarised proof.
jacsTypeinline-md for inline markdown/text footers.
content.signedContentHashBase64url SHA-256 of the canonicalised markdown/text body.
content.canonicalizationTag identifying how the bytes are normalised before hashing (currently jacs-text-v1).
jacsSignature.agentIDAgent ID. Resolves to a public key via the local trust store, the --key-dir override, or DNS.
jacsSignature.publicKeyHashSHA-256 of the signer's public key — defends against silent key swap.
jacsSignature.signatureNormal JACS document signature over the footer document fields.
jacsSha256Normal JACS document hash for the signed footer.

The body is valid YAML 1.2; any YAML 1.2 parser can read it, and JACS converts it back through the same yaml_to_jacs path used by email YAML signing. Legacy v0.10.0 mini-blocks still verify, but new writes use the full JACS footer. A file may contain at most 256 signature blocks.

Generic document verification also recognizes inline-signed markdown/text: when the body has a signature footer, jacs verify <file> and SimpleAgent::verify() dispatch to the inline verifier instead of treating the input as JSON. When the MIME type is known, callers should pass text/markdown or text/plain and use the same inline verification path.

Canonicalization (jacs-text-v1)

The bytes hashed by content.signedContentHash are derived from the file content (everything before the first signature block) by:

  1. Normalising line endings to \n (CRLF / CR collapsed to LF).
  2. Stripping trailing spaces, tabs, and blank lines at the end of the content.

The file on disk is NOT modified by canonicalization — the original bytes are preserved. Canonicalization only affects the hash input.

Permissive vs strict verification

By default, verification is permissive: a missing signature is a typed status, not an error. Strict mode opts in to treating missing signatures as a real failure.

ModeCLI exit codePythonNodeGoRust
Valid signature0result.status == 'signed'result.status === 'signed'result.Status == "signed"Status::Signed { signers }
Invalid signature1raises VerificationErrorrejectsreturns errorErr(ErrorKind::InvalidSignature)
Missing signature (permissive)2result.status == 'missing_signature'result.status === 'missing_signature'result.Status == "missing_signature"Status::MissingSignature
Missing signature (strict)1 (stderr: no JACS signature found)raises MissingSignatureErrorrejects with MissingSignatureerrors.Is(err, jacs.ErrMissingSignature)Err(ErrorKind::MissingSignature)

Pick permissive when verification is part of a check that may run against unsigned files (CI on a PR that adds the first signature). Pick strict when a missing signature is genuinely a failure (release-gate, post-merge audit).

Worked example — countersigned README

Initial state: README.md is one paragraph, unsigned.

# Agent A signs
JACS_CONFIG=./agent-a.config.json jacs sign-text README.md

# Agent B counter-signs
JACS_CONFIG=./agent-b.config.json jacs sign-text README.md

# Anyone verifies
jacs verify-text README.md
# - agent-a (ed25519)   valid
# - agent-b (pq2025)    valid

After both signers, the on-disk file is:

This README documents the v1 release.
-----BEGIN JACS SIGNATURE-----
jacsId: ...
jacsType: inline-md
... (agent-a)
jacsSignature:
  signature: ...
-----END JACS SIGNATURE-----
-----BEGIN JACS SIGNATURE-----
jacsId: ...
jacsType: inline-md
... (agent-b)
jacsSignature:
  signature: ...
-----END JACS SIGNATURE-----

You can swap the order of the two blocks on disk and jacs verify-text still reports both as valid — multi-signer is unordered.

--key-dir override

When verifying a file from someone whose key is not in your local trust store, point at a directory of <signer_id>.public.pem files:

jacs verify-text README.md --key-dir ./trusted-keys/

Resolution order: self → --key-dir (when provided) → local trust store → DNS (deferred). When --key-dir is supplied, a matching <signer_id>.public.pem wins for that specific signer; signers absent from the directory fall through to the trust store.

Marker collision — what happens if your file mentions the marker

sign-text refuses (hard error) to sign input that already contains a column-zero -----BEGIN JACS SIGNATURE----- line outside a valid block. Authors writing about the format in a markdown file have three workarounds:

  1. Indent the marker by two spaces — the parser only matches column-zero markers.
  2. Use a zero-width space prefix — the literal string differs by one byte.
  3. Use inline emphasis — wrap the marker in backticks or asterisks.

There is intentionally no --force-overwrite-markers escape hatch. Its semantics under the first-marker-splits-content parser are ambiguous — easier to fix the input file than to ship a footgun.

Caps and rejected inputs

LimitCause failure
Block body > 16 KiBErr(ErrorKind::PayloadTooLarge) (Rust) / equivalents in bindings
File > 256 signature blocksErr(ErrorKind::TooManySignatures)
Body contains YAML anchor / tag / aliasErr(ErrorKind::InvalidSignatureBlock)
Body field unknown to the schemaErr(ErrorKind::InvalidSignatureBlock) (deny_unknown_fields)
File on disk contains stray column-zero -----BEGIN JACS SIGNATURE----- outside a blockErr(ErrorKind::MarkerCollision)

Next step

If you need the same provenance for image bytes (PNG / JPEG / WebP), see Image and Media Signatures.

Image and Media Signatures

Why this matters

You have an image — a photo, an AI-generated render, a chart — and you want a downstream consumer to verify who produced it and when (claimed time) before trusting the asset. With jacs sign-image the signed-document JSON sits inside the image itself in a metadata chunk (PNG iTXt / JPEG APP11 / WebP XMP); no sidecar file, no AGPL dependency, no external service. The signature proves who signed which canonical bytes at the agent's claimed time — it does not prove first creation or legal ownership.

At a glance

ActionCLI verbPythonNodeGoRust
Signjacs sign-image <in> --out <out>jacs.sign_image(path, out=...)jacs.signImage(in, out)jacs.SignImage(in, out, nil)jacs::media::sign_image(&agent, in, out, opts)
Verifyjacs verify-image <file>jacs.verify_image(path)jacs.verifyImage(path)jacs.VerifyImage(path, nil)jacs::media::verify_image(&agent, path, opts)
Extractjacs extract-media-signature <file>jacs.extract_media_signature(path)jacs.extractMediaSignature(path)jacs.ExtractMediaSignature(path, nil)jacs::media::extract_signature(path, opts)

Per-format embedding

FormatWhere the signature livesNotes
PNGiTXt chunk with keyword jacs-signatureInserted before the IDAT chunks. Lossless; existing chunks (incl. tEXt, pHYs, iCCP) are preserved.
JPEGAPP11 segment with marker JACS\0Inserted after SOI and any APP0/APP1 (Exif/JFIF). Existing markers preserved.
WebPXMP chunk inside RIFF containerThe RIFF header size field is updated. Lossless and lossy variants are both supported.

The embedded payload is base64url-encoded JACS signed-document JSON (deliberately JSON, not YAML — images are binary containers and YAML's whitespace sensitivity adds no value). The size cap is 64 KiB; oversized payloads surface as PayloadTooLarge.

Signed claim shape

{
  "mediaSignatureVersion": 1,
  "format": "png",
  "canonicalization": "jacs-media-v1",
  "hashAlgorithm": "sha256",
  "contentHash": "sha256:e3b0c44...",
  "embeddingChannels": ["chunk:iTXt:jacs-signature"],
  "robust": false,
  "pixelHash": "sha256:9f86d08..."
}

Wrapped in a JACS signed document, the same shape is what verify-image reads to confirm signer identity, claimed timestamp, and pixel-content integrity.

Inline text vs media — why the asymmetry

Inline text uses YAML because humans skim it next to the markdown. Image payloads are read by tooling, not humans — JSON is what every JACS binding already canonicalises and signs. Both paths now carry full signed JACS documents; text renders that document as YAML between footer markers, while images embed the signed document JSON in the media container.

Robust mode (--robust) — opt-in LSB fallback

The default mode embeds in metadata chunks, which survives most pipelines but does not survive a deliberate "strip all metadata" pass. Robust mode adds an LSB (least-significant-bit) embedding in the pixel data so a stripped image still verifies.

FormatRobust support
PNGYes (--robust)
JPEGYes (--robust)
WebPDeferred to a future release

Capacity math: roughly width × height bits. A 512×512 image holds about 32 KiB of robust-mode payload; small thumbnails may not fit.

Robust mode is OFF by default because LSB embedding mutates pixel values (imperceptible to humans, but real). Opt in only when the metadata-strip threat model justifies the change.

extract-media-signature — getting the payload back out

# Default — decoded JSON (ready to read or pipe to jq)
jacs extract-media-signature signed.png
# {"mediaSignatureVersion": 1, "format": "png", ...}

# Wire form — raw base64url payload (useful for re-embedding or transport)
jacs extract-media-signature signed.png --raw-payload
# eyJtZWRpYVNpZ25hdHVyZVZlcnNpb24iOjEsImZvcm1hdC...

# Pipeline-friendly — extract the signer's claimed timestamp
jacs extract-media-signature signed.png | jq -r .signedAt
# 2026-04-24T18:00:00Z

extract-media-signature does NOT verify the signature; it only decodes the embedded payload. Use verify-image for verification.

Single-signer and overwrite policy

Images are single-signer: re-running sign-image overwrites the embedded signature with the new one. Use --refuse-overwrite to opt into first-signer-wins:

# Default: overwrite if already signed
jacs sign-image photo.png --out signed.png

# Refuse to overwrite — exit non-zero if already signed
jacs sign-image photo.png --out signed.png --refuse-overwrite

This is intentionally narrower than inline text: text is reviewed and counter-signed by humans / agents; images are typically signed once at the point of capture or generation. If you need a multi-signer flow over images, stage the signatures off-image (e.g. as a JACS agreement that references contentHash).

Permissive vs strict verification

Same model as inline text — see Inline Text Signatures.

.bak policy

When sign-image overwrites an existing file, it writes a .bak next to the output (mode 0o600 on Unix — owner-read/write only). .bak files contain the unsigned original, which may include sensitive metadata. Treat them like any other plaintext source file: they are not encrypted, do not commit them, and remove them when no longer needed.

--out <path> to a different filename avoids .bak files entirely.

--key-dir override

Same semantics as inline text — directory of <signer_id>.public.pem files, used when the trust store does not have the signer's key:

jacs verify-image signed.png --key-dir ./trusted-keys/

Caps and rejected inputs

LimitCause failure
Embedded payload > 64 KiBErr(ErrorKind::PayloadTooLarge)
Unknown format (extension or magic)Err(ErrorKind::UnsupportedFormat)
--refuse-overwrite + already-signed inputErr(ErrorKind::AlreadySigned)
Robust mode requested for WebPErr(ErrorKind::RobustNotSupported)

Clean-room provenance

The jacs-media crate is 100% Rust, dual-licensed Apache-2.0 / MIT. Zero AGPL dependencies — we cite the prior art (PNG iTXt RFC, Adobe XMP spec) without copying any source. A cargo deny license gate prevents future regressions.

CLI Examples

This chapter provides practical examples of using the JACS CLI for common workflows.

Quick Reference

jacs init                  # Initialize JACS (config + agent + keys)
jacs agent create          # Create a new agent
jacs document create       # Create a signed document
jacs document verify       # Verify a document signature
jacs document sign-agreement  # Sign an agreement

Getting Started

First-Time Setup

Initialize JACS in a new project:

# Create a new directory
mkdir my-jacs-project
cd my-jacs-project

# Initialize JACS
jacs init

# This creates:
# - jacs.config.json (configuration)
# - jacs_keys/ (private and public keys)
# - jacs_data/ (document storage)
# - An initial agent document

Verify Your Setup

# Check the configuration
jacs config read

# Verify your agent
jacs agent verify

# Expected output:
# Agent verification successful
# Agent ID: 550e8400-e29b-41d4-a716-446655440000
# Agent Version: f47ac10b-58cc-4372-a567-0e02b2c3d479

Document Operations

Creating Documents

Create from a JSON file:

# Create input file
cat > invoice.json << 'EOF'
{
  "type": "invoice",
  "invoiceNumber": "INV-001",
  "customer": "Acme Corp",
  "amount": 1500.00,
  "items": [
    {"description": "Consulting", "quantity": 10, "price": 150}
  ]
}
EOF

# Create signed document
jacs document create -f invoice.json

# Output shows the exact saved document path, for example:
# Saved signed document: jacs_data/<uuid>:<version>

Create with custom output:

# Specify output filename
jacs document create -f invoice.json -o signed-invoice.json

# Machine-readable document key and path
jacs document create -f invoice.json --json

# Print to stdout (don't save)
jacs document create -f invoice.json --no-save

Create with file attachments:

# Create document with PDF attachment
jacs document create -f contract.json --attach ./contract.pdf

# Embed attachment content in document
jacs document create -f contract.json --attach ./contract.pdf --embed true

# Attach entire directory
jacs document create -f report.json --attach ./attachments/

Create with custom schema:

# Use a custom schema for validation
jacs document create -f order.json -s ./schemas/order.schema.json

Verifying Documents

Basic verification:

# Verify a document
jacs document verify -f ./signed-invoice.json

# Expected output:
# Document verified successfully
# Document ID: 550e8400-e29b-41d4-a716-446655440000
# Signer: Agent Name (agent-uuid)

Verbose verification:

# Get detailed verification info
jacs document verify -f ./signed-invoice.json -v

# Output includes:
# - Document ID and version
# - Signature algorithm used
# - Signing agent details
# - Timestamp
# - Schema validation results

Batch verification:

# Verify all documents in a directory
jacs document verify -d ./documents/

# With custom schema
jacs document verify -d ./invoices/ -s ./schemas/invoice.schema.json

Updating Documents

Create a new version of an existing document:

# Original document
cat > original.json << 'EOF'
{
  "title": "Project Plan",
  "status": "draft",
  "content": "Initial version"
}
EOF

jacs document create -f original.json -o project-v1.json

# Updated content
cat > updated.json << 'EOF'
{
  "title": "Project Plan",
  "status": "approved",
  "content": "Final version with updates"
}
EOF

# Create new version (maintains version history)
jacs document update -f project-v1.json -n updated.json -o project-v2.json

# Verify the updated document
jacs document verify -f project-v2.json -v

Extracting Embedded Content

# Extract embedded files from a document
jacs document extract -f ./document-with-attachments.json

# Extracts to: jacs_data/extracted/[document-id]/

# Extract from multiple documents
jacs document extract -d ./documents/

Agreement Workflows

This section shows the legacy jacsAgreement sidecar commands for simple approval of an existing signed document. For new standalone consent workflows, prefer jacs agreement-v2; see Agreement v2 Developer Guide.

Creating an Agreement

An agreement requires multiple agents to sign a document:

# First, create the document to be agreed upon
cat > service-agreement.json << 'EOF'
{
  "type": "service_agreement",
  "title": "Professional Services Agreement",
  "parties": ["Company A", "Company B"],
  "terms": "...",
  "effectiveDate": "2024-02-01"
}
EOF

jacs document create -f service-agreement.json -o agreement.json

# Create agreement requiring signatures from two agents
# (Use actual agent UUIDs)
jacs document create-agreement \
  -f agreement.json \
  -i "agent1-uuid-here,agent2-uuid-here" \
  -o agreement-pending.json

# Output:
# Agreement created
# Required signatures: 2
# Current signatures: 0

Signing an Agreement

# First agent signs
jacs document sign-agreement -f agreement-pending.json -o agreement-signed-1.json

# Check status
jacs document check-agreement -f agreement-signed-1.json
# Output:
# Agreement status: pending
# Signatures: 1/2
# Missing: agent2-uuid

# Second agent signs (using their configuration)
JACS_CONFIG_PATH=./agent2.config.json \
  jacs document sign-agreement -f agreement-signed-1.json -o agreement-complete.json

# Verify completion
jacs document check-agreement -f agreement-complete.json
# Output:
# Agreement status: complete
# Signatures: 2/2

Complete Agreement Workflow

#!/bin/bash
# agreement-workflow.sh

# Step 1: Create the contract document
cat > contract.json << 'EOF'
{
  "type": "contract",
  "parties": {
    "seller": "Widget Corp",
    "buyer": "Acme Inc"
  },
  "terms": "Sale of 1000 widgets at $10 each",
  "totalValue": 10000
}
EOF

echo "Creating contract document..."
jacs document create -f contract.json -o contract-signed.json

# Step 2: Get agent IDs
SELLER_AGENT=$(jacs config read | grep agent_id | cut -d: -f2 | tr -d ' ')
BUYER_AGENT="buyer-agent-uuid-here"  # Replace with actual ID

# Step 3: Create agreement
echo "Creating agreement..."
jacs document create-agreement \
  -f contract-signed.json \
  -i "$SELLER_AGENT,$BUYER_AGENT" \
  -o contract-agreement.json

# Step 4: Seller signs
echo "Seller signing..."
jacs document sign-agreement \
  -f contract-agreement.json \
  -o contract-seller-signed.json

# Step 5: Check intermediate status
echo "Checking status..."
jacs document check-agreement -f contract-seller-signed.json

# Step 6: Buyer signs
echo "Buyer signing..."
JACS_CONFIG_PATH=./buyer.config.json \
  jacs document sign-agreement \
  -f contract-seller-signed.json \
  -o contract-complete.json

# Step 7: Verify complete agreement
echo "Final verification..."
jacs document verify -f contract-complete.json -v
jacs document check-agreement -f contract-complete.json

echo "Agreement workflow complete!"

Agent Operations

Creating a Custom Agent

# Create agent definition file
cat > my-agent.json << 'EOF'
{
  "jacsAgentType": "ai",
  "name": "My Custom Agent",
  "description": "An AI agent for document processing",
  "contact": {
    "email": "agent@example.com"
  },
  "services": [
    {
      "name": "document-processing",
      "description": "Process and sign documents"
    }
  ]
}
EOF

# Create agent with new keys
jacs agent create --create-keys true -f my-agent.json

# Create agent using existing keys
jacs agent create --create-keys false -f my-agent.json

DNS-Based Identity

Generate DNS record commands:

# Generate TXT record for your domain
jacs agent dns --domain myagent.example.com

# Output (example):
# Add the following DNS TXT record:
# _v1.agent.jacs.myagent.example.com TXT "pk=<base64-public-key-hash>"

# Different providers
jacs agent dns --domain myagent.example.com --provider aws
jacs agent dns --domain myagent.example.com --provider cloudflare
jacs agent dns --domain myagent.example.com --provider azure

# Custom TTL
jacs agent dns --domain myagent.example.com --ttl 7200

Verify DNS-published agent:

# Look up agent by domain
jacs agent lookup partner.example.com

# Require strict DNSSEC validation
jacs agent lookup partner.example.com --strict

# Verify local agent file against DNS
jacs agent verify -a ./partner-agent.json --require-strict-dns

Agent Verification

# Basic verification
jacs agent verify

# Verify another agent's file
jacs agent verify -a ./other-agent.json

# With DNS requirements
jacs agent verify --require-dns          # Require DNS (not strict)
jacs agent verify --require-strict-dns   # Require DNSSEC
jacs agent verify --no-dns              # Skip DNS entirely
jacs agent verify --ignore-dns          # Ignore DNS validation failures

Scripting Examples

Batch Document Processing

#!/bin/bash
# batch-sign.sh - Sign all JSON files in a directory

INPUT_DIR=$1
OUTPUT_DIR=${2:-"./signed"}

mkdir -p "$OUTPUT_DIR"

for file in "$INPUT_DIR"/*.json; do
  filename=$(basename "$file")
  echo "Signing: $filename"

  jacs document create -f "$file" -o "$OUTPUT_DIR/$filename"

  if [ $? -eq 0 ]; then
    echo "  ✓ Signed successfully"
  else
    echo "  ✗ Signing failed"
  fi
done

echo "Batch signing complete. Output in $OUTPUT_DIR"

Verification Report

#!/bin/bash
# verify-report.sh - Generate verification report

DOC_DIR=$1
REPORT="verification-report.txt"

echo "JACS Document Verification Report" > $REPORT
echo "Generated: $(date)" >> $REPORT
echo "=================================" >> $REPORT
echo "" >> $REPORT

passed=0
failed=0

for file in "$DOC_DIR"/*.json; do
  filename=$(basename "$file")

  if jacs document verify -f "$file" > /dev/null 2>&1; then
    echo "✓ PASS: $filename" >> $REPORT
    ((passed++))
  else
    echo "✗ FAIL: $filename" >> $REPORT
    ((failed++))
  fi
done

echo "" >> $REPORT
echo "Summary: $passed passed, $failed failed" >> $REPORT

cat $REPORT

Watch for New Documents

#!/bin/bash
# watch-and-verify.sh - Monitor directory and verify new documents

WATCH_DIR=${1:-"./incoming"}

echo "Watching $WATCH_DIR for new documents..."

inotifywait -m "$WATCH_DIR" -e create -e moved_to |
  while read dir action file; do
    if [[ "$file" == *.json ]]; then
      echo "New document: $file"

      if jacs document verify -f "$WATCH_DIR/$file"; then
        mv "$WATCH_DIR/$file" "./verified/"
        echo "  Moved to verified/"
      else
        mv "$WATCH_DIR/$file" "./rejected/"
        echo "  Moved to rejected/"
      fi
    fi
  done

Environment Configuration

Using Environment Variables

# Use a specific config file
export JACS_CONFIG_PATH=./production.config.json
jacs document create -f invoice.json

# Override specific settings
export JACS_DATA_DIRECTORY=./custom-data
export JACS_KEY_DIRECTORY=./secure-keys
jacs agent create --create-keys true

# One-time override
JACS_CONFIG_PATH=./test.config.json jacs document verify -f test-doc.json

Multiple Configurations

# Development
alias jacs-dev='JACS_CONFIG_PATH=./dev.config.json jacs'
jacs-dev document create -f test.json

# Production
alias jacs-prod='JACS_CONFIG_PATH=./prod.config.json jacs'
jacs-prod document verify -f important.json

# Different agents
alias jacs-alice='JACS_CONFIG_PATH=./alice.config.json jacs'
alias jacs-bob='JACS_CONFIG_PATH=./bob.config.json jacs'

Error Handling

Understanding Exit Codes

jacs document verify -f document.json
exit_code=$?

case $exit_code in
  0) echo "Success" ;;
  1) echo "General error" ;;
  2) echo "Invalid arguments" ;;
  3) echo "File not found" ;;
  4) echo "Verification failed" ;;
  5) echo "Signature invalid" ;;
  *) echo "Unknown error: $exit_code" ;;
esac

Handling Failures

#!/bin/bash
# robust-signing.sh

sign_document() {
  local input=$1
  local output=$2

  if ! jacs document create -f "$input" -o "$output" 2>/dev/null; then
    echo "Error: Failed to sign $input" >&2
    return 1
  fi

  if ! jacs document verify -f "$output" 2>/dev/null; then
    echo "Error: Verification failed for $output" >&2
    rm -f "$output"
    return 1
  fi

  echo "Successfully signed: $output"
  return 0
}

# Usage
sign_document "invoice.json" "signed-invoice.json" || exit 1

See Also

Node.js Examples

Registry status (observed 2026-07-11): npm serves @hai.ai/jacs@0.10.1, while these pages describe source 0.11.4. The registry package does not expose every method shown here. Pin the version and inspect its exports; use a source build only when that is an explicit deployment choice.

This chapter provides practical Node.js examples using the @hai.ai/jacs package.

Setup

# Install dependencies
npm install @hai.ai/jacs express @modelcontextprotocol/sdk zod

v0.7.0 uses an async-first API. All NAPI operations return Promises by default; sync variants use a Sync suffix.

// Initialize JACS (ES Modules, async)
import { JacsAgent } from '@hai.ai/jacs';

const agent = new JacsAgent();
await agent.load('./jacs.config.json');

Basic Document Operations

Creating and Signing Documents

import { JacsAgent } from '@hai.ai/jacs';

async function createSignedDocument() {
  const agent = new JacsAgent();
  await agent.load('./jacs.config.json');

  // Create a simple document
  const content = {
    title: 'Invoice',
    invoiceNumber: 'INV-001',
    amount: 1500.00,
    customer: 'Acme Corp',
    items: [
      { description: 'Consulting', quantity: 10, price: 150 }
    ]
  };

  // Create and sign the document
  const signedDoc = await agent.createDocument(JSON.stringify(content));

  // Parse the result
  const doc = JSON.parse(signedDoc);
  console.log('Document ID:', doc.jacsId);
  console.log('Version:', doc.jacsVersion);
  console.log('Signature:', doc.jacsSignature ? 'Present' : 'Missing');

  return doc;
}

createSignedDocument();

Verifying Documents

import { JacsAgent } from '@hai.ai/jacs';
import fs from 'fs';

async function verifyDocument(filePath) {
  const agent = new JacsAgent();
  await agent.load('./jacs.config.json');

  // Read the document
  const docString = fs.readFileSync(filePath, 'utf-8');

  // Verify signature
  const isValid = await agent.verifyDocument(docString);

  if (isValid) {
    console.log('✓ Document signature is valid');
    const doc = JSON.parse(docString);
    console.log('  Signed by:', doc.jacsSignature?.agentID);
    console.log('  Signed at:', doc.jacsSignature?.date);
  } else {
    console.log('✗ Document signature is INVALID');
  }

  return isValid;
}

verifyDocument('./invoice.json');

Updating Documents

import { JacsAgent } from '@hai.ai/jacs';
import fs from 'fs';

async function updateDocument(originalPath, newContent) {
  const agent = new JacsAgent();
  await agent.load('./jacs.config.json');

  // Read original document
  const originalDoc = fs.readFileSync(originalPath, 'utf-8');

  // Update with new content (preserves version chain)
  const updatedDoc = await agent.updateDocument(
    originalDoc,
    JSON.stringify(newContent)
  );

  const doc = JSON.parse(updatedDoc);
  console.log('Updated Document ID:', doc.jacsId);
  console.log('New Version:', doc.jacsVersion);

  return doc;
}

// Usage
const updated = await updateDocument('./invoice-v1.json', {
  title: 'Invoice',
  invoiceNumber: 'INV-001',
  amount: 1500.00,
  customer: 'Acme Corp',
  status: 'paid'  // New field
});

HTTP Server with Express

Complete Express Server

import express from 'express';
import { JACSExpressMiddleware } from '@hai.ai/jacs/http';
import { JacsAgent } from '@hai.ai/jacs';

const app = express();
const PORT = 3000;

// Initialize JACS
const agent = new JacsAgent();
await agent.load('./jacs.config.json');

// Health check (no JACS)
app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

// JACS-protected API routes
app.use('/api', express.text({ type: '*/*' }));
app.use('/api', JACSExpressMiddleware({
  configPath: './jacs.config.json'
}));

// Validation middleware
function requirePayload(req, res, next) {
  if (!req.jacsPayload) {
    return res.status(400).json({
      error: 'Invalid JACS request',
      message: 'Request must be signed with valid JACS credentials'
    });
  }
  next();
}

// Echo endpoint
app.post('/api/echo', requirePayload, (req, res) => {
  res.send({
    echo: req.jacsPayload,
    serverTime: new Date().toISOString()
  });
});

// Calculate endpoint
app.post('/api/calculate', requirePayload, (req, res) => {
  const { operation, a, b } = req.jacsPayload;

  let result;
  switch (operation) {
    case 'add': result = a + b; break;
    case 'subtract': result = a - b; break;
    case 'multiply': result = a * b; break;
    case 'divide': result = b !== 0 ? a / b : null; break;
    default:
      return res.status(400).send({ error: 'Unknown operation' });
  }

  res.send({ operation, a, b, result });
});

// Create document endpoint
app.post('/api/documents', requirePayload, async (req, res) => {
  try {
    const signedDoc = await agent.createDocument(
      JSON.stringify(req.jacsPayload)
    );
    const doc = JSON.parse(signedDoc);

    res.send({
      success: true,
      documentId: doc.jacsId,
      version: doc.jacsVersion
    });
  } catch (error) {
    res.status(500).send({ error: error.message });
  }
});

// Error handler
app.use((err, req, res, next) => {
  console.error('Error:', err);
  res.status(500).send({ error: 'Internal server error' });
});

app.listen(PORT, () => {
  console.log(`JACS Express server running on port ${PORT}`);
});

HTTP Client

import { JacsAgent } from '@hai.ai/jacs';

async function callJacsApi(url, payload) {
  const agent = new JacsAgent();
  await agent.load('./jacs.client.config.json');

  // Sign the request
  const signedRequest = await agent.signRequest(payload);

  // Send HTTP request
  const response = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'text/plain' },
    body: signedRequest
  });

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  // Verify and extract response
  const responseText = await response.text();
  const verified = await agent.verifyResponse(responseText);

  return verified.payload;
}

// Usage
async function main() {
  // Call echo endpoint
  const echoResult = await callJacsApi(
    'http://localhost:3000/api/echo',
    { message: 'Hello, server!' }
  );
  console.log('Echo:', echoResult);

  // Call calculate endpoint
  const calcResult = await callJacsApi(
    'http://localhost:3000/api/calculate',
    { operation: 'multiply', a: 7, b: 6 }
  );
  console.log('Calculate:', calcResult);
}

main().catch(console.error);

MCP Integration

MCP Server

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { JacsClient } from '@hai.ai/jacs/client';
import { createJACSTransportProxy } from '@hai.ai/jacs/mcp';
import { z } from 'zod';

async function main() {
  console.error("JACS MCP Server starting...");

  const client = await JacsClient.quickstart({
    name: 'jacs-demo-server',
    domain: 'mcp.local',
  });

  const baseTransport = new StdioServerTransport();
  const secureTransport = createJACSTransportProxy(
    baseTransport,
    client,
    "server"
  );

  const server = new McpServer({
    name: "jacs-demo-server",
    version: "1.0.0"
  });

  // Register tools
  server.tool("echo", {
    message: z.string().describe("Message to echo")
  }, async ({ message }) => {
    console.error(`Echo called: ${message}`);
    return { content: [{ type: "text", text: `Echo: ${message}` }] };
  });

  server.tool("calculate", {
    operation: z.enum(["add", "subtract", "multiply", "divide"]),
    a: z.number(),
    b: z.number()
  }, async ({ operation, a, b }) => {
    let result;
    switch (operation) {
      case 'add': result = a + b; break;
      case 'subtract': result = a - b; break;
      case 'multiply': result = a * b; break;
      case 'divide': result = b !== 0 ? a / b : 'undefined'; break;
    }
    return { content: [{ type: "text", text: `${a} ${operation} ${b} = ${result}` }] };
  });

  // Register resource
  server.resource(
    "server-info",
    "info://server",
    async (uri) => ({
      contents: [{
        uri: uri.href,
        text: JSON.stringify({
          name: "JACS Demo Server",
          version: "1.0.0",
          capabilities: ["echo", "calculate"]
        }),
        mimeType: "application/json"
      }]
    })
  );

  // Connect
  await server.connect(secureTransport);
  console.error("Server running with JACS encryption");
}

main().catch(err => {
  console.error("Fatal error:", err);
  process.exit(1);
});

MCP Client

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { JacsClient } from '@hai.ai/jacs/client';
import { createJACSTransportProxy } from '@hai.ai/jacs/mcp';

async function main() {
  console.log("JACS MCP Client starting...");

  const client = await JacsClient.quickstart({
    name: 'jacs-demo-client',
    domain: 'mcp.local',
  });

  const baseTransport = new StdioClientTransport({
    command: 'node',
    args: ['mcp-server.js']
  });

  const secureTransport = createJACSTransportProxy(
    baseTransport,
    client,
    "client"
  );

  const mcpClient = new Client({
    name: "jacs-demo-client",
    version: "1.0.0"
  }, {
    capabilities: { tools: {} }
  });

  await mcpClient.connect(secureTransport);
  console.log("Connected to JACS MCP Server");

  // List tools
  const tools = await mcpClient.listTools();
  console.log("Available tools:", tools.tools.map(t => t.name));

  // Call echo
  const echoResult = await mcpClient.callTool({
    name: "echo",
    arguments: { message: "Hello, JACS!" }
  });
  console.log("Echo:", echoResult.content[0].text);

  // Call calculate
  const calcResult = await mcpClient.callTool({
    name: "calculate",
    arguments: { operation: "multiply", a: 6, b: 7 }
  });
  console.log("Calculate:", calcResult.content[0].text);

  await mcpClient.close();
  console.log("Done!");
}

main().catch(console.error);

Agreements

This section shows the legacy jacsAgreement sidecar API for simple approval of an existing signed document. For new standalone consent workflows, use Agreement v2: see Agreement v2 Developer Guide.

Creating Multi-Party Agreements

import { JacsAgent } from '@hai.ai/jacs';
import fs from 'fs';

async function createAgreement() {
  const agent = new JacsAgent();
  await agent.load('./jacs.config.json');

  // Create the contract document
  const contract = {
    type: 'service_agreement',
    title: 'Professional Services Agreement',
    parties: ['Company A', 'Company B'],
    terms: 'Terms and conditions here...',
    value: 50000,
    effectiveDate: '2024-02-01'
  };

  const signedContract = await agent.createDocument(JSON.stringify(contract));

  // Get agent IDs (replace with actual UUIDs)
  const agentIds = [
    'agent1-uuid-here',
    'agent2-uuid-here'
  ];

  // Create agreement
  const agreementDoc = await agent.createAgreement(
    signedContract,
    agentIds,
    'Do you agree to the terms of this service agreement?',
    'This is a legally binding agreement'
  );

  console.log('Agreement created');
  const doc = JSON.parse(agreementDoc);
  console.log('Document ID:', doc.jacsId);
  console.log('Required signatures:', doc.jacsAgreement?.agentIDs?.length);

  // Save for signing
  fs.writeFileSync('agreement-pending.json', agreementDoc);

  return doc;
}

createAgreement();

Signing Agreements

import { JacsAgent } from '@hai.ai/jacs';
import fs from 'fs';

async function signAgreement(agreementPath, outputPath) {
  const agent = new JacsAgent();
  await agent.load('./jacs.config.json');

  // Read agreement
  const agreementDoc = fs.readFileSync(agreementPath, 'utf-8');

  // Sign agreement
  const signedAgreement = await agent.signAgreement(agreementDoc);

  // Check status
  const statusJson = await agent.checkAgreement(signedAgreement);
  const status = JSON.parse(statusJson);

  console.log('Agreement signed');
  console.log('Status:', status.complete ? 'Complete' : 'Pending');
  console.log('Signatures:', status.signatures?.length || 0);

  // Save
  fs.writeFileSync(outputPath, signedAgreement);

  return status;
}

signAgreement('./agreement-pending.json', './agreement-signed.json');

Checking Agreement Status

import { JacsAgent } from '@hai.ai/jacs';
import fs from 'fs';

async function checkAgreementStatus(agreementPath) {
  const agent = new JacsAgent();
  await agent.load('./jacs.config.json');

  const agreementDoc = fs.readFileSync(agreementPath, 'utf-8');
  const statusJson = await agent.checkAgreement(agreementDoc);
  const status = JSON.parse(statusJson);

  console.log('Agreement Status:');
  console.log('  Complete:', status.complete);
  console.log('  Required agents:', status.requiredAgents);
  console.log('  Signed by:', status.signedBy || []);
  console.log('  Missing:', status.missing || []);

  return status;
}

checkAgreementStatus('./agreement.json');

Document Store

Simple File-Based Store

import { JacsAgent } from '@hai.ai/jacs';
import fs from 'fs';
import path from 'path';

class JacsDocumentStore {
  constructor(configPath, dataDir = './documents') {
    this.configPath = configPath;
    this.dataDir = dataDir;
    this.agent = null;
  }

  async initialize() {
    this.agent = new JacsAgent();
    await this.agent.load(this.configPath);

    if (!fs.existsSync(this.dataDir)) {
      fs.mkdirSync(this.dataDir, { recursive: true });
    }
  }

  async create(content) {
    const signedDoc = await this.agent.createDocument(JSON.stringify(content));
    const doc = JSON.parse(signedDoc);

    const filename = `${doc.jacsId}.json`;
    const filepath = path.join(this.dataDir, filename);

    fs.writeFileSync(filepath, signedDoc);

    return { id: doc.jacsId, version: doc.jacsVersion, path: filepath };
  }

  async get(documentId) {
    const filepath = path.join(this.dataDir, `${documentId}.json`);

    if (!fs.existsSync(filepath)) {
      return null;
    }

    const docString = fs.readFileSync(filepath, 'utf-8');
    return JSON.parse(docString);
  }

  async verify(documentId) {
    const filepath = path.join(this.dataDir, `${documentId}.json`);

    if (!fs.existsSync(filepath)) {
      return { valid: false, error: 'Document not found' };
    }

    const docString = fs.readFileSync(filepath, 'utf-8');
    const isValid = await this.agent.verifyDocument(docString);

    return { valid: isValid, document: JSON.parse(docString) };
  }

  list() {
    const files = fs.readdirSync(this.dataDir);
    return files
      .filter(f => f.endsWith('.json'))
      .map(f => f.replace('.json', ''));
  }
}

// Usage
async function main() {
  const store = new JacsDocumentStore('./jacs.config.json');
  await store.initialize();

  // Create document
  const result = await store.create({
    type: 'note',
    title: 'Meeting Notes',
    content: 'Discussed project timeline...'
  });
  console.log('Created:', result.id);

  // Verify document
  const verification = await store.verify(result.id);
  console.log('Valid:', verification.valid);

  // List all documents
  const docs = store.list();
  console.log('Documents:', docs);
}

main();

Error Handling

Robust Error Handling Pattern

import { JacsAgent } from '@hai.ai/jacs';

class JacsError extends Error {
  constructor(message, code, details = {}) {
    super(message);
    this.name = 'JacsError';
    this.code = code;
    this.details = details;
  }
}

async function robustDocumentCreate(configPath, content) {
  let agent;

  try {
    agent = new JacsAgent();
    await agent.load(configPath);
  } catch (error) {
    throw new JacsError(
      'Failed to initialize JACS agent',
      'INIT_ERROR',
      { originalError: error.message }
    );
  }

  try {
    const signedDoc = await agent.createDocument(JSON.stringify(content));
    return JSON.parse(signedDoc);
  } catch (error) {
    throw new JacsError(
      'Failed to create document',
      'CREATE_ERROR',
      { originalError: error.message, content }
    );
  }
}

async function robustDocumentVerify(configPath, docString) {
  let agent;

  try {
    agent = new JacsAgent();
    await agent.load(configPath);
  } catch (error) {
    throw new JacsError(
      'Failed to initialize JACS agent',
      'INIT_ERROR',
      { originalError: error.message }
    );
  }

  try {
    const isValid = await agent.verifyDocument(docString);
    return { valid: isValid };
  } catch (error) {
    throw new JacsError(
      'Verification error',
      'VERIFY_ERROR',
      { originalError: error.message }
    );
  }
}

// Usage with error handling
async function main() {
  try {
    const doc = await robustDocumentCreate('./jacs.config.json', {
      title: 'Test'
    });
    console.log('Created:', doc.jacsId);
  } catch (error) {
    if (error instanceof JacsError) {
      console.error(`JACS Error [${error.code}]:`, error.message);
      console.error('Details:', error.details);
    } else {
      console.error('Unexpected error:', error);
    }
  }
}

main();

Testing

Jest Test Setup

// tests/jacs.test.js
import { JacsAgent } from '@hai.ai/jacs';
import fs from 'fs';
import path from 'path';
import os from 'os';

describe('JACS Document Operations', () => {
  let agent;
  let tempDir;
  let configPath;

  beforeAll(async () => {
    // Create temp directory
    tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jacs-test-'));
    const dataDir = path.join(tempDir, 'data');
    const keyDir = path.join(tempDir, 'keys');

    fs.mkdirSync(dataDir);
    fs.mkdirSync(keyDir);

    // Create test config
    const config = {
      jacs_data_directory: dataDir,
      jacs_key_directory: keyDir,
      jacs_agent_key_algorithm: 'ring-Ed25519',
      jacs_default_storage: 'fs'
    };

    configPath = path.join(tempDir, 'jacs.config.json');
    fs.writeFileSync(configPath, JSON.stringify(config));

    // Initialize agent
    agent = new JacsAgent();
    await agent.load(configPath);
  });

  afterAll(() => {
    fs.rmSync(tempDir, { recursive: true });
  });

  test('creates a signed document', async () => {
    const content = { title: 'Test Document', value: 42 };
    const signedDoc = await agent.createDocument(JSON.stringify(content));
    const doc = JSON.parse(signedDoc);

    expect(doc.jacsId).toBeDefined();
    expect(doc.jacsVersion).toBeDefined();
    expect(doc.jacsSignature).toBeDefined();
    expect(doc.title).toBe('Test Document');
  });

  test('verifies a valid document', async () => {
    const content = { title: 'Verify Test' };
    const signedDoc = await agent.createDocument(JSON.stringify(content));

    const isValid = await agent.verifyDocument(signedDoc);
    expect(isValid).toBe(true);
  });

  test('detects tampered document', async () => {
    const content = { title: 'Tamper Test' };
    const signedDoc = await agent.createDocument(JSON.stringify(content));

    // Tamper with document
    const doc = JSON.parse(signedDoc);
    doc.title = 'Modified Title';
    const tamperedDoc = JSON.stringify(doc);

    const isValid = await agent.verifyDocument(tamperedDoc);
    expect(isValid).toBe(false);
  });
});

See Also

Python Examples

This chapter provides practical Python examples using the jacs (jacspy) package.

Setup

# Install dependencies
pip install jacs fastmcp fastapi uvicorn
# Initialize JACS
import jacs

agent = jacs.JacsAgent()
agent.load('./jacs.config.json')

Basic Document Operations

Creating and Signing Documents

import jacs
import json

def create_signed_document():
    # Initialize agent
    agent = jacs.JacsAgent()
    agent.load('./jacs.config.json')

    # Create document content
    content = {
        "title": "Invoice",
        "invoiceNumber": "INV-001",
        "amount": 1500.00,
        "customer": "Acme Corp",
        "items": [
            {"description": "Consulting", "quantity": 10, "price": 150}
        ]
    }

    # Create and sign the document
    signed_doc = agent.create_document(json.dumps(content))

    # Parse the result
    doc = json.loads(signed_doc)
    print(f"Document ID: {doc['jacsId']}")
    print(f"Version: {doc['jacsVersion']}")
    print(f"Signature: {'Present' if 'jacsSignature' in doc else 'Missing'}")

    return doc

if __name__ == "__main__":
    create_signed_document()

Verifying Documents

import jacs
import json

def verify_document(file_path: str) -> bool:
    # Initialize agent
    agent = jacs.JacsAgent()
    agent.load('./jacs.config.json')

    # Read the document
    with open(file_path, 'r') as f:
        doc_string = f.read()

    # Verify signature
    is_valid = agent.verify_document(doc_string)

    if is_valid:
        doc = json.loads(doc_string)
        print("✓ Document signature is valid")
        print(f"  Signed by: {doc.get('jacsSignature', {}).get('agentID')}")
        print(f"  Signed at: {doc.get('jacsSignature', {}).get('date')}")
    else:
        print("✗ Document signature is INVALID")

    return is_valid

if __name__ == "__main__":
    verify_document('./invoice.json')

Updating Documents

import jacs
import json

def update_document(original_path: str, new_content: dict) -> dict:
    # Initialize agent
    agent = jacs.JacsAgent()
    agent.load('./jacs.config.json')

    # Read original document
    with open(original_path, 'r') as f:
        original_doc = f.read()

    # Update with new content (preserves version chain)
    updated_doc = agent.update_document(
        original_doc,
        json.dumps(new_content)
    )

    doc = json.loads(updated_doc)
    print(f"Updated Document ID: {doc['jacsId']}")
    print(f"New Version: {doc['jacsVersion']}")

    return doc

if __name__ == "__main__":
    updated = update_document('./invoice-v1.json', {
        "title": "Invoice",
        "invoiceNumber": "INV-001",
        "amount": 1500.00,
        "customer": "Acme Corp",
        "status": "paid"  # New field
    })

HTTP Server with FastAPI

Signed-artifact example, not request authentication: the legacy sign_request / verify_request calls below sign a generic JACS document but do not bind the HTTP method, absolute URL, query, body bytes, or audience. Do not use this pattern to authorize an endpoint. A source 0.11.4 client builds JACS v2 with SimpleAgent.build_request_auth_header(method, url, body, audience); the server must verify the same context with a trusted key and atomically consume the nonce. Likewise, use SimpleAgent.sign_response and unwrap_signed_event with pinned keys for fail-closed response events. See Security.

Complete FastAPI Server

from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import PlainTextResponse
import jacs
import json

app = FastAPI(title="JACS API")

# Initialize JACS agent at startup
agent = None

@app.on_event("startup")
async def startup():
    global agent
    agent = jacs.JacsAgent()
    agent.load('./jacs.config.json')

# Health check (no JACS)
@app.get("/health")
async def health():
    return {"status": "ok"}

# JACS-protected endpoint
@app.post("/api/echo")
async def echo(request: Request):
    # Read raw body
    body = await request.body()
    body_str = body.decode('utf-8')

    # Verify JACS request
    try:
        verified = jacs.verify_request(body_str)
        payload = json.loads(verified).get('payload')
    except Exception as e:
        raise HTTPException(status_code=400, detail="Invalid JACS request")

    # Process and respond
    result = {
        "echo": payload,
        "serverTime": str(datetime.now())
    }

    # Sign response
    signed_response = jacs.sign_response(result)
    return PlainTextResponse(content=signed_response)

# Create document endpoint
@app.post("/api/documents")
async def create_document(request: Request):
    body = await request.body()
    body_str = body.decode('utf-8')

    try:
        verified = jacs.verify_request(body_str)
        payload = json.loads(verified).get('payload')
    except Exception as e:
        raise HTTPException(status_code=400, detail="Invalid JACS request")

    # Create signed document
    signed_doc = agent.create_document(json.dumps(payload))
    doc = json.loads(signed_doc)

    result = {
        "success": True,
        "documentId": doc['jacsId'],
        "version": doc['jacsVersion']
    }

    signed_response = jacs.sign_response(result)
    return PlainTextResponse(content=signed_response)

# Calculate endpoint
@app.post("/api/calculate")
async def calculate(request: Request):
    body = await request.body()
    body_str = body.decode('utf-8')

    try:
        verified = jacs.verify_request(body_str)
        payload = json.loads(verified).get('payload')
    except Exception as e:
        raise HTTPException(status_code=400, detail="Invalid JACS request")

    operation = payload.get('operation')
    a = payload.get('a', 0)
    b = payload.get('b', 0)

    if operation == 'add':
        result = a + b
    elif operation == 'subtract':
        result = a - b
    elif operation == 'multiply':
        result = a * b
    elif operation == 'divide':
        result = a / b if b != 0 else None
    else:
        raise HTTPException(status_code=400, detail="Unknown operation")

    response = {"operation": operation, "a": a, "b": b, "result": result}
    signed_response = jacs.sign_response(response)
    return PlainTextResponse(content=signed_response)

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="localhost", port=8000)

HTTP Client

import jacs
import requests
import json

def call_jacs_api(url: str, payload: dict) -> dict:
    # Initialize agent
    agent = jacs.JacsAgent()
    agent.load('./jacs.client.config.json')

    # Sign the request
    signed_request = jacs.sign_request(payload)

    # Send HTTP request
    response = requests.post(
        url,
        data=signed_request,
        headers={"Content-Type": "text/plain"}
    )

    if response.status_code != 200:
        raise Exception(f"HTTP {response.status_code}")

    # Verify and extract response
    verified = jacs.verify_response(response.text)
    return json.loads(verified).get('payload')

if __name__ == "__main__":
    # Call echo endpoint
    echo_result = call_jacs_api(
        'http://localhost:8000/api/echo',
        {"message": "Hello, server!"}
    )
    print("Echo:", echo_result)

    # Call calculate endpoint
    calc_result = call_jacs_api(
        'http://localhost:8000/api/calculate',
        {"operation": "multiply", "a": 7, "b": 6}
    )
    print("Calculate:", calc_result)

MCP Integration

FastMCP Server with JACS

import jacs
from jacs.mcp import JACSMCPServer
from fastmcp import FastMCP
import uvicorn

# Initialize JACS
agent = jacs.JacsAgent()
agent.load('./jacs.config.json')

# Create FastMCP server with JACS
mcp = JACSMCPServer(
    FastMCP("JACS Demo Server"),
    "./jacs.config.json",
    allowed_peer_agent_ids=["CLIENT_AGENT_ID"],
)

@mcp.tool()
def echo(message: str) -> str:
    """Echo the input message"""
    return f"Echo: {message}"

@mcp.tool()
def calculate(operation: str, a: float, b: float) -> str:
    """Perform basic arithmetic"""
    if operation == 'add':
        result = a + b
    elif operation == 'subtract':
        result = a - b
    elif operation == 'multiply':
        result = a * b
    elif operation == 'divide':
        result = a / b if b != 0 else "undefined"
    else:
        return f"Unknown operation: {operation}"

    return f"{a} {operation} {b} = {result}"

@mcp.resource("info://server")
def server_info() -> str:
    """Get server information"""
    return json.dumps({
        "name": "JACS Demo Server",
        "version": "1.0.0",
        "tools": ["echo", "calculate"]
    })

# Get ASGI app with JACS middleware
app = mcp.http_app(transport="sse")

if __name__ == "__main__":
    print("Starting JACS MCP Server...")
    uvicorn.run(app, host="localhost", port=8000)

MCP Client with JACS

import asyncio
import jacs
from jacs.mcp import JACSMCPClient

async def main():
    # Initialize JACS
    agent = jacs.JacsAgent()
    agent.load('./jacs.client.config.json')

    # Create authenticated client
    client = JACSMCPClient(
        "http://localhost:8000/sse",
        "./jacs.client.config.json",
        expected_peer_agent_id="SERVER_AGENT_ID",
    )

    async with client:
        # Call echo tool
        echo_result = await client.call_tool("echo", {
            "message": "Hello from JACS client!"
        })
        print(f"Echo: {echo_result}")

        # Call calculate tool
        calc_result = await client.call_tool("calculate", {
            "operation": "multiply",
            "a": 6,
            "b": 7
        })
        print(f"Calculate: {calc_result}")

        # Read resource
        info = await client.read_resource("info://server")
        print(f"Server info: {info}")

if __name__ == "__main__":
    asyncio.run(main())

Agreements

This section shows the legacy jacsAgreement sidecar API for simple approval of an existing signed document. For new standalone consent workflows, use Agreement v2: see Agreement v2 Developer Guide and examples/agreement_v2_three_party.py.

Creating Multi-Party Agreements

import jacs
import json

def create_agreement():
    # Initialize agent
    agent = jacs.JacsAgent()
    agent.load('./jacs.config.json')

    # Create contract document
    contract = {
        "type": "service_agreement",
        "title": "Professional Services Agreement",
        "parties": ["Company A", "Company B"],
        "terms": "Terms and conditions here...",
        "value": 50000,
        "effectiveDate": "2024-02-01"
    }

    signed_contract = agent.create_document(json.dumps(contract))

    # Define required signers (replace with actual UUIDs)
    agent_ids = [
        "agent1-uuid-here",
        "agent2-uuid-here"
    ]

    # Create agreement
    agreement_doc = agent.create_agreement(
        signed_contract,
        agent_ids,
        question="Do you agree to the terms of this service agreement?",
        context="This is a legally binding agreement"
    )

    doc = json.loads(agreement_doc)
    print("Agreement created")
    print(f"Document ID: {doc['jacsId']}")
    print(f"Required signatures: {len(doc.get('jacsAgreement', {}).get('agentIDs', []))}")

    # Save for signing
    with open('agreement-pending.json', 'w') as f:
        f.write(agreement_doc)

    return doc

if __name__ == "__main__":
    create_agreement()

Signing Agreements

import jacs
import json

def sign_agreement(agreement_path: str, output_path: str) -> dict:
    # Initialize agent
    agent = jacs.JacsAgent()
    agent.load('./jacs.config.json')

    # Read agreement
    with open(agreement_path, 'r') as f:
        agreement_doc = f.read()

    # Sign agreement
    signed_agreement = agent.sign_agreement(agreement_doc)

    # Check status
    status_json = agent.check_agreement(signed_agreement)
    status = json.loads(status_json)

    print("Agreement signed")
    print(f"Status: {'Complete' if status.get('complete') else 'Pending'}")
    print(f"Signatures: {len(status.get('signatures', []))}")

    # Save
    with open(output_path, 'w') as f:
        f.write(signed_agreement)

    return status

if __name__ == "__main__":
    sign_agreement('./agreement-pending.json', './agreement-signed.json')

Checking Agreement Status

import jacs
import json

def check_agreement_status(agreement_path: str) -> dict:
    # Initialize agent
    agent = jacs.JacsAgent()
    agent.load('./jacs.config.json')

    with open(agreement_path, 'r') as f:
        agreement_doc = f.read()

    status_json = agent.check_agreement(agreement_doc)
    status = json.loads(status_json)

    print("Agreement Status:")
    print(f"  Complete: {status.get('complete')}")
    print(f"  Required agents: {status.get('requiredAgents', [])}")
    print(f"  Signed by: {status.get('signedBy', [])}")
    print(f"  Missing: {status.get('missing', [])}")

    return status

if __name__ == "__main__":
    check_agreement_status('./agreement.json')

Document Store

Simple File-Based Store

import jacs
import json
import os
from pathlib import Path
from typing import Optional, Dict, List

class JacsDocumentStore:
    def __init__(self, config_path: str, data_dir: str = './documents'):
        self.config_path = config_path
        self.data_dir = Path(data_dir)
        self.agent = None

    def initialize(self):
        self.agent = jacs.JacsAgent()
        self.agent.load(self.config_path)
        self.data_dir.mkdir(parents=True, exist_ok=True)

    def create(self, content: dict) -> dict:
        signed_doc = self.agent.create_document(json.dumps(content))
        doc = json.loads(signed_doc)

        filename = f"{doc['jacsId']}.json"
        filepath = self.data_dir / filename

        with open(filepath, 'w') as f:
            f.write(signed_doc)

        return {
            'id': doc['jacsId'],
            'version': doc['jacsVersion'],
            'path': str(filepath)
        }

    def get(self, document_id: str) -> Optional[dict]:
        filepath = self.data_dir / f"{document_id}.json"

        if not filepath.exists():
            return None

        with open(filepath, 'r') as f:
            return json.load(f)

    def verify(self, document_id: str) -> dict:
        filepath = self.data_dir / f"{document_id}.json"

        if not filepath.exists():
            return {'valid': False, 'error': 'Document not found'}

        with open(filepath, 'r') as f:
            doc_string = f.read()

        is_valid = self.agent.verify_document(doc_string)
        return {'valid': is_valid, 'document': json.loads(doc_string)}

    def list(self) -> List[str]:
        return [
            f.stem for f in self.data_dir.glob('*.json')
        ]

if __name__ == "__main__":
    store = JacsDocumentStore('./jacs.config.json')
    store.initialize()

    # Create document
    result = store.create({
        'type': 'note',
        'title': 'Meeting Notes',
        'content': 'Discussed project timeline...'
    })
    print(f"Created: {result['id']}")

    # Verify document
    verification = store.verify(result['id'])
    print(f"Valid: {verification['valid']}")

    # List all documents
    docs = store.list()
    print(f"Documents: {docs}")

Batch Processing

Batch Document Creator

import jacs
import json
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor

class BatchDocumentProcessor:
    def __init__(self, config_path: str):
        self.config_path = config_path

    def create_documents(self, documents: list, output_dir: str) -> list:
        """Create multiple signed documents"""
        output_path = Path(output_dir)
        output_path.mkdir(parents=True, exist_ok=True)

        results = []

        # Initialize agent
        agent = jacs.JacsAgent()
        agent.load(self.config_path)

        for i, content in enumerate(documents):
            try:
                signed_doc = agent.create_document(json.dumps(content))
                doc = json.loads(signed_doc)

                filename = f"{doc['jacsId']}.json"
                filepath = output_path / filename

                with open(filepath, 'w') as f:
                    f.write(signed_doc)

                results.append({
                    'success': True,
                    'index': i,
                    'id': doc['jacsId'],
                    'path': str(filepath)
                })
            except Exception as e:
                results.append({
                    'success': False,
                    'index': i,
                    'error': str(e)
                })

        return results

    def verify_documents(self, input_dir: str) -> list:
        """Verify all documents in a directory"""
        input_path = Path(input_dir)

        # Initialize agent
        agent = jacs.JacsAgent()
        agent.load(self.config_path)

        results = []

        for filepath in input_path.glob('*.json'):
            try:
                with open(filepath, 'r') as f:
                    doc_string = f.read()

                is_valid = agent.verify_document(doc_string)
                doc = json.loads(doc_string)

                results.append({
                    'file': filepath.name,
                    'valid': is_valid,
                    'id': doc.get('jacsId')
                })
            except Exception as e:
                results.append({
                    'file': filepath.name,
                    'valid': False,
                    'error': str(e)
                })

        return results

if __name__ == "__main__":
    processor = BatchDocumentProcessor('./jacs.config.json')

    # Create batch of documents
    documents = [
        {'type': 'invoice', 'number': f'INV-{i:03d}', 'amount': i * 100}
        for i in range(1, 11)
    ]

    results = processor.create_documents(documents, './batch-output')

    success_count = sum(1 for r in results if r['success'])
    print(f"Created {success_count}/{len(documents)} documents")

    # Verify all documents
    verification_results = processor.verify_documents('./batch-output')

    valid_count = sum(1 for r in verification_results if r['valid'])
    print(f"Valid: {valid_count}/{len(verification_results)} documents")

Testing

Pytest Setup

# tests/test_jacs.py
import pytest
import jacs
import json
import tempfile
import shutil
from pathlib import Path

@pytest.fixture
def jacs_agent():
    """Create a test JACS agent with temporary directories"""
    temp_dir = tempfile.mkdtemp()
    data_dir = Path(temp_dir) / 'data'
    key_dir = Path(temp_dir) / 'keys'

    data_dir.mkdir()
    key_dir.mkdir()

    config = {
        'jacs_data_directory': str(data_dir),
        'jacs_key_directory': str(key_dir),
        'jacs_agent_key_algorithm': 'ring-Ed25519',
        'jacs_default_storage': 'fs'
    }

    config_path = Path(temp_dir) / 'jacs.config.json'
    with open(config_path, 'w') as f:
        json.dump(config, f)

    agent = jacs.JacsAgent()
    agent.load(str(config_path))

    yield agent

    shutil.rmtree(temp_dir)

class TestDocumentOperations:
    def test_create_document(self, jacs_agent):
        content = {'title': 'Test Document', 'value': 42}
        signed_doc = jacs_agent.create_document(json.dumps(content))
        doc = json.loads(signed_doc)

        assert 'jacsId' in doc
        assert 'jacsVersion' in doc
        assert 'jacsSignature' in doc
        assert doc['title'] == 'Test Document'

    def test_verify_valid_document(self, jacs_agent):
        content = {'title': 'Verify Test'}
        signed_doc = jacs_agent.create_document(json.dumps(content))

        is_valid = jacs_agent.verify_document(signed_doc)
        assert is_valid is True

    def test_detect_tampered_document(self, jacs_agent):
        content = {'title': 'Tamper Test'}
        signed_doc = jacs_agent.create_document(json.dumps(content))

        # Tamper with document
        doc = json.loads(signed_doc)
        doc['title'] = 'Modified Title'
        tampered_doc = json.dumps(doc)

        is_valid = jacs_agent.verify_document(tampered_doc)
        assert is_valid is False

    def test_different_content_different_signatures(self, jacs_agent):
        doc1 = jacs_agent.create_document(json.dumps({'a': 1}))
        doc2 = jacs_agent.create_document(json.dumps({'a': 2}))

        parsed1 = json.loads(doc1)
        parsed2 = json.loads(doc2)

        sig1 = parsed1['jacsSignature']['signature']
        sig2 = parsed2['jacsSignature']['signature']

        assert sig1 != sig2

Error Handling

Robust Error Handling Pattern

import jacs
import json
from typing import Optional

class JacsError(Exception):
    def __init__(self, message: str, code: str, details: dict = None):
        super().__init__(message)
        self.code = code
        self.details = details or {}

def robust_create_document(config_path: str, content: dict) -> dict:
    """Create a document with comprehensive error handling"""
    try:
        agent = jacs.JacsAgent()
        agent.load(config_path)
    except FileNotFoundError:
        raise JacsError(
            "Configuration file not found",
            "CONFIG_NOT_FOUND",
            {"path": config_path}
        )
    except Exception as e:
        raise JacsError(
            "Failed to initialize JACS agent",
            "INIT_ERROR",
            {"original_error": str(e)}
        )

    try:
        signed_doc = agent.create_document(json.dumps(content))
        return json.loads(signed_doc)
    except Exception as e:
        raise JacsError(
            "Failed to create document",
            "CREATE_ERROR",
            {"original_error": str(e), "content": content}
        )

def robust_verify_document(config_path: str, doc_string: str) -> dict:
    """Verify a document with comprehensive error handling"""
    try:
        agent = jacs.JacsAgent()
        agent.load(config_path)
    except Exception as e:
        raise JacsError(
            "Failed to initialize JACS agent",
            "INIT_ERROR",
            {"original_error": str(e)}
        )

    try:
        is_valid = agent.verify_document(doc_string)
        return {"valid": is_valid}
    except Exception as e:
        raise JacsError(
            "Verification error",
            "VERIFY_ERROR",
            {"original_error": str(e)}
        )

if __name__ == "__main__":
    try:
        doc = robust_create_document('./jacs.config.json', {'title': 'Test'})
        print(f"Created: {doc['jacsId']}")
    except JacsError as e:
        print(f"JACS Error [{e.code}]: {e}")
        print(f"Details: {e.details}")
    except Exception as e:
        print(f"Unexpected error: {e}")

See Also

Integration Examples

This page is now a curated index of examples that still line up with the current APIs. The old monolithic example chapter mixed outdated agent APIs with supported workflows.

MCP

  • jacs mcp
    • Best starting point for the full Rust MCP server
  • jacspy/examples/mcp/server.py
    • Python FastMCP server wrapped with JACSMCPServer
  • jacspy/examples/mcp/client.py
    • Python FastMCP client wrapped with JACSMCPClient
  • jacsnpm/examples/mcp.stdio.server.js
    • Node stdio server with createJACSTransportProxy()
  • jacsnpm/examples/mcp.stdio.client.js
    • Node stdio client with signed transport

LangChain / LangGraph

  • jacspy/examples/langchain/signing_callback.py
    • Best current Python example for signed LangGraph tool execution
  • jacsnpm/examples/langchain/basic-agent.ts
    • Node LangChain.js agent using JACS tools
  • jacsnpm/examples/langchain/signing-callback.ts
    • Node auto-signing pattern for LangGraph-style flows

A2A

  • jacspy/tests/test_a2a_server.py
    • Best current Python reference for generated .well-known routes
  • jacsnpm/src/a2a-server.js
    • Node Express A2A discovery middleware
  • jacsnpm/examples/a2a-agent-example.js
    • Node A2A card and artifact demo
  • jacs/tests/a2a_cross_language_tests.rs
    • Cross-language behavior reference for signing and verification

HTTP / App Middleware

  • jacspy/examples/http/server.py
    • FastAPI app with JacsMiddleware
  • jacspy/examples/http/client.py
    • Python client consuming signed responses
  • jacsnpm/examples/expressmiddleware.js
    • Express middleware example

Rule Of Thumb

If an example and a higher-level prose page disagree, trust:

  1. the current binding README
  2. the current tests
  3. the example that imports the API you intend to use today

CLI Command Reference

This page provides a comprehensive reference for all JACS command-line interface commands. For a workflow-oriented tutorial, see CLI Tutorial. For practical scripting examples, see CLI Examples.

Global Commands

jacs version

Prints version and build information for the JACS installation.

jacs version

jacs quickstart

Create a persistent agent with keys on disk and optionally sign data -- no manual setup needed. If ./jacs.config.json already exists, loads it; otherwise creates a new agent. Agent, keys, and config are saved to ./jacs_data, ./jacs_keys, and ./jacs.config.json. Password is required: set JACS_PRIVATE_KEY_PASSWORD (recommended) or JACS_PASSWORD_FILE (CLI file bootstrap). Set exactly one explicit source; if both are set, CLI exits with an error. This is the fastest way to start using JACS.

# Print agent info (ID, algorithm)
jacs quickstart --name my-agent --domain my-agent.example.com

# Sign JSON from stdin
echo '{"action":"approve"}' | jacs quickstart --name my-agent --domain my-agent.example.com --sign

# Sign a file
jacs quickstart --name my-agent --domain my-agent.example.com --sign --file mydata.json

Options:

  • --name <name> - Agent name used for first-time quickstart creation (required)
  • --domain <domain> - Agent domain used for DNS/public-key verification workflows (required)
  • --algorithm <algo> - Signing algorithm: pq2025 (default) or ed25519; the legacy input alias ring-Ed25519 is also accepted and the canonical Ed25519 wire label remains ring-Ed25519
  • --sign - Sign input (from stdin or --file) instead of printing info
  • --file <path> - Read JSON input from file instead of stdin (requires --sign)

jacs verify

Verify a signed JACS document. No agent or config file required -- the CLI creates an ephemeral verifier if needed.

# Verify a local file
jacs verify signed-document.json

# JSON output (for scripting)
jacs verify signed-document.json --json

# Verify a remote document
jacs verify --remote https://example.com/signed-doc.json

# Specify a directory of public keys
jacs verify signed-document.json --key-dir ./trusted-keys/

Options:

  • <file> - Path to the signed JACS JSON file (positional, required unless --remote is used)
  • --remote <url> - Fetch document from URL before verifying
  • --json - Output result as JSON ({"valid": true, "signerId": "...", "timestamp": "..."})
  • --key-dir <dir> - Directory containing public keys for verification

Exit codes: 0 for valid, 1 for invalid or error.

Output (text):

Status:    VALID
Signer:    550e8400-e29b-41d4-a716-446655440000
Signed at: 2026-02-10T12:00:00Z

Output (JSON):

{
  "valid": true,
  "signerId": "550e8400-e29b-41d4-a716-446655440000",
  "timestamp": "2026-02-10T12:00:00Z"
}

If ./jacs.config.json and agent keys exist in the current directory, the CLI uses them automatically. Otherwise it creates a temporary ephemeral verifier internally.

See the Verification Guide for Python, Node.js, and DNS verification workflows.

jacs keychain

Manage private key passwords in the OS keychain (macOS Keychain or Linux Secret Service via D-Bus). This allows JACS to retrieve your private key password without environment variables or password files.

Requires the keychain feature (enabled by default in jacs-cli). Set JACS_KEYCHAIN_BACKEND=disabled to skip keychain lookups in CI/headless environments.

Every keychain command requires --agent-id so that each agent's password is stored separately. This prevents collisions when multiple agents coexist on the same machine.

# Store a password interactively (prompts for input)
jacs keychain set --agent-id <AGENT_UUID>

# Store a password non-interactively (for scripting)
jacs keychain set --agent-id <AGENT_UUID> --password "YourStr0ng!Pass#Here"

# Retrieve the stored password (prints to stdout, for piping)
export JACS_PRIVATE_KEY_PASSWORD=$(jacs keychain get --agent-id <AGENT_UUID>)

# Remove the stored password
jacs keychain delete --agent-id <AGENT_UUID>

# Check keychain availability and whether a password is stored
jacs keychain status --agent-id <AGENT_UUID>
SubcommandDescription
jacs keychain set --agent-id <ID>Store a password for an agent (prompts interactively)
jacs keychain set --agent-id <ID> --password <pw>Store a specific password (for scripting)
jacs keychain get --agent-id <ID>Print stored password to stdout
jacs keychain delete --agent-id <ID>Remove stored password from OS keychain
jacs keychain status --agent-id <ID>Check keychain availability and storage state

Output conventions: Human-friendly messages go to stderr; machine-friendly data goes to stdout. This means jacs keychain get output can be safely piped or captured in a variable.

Password resolution order: When JACS needs the private key password, it checks sources in this order:

  1. JACS_PRIVATE_KEY_PASSWORD environment variable (highest priority)
  2. JACS_PASSWORD_FILE / legacy .jacs_password file
  3. OS keychain keyed by agent ID (if keychain feature is enabled and not disabled)

jacs init

Initialize JACS by creating both configuration and agent (with cryptographic keys). Use this for persistent agent setup. New agents get the selected native root (pq2025 by default) plus an ES256 ecosystem compatibility key; pass --no-compat-key to skip the compatibility key (minimal/air-gapped agents).

jacs init
jacs init --no-compat-key

jacs help

Print help information for JACS commands.

jacs help [COMMAND]

Configuration Commands

jacs config

Work with JACS configuration settings.

jacs config [SUBCOMMAND]

Note: Specific subcommands for config are not detailed in the current help output.

Agent Commands

jacs agent

Work with JACS agents - the cryptographic identities that sign and verify documents.

jacs agent [SUBCOMMAND]

jacs agent add-compat-key

Add the ES256 ecosystem_signing compatibility key to an existing agent (explicit migration — loading never creates key material). New agents mint this key at creation unless --no-compat-key was used. Errors if the key already exists.

jacs agent add-compat-key [--config ./jacs.config.json]

jacs agent issue-compat-binding

Issue (or re-issue) the native-root-signed compatibility key binding. Content scopes (ap2-mandate, agreement-vc) are never auto-issued — grant them here explicitly. This is also the re-issue path after agent rotate-keys (a binding signed by a previous root no longer authorizes exports). The same grant is available from language bindings as issue_compat_binding_json(scopes_json, expires_at) (scopes as a JSON array, e.g. ["jwks","did","ap2-mandate"]), so Python/Node/Go callers can enable content exports without shelling out to the CLI.

# default: identity scopes only (jwks,did,a2a-agent-card,w3c-agent-identity)
jacs agent issue-compat-binding

# grant a content scope explicitly
jacs agent issue-compat-binding --scopes jwks,did,a2a-agent-card,w3c-agent-identity,ap2-mandate

# optional expiry (RFC 3339)
jacs agent issue-compat-binding --expires-at 2027-01-01T00:00:00Z

Agreement VC Export

jacs agreement-v2 export-vc

Export an Agreement-v2 document as a W3C Verifiable Credential with an ecdsa-jcs-2019 Data Integrity proof (W3C vc-di-ecdsa, Multikey verification method). Requires the agreement-vc binding scope. The native agreement is unchanged; incoming VC verification is out of scope.

jacs agreement-v2 export-vc --agreement agreement.json   # file path
cat agreement.json | jacs agreement-v2 export-vc --agreement -  # stdin

AP2 Commands

jacs ap2 export-mandate

Export the AP2 merchant-authorization mandate for a UCP checkout as a detached ES256 JWS (UCP AP2-Mandates extension, rev 2026-01-23). Requires the ap2-mandate binding scope. Input is validated against the named ap2-mandate schema; user-side AP2 Checkout Mandates (SD-JWT-VC) are out of scope. See AP2 Mandate Export.

jacs ap2 export-mandate --input checkout.json          # file path
jacs ap2 export-mandate --input '{"id":"c1", ... }'    # inline JSON
cat checkout.json | jacs ap2 export-mandate --input -  # stdin

Document Commands

The jacs document command provides comprehensive document management capabilities.

jacs document create

Create a new JACS document, either by embedding or parsing a document with optional file attachments.

Usage:

jacs document create [OPTIONS]

Options:

  • -a <agent-file> - Path to the agent file. If not specified, uses config jacs_agent_id_and_version
  • -f <filename> - Path to input file. Must be JSON format
  • -o, --output <output> - Output filename for the created document (cannot be combined with --directory)
  • -d <directory> - Path to directory of files. Files should end with .json
  • -v, --verbose - Enable verbose output
  • --json - Emit a machine-readable result containing each document key and saved path
  • -n, --no-save - Instead of saving files, print to stdout
  • -s, --schema <schema> - Path to JSON schema file to use for validation
  • --attach <attach> - Path to file or directory for file attachments
  • -e, --embed <embed> - Embed documents or keep them external [possible values: true, false]
  • -h, --help - Print help information

Examples:

# Create document from JSON file
jacs document create -f my-document.json
# Prints: Saved signed document: <actual path>

# Create document with embedded attachment
jacs document create -f document.json --attach ./image.jpg --embed true

# Create document with referenced attachment
jacs document create -f document.json --attach ./data.csv --embed false

# Create from directory of JSON files
jacs document create -d ./documents/

# Create with custom schema validation
jacs document create -f document.json -s custom-schema.json

# Print to stdout instead of saving
jacs document create -f document.json --no-save

jacs document update

Create a new version of an existing document. Requires both the original JACS file and the modified JACS metadata.

Usage:

jacs document update [OPTIONS]

Options:

  • -a <agent-file> - Path to the agent file
  • -f <filename> - Path to original document file
  • -n <new-file> - Path to new/modified document file
  • -o <output> - Output filename for updated document
  • -v, --verbose - Enable verbose output
  • -n, --no-save - Print to stdout instead of saving
  • -s, --schema <schema> - Path to JSON schema file for validation
  • --attach <attach> - Path to file or directory for additional attachments
  • -e, --embed <embed> - Embed new attachments or keep them external
  • -h, --help - Print help information

Example:

# Update document with new version
jacs document update -f original.json -n modified.json -o updated.json

# Update and add new attachments
jacs document update -f original.json -n modified.json --attach ./new-file.pdf --embed false

jacs document verify

Verify a document's hash, signatures, and schema compliance.

Usage:

jacs document verify [OPTIONS]

Options:

  • -a <agent-file> - Path to the agent file
  • -f <filename> - Path to input file. Must be JSON format
  • -d <directory> - Path to directory of files. Files should end with .json
  • -v, --verbose - Enable verbose output
  • -s, --schema <schema> - Path to JSON schema file to use for validation
  • -h, --help - Print help information

Examples:

# Verify single document
jacs document verify -f signed-document.json

# Verify all documents in directory
jacs document verify -d ./documents/

# Verify with custom schema
jacs document verify -f document.json -s custom-schema.json

Verification Process:

  1. Hash verification - Confirms document integrity
  2. Signature verification - Validates cryptographic signatures
  3. Schema validation - Ensures document structure compliance
  4. File integrity - Checks SHA256 checksums of attached files

jacs document extract

Extract embedded file contents from documents back to the filesystem.

Usage:

jacs document extract [OPTIONS]

Options:

  • -a <agent-file> - Path to the agent file
  • -f <filename> - Path to input file containing embedded files
  • -d <directory> - Path to directory of files to process
  • -s, --schema <schema> - Path to JSON schema file for validation
  • -h, --help - Print help information

Examples:

# Extract embedded files from single document
jacs document extract -f document-with-embedded-files.json

# Extract from all documents in directory  
jacs document extract -d ./documents/

Extract Process:

  1. Reads embedded file contents from document
  2. Decodes base64-encoded data
  3. Writes files to their original paths
  4. Creates backup of existing files (with timestamp)

Agreement Commands

JACS provides specialized commands for managing multi-agent agreements. Prefer jacs agreement-v2 for new standalone consent workflows. The jacs document *-agreement commands below are legacy sidecar commands for simple approval of an existing signed document.

jacs document check-agreement

Given a document, provide a list of agents that should sign the document.

Usage:

jacs document check-agreement [OPTIONS]

jacs document create-agreement

Create an agreement structure for a document that requires multiple agent signatures.

Usage:

jacs document create-agreement [OPTIONS]

jacs document sign-agreement

Sign the agreement section of a document with the current agent's cryptographic signature.

Usage:

jacs document sign-agreement [OPTIONS]

jacs agreement-v2

Standalone agreement v2 documents are the recommended consent workflow. Each is a self-contained, cryptographically signed jacsType: "agreement" artifact — terms, parties, signature policy (quorum, required roles), an optional transcript, links, controllers, and owners — with its own content hash and version chain. Running jacs agreement-v2 with no subcommand prints help and exits non-zero.

Typical order: create → sign → verify. Use apply to evolve an agreement via a typed mutation; use the branch commands when two agents diverge from the same prior version.

jacs agreement-v2 create

Create a standalone agreement v2 document from a CreateAgreementV2 JSON input.

jacs agreement-v2 create --input <CreateAgreementV2 JSON | path | ->

jacs agreement-v2 sign

Add this agent's signature as a signer, witness, or notary.

jacs agreement-v2 sign --agreement <JSON | path | -> [--role signer|witness|notary]

--role defaults to signer.

jacs agreement-v2 verify

Recompute the agreement and transcript hashes and re-check quorum, roles, status, and every signature. Exits non-zero when the agreement is invalid.

jacs agreement-v2 verify --agreement <JSON | path | ->

jacs agreement-v2 apply

Apply a typed mutation (appendTranscript, updateTerms, setStatus, addLink, setParties, setPolicy, setOwners) and emit a successor version. Mutation fields are camelCase.

jacs agreement-v2 apply --agreement <JSON | path | -> --mutation <mutation JSON | path>

jacs agreement-v2 detect-conflict

Report whether two successor versions of --base are transcript-only (auto-mergeable) or conflict on consent-scope fields.

jacs agreement-v2 detect-conflict --base <common ancestor> --left <branch> --right <branch>

jacs agreement-v2 merge-transcript

Auto-merge two transcript-only branches of --base into one successor version.

jacs agreement-v2 merge-transcript --base <common ancestor> --left <branch> --right <branch>

jacs agreement-v2 resolve-conflict

Resolve a consent-scope conflict by rebasing an explicit mutation onto --previous; the divergent --side-branch is recorded as a link on the result.

jacs agreement-v2 resolve-conflict \
  --base <common ancestor> \
  --previous <branch you keep> \
  --side-branch <divergent branch> \
  --mutation <resolving mutation JSON | path>

--side-branch replaces the older --side flag; --side still works as a hidden alias for backward compatibility.

Provenance Commands

The provenance commands sign and verify content in place — text files keep a YAML-bodied signature block at the end of the file, images carry the signature in a metadata chunk. See the Inline Text Signatures and Image and Media Signatures guides for the full feature set.

jacs sign-text

Append a YAML-bodied JACS signature block to the end of a markdown or text file. The file content (everything before the first signature block) is preserved byte-for-byte.

Usage:

jacs sign-text <file>

The command refuses (hard error) to sign input that contains a column-zero -----BEGIN JACS SIGNATURE----- line outside a valid block — see the inline-text-signing guide for documented workarounds.

jacs verify-text

Verify all signature blocks in a text file. Permissive by default — missing signatures are exit code 2, not a thrown error. Per-signer status is printed to stdout.

Usage:

jacs verify-text <file> [--strict] [--key-dir <dir>]

Options:

  • --strict — treat missing signature as exit 1 (instead of 2); stderr: no JACS signature found.
  • --key-dir <dir> — directory of <signer_id>.public.pem files used in addition to the local trust store.

jacs sign-image

Embed a JACS signature inside a PNG, JPEG, or WebP image. The signature lives in a metadata chunk (PNG iTXt / JPEG APP11 / WebP XMP).

Usage:

jacs sign-image <input> --out <output> [--robust] [--refuse-overwrite]

Options:

  • --out <path> — output file (required).
  • --robust — opt-in LSB embedding fallback for PNG/JPEG (off by default; WebP deferred).
  • --refuse-overwrite — exit non-zero if the input already carries a JACS signature (default is overwrite).

jacs verify-image

Verify the embedded signature on an image. Same permissive / strict model as verify-text.

Usage:

jacs verify-image <file> [--strict] [--key-dir <dir>]

jacs extract-media-signature

Extract the embedded signature payload without verifying it.

Usage:

jacs extract-media-signature <file> [--raw-payload]

Options:

  • --raw-payload — emit the base64url wire form instead of decoded JSON.

Common Patterns

Basic Document Lifecycle

# 1. Initialize JACS
jacs init

# 2. Create document with attachments
jacs document create -f document.json --attach ./files/ --embed true

# 3. Verify document integrity
jacs document verify -f created-document.json

# 4. Update document if needed
jacs document update -f original.json -n modified.json

# 5. Extract embedded files when needed
jacs document extract -f document.json

Working with Attachments

# Embed small files for portability
jacs document create -f doc.json --attach ./small-image.png --embed true

# Reference large files to save space
jacs document create -f doc.json --attach ./large-video.mp4 --embed false

# Attach multiple files from directory
jacs document create -f doc.json --attach ./attachments/ --embed false

Schema Validation Workflow

# Create with schema validation
jacs document create -f document.json -s schema.json

# Verify against specific schema
jacs document verify -f document.json -s schema.json

Global Options

Most commands support these common options:

  • -h, --help - Show help information
  • -v, --verbose - Enable verbose output for debugging
  • -a <agent-file> - Specify custom agent file (overrides config default)

Exit Codes

  • 0 - Success
  • 1 - General error (invalid arguments, file not found, etc.)
  • 2 - Missing signature OR verification failure — exact meaning per-command in its section. For verify-text / verify-image (v0.10.0): 2 = permissive missing-signature; --strict collapses it into 1. For other verify surfaces: 2 = verification failure (hash mismatch, invalid signature).
  • 3 - Schema validation failure

Environment Variables

  • JACS_CONFIG_PATH - Override default configuration file location
  • JACS_DATA_DIR - Override default data directory location
  • JACS_AGENT_FILE - Default agent file to use (if not specified with -a)

File Formats

Input Files

  • JSON documents - Must be valid JSON format
  • Schema files - JSON Schema format (draft-07 compatible)
  • Agent files - JACS agent format with cryptographic keys
  • Attachments - Any file type (automatically detected MIME type)

Output Files

  • JACS documents - JSON format with JACS metadata, signatures, and checksums
  • Extracted files - Original format of embedded attachments

Configuration Reference

This is the comprehensive configuration guide covering zero-config quickstart, storage backends, observability, and environment variables. For the raw schema field list, see Config File Schema.

Overview

Key resolution for verifiers

When verifying signed documents, JACS resolves the signer’s public key using a configurable order of sources. Set JACS_KEY_RESOLUTION (environment variable or in config) to a comma-separated list of sources: local (local key cache by publicKeyHash), dns (DNS TXT fingerprint validation), hai (HAI key service). Example: JACS_KEY_RESOLUTION=local,hai or local,dns,hai. The first source that yields verifiable key material is used. Use verify_standalone() with explicit keyResolution for one-off verification without loading a full config.

Zero-Config Path

If you just want to sign and verify without manual config setup, use quickstart(name, domain, ...):

import jacs.simple as jacs
info = jacs.quickstart(name="config-agent", domain="config.example.com")
print(info.config_path, info.public_key_path, info.private_key_path)
const jacs = require('@hai.ai/jacs/simple');
const info = await jacs.quickstart({
  name: 'config-agent',
  domain: 'config.example.com',
});
console.log(info.configPath, info.publicKeyPath, info.privateKeyPath);
jacs quickstart --name config-agent --domain config.example.com

quickstart(name, domain, ...) creates a persistent agent with keys on disk (default algorithm: pq2025). If ./jacs.config.json already exists, it loads it; otherwise it creates a new agent. Returned AgentInfo includes config/key paths (config_path/public_key_path/private_key_path in Python, configPath/publicKeyPath/privateKeyPath in Node) plus key directory metadata so you can locate key material immediately. Rust/CLI quickstart requires an explicit password source (JACS_PRIVATE_KEY_PASSWORD, or JACS_PASSWORD_FILE in CLI). Python/Node can auto-generate a password if needed; set JACS_SAVE_PASSWORD_FILE=true if you want that generated password persisted to ./jacs_keys/.jacs_password.

Minimal Configuration

For persistent agents, a config file needs only two fields (plus $schema):

{
  "$schema": "https://hai.ai/schemas/jacs.config.schema.json",
  "jacs_agent_id_and_version": "YOUR_AGENT_ID:YOUR_VERSION",
  "jacs_agent_key_algorithm": "pq2025"
}

All other settings use sensible defaults (./jacs_data, ./jacs_keys, fs storage). Override only what you need.

Signed-config integrity

Persistent agent configs are verified before JACS applies any config-controlled key path, storage backend, database URL, or network setting. A bad hash, signature, signer/identity mismatch, algorithm mismatch, unsafe key path, or invalid rotation journal stops the load; JACS does not continue with the tampered settings.

An unsigned file may be used as a template while creating a new identity. Loading an existing identity from an unsigned file is rejected by default. For a one-time, audited legacy migration only, set JACS_ALLOW_UNSIGNED_AGENT_CONFIG=true, load and immediately re-sign the config, then remove the variable. This compatibility path emits a security_outcome WARN event and increments jacs_security_outcomes_total with source="config", outcome="unverified", and policy="permissive".

Config signatures prove integrity relative to the locally resolved signing key. They do not by themselves prove a real-world identity; deployments that need that guarantee must also pin or independently trust the agent key and protect the config/key directories from replacement.

Complete Example Configuration

{
  "$schema": "https://hai.ai/schemas/jacs.config.schema.json",
  "jacs_use_security": "false",
  "jacs_data_directory": "./jacs_data",
  "jacs_key_directory": "./jacs_keys",
  "jacs_agent_private_key_filename": "jacs.private.pem.enc",
  "jacs_agent_public_key_filename": "jacs.public.pem",
  "jacs_agent_key_algorithm": "pq2025",
  "jacs_default_storage": "fs",
  "observability": {
    "logs": {
      "enabled": true,
      "level": "info",
      "destination": {
        "type": "file",
        "path": "./logs"
      },
      "headers": {
        "Authorization": "Bearer token",
        "X-API-Key": "secret"
      }
    },
    "metrics": {
      "enabled": true,
      "destination": {
        "type": "prometheus",
        "endpoint": "http://localhost:9090/api/v1/write",
        "headers": {
          "Authorization": "Basic dXNlcjpwYXNz"
        }
      },
      "export_interval_seconds": 60,
      "headers": {
        "X-Service": "jacs"
      }
    },
    "tracing": {
      "enabled": true,
      "sampling": {
        "ratio": 0.1,
        "parent_based": true,
        "rate_limit": 100
      },
      "resource": {
        "service_name": "jacs",
        "service_version": "0.4.0",
        "environment": "production",
        "attributes": {
          "team": "platform",
          "region": "us-west-2"
        }
      }
    }
  }
}

Observability Configuration

JACS supports comprehensive observability through configurable logging, metrics, and tracing. All observability features are optional and can be configured in the jacs.config.json file.

For the P2 compatibility/export WARN events and their jacs_*_total Prometheus counters (what to alert on and the likely fix), see Failure Modes.

Logs Configuration

Controls how JACS generates and outputs log messages.

FieldTypeRequiredDescription
enabledbooleanYesWhether logging is enabled
levelstringYesMinimum log level: trace, debug, info, warn, error
destinationobjectYesWhere logs are sent (see destinations below)
headersobjectNoAdditional headers for remote destinations

Log Destinations

File Logging

{
  "type": "file",
  "path": "./logs"
}

Writes logs to rotating files in the specified directory.

Console Logging (stderr)

{
  "type": "stderr"
}

Outputs logs to standard error stream.

OpenTelemetry Protocol (OTLP)

{
  "type": "otlp",
  "endpoint": "http://localhost:4317",
  "headers": {
    "Authorization": "Bearer token"
  }
}

Sends logs to an OTLP-compatible endpoint (like Jaeger, Grafana Cloud).

Null (disabled)

{
  "type": "null"
}

Discards all log output.

Metrics Configuration

Controls collection and export of application metrics.

FieldTypeRequiredDescription
enabledbooleanYesWhether metrics collection is enabled
destinationobjectYesWhere metrics are exported (see destinations below)
export_interval_secondsintegerNoHow often to export metrics (default: 60)
headersobjectNoAdditional headers for remote destinations

Metrics Destinations

Prometheus Remote Write

{
  "type": "prometheus",
  "endpoint": "http://localhost:9090/api/v1/write",
  "headers": {
    "Authorization": "Basic dXNlcjpwYXNz"
  }
}

Exports metrics in Prometheus format to a remote write endpoint.

OpenTelemetry Protocol (OTLP)

{
  "type": "otlp",
  "endpoint": "http://localhost:4317",
  "headers": {
    "Authorization": "Bearer token"
  }
}

Exports metrics to an OTLP-compatible endpoint.

File Export

{
  "type": "file",
  "path": "./metrics.txt"
}

Writes metrics to a local file.

Console Output (stdout)

{
  "type": "stdout"
}

Prints metrics to standard output.

Tracing Configuration

Controls distributed tracing for request flows.

FieldTypeRequiredDescription
enabledbooleanYesWhether tracing is enabled
samplingobjectNoSampling configuration (see below)
resourceobjectNoService identification (see below)

Sampling Configuration

Controls which traces are collected to manage overhead.

FieldTypeDefaultDescription
rationumber1.0Fraction of traces to sample (0.0-1.0)
parent_basedbooleantrueWhether to respect parent trace sampling decisions
rate_limitintegernoneMaximum traces per second

Examples:

  • "ratio": 1.0 - Sample all traces (100%)
  • "ratio": 0.1 - Sample 10% of traces
  • "ratio": 0.01 - Sample 1% of traces
  • "rate_limit": 10 - Maximum 10 traces per second

Resource Configuration

Identifies the service in distributed tracing systems.

FieldTypeRequiredDescription
service_namestringYesName of the service
service_versionstringNoVersion of the service
environmentstringNoEnvironment (dev, staging, prod)
attributesobjectNoCustom key-value attributes

Authentication & Headers

For remote destinations (OTLP, Prometheus), you can specify authentication headers:

Bearer Token Authentication:

"headers": {
  "Authorization": "Bearer your-token-here"
}

Basic Authentication:

"headers": {
  "Authorization": "Basic dXNlcjpwYXNz"
}

API Key Authentication:

"headers": {
  "X-API-Key": "your-api-key",
  "X-Auth-Token": "your-auth-token"
}

Common Patterns

Development Configuration

"observability": {
  "logs": {
    "enabled": true,
    "level": "debug",
    "destination": { "type": "stderr" }
  },
  "metrics": {
    "enabled": true,
    "destination": { "type": "stdout" }
  }
}

Production Configuration

"observability": {
  "logs": {
    "enabled": true,
    "level": "info",
    "destination": {
      "type": "otlp",
      "endpoint": "https://logs.example.com:4317",
      "headers": {
        "Authorization": "Bearer prod-token"
      }
    }
  },
  "metrics": {
    "enabled": true,
    "destination": {
      "type": "prometheus",
      "endpoint": "https://metrics.example.com/api/v1/write"
    },
    "export_interval_seconds": 30
  },
  "tracing": {
    "enabled": true,
    "sampling": {
      "ratio": 0.05,
      "rate_limit": 100
    },
    "resource": {
      "service_name": "jacs",
      "service_version": "0.4.0",
      "environment": "production"
    }
  }
}

File-based Configuration

"observability": {
  "logs": {
    "enabled": true,
    "level": "info",
    "destination": {
      "type": "file",
      "path": "/var/log/jacs"
    }
  },
  "metrics": {
    "enabled": true,
    "destination": {
      "type": "file",
      "path": "/var/log/jacs/metrics.txt"
    },
    "export_interval_seconds": 60
  }
}

Environment Variable Integration

The observability configuration works alongside JACS's core configuration system.

Required Environment Variable

Only one environment variable is truly required (unless using the OS keychain):

  • JACS_PRIVATE_KEY_PASSWORD - Password for encrypting/decrypting private keys (required for cryptographic operations, unless the password is stored in the OS keychain)

Configuration-Based Settings

All other JACS settings are configuration file fields that have sensible defaults:

  • jacs_data_directory - Where agent/document data is stored (default: ./jacs_data)
  • jacs_key_directory - Where cryptographic keys are stored (default: ./jacs_keys)
  • jacs_agent_key_algorithm - The agent's native signing algorithm (default: pq2025). New creation honors pq2025, ed25519, or the legacy input alias ring-Ed25519, then records the canonical value actually minted (pq2025 or ring-Ed25519). Rotation preserves this value unless an Ed25519 identity explicitly upgrades to pq2025; PQ-to-Ed25519 downgrade is rejected.
  • jacs_default_storage - Storage backend (default: fs)
  • jacs_keychain_backend - OS keychain backend for password storage (default: "auto"). See below.
  • jacs_use_security / JACS_ENABLE_FILESYSTEM_QUARANTINE - Enable filesystem quarantine of executable files (default: false). The env var JACS_USE_SECURITY is deprecated; use JACS_ENABLE_FILESYSTEM_QUARANTINE instead.

OS Keychain Configuration

The jacs_keychain_backend field controls whether JACS looks up the private key password from the OS credential store (macOS Keychain or Linux Secret Service):

{
  "jacs_keychain_backend": "auto"
}
ValueDescription
"auto"Detect the platform default (macOS Keychain or Linux Secret Service). This is the default when the field is omitted.
"macos-keychain"Explicitly use the macOS Keychain
"linux-secret-service"Explicitly use the Linux D-Bus Secret Service (GNOME Keyring, KDE Wallet, KeePassXC)
"disabled"Never consult the OS keychain. Recommended for CI, headless servers, and containers.

You can also override via environment variable:

export JACS_KEYCHAIN_BACKEND=disabled  # skip keychain in CI

When not set to "disabled", password resolution checks: env var first, then OS keychain, then error. See the CLI keychain commands for storing and managing passwords.

These can be overridden by environment variables if needed, but they are primarily configured through the jacs.config.json file.

The observability configuration is completely optional - JACS will work without any observability configuration.

Storage Configuration

The jacs_default_storage field determines where JACS stores agent data, documents, and keys. This is a critical configuration that affects how your data is persisted and accessed.

Available Storage Backends

BackendValueDescriptionUse Case
Filesystem"fs"Signed JSON documents on local diskDefault, development, single-node deployments
Local Indexed SQLite"rusqlite"Signed documents in SQLite with FTS searchLocal search, bindings, MCP
AWS S3"aws"Amazon S3 object storage (requires the Rust s3 feature)Remote object storage
Memory"memory"In-memory object storage (non-persistent)Testing, temporary data
Web Local"local"Browser local storage (WASM only)Web applications

For local indexed document search in JACS core, use "rusqlite". Additional database backends such as PostgreSQL, DuckDB, Redb, and SurrealDB live in separate crates and are not core jacs_default_storage values.

Backend-Specific Configuration

Filesystem Storage ("fs")

{
  "jacs_default_storage": "fs",
  "jacs_data_directory": "./jacs_data",
  "jacs_key_directory": "./jacs_keys"
}

Requirements: None - works out of the box Data location: Local directories as specified in config Best for: Development, local testing, single-machine deployments

Local Indexed SQLite ("rusqlite")

{
  "jacs_default_storage": "rusqlite",
  "jacs_data_directory": "./jacs_data",
  "jacs_key_directory": "./jacs_keys"
}

Requirements: Built with the default sqlite Cargo feature Database path: <jacs_data_directory>/jacs_documents.sqlite3 Best for: Local full-text search, MCP/binding document operations, single-machine deployments that want indexed reads

AWS S3 Storage ("aws")

AWS support is not compiled into the default JACS build. Enable it explicitly:

cargo add jacs --features s3

Without that feature, selecting "aws" fails with an actionable configuration error instead of silently activating the cloud dependency stack.

{
  "jacs_default_storage": "aws"
}

Required Environment Variables:

  • JACS_ENABLE_AWS_BUCKET_NAME - S3 bucket name
  • AWS_ACCESS_KEY_ID - AWS access key
  • AWS_SECRET_ACCESS_KEY - AWS secret key
  • AWS_REGION - AWS region (optional, defaults to us-east-1)

Best for: Production deployments, distributed systems, cloud-native applications

Memory Storage ("memory")

{
  "jacs_default_storage": "memory"
}

Requirements: None Data persistence: None - data is lost when application stops Best for: Unit testing, temporary operations, development scenarios

Storage Behavior

  • Filesystem (fs) stores signed documents as JSON files under jacs_data/documents/.
  • Rusqlite (rusqlite) stores signed documents in jacs_data/jacs_documents.sqlite3 and is the indexed DocumentService path used by bindings and MCP.
  • Agent files and keys remain path-based assets under jacs_data/ and jacs_keys/.
  • Observability data (logs, metrics) can use separate storage via observability configuration.

DocumentService Guarantees

  • Every DocumentService read verifies the stored JACS document before returning it.
  • Every create() and update() verifies the signed document before persisting it.
  • If an update payload changes a signed JACS document without re-signing it, the write fails.
  • Visibility changes create a new signed version instead of mutating metadata in place.

Configuration Examples

Development Setup (Filesystem)

{
  "jacs_default_storage": "fs",
  "jacs_data_directory": "./dev_data",
  "jacs_key_directory": "./dev_keys"
}

Production Setup (AWS S3)

Build the application with jacs's s3 feature before selecting this backend.

{
  "jacs_default_storage": "aws"
}

With environment variables:

export JACS_ENABLE_AWS_BUCKET_NAME="my-jacs-production-bucket"
export AWS_ACCESS_KEY_ID="AKIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_REGION="us-west-2"

Security Considerations

  • AWS S3: Ensure proper IAM permissions for bucket access
  • Rusqlite: Protect the local database file with the same filesystem permissions you use for other signed artifacts
  • Filesystem: Ensure proper file system permissions for data and key directories
  • Keys: Regardless of storage backend, always set JACS_PRIVATE_KEY_PASSWORD for key encryption

Migration Between Storage Backends

When changing storage backends, you'll need to:

  1. Export existing data from the current backend
  2. Update the jacs_default_storage configuration
  3. Set any required environment variables for the new backend
  4. Import data into the new backend

JACS doesn't automatically migrate data between storage backends - this must be done manually or via custom scripts.

Error Codes

This reference documents error codes and messages you may encounter when using JACS.

CLI Exit Codes

CodeNameDescription
0SuccessOperation completed successfully
1General ErrorUnspecified error occurred (in verify-text / verify-image --strict, this also covers the missing-signature case; stderr: no JACS signature found)
2Invalid Arguments OR Missing Signature (v0.10.0)Command-line arguments invalid; OR for permissive verify-text / verify-image, the file is unsigned (typed status, not an error)
3File Not FoundSpecified file does not exist
4Verification FailedDocument or signature verification failed
5Signature InvalidCryptographic signature is invalid

Configuration Errors

Missing Configuration

Error: Configuration file not found: jacs.config.json

Cause: JACS cannot find the configuration file.

Solution:

# Initialize JACS to create configuration
jacs init

# Or specify a custom config path
JACS_CONFIG_PATH=./custom.config.json jacs agent verify

Invalid Configuration

Error: Invalid configuration: missing required field 'jacs_key_directory'

Cause: Configuration file is missing required fields.

Solution: Ensure your jacs.config.json contains all required fields:

{
  "jacs_data_directory": "./jacs_data",
  "jacs_key_directory": "./jacs_keys",
  "jacs_agent_key_algorithm": "ring-Ed25519",
  "jacs_default_storage": "fs"
}

Key Directory Not Found

Error: Key directory not found: ./jacs_keys

Cause: The specified key directory does not exist.

Solution:

# Create the directory
mkdir -p ./jacs_keys

# Or run init to create everything
jacs init

Cryptographic Errors

Private Key Not Found

Error: Private key file not found: private.pem

Cause: The private key file is missing from the key directory.

Solution:

# Generate new keys
jacs agent create --create-keys true

Invalid Key Format

Error: Failed to parse private key: invalid PEM format

Cause: The key file is corrupted or in wrong format.

Solution:

  • Regenerate keys with jacs agent create --create-keys true
  • Ensure the key file is not corrupted

Key Password Required

Error: Private key is encrypted but no password provided

Cause: Encrypted private key requires password.

Solution:

export JACS_PRIVATE_KEY_PASSWORD="your-password"
jacs document create -f doc.json

Algorithm Mismatch

Error: Key algorithm 'ring-Ed25519' does not match configured algorithm 'pq2025'

Cause: The key file was created with a different algorithm than configured.

Solution:

  • Update config to match key algorithm, or
  • Regenerate keys with the correct algorithm

Signature Errors

Verification Failed

Error: Document verification failed: signature does not match content

Cause: Document content has been modified after signing.

Solution:

  • The document may have been tampered with
  • Re-sign the document if you have the original content

Missing jacsSignature Field

Error: Document missing jacsSignature field

Cause: Document was not signed or signature was removed.

Solution:

# Create a signed document
jacs document create -f unsigned-doc.json

Missing Signature (v0.10.0)

Error / Status: MissingSignature — surfaced by verify-text / verify-image (and the corresponding binding methods) when the input file does not carry an embedded JACS signature.

Default behaviour is permissive — missing signature is a typed status, not a thrown error. Strict mode opts in to treating it as an error.

SurfacePermissive (default)Strict (--strict / strict=True / { strict: true } / Strict: true)
CLIExit code 2; stdout reports missing_signatureExit code 1; stderr: no JACS signature found
Python (jacs.simple / JacsClient)Returns result with status == "missing_signature"Raises MissingSignatureError
Node (@hai.ai/jacs/simple / JacsClient)Returns result with status === "missing_signature"Promise rejects with error message containing MissingSignature
Go (jacsgo)Returns result with Status == "missing_signature"Returns error matching errors.Is(err, jacs.ErrMissingSignature)
Rust (jacs::text / jacs::media)Ok(VerifyResult { status: Status::MissingSignature, .. })Err(JacsError::new(ErrorKind::MissingSignature, ..))

When to pick strict. Pick strict when a missing signature is genuinely a failure (release-gate, post-merge audit, CI on a branch that should always be signed). Pick permissive when verification runs against files that may legitimately be unsigned (the first PR introducing a signature).

Solution: sign the file, or — if missing signatures are expected — switch the call site to permissive mode and inspect result.status.

Invalid Signature Format

Error: Invalid signature format: expected base64 encoded string

Cause: The signature field is malformed.

Solution:

  • Re-sign the document
  • Verify the document hasn't been corrupted

Unknown Signing Algorithm

Error: Unknown signing algorithm: unknown-algo

Cause: Document was signed with an unsupported algorithm.

Solution:

  • Use a supported user-facing algorithm for new keys: ed25519, pq2025 (ring-Ed25519 remains a legacy input and canonical wire label)

DNS Verification Errors

DNSSEC Validation Failed

Error: strict DNSSEC validation failed for <owner> (TXT not authenticated). Enable DNSSEC and publish DS at registrar

Cause: DNSSEC mode was requested but the TXT response wasn't authenticated.

Solution:

  1. Enable DNSSEC for your domain zone
  2. Publish the DS record at your registrar
  3. Wait for propagation (up to 48 hours)

DNS Record Not Found

Error: DNS TXT lookup failed for <owner> (record missing or not yet propagated)

Cause: The JACS TXT record doesn't exist or hasn't propagated.

Solution:

  1. Verify the TXT record was created:
    dig _v1.agent.jacs.yourdomain.com TXT
    
  2. Wait for DNS propagation (can take up to 48 hours)
  3. Confirm record name and value are correct

DNS Required

Error: DNS TXT lookup required (domain configured) or provide embedded fingerprint

Cause: Strict DNS mode is active because a domain is configured.

Solution:

  • Publish the TXT record, or
  • Run with --no-dns during initial setup:
    jacs agent verify --no-dns
    

DNS Lookup Timeout

Error: DNS lookup timed out for <domain>

Cause: DNS server did not respond in time.

Solution:

  • Check network connectivity
  • Try again later
  • Verify DNS server is accessible

Document Errors

Invalid JSON

Error: Failed to parse document: invalid JSON at line 5

Cause: Document file contains invalid JSON.

Solution:

  • Validate JSON with a linter
  • Check for syntax errors (missing commas, quotes)

Schema Validation Failed

Error: Schema validation failed: missing required field 'amount'

Cause: Document doesn't conform to the specified schema.

Solution:

# Check which fields are required by the schema
cat schema.json | jq '.required'

# Add missing fields to your document

Document Not Found

Error: Document not found: 550e8400-e29b-41d4-a716-446655440000

Cause: The specified document ID doesn't exist in storage.

Solution:

  • Verify the document ID is correct
  • Check the storage directory

Version Mismatch

Error: Document version mismatch: expected v2, got v1

Cause: Attempting to update with incorrect base version.

Solution:

  • Get the latest version of the document
  • Apply updates to the correct version

Agreement Errors

Agreement Not Found

Error: Document has no jacsAgreement field

Cause: Attempting agreement operations on a document without an agreement.

Solution:

# Create an agreement first
jacs document create-agreement -f doc.json -i agent1-id,agent2-id

Already Signed

Error: Agent has already signed this agreement

Cause: Attempting to sign an agreement that was already signed by this agent.

Solution:

  • No action needed, the signature is already present

Not Authorized

Error: Agent is not in the agreement's agentIDs list

Cause: Attempting to sign with an agent not listed in the agreement.

Solution:

  • Only agents listed in jacsAgreement.agentIDs can sign

Agreement Locked

Error: Cannot modify document: agreement is complete

Cause: Attempting to modify a document with a completed agreement.

Solution:

  • Create a new version/agreement if changes are needed

Storage Errors

Storage Backend Error

Error: Storage error: failed to write to filesystem

Cause: Unable to write to the configured storage backend.

Solution:

  • Check filesystem permissions
  • Verify storage directory exists
  • Check disk space

AWS S3 Error

Error: S3 error: AccessDenied

Cause: AWS credentials don't have required permissions.

Solution:

  • Verify IAM permissions include s3:GetObject, s3:PutObject
  • Check bucket policy
  • Verify credentials are correct

Connection Error

Error: Failed to connect to storage: connection refused

Cause: Cannot connect to remote storage backend.

Solution:

  • Check network connectivity
  • Verify endpoint URL is correct
  • Check firewall rules

HTTP/MCP Errors

Request Verification Failed

Error: JACS request verification failed

Cause: Incoming HTTP request has invalid JACS signature.

Solution:

  • Ensure client is signing requests correctly
  • Verify client and server are using compatible keys

Response Verification Failed

Error: JACS response verification failed

Cause: Server response has invalid signature.

Solution:

  • Check server JACS configuration
  • Verify server is signing responses

Middleware Configuration Error

Error: JACSExpressMiddleware: config file not found

Cause: Middleware cannot find JACS configuration.

Solution:

app.use('/api', JACSExpressMiddleware({
  configPath: './jacs.config.json'  // Verify path is correct
}));

Debugging Tips

Enable Verbose Output

# CLI verbose mode
jacs document verify -f doc.json -v

# Environment variable
export JACS_DEBUG=true

Check Configuration

# Display current configuration
jacs config read

Verify Agent

# Verify agent is properly configured
jacs agent verify -v

Test Signing

# Create a test document
echo '{"test": true}' > test.json
jacs document create -f test.json -v

See Also

Attestation Verification Results

This reference explains every field in the AttestationVerificationResult returned by verify_attestation() and verify_attestation_full().

Result Structure

{
  "valid": true,
  "crypto": {
    "signature_valid": true,
    "hash_valid": true
  },
  "evidence": [
    {
      "kind": "custom",
      "digest_valid": true,
      "freshness_valid": true,
      "errors": []
    }
  ],
  "chain": {
    "depth": 1,
    "all_links_valid": true,
    "links": []
  },
  "errors": []
}

Top-Level Fields

FieldTypeDescription
validbooleanOverall result. true only if all sub-checks pass.
cryptoobjectCryptographic verification results.
evidencearrayPer-evidence-ref verification results (full tier only).
chainobject|nullDerivation chain verification (full tier only, if derivation exists).
errorsarrayHuman-readable error messages for any failures.

crypto Object

FieldTypeDescription
signature_validbooleanThe cryptographic signature matches the document content and the signer's public key.
hash_validbooleanThe jacsSha256 hash matches the canonicalized document content.

Common failure scenarios:

  • signature_valid: false -- The document was tampered with after signing, or the wrong public key was used.
  • hash_valid: false -- The document body was modified after the hash was computed.

evidence Array (Full Tier Only)

Each entry corresponds to one evidence reference in the attestation's evidence array.

FieldTypeDescription
kindstringEvidence type (a2a, email, jwt, tlsnotary, custom).
digest_validbooleanThe evidence digest matches the expected value.
freshness_validbooleanThe collectedAt timestamp is within acceptable bounds.
errorsarrayError messages specific to this evidence item.

Common failure scenarios:

  • digest_valid: false -- The evidence content has changed since the attestation was created.
  • freshness_valid: false -- The evidence is too old. Check collectedAt and your freshness policy.

chain Object (Full Tier Only)

Present only when the attestation has a derivation field.

FieldTypeDescription
depthnumberNumber of links in the derivation chain.
all_links_validbooleanEvery derivation link verified successfully.
linksarrayPer-link verification details.

Each link in links:

FieldTypeDescription
input_digests_validbooleanInput digests match the referenced documents.
output_digests_validbooleanOutput digests match the transformation result.
transformobjectTransform metadata (name, hash, reproducible).

Verification Tiers

Local Tier (verify_attestation())

  • Checks: crypto.signature_valid + crypto.hash_valid
  • Speed: < 1ms typical
  • Network: None
  • Use for: Real-time validation, hot path

Full Tier (verify_attestation(full=True))

  • Checks: Everything in local + evidence digests + freshness + derivation chain
  • Speed: < 10ms typical (no network), varies with evidence count
  • Network: Optional (for remote evidence resolution)
  • Use for: Audit trails, compliance, trust decisions

Troubleshooting

"valid is false but crypto shows all true"

The valid field aggregates all sub-checks. If crypto passes but evidence or chain checks fail, valid will be false. Check the evidence and chain fields for details.

"evidence is empty"

If you created the attestation without evidence references, the evidence array will be empty. This is not an error -- it means there are no external proofs to verify.

"chain is null"

If the attestation has no derivation field, chain will be null. This is normal for standalone attestations that don't reference prior attestations.

"signature_valid is false after serialization"

JACS uses JSON Canonicalization Scheme (JCS) for hashing. If you serialize and re-parse the document, ensure the serializer preserves field order and does not add/remove whitespace in a way that changes the canonical form.

CLI Reference: jacs attest

The jacs attest command creates and verifies attestation documents from the command line. Attestation extends basic signing with structured claims, evidence references, and derivation chains.

jacs attest create

Create a signed attestation document.

Synopsis

jacs attest create --claims '<JSON>' [options]

Options

FlagRequiredDescription
--claims '<JSON>'YesJSON array of claims. Each claim must have name and value fields.
--subject-type <TYPE>NoType of subject: agent, artifact, workflow, identity. Default: derived from context.
--subject-id <ID>NoIdentifier of the subject being attested.
--subject-digest <SHA256>NoSHA-256 digest of the subject content.
--evidence '<JSON>'NoJSON array of evidence references.
--from-document <FILE>NoLift an existing signed JACS document into an attestation. Overrides subject flags.
-o, --output <FILE>NoWrite attestation to file instead of stdout.

Examples

Create a basic attestation:

jacs attest create \
  --subject-type artifact \
  --subject-id "doc-001" \
  --subject-digest "abc123def456..." \
  --claims '[{"name": "reviewed_by", "value": "human", "confidence": 0.95}]'

Attestation with multiple claims:

jacs attest create \
  --subject-type agent \
  --subject-id "agent-abc" \
  --subject-digest "sha256hash..." \
  --claims '[
    {"name": "reviewed", "value": true, "confidence": 0.95},
    {"name": "source", "value": "internal_db", "assuranceLevel": "verified"}
  ]'

Lift an existing signed document to attestation:

jacs attest create \
  --from-document mydata.signed.json \
  --claims '[{"name": "approved", "value": true}]'

With evidence references:

jacs attest create \
  --subject-type artifact \
  --subject-id "report-456" \
  --subject-digest "def789..." \
  --claims '[{"name": "scanned", "value": true}]' \
  --evidence '[{
    "kind": "custom",
    "digests": {"sha256": "evidence-hash..."},
    "uri": "https://scanner.example.com/results/123",
    "collectedAt": "2026-03-04T00:00:00Z",
    "verifier": {"name": "security-scanner", "version": "2.0"}
  }]'

Write to file:

jacs attest create \
  --subject-type artifact \
  --subject-id "doc-001" \
  --subject-digest "abc123..." \
  --claims '[{"name": "ok", "value": true}]' \
  -o attestation.json

jacs attest verify

Verify an attestation document.

Synopsis

jacs attest verify <FILE> [options]

Arguments

ArgumentRequiredDescription
<FILE>YesPath to the attestation JSON file to verify.

Options

FlagRequiredDescription
--fullNoUse full verification (evidence + derivation chain). Default: local verification (crypto + hash only).
--jsonNoOutput the verification result as JSON.
--key-dir <DIR>NoDirectory containing public keys for verification.
--max-depth <N>NoMaximum derivation chain depth. Default: 10.

Examples

Basic verification (local tier):

jacs attest verify attestation.json

Output:

Attestation verification: VALID
  Signature: OK
  Hash: OK
  Signer: agent-id-abc123
  Algorithm: ring-Ed25519

Full verification:

jacs attest verify attestation.json --full

Output:

Attestation verification: VALID
  Signature: OK
  Hash: OK
  Signer: agent-id-abc123
  Algorithm: ring-Ed25519
  Evidence: 1 item(s) verified
    [0] custom: digest OK, freshness OK
  Chain: not present

JSON output (for scripting):

jacs attest verify attestation.json --json

Output:

{
  "valid": true,
  "crypto": {
    "signature_valid": true,
    "hash_valid": true,
    "signer_id": "agent-id-abc123",
    "algorithm": "ring-Ed25519"
  },
  "evidence": [],
  "chain": null,
  "errors": []
}

Verify with external keys:

jacs attest verify attestation.json --key-dir ./trusted_keys/

Pipe through jq:

jacs attest verify attestation.json --json | jq '.crypto'

Piping and Scripting Patterns

Create and verify in one pipeline

jacs attest create \
  --subject-type artifact \
  --subject-id "doc-001" \
  --subject-digest "abc..." \
  --claims '[{"name": "ok", "value": true}]' \
  -o att.json && \
jacs attest verify att.json --json | jq '.valid'

Check validity in a script

#!/bin/bash
set -e

RESULT=$(jacs attest verify "$1" --json 2>/dev/null)
VALID=$(echo "$RESULT" | jq -r '.valid')

if [ "$VALID" = "true" ]; then
    echo "Attestation is valid"
    exit 0
else
    echo "Attestation is INVALID"
    echo "$RESULT" | jq '.errors'
    exit 1
fi

Batch verify multiple attestations

for file in attestations/*.json; do
    echo -n "$file: "
    jacs attest verify "$file" --json | jq -r '.valid'
done

Exit Codes

CodeMeaning
0Success (create: attestation created; verify: attestation valid)
1Failure (create: error creating attestation; verify: attestation invalid or error)

Environment Variables

VariableDescription
JACS_PRIVATE_KEY_PASSWORDPassword for the agent's private key
JACS_MAX_DERIVATION_DEPTHOverride maximum derivation chain depth (default: 10)
JACS_DATA_DIRECTORYDirectory for JACS data files
JACS_KEY_DIRECTORYDirectory containing keys
JACS_AGENT_ID_AND_VERSIONAgent identity for signing

See Also

Migration Guide

Registry status (observed 2026-07-11): npm serves @hai.ai/jacs@0.10.1, while these pages describe source 0.11.4. The registry package does not expose every method shown here. Pin the version and inspect its exports; use a source build only when that is an explicit deployment choice.

This guide covers migrating between JACS versions and common migration scenarios.

Version Compatibility

JACS maintains backward compatibility for document verification:

  • Documents signed with older versions can be verified with newer versions
  • Older JACS versions cannot verify documents using newer cryptographic algorithms

0.9 → 0.10

v0.10.0 adds inline text signatures, image / media embedding, and the new MissingSignature error variant. No existing API changes behaviour.

Additive surface — no opt-in required

Five new methods per binding, five new CLI verbs, five new MCP tools. Pre-existing call sites are unaffected.

DomainNew surface
CLIjacs sign-text, jacs verify-text, jacs sign-image, jacs verify-image, jacs extract-media-signature
Python (jacs.simple / JacsClient)sign_text, verify_text, sign_image, verify_image, extract_media_signature
Node (@hai.ai/jacs/simple / JacsClient)signText, verifyText, signImage, verifyImage, extractMediaSignature
Go (jacsgo)SignText, VerifyText, SignImage, VerifyImage, ExtractMediaSignature
MCPjacs_sign_text, jacs_verify_text, jacs_sign_image, jacs_verify_image, jacs_extract_media_signature

Breaking consideration: ErrorKind exhaustive matches

The new MissingSignature variant on jacs::error::ErrorKind will trigger an unreachable-pattern warning (or a compile error in -D warnings) on Rust call sites that exhaustively match without a catch-all.

#![allow(unused)]
fn main() {
// before — does not compile under -D warnings in v0.10.0
match err.kind() {
    ErrorKind::InvalidSignature => ...,
    ErrorKind::DocumentNotFound => ...,
}

// after — option 1: explicit arm
match err.kind() {
    ErrorKind::InvalidSignature => ...,
    ErrorKind::DocumentNotFound => ...,
    ErrorKind::MissingSignature => ...,
}

// after — option 2: catch-all
match err.kind() {
    ErrorKind::InvalidSignature => ...,
    ErrorKind::DocumentNotFound => ...,
    _ => ...,
}
}

Python / Node / Go bindings are unaffected — their error types are not exhaustive enums.

Verify semantics change is opt-in via strict

verify-text and verify-image are new commands in v0.10.0. There is no existing caller to break. The default mode is permissive (missing-signature is a typed status, not an error); --strict opts in to error-on-missing.

No existing command (jacs verify, jacs document verify, etc.) changes its exit codes or its return shape.

Migrating Node.js from 0.6.x to 0.7.0

Breaking Change: Async-First API

In v0.7.0, all NAPI operations return Promises by default. Sync variants are available with a Sync suffix, following the Node.js convention (like fs.readFile vs fs.readFileSync).

Before (v0.6.x):

const agent = new JacsAgent();
agent.load('./jacs.config.json');
const doc = agent.createDocument(JSON.stringify(content));
const isValid = agent.verifyDocument(doc);

After (v0.7.0, async -- recommended):

const agent = new JacsAgent();
await agent.load('./jacs.config.json');
const doc = await agent.createDocument(JSON.stringify(content));
const isValid = await agent.verifyDocument(doc);

After (v0.7.0, sync -- for scripts/CLI):

const agent = new JacsAgent();
agent.loadSync('./jacs.config.json');
const doc = agent.createDocumentSync(JSON.stringify(content));
const isValid = agent.verifyDocumentSync(doc);

Method Renaming Summary

v0.6.xv0.7.0 Async (default)v0.7.0 Sync
agent.load(path)await agent.load(path)agent.loadSync(path)
agent.createDocument(...)await agent.createDocument(...)agent.createDocumentSync(...)
agent.verifyDocument(doc)await agent.verifyDocument(doc)agent.verifyDocumentSync(doc)
agent.verifyAgent()await agent.verifyAgent()agent.verifyAgentSync()
agent.updateAgent(json)await agent.updateAgent(json)agent.updateAgentSync(json)
agent.updateDocument(...)await agent.updateDocument(...)agent.updateDocumentSync(...)
agent.signString(data)await agent.signString(data)agent.signStringSync(data)
agent.createAgreement(...)await agent.createAgreement(...)agent.createAgreementSync(...)
agent.signAgreement(...)await agent.signAgreement(...)agent.signAgreementSync(...)
agent.checkAgreement(...)await agent.checkAgreement(...)agent.checkAgreementSync(...)

V8-Thread-Only Methods (No Change)

These methods remain synchronous without a Sync suffix because they use V8-thread-only APIs (Env, JsObject):

  • agent.signRequest(params) -- unchanged
  • agent.verifyResponse(doc) -- unchanged
  • agent.verifyResponseWithAgentId(doc) -- unchanged

Simplified API (Module-Level)

The @hai.ai/jacs/simple module follows the same pattern:

// v0.6.x
await jacs.quickstart({ name: 'my-agent', domain: 'agent.example.com' });
const signed = jacs.signMessage({ action: 'approve' });

// v0.7.0 async (recommended)
await jacs.quickstart({ name: 'my-agent', domain: 'agent.example.com' });
const signed = await jacs.signMessage({ action: 'approve' });

// v0.7.0 sync
jacs.quickstartSync({ name: 'my-agent', domain: 'agent.example.com' });
const signed = jacs.signMessageSync({ action: 'approve' });

Pure Sync Functions (No Change)

These functions do not call NAPI and remain unchanged (no suffix needed):

  • hashString(), createConfig(), getPublicKey(), isLoaded(), exportAgent(), getAgentInfo()
  • getDnsRecord(), getWellKnownJson(), verifyStandalone()
  • Trust store: trustAgent(), listTrustedAgents(), untrustAgent(), isTrusted(), getTrustedAgent()

Migration Steps

  1. Update dependency: npm install @hai.ai/jacs@0.7.0
  2. Add await to all NAPI method calls, or append Sync to method names
  3. Update test assertions to handle Promises (use async/await in test functions)
  4. V8-thread-only methods (signRequest, verifyResponse, verifyResponseWithAgentId) need no changes

Migrating from 0.5.1 to 0.5.2

Migration Notes

PBKDF2 Iteration Count: New key encryptions use 600,000 iterations (up from 100,000). Existing encrypted keys are decrypted automatically via fallback. To upgrade existing keys, re-encrypt them:

# Re-generate keys to use the new iteration count
jacs keygen

Deprecated Environment Variables

  • JACS_USE_SECURITY is now JACS_ENABLE_FILESYSTEM_QUARANTINE. The old name still works with a deprecation warning.

New Environment Variables

VariableDefaultDescription
JACS_MAX_SIGNATURE_AGE_SECONDS0 (no expiration)Maximum age of valid signatures. Set to a positive value to enable (e.g., 7776000 for 90 days).
JACS_REQUIRE_EXPLICIT_ALGORITHMfalseWhen true, reject verification if signingAlgorithm is missing.
JACS_ENABLE_FILESYSTEM_QUARANTINEfalseEnable filesystem quarantine (replaces JACS_USE_SECURITY).

Deprecated Method Aliases (0.9.0)

In v0.9.0, several method names were standardized. The old names remain as aliases for backward compatibility but are deprecated and will be removed in 1.0.0 (minimum 2 minor releases after deprecation).

Runtime Deprecation Warnings

Set the JACS_SHOW_DEPRECATIONS=1 environment variable to emit runtime warnings when deprecated methods are called:

export JACS_SHOW_DEPRECATIONS=1

This is recommended during development and CI to identify code that needs updating.

Deprecated Alias Table

SDKDeprecated MethodCanonical ReplacementSinceRemoval
Python (binding)agent.wrap_a2a_artifact()agent.sign_artifact()0.9.01.0.0
Python (A2A)a2a.wrap_artifact_with_provenance()a2a.sign_artifact()0.9.01.0.0
Node.js (binding)agent.wrapA2aArtifact()agent.signArtifact()0.9.01.0.0
Node.js (binding)agent.wrapA2aArtifactSync()agent.signArtifactSync()0.9.01.0.0
Node.js (A2A)a2a.wrapArtifactWithProvenance()a2a.signArtifact()0.9.01.0.0
Rust (core)agent.wrap_a2a_artifact()agent.sign_artifact()0.9.01.0.0
GoSignA2AArtifact() (simple API)Uses sign_artifact internally----

All aliases behave identically to their canonical replacements. No behavioral changes are needed when migrating -- only rename the method call.

Migration Examples

Python:

# Before (deprecated)
wrapped = agent.wrap_a2a_artifact(artifact_json, "artifact")

# After (canonical)
signed = agent.sign_artifact(artifact_json, "artifact")

Node.js:

// Before (deprecated)
const wrapped = await agent.wrapA2aArtifact(artifactJson, 'artifact');

// After (canonical)
const signed = await agent.signArtifact(artifactJson, 'artifact');

Python A2A integration:

# Before (deprecated)
wrapped = a2a.wrap_artifact_with_provenance(artifact, "artifact")

# After (canonical)
signed = a2a.sign_artifact(artifact, "artifact")

Node.js A2A integration:

// Before (deprecated)
const wrapped = await a2a.wrapArtifactWithProvenance(artifact, 'artifact');

// After (canonical)
const signed = await a2a.signArtifact(artifact, 'artifact');

Module-Level Function Deprecation (Reminder)

Module-level functions (e.g., jacs.load(), jacs.sign_request() in Python; load(), signRequest() in Node.js) were deprecated in earlier releases. Use JacsAgent instance methods instead. See the Python and Node.js API references for the full list.


Migrating from 0.2.x to 0.3.x

Configuration Changes

New Configuration Fields:

{
  "observability": {
    "logs": { "enabled": true, "level": "info" },
    "metrics": { "enabled": false },
    "tracing": { "enabled": false }
  }
}

Deprecated Fields:

  • jacs_log_level → Use observability.logs.level
  • jacs_log_file → Use observability.logs.destination

Migration Steps

  1. Update Configuration:

    # Backup current config
    cp jacs.config.json jacs.config.json.backup
    
    # Update to new format
    # Add observability section if needed
    
  2. Update Dependencies:

    # Node.js
    npm install @hai.ai/jacs@latest
    
    # Python
    pip install --upgrade jacs
    
  3. Verify Existing Documents:

    jacs document verify -d ./jacs_data/documents/
    

Migrating Storage Backends

Filesystem to AWS S3

  1. Create S3 Bucket:

    aws s3 mb s3://my-jacs-bucket
    
  2. Update Configuration:

    {
      "jacs_default_storage": "aws",
      "jacs_data_directory": "s3://my-jacs-bucket/data"
    }
    
  3. Set Environment Variables:

    export AWS_ACCESS_KEY_ID="your-key"
    export AWS_SECRET_ACCESS_KEY="your-secret"
    export AWS_REGION="us-east-1"
    
  4. Migrate Documents:

    # Upload existing documents
    aws s3 sync ./jacs_data/ s3://my-jacs-bucket/data/
    
  5. Verify Migration:

    jacs document verify -d s3://my-jacs-bucket/data/documents/
    

AWS S3 to Filesystem

  1. Download Documents:

    aws s3 sync s3://my-jacs-bucket/data/ ./jacs_data/
    
  2. Update Configuration:

    {
      "jacs_default_storage": "fs",
      "jacs_data_directory": "./jacs_data"
    }
    
  3. Verify Documents:

    jacs document verify -d ./jacs_data/documents/
    

Migrating Cryptographic Algorithms

Ed25519 to Post-Quantum

Native JACS signing defaults to pq2025 (ML-DSA-87 / FIPS-204). Explicit ed25519 creation remains supported; agents using it continue to load and sign with the canonical ring-Ed25519 label until they rotate.

Key rotation is the designated migration path. Every rotation resolves to pq2025 — with or without an explicit algorithm argument — so rotating an Ed25519 agent converges it to a post-quantum root. Rotating to any other algorithm is a typed error.

  1. Rotate Keys:

    jacs agent rotate-keys
    
  2. Re-issue the Compatibility Key Binding (if present):

    The ES256 compatibility key binding is signed by the native root, so rotation supersedes it. Ecosystem exports fail with a "re-issue" error until a new binding is signed by the current native root:

    jacs agent issue-compat-binding
    
  3. Re-sign Critical Documents (Optional):

    // Re-sign documents with new algorithm
    const oldDoc = JSON.parse(fs.readFileSync('./old-doc.json'));
    
    // Remove old signature fields
    delete oldDoc.jacsSignature;
    delete oldDoc.jacsSha256;
    
    // Create new signed version
    const newDoc = await agent.createDocument(JSON.stringify(oldDoc));
    

Note: Old documents remain valid with old signatures. Re-signing is only needed for documents that require the new algorithm.

Migrating Between Platforms

Node.js to Python

Both platforms use the same document format:

// Node.js - create document
const signedDoc = await agent.createDocument(JSON.stringify(content));
fs.writeFileSync('doc.json', signedDoc);
# Python - verify the same document
with open('doc.json', 'r') as f:
    doc_string = f.read()

is_valid = agent.verify_document(doc_string)

Sharing Agents Between Platforms

Agents can be used across platforms by sharing configuration:

  1. Export Agent Files:

    jacs_keys/
    ├── private.pem
    └── public.pem
    jacs.config.json
    
  2. Use Same Config in Both:

    // Node.js
    await agent.load('./jacs.config.json');
    
    # Python
    agent.load('./jacs.config.json')
    

Migrating Key Formats

Unencrypted to Encrypted Keys

  1. Encrypt Existing Key:

    # Backup original
    cp jacs_keys/private.pem jacs_keys/private.pem.backup
    
    # Encrypt with password
    openssl pkcs8 -topk8 -in jacs_keys/private.pem \
      -out jacs_keys/private.pem.enc -v2 aes-256-cbc
    
    # Remove unencrypted key
    rm jacs_keys/private.pem
    mv jacs_keys/private.pem.enc jacs_keys/private.pem
    
  2. Update Configuration:

    {
      "jacs_agent_private_key_filename": "private.pem"
    }
    
  3. Set Password:

    export JACS_PRIVATE_KEY_PASSWORD="your-secure-password"
    

Database Migration

Adding Database Storage

If migrating from filesystem to include database storage:

  1. Create Database Schema:

    CREATE TABLE jacs_documents (
      id UUID PRIMARY KEY,
      version_id UUID NOT NULL,
      document JSONB NOT NULL,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
      UNIQUE(id, version_id)
    );
    
  2. Import Existing Documents:

    const fs = require('fs');
    const path = require('path');
    const { Pool } = require('pg');
    
    const pool = new Pool({ connectionString: process.env.DATABASE_URL });
    const docsDir = './jacs_data/documents';
    
    async function importDocuments() {
      const docDirs = fs.readdirSync(docsDir);
    
      for (const docId of docDirs) {
        const docPath = path.join(docsDir, docId);
        const versions = fs.readdirSync(docPath);
    
        for (const versionFile of versions) {
          const docString = fs.readFileSync(
            path.join(docPath, versionFile),
            'utf-8'
          );
          const doc = JSON.parse(docString);
    
          await pool.query(`
            INSERT INTO jacs_documents (id, version_id, document)
            VALUES ($1, $2, $3)
            ON CONFLICT (id, version_id) DO NOTHING
          `, [doc.jacsId, doc.jacsVersion, doc]);
        }
      }
    }
    
    importDocuments();
    

MCP Integration Migration

Adding JACS to Existing MCP Server

  1. Install JACS:

    npm install @hai.ai/jacs
    
  2. Wrap Existing Transport:

    // Before
    const transport = new StdioServerTransport();
    await server.connect(transport);
    
    // After
    import { createJACSTransportProxy } from '@hai.ai/jacs/mcp';
    
    const baseTransport = new StdioServerTransport();
    const secureTransport = createJACSTransportProxy(
      baseTransport,
      './jacs.config.json',
      'server'
    );
    await server.connect(secureTransport);
    
  3. Update Client:

    // Client also needs JACS
    const baseTransport = new StdioClientTransport({ command: 'node', args: ['server.js'] });
    const secureTransport = createJACSTransportProxy(
      baseTransport,
      './jacs.client.config.json',
      'client'
    );
    await client.connect(secureTransport);
    

HTTP API Migration

Migrating unbound request credentials to JACS v2

Legacy request credentials signed only signer ID, timestamp, and nonce. They did not authenticate the HTTP method, target, query, body, or receiving service, so a fresh credential could be transplanted to another request. Legacy builders remain available for compatibility and emit a WARN; migrate callers to v2.

  1. Serialize the final request body once.
  2. Build a v2 header from the actual method, absolute URL (including query), those exact body bytes, and a non-empty service-specific audience.
  3. Send the body without reformatting it.
  4. On the server, resolve the expected signer to a trusted public key and verify against the actual request context.
  5. Atomically consume the nonce. Use a shared store for multiple replicas and retain it through issued_at + max_age; Rust callers should use request_auth_replay_ttl(&verified_claims, max_age).
  6. Reject legacy headers after every client is migrated.
APILegacyJACS v2
Rustprotocol::build_auth_header(&mut agent)SimpleAgent::build_request_auth_header(method, url, body, audience)
PythonSimpleAgent.build_auth_header()SimpleAgent.build_request_auth_header(method, url, body, audience)
Node.jsJacsSimpleAgent.buildAuthHeader()JacsSimpleAgent.buildRequestAuthHeader(method, url, body, audience)
Go JacsAgentBuildAuthHeader()BuildRequestAuthHeader(method, url, body, audience)
Go JacsSimpleAgentBuildAuthHeader()BuildRequestAuthHeader(method, url, body, audience)

The APIs are additive: the legacy name takes no request context, while the request-bound name requires the request values and emits JACS v2.<claims>.<signature>. A header without that prefix is not the new contract.

Legacy construction emits a structured WARN on every use. Strict deployments can set JACS_REJECT_UNBOUND_AUTH_HEADER=true after migrating callers. Verification policy should distinguish legacy and v2 explicitly, never silently fall back after a v2 verification failure.

For signed responses, migrate payload-only or generic document verification to sign_response / signResponse and strict event unwrapping. The v2 response signature covers payload and all metadata. Unknown signers, plain events, legacy envelopes, and mutations are errors; do not catch the error and parse the untrusted payload anyway.

Verification proves possession of the public key selected under your trust policy. It does not establish a real-world identity merely because the signed envelope contains a name, agent ID, domain, or timestamp.

Adding JACS to Existing Express API

  1. Install Middleware:

    npm install @hai.ai/jacs
    
  2. Add Middleware to Routes:

    import { JACSExpressMiddleware } from '@hai.ai/jacs/http';
    
    // Before
    app.use('/api', express.json());
    
    // After - for JACS-protected routes
    app.use('/api/secure', express.text({ type: '*/*' }));
    app.use('/api/secure', JACSExpressMiddleware({
      configPath: './jacs.config.json'
    }));
    
    // Keep non-JACS routes unchanged
    app.use('/api/public', express.json());
    
  3. Update Route Handlers:

    // Before
    app.post('/api/data', (req, res) => {
      const payload = req.body;
      // ...
    });
    
    // After
    app.post('/api/secure/data', (req, res) => {
      const payload = req.jacsPayload;  // Verified payload
      // ...
    });
    

Troubleshooting Migration

Common Issues

Documents Not Verifying After Migration:

  • Check algorithm compatibility
  • Verify keys were copied correctly
  • Ensure configuration paths are correct

Key File Errors:

  • Verify file permissions (600 for private key)
  • Check key format matches algorithm
  • Ensure password is set for encrypted keys

Storage Errors After Migration:

  • Verify storage backend is accessible
  • Check credentials/permissions
  • Ensure directory structure is correct

Verification Checklist

After any migration:

  1. Verify Configuration:

    jacs config read
    
  2. Verify Agent:

    jacs agent verify
    
  3. Verify Sample Document:

    jacs document verify -f ./sample-doc.json
    
  4. Test Document Creation:

    echo '{"test": true}' > test.json
    jacs document create -f test.json
    
  5. Verify Version:

    jacs version
    

Rollback Procedures

If migration fails:

  1. Restore Configuration:

    cp jacs.config.json.backup jacs.config.json
    
  2. Restore Keys:

    cp -r jacs_keys.backup/* jacs_keys/
    
  3. Restore Dependencies:

    # Node.js
    npm install @hai.ai/jacs@previous-version
    
    # Python
    pip install jacs==previous-version
    
  4. Verify Rollback:

    jacs agent verify
    jacs document verify -d ./jacs_data/documents/
    

See Also