JACS: JSON Agent Communication Standard

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

Start With The Deployment

Most teams adopt JACS in one of four ways:

  • LangChain / LangGraph / CrewAI / FastAPI: add signing at tool or API boundaries without changing the rest of the app
  • MCP: secure a local tool server or expose JACS itself as an MCP tool suite
  • A2A: publish an Agent Card, exchange signed artifacts, and apply trust policy across organizations
  • Core signing: sign JSON, files, or agreements directly from Rust, Python, Node.js, or Go

The book now focuses on those supported workflows first. Older roadmap-style integration chapters have been reduced or removed from navigation.

What JACS Gives You

  • Signed JSON and file envelopes with tamper detection
  • Persistent agent identity with encrypted private keys
  • Trust bootstrap primitives such as share_public_key, share_agent, and trust_agent_with_key
  • A2A artifact signing and trust policies (open, verified, strict)
  • MCP integration paths for ready-made servers, transport security, or tool registration
  • Framework adapters for Python and Node.js ecosystems
  • Multi-party agreements with quorum, timeout, and algorithm constraints
  • Cross-language compatibility across Rust, Python, Node.js, and Go

Best Entry Points

If you are choosing where to start:

  1. Which Integration?
  2. Use Cases
  3. MCP Overview
  4. A2A Interoperability
  5. Python Framework Adapters
  6. Node.js LangChain.js

Implementations

Rust

  • Deepest feature surface
  • CLI plus library APIs
  • Best fit when you want a ready-made MCP server via jacs mcp

Python (jacs)

  • Best fit for LangChain, LangGraph, CrewAI, FastAPI, and local MCP/A2A helpers
  • Strong adapter story for adding provenance inside an existing app

Node.js (@hai.ai/jacs)

  • Best fit for Express, Koa, Vercel AI SDK, LangChain.js, and MCP transport/tool integration
  • Also exposes A2A helpers and Express discovery middleware

Go (jacsgo)

  • Good fit for services that need signing and verification without framework adapters

Quick Start

Rust CLI

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

Python

pip install jacs

Node.js

npm install @hai.ai/jacs

Go

go get github.com/HumanAssisted/JACS/jacsgo

Rust, Python, and Node quickstart flows create or load a persistent agent and return agent metadata including config and key paths.

What This Book Does Not Claim

  • It does not treat MCP and A2A as the same thing. MCP is for model-to-tool calls inside an application boundary; A2A is for agent discovery and exchange across boundaries.
  • It does not assume every aspirational integration is first-class. If a chapter describes a feature that is not fully supported today, it has been moved out of the main path and tracked separately.
  • It does not require a registry or blockchain to work. JACS identity is key-based and can be used entirely locally.

Community

What is JACS?

JACS (JSON Agent Communication Standard) is a comprehensive framework designed to solve a critical problem in AI systems: How do agents communicate and collaborate securely with verifiable trust?

The Problem JACS Solves

As AI systems become more sophisticated, we're moving toward multi-agent architectures where different AI agents need to:

  • Exchange tasks and delegate work to each other
  • Create agreements and verify their completion
  • Share data with guaranteed authenticity
  • Maintain audit trails of decisions and actions
  • Establish trust with flexible key resolution (local trust stores, DNS, optional key services)

Traditional approaches fall short because they lack:

  • Cryptographic integrity for agent communications
  • Standardized formats for agent interactions
  • Built-in versioning and audit trails
  • Support for multi-agent agreements and workflows

JACS Core Philosophy

🎯 Agent-First Design

JACS is built specifically for AI agent communication patterns, while still being usable as a general-purpose signed JSON provenance layer. It understands concepts like:

  • Agents with identities and capabilities
  • Tasks that can be delegated and tracked
  • Agreements between multiple parties
  • Versioning for iterative improvements

πŸ” Trust Through Cryptography

Every JACS document includes:

  • Digital signatures proving authenticity
  • Hash verification ensuring integrity
  • Public key cryptography for identity verification
  • Timestamps for chronological ordering

πŸ“‹ Standards-Based

JACS builds on proven standards:

  • JSON for universal compatibility
  • JSON Schema for structure validation
  • RFC 3339 timestamps for consistency
  • Standard cryptographic algorithms (RSA, Ed25519, post-quantum)

Key Concepts

Agents

An Agent is an autonomous entity with:

  • A unique identity (UUID)
  • Cryptographic keys for signing
  • Capabilities defined in services
  • The ability to create and verify documents

Documents

A Document is any JSON object that includes:

  • JACS header fields (ID, version, creator, etc.)
  • A cryptographic signature
  • A hash for integrity verification
  • Business logic specific to the document type

Tasks

A Task is a special document type representing:

  • Work to be performed
  • Success/failure criteria
  • Input/output specifications
  • Delegation and completion tracking

Agreements

An Agreement is a mechanism for:

  • Multiple agents to consent to terms
  • Tracking signatures from all required parties
  • Ensuring all participants have signed before proceeding
  • Creating binding commitments between agents

How JACS Works

graph TD
    A[Agent A] -->|Creates Task| T[JACS Task Document]
    T -->|Contains| S[Digital Signature]
    T -->|Contains| H[SHA256 Hash]
    T -->|Contains| M[Metadata]
    
    A -->|Sends to| B[Agent B]
    B -->|Verifies| T
    B -->|Signs Agreement| AG[Agreement Document]
    AG -->|Returns to| A
  1. Agent A creates a task document with their requirements
  2. The document is signed with Agent A's private key
  3. A hash is calculated for integrity verification
  4. Agent B receives and verifies the signature and hash
  5. Agent B can create an agreement to accept the task
  6. Both agents have a verifiable record of the interaction

Real-World Examples

πŸ€– AI Content Pipeline

Content Agent β†’ Research Agent β†’ Review Agent β†’ Publishing Agent

Each handoff includes signed task documents with clear requirements and deliverables.

πŸ“Š Data Processing Workflow

Data Ingestion Agent β†’ Processing Agent β†’ Validation Agent β†’ Storage Agent

Each step is tracked with verifiable completion certificates and quality metrics.

πŸ” Multi-Agent Analysis

Query Agent β†’ Research Agent β†’ Analysis Agent β†’ Reporting Agent

Complex analysis tasks are broken down with clear accountability for each step.

Benefits Over Alternatives

FeatureJACSTraditional APIsGeneral Signing
Agent Identityβœ… Built-in❌ Custom implementation❌ Not agent-focused
Task Management⚠️ Schema-native (lifecycle via integrations)❌ Application-specific❌ Not applicable
Multi-Party Agreementsβœ… Core feature❌ Complex to implement⚠️ Possible but difficult
Audit Trailsβœ… Automatic❌ Manual logging⚠️ Basic signing only
Schema Validationβœ… JSON Schema❌ Custom validation❌ No structure
Versioningβœ… Built-in❌ Manual versioning❌ Not supported
Cross-Platformβœ… JSON everywhere⚠️ Protocol dependent⚠️ Format dependent

JACS provides signed artifacts, schemas, trust primitives, and auditability. Real-time transport and task orchestration are handled by integrations (e.g., A2A, MCP, HTTP server layers).

When to Use JACS

βœ… Perfect for:

  • Multi-agent AI systems
  • Task delegation and tracking
  • Audit trail requirements
  • Cross-organization AI collaboration
  • Compliance-critical AI applications
  • Research environments with multiple AI models

⚠️ Consider alternatives for:

  • Simple single-agent systems
  • Real-time streaming data
  • High-frequency micro-transactions
  • Systems where trust is not a concern

Next Steps

Ready to dive deeper? Continue with:

Which JACS Path Should I Use?

Choose the smallest supported integration that matches your deployment.

Start Here

If you need...Start hereWhy
Signed tool outputs inside LangChain / LangGraph on PythonPython Framework AdaptersSmallest path: sign tool results without adding MCP
Signed tool outputs inside LangChain.js / LangGraph on NodeNode.js LangChain.jsSame idea for TypeScript
A ready-made local MCP server for Claude, Codex, or another MCP clientMCP Overview and jacs-mcpFastest full server path
To secure your existing MCP server/client codePython MCP or Node.js MCPUse wrappers or transport proxies around code you already have
Cross-organization agent discovery and signed artifact exchangeA2A InteroperabilityMCP is not enough for this boundary
Signed HTTP APIs without adopting MCPPython Framework Adapters, Express, KoaSign requests or responses at the web layer
Multi-party approval or quorum workflowsMulti-Agent AgreementsAgreements are the right primitive, not just one-off signatures
Direct signing from scripts, jobs, or servicesQuick Start, Python Basic Usage, Node Basic Usage, Go InstallationStart from sign/verify before adding framework layers

When You Probably Do Not Need JACS

  • Everything stays inside one service you control and your own 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 in your system: LangChain tool, FastAPI response, MCP call, or A2A artifact.
  3. Add trust policy only when other agents or organizations enter the picture.
  4. Add agreements, DNS, or attestations only if your deployment actually needs them.

The mistake to avoid is starting with the broadest story. Start with the boundary you need to secure now.

Use Cases

This chapter stays close to current product use, not roadmap integrations.

1. Secure A Local MCP Tool Server

Use this when: Claude Desktop, Codex, or another MCP client is calling tools that should not run on blind trust.

Recommended JACS path:

What JACS adds:

  • Signed JSON-RPC messages
  • Fail-closed verification by default
  • Agent identity and auditability for tool calls

2. Add Provenance To LangChain Or LangGraph

Use this when: your model already runs inside LangChain or LangGraph and you want signed tool outputs without introducing MCP.

Recommended JACS path:

What JACS adds:

  • Signed tool results
  • Optional strict mode at the adapter boundary
  • Minimal changes to existing framework code

3. Exchange Signed Artifacts Across Organizations

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

Recommended JACS path:

What JACS adds:

  • Agent Cards with JACS provenance metadata
  • Signed A2A artifacts
  • Trust policies for admission control

4. Sign HTTP Or API Boundaries Without MCP

Use this when: the boundary is an API route, not an MCP transport.

Recommended JACS path:

What JACS adds:

  • Signed JSON responses
  • Verified inbound requests
  • A clean upgrade path to A2A discovery on the same app boundary

5. Run Multi-Agent Approval Workflows

Use this when: multiple agents must sign off on the same document, deployment, or decision.

Recommended JACS path:

What JACS adds:

  • M-of-N quorum
  • Timeout and algorithm constraints
  • Verifiable signature chain across signers

6. Keep Signed Files Or JSON As Durable Artifacts

Use this when: you need an artifact to stay verifiable after it leaves the process that created it.

Recommended JACS path:

What JACS adds:

  • Self-contained signed envelopes
  • Re-verification at read time
  • Cross-language interoperability

7. Publish Public Identity Without A Central Auth Service

Use this when: external systems need to verify your agent identity but you do not want a shared auth server in the middle.

Recommended JACS path:

What JACS adds:

  • Public key fingerprint anchoring
  • DNS-based verification flows
  • Local private-key custody

Core Concepts

Understanding JACS requires familiarity with several key concepts that work together to create a secure, verifiable communication framework for AI agents.

Agents

An Agent is the fundamental entity in JACS - an autonomous participant that can create, sign, and verify documents.

Agent Identity

{
  "jacsId": "550e8400-e29b-41d4-a716-446655440000",
  "jacsVersion": "123e4567-e89b-12d3-a456-426614174000",
  "jacsType": "agent",
  "name": "Content Creation Agent",
  "description": "Specialized in creating marketing content"
}

Key Properties:

  • jacsId: Permanent UUID identifying the agent
  • jacsVersion: UUID that changes with each update
  • Cryptographic Keys: Ed25519, RSA, or post-quantum key pairs
  • Services: Capabilities the agent offers
  • Contacts: How to reach the agent

Agent Lifecycle

  1. Creation: Generate keys and initial agent document
  2. Registration: Store public keys for verification
  3. Operation: Create and sign documents
  4. Updates: Version changes while maintaining identity
  5. Verification: Other agents validate signatures

Verification: load() vs verify_standalone()

When consuming signed documents, you can verify in two ways:

  • With a loaded agent (load(config) first): Call verify(signedDocument). The loaded agent uses its config (trust store, key resolution) to resolve the signer’s public key and verify the signature. Use this when your process already has a JACS config (e.g. it also signs) or when you want to use a specific key directory and resolution order.

  • Without loading an agent (one-off verification): Call verify_standalone(signedDocument, options) (or the language equivalent: verifyStandalone, VerifyStandalone). This verifies the document using only the options you pass (e.g. keyResolution, keyDirectory). No config file or persistent agent state is required. Use this in lightweight services that only need to verify incoming documents.

Documents

A Document is any JSON object that follows JACS conventions for identity, versioning, and cryptographic integrity.

Document Structure

{
  "jacsId": "doc-uuid-here",
  "jacsVersion": "version-uuid-here",
  "jacsType": "task",
  "jacsVersionDate": "2024-01-15T10:30:00Z",
  "jacsPreviousVersion": "previous-version-uuid",
  
  "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": ["jacsId", "title", "description"]
  }
}

Required JACS Fields

FieldPurposeExample
$schemaJSON Schema referenceURL to schema
jacsIdPermanent document identifierUUID v4
jacsVersionVersion identifier (changes on update)UUID v4
jacsTypeDocument type"agent", "task", "message"
jacsVersionDateWhen this version was createdRFC 3339 timestamp
jacsOriginalVersionOriginal version UUIDUUID v4
jacsOriginalDateOriginal creation timestampRFC 3339 timestamp
jacsLevelData level/intent"raw", "config", "artifact", "derived"
jacsPreviousVersionPrevious version UUID (optional)UUID v4 or null
jacsSha256Hash of document contentSHA-256 hex string
jacsSignatureCryptographic signatureSignature object

Document Types

Agent Documents

  • Define agent identity and capabilities
  • Contain service definitions and contact information
  • Self-signed by the agent

Task Documents

  • Describe work to be performed
  • Include success/failure criteria
  • Can be delegated between agents

Message Documents

  • General communication between agents
  • Can include attachments and metadata
  • Support threaded conversations

Agreement Documents

  • Multi-party consent mechanisms
  • Track required and actual signatures
  • Enforce completion before proceeding

Tasks

Tasks represent work that can be delegated, tracked, and verified between agents.

Task Structure

{
  "jacsType": "task",
  "title": "Generate Marketing Copy",
  "description": "Create compelling copy for product launch",
  
  "actions": [
    {
      "id": "research",
      "name": "Research competitors",
      "description": "Analyze competitor messaging",
      "success": "Complete competitive analysis report",
      "failure": "Unable to access competitor data"
    }
  ],
  
  "jacsTaskCustomer": {
    "agentID": "customer-agent-uuid",
    "signature": "customer-signature"
  }
}

Task Lifecycle

  1. Creation: Customer agent creates task with requirements
  2. Delegation: Task sent to service provider agent
  3. Agreement: Provider signs agreement to accept task
  4. Execution: Provider performs the work
  5. Completion: Provider creates completion document
  6. Verification: Customer verifies and accepts results

Task Components

Actions: Individual steps within a task

  • id: Unique identifier within the task
  • name: Human-readable action name
  • description: Detailed requirements
  • success: Definition of successful completion
  • failure: What constitutes failure

Services: Required capabilities

  • type: Service category
  • requirements: Specific needs
  • constraints: Limitations or restrictions

Agreements

Agreements enable multi-party consent and coordination between agents.

Agreement Structure

{
  "jacsType": "agreement",
  "title": "Task Acceptance Agreement",
  "question": "Do you agree to complete the marketing copy task?",
  "context": "Task ID: abc123, Deadline: 2024-01-20",
  
  "agents": [
    "agent-1-uuid",
    "agent-2-uuid",
    "agent-3-uuid"
  ],
  
  "jacsAgreement": {
    "agent-1-uuid": {
      "agentID": "agent-1-uuid",
      "signature": "base64-signature",
      "date": "2024-01-15T10:30:00Z"
    },
    "agent-2-uuid": {
      "agentID": "agent-2-uuid", 
      "signature": "base64-signature",
      "date": "2024-01-15T11:15:00Z"
    }
    // agent-3-uuid has not signed yet
  },
  
  "jacsAgreementHash": "hash-of-agreement-content"
}

Agreement Process

  1. Creation: Initial agent creates agreement with required participants
  2. Distribution: Agreement sent to all required agents
  3. Review: Each agent reviews terms and conditions
  4. Signing: Agents add their signatures if they consent
  5. Completion: Agreement becomes binding when all parties have signed
  6. Verification: Any party can verify all signatures

Agreement Types

Task Agreements: Consent to perform specific work Service Agreements: Long-term service provision contracts
Data Sharing Agreements: Permission to access or use data Update Agreements: Consent to system or process changes

Cryptographic Security

JACS uses industry-standard cryptographic primitives for security.

Supported Algorithms

Current Standards

  • ring-Ed25519: Fast elliptic curve signatures using the ring library (recommended)
  • RSA-PSS: Traditional RSA with probabilistic signature scheme

Post-Quantum

  • pq-dilithium: NIST-standardized post-quantum signatures

Signature Process

  1. Content Extraction: Specific fields are extracted for signing
  2. Canonicalization: Fields are sorted and formatted consistently
  3. Hashing: SHA-256 hash of the canonical content
  4. Signing: Private key signs the hash
  5. Verification: Public key verifies the signature

Key Management

  • Agent Keys: Each agent has a unique key pair
  • Public Key Distribution: Public keys shared through secure channels
  • Key Rotation: Agents can update keys while maintaining identity
  • Key Verification: Public key hashes ensure integrity

Versioning and Audit Trails

JACS provides comprehensive versioning for tracking document evolution.

Version Management

  • Immutable IDs: jacsId never changes for a document
  • Version IDs: jacsVersion changes with each update
  • Previous Versions: jacsPreviousVersion creates a chain
  • Timestamps: jacsVersionDate provides chronological order

Audit Trail Benefits

  • Complete History: Track all changes to any document
  • Attribution: Know exactly who made each change
  • Verification: Cryptographic proof of authenticity
  • Compliance: Meet regulatory audit requirements

Storage and Transport

JACS documents are designed to be storage and transport agnostic.

Storage Options

  • File System: Simple JSON files
  • Databases: Store as JSON/JSONB fields
  • Object Storage: S3, Azure Blob, Google Cloud Storage
  • Version Control: Git repositories for change tracking

Transport Mechanisms

  • HTTP APIs: RESTful or GraphQL endpoints
  • Message Queues: RabbitMQ, Kafka, SQS
  • Email: Documents as attachments
  • Direct Transfer: USB drives, file sharing

Format Compatibility

  • JSON: Universal compatibility across all systems
  • Schema Validation: Ensures consistent structure
  • Self-Contained: All necessary information in the document
  • Human Readable: Can be inspected and debugged easily

Next Steps

Now that you understand the core concepts:

  1. Quick Start - Try JACS hands-on
  2. Choose Implementation:
  3. Examples - See real-world usage patterns

Quick Start Guide

Get signing and verifying in under a minute. No manual setup needed.

Zero-Config Quick Start

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.

Password bootstrap

Rust CLI quickstart requires exactly one explicit password source:

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

# CLI convenience (file contains only the password)
export JACS_PASSWORD_FILE=/secure/path/jacs-password.txt

If both JACS_PRIVATE_KEY_PASSWORD and JACS_PASSWORD_FILE are set, CLI fails fast to avoid ambiguity. Python/Node quickstart can auto-generate a secure password if JACS_PRIVATE_KEY_PASSWORD is unset. Set JACS_SAVE_PASSWORD_FILE=true if you want the generated password persisted to ./jacs_keys/.jacs_password. In production, set JACS_PRIVATE_KEY_PASSWORD explicitly. One call and you're signing.

pip install jacs
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}")
npm install @hai.ai/jacs
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}`);
cargo install jacs-cli
# Info mode -- prints agent ID and 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

Pass algorithm="ring-Ed25519" (or { algorithm: 'ring-Ed25519' } in JS, --algorithm ring-Ed25519 in CLI) to override the default (pq2025).

That's it -- you're signing. For most use cases, the quick start above is all you need. Jump to Which integration should I use? to find the right framework adapter, or read on for manual agent setup.

macOS Homebrew install (Rust CLI)

brew tap HumanAssisted/homebrew-jacs
brew install jacs

MCP server (Rust CLI)

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

# Start the MCP server (stdio transport)
jacs mcp

Advanced: Explicit Agent Setup

For full control over agent creation, you can set up an agent manually with a config file and JACS_PRIVATE_KEY_PASSWORD environment variable. This is optional since quickstart(...) already creates a persistent agent.

Install

cargo install jacs-cli

Initialize

# Create configuration and agent in one step
jacs init

# Or step by step:
# 1. Create config
jacs config create
# 2. Create agent with keys
jacs agent create --create-keys true
# 3. Verify
jacs agent verify

Sign a document

jacs document create -f mydata.json

Install

npm install @hai.ai/jacs

Load and use

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

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

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

Install

pip install jacs

Load and use

import jacs.simple as jacs

# Load from config file
jacs.load("./jacs.config.json")

signed = jacs.sign_message({"action": "approve", "amount": 100})
result = jacs.verify(signed.raw)
print(f"Valid: {result.valid}")

Programmatic Agent Creation (v0.6.0+)

For scripts, CI/CD, and server environments where you need agents created programmatically with explicit parameters (without interactive prompts), use create(). For most cases, quickstart(...) above is simpler and also creates a persistent agent.

import jacs.simple as jacs

agent = jacs.create(
    name="my-agent",
    password="Str0ng-P@ssw0rd!",  # or set JACS_PRIVATE_KEY_PASSWORD
    algorithm="pq2025",
)
print(f"Agent: {agent.agent_id}")
const jacs = require('@hai.ai/jacs/simple');

const agent = await jacs.create({
  name: 'my-agent',
  password: process.env.JACS_PRIVATE_KEY_PASSWORD,
  algorithm: 'pq2025',
});
console.log(`Agent: ${agent.agentId}`);
info, err := jacs.Create("my-agent", &jacs.CreateAgentOptions{
    Password:  os.Getenv("JACS_PRIVATE_KEY_PASSWORD"),
    Algorithm: "pq2025",
})
#![allow(unused)]
fn main() {
use jacs::simple::{CreateAgentParams, SimpleAgent};

let params = CreateAgentParams {
    name: "my-agent".into(),
    password: std::env::var("JACS_PRIVATE_KEY_PASSWORD").unwrap(),
    algorithm: "pq2025".into(),
    ..Default::default()
};
let (agent, info) = SimpleAgent::create_with_params(params)?;
}

Password requirements: At least 8 characters, with uppercase, lowercase, a digit, and a special character.

Algorithm note: pq-dilithium is deprecated in v0.6.0. Use pq2025 (ML-DSA-87, FIPS-204) instead.

Understanding What Happened

When you completed the quick start, several important things occurred:

1. Agent Creation

  • A unique identity (UUID) was generated for your agent
  • Cryptographic key pair was created for signing
  • Agent document was created and self-signed
  • Public key was stored for verification

2. Configuration Setup

  • Storage directories were configured
  • Cryptographic algorithm was selected
  • Agent identity was linked to configuration

3. Task Creation

  • Task document was structured according to JACS schema
  • Document was signed with your agent's private key
  • SHA-256 hash was calculated for integrity
  • Signature metadata was embedded in the document

Verify Everything Works

Let's verify that the documents are properly signed and can be validated:

# Verify agent signature
jacs agent verify

# Verify a specific document
jacs document verify -f ./jacs_data/[document-id].json

# Sign a document
jacs document sign -f ./jacs_data/[document-id].json
// Verify agent signature (async)
const isValid = await agent.verifyAgent();
console.log('Agent signature valid:', isValid);

// Verify task signature
const taskValid = await agent.verifyDocument(signedTask);
console.log('Task signature valid:', taskValid);
# Verify agent signature
is_valid = agent.verify_agent()
print(f'Agent signature valid: {is_valid}')

# List all documents
documents = agent.list_documents()
print(f'Documents: {len(documents)}')

# Verify task signature  
task_valid = agent.verify_document(signed_task)
print(f'Task signature valid: {task_valid}')

# Get document details
task_details = agent.get_document(signed_task["jacsId"])
print(f'Task details: {task_details}')

Next Steps: Multi-Agent Workflow

Now let's create a second agent and demonstrate inter-agent communication:

# Create a second agent configuration
cp jacs.config.json reviewer.config.json
# Edit reviewer.config.json to set jacs_agent_id_and_version to null

# Create reviewer agent (uses JACS_CONFIG_PATH environment variable)
JACS_CONFIG_PATH=./reviewer.config.json jacs agent create --create-keys true

# Create an agreement on a document
jacs agreement create -f ./document.json \
  --agents [agent-1-id],[agent-2-id] \
  --question "Do you agree to collaborate on this content task?"

# Sign the agreement as first agent
jacs agreement sign -f ./document.json

# Sign as second agent (using reviewer config)
JACS_CONFIG_PATH=./reviewer.config.json jacs agreement sign -f ./document.json

# Verify agreement is complete
jacs agreement check -f ./document.json
// Create second agent with separate config file
const reviewerConfig = { ...config };
reviewerConfig.jacs_agent_id_and_version = null;

fs.writeFileSync('./reviewer.config.json', JSON.stringify(reviewerConfig, null, 2));

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

// Create agreement between agents
const signedAgreement = await agent.createAgreement(
  signedTask,
  [agentDoc.jacsId, reviewerDoc.jacsId],
  'Do you agree to collaborate on this content task?'
);

// Both agents sign the agreement
const signed1 = await agent.signAgreement(signedAgreement);
const signed2 = await reviewer.signAgreement(signed1);

// Check agreement status
const status = await agent.checkAgreement(signed2);
console.log('Agreement status:', JSON.parse(status));
# Create second agent with separate config file
reviewer_config = config.copy()
reviewer_config["jacs_agent_id_and_version"] = None

with open('reviewer.config.json', 'w') as f:
    json.dump(reviewer_config, f, indent=2)

reviewer = jacs.JacsAgent()
reviewer.load("./reviewer.config.json")
reviewer.generate_keys()

reviewer_doc = reviewer.create_agent({
    "name": "Content Reviewer Bot", 
    "description": "AI agent specialized in content review"
})

# Create agreement between agents
agreement = {
    "title": "Content Collaboration Agreement",
    "question": "Do you agree to collaborate on this content task?",
    "context": f"Task: {signed_task['jacsId']}",
    "agents": [agent_doc["jacsId"], reviewer_doc["jacsId"]]
}

signed_agreement = agent.create_agreement(agreement)

# Both agents sign the agreement
agent.sign_agreement(signed_agreement["jacsId"])
reviewer.sign_agreement(signed_agreement["jacsId"])

# Verify all signatures
agreement_valid = agent.verify_agreement(signed_agreement["jacsId"])
print(f'Agreement complete: {agreement_valid}')

What You've Accomplished

Congratulations! You've successfully:

βœ… Created JACS agents with cryptographic identities βœ… Generated and signed documents with verifiable integrity
βœ… Established multi-agent agreements with cryptographic consent βœ… Verified signatures and document authenticity βœ… Created an audit trail of all interactions

Key Takeaways

  • Everything is verifiable: All documents have cryptographic signatures
  • Agents are autonomous: Each has its own identity and keys
  • Agreements enable trust: Multi-party consent before proceeding
  • Audit trails are automatic: Complete history of all interactions
  • JSON is universal: Documents work everywhere

Where to Go Next

Now that you have the basics working:

  1. Verify Signed Documents - Verify any document from CLI, Python, or Node.js -- no agent required
  2. A2A Quickstart - Make your agent discoverable by other A2A agents in minutes
  3. Framework Adapters - Add auto-signing to LangChain, FastAPI, CrewAI, or Anthropic SDK in 1-3 lines
  4. Multi-Agent Agreements - Cross-trust-boundary verification with quorum and timeout
  5. Rust Deep Dive - Learn the full Rust API
  6. Node.js Integration - Add MCP support
  7. Python MCP - Build authenticated MCP servers
  8. Production Security - Harden runtime settings and key management
  9. Real Examples - See production patterns

Troubleshooting

Agent creation fails: Check that the data and key directories exist and are writable Signature verification fails: Ensure public keys are properly stored and accessible Agreement signing fails: Verify all agent IDs are correct and agents exist Documents not found: Check the data directory configuration

Need help? Check the GitHub issues or review the detailed implementation guides.

Multi-Agent Agreements

Three agents from different organizations sign an agreement with 2-of-3 quorum.

Imagine three departments -- Finance, Compliance, and Legal -- must approve a production deployment. Requiring all three creates bottlenecks. With JACS quorum agreements, any two of three is sufficient: cryptographically signed, independently verifiable, with a full audit trail.

No central authority. No shared database. Every signature is independently verifiable.

The Lifecycle

Create Agreement --> Agent A Signs --> Agent B Signs --> Quorum Met (2/3) --> Verified

Python

from jacs.client import JacsClient

# Step 1: Create three agents (one per organization)
finance = JacsClient.quickstart(
    name="finance",
    domain="finance.example.com",
    algorithm="ring-Ed25519",
    config_path="./finance.config.json",
)
compliance = JacsClient.quickstart(
    name="compliance",
    domain="compliance.example.com",
    algorithm="ring-Ed25519",
    config_path="./compliance.config.json",
)
legal = JacsClient.quickstart(
    name="legal",
    domain="legal.example.com",
    algorithm="ring-Ed25519",
    config_path="./legal.config.json",
)

# Step 2: Finance proposes an agreement with quorum
from datetime import datetime, timedelta, timezone

proposal = {
    "action": "Deploy model v2 to production",
    "conditions": ["passes safety audit", "approved by 2 of 3 signers"],
}
deadline = (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat()

agreement = finance.create_agreement(
    document=proposal,
    agent_ids=[finance.agent_id, compliance.agent_id, legal.agent_id],
    question="Do you approve deployment of model v2?",
    context="Production rollout pending safety audit sign-off.",
    quorum=2,           # only 2 of 3 need to sign
    timeout=deadline,
)

# Step 3: Finance signs
agreement = finance.sign_agreement(agreement)

# Step 4: Compliance co-signs -- quorum is now met
agreement = compliance.sign_agreement(agreement)

# Step 5: Verify -- any party can confirm independently
status = finance.check_agreement(agreement)
print(f"Complete: {status.complete}")  # True -- 2 of 3 signed

for s in status.signers:
    label = "signed" if s.signed else "pending"
    print(f"  {s.agent_id[:12]}... {label}")

Node.js / TypeScript

import { JacsClient } from "@hai.ai/jacs/client";

async function main() {
  // Step 1: Create three agents
  const finance    = await JacsClient.ephemeral("ring-Ed25519");
  const compliance = await JacsClient.ephemeral("ring-Ed25519");
  const legal      = await JacsClient.ephemeral("ring-Ed25519");

  // Step 2: Finance proposes an agreement with quorum
  const proposal = {
    action: "Deploy model v2 to production",
    conditions: ["passes safety audit", "approved by 2 of 3 signers"],
  };
  const deadline = new Date(Date.now() + 60 * 60 * 1000).toISOString();
  const agentIds = [finance.agentId, compliance.agentId, legal.agentId];

  let agreement = await finance.createAgreement(proposal, agentIds, {
    question: "Do you approve deployment of model v2?",
    context: "Production rollout pending safety audit sign-off.",
    quorum: 2,
    timeout: deadline,
  });

  // Step 3: Finance signs
  agreement = await finance.signAgreement(agreement);

  // Step 4: Compliance co-signs -- quorum is now met
  agreement = await compliance.signAgreement(agreement);

  // Step 5: Verify
  const doc = JSON.parse(agreement.raw);
  const ag = doc.jacsAgreement;
  const sigCount = ag.signatures?.length ?? 0;
  console.log(`Signatures: ${sigCount} of ${agentIds.length}`);
  console.log(`Quorum met: ${sigCount >= (ag.quorum ?? agentIds.length)}`);
}

main().catch(console.error);

What Just Happened?

  1. Three independent agents were created, each with their own keys -- no shared secrets.
  2. Finance proposed an agreement requiring 2-of-3 quorum with a one-hour deadline.
  3. Finance and Compliance signed. Legal never needed to act -- quorum was met.
  4. Any party can verify the agreement independently. The cryptographic proof chain is self-contained.

Every signature includes: the signer's agent ID, the signing algorithm, a timestamp, and a hash of the agreement content. If anyone tampers with the document after signing, verification fails.

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

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');

Generate a URL that lets anyone verify a signed document through a web verifier (e.g., hai.ai):

Python:

url = jacs.generate_verify_link(signed_doc.raw_json)
# https://hai.ai/jacs/verify?s=<base64url-encoded-document>

Node.js:

const url = jacs.generateVerifyLink(signed.raw);

The document is base64url-encoded into the URL query parameter. Documents must be under ~1.5 KB to fit within the 2048-character URL limit. For larger documents, share the file directly and verify with the CLI or SDK.

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.

This is tested on every commit: Rust generates signed fixtures, then Python calls verify_standalone() and Node.js calls verifyStandalone() to verify them. 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. Every JACS document carries a cryptographic signature that proves which agent created it and that the content hasn't changed. 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
verifiedAgent must have the urn:jacs:provenance-v1 extension
strictAgent must be in the local trust store

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 includes native bindings (Rust compiled to platform-specific libraries), so deployment depends on pre-built binary availability for your target platform.

Supported Platforms

PlatformLanguageNotes
Linux (x86_64, aarch64)AllPrimary target
macOS (Apple Silicon, Intel)AllFull support
Windows (x86_64)Rust, Node.jsPython wheels may need manual build
AWS LambdaPython, Node.jsUse Lambda layers for native deps
Docker / KubernetesAllStandard containerization
Vercel (Node.js runtime)Node.jsVia serverless functions

Not Yet Supported

PlatformWhyWorkaround
Cloudflare WorkersNo native module support (WASM-only)Use a proxy service
Deno DeployNo native Node.js addonsUse Deno with --allow-ffi locally
BunNative builds may failUse Node.js runtime instead
Browser / WASMPost-quantum crypto not available in WASMPlanned for a future release

Version Requirements

LanguageMinimum Version
Rust1.93+ (edition 2024)
Python3.10+
Node.js18+ (LTS recommended)

Docker Example

FROM python:3.12-slim
RUN pip install jacs
COPY . /app
WORKDIR /app
RUN python -c "import jacs.simple as j; j.quickstart(name='docker-agent', domain='docker.local')"
CMD ["python", "main.py"]

Lambda Deployment

For AWS Lambda, include the JACS native library in a Lambda layer or bundle it in your deployment package. Set JACS_PRIVATE_KEY_PASSWORD as a Lambda environment variable (use AWS Secrets Manager for production).

Building from Source

If no pre-built binary exists for your platform:

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

# Node.js
cd jacsnpm && npm run build

Requires Rust 1.93+ toolchain installed via rustup.

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. Set exactly one explicit source; if both are set, CLI fails by design.

Algorithm detection failed

Set the signingAlgorithm field in your config, or pass it explicitly to quickstart(...) / create(...). Valid values: pq2025, ring-Ed25519, RSA-PSS.

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.93+ (install via rustup).

Getting Help

Installation

This guide covers installing the JACS Rust CLI and library.

Requirements

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

Verify Rust Version

rustc --version
# Should show rustc 1.93.0 or later

If you need to update Rust:

rustup update stable

Installing the CLI

cargo install jacs-cli

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 mcp

Using as a Library

Add JACS to your Cargo.toml:

[dependencies]
jacs = "0.3"

With Optional Features

JACS supports several optional features for observability and integrations:

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

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

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

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

# With all observability features
jacs = { version = "0.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)
agreementsAgreement lifecycle support
a2aAgent-to-Agent protocol support
attestationAttestation support

Platform Support

JACS supports the following platforms:

PlatformArchitectureSupport
Linuxx86_64, aarch64Full support
macOSx86_64, aarch64Full support
Windowsx86_64Full support
WebAssemblywasm32Partial (no post-quantum crypto, limited storage)

WebAssembly Notes

When targeting WebAssembly, some features are unavailable:

  • Post-quantum cryptographic algorithms (pq2025, legacy pq-dilithium)
  • File system storage backend
  • HTTP-based remote operations

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 (ring-Ed25519, RSA-PSS, pq2025, legacy pq-dilithium)ring-Ed25519

Troubleshooting

Build Errors

"edition 2024 is required" Update Rust to version 1.93 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, tasks, and agreements.

Getting Help

# General help
jacs --help

# Command-specific help
jacs agent --help
jacs document --help
jacs task --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 taskManage tasks
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 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

Task Commands

Create Task

jacs task create -n "Task Name" -d "Task description"

# With optional agent file
jacs task create -n "Task Name" -d "Description" -a ./agent.json

# With input file
jacs task create -n "Task Name" -d "Description" -f ./task-details.json

Options:

OptionShortRequiredDescription
-n--nameYesName of the task
-d--descriptionYesDescription of the task
-a--agent-fileNoPath to agent file
-f--filenameNoPath to JSON file with additional task data

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 autonomous entity that can create, sign, and verify documents. This guide covers creating and managing agents.

What is an Agent?

A JACS agent is:

  • A unique identity with a UUID that never changes
  • A holder of cryptographic keys for signing
  • A provider of services defined in the agent document
  • Self-signed to prove authenticity

Creating Your First Agent

# Initialize JACS (creates config and agent)
jacs init

This creates:

  • Configuration file
  • Cryptographic key pair
  • Initial agent document

Manual Method

# 1. Create configuration
jacs config create

# 2. Create agent with new keys
jacs agent create --create-keys true

With Custom Agent Definition

Create an agent definition file (my-agent.json):

{
  "$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",
  "jacsServices": [
    {
      "name": "content-generation",
      "serviceDescription": "Generate high-quality content",
      "successDescription": "Engaging, accurate content delivered",
      "failureDescription": "Unable to generate requested content"
    }
  ]
}

Then create the agent:

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

Agent Types

JACS supports four agent types:

TypeDescriptionContacts Required
aiFully artificial intelligenceNo
humanIndividual personYes
human-orgGroup of people (organization)Yes
hybridHuman-AI combinationYes

AI Agent Example

{
  "$schema": "https://hai.ai/schemas/agent/v1/agent.schema.json",
  "jacsAgentType": "ai",
  "name": "DataBot",
  "description": "Data processing agent",
  "jacsServices": [
    {
      "name": "data-processing",
      "serviceDescription": "Process and transform data",
      "successDescription": "Data transformed successfully",
      "failureDescription": "Input data could not be processed"
    }
  ]
}

Human Agent Example

{
  "$schema": "https://hai.ai/schemas/agent/v1/agent.schema.json",
  "jacsAgentType": "human",
  "name": "John Smith",
  "description": "Software engineer",
  "jacsContacts": [
    {
      "firstName": "John",
      "lastName": "Smith",
      "email": "john@example.com",
      "isPrimary": true
    }
  ],
  "jacsServices": [
    {
      "name": "code-review",
      "serviceDescription": "Review code for quality and security",
      "successDescription": "Actionable review delivered",
      "failureDescription": "Could not complete review"
    }
  ]
}

Agent Services

Services define what an agent can do. Each service has:

{
  "name": "service-identifier",
  "serviceDescription": "What the service does",
  "successDescription": "Definition of successful completion",
  "failureDescription": "What constitutes failure"
}

Detailed Service Example

{
  "name": "document-processing",
  "serviceDescription": "Process and analyze documents",
  "successDescription": "Documents processed accurately",
  "failureDescription": "Unable to process one or more documents",
  "costDescription": "Usage-based pricing",
  "privacyPolicy": "https://example.com/privacy",
  "termsOfService": "https://example.com/terms"
}

Agent Contacts

For human and hybrid agents, contacts are required:

{
  "jacsContacts": [
    {
      "firstName": "Example",
      "lastName": "Agent",
      "email": "agent@example.com",
      "phone": "+1-555-0123",
      "isPrimary": true
    }
  ]
}

Cryptographic Keys

Key Algorithms

JACS supports multiple cryptographic algorithms:

AlgorithmDescriptionRecommended For
ring-Ed25519Fast elliptic curve signaturesGeneral use (default)
RSA-PSSTraditional RSA signaturesLegacy compatibility
pq2025Post-quantum ML-DSA-87 signaturesFuture-proof security
pq-dilithiumLegacy post-quantum signaturesBackward compatibility only (deprecated)

Configure Key Algorithm

In jacs.config.json:

{
  "jacs_agent_key_algorithm": "ring-Ed25519"
}

Or via environment variable:

JACS_AGENT_KEY_ALGORITHM=ring-Ed25519 jacs agent create --create-keys true

Key Storage

Keys are stored in the key directory (default: ./jacs_keys):

jacs_keys/
β”œβ”€β”€ private_key.pem    # Private key (keep secure!)
└── public_key.pem     # Public key (can be shared)

Verifying Agents

Verify Your Own Agent

jacs agent verify

Verify a Specific Agent File

jacs agent verify -a ./path/to/agent.json

With DNS Verification

# Require DNS validation
jacs agent verify --require-dns

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

Updating Agents

Agent updates create a new version while maintaining the same jacsId:

  1. Modify the agent document
  2. Re-sign with the agent's keys

The jacsVersion changes but jacsId remains constant.

Agent Document Structure

A complete agent document looks like:

{
  "$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",

  "jacsServices": [
    {
      "name": "content-generation",
      "serviceDescription": "Generate high-quality content",
      "successDescription": "High-quality content generated",
      "failureDescription": "Unable to generate requested content"
    }
  ],

  "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", "name", "jacsServices"]
  }
}

Best Practices

Security

  1. Protect private keys: Never share or commit private keys
  2. Use strong algorithms: Prefer Ed25519 or post-quantum
  3. Enable DNS verification: For production agents
  4. Regular key rotation: Update keys periodically

Agent Design

  1. Clear service definitions: Be specific about capabilities
  2. Meaningful names: Use descriptive agent names
  3. Contact information: Include for human agents
  4. Version control: Track agent document changes

Operations

  1. Backup keys: Keep secure backups of private keys
  2. Monitor signatures: Watch for unauthorized signing
  3. Document services: Keep service definitions current

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 signature, producing a signed document.

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 Types

Task Documents

Tasks are specialized documents for work tracking:

jacs task create -n "Code Review" -d "Review PR #123"

See Task Schema for details.

Message Documents

Messages for agent communication:

{
  "$schema": "https://hai.ai/schemas/message/v1/message.schema.json",
  "jacsType": "message",
  "jacsMessageContent": "Hello, I've completed the task.",
  "jacsMessageReplyTo": "previous-message-uuid"
}

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

Agreements enable multi-party consent in JACS. They allow multiple agents to cryptographically sign a document, creating binding commitments between parties.

What is an Agreement?

An agreement is a mechanism for:

  • Collecting signatures from multiple agents
  • Tracking consent from required parties
  • Enforcing completion before proceeding
  • Creating audit trails of who agreed and when

Agreement Lifecycle

1. Create Agreement β†’ 2. Distribute β†’ 3. Agents Sign β†’ 4. Verify Complete
  1. Create: Initial agent creates agreement with required participants
  2. Distribute: Agreement document shared with all parties
  3. Sign: Each agent reviews and adds their signature
  4. Verify: Check that all required parties have signed

Creating Agreements

Basic Agreement

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

With Context

Include a question and context for clarity:

{
  "jacsAgreement": {
    "jacsAgreementQuestion": "Do you agree to the terms of this contract?",
    "jacsAgreementContext": "Service agreement for Q1 2024",
    "jacsAgreementAgents": ["agent1-uuid", "agent2-uuid"]
  }
}

Signing Agreements

Sign as Current Agent

jacs document sign-agreement -f ./document-with-agreement.json

Sign as Different Agent

# Use a different configuration/agent
JACS_CONFIG_PATH=./agent2.config.json jacs document sign-agreement -f ./document.json

Sign with Response

When signing, agents can include a response:

{
  "jacsAgreement": {
    "signatures": {
      "agent1-uuid": {
        "agentID": "agent1-uuid",
        "signature": "base64-signature",
        "date": "2024-01-15T10:30:00Z",
        "response": "Agreed with minor reservation about timeline",
        "responseType": "agree"
      }
    }
  }
}

Response types:

  • agree - Agent consents
  • disagree - Agent does not consent
  • reject - Agent considers the question invalid or irrelevant

Checking Agreement Status

Check if Complete

jacs document check-agreement -f ./document.json

This shows:

  • Which agents have signed
  • Which agents still need to sign
  • Whether the agreement is complete

Agreement Structure

A document with an agreement includes:

{
  "jacsId": "doc-uuid",
  "jacsType": "contract",

  "jacsAgreement": {
    "jacsAgreementQuestion": "Do you agree to these terms?",
    "jacsAgreementContext": "Annual service contract",
    "jacsAgreementAgents": [
      "550e8400-e29b-41d4-a716-446655440000",
      "123e4567-e89b-12d3-a456-426614174000"
    ],
    "signatures": {
      "550e8400-e29b-41d4-a716-446655440000": {
        "agentID": "550e8400-e29b-41d4-a716-446655440000",
        "agentVersion": "version-uuid",
        "signature": "base64-signature",
        "signingAlgorithm": "ring-Ed25519",
        "publicKeyHash": "hash",
        "date": "2024-01-15T10:30:00Z",
        "responseType": "agree",
        "fields": ["jacsId", "jacsAgreement"]
      }
    }
  },

  "jacsAgreementHash": "hash-of-agreement-content"
}

Task Agreements

Tasks have built-in support for start and end agreements:

{
  "jacsType": "task",
  "jacsTaskName": "Code Review",

  "jacsStartAgreement": {
    "jacsAgreementQuestion": "Do you agree to start this task?",
    "jacsAgreementAgents": ["customer-uuid", "provider-uuid"]
  },

  "jacsEndAgreement": {
    "jacsAgreementQuestion": "Do you agree the task is complete?",
    "jacsAgreementAgents": ["customer-uuid", "provider-uuid"]
  }
}

Multi-Agent Workflow Example

# 1. Agent A creates a task
jacs task create -n "Write Report" -d "Quarterly sales report"

# 2. Agent A adds agreement requiring both agents
jacs document create-agreement \
  -f ./task.json \
  -i agent-a-uuid,agent-b-uuid

# 3. Agent A signs the agreement
jacs document sign-agreement -f ./task.json

# 4. Agent B signs the agreement
JACS_CONFIG_PATH=./agent-b.config.json \
  jacs document sign-agreement -f ./task.json

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

Agreement Hash

The jacsAgreementHash ensures all agents agree to the same content:

  1. Hash is computed from the agreement content
  2. Each signature includes the hash
  3. If content changes, hash changes, invalidating existing signatures

This prevents modifications after some parties have signed.

Agreement Options (v0.6.2+)

Timeout

Set a deadline after which the agreement expires:

# Python
agreement = client.create_agreement(
    document=proposal,
    agent_ids=[alice.agent_id, bob.agent_id],
    timeout="2025-12-31T23:59:59Z"
)

If the deadline passes before all required signatures are collected, check_agreement() returns an error.

Quorum (M-of-N Signing)

Require only a subset of agents to sign:

# Only 2 of 3 agents need to sign
agreement = client.create_agreement(
    document=proposal,
    agent_ids=[alice.agent_id, bob.agent_id, carol.agent_id],
    quorum=2
)

When quorum is met, check_agreement() succeeds even if some agents haven't signed.

Algorithm Constraints

Enforce that only specific cryptographic algorithms can be used:

# Only post-quantum algorithms allowed
agreement = client.create_agreement(
    document=proposal,
    agent_ids=agent_ids,
    required_algorithms=["pq2025", "pq-dilithium"],
    minimum_strength="post-quantum"
)

An agent using RSA-PSS or Ed25519 will be rejected when trying to sign this agreement.

Combined Options

agreement = client.create_agreement(
    document={"proposal": "Deploy model v2"},
    agent_ids=[alice.agent_id, bob.agent_id, mediator.agent_id],
    question="Do you approve deployment?",
    timeout="2025-06-30T00:00:00Z",
    quorum=2,
    minimum_strength="post-quantum"
)

Using JacsClient (Instance-Based API)

For running multiple agents in one process, use JacsClient instead of the module-level API:

Python

from jacs.client import JacsClient

alice = JacsClient.ephemeral("ring-Ed25519")  # for testing
bob = JacsClient.ephemeral("ring-Ed25519")

signed = alice.sign_message({"action": "approve"})
# alice.agent_id, bob.agent_id are unique

See the full example: examples/multi_agent_agreement.py

Node.js

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

const alice = JacsClient.ephemeral('ring-Ed25519');
const bob = JacsClient.ephemeral('ring-Ed25519');

const signed = alice.signMessage({ action: 'approve' });

See the full example: examples/multi_agent_agreement.ts

MCP Tools for Agreements

The JACS MCP server exposes agreement tools that LLMs can use directly:

ToolDescription
jacs_create_agreementCreate agreement with quorum, timeout, algorithm constraints
jacs_sign_agreementCo-sign an agreement
jacs_check_agreementCheck status: who signed, quorum met, expired

See MCP Integration for setup.

Best Practices

  1. Verify before signing: Always review documents before signing
  2. Check agent identities: Verify who you're agreeing with (use DNS)
  3. Include context: Make the agreement purpose clear
  4. Handle disagreement: Have a process for when agents disagree
  5. Use quorum for resilience: Don't require unanimous consent unless necessary
  6. Set timeouts: Prevent agreements from hanging indefinitely
  7. Enforce post-quantum for sensitive agreements: Use minimum_strength: "post-quantum" for long-term security

Next Steps

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",
  "jacsServices": [...]
}

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)
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
)?;
}

Creating Tasks

use jacs::{get_empty_agent, create_task};

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 task
    let task_json = create_task(
        &mut agent,
        "Review Code".to_string(),
        "Review pull request #123".to_string(),
    )?;

    println!("Task created: {}", task_json);

    Ok(())
}

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, create_task};
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)?;

    // Create a task
    let task = create_task(
        &mut agent,
        "Review Proposal".to_string(),
        "Review and approve the project proposal".to_string(),
    )?;

    println!("Task created");

    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

The JACS Node.js package (@hai.ai/jacs) provides JavaScript/TypeScript bindings to the JACS Rust library, making it easy to integrate JACS into web applications, servers, and Node.js projects.

Requirements

  • Node.js: Version 16.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

RSA-PSS

{
  "jacs_agent_key_algorithm": "RSA-PSS"
}

Pros: Widely supported, proven security Cons: Larger signatures, slower

pq-dilithium (Post-Quantum)

{
  "jacs_agent_key_algorithm": "pq-dilithium"
}

Pros: Quantum-resistant Cons: Experimental, large signatures

pq2025 (Post-Quantum Hybrid)

{
  "jacs_agent_key_algorithm": "pq2025"
}

Pros: Combines ML-DSA-87 with hybrid approach Cons: Newest algorithm, largest signatures

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 16+

# 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

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: 'ring-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
generateVerifyLink(doc, baseUrl?)Generate verification URL

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". Also: "ring-Ed25519", "RSA-PSS".

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: 'ring-Ed25519',
});

// Sync variant (blocks event loop)
const info = jacs.quickstartSync({
  name: 'my-agent',
  domain: 'my-agent.example.com',
  algorithm: 'ring-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);

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

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

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 task document
  const task = {
    title: 'Code Review',
    description: 'Review pull request #123',
    assignee: 'developer-uuid',
    deadline: '2024-02-01'
  };

  const signedTask = await agent.createDocument(JSON.stringify(task));
  console.log('Task created');

  // Verify the task
  if (await agent.verifyDocument(signedTask)) {
    console.log('Task signature valid');
  }

  // Create agreement for task acceptance
  const taskWithAgreement = await agent.createAgreement(
    signedTask,
    ['manager-uuid', 'developer-uuid'],
    'Do you accept this task assignment?'
  );

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

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

  // Hash some data for reference
  const taskHash = hashString(signedTask);
  console.log('Task hash:', taskHash);
}

main().catch(console.error);

Next Steps

MCP Integration (Node.js)

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, prefer the Rust jacs-mcp binary.

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
  • audit and 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

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_audit
  • 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

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

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

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

JACS provides middleware and utilities for building HTTP servers with cryptographic request/response signing. This enables secure communication between JACS agents over HTTP.

Overview

JACS HTTP integration provides:

  • Request signing: Sign outgoing HTTP requests with your agent's key
  • Request verification: Verify incoming requests were signed by a valid agent
  • Response signing: Automatically sign responses before sending
  • Response verification: Verify server responses on the client side
  • Framework middleware: Ready-to-use middleware for Express and Koa

Core Concepts

Request/Response Flow

Client                          Server
  |                               |
  |-- signRequest(payload) -----> |
  |                               |-- verifyResponse() --> payload
  |                               |-- process payload
  |                               |-- signResponse(result)
  |<-- verifyResponse(result) ---|
  |

All messages are cryptographically signed, ensuring:

  • Message integrity (no tampering)
  • Agent identity (verified sender)
  • Non-repudiation (proof of origin)

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.

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.

const result = await jacs.verifyResponse(jacsResponseString);
// Returns: { payload: {...}, jacsId: '...', ... }

const payload = result.payload;

jacs.signResponse(payload)

Sign an object as a JACS response.

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

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(...)

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., "task", "message")
  • 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' }),
  'task'
);

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

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, CrewAI, 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

RSA-PSS

{
  "jacs_agent_key_algorithm": "RSA-PSS"
}

Pros: Widely supported, proven security Cons: Larger signatures, slower

pq-dilithium (Post-Quantum)

{
  "jacs_agent_key_algorithm": "pq-dilithium"
}

Pros: Quantum-resistant Cons: Experimental, large signatures

pq2025 (Post-Quantum Hybrid)

{
  "jacs_agent_key_algorithm": "pq2025"
}

Pros: Combines ML-DSA-87 with hybrid approach Cons: Newest algorithm, largest signatures

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="ring-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". Also: "ring-Ed25519", "RSA-PSS".
  • 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="ring-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


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["jacsContacts"] = [{"contactFirstName": "Jane", "contactLastName": "Doe"}]

# 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"
if not agent_doc.get("jacsContacts") or len(agent_doc["jacsContacts"]) == 0:
    agent_doc["jacsContacts"] = [{"contactFirstName": "AI", "contactLastName": "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

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

Request/Response Signing

Sign a Request

# Sign request parameters as a JACS document
signed_request = agent.sign_request({
    "method": "GET",
    "path": "/api/resource",
    "timestamp": datetime.now().isoformat(),
    "body": {"query": "data"}
})

Verify a Response

# Verify a signed response
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 task document
    task = {
        "title": "Code Review",
        "description": "Review pull request #123",
        "assignee": "developer-uuid",
        "deadline": "2024-02-01"
    }

    signed_task = agent.create_document(json.dumps(task))
    print('Task created')

    # Verify the task
    if agent.verify_document(signed_task):
        print('Task signature valid')

    # Create agreement for task acceptance
    task_with_agreement = agent.create_agreement(
        signed_task,
        ['manager-uuid', 'developer-uuid'],
        'Do you accept this task assignment?'
    )

    # Sign the agreement
    signed_agreement = agent.sign_agreement(task_with_agreement)
    print('Agreement signed')

    # Check agreement status
    status = agent.check_agreement(signed_agreement)
    print('Status:', status)

    # Hash some data for reference
    task_hash = jacs.hash_string(signed_task)
    print('Task hash:', task_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
  • strict=True is about config loading and failure behavior, not an opt-in to security

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

@mcp.tool()
def hello(name: str) -> str:
    return f"Hello, {name}!"

If you already have a FastMCP instance:

from fastmcp import FastMCP
from jacs.mcp import JACSMCPServer

mcp = JACSMCPServer(FastMCP("Secure Server"), "./jacs.config.json")

2. Secure A FastMCP Client

from jacs.mcp import JACSMCPClient

client = JACSMCPClient("http://localhost:8000/sse", "./jacs.config.json")

async with client:
    result = await client.call_tool("hello", {"name": "World"})

To allow unsigned fallback explicitly:

client = JACSMCPClient(
    "http://localhost:8000/sse",
    "./jacs.config.json",
    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

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, CrewAI, 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 CrewAI task outputjacs_guardrail, signed_taskCrewAI 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[crewai]
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:

  • strict=True to reject verification failures instead of passing through
  • sign_responses=False or verify_requests=False to narrow the behavior
  • a2a=True to also expose A2A discovery routes from the same FastAPI app

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}

CrewAI

CrewAI support is guardrail-first:

from crewai import Task
from jacs.adapters.crewai import jacs_guardrail

task = Task(
    description="Summarize the report",
    agent=my_agent,
    guardrail=jacs_guardrail(client=client),
)

If you build tasks with factories, signed_task() can pre-attach the guardrail.

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)

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., "task", "message")
  • 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"}),
    "task"
)

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.

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.

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.

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.

signed = jacs.sign_response({'result': 'success'})

jacs.verify_response(data)

Verify an incoming response using the global agent.

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(mcp_server, config_path="./jacs.config.json", strict=False) - Wrap a FastMCP server with JACS request verification and response signing.
  • JACSMCPClient(url, config_path="./jacs.config.json", strict=False, **kwargs) - Create a FastMCP client with JACS signing/verification interceptors.
  • create_jacs_mcp_server(name, config_path=None) - One-line server factory.
  • jacs_call(server_url, method, **params) - One-shot authenticated MCP call.

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", "RSA-PSS", "pq-dilithium", "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 full MCP surface use the Rust jacs-mcp server; the Go MCP examples in the repo are demo code.

Install

go get github.com/HumanAssisted/JACS/jacsgo

Minimal Sign + Verify

Create an agent first (CLI: jacs create --name my-agent, 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 create --name my-agent")
	}

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

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 uses JSON Schema (Draft-07) to define the structure and validation rules for all documents in the system. These schemas ensure consistency, enable validation, and provide a contract for interoperability between agents.

Schema Architecture

JACS schemas follow a hierarchical composition pattern:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Document Schemas                      β”‚
β”‚  (agent.schema.json, task.schema.json, message.schema.json)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   Header Schema                          β”‚
β”‚              (header.schema.json)                        β”‚
β”‚  Base fields: jacsId, jacsVersion, jacsSignature, etc.  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 Component Schemas                        β”‚
β”‚   signature.schema.json, agreement.schema.json,         β”‚
β”‚   files.schema.json, embedding.schema.json, etc.        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Schema Categories

Configuration Schema

SchemaPurpose
jacs.config.schema.jsonAgent configuration file format

Document Schemas

SchemaPurpose
header/v1/header.schema.jsonBase fields for all JACS documents
agent/v1/agent.schema.jsonAgent identity and capabilities
task/v1/task.schema.jsonTask workflow and state management
message/v1/message.schema.jsonInter-agent messages
node/v1/node.schema.jsonGraph node representation
program/v1/program.schema.jsonExecutable program definitions
eval/v1/eval.schema.jsonEvaluation and assessment records
agentstate/v1/agentstate.schema.jsonSigned agent state files (memory, skills, plans, configs, hooks, other)
commitment/v1/commitment.schema.jsonShared, signed agreements between agents
todo/v1/todo.schema.jsonPrivate, signed todo lists with inline items

Component Schemas

SchemaPurpose
signature/v1/signature.schema.jsonCryptographic signatures
agreement/v1/agreement.schema.jsonMulti-party agreements
files/v1/files.schema.jsonFile attachments
embedding/v1/embedding.schema.jsonVector embeddings
contact/v1/contact.schema.jsonContact information
service/v1/service.schema.jsonService definitions
tool/v1/tool.schema.jsonTool capabilities
action/v1/action.schema.jsonAction definitions
unit/v1/unit.schema.jsonUnit of measurement
todoitem/v1/todoitem.schema.jsonInline todo item (goal or task)

Schema Locations

Schemas are available at:

  • HTTPS URLs: https://hai.ai/schemas/...
  • Local files: jacs/schemas/...

Example schema URLs:

https://hai.ai/schemas/jacs.config.schema.json
https://hai.ai/schemas/header/v1/header.schema.json
https://hai.ai/schemas/agent/v1/agent.schema.json
https://hai.ai/schemas/components/signature/v1/signature.schema.json

Using Schemas

In Documents

Every JACS document must include a $schema field:

{
  "$schema": "https://hai.ai/schemas/agent/v1/agent.schema.json",
  "jacsId": "550e8400-e29b-41d4-a716-446655440000",
  "jacsVersion": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "jacsType": "agent",
  ...
}

In Configuration Files

Reference the config schema for IDE support:

{
  "$schema": "https://hai.ai/schemas/jacs.config.schema.json",
  "jacs_data_directory": "./jacs_data",
  "jacs_key_directory": "./jacs_keys",
  ...
}

Custom Schema Validation

Validate documents against custom schemas:

import jacs
import json

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

# Create document with custom schema
doc = agent.create_document(
    json.dumps({'invoice_id': 'INV-001', 'amount': 100.00}),
    custom_schema='./schemas/invoice.schema.json'
)
import { JacsAgent } from '@hai.ai/jacs';

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

// Create document with custom schema
const doc = agent.createDocument(
  JSON.stringify({ invoice_id: 'INV-001', amount: 100.00 }),
  './schemas/invoice.schema.json'
);

HAI Extensions

JACS schemas include a custom hai property that categorizes fields:

ValueDescription
metaMetadata fields (IDs, dates, versions)
baseCore cryptographic fields (hashes, signatures)
agentAgent-controlled content fields

This categorization helps determine which fields should be included in hash calculations and signature operations.

Versioning

Schemas are versioned using directory paths:

schemas/
β”œβ”€β”€ header/
β”‚   └── v1/
β”‚       └── header.schema.json
β”œβ”€β”€ agent/
β”‚   └── v1/
β”‚       └── agent.schema.json
└── components/
    └── signature/
        └── v1/
            └── signature.schema.json

Configuration options allow specifying schema versions:

{
  "jacs_agent_schema_version": "v1",
  "jacs_header_schema_version": "v1",
  "jacs_signature_schema_version": "v1"
}

Schema Composition

Document schemas use JSON Schema's allOf to compose the header with type-specific fields:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://hai.ai/schemas/agent/v1/agent.schema.json",
  "allOf": [
    { "$ref": "https://hai.ai/schemas/header/v1/header.schema.json" },
    {
      "type": "object",
      "properties": {
        "jacsAgentType": { ... },
        "jacsServices": { ... }
      }
    }
  ]
}

This ensures all documents share common header fields while allowing type-specific extensions.

Creating Custom Schemas

Create custom schemas that extend JACS schemas:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://example.com/schemas/invoice.schema.json",
  "title": "Invoice",
  "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,
          "description": "Invoice amount"
        },
        "currency": {
          "type": "string",
          "enum": ["USD", "EUR", "GBP"],
          "default": "USD"
        },
        "lineItems": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "description": { "type": "string" },
              "quantity": { "type": "integer", "minimum": 1 },
              "unitPrice": { "type": "number", "minimum": 0 }
            },
            "required": ["description", "quantity", "unitPrice"]
          }
        }
      },
      "required": ["invoiceNumber", "amount"]
    }
  ]
}

Validation Rules

Required Fields

All JACS documents require these header fields:

  • jacsId - Unique document identifier (UUID v4)
  • jacsType - Document type identifier
  • jacsVersion - Version identifier (UUID v4)
  • jacsVersionDate - Version timestamp (ISO 8601)
  • jacsOriginalVersion - First version identifier
  • jacsOriginalDate - Creation timestamp
  • jacsLevel - Document level (raw, config, artifact, derived)
  • $schema - Schema reference URL

Format Validation

Fields use JSON Schema format keywords:

  • uuid - UUID v4 format
  • date-time - ISO 8601 date-time format
  • uri - Valid URI format

Enum Constraints

Many fields have enumerated valid values:

{
  "jacsLevel": {
    "enum": ["raw", "config", "artifact", "derived"]
  },
  "jacsAgentType": {
    "enum": ["human", "human-org", "hybrid", "ai"]
  },
  "jacs_agent_key_algorithm": {
    "enum": ["RSA-PSS", "ring-Ed25519", "pq-dilithium", "pq2025"]
  }
}

Schema Reference

For detailed documentation on specific schemas:

See Also

Agent Schema

The Agent Schema defines the structure for agent identity documents in JACS. Agents represent entities that can sign documents, participate in agreements, and provide services.

Schema Location

https://hai.ai/schemas/agent/v1/agent.schema.json

Overview

Agent documents describe:

  • Identity: Unique identifiers and versioning
  • Type: Human, organizational, hybrid, or AI classification
  • Services: Capabilities the agent offers
  • Contacts: How to reach human or hybrid agents
  • Domain: Optional DNS-based verification

Schema Structure

The agent schema extends the Header Schema using JSON Schema composition:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://hai.ai/schemas/agent/v1/agent.schema.json",
  "title": "Agent",
  "description": "General schema for human, hybrid, and AI agents",
  "allOf": [
    { "$ref": "https://hai.ai/schemas/header/v1/header.schema.json" },
    { "type": "object", "properties": { ... } }
  ]
}

Agent Types

The jacsAgentType field classifies the agent:

TypeDescription
humanA biological entity (individual person)
human-orgA group of people (organization, company)
hybridCombination of human and AI components
aiFully artificial intelligence
{
  "jacsAgentType": {
    "type": "string",
    "enum": ["human", "human-org", "hybrid", "ai"]
  }
}

Contact Requirements

Human and hybrid agents must provide contact information:

{
  "if": {
    "properties": {
      "jacsAgentType": { "enum": ["human", "human-org", "hybrid"] }
    }
  },
  "then": {
    "required": ["jacsContacts"]
  }
}

Agent Properties

Core Fields (from Header)

All agents inherit these header 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
jacsSignatureobjectNoCryptographic signature
jacsSha256stringNoContent hash

Agent-Specific Fields

FieldTypeRequiredDescription
jacsAgentTypestringYesAgent classification
jacsAgentDomainstringNoDomain for DNS verification
jacsServicesarrayYesServices the agent provides
jacsContactsarrayConditionalContact information (required for human/hybrid)

Services

Services describe capabilities the agent offers:

{
  "jacsServices": [{
    "name": "Document Signing Service",
    "serviceDescription": "Sign and verify JACS documents",
    "successDescription": "Documents are signed with valid signatures",
    "failureDescription": "Invalid documents or signing errors",
    "costDescription": "Free for basic usage, paid tiers available",
    "idealCustomerDescription": "Developers building secure agent systems",
    "termsOfService": "https://example.com/tos",
    "privacyPolicy": "https://example.com/privacy",
    "isDev": false,
    "tools": [...]
  }]
}

Service Schema Fields

FieldTypeRequiredDescription
namestringNoService name
serviceDescriptionstringYesWhat the service does
successDescriptionstringYesWhat success looks like
failureDescriptionstringYesWhat failure looks like
costDescriptionstringNoPricing information
idealCustomerDescriptionstringNoTarget customer profile
termsOfServicestringNoLegal terms URL or text
privacyPolicystringNoPrivacy policy URL or text
copyrightstringNoUsage rights for provided data
eulastringNoEnd-user license agreement
isDevbooleanNoWhether this is a dev/test service
toolsarrayNoTool definitions
piiDesiredarrayNoTypes of sensitive data needed

PII Types

Services can declare what personally identifiable information they need:

{
  "piiDesired": ["email", "phone", "address"]
}

Valid PII types:

  • signature - Digital signatures
  • cryptoaddress - Cryptocurrency addresses
  • creditcard - Payment card numbers
  • govid - Government identification
  • social - Social security numbers
  • email - Email addresses
  • phone - Phone numbers
  • address - Physical addresses
  • zip - Postal codes
  • PHI - Protected health information
  • MHI - Mental health information
  • identity - Identity documents
  • political - Political affiliation
  • bankaddress - Banking information
  • income - Income data

Contacts

Contact information for human and hybrid agents:

{
  "jacsContacts": [{
    "firstName": "Jane",
    "lastName": "Smith",
    "email": "jane@example.com",
    "phone": "+1-555-0123",
    "isPrimary": true,
    "mailAddress": "123 Main St",
    "mailState": "CA",
    "mailZip": "94102",
    "mailCountry": "USA"
  }]
}

Contact Schema Fields

FieldTypeDescription
firstNamestringFirst name
lastNamestringLast name
addressNamestringLocation name
phonestringPhone number
emailstring (email)Email address
mailNamestringName at address
mailAddressstringStreet address
mailAddressTwostringAddress line 2
mailStatestringState/province
mailZipstringPostal code
mailCountrystringCountry
isPrimarybooleanPrimary contact flag

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.

See the DNS chapter for complete setup instructions.

Complete Example

AI Agent

{
  "$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",
  "jacsServices": [{
    "name": "Code Review Service",
    "serviceDescription": "Automated code review and analysis",
    "successDescription": "Review completed with actionable feedback",
    "failureDescription": "Unable to process or analyze code",
    "isDev": false
  }],
  "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", "jacsServices"]
  },
  "jacsSha256": "document-hash..."
}

Human Agent

{
  "$schema": "https://hai.ai/schemas/agent/v1/agent.schema.json",
  "jacsId": "660e8400-e29b-41d4-a716-446655440001",
  "jacsVersion": "a47ac10b-58cc-4372-a567-0e02b2c3d480",
  "jacsVersionDate": "2024-01-15T11:00:00Z",
  "jacsType": "agent",
  "jacsOriginalVersion": "a47ac10b-58cc-4372-a567-0e02b2c3d480",
  "jacsOriginalDate": "2024-01-15T11:00:00Z",
  "jacsLevel": "artifact",
  "jacsAgentType": "human",
  "jacsAgentDomain": "smith.example.com",
  "jacsServices": [{
    "name": "Consulting",
    "serviceDescription": "Technical consulting services",
    "successDescription": "Project goals achieved",
    "failureDescription": "Unable to meet requirements",
    "termsOfService": "https://smith.example.com/tos"
  }],
  "jacsContacts": [{
    "firstName": "John",
    "lastName": "Smith",
    "email": "john@smith.example.com",
    "isPrimary": true
  }]
}

Organization Agent

{
  "$schema": "https://hai.ai/schemas/agent/v1/agent.schema.json",
  "jacsId": "770e8400-e29b-41d4-a716-446655440002",
  "jacsVersion": "b47ac10b-58cc-4372-a567-0e02b2c3d481",
  "jacsVersionDate": "2024-01-15T12:00:00Z",
  "jacsType": "agent",
  "jacsOriginalVersion": "b47ac10b-58cc-4372-a567-0e02b2c3d481",
  "jacsOriginalDate": "2024-01-15T12:00:00Z",
  "jacsLevel": "artifact",
  "jacsAgentType": "human-org",
  "jacsAgentDomain": "acme.com",
  "jacsServices": [{
    "name": "Enterprise Software",
    "serviceDescription": "Enterprise software solutions",
    "successDescription": "Software deployed and operational",
    "failureDescription": "Deployment or integration failure",
    "privacyPolicy": "https://acme.com/privacy",
    "piiDesired": ["email", "phone"]
  }],
  "jacsContacts": [{
    "addressName": "Acme Corporation",
    "email": "contact@acme.com",
    "phone": "+1-800-555-ACME",
    "mailAddress": "1 Corporate Plaza",
    "mailState": "NY",
    "mailZip": "10001",
    "mailCountry": "USA",
    "isPrimary": true
  }]
}

Creating Agents

Python

import jacs
import json

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

# The agent is created during configuration setup
# Agent document is available after loading
agent_json = agent.load('./jacs.config.json')
agent_doc = json.loads(agent_json)

print(f"Agent ID: {agent_doc['jacsId']}")
print(f"Agent Type: {agent_doc['jacsAgentType']}")

Node.js

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

const agent = new JacsAgent();
const agentJson = agent.load('./jacs.config.json');
const agentDoc = JSON.parse(agentJson);

console.log(`Agent ID: ${agentDoc.jacsId}`);
console.log(`Agent Type: ${agentDoc.jacsAgentType}`);

CLI

# Create a new agent
jacs agent create

# View agent details
jacs agent show

Verifying Agents

# Verify the loaded agent
is_valid = agent.verify_agent()

# Verify another agent file
is_valid = agent.verify_agent('./other-agent.json')
// Verify the loaded agent
const isValid = agent.verifyAgent();

// Verify another agent file
const isOtherValid = agent.verifyAgent('./other-agent.json');

See Also

Document Schema

The Document Schema (Header Schema) defines the base structure for all JACS documents. Every JACS document type (agents, tasks, messages, etc.) extends this schema.

Schema Location

https://hai.ai/schemas/header/v1/header.schema.json

Overview

The header schema provides:

  • Unique Identification: Every document has a unique ID and version
  • Version Tracking: Full history with previous version references
  • Cryptographic Integrity: Signatures and hashes for verification
  • File Attachments: Support for embedded or linked files
  • Vector Embeddings: Pre-computed embeddings for semantic search
  • Agreements: Multi-party signature support

Core Fields

Identification

FieldTypeRequiredDescription
$schemastringYesSchema URL for validation
jacsIdstring (UUID)YesUnique document identifier
jacsTypestringYesDocument type (agent, task, etc.)
{
  "$schema": "https://hai.ai/schemas/header/v1/header.schema.json",
  "jacsId": "550e8400-e29b-41d4-a716-446655440000",
  "jacsType": "document"
}

Versioning

FieldTypeRequiredDescription
jacsVersionstring (UUID)YesCurrent version identifier
jacsVersionDatestring (date-time)YesVersion creation timestamp
jacsPreviousVersionstring (UUID)NoPrevious version (if not first)
jacsOriginalVersionstring (UUID)YesFirst version identifier
jacsOriginalDatestring (date-time)YesDocument creation timestamp
jacsBranchstring (UUID)NoBranch identifier for JACS databases
{
  "jacsVersion": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "jacsVersionDate": "2024-01-15T10:30:00Z",
  "jacsPreviousVersion": "e36ac10b-58cc-4372-a567-0e02b2c3d478",
  "jacsOriginalVersion": "a47ac10b-58cc-4372-a567-0e02b2c3d476",
  "jacsOriginalDate": "2024-01-01T09:00:00Z"
}

Document Level

The jacsLevel field indicates the intended use:

LevelDescription
rawRaw data that should not change
configConfiguration meant to be updated
artifactGenerated content that may be updated
derivedComputed from other documents
{
  "jacsLevel": "artifact"
}

Cryptographic Fields

Signature

The jacsSignature field contains the creator's cryptographic signature:

{
  "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"]
  }
}

Signature Schema Fields

FieldTypeRequiredDescription
agentIDstring (UUID)YesSigning agent's ID
agentVersionstring (UUID)YesSigning agent's version
datestring (date-time)YesSigning timestamp
signaturestringYesBase64-encoded signature
publicKeyHashstringYesHash of public key used
signingAlgorithmstringYesAlgorithm used (ring-Ed25519, RSA-PSS, pq2025; pq-dilithium is legacy/deprecated)
fieldsarrayYesFields included in signature
responsestringNoText response with signature
responseTypestringNoagree, disagree, or reject

Registration

The jacsRegistration field contains a signature from a registration authority:

{
  "jacsRegistration": {
    "agentID": "registrar-agent-id",
    "agentVersion": "registrar-version",
    "date": "2024-01-15T10:35:00Z",
    "signature": "registrar-signature",
    "publicKeyHash": "registrar-key-hash",
    "signingAlgorithm": "ring-Ed25519",
    "fields": ["jacsId", "jacsSignature"]
  }
}

Hash

The jacsSha256 field contains a SHA-256 hash of all document content (excluding this field):

{
  "jacsSha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}

Agreements

Documents can include multi-party agreements using jacsAgreement:

{
  "jacsAgreement": {
    "agentIDs": [
      "agent-1-uuid",
      "agent-2-uuid",
      "agent-3-uuid"
    ],
    "question": "Do you agree to these terms?",
    "context": "Q1 2024 Service Agreement",
    "signatures": [
      {
        "agentID": "agent-1-uuid",
        "signature": "...",
        "responseType": "agree",
        "date": "2024-01-15T11:00:00Z"
      }
    ]
  },
  "jacsAgreementHash": "hash-of-content-at-agreement-time"
}

Agreement Schema Fields

FieldTypeRequiredDescription
agentIDsarrayYesRequired signers
questionstringNoWhat parties are agreeing to
contextstringNoAdditional context
signaturesarrayNoCollected signatures

File Attachments

Documents can include file attachments using jacsFiles:

{
  "jacsFiles": [
    {
      "mimetype": "application/pdf",
      "path": "./documents/contract.pdf",
      "embed": true,
      "contents": "base64-encoded-file-contents",
      "sha256": "file-content-hash"
    },
    {
      "mimetype": "image/png",
      "path": "./images/diagram.png",
      "embed": false,
      "sha256": "file-content-hash"
    }
  ]
}

File Schema Fields

FieldTypeRequiredDescription
mimetypestringYesMIME type of the file
pathstringYesFile location (local path)
embedbooleanYesWhether to embed contents
contentsstringNoBase64-encoded file contents
sha256stringNoHash for content verification

Vector Embeddings

Documents can include pre-computed embeddings for semantic search:

{
  "jacsEmbedding": [
    {
      "llm": "text-embedding-ada-002",
      "vector": [0.0023, -0.0089, 0.0156, ...]
    }
  ]
}

Embedding Schema Fields

FieldTypeRequiredDescription
llmstringYesModel used for embedding
vectorarrayYesVector of numbers

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",

  "title": "Sample Document",
  "content": "This is the document content.",

  "jacsFiles": [
    {
      "mimetype": "application/pdf",
      "path": "./attachment.pdf",
      "embed": false,
      "sha256": "abc123..."
    }
  ],

  "jacsSignature": {
    "agentID": "550e8400-e29b-41d4-a716-446655440000",
    "agentVersion": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "date": "2024-01-15T10:30:00Z",
    "signature": "signature-base64...",
    "publicKeyHash": "key-hash...",
    "signingAlgorithm": "ring-Ed25519",
    "fields": ["jacsId", "jacsVersion", "title", "content"]
  },

  "jacsSha256": "document-hash..."
}

HAI Field Categories

Fields include a hai property indicating their category:

CategoryDescriptionExamples
metaMetadata (IDs, dates)jacsId, jacsVersion, jacsVersionDate
baseCryptographic datajacsSha256, signature
agentAgent-controlled contentCustom content fields

This categorization determines which fields are included in hash and signature calculations.

Working with Documents

Creating Documents

import jacs
import json

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

# Create a basic document
doc = agent.create_document(json.dumps({
    'title': 'My Document',
    'content': 'Document content here'
}))
import { JacsAgent } from '@hai.ai/jacs';

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

const doc = agent.createDocument(JSON.stringify({
  title: 'My Document',
  content: 'Document content here'
}));

Verifying Documents

is_valid = agent.verify_document(doc_json)
const isValid = agent.verifyDocument(docJson);

Updating Documents

doc = json.loads(signed_doc)
document_key = f"{doc['jacsId']}:{doc['jacsVersion']}"

updated = agent.update_document(
    document_key,
    json.dumps({**doc, 'content': 'Updated content'})
)
const doc = JSON.parse(signedDoc);
const documentKey = `${doc.jacsId}:${doc.jacsVersion}`;

const updated = agent.updateDocument(
  documentKey,
  JSON.stringify({...doc, content: 'Updated content'})
);

Adding Attachments

doc = agent.create_document(
    json.dumps({'title': 'Report'}),
    attachments='./report.pdf',
    embed=True
)
const doc = agent.createDocument(
  JSON.stringify({ title: 'Report' }),
  null,    // custom_schema
  null,    // output_filename
  false,   // no_save
  './report.pdf',  // attachments
  true     // embed
);

Version History

Documents maintain a version chain:

Original (v1) ← Previous (v2) ← Current (v3)
     β”‚
     └── jacsOriginalVersion points here for all versions

Each version:

  • Has its own jacsVersion UUID
  • References jacsPreviousVersion (except the first)
  • All share the same jacsId and jacsOriginalVersion

See Also

Task Schema

The Task Schema defines the structure for task documents in JACS. Tasks represent work items with defined states, assigned agents, and completion criteria.

Schema Location

https://hai.ai/schemas/task/v1/task.schema.json

Overview

Task documents manage:

  • Workflow States: From creation through completion
  • Agent Assignment: Customer and assigned agent tracking
  • Actions: Desired outcomes and completion criteria
  • Agreements: Start and end agreements between parties
  • Relationships: Sub-tasks, copies, and merges

Schema Structure

The task schema extends the Header Schema:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://hai.ai/schemas/task/v1/task-schema.json",
  "title": "Task",
  "description": "General schema for stateful resources.",
  "allOf": [
    { "$ref": "https://hai.ai/schemas/header/v1/header.schema.json" },
    { "type": "object", "properties": { ... } }
  ]
}

Task States

Tasks progress through defined workflow states:

StateDescription
creatingTask is being drafted
rfpRequest for proposal - seeking agents
proposalAgent has submitted a proposal
negotiationTerms being negotiated
startedWork has begun
reviewWork submitted for review
completedTask is finished
{
  "jacsTaskState": "started"
}

State Transitions

creating β†’ rfp β†’ proposal β†’ negotiation β†’ started β†’ review β†’ completed
                    ↑_______________|
                (may cycle back for renegotiation)

Task Properties

Core Fields (from Header)

Tasks inherit all document header fields plus task-specific fields.

Task-Specific Fields

FieldTypeRequiredDescription
jacsTaskNamestringNoHuman-readable task name
jacsTaskSuccessstringNoDescription of success criteria
jacsTaskStatestringYesCurrent workflow state
jacsTaskCustomerobjectYesCustomer agent signature
jacsTaskAgentobjectNoAssigned agent signature
jacsTaskStartDatestring (date-time)NoWhen work started
jacsTaskCompleteDatestring (date-time)NoWhen work completed
jacsTaskActionsDesiredarrayYesRequired actions
jacsStartAgreementobjectNoAgreement to begin work
jacsEndAgreementobjectNoAgreement that work is complete

Relationship Fields

FieldTypeDescription
jacsTaskSubTaskOfarrayParent task IDs
jacsTaskCopyOfarraySource task IDs (branching)
jacsTaskMergedTasksarrayTasks folded into this one

Actions

Actions define what needs to be accomplished:

{
  "jacsTaskActionsDesired": [
    {
      "name": "Create API Endpoint",
      "description": "Build REST endpoint for user registration",
      "cost": {
        "value": 500,
        "unit": "USD"
      },
      "duration": {
        "value": 8,
        "unit": "hours"
      },
      "completionAgreementRequired": true,
      "tools": [...]
    }
  ]
}

Action Schema Fields

FieldTypeRequiredDescription
namestringYesAction name
descriptionstringYesWhat needs to be done
toolsarrayNoTools that can be used
costobjectNoCost estimate
durationobjectNoTime estimate
completionAgreementRequiredbooleanNoRequires sign-off

Unit Schema

Costs and durations use the unit schema:

{
  "cost": {
    "value": 100,
    "unit": "USD"
  },
  "duration": {
    "value": 2,
    "unit": "days"
  }
}

Agreements

Tasks can include start and end agreements:

Start Agreement

Signed when parties agree to begin work:

{
  "jacsStartAgreement": {
    "agentIDs": ["customer-uuid", "agent-uuid"],
    "question": "Do you agree to begin this work?",
    "context": "Project XYZ - Phase 1",
    "signatures": [...]
  }
}

End Agreement

Signed when parties agree work is complete:

{
  "jacsEndAgreement": {
    "agentIDs": ["customer-uuid", "agent-uuid"],
    "question": "Do you agree this work is complete?",
    "context": "Final deliverables reviewed",
    "signatures": [...]
  }
}

Complete Example

{
  "$schema": "https://hai.ai/schemas/task/v1/task.schema.json",
  "jacsId": "550e8400-e29b-41d4-a716-446655440000",
  "jacsType": "task",
  "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",

  "jacsTaskName": "Build Authentication System",
  "jacsTaskSuccess": "Users can register, login, and manage sessions",
  "jacsTaskState": "started",

  "jacsTaskCustomer": {
    "agentID": "customer-agent-uuid",
    "agentVersion": "customer-version-uuid",
    "date": "2024-01-15T10:30:00Z",
    "signature": "customer-signature...",
    "publicKeyHash": "customer-key-hash",
    "signingAlgorithm": "ring-Ed25519",
    "fields": ["jacsTaskName", "jacsTaskActionsDesired"]
  },

  "jacsTaskAgent": {
    "agentID": "assigned-agent-uuid",
    "agentVersion": "agent-version-uuid",
    "date": "2024-01-16T09:00:00Z",
    "signature": "agent-signature...",
    "publicKeyHash": "agent-key-hash",
    "signingAlgorithm": "ring-Ed25519",
    "fields": ["jacsTaskName", "jacsTaskActionsDesired"]
  },

  "jacsTaskStartDate": "2024-01-16T09:00:00Z",

  "jacsStartAgreement": {
    "agentIDs": ["customer-agent-uuid", "assigned-agent-uuid"],
    "question": "Do you agree to begin work on this task?",
    "signatures": [
      {
        "agentID": "customer-agent-uuid",
        "signature": "...",
        "responseType": "agree",
        "date": "2024-01-16T09:00:00Z"
      },
      {
        "agentID": "assigned-agent-uuid",
        "signature": "...",
        "responseType": "agree",
        "date": "2024-01-16T09:05:00Z"
      }
    ]
  },

  "jacsTaskActionsDesired": [
    {
      "name": "User Registration",
      "description": "Implement user registration with email verification",
      "duration": { "value": 4, "unit": "hours" },
      "completionAgreementRequired": true
    },
    {
      "name": "User Login",
      "description": "Implement secure login with password hashing",
      "duration": { "value": 3, "unit": "hours" },
      "completionAgreementRequired": true
    },
    {
      "name": "Session Management",
      "description": "Implement JWT-based session tokens",
      "duration": { "value": 2, "unit": "hours" },
      "completionAgreementRequired": false
    }
  ],

  "jacsSignature": {
    "agentID": "customer-agent-uuid",
    "agentVersion": "customer-version-uuid",
    "date": "2024-01-15T10:30:00Z",
    "signature": "document-signature...",
    "publicKeyHash": "key-hash...",
    "signingAlgorithm": "ring-Ed25519",
    "fields": ["jacsId", "jacsTaskName", "jacsTaskActionsDesired"]
  }
}

Task Relationships

Sub-Tasks

Break large tasks into smaller units:

{
  "jacsTaskSubTaskOf": ["parent-task-uuid"]
}

Task Copies (Branching)

Create variations or branches:

{
  "jacsTaskCopyOf": ["original-task-uuid"]
}

Merged Tasks

Combine completed tasks:

{
  "jacsTaskMergedTasks": [
    "subtask-1-uuid",
    "subtask-2-uuid"
  ]
}

Task Workflow

1. Creating a Task

import jacs
import json

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

task = agent.create_document(json.dumps({
    'jacsTaskName': 'Build Feature X',
    'jacsTaskSuccess': 'Feature is deployed and tested',
    'jacsTaskState': 'creating',
    'jacsTaskActionsDesired': [
        {
            'name': 'Implementation',
            'description': 'Write the code',
            'completionAgreementRequired': True
        }
    ]
}), custom_schema='https://hai.ai/schemas/task/v1/task.schema.json')

2. Assigning an Agent

When an agent accepts the task, add their signature to jacsTaskAgent and update state to started.

3. Signing Start Agreement

Both parties sign the start agreement to confirm work begins.

4. Completing Work

Update state to review, then both parties sign the end agreement.

5. Final Completion

After end agreement is signed by all parties, update state to completed.

State Machine Rules

Current StateValid Next States
creatingrfp
rfpproposal, creating
proposalnegotiation, rfp
negotiationstarted, proposal
startedreview
reviewcompleted, started
completed(terminal)

See Also

Agent State Schema

The Agent State Schema defines the structure for signed agent state documents in JACS. Agent state documents wrap and cryptographically sign any agent configuration file -- memory files, skills, plans, configs, hooks, or any other document an agent wants to verify.

Schema Location

https://hai.ai/schemas/agentstate/v1/agentstate.schema.json

Overview

Agent state documents provide:

  • Signed state files: Cryptographically sign MEMORY.md, skill files, plans, configs, hooks, or any file
  • File integrity: SHA-256 hashes verify file contents haven't been tampered with
  • Origin tracking: Record whether state was authored, adopted, generated, or imported
  • Framework tagging: Identify which agent framework (claude-code, langchain, etc.) the state belongs to
  • General-purpose signing: Use type other to sign any document an agent wants to verify

All documents are stored within the JACS data directory for security.

Schema Structure

The agent state schema extends the Header Schema:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://hai.ai/schemas/agentstate/v1/agentstate.schema.json",
  "title": "Agent State Document",
  "allOf": [
    { "$ref": "https://hai.ai/schemas/header/v1/header.schema.json" }
  ],
  "properties": {
    "jacsAgentStateType": {
      "type": "string",
      "enum": ["memory", "skill", "plan", "config", "hook", "other"]
    },
    "jacsAgentStateName": { "type": "string" }
  },
  "required": ["jacsAgentStateType", "jacsAgentStateName"]
}

State Types

TypeDescriptionExample
memoryAgent memory/knowledge filesMEMORY.md, context files
skillAgent skill definitionsCoding patterns, domain knowledge
planAgent plans and strategiesImplementation plans, workflows
configAgent configuration filesSettings, preferences
hookAgent hooks and triggers (always embedded)Pre-commit hooks, event handlers
otherAny document the agent wants to sign and verifyReports, artifacts, custom files

Properties

Required Fields

FieldTypeDescription
jacsAgentStateTypestring (enum)Type of agent state: memory, skill, plan, config, hook, other
jacsAgentStateNamestringHuman-readable name for this state document

Optional Fields

FieldTypeDescription
jacsAgentStateDescriptionstringDescription of what this state contains
jacsAgentStateFrameworkstringAgent framework (e.g., "claude-code", "langchain")
jacsAgentStateVersionstringContent version (distinct from jacsVersion)
jacsAgentStateContentTypestringMIME type (text/markdown, application/json, etc.)
jacsAgentStateContentstringInline content (used when embedding)
jacsAgentStateTagsstring[]Tags for categorization and search
jacsAgentStateOriginstring (enum)How created: authored, adopted, generated, imported
jacsAgentStateSourceUrlstring (uri)Where content was obtained from

Origin Tracking

Every agent state document can track its provenance:

OriginMeaning
authoredCreated by the signing agent
adoptedFound unsigned, signed by adopting agent
generatedProduced by AI/automation
importedBrought from another JACS installation

File References

Agent state documents can reference external files using jacsFiles:

{
  "jacsFiles": [
    {
      "mimetype": "text/markdown",
      "path": "MEMORY.md",
      "embed": true,
      "sha256": "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
      "contents": "base64-encoded-gzipped-content"
    }
  ]
}

When embed is true, the file content is stored inline in the document. Hook-type documents always embed content for security (prevents time-of-check/time-of-use attacks).

Examples

Minimal Agent State

{
  "$schema": "https://hai.ai/schemas/agentstate/v1/agentstate.schema.json",
  "jacsAgentStateType": "memory",
  "jacsAgentStateName": "Project Memory",
  "jacsType": "agentstate",
  "jacsLevel": "config"
}

Memory File with Embedding

{
  "$schema": "https://hai.ai/schemas/agentstate/v1/agentstate.schema.json",
  "jacsAgentStateType": "memory",
  "jacsAgentStateName": "JACS Project Memory",
  "jacsAgentStateDescription": "Agent memory for the JACS project workspace",
  "jacsAgentStateFramework": "claude-code",
  "jacsAgentStateOrigin": "authored",
  "jacsAgentStateContentType": "text/markdown",
  "jacsAgentStateContent": "# MEMORY.md\n\n## Project: JACS\n- Location: /home/agent/jacs\n- Rust library for cryptographic signing\n",
  "jacsAgentStateTags": ["jacs", "rust", "crypto"],
  "jacsType": "agentstate",
  "jacsLevel": "config"
}

Adopted Skill

{
  "$schema": "https://hai.ai/schemas/agentstate/v1/agentstate.schema.json",
  "jacsAgentStateType": "skill",
  "jacsAgentStateName": "JSON Schema Validation",
  "jacsAgentStateOrigin": "adopted",
  "jacsAgentStateSourceUrl": "https://agentskills.io/skills/json-schema",
  "jacsAgentStateVersion": "2.1.0",
  "jacsType": "agentstate",
  "jacsLevel": "config"
}

General-Purpose Signed Document

Use type other to sign any document:

{
  "$schema": "https://hai.ai/schemas/agentstate/v1/agentstate.schema.json",
  "jacsAgentStateType": "other",
  "jacsAgentStateName": "Q1 Financial Report",
  "jacsAgentStateDescription": "Quarterly financial summary for verification",
  "jacsAgentStateContentType": "application/json",
  "jacsAgentStateContent": "{\"revenue\": 150000, \"expenses\": 120000}",
  "jacsType": "agentstate",
  "jacsLevel": "config"
}

Rust API

Creating Agent State Documents

#![allow(unused)]
fn main() {
use jacs::schema::agentstate_crud::*;

// Minimal state
let doc = create_minimal_agentstate("memory", "Project Memory", Some("Agent memory file"))?;

// With file reference
let doc = create_agentstate_with_file("skill", "Rust Patterns", "./skills/rust.md", true)?;

// With inline content
let doc = create_agentstate_with_content(
    "config",
    "Agent Settings",
    "{\"theme\": \"dark\"}",
    "application/json"
)?;

// General-purpose signing
let doc = create_agentstate_with_content(
    "other",
    "Audit Report",
    "Report contents here...",
    "text/plain"
)?;

// Set metadata
let mut doc = create_minimal_agentstate("memory", "My Memory", None)?;
set_agentstate_framework(&mut doc, "claude-code")?;
set_agentstate_origin(&mut doc, "authored", None)?;
set_agentstate_tags(&mut doc, vec!["project", "notes"])?;
set_agentstate_version(&mut doc, "1.0.0")?;
}

Signing and Verification

#![allow(unused)]
fn main() {
// Create, sign, and store
let doc_string = serde_json::to_string(&doc)?;
let signed_doc = agent.create_document_and_load(&doc_string, None, None)?;

// Verify file integrity
let hash_valid = verify_agentstate_file_hash(&doc)?;
}

MCP Tools

Six MCP tools are available for agent state operations:

ToolDescription
jacs_sign_stateCreate and sign a new agent state document
jacs_verify_stateVerify an existing agent state document by JACS document ID (jacs_id)
jacs_load_stateLoad an agent state document by JACS document ID (jacs_id)
jacs_update_stateUpdate and re-sign an agent state document by JACS document ID (jacs_id)
jacs_list_stateList all agent state documents
jacs_adopt_stateAdopt an external file as a signed agent state

MCP Example: Sign a Memory File

{
  "tool": "jacs_sign_state",
  "arguments": {
    "state_type": "memory",
    "name": "Project Memory",
    "content": "# My Agent Memory\n\nKey facts about the project...",
    "content_type": "text/markdown",
    "framework": "claude-code",
    "tags": ["project", "memory"]
  }
}

MCP Example: Sign Any Document

{
  "tool": "jacs_sign_state",
  "arguments": {
    "state_type": "other",
    "name": "Verification Report",
    "content": "{\"status\": \"passed\", \"checks\": 42}",
    "content_type": "application/json"
  }
}

Security Notes

  • All agent state documents are stored within the JACS data directory for security
  • MCP verify/load/update flows are jacs_id-based; direct path-only access is disabled
  • Hook-type documents always embed content to prevent TOCTOU attacks
  • File hashes (SHA-256) are verified on load to detect tampering
  • Origin tracking provides provenance auditing
  • Documents are signed with the agent's private key, providing non-repudiation

See Also

Commitment Schema

Commitments are shared, signed agreements between agents. They represent what an agent commits to doing, optionally within a conversation or linked to a task or todo item.

Key design: Commitments work standalone. They do not require goals, tasks, conversations, or any other document type to be created first.

Schema

  • ID: https://hai.ai/schemas/commitment/v1/commitment.schema.json
  • Type: jacsType: "commitment"
  • Level: jacsLevel: "config" (editable, versioned)
  • Extends: header.schema.json via allOf

Required Fields

FieldTypeDescription
jacsCommitmentDescriptionstringHuman-readable description of the commitment
jacsCommitmentStatusenumLifecycle status

Status Lifecycle

pending -> active -> completed
                  -> failed
                  -> renegotiated
         -> disputed
         -> revoked
StatusMeaning
pendingCreated but not yet started
activeWork is underway
completedSuccessfully fulfilled
failedCould not be fulfilled
renegotiatedTerms changed, replaced by new commitment
disputedOne party contests the commitment
revokedWithdrawn by the owner

Optional Fields

FieldTypeDescription
jacsCommitmentTermsobjectStructured terms (deliverable, deadline, compensation, etc.)
jacsCommitmentDisputeReasonstringReason when status is disputed or revoked
jacsCommitmentTaskIduuidReference to a task document
jacsCommitmentConversationRefuuidThread ID of the conversation that produced this commitment
jacsCommitmentTodoRefstringTodo item reference in format list-uuid:item-uuid
jacsCommitmentQuestionstringStructured question prompt
jacsCommitmentAnswerstringAnswer to the question
jacsCommitmentCompletionQuestionstringQuestion to verify completion
jacsCommitmentCompletionAnswerstringAnswer verifying completion
jacsCommitmentStartDatedate-timeWhen the commitment period begins
jacsCommitmentEndDatedate-timeDeadline
jacsCommitmentRecurrenceobjectRecurrence pattern (frequency + interval)
jacsCommitmentOwnersignatureSingle-agent owner signature

Cross-References

Commitments can link to other document types:

  • Conversation: jacsCommitmentConversationRef holds a thread UUID
  • Todo item: jacsCommitmentTodoRef uses format list-uuid:item-uuid
  • Task: jacsCommitmentTaskId holds a task document UUID

These references are optional. Commitments work independently.

Multi-Agent Agreements

Commitments use the standard JACS agreement mechanism from the header schema. Two or more agents can co-sign a commitment using jacsAgreement.

Example

{
  "$schema": "https://hai.ai/schemas/commitment/v1/commitment.schema.json",
  "jacsCommitmentDescription": "Deliver Q1 analytics report by March 15",
  "jacsCommitmentStatus": "active",
  "jacsCommitmentTerms": {
    "deliverable": "PDF report with charts",
    "deadline": "2026-03-15T00:00:00Z"
  },
  "jacsCommitmentStartDate": "2026-01-15T00:00:00Z",
  "jacsCommitmentEndDate": "2026-03-15T00:00:00Z",
  "jacsType": "commitment",
  "jacsLevel": "config"
}

Rust API

#![allow(unused)]
fn main() {
use jacs::schema::commitment_crud::*;

// Create
let commitment = create_minimal_commitment("Deliver report").unwrap();

// With structured terms
let commitment = create_commitment_with_terms(
    "Weekly standup",
    serde_json::json!({"frequency": "weekly"}),
).unwrap();

// Update status
update_commitment_status(&mut commitment, "active").unwrap();

// Dispute
dispute_commitment(&mut commitment, "Terms not met").unwrap();

// Cross-references
set_conversation_ref(&mut commitment, &thread_id).unwrap();
set_todo_ref(&mut commitment, "list-uuid:item-uuid").unwrap();
set_task_ref(&mut commitment, &task_id).unwrap();
}

Versioning

Since commitments use jacsLevel: "config", they can be updated. Each update creates a new jacsVersion linked to the previous via jacsPreviousVersion. This provides a full audit trail of status changes and modifications.

See Also

Todo List Schema

Todo lists are private, signed documents belonging to a single agent. They contain inline items (goals and tasks) and are re-signed as a whole when any item changes.

Schema

  • ID: https://hai.ai/schemas/todo/v1/todo.schema.json
  • Type: jacsType: "todo"
  • Level: jacsLevel: "config" (editable, versioned)
  • Extends: header.schema.json via allOf
  • Component: todoitem.schema.json for inline items

Required Fields

FieldTypeDescription
jacsTodoNamestringHuman-readable name for this list
jacsTodoItemsarrayInline todo items

Optional Fields

FieldTypeDescription
jacsTodoArchiveRefsuuid[]UUIDs of archived todo lists

Todo Items

Each item in jacsTodoItems is an inline object following todoitem.schema.json.

Required Item Fields

FieldTypeDescription
itemIduuidStable UUID that does not change on re-signing
itemTypeenum"goal" (broad objective) or "task" (specific action)
descriptionstringHuman-readable description
statusenum"pending", "in-progress", "completed", "abandoned"

Optional Item Fields

FieldTypeDescription
priorityenum"low", "medium", "high", "critical"
childItemIdsuuid[]Sub-goals or tasks under this item
relatedCommitmentIduuidCommitment that formalizes this item
relatedConversationThreaduuidConversation thread related to this item
completedDatedate-timeWhen the item was completed
assignedAgentuuidAgent assigned to this item
tagsstring[]Tags for categorization

Cross-References

Todo items can link to other document types:

  • Commitment: relatedCommitmentId links an item to a commitment
  • Conversation: relatedConversationThread links an item to a message thread

References use the list-uuid:item-uuid format when referenced FROM other documents (e.g., jacsCommitmentTodoRef on a commitment). Use build_todo_item_ref() and parse_todo_item_ref() from reference_utils for this format.

Item Hierarchy

Items support parent-child relationships via childItemIds:

Goal: "Ship Q1 release"
  β”œβ”€β”€ Task: "Write documentation"
  β”œβ”€β”€ Task: "Run integration tests"
  └── Goal: "Performance optimization"
       β”œβ”€β”€ Task: "Profile database queries"
       └── Task: "Add caching layer"

Example

{
  "$schema": "https://hai.ai/schemas/todo/v1/todo.schema.json",
  "jacsTodoName": "Q1 Sprint",
  "jacsTodoItems": [
    {
      "itemId": "550e8400-e29b-41d4-a716-446655440001",
      "itemType": "goal",
      "description": "Ship analytics dashboard",
      "status": "in-progress",
      "priority": "high",
      "childItemIds": [
        "550e8400-e29b-41d4-a716-446655440002"
      ]
    },
    {
      "itemId": "550e8400-e29b-41d4-a716-446655440002",
      "itemType": "task",
      "description": "Build chart components",
      "status": "pending",
      "priority": "medium",
      "relatedCommitmentId": "660e8400-e29b-41d4-a716-446655440000",
      "tags": ["frontend", "charts"]
    }
  ],
  "jacsType": "todo",
  "jacsLevel": "config"
}

Rust API

#![allow(unused)]
fn main() {
use jacs::schema::todo_crud::*;

// Create a list
let mut list = create_minimal_todo_list("Sprint Work").unwrap();

// Add items
let goal_id = add_todo_item(&mut list, "goal", "Ship Q1", Some("high")).unwrap();
let task_id = add_todo_item(&mut list, "task", "Write tests", None).unwrap();

// Build hierarchy
add_child_to_item(&mut list, &goal_id, &task_id).unwrap();

// Progress tracking
update_todo_item_status(&mut list, &task_id, "in-progress").unwrap();
mark_todo_item_complete(&mut list, &task_id).unwrap();

// Cross-references
set_item_commitment_ref(&mut list, &task_id, &commitment_id).unwrap();
set_item_conversation_ref(&mut list, &task_id, &thread_id).unwrap();

// Archive completed items
let completed = remove_completed_items(&mut list).unwrap();
}

Versioning

Since todo lists use jacsLevel: "config", each modification creates a new signed version. The itemId fields remain stable across versions, enabling consistent cross-referencing even as items are added, updated, or removed.

See Also

Conversation Schema

Conversations use the existing message schema enhanced with thread tracking and message ordering. There is no separate "conversation" schema - conversations are sequences of signed messages sharing a thread ID.

Schema

  • ID: https://hai.ai/schemas/message/v1/message.schema.json
  • Type: jacsType: "message"
  • Level: jacsLevel: "raw" (immutable once signed)
  • Extends: header.schema.json via allOf

Message Fields

Required

FieldTypeDescription
tostring[]Recipient agent identifiers
fromstring[]Sender agent identifiers
contentobjectMessage body (free-form object)

Optional

FieldTypeDescription
threadIDstringUUID of the conversation thread
jacsMessagePreviousIduuidUUID of the previous message in this thread
attachmentsarrayFile attachments

Threading Model

Messages form a thread via two fields:

  1. threadID - All messages in a conversation share the same thread ID
  2. jacsMessagePreviousId - Each message references the previous one, creating an ordered chain
Message 1 (threadID: "abc-123", previousId: null)
    └── Message 2 (threadID: "abc-123", previousId: msg1.jacsId)
         └── Message 3 (threadID: "abc-123", previousId: msg2.jacsId)

Immutability

Messages use jacsLevel: "raw", making them immutable once signed. To continue a conversation, create a new message referencing the previous one. This ensures the integrity of the conversation history.

Example

{
  "$schema": "https://hai.ai/schemas/message/v1/message.schema.json",
  "threadID": "550e8400-e29b-41d4-a716-446655440000",
  "content": {
    "body": "I agree to the proposed terms.",
    "subject": "Re: Q1 Deliverables"
  },
  "to": ["agent-b-uuid"],
  "from": ["agent-a-uuid"],
  "jacsMessagePreviousId": "660e8400-e29b-41d4-a716-446655440001",
  "jacsType": "message",
  "jacsLevel": "raw"
}

Rust API

#![allow(unused)]
fn main() {
use jacs::schema::conversation_crud::*;

// Start a new conversation (generates thread ID)
let (first_msg, thread_id) = start_new_conversation(
    serde_json::json!({"body": "Hello, let's discuss terms."}),
    vec!["agent-b".to_string()],
    vec!["agent-a".to_string()],
).unwrap();

// Continue the conversation
let reply = create_conversation_message(
    &thread_id,
    serde_json::json!({"body": "Sounds good. Here are my terms."}),
    vec!["agent-a".to_string()],
    vec!["agent-b".to_string()],
    Some(&previous_message_jacs_id),
).unwrap();

// Extract thread info
let tid = get_thread_id(&message).unwrap();
let prev = get_previous_message_id(&message);
}

Cross-References

Conversations can be referenced by other document types:

  • Commitment: jacsCommitmentConversationRef stores the thread UUID
  • Todo item: relatedConversationThread stores the thread UUID

This allows tracking which conversation led to a commitment or is related to a work item.

See Also

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

Configuration Options

Key Configuration

jacs_agent_key_algorithm

Specifies the cryptographic algorithm for signing:

ValueDescription
ring-Ed25519Ed25519 signatures (recommended)
RSA-PSSRSA with PSS padding
pq-dilithiumPost-quantum Dilithium
pq2025Post-quantum composite
{
  "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 instead.

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_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

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 must be set only via the JACS_PRIVATE_KEY_PASSWORD environment variable. 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.
  • 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": "ring-Ed25519"
  }
}

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 all bindings and as an MCP tool (jacs_audit) for automation.

Threat Model

Protected Against

ThreatProtection
TamperingContent hashes detect modifications
ImpersonationCryptographic signatures verify identity
Replay AttacksTimestamps and version IDs ensure freshness; future timestamps rejected; optional signature expiration via JACS_MAX_SIGNATURE_AGE_SECONDS
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

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

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.

# Set via environment variable only
export JACS_PRIVATE_KEY_PASSWORD="secure-password"

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.

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

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

JACS signatures include timestamps to prevent replay attacks and ensure temporal integrity.

How It Works

  1. Timestamp Inclusion: Every signature includes a UTC timestamp recording when it was created
  2. Future Timestamp Rejection: Signatures with timestamps more than 5 minutes in the future are rejected
  3. Optional Signature Expiration: Configurable via JACS_MAX_SIGNATURE_AGE_SECONDS (disabled by default since JACS documents are designed to be eternal)
  4. Validation: Timestamp validation occurs during signature verification

Configuring Signature Expiration

By default, signatures do not expire. JACS documents are designed to be idempotent and eternal. For use cases that require expiration:

# Enable expiration (e.g., 90 days)
export JACS_MAX_SIGNATURE_AGE_SECONDS=7776000

# Default: no expiration (0)
export JACS_MAX_SIGNATURE_AGE_SECONDS=0

Protection Against Replay Attacks

The 5-minute future tolerance window:

  • Allows for reasonable clock skew between systems
  • Prevents attackers from creating signatures with future timestamps
  • Ensures signatures cannot be pre-generated for later fraudulent use
{
  "jacsSignature": {
    "agentID": "550e8400-e29b-41d4-a716-446655440000",
    "signature": "...",
    "date": "2024-01-15T10:30:00Z"  // Must be within 5 min of verifier's clock
  }
}

Clock Synchronization

For reliable timestamp validation across distributed systems:

  • Ensure all agents use NTP or similar time synchronization
  • Monitor for clock drift in production environments
  • Consider the 5-minute tolerance when debugging verification failures

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",
  "jacsServices": [...]
}

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 MCP and HTTP communication:

Request Signing

signed_request = agent.sign_request({
    'method': 'tools/call',
    'params': {'name': 'echo', 'arguments': {'text': 'hello'}}
})

The signed request includes:

  • Full JACS document structure
  • Agent signature
  • Timestamp
  • Content hash

Response Verification

result = agent.verify_response(response_string)
payload = result.get('payload')
agent_id = result.get('agentId')  # Who signed the response

Algorithm Security

Supported Algorithms

AlgorithmTypeSecurity Level
ring-Ed25519Elliptic CurveHigh (recommended)
RSA-PSSRSAHigh
pq-dilithiumPost-QuantumQuantum-resistant
pq2025CompositeTransitional

Algorithm Selection

Choose based on requirements:

  • General Use: ring-Ed25519 - fast, secure, small signatures
  • Legacy Systems: RSA-PSS - widely supported
  • Future-Proofing: pq-dilithium - quantum-resistant
  • Transition: pq2025 - hybrid classical/post-quantum

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

# Use environment variables
export JACS_PRIVATE_KEY_PASSWORD="$(pass show jacs/key-password)"

3. Transport Security

Always use TLS for network communication:

# HTTPS for web transport
client = JACSMCPClient("https://localhost:8000/sse")  # Good
# client = JACSMCPClient("http://localhost:8000/sse")  # Avoid in production

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 for secrets (never store jacs_private_key_password in config)
  • 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 create --type ai --claim unverified

"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: unverified β†’ verified β†’ verified-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

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 (Planned)

Note: These CLI commands are planned for a future release. Currently, key rotation must be performed programmatically using the Rust API.

# Rotate keys with default algorithm (Coming Soon)
jacs agent rotate-keys

# Rotate to post-quantum algorithm (Coming Soon)
jacs agent rotate-keys --algorithm pq2025

# List key history (Coming Soon)
jacs agent keys list

# Revoke a compromised key (Coming Soon)
jacs agent keys revoke <key-hash>

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

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.

Supported Algorithms

AlgorithmConfig ValueTypeKey SizeSignature SizeRecommended Use
Ed25519ring-Ed25519Elliptic Curve32 bytes64 bytesGeneral purpose (default)
RSA-PSSRSA-PSSRSA2048-4096 bits256-512 bytesLegacy systems
Dilithiumpq-dilithiumLattice-based~1.3 KB~2.4 KBPost-quantum
PQ2025pq2025Hybrid~1.3 KB~2.5 KBTransitional

Ed25519 (ring-Ed25519)

The recommended algorithm for most use cases.

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"
}

Use Cases

  • General agent communication
  • MCP message signing
  • HTTP request/response signing
  • Document signing

Example

import jacs
import json

agent = jacs.JacsAgent()
agent.load('./jacs.config.json')  # Using ring-Ed25519

# Sign a message
signature = agent.sign_string("Hello, World!")
print(f"Signature (64 bytes): {len(signature)} characters base64")

RSA-PSS

Industry-standard RSA with Probabilistic Signature Scheme padding.

Overview

RSA-PSS provides compatibility with systems that require RSA signatures. JACS uses 2048-bit or larger keys.

Characteristics

  • Speed: Slower than Ed25519
  • Key Size: 2048-4096 bits
  • Signature Size: Same as key size (256-512 bytes)
  • Security Level: ~112-128 bits (2048-bit key)

Configuration

{
  "jacs_agent_key_algorithm": "RSA-PSS"
}

Use Cases

  • Integration with legacy systems
  • Compliance requirements mandating RSA
  • Interoperability with enterprise PKI

Considerations

  • Larger signatures increase document size
  • Slower than Ed25519
  • Larger keys needed for equivalent security

Dilithium (pq-dilithium)

NIST-standardized post-quantum digital signature algorithm.

Overview

Dilithium is a lattice-based signature scheme selected by NIST for post-quantum cryptography standardization. It provides security against both classical and quantum computers.

Characteristics

  • Speed: Moderate (faster than RSA, slower than Ed25519)
  • Key Size: ~1.3 KB public key, ~2.5 KB private key
  • Signature Size: ~2.4 KB
  • Security Level: NIST Level 3 (quantum-resistant)

Configuration

{
  "jacs_agent_key_algorithm": "pq-dilithium"
}

Use Cases

  • Long-term document security
  • Protection against future quantum attacks
  • High-security applications
  • Government/defense requirements

Considerations

  • Larger signatures and keys than classical algorithms
  • Newer algorithm (less battle-tested)
  • May be required for future compliance

PQ2025 (Hybrid)

Transitional hybrid scheme combining classical and post-quantum algorithms.

Overview

PQ2025 combines Ed25519 with Dilithium, providing security even if one algorithm is broken. This approach is recommended by security researchers during the quantum transition period.

Characteristics

  • Speed: Slower (two signatures computed)
  • Key Size: Combined Ed25519 + Dilithium
  • Signature Size: ~2.5 KB (combined)
  • Security Level: Max of both algorithms

Configuration

{
  "jacs_agent_key_algorithm": "pq2025"
}

Use Cases

  • Transitioning to post-quantum
  • Maximum security requirements
  • Uncertainty about algorithm security
  • Long-lived documents

Considerations

  • Largest signatures
  • Slowest signing/verification
  • Best for paranoid security requirements

Algorithm Selection Guide

Decision Matrix

RequirementRecommended Algorithm
Best performancering-Ed25519
Smallest signaturesring-Ed25519
Legacy compatibilityRSA-PSS
Quantum resistancepq-dilithium
Maximum securitypq2025
General purposering-Ed25519

By Use Case

Web APIs and MCP:

{
  "jacs_agent_key_algorithm": "ring-Ed25519"
}

Fast signing is critical for real-time communication.

Legal/Financial Documents:

{
  "jacs_agent_key_algorithm": "pq-dilithium"
}

Long-term validity requires quantum resistance.

Enterprise Integration:

{
  "jacs_agent_key_algorithm": "RSA-PSS"
}

Compatibility with existing PKI infrastructure.

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)
RSA-PSSPEM (PKCS#8)PEM (SPKI)
pq-dilithiumPEM (custom)PEM (custom)
pq2025PEM (combined)PEM (combined)

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": "ring-Ed25519",
    "fields": ["jacsId", "jacsVersion", "content"]
  }
}

The signingAlgorithm field enables verifiers to use the correct verification method.

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

To migrate to a new algorithm:

  1. Generate New Keys

    {
      "jacs_agent_key_algorithm": "pq-dilithium"
    }
    
  2. Create New Agent Version

    # Load with old algorithm
    agent.load('./old-config.json')
    
    # Update to new algorithm and generate new version
    new_agent = agent.update_agent(json.dumps({
        # ... agent data with new keys
    }))
    
  3. Update Configuration

    {
      "jacs_agent_id_and_version": "agent-id:new-version",
      "jacs_agent_key_algorithm": "pq-dilithium"
    }
    
  4. 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
RSA-PSS (2048)~1,000~30,000~100
pq-dilithium~5,000~10,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.

Supported Algorithms

AlgorithmConfig ValuePublic KeySignatureBest For
Ed25519ring-Ed2551932 bytes64 bytesSpeed, small signatures
RSA-PSSRSA-PSS~550 bytes (4096-bit)~512 bytesBroad compatibility
ML-DSA-87pq20252,592 bytes4,627 bytesPost-quantum compliance (FIPS-204)
Dilithiumpq-dilithium>1,000 bytes~3,293-4,644 bytesDeprecated -- use pq2025

How to Choose

Do you need FIPS/NIST post-quantum compliance?
  β”œβ”€β”€ Yes β†’ pq2025
  └── No
       β”œβ”€β”€ Need maximum interop with existing PKI/TLS systems? β†’ RSA-PSS
       └── Need speed and small payloads? β†’ ring-Ed25519

Default recommendation for new projects: pq2025

Ed25519 and RSA-PSS are well-understood and widely deployed, but neither is quantum-resistant. If you don't have a specific reason to choose one of them, start with pq2025 so you don't have to migrate later.

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 algorithms. An agreement can contain signatures from RSA, Ed25519, and ML-DSA agents and all verify correctly. This heterogeneous verification is important for cross-organization scenarios where different parties chose different 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

Valid values: ring-Ed25519, RSA-PSS, pq2025

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.
  • pq-dilithium is deprecated in favor of pq2025 (ML-DSA-87). Use pq2025 for new agents 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. Each 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.

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

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)

@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

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

There are three supported ways to use JACS with MCP today:

  1. Run jacs mcp when you want a ready-made MCP server with the broadest tool surface.
  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
Rustjacs-mcpFull MCP server with document, agreement, trust, A2A, and audit 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.

1. Ready-Made Server: jacs mcp

Install the unified binary and start the MCP server:

cargo install jacs-cli
jacs mcp

The MCP server is built into the jacs binary (stdio transport only, no HTTP). It includes document signing, agreements, trust store operations, A2A tools, and security audit tools. See jacs-mcp/README.md in the repo for the full tool list and client configuration examples.

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

For clients:

from jacs.mcp import JACSMCPClient

client = JACSMCPClient("http://localhost:8000/sse", "./jacs.config.json")

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

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

The Node tool set is intentionally smaller than the Rust MCP server. Use jacs mcp when you need the largest supported MCP surface.

Example Paths In This Repo

  • jacs-mcp/README.md
  • jacspy/examples/mcp/server.py
  • jacspy/examples/mcp/client.py
  • jacsnpm/examples/mcp.stdio.server.js
  • jacsnpm/examples/mcp.stdio.client.js

A2A Interoperability

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(url="http://localhost:8080")

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 has the strongest first-class server helpers today.

Quick demo server:

from jacs.a2a import JACSA2AIntegration

JACSA2AIntegration.quickstart(
    name="my-agent",
    domain="my-agent.example.com",
    url="http://localhost:8080",
).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, { url: 'http://localhost:3000' }));
app.listen(3000);

3. Sign And Verify Artifacts

Python:

signed = client.sign_artifact({"taskId": "t-1", "operation": "classify"}, "task")
result = client.get_a2a().verify_wrapped_artifact(signed)
assert result["valid"]

Node.js:

const signed = await client.signArtifact(
  { taskId: 't-1', operation: 'classify' },
  'task',
);

const result = await client.verifyArtifact(signed);
console.log(result.valid);

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 the JACS provenance extension (urn:jacs:provenance-v1) in the agent card (default)
strictRequire the signer to be in the local trust store

That means verified is about JACS provenance on the Agent Card, not about a promise that every foreign key has already been resolved.

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.

Current Runtime Differences

  • Python: jacs.a2a_server is the clearest full discovery story.
  • Node.js: jacsA2AMiddleware() serves five .well-known routes from Express, but the generated jwks.json and jacs-pubkey.json payloads are still placeholder metadata. listen() is intentionally smaller and only suitable for demos.

Example Paths In This Repo

  • jacs-mcp/README.md
  • jacspy/tests/test_a2a_server.py
  • jacsnpm/src/a2a-server.js
  • jacsnpm/examples/a2a-agent-example.js
  • jacs/tests/a2a_cross_language_tests.rs

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 follow the v0.4.0 shape. Your existing Agent Card fields (name, description, skills, url) are preserved.
  • Discovery uses /.well-known/agent-card.json. No new endpoints are required for basic interop.
  • 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, "task")
# 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:

  • Post-quantum (ML-DSA-87) for JACS document signatures -- future-proof
  • Traditional (RSA/ECDSA) for JWS Agent Card signatures -- A2A ecosystem compatibility

This means your agent is compatible with both the current A2A ecosystem and quantum-resistant verification.


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 signature is cryptographically correct, but the trust policy rejected the signer. With strict policy, the signer must be in your local trust store. Add them with a2a.trust_a2a_agent(card_json).

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 derived from jacsServices in the agent definition. Pass skills=[...] to export_agent_card() to override, or define services when creating the agent.

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

Make your JACS agent discoverable by other A2A agents.

Prerequisites: pip install jacs[a2a-server] (Python) or npm install @hai.ai/jacs express (Node.js).

from jacs.a2a import JACSA2AIntegration

JACSA2AIntegration.quickstart(url="http://localhost:8080").serve(port=8080)

Your agent is now discoverable at http://localhost:8080/.well-known/agent-card.json.

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

All five .well-known endpoints are served automatically:

EndpointPurpose
/.well-known/agent-card.jsonA2A Agent Card with JWS signature
/.well-known/jwks.jsonJWK set for A2A verifiers
/.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.

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, agents must be in your local trust store:

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']}")

# Add to trust store (verifies agent's self-signature first)
a2a.trust_a2a_agent(remote_card_json)

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

// Add to trust store
a2a.trustA2AAgent(remoteAgentId);

Trust Policies

PolicyBehavior
openAccept all agents without verification
verifiedRequire the JACS provenance extension (urn:jacs:provenance-v1) in the agent card (default)
strictRequire the signer to be in the local trust store

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 policy, all agents pass step 3. With verified, agents must have the JACS extension. With strict, agents must be explicitly added to the trust store in step 4 before they pass.

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"}, "task")

# 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"}, "message")

# Agent B signs step 2, referencing step 1 as parent
step2 = client_b.sign_artifact(
    {"step": 2, "data": "processed"},
    "message",
    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

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.

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.

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)

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)
        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-issuer".into(),
                value: iss.clone(),
                confidence: Some(0.8),
                assurance_level: Some(AssuranceLevel::Verified),
                issuer: iss.as_str().map(String::from),
                issued_at: Some(crate::time_utils::now_rfc3339()),
            });
        }
        if let Some(sub) = payload.get("sub") {
            claims.push(Claim {
                name: "jwt-subject".into(),
                value: sub.clone(),
                confidence: Some(0.8),
                assurance_level: Some(AssuranceLevel::Verified),
                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 digest verified".into()
            } else {
                "JWT digest mismatch or data unavailable".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\":\"RS256\"}",
        );
        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-issuer"));
        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, CrewAI, 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.

CrewAI

Attestation Guardrails

Use jacs_guardrail with attestation mode to create trust-verified task execution:

from jacs.adapters.crewai import jacs_guardrail, JacsSignedTool
from jacs.client import JacsClient

client = JacsClient.quickstart()

@jacs_guardrail(client, attest=True)
def verified_analysis(task_result):
    """Guardrail that attests to analysis quality."""
    return task_result

Signed Tasks

from jacs.adapters.crewai import signed_task

@signed_task(client, attest=True, claims=[
    {"name": "analysis_type", "value": "financial", "confidence": 0.9},
])
def analyze_portfolio(data):
    return {"risk_score": 0.3, "recommendation": "hold"}

JacsSignedTool

class MyTool(JacsSignedTool):
    """A CrewAI tool with built-in attestation."""
    name = "market_data"
    description = "Fetch market data"
    attest = True
    default_claims = [
        {"name": "data_source", "value": "bloomberg"},
    ]

    def _run(self, ticker: str) -> dict:
        return {"ticker": ticker, "price": 150.0}

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"},
    "message",
)

# --- 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"]},
    "message",
    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' },
  'message',
);

// --- 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 algorithms (pq2025, pq-dilithium) 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 provides a detached-signature model for email. Your agent signs a raw RFC 5322 .eml file and the result is the same email with a jacs-signature.json MIME attachment. The recipient extracts that attachment, verifies the cryptographic signature, and compares content hashes to detect tampering.

There are only two functions you need:

ActionFunctionWhat you supplyWhat you get back
Signjacs::email::sign_email()raw .eml bytes + your EmailSigner.eml bytes with jacs-signature.json
Verifyjacs::email::verify_email()signed .eml bytes + sender's public key + verifierContentVerificationResult (pass/fail per field)

Signing an email

#![allow(unused)]
fn main() {
use jacs::email::{sign_email, EmailSigner};

// 1. Load raw email bytes (RFC 5322 format)
let raw_eml = std::fs::read("outgoing.eml")?;

// 2. Sign β€” your agent implements EmailSigner (see below)
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 EmailSigner trait

Your agent must implement four methods:

#![allow(unused)]
fn main() {
pub trait EmailSigner {
    /// Sign raw bytes. Return the signature bytes.
    fn sign_bytes(&self, data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>>;

    /// Your agent's JACS ID (e.g. "abc123:v1").
    fn jacs_id(&self) -> &str;

    /// The key identifier used for signing.
    fn key_id(&self) -> &str;

    /// The signing algorithm name. This comes from your JACS agent's
    /// key configuration β€” never hardcode it.
    fn algorithm(&self) -> &str;
}
}

The algorithm value (e.g. "ed25519", "rsa-pss", "pq2025") is read from your JACS agent's key metadata at runtime. sign_email records it in the jacs-signature.json document so the verifier knows which algorithm to use.

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. Canonicalizes the payload via RFC 8785 (JCS)
  5. Calls your sign_bytes() to produce the cryptographic signature
  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

This builds a verifiable forwarding chain. No extra code needed.

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 HAI registry or local store */;

// 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!("Email is authentic and unmodified");
} else {
    // Inspect which fields failed
    for field in &result.field_results {
        println!("{}: {:?}", field.field, field.status);
    }
}
}

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 every hash in the JACS document against the actual email content
  5. Returns per-field results

Two-step API (when you need the JACS document)

If you need to inspect the JACS document 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 cryptographic signature β€” returns the trusted JACS document
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
PassHash matches β€” field is authentic
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)

Fields checked: from, to, cc, subject, date, message_id, in_reply_to, references, body_plain, body_html, and all attachments.

The JACS signature document

The jacs-signature.json attachment has this structure:

{
  "version": "1.0",
  "document_type": "email_signature",
  "payload": {
    "headers": {
      "from":       { "value": "agent@example.com", "hash": "sha256:..." },
      "to":         { "value": "recipient@example.com", "hash": "sha256:..." },
      "subject":    { "value": "Hello", "hash": "sha256:..." },
      "date":       { "value": "Fri, 28 Feb 2026 12:00:00 +0000", "hash": "sha256:..." },
      "message_id": { "value": "<msg@example.com>", "hash": "sha256:..." }
    },
    "body_plain": { "content_hash": "sha256:..." },
    "body_html":  null,
    "attachments": [
      { "filename": "report.pdf", "content_hash": "sha256:..." }
    ],
    "parent_signature_hash": null
  },
  "metadata": {
    "issuer": "agent-jacs-id:v1",
    "document_id": "uuid",
    "created_at": "2026-02-28T12:00:00Z",
    "hash": "sha256:..."
  },
  "signature": {
    "key_id": "agent-key-id",
    "algorithm": "ed25519",
    "signature": "base64...",
    "signed_at": "2026-02-28T12:00:00Z"
  }
}

metadata.hash is the SHA-256 of the RFC 8785 canonical JSON of payload. signature.signature is the cryptographic signature over that same canonical JSON. The algorithm is always read from the agent β€” never hardcoded.

Public API summary

All items are re-exported from jacs::email:

#![allow(unused)]
fn main() {
// Signing
jacs::email::sign_email(raw_email: &[u8], signer: &dyn EmailSigner) -> Result<Vec<u8>, EmailError>
jacs::email::EmailSigner                  // trait your agent implements

// Verification
jacs::email::verify_email(raw, &agent, pubkey)       // one-call: crypto + content check
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::normalize_algorithm(...)                 // algorithm name normalization

// Types
jacs::email::ContentVerificationResult    // overall result with field_results
jacs::email::FieldResult                  // per-field status
jacs::email::FieldStatus                  // Pass | Modified | Fail | Unverifiable
jacs::email::JacsEmailSignatureDocument   // the full signature document
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

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 saved document path
# Document saved to: jacs_data/documents/[uuid]/[version].json

Create with custom output:

# Specify output filename
jacs document create -f invoice.json -o signed-invoice.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

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

Task Management

Creating Tasks

# Simple task
jacs task create \
  -n "Review Contract" \
  -d "Review the service contract and provide feedback"

# Task with additional data from file
cat > task-details.json << 'EOF'
{
  "priority": "high",
  "dueDate": "2024-02-15",
  "assignee": "legal-team"
}
EOF

jacs task create \
  -n "Contract Review" \
  -d "Detailed review required" \
  -f task-details.json

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

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

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

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

@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.sse_app()

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

    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

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/README.md
    • 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

# Use a specific algorithm
jacs quickstart --name my-agent --domain my-agent.example.com --algorithm ring-Ed25519

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 (default: pq2025). Also: ring-Ed25519, RSA-PSS
  • --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 init

Initialize JACS by creating both configuration and agent (with cryptographic keys). Use this for persistent agent setup.

jacs init

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]

Note: Specific subcommands for agent management are not detailed in the current help output.

Task Commands

jacs task

Work with JACS agent tasks - structured workflows between agents.

jacs task [SUBCOMMAND]

Note: Specific subcommands for task management are not detailed in the current help output.

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 filename for the created document
  • -d <directory> - Path to directory of files. Files should end with .json
  • -v, --verbose - Enable verbose output
  • -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

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

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]

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 - Verification failure (hash mismatch, invalid signature, etc.)
  • 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.

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.

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:

  • JACS_PRIVATE_KEY_PASSWORD - Password for encrypting/decrypting private keys (required for cryptographic operations)

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 - Cryptographic algorithm to use (default: pq2025)
  • jacs_default_storage - Storage backend (default: fs)
  • 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.

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 storageRemote 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")

{
  "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)

{
  "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
2Invalid ArgumentsCommand line arguments invalid
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 'RSA-PSS'

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 Signature

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

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 algorithm: ring-Ed25519, RSA-PSS, pq-dilithium, pq2025

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

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

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, "task")

# After (canonical)
signed = agent.sign_artifact(artifact_json, "task")

Node.js:

// Before (deprecated)
const wrapped = await agent.wrapA2aArtifact(artifactJson, 'task');

// After (canonical)
const signed = await agent.signArtifact(artifactJson, 'task');

Python A2A integration:

# Before (deprecated)
wrapped = a2a.wrap_artifact_with_provenance(artifact, "task")

# After (canonical)
signed = a2a.sign_artifact(artifact, "task")

Node.js A2A integration:

// Before (deprecated)
const wrapped = await a2a.wrapArtifactWithProvenance(artifact, 'task');

// After (canonical)
const signed = await a2a.signArtifact(artifact, 'task');

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

For increased security, you may want to migrate to post-quantum algorithms.

  1. Create New Agent with New Algorithm:

    {
      "jacs_agent_key_algorithm": "pq-dilithium"
    }
    
    jacs agent create --create-keys true -f new-agent.json
    
  2. Update Configuration:

    {
      "jacs_agent_key_algorithm": "pq-dilithium",
      "jacs_agent_id_and_version": "new-agent-id:new-version"
    }
    
  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

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