Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
JACS: JSON Agent Communication Standard
JACS is an open source provenance layer for agent systems. Use it when an output, tool call, file, image, email, or agent handoff crosses a trust boundary and logs alone are not enough.
Start With The Boundary
Most deployments start in one of four places:
- Core signing: sign JSON, files, Markdown/text, images, or Rust email payloads directly.
- MCP: run
jacs-compat mcpas a local tool server, or add signing around an existing MCP transport. - Frameworks: add provenance at LangChain, LangGraph, FastAPI, Express, Koa, or Vercel AI SDK boundaries.
- A2A and agreements: exchange signed artifacts or require multiple agents to sign off.
What JACS Gives You
- Persistent agent identity with encrypted private keys
- Tamper-evident signed JSON and file envelopes
- Inline Markdown/text signatures that stay readable in place
- Embedded PNG/JPEG/WebP signatures for media provenance
- Rust email signing and verification with field-level content hashes
- Trust policies and local trust-store workflows
- Cross-language verification across Rust, Python, Node.js, and Go
For platform workflows around verified documents, agent behavior, benchmarks, and hosted JACS identity flows, see HumanAssisted/haiai.
Best Entry Points
- Which Integration?
- Quick Start
- Use Cases
- MCP Overview
- Inline Text Signatures
- Image and Media Signatures
- Email Signing and Verification
Install
Rust CLI and MCP server
cargo install jacs-cli-compat --locked
jacs-compat quickstart --name my-agent --domain my-agent.example.com
jacs-compat mcp
Python
pip install jacs
Node.js
Source and published packages: Version 0.16.1 has a recorded release inventory. Version 0.16.0 is the first release whose source includes the native PQ DID/JWK and Agent Card behavior. See the identity integration guide.
npm install @hai.ai/jacs
Go
go get needs the matching CGo shared library. Each release publishes both
the semantic module tag and native libraries for macOS and Linux. Follow
Go Installation and Quick Start for the verified installer
or a full-repository source build.
What This Book Does Not Claim
- MCP and A2A are different boundaries: MCP is model-to-tool; A2A is agent-to-agent discovery and exchange.
- JACS does not require a registry, blockchain, or central server.
- Email signing is currently documented as a Rust core API, not a CLI or Python/Node/Go binding surface.
Community
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
What is JACS?
JACS is a cryptographic provenance layer for AI agents and the artifacts they produce. It gives an agent a persistent signing identity, wraps important outputs in tamper-evident signatures, and lets other systems verify origin and integrity later.
Use JACS when data crosses a trust boundary: another service, another agent, another organization, a user-facing audit trail, or a file that must remain verifiable after it leaves the process that created it.
The Problem
Agent systems increasingly produce artifacts that other systems act on:
- JSON tool results, reports, memories, and configs
- Markdown plans, design docs, and release notes
- Images, screenshots, charts, and generated media
- Email messages and attachments
- A2A artifacts, MCP tool calls, and multi-agent approvals
Logs can say what happened, but they are usually controlled by the same system that produced the data. JACS attaches proof to the artifact itself.
Core Ideas
| Concept | Meaning |
|---|---|
| Agent | A named identity with signing keys and metadata. |
| Signed document | A JSON envelope with payload, hash, signer metadata, and signature. |
| Artifact signature | A JACS signature attached to non-JSON content, such as text, images, or email. |
| Trust store | Local public keys and trust decisions used during verification. |
| Agreement | A signed multi-party approval with quorum, timeout, and algorithm constraints. |
What Verification Proves
Verification answers two practical questions:
- Did the signer with this public key sign these canonical bytes?
- Has the signed content changed since it was signed?
Verification does not prove first creation, copyright ownership, human authorship, or that a real-world statement is true. It gives you cryptographic accountability for the artifact.
Where JACS Fits
flowchart LR
A["Agent or service"] --> B["Create artifact"]
B --> C["Sign with JACS"]
C --> D["Share or store"]
D --> E["Verify before trust"]
JACS does not replace your application protocol. It works with CLI jobs, Rust services, Python and Node frameworks, MCP tools, A2A exchange, and file-based workflows.
When To Use It
- A downstream system should verify who produced an artifact.
- Multiple agents or teams need an auditable handoff.
- A document, image, or email must stay verifiable outside your database.
- You need open source signing primitives without a mandatory central registry.
When Not To Use It
- Everything stays inside one trusted process and logs are enough.
- You only need accidental-corruption detection.
- There is no signer identity or audit requirement.
Next Steps
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Which JACS Path Should I Use?
Choose the smallest supported integration that matches the boundary you need to secure.
Start Here
| If you need... | Start here | Why |
|---|---|---|
| Sign and verify JSON, files, or one-off artifacts | Quick Start | Establish the agent identity and trust model first. |
| A ready-made local MCP server for Claude, Codex, or another MCP client | MCP Overview and jacs-compat mcp | Fastest full server path; stdio only, no HTTP port. |
| Signed tool outputs inside LangChain / LangGraph on Python | Python Framework Adapters | Add provenance without changing the rest of the app. |
| Signed tool outputs inside LangChain.js / LangGraph on Node | Node.js LangChain.js | Same pattern for TypeScript. |
| Secure existing MCP server/client code | Python MCP or Node.js MCP | Wrap the transport or register JACS tools. |
| Sign Markdown or text in place | Inline Text Signatures | Signature stays with the readable file; supports multi-signer review. |
| Attach provenance to PNG, JPEG, or WebP images | Image and Media Signatures | Signature is embedded in the image; no sidecar JSON. |
| Sign and verify raw email | Email Signing and Verification | Rust API for .eml messages with field-level content hashes. |
| Cross-organization agent discovery and artifact exchange | A2A Interoperability | Use A2A for agent-to-agent boundaries. |
| Multi-party approval or quorum workflows | Multi-Agent Agreements | Agreements are the right primitive for sign-off. |
When You Probably Do Not Need JACS
- Everything stays inside one service you control and logs are enough.
- You only need integrity, not signer identity or third-party verification.
- A plain checksum or database audit log already satisfies the requirement.
Recommended Adoption Order
- Prototype with quickstart and simple sign/verify calls.
- Attach provenance at the boundary that already exists: file, API response, framework tool, MCP call, image, email, or A2A artifact.
- Add trust policy when other agents, services, or organizations enter the workflow.
- Add agreements, DNS, or attestations only when the deployment needs them.
Start with the boundary you need to secure now.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Use Cases
These are current JACS use cases, not speculative integrations.
1. Keep JSON and files verifiable
Use JACS when a report, memory, config, policy, or audit artifact needs to stay verifiable after it leaves the process that created it.
Start with:
What JACS adds:
- Self-contained signed envelopes
- Re-verification at read time
- Cross-language compatibility across Rust, Python, Node.js, and Go
2. Sign Markdown in place for review
Use JACS when a README, design doc, policy, or release note needs visible content and verifiable sign-off in the same file.
Start with:
JACS_CONFIG=./reviewer-a.config.json jacs-compat sign-text DESIGN.md
JACS_CONFIG=./reviewer-b.config.json jacs-compat sign-text DESIGN.md
jacs-compat verify-text DESIGN.md
What JACS adds:
- A readable signature block appended at the end
- Multi-signer review without sidecar JSON
- Permissive or strict verification modes
3. Embed provenance in images
Use JACS when a photo, chart, screenshot, or AI-generated image needs verifiable signer identity without a separate metadata file.
Start with:
jacs-compat sign-image photo.png --out signed.png
jacs-compat verify-image signed.png
jacs-compat extract-media-signature signed.png
What JACS adds:
- Embedded PNG iTXt, JPEG APP11, or WebP XMP signatures
- Pixel-content integrity checks
- Optional robust mode for PNG/JPEG metadata-strip threat models
4. Sign email with Rust
Use JACS when raw RFC 5322 email needs a verifiable signature attachment and field-level content verification.
Start with:
What JACS adds:
jacs-signature.jsonMIME attachment- Header, body, and attachment hashes
- Forwarding chains through parent signature hashes
Email signing is currently documented as a Rust core API via jacs::email, not as a CLI or Python/Node/Go binding surface.
5. Secure a local MCP tool server
Use JACS when an MCP client needs local document signing or supplied-key integrity checks.
Start with:
For keyless verification, install the CLI and start the explicit verification profile:
cargo install jacs-cli-compat --locked
jacs-compat mcp --profile verify-only
For document signing, configure a password source before creating an identity, or use an existing signing config. Then start the explicit signing profile:
jacs-compat quickstart --name mcp-agent --domain mcp-agent.example
jacs-compat mcp --profile local-sign --config ./jacs.config.json
To also enable scoped text/image tools, prefix the final command with
JACS_MCP_BASE_DIR=/path/to/content. Each server command runs until the MCP
client closes it; verification and signing are alternative startup choices.
What JACS adds:
- A ready-made stdio MCP server
- Explicit-key document verification by default; JSON/Agreement tools with
local-sign - Five text/image tools only with
local-sign, a signing config andJACS_MCP_BASE_DIR - Local private-key custody; no HTTP listener
Signatures establish key-backed provenance, not per-action human approval,
truth or authority. Loading a config alone does not grant signing. Trust/admin
capabilities remain unavailable through these profiles; inspect tools/list
for the running process's actual inventory.
6. Add provenance to framework boundaries
Use JACS when an existing app already has a boundary such as LangChain tool output, FastAPI response, Express middleware, or Koa middleware.
Start with:
What JACS adds:
- Signed tool results and responses
- Optional strict verification at adapter boundaries
- Minimal changes to existing framework code
7. Exchange artifacts across agents or organizations
Use JACS when one agent produces work that another agent, service, or organization must verify before acting on it.
Start with:
What JACS adds:
- Signed A2A artifacts
- Trust policies for admission control
- Agreement-v2 inspection of mathematical signature coverage; M-of-N, role, status and algorithm policy claims require separate application evaluation
Agreement-v2 math is not policy acceptance: its native valid and
policyAccepted stay false. Quorum counts, human/notary labels and final status
cannot establish human approval or authority. See the
Agreement-v2 guide for the inspection/report distinction.
8. Publish identity without a central auth service
Use JACS when external systems need to verify an agent identity without putting a shared server in the middle.
Start with:
What JACS adds:
- Public key fingerprint anchoring
- Local trust-store workflows
- Local private-key custody
9. Add hosted verification with HAI.AI
Use HumanAssisted/haiai when you want platform workflows for verified documents, agent behavior, benchmarks, and hosted JACS identity flows.
JACS remains the local open source signing and verification layer. HAI.AI is the hosted platform path around those identities and artifacts.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Core Concepts
JACS is a portable signing and verification system for JSON documents. Its main primitive is a verifiable JSON envelope: canonical bytes, schema-declared structure, content hash, signer identity, signing algorithm, and cryptographic signature.
Agents
An Agent is an identity that can create, sign, and verify documents.
{
"jacsId": "550e8400-e29b-41d4-a716-446655440000",
"jacsVersion": "123e4567-e89b-12d3-a456-426614174000",
"jacsType": "agent",
"jacsAgentType": "ai",
"jacsAgentDomain": "agent.example.com"
}
Agents have stable IDs, versioned identity documents, and cryptographic keys. A2A capabilities are represented through A2A Agent Cards.
Documents
A Document is any JSON object signed with the JACS header/signature wrapper.
{
"$schema": "https://hai.ai/schemas/header/v1/header.schema.json",
"jacsId": "doc-uuid-here",
"jacsVersion": "version-uuid-here",
"jacsType": "document",
"jacsVersionDate": "2024-01-15T10:30:00Z",
"jacsLevel": "artifact",
"content": {
"title": "Analyze Q4 Sales Data",
"description": "Generate insights from sales data"
},
"jacsSha256": "hash-of-document-content",
"jacsSignature": {
"agentID": "agent-uuid",
"agentVersion": "agent-version-uuid",
"signature": "base64-signature",
"signingAlgorithm": "ring-Ed25519",
"publicKeyHash": "hash-of-public-key",
"date": "2024-01-15T10:30:00Z",
"fields": ["$schema", "jacsId", "jacsVersion", "jacsType", "content"]
}
}
Application-specific meaning belongs inside the payload or in a custom schema that extends the header.
Agreements
Agreements collect multiple signatures over the same document payload.
{
"jacsAgreement": {
"agentIDs": ["agent-1-uuid", "agent-2-uuid"],
"question": "Do you approve this change?",
"context": "Deployment approval",
"quorum": 2,
"signatures": []
},
"jacsAgreementHash": "hash-of-agreement-content"
}
This legacy sidecar permits signature inspection only: its participant list,
roles, question, context and quorum are unauthenticated. A claimed completion
status is not consent-policy acceptance. New portable consent uses Agreement
v3; native Agreement v2 inspection likewise keeps policyAccepted=false.
See the agreement guide for the version boundaries.
Verification
When consuming signed documents, verify in one of two ways:
- With a loaded agent: call
verify(signedDocument)using the configured trust store and key resolution. - Without loading an agent: call
verify_standalone(signedDocument, options)with explicit key-resolution options.
Cryptographic Security
JACS signs canonical JSON bytes. Verification checks the content hash, signature fields, signer identity, algorithm, and public key.
Supported algorithms:
| Algorithm | Description |
|---|---|
ring-Ed25519 | Fast elliptic curve signatures |
pq2025 | Post-quantum ML-DSA-87 signatures |
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Quick Start Guide
Get a persistent agent identity, sign data, and verify it.
Start with MCP
Install the portable CLI and start its default verification-only MCP server:
cargo install jacs-cli --locked
jacs mcp
{
"mcpServers": {
"jacs": { "command": "jacs", "args": ["mcp"] }
}
}
Verification requires independently trusted public evidence. A valid signature proves integrity under that key; your application decides identity and authority.
Portable CLI
Create an encrypted identity, sign JSON and verify with the exported public identity:
mkdir -m 700 .jacs
jacs create --output .jacs/agent.json > public-identity.json
printf '%s' '{"message":"hello"}' | jacs sign --agent .jacs/agent.json --input - > signed.json
jacs verify --public-identity public-identity.json --input signed.json
The terminal prompts for the password. For an MCP client that can provide the
host's configured secret source, explicitly enable signing with
jacs mcp --profile local-sign --agent /absolute/path/to/.jacs/agent.json.
Use the same arguments in its mcpServers configuration. See the
current MCP guide
for secret handling and the available tools.
The remaining native examples use a separate installation:
cargo install jacs-cli-compat --locked
jacs-compat quickstart --name my-agent --domain my-agent.example.com
This creates jacs.config.json and persistent native keys for direct libraries
and extended text/image commands. The native MCP profiles use jacs-compat mcp;
see extended profiles.
Python
python -m pip install jacs==0.16.1
import jacs.simple as jacs
info = jacs.quickstart(name="my-agent", domain="my-agent.example.com")
signed = jacs.sign_message({"action": "approve", "amount": 100})
result = jacs.verify(signed.raw)
print(f"Valid: {result.valid}, Signer: {result.signer_id}")
Node.js
Source and published packages: Version 0.16.1 has a recorded release inventory. Version 0.16.0 is the first release whose source includes the native PQ DID/JWK and Agent Card behavior. See the identity integration guide.
npm install --save-exact @hai.ai/jacs@0.16.1
import * as jacs from '@hai.ai/jacs/simple';
await jacs.quickstart({
name: 'my-agent',
domain: 'my-agent.example.com',
});
const signed = await jacs.signMessage({ action: 'approve', amount: 100 });
const result = await jacs.verify(signed.raw);
console.log(`Valid: ${result.valid}, Signer: ${result.signerId}`);
Sign Markdown or text
jacs-compat sign-text README.md
jacs-compat verify-text README.md
jacs-compat verify-text --strict README.md
sign-text appends a -----BEGIN JACS SIGNATURE----- block at the end of the file. The original content remains readable and can be counter-signed by another agent.
See Inline Text Signatures for multi-signer flows, strict mode, and --key-dir.
Sign images
jacs-compat sign-image photo.png --out signed.png
jacs-compat verify-image signed.png
jacs-compat extract-media-signature signed.png
JACS embeds the signature in PNG, JPEG, or WebP metadata. See Image and Media Signatures for overwrite policy, robust mode, and verification details.
Native password bootstrap
Choose one explicit password source for jacs-compat: the host-provided
JACS_PRIVATE_KEY_PASSWORD, or JACS_PASSWORD_FILE pointing to a protected
secret file. Setting both is an error. When neither is set, the native CLI can
use the OS keychain; see password and keychain setup.
Python and Node quickstart can generate a password in-process. Retain it through your host secret source for later processes; see the language quickstart guide.
Algorithm
The default signing algorithm is pq2025 (ML-DSA-87 / FIPS-204). Use the
user-facing ed25519 choice if you need a smaller classical signature. JACS
stores and emits the canonical Ed25519 wire label as ring-Ed25519:
jacs-compat quickstart --name my-agent --domain my-agent.example.com --algorithm ed25519
Next steps
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Multi-Agent Agreements
Agreement v2 records terms and agent consent signatures in a standalone jacsType: "agreement" document. Its verifier inspects mathematical and structural checks. It does not accept the full authorization policy or prove human approval, even when the supplied quorum produces a final status.
Use this walkthrough to inspect agent provenance. An application must separately establish the authority and any exact-action human approval required before acting.
The Lifecycle
Create agreement -> append transcript refs -> collect signer/notary signatures -> inspect math and policy scope
Python
All three demo identities are controlled by this code, including the agent labeled notary; no person or external notary approves anything here.
The three agents are persistent and share one data_directory so each can resolve the others' public keys when verifying. Independent ephemeral() agents keep keys in memory only and cannot verify each other's signatures.
import os
import secrets
import tempfile
from pathlib import Path
from jacs import SimpleAgent
os.environ["JACS_KEYCHAIN_BACKEND"] = "disabled" # throwaway demo only
PASSWORD = secrets.token_urlsafe(32)
workspace = Path(tempfile.mkdtemp(prefix="jacs_multi_agent_"))
shared_data = workspace / "shared_data" # all agents share this so keys resolve
def make_agent(name, agent_type="ai"):
agent, info = SimpleAgent.create_agent(
name=name,
password=PASSWORD,
algorithm="ring-Ed25519",
data_directory=str(shared_data),
key_directory=str(workspace / f"{name}_keys"),
config_path=str(workspace / f"{name}.config.json"),
agent_type=agent_type,
)
return agent, info
agent_a, a = make_agent("agent-a")
agent_b, b = make_agent("agent-b")
notary, n = make_agent("demo-notary")
agreement = agent_a.create_agreement_v2({
"title": "Bounded refund authorization",
"description": "Demo refund terms for agent signature inspection; no refund is authorized.",
"terms": "Agent B may issue a refund up to $25 for order 123 after Agent A approval.",
"termsFormat": "text/markdown",
"status": "proposed",
"parties": [
{"agentId": a["agent_id"], "agentType": "ai", "role": "signer"},
{"agentId": b["agent_id"], "agentType": "ai", "role": "signer"},
{"agentId": n["agent_id"], "agentType": "ai", "role": "notary"},
],
"signaturePolicy": {
"partyQuorum": "all",
"witnessRequired": 0,
"notaryRequired": 1,
"minimumStrength": "classical",
},
"controllers": [a["agent_id"], b["agent_id"], n["agent_id"]],
})
agreement = agent_a.sign_agreement_v2(agreement, "signer")
agreement = agent_b.sign_agreement_v2(agreement, "signer")
agreement = notary.sign_agreement_v2(agreement, "notary")
# The demo notary resolves the signers' public keys from shared storage.
report = notary.verify_agreement_v2(agreement)
assert report["mathematicalChecksValid"] is True
assert report["valid"] is False
assert report["policyAccepted"] is False
assert report["overallScope"] == "consent_signatures_only"
assert report["expectedStatus"] == "final" # structural diagnostic only
This abbreviated snippet leaves its temporary workspace for inspection; remove it when finished. The runnable version includes transcript references and adversarial checks, and cleans up its temporary keys:
python examples/agreement_v2_three_party.py
What the Report Means
mathematicalChecksValidcovers the native hash, signature and structural checks. Inspecterrorsfor failures andnotes,verifiedChainDepthandchainFullyVerifiedfor lineage coverage limits.- Consent signatures bind the agreement identity and consent hash; signatures made with a nonempty transcript also bind its prefix at signing. Later unsigned transcript entries are not retroactively covered.
- Signer counts, quorum, witness/notary requirements and
expectedStatusare diagnostics against the supplied document. Neither a count norstatus: "final"is an accepted policy verdict. - Helpers check membership, role and controller lists. These checks do not establish portable role/controller authority or a person's approval. Changing an agent's label to
humandoes not add human approval evidence. validandpolicyAcceptedremain false, including for the successful mathematical inspection above. Do not turn either field into true or treatmathematicalChecksValidas permission to act.
Legacy Sidecar Agreements
The older create_agreement() / sign_agreement() / check_agreement() API still exists for adding jacsAgreement metadata to an arbitrary signed document. It supports signature inspection of an existing payload; those signatures alone do not establish human approval or the complete application policy.
V2 adds standalone terms, transcript-prefix evidence and branch helpers. Moving to v2 does not create an actionable authorization.
Next Steps
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Verifying Signed Documents
Verify a JACS-signed document in under 2 minutes. Verification checks that the document has not changed and was signed by the key selected under your trust policy. A signature alone does not establish a real-world identity or permission to act.
Verification does NOT require creating an agent. You need the signed document and independently trusted public evidence for its signer.
Raw JSON is decoded strictly before signing, canonicalization, or verification.
Duplicate object names are rejected at every nesting level, including names
that become equal after JSON escape decoding (for example, agentID and
agent\u0049D). This prevents two consumers from interpreting the same signed
bytes with different first-key/last-key rules. Repeated values in arrays are
ordinary JSON and remain supported.
Strict vs permissive verification
verify-text and verify-image use a permissive default: a missing signature is a typed status, not an error. Strict mode opts in to error-on-missing. The choice maps onto every binding the same way.
| Surface | Permissive (default) | Strict (--strict / strict=True / { strict: true } / Strict: true) |
|---|---|---|
jacs-compat verify-text (CLI) | Exit 0 valid · 2 missing-sig · 1 invalid | Exit 0 valid · 1 missing OR invalid (stderr: no JACS signature found) |
jacs-compat verify-image (CLI) | Same as above | Same as above |
Python (jacs.verify_text, jacs.verify_image) | Returns result with status == "missing_signature" | Raises MissingSignatureError |
Node (jacs.verifyText, jacs.verifyImage) | Returns result with status === "missing_signature" | Promise rejects with MissingSignature-bearing message |
Go (jacs.VerifyText, jacs.VerifyImage) | Returns Status == "missing_signature" | Returns error matching errors.Is(err, jacs.ErrMissingSignature) |
Rust (jacs::simple::advanced) | Successful result with a missing-signature variant/status | Err(JacsError::MissingSignature) |
Pre-existing verify surfaces (jacs-compat verify, jacs-compat document verify, jacs.verify) are unaffected — they keep their existing exit codes and return shapes.
For hands-on examples see Inline Text Signatures and Image and Media Signatures.
Portable CLI
cargo install jacs-cli --locked
jacs verify --public-identity trusted-public-identity.json --input signed-document.json
Obtain trusted-public-identity.json independently of the untrusted document.
The portable CLI uses explicit --input and --public-identity arguments;
it does not use the native config or remote/key-directory options below.
Install cargo install jacs-cli-compat --locked for the extended native commands.
CLI: jacs-compat verify
The fastest way to verify a document from the command line. No config file, no agent setup.
# Verify a local file
jacs-compat verify signed-document.json
# Verify with JSON output (for scripting)
jacs-compat verify signed-document.json --json
# Verify a remote document by URL
jacs-compat verify --remote https://example.com/signed-doc.json
# Specify a directory containing public keys
jacs-compat 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-compat verify artifact.json --json; then
echo "Artifact is authentic"
else
echo "Verification failed" >&2
exit 1
fi
If a jacs.config.json and agent keys exist in the current directory, the CLI uses them automatically. Otherwise, it creates a temporary ephemeral verifier internally.
Python
With an agent loaded
import jacs.simple as jacs
jacs.load("./jacs.config.json")
result = jacs.verify(signed_json)
if result.valid:
print(f"Signed by: {result.signer_id}")
else:
print(f"Errors: {result.errors}")
Without an agent (standalone)
import jacs.simple as jacs
result = jacs.verify_standalone(
signed_json,
key_resolution="local",
key_directory="./trusted-keys/"
)
print(f"Valid: {result.valid}, Signer: {result.signer_id}")
verify_standalone does not use a global agent. Pass the key resolution strategy and directories explicitly.
Verify by document ID
If the document is in local storage and you know its ID:
result = jacs.verify_by_id("550e8400-e29b-41d4:1")
Node.js
With an agent loaded
import * as jacs from '@hai.ai/jacs/simple';
await jacs.load('./jacs.config.json');
const result = await jacs.verify(signedJson);
console.log(`Valid: ${result.valid}, Signer: ${result.signerId}`);
Without an agent (standalone)
import { verifyStandalone } from '@hai.ai/jacs/simple';
const result = verifyStandalone(signedJson, {
keyResolution: 'local',
keyDirectory: './trusted-keys/',
});
console.log(`Valid: ${result.valid}, Signer: ${result.signerId}`);
Verify by document ID
const result = await jacs.verifyById('550e8400-e29b-41d4:1');
DNS Verification
DNS verification checks that an agent's public key hash matches a DNS TXT record published at _v1.agent.jacs.<domain>. This provides a decentralized trust anchor: anyone can look up the agent's expected key fingerprint via DNS without contacting a central server.
Publishing a DNS record
jacs-compat 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-compat agent lookup example.com
This inspection command fetches and prints https://example.com/.well-known/jacs-pubkey.json and the DNS TXT record at _v1.agent.jacs.example.com. It does not compare the fetched key against the TXT fingerprint or establish trust. Use the verification APIs and an independently selected key for that decision.
CLI verification with DNS
# Require DNS validation (fail if no DNS record)
jacs-compat agent verify --require-dns
# Require strict DNSSEC validation
jacs-compat agent verify --require-strict-dns
For full DNS setup instructions, see DNS-Based Verification and DNS Trust Anchoring.
Cross-Language Verification
JACS signatures are language-agnostic. A document signed by a Rust agent verifies identically in Python and Node.js, and vice versa. This holds for both Ed25519 and post-quantum (ML-DSA-87/pq2025) algorithms.
Legacy-v1 signatures that lack signatureContentVersion are denied by default
because their signer, timestamp, nonce, and algorithm metadata was never covered
by the signature. Use the migration API to re-sign them as v2. For a narrowly
audited archive workflow, JACS_ALLOW_LEGACY_SIGNATURE_CONTENT=true enables
payload-only compatibility; verification results deliberately leave signer,
agent-version, and timestamp fields empty. Do not use that mode for
authorization or identity attribution. The historical
JACS_REJECT_LEGACY_SIGNATURE_CONTENT=true setting remains supported and
takes precedence.
This is tested on every commit: current v2 fixtures verify by default across bindings, while committed v1 fixtures prove both default rejection and explicit payload-only compatibility. Each binding also countersigns the fixture with a different algorithm, proving round-trip interoperability.
Test sources:
- Rust fixture generator:
jacs/tests/cross_language/mod.rs - Python consumer:
jacspy/tests/test_cross_language.py - Node.js consumer:
jacsnpm/test/cross-language.test.js
Key Resolution Order
Native document verification uses JACS_KEY_RESOLUTION (environment only),
defaulting to local,registry. Local storage supplies cached/trusted public
keys; registry lookup requires the relevant network policy. Adding dns, with
DNS and remote key fetch allowed and a configured or signed publisher domain,
fetches the exact signed version's key over HTTPS and pins it to the TXT
record's raw-key fingerprint. Discovered keys do not enroll a trusted identity.
See the key discovery guide,
DNS verification and trust enrollment.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
What Is an Attestation?
Attestations bind a signer to claims and evidence references.
A JACS attestation is a cryptographically signed document that goes beyond checking a signature. It records claims, evidence and the signer's stated reasoning. The relying application decides whether those claims are true and whether the signer and evidence are trusted.
Signing vs. Attestation
sign_message() | create_attestation() | |
|---|---|---|
| Checks | Integrity under the selected key | Integrity plus bounded evidence and derivation checks |
| Contains | Signature + hash | Signature + hash + subject + claims + evidence |
| Use case | Data integrity | Trust decisions, audit trails, compliance |
| Verification | Was it tampered with? | Integrity, evidence digests, derivation and configured freshness limits |
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 evidence and derivation
checks. Freshness is checked when maxEvidenceAge is configured; claim truth,
identity and application authorization are not established by this report.
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.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
JACS Trust Layers
JACS organizes trust into three distinct layers. Each layer has a clear scope and its own vocabulary. Understanding which layer you need prevents confusion between identity, transport policy, and evidentiary trust.
The Three Layers
Layer A: Identity + Integrity (JACS Core)
Scope: Who signed what, and has it been tampered with?
APIs: sign_message(), verify(), verify_standalone()
This is the foundation. A valid JACS signature links the covered content to the signing key and detects changes to that content. Binding the key to an agent or person requires trusted identity evidence; the signature alone does not establish who created the content, whether its claims are true, or whether a person approved it. Layer A answers: "Is this signature valid?"
Crypto status values: Verified · SelfSigned · Unverified · Invalid
- Verified: Signature is valid and signer's key was resolved from a trusted source.
- SelfSigned: Signature is valid but signer is the same as verifier (no third-party trust).
- Unverified: Signature could not be checked because the signer's key was not available.
- Invalid: Signature check failed — the content was tampered with or the wrong key was used.
Layer B: Exchange + Discovery (A2A Integration)
Scope: Is this agent allowed to communicate with me?
APIs: sign_artifact(), verify_wrapped_artifact(), assess_remote_agent(), discover_agent()
Layer B handles cross-boundary exchange between agents using the A2A protocol. It adds trust policy on top of Layer A's cryptographic status. Layer B answers: "Should I accept artifacts from this agent?"
Policy status values: allowed · blocked · not_assessed
Trust policies (open, verified, strict) control admission:
| Policy | Requirement |
|---|---|
open | Accept all agents without identity assurance |
verified | Valid same-origin Agent Card JWS plus a durable signing-key TOFU pin; proves origin/key continuity, not native identity |
strict | Explicitly trusted native root matching a directly signed card; explicit ES256 additionally requires a valid binding for identity, version, key, scope and lifecycle |
These policies parse legacy cards, not A2A 1.0. 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
| Term | Layer | Meaning |
|---|---|---|
| Crypto status | A | Outcome of signature verification: Verified, SelfSigned, Unverified, Invalid |
| Policy status | B | Outcome of trust policy check: allowed, blocked, not_assessed |
| Attestation status | C | Outcome of attestation verification: local_valid, full_valid |
| Verified | A | Signature is valid and signer key was resolved |
| SelfSigned | A | Signature is valid but signer is the verifier |
| Unverified | A | Key not available — cannot check signature |
| Invalid | A | Signature check failed |
| Allowed | B | Agent passes the configured trust policy |
| Blocked | B | Agent does not pass the trust policy |
| Not assessed | B | No agent card provided — trust not evaluated |
Quick Decision Flow
"Which layer do I need?"
- I just need to prove data hasn't been tampered with → Layer A. Use
sign_message()andverify(). - I need to exchange signed data with other agents → Layer B. Use
sign_artifact()and A2A discovery. See the A2A Quickstart. - I need to record WHY data should be trusted → Layer C. Use
create_attestation(). See the Attestation Tutorial. - 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).
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Deployment Compatibility
The release inventory records verified 0.16.1 packages, platforms, checksums and provenance. Prebuilt, CI-tested and runtime-tested coverage differ; source compilation alone does not establish them. Later source changes need separate release evidence.
Declared toolchain baselines
| Surface | Declared or tested baseline |
|---|---|
| Rust library, CLI, and WASM source | Rust 1.97; Edition 2024 |
| Python | Python 3.11–3.14 is declared by pyproject.toml; the source release workflow smoke-tests the exact wheel candidate on each version before publication. |
| Node | Installed native addon: Node 20+. Building with NAPI-RS CLI 3 requires Node ^20.17.0, ^22.13.0 or >=23.5.0. |
| Go | go.mod declares Go 1.21; CGo and a platform C toolchain are also required |
Platform decisions
Windows
The observed Windows prebuilt is the x86_64 CLI only. Do not infer Windows
Python or Node support from the CLI artifact: PyPI 0.16.1 has no Windows
wheel, npm 0.16.1 has no Windows .node binary, and the Go bindings have no
Windows CGo linker directive or native asset. The browser/WASM source may be
portable at the WebAssembly level, but no Windows browser runtime lane was
recorded.
Linux libc and architecture
“Linux” is not one ABI:
- Python glibc wheels are tagged
manylinux_2_28; systems with glibc older than 2.28 cannot use those wheels. The observed musllinux wheel is x86_64 only. - Node
0.16.1ships glibc x86_64 and arm64 binaries and a musl x86_64 binary; there is no musl arm64 binary. - CLI
0.16.1ships glibc Linux x86_64 and arm64 archives. No musl CLI archive is recorded in the shipped inventory. - A source build is a separate claim. It may work on additional targets when the required compiler, linker, libc headers, Rust dependencies, and system libraries are available, but that is not prebuilt support.
Containers, Lambda, and serverless platforms
A container orchestrator does not remove the native ABI requirement. Match the
image architecture and libc to an actual wheel, .node file, CLI archive, or
Go shared library. For example, the observed Python musllinux wheel supports an
x86_64 Alpine-style image, while its glibc wheels require glibc 2.28 or newer.
This x86_64 example deliberately requires a prebuilt musllinux wheel instead of silently falling back to an undocumented source build:
FROM --platform=linux/amd64 python:3.12-alpine
RUN python -m pip install --only-binary=:all: "jacs==0.16.1"
ENV JACS_KEYCHAIN_BACKEND=disabled
WORKDIR /app
COPY . /app
CMD ["python", "main.py"]
AWS Lambda, Vercel, Cloudflare Workers, Deno, and Bun do not have dedicated
runtime-smoke lanes in the recorded matrix. Treat them as unverified. For
Lambda or another native serverless runtime, build and test a layer against
the exact runtime image and architecture; do not assume a generic Linux
artifact is ABI-compatible. Browser-only runtimes require the published @hai.ai/jacs-wasm package.
For headless native environments, set JACS_KEYCHAIN_BACKEND=disabled and
supply key passwords through the deployment platform's secret mechanism. Do
not bake passwords or private keys into an image.
Browser/WASM capability limits
The WASM surface defaults to pq2025 identities, with explicit Ed25519 and
ES256 compatibility, and can
sign and verify JSON, and use encrypted browser persistence. It does not expose
native filesystem storage, DNS resolution, MCP, or the native CLI. Browser
WebAssembly memory is JavaScript-visible; source compatibility is not a claim
of hardware-backed key isolation. See the WASM package security caveats before
using it for long-lived signing keys.
Building from source
Source builds require the full repository because the language bindings use path dependencies on the Rust crates:
git clone https://github.com/HumanAssisted/JACS.git
cd JACS
# Python development build
python -m pip install maturin
make build-jacspy
# Node development build
make build-jacsnpm
# Go native library + module
make build-jacsgo
# Browser package
make build-wasm
Rust 1.97 or newer is required. Native language bindings also require their language toolchain, a C linker, and platform system dependencies. A successful source build on one machine does not add that target to the published-artifact matrix.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Troubleshooting
Common issues and solutions when installing or using JACS.
Installation Issues
pip install fails
Published 0.16.1 wheels and this source checkout require Python 3.11–3.14. If no pre-built wheel exists for your platform, install the Rust toolchain and build from source:
pip install maturin
make build-jacspy # from the repository root
npm install fails
Native npm binaries cover macOS x64/arm64, Linux x64/arm64 for glibc and
Linux x64 for musl. There is no Windows addon; a Windows CLI archive is a separate artifact.
For custom source builds, use make build-jacsnpm from the repository root
with the documented Node and Rust toolchains.
Alpine Linux / musl libc
Published 0.16.1 includes a Python musllinux x86_64 wheel and an npm musl x86_64 addon; neither has a musl arm64 build. CLI archives are glibc-only; the Python/npm CLI shims reject musl even when library APIs work. Match the artifact to architecture and libc; see deployment compatibility.
Configuration Issues
Config not found
Run jacs-compat 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 jacs-compat, you may also set JACS_PASSWORD_FILE to a file that contains only the password, or use jacs-compat keychain set to store the password in the OS keychain. Set exactly one explicit source; if both env var and password file are set, CLI fails by design. The OS keychain is only consulted when neither env var nor password file is present.
Algorithm detection failed
Set jacs_agent_key_algorithm in the config, or pass the algorithm explicitly
to quickstart(...) / create(...). User-facing values are pq2025 and
ed25519; the legacy input alias and canonical Ed25519 wire label are
ring-Ed25519.
Runtime Issues
Agent creation fails
Ensure the data and key directories exist and are writable. By default these are ./jacs_data and ./jacs_keys.
Signature verification fails
Ensure the signer's public key is accessible. If verifying a document from another agent, you may need to import their public key or use the trust store.
Documents not found
Check the jacs_data_directory path in your config. Documents are stored as JSON files in that directory.
Building from Source
git clone https://github.com/HumanAssisted/JACS.git
cd JACS
# Rust core + CLI
cargo build --release
cargo install --path jacs-cli --locked
# Python binding
make build-jacspy # from the repository root
# Node.js binding
make build-jacsnpm
Requires Rust 1.97+ (install via rustup).
Getting Help
- GitHub Issues -- report bugs and feature requests
- Quick Start Guide -- step-by-step setup
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Rust and CLI installation
Portable CLI and MCP
cargo install jacs-cli --locked
jacs mcp
The jacs binary starts MCP in verification-only mode by default. For signing,
create an encrypted identity and explicitly select it; see the
quick start and
current MCP guide.
Native integrations
The extended commands documented in this reference use a separate executable:
cargo install jacs-cli-compat --locked
jacs-compat quickstart --name my-agent --domain my-agent.example.com
jacs-compat --help
Its jacs.config.json, key directories, DNS, storage and extended MCP profiles
belong to the native compatibility workspace. See CLI usage.
Rust libraries
For portable cryptography without I/O:
[dependencies]
jacs-core = "=0.16.1"
serde_json = "1"
For native identity, storage and integrations:
[dependencies]
jacs = "=0.16.1"
serde_json = "1"
Native optional features include a2a, agreements, attestation, s3,
sqlx-sqlite, otlp-logs, otlp-metrics and otlp-tracing; SQLite is enabled
by default. Select only the features your integration needs. The
library guide covers the native API.
Publication and platforms
The release inventory
records 0.16.1 across 16 of the 17 Rust crates (jacs-surrealdb stays at 0.15.0),
CLI archives and language packages.
Cargo builds libraries from source. CLI archives cover macOS arm64/x86_64,
Linux glibc arm64/x86_64 and Windows x86_64; this does not imply Windows Python,
Node or Go native artifacts. See deployment compatibility.
The browser package is published as @hai.ai/jacs-wasm; see its
installation guide. Release 0.16.0 is the first in which the
identity-standard exports reuse the persistent ML-DSA-87 identity.
Build this checkout
Source builds require Rust 1.97 or newer (Edition 2024), a linker and any native system dependencies. From the repository root:
git clone https://github.com/HumanAssisted/JACS.git
cd JACS
cargo install --path jacs-cli --locked
cargo install --path archive/native/jacs-cli --locked
The active workspace contains jacs-core, jacs-wasm, jacs-mobile, jacs-mcp
and jacs-cli. Native packages use archive/native/Cargo.toml; for example:
cargo check --manifest-path archive/native/Cargo.toml -p jacs --locked
Use the developer commands for binding builds and the shared Rust cache setup before substantial builds. Never place a key password in command arguments. Use the prompt or host secret source documented for the selected CLI.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
CLI Tutorial
This page walks through common CLI workflows. For a complete command reference, see the CLI Command Reference. For practical scripting examples, see CLI Examples.
The JACS CLI provides a command-line interface for managing agents, documents, and agreements.
Getting Help
# General help
jacs --help
# Command-specific help
jacs-compat agent --help
jacs-compat document --help
Commands Overview
| Command | Description |
|---|---|
jacs-compat init | Initialize JACS (create config and agent with keys) |
jacs-compat --version | Print version information |
jacs-compat config | Manage configuration |
jacs-compat agent | Manage agents |
jacs-compat document | Manage documents |
jacs-compat mcp | Start the built-in MCP server (stdio transport) |
Initialization
Quick Start
# Initialize everything in one step
jacs-compat init
This command:
- Creates a configuration file (
jacs.config.json) - Generates cryptographic keys
- Creates an initial agent document
MCP Server
The MCP server is built into the jacs-compat binary. No separate install step needed.
# Start the MCP server (stdio transport)
JACS_CONFIG="$PWD/jacs.config.json" jacs-compat mcp
Configuration Commands
Create Configuration
jacs-compat config create
Creates a new jacs.config.json file in the current directory with default settings.
Read Configuration
jacs-compat config read
Displays the current configuration, including values from both the config file and environment variables.
Agent Commands
Create Agent
jacs-compat agent create --create-keys true
# With a custom agent definition file
jacs-compat agent create --create-keys true -f my-agent.json
# Without creating new keys (use existing)
jacs-compat agent create --create-keys false -f my-agent.json
Options:
| Option | Short | Required | Description |
|---|---|---|---|
--create-keys | Yes | Whether to create new cryptographic keys | |
-f | No | Path to JSON file with agent definition |
Verify Agent
# Verify agent from config
jacs-compat agent verify
# Verify specific agent file
jacs-compat agent verify -a ./path/to/agent.json
# With DNS validation options
jacs-compat agent verify --require-dns
jacs-compat agent verify --require-strict-dns
jacs-compat agent verify --no-dns
jacs-compat agent verify --ignore-dns
Options:
| Option | Short | Description |
|---|---|---|
-a | --agent-file | Path to agent file (optional) |
--no-dns | Attempt DNS with embedded-fingerprint fallback | |
--require-dns | Require DNS validation (not strict) | |
--require-strict-dns | Require DNSSEC validation | |
--ignore-dns | Ignore DNS validation entirely |
DNS Commands
# Generate DNS TXT record commands for agent publishing
jacs-compat agent dns --domain example.com --agent-id [uuid]
# With different output formats
jacs-compat agent dns --domain example.com --encoding hex
jacs-compat agent dns --domain example.com --provider aws
# With custom TTL
jacs-compat agent dns --domain example.com --ttl 7200
Options:
| Option | Default | Description |
|---|---|---|
--domain | Domain for DNS record | |
--agent-id | Agent UUID (optional, uses config if not provided) | |
--ttl | 3600 | Time-to-live in seconds |
--encoding | base64 | Encoding format (base64, hex) |
--provider | plain | Output format (plain, aws, azure, cloudflare) |
Lookup Agent
# Look up another agent's public key from their domain
jacs-compat agent lookup agent.example.com
# With strict DNSSEC validation
jacs-compat agent lookup agent.example.com --strict
# Skip DNS lookup
jacs-compat agent lookup agent.example.com --no-dns
Document Commands
Create Document
# Create from a JSON file
jacs-compat document create -f ./document.json
# Create from a directory of files
jacs-compat document create -d ./documents/
# With custom schema
jacs-compat document create -f ./document.json -s ./custom-schema.json
# With file attachments
jacs-compat document create -f ./document.json --attach ./attachment.pdf
# Embed attachments in document
jacs-compat document create -f ./document.json --attach ./files/ --embed true
# Output to specific file
jacs-compat document create -f ./document.json -o ./output.json
# Print to stdout instead of saving
jacs-compat document create -f ./document.json --no-save
Options:
| Option | Short | Description |
|---|---|---|
-f | --filename | Path to input JSON file |
-d | --directory | Path to directory of JSON files |
-o | --output | Output filename |
-s | --schema | Path to custom JSON schema |
--attach | Path to file/directory for attachments | |
--embed | -e | Embed documents (true/false) |
--no-save | -n | Print to stdout instead of saving |
-v | --verbose | Enable verbose output |
-a | --agent-file | Path to agent file |
Update Document
# Update an existing document with new content
jacs-compat document update -f ./original.json -n ./updated.json
# With output file
jacs-compat document update -f ./original.json -n ./updated.json -o ./result.json
# With file attachments
jacs-compat document update -f ./original.json -n ./updated.json --attach ./new-file.pdf
Options:
| Option | Short | Required | Description |
|---|---|---|---|
-f | --filename | Yes | Path to original document |
-n | --new | Yes | Path to new version |
-o | --output | No | Output filename |
--attach | No | Path to file attachments | |
--embed | -e | No | Embed documents (true/false) |
Verify Document
# Verify a document
jacs-compat document verify -f ./document.json
# Verify all documents in a directory
jacs-compat document verify -d ./documents/
# With custom schema
jacs-compat document verify -f ./document.json -s ./schema.json
# Verbose output
jacs-compat document verify -f ./document.json -v
Options:
| Option | Short | Description |
|---|---|---|
-f | --filename | Path to document file |
-d | --directory | Path to directory of documents |
-s | --schema | Path to JSON schema for validation |
-v | --verbose | Enable verbose output |
-a | --agent-file | Path to agent file |
Extract Embedded Content
# Extract embedded content from a document
jacs-compat document extract -f ./document.json
# Extract from all documents in directory
jacs-compat document extract -d ./documents/
Agreement Commands
# Create an agreement requiring signatures from specified agents
jacs-compat document create-agreement -f ./document.json -i agent1-uuid,agent2-uuid
# Check agreement status
jacs-compat document check-agreement -f ./document.json
# Sign an agreement
jacs-compat document sign-agreement -f ./document.json
Create Agreement Options:
| Option | Short | Required | Description |
|---|---|---|---|
-f | --filename | Yes | Path to document |
-i | --agentids | Yes | Comma-separated list of agent UUIDs |
-o | --output | No | Output filename |
--no-save | -n | No | Print to stdout |
Environment Variables
The CLI respects the following environment variables:
# Use a specific configuration file
JACS_CONFIG=./custom-config.json jacs-compat agent verify
# Override settings
JACS_DATA_DIRECTORY=./data jacs-compat document create -f ./doc.json
JACS_KEY_DIRECTORY=./keys jacs-compat agent create --create-keys true
Common Workflows
Create and Sign a Document
# 1. Initialize (if not done)
jacs-compat init
# 2. Create document
jacs-compat document create -f ./my-document.json
# 3. Verify the signed document
jacs-compat document verify -f ./jacs_data/[document-id].json
Multi-Agent Agreement
# 1. Create agreement on a document
jacs-compat document create-agreement -f ./document.json -i agent1-id,agent2-id
# 2. First agent signs
jacs-compat document sign-agreement -f ./document.json
# 3. Second agent signs (using their config)
JACS_CONFIG=./agent2.config.json jacs-compat document sign-agreement -f ./document.json
# 4. Check agreement is complete
jacs-compat document check-agreement -f ./document.json
Verify Another Agent
# Look up agent by domain
jacs-compat agent lookup other-agent.example.com
# Verify with strict DNS
jacs-compat agent verify -a ./other-agent.json --require-strict-dns
Exit Codes
Success is 0; operation/verification errors generally use 1. Argument
parsing uses 2, as does permissive text/image inspection with no signature.
Check the error reference and the command's result
fields; a completed inspection is not an authorization decision.
Next Steps
- Creating an Agent - Detailed agent creation guide
- Working with Documents - Document operations in depth
- Agreements - Multi-agent agreements
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Creating an Agent
An agent is the fundamental identity in JACS: an entity with signing keys that can create, sign, and verify documents.
What is an Agent?
A JACS agent is:
- A stable UUID identity
- A holder of cryptographic signing keys
- A self-signed identity document
- An optional DNS-verifiable identity
Capabilities for A2A interoperability live in A2A Agent Cards. The JACS agent document stays focused on identity and signing metadata.
Creating Your First Agent
jacs-compat init
This creates:
- Configuration file
- Cryptographic key pair
- Initial agent document
Manual Creation
jacs-compat config create
jacs-compat agent create --create-keys true
Custom Agent Definition
Create an agent definition file:
{
"$schema": "https://hai.ai/schemas/agent/v1/agent.schema.json",
"jacsAgentType": "ai",
"jacsAgentDomain": "myagent.example.com",
"name": "Content Creation Agent",
"description": "AI agent specialized in content creation"
}
Then create the agent:
jacs-compat agent create --create-keys true -f my-agent.json
Agent Types
| Type | Description |
|---|---|
ai | Fully artificial intelligence |
human | Individual person |
human-org | Organization or group |
hybrid | Human-AI combination |
Cryptographic Keys
New agents hold two key roles:
| Role | Algorithm | Purpose |
|---|---|---|
native_root | pq2025 (default) or ring-Ed25519 | Signs native JACS documents. Creation honors explicit Ed25519 selection. Rotation preserves the current algorithm unless Ed25519 explicitly upgrades to pq2025; downgrade is rejected. |
ecosystem_signing | ES256 (ECDSA P-256) | Compatibility credential for ecosystems that require classical signatures (JWKS/DID/A2A). Never signs native documents. |
Use ed25519 as the user-facing creation label; configuration and signed
documents use the canonical ring-Ed25519 wire label.
The compatibility key is minted eagerly at creation (skip with
CreateAgentParams::builder().no_compat_key(true) or the CLI
--no-compat-key). Existing agents migrate explicitly:
let compat = agent.add_compat_key()?; // role, algorithm, RFC 7638 kid, paths
let info = agent.ecosystem_key_info()?; // typed KeyNotFound when absent
Roles are recorded in jacs_keys/jacs.keyring.json (metadata only); the
ES256 private key uses the same AES-256-GCM + Argon2id envelope and 0600
permissions as the native root.
Verifying Agents
jacs-compat agent verify
jacs-compat agent verify -a ./path/to/agent.json
jacs-compat agent verify --require-dns
jacs-compat agent verify --require-strict-dns
HTTP protocol helpers
SimpleAgent signs request context and complete response envelopes without
exposing its internal mutex:
#![allow(unused)] fn main() { use std::collections::HashMap; use jacs::{protocol::verify_signed_event_with_trusted_keys, simple::SimpleAgent}; let (agent, _) = SimpleAgent::ephemeral(Some("ed25519"))?; let body = br#"{"action":"approve"}"#; let authorization = agent.build_request_auth_header( "POST", "https://api.example.com/v1/jobs?mode=strict", body, "jobs-api", )?; let envelope = agent.sign_response(&serde_json::json!({"decision": "allow"}))?; let signer_id = envelope["jacsSignature"]["agentID"] .as_str() .ok_or("missing signer ID")?; let keys = HashMap::from([(signer_id.to_owned(), agent.get_public_key()?)]); let verified = verify_signed_event_with_trusted_keys(&envelope, &keys)?; assert!(authorization.starts_with("JACS v2.")); assert_eq!(verified.data["decision"], "allow"); }
The request header binds method, absolute URL including query, exact body bytes, audience, key, time, and nonce. The response signature covers both data and every envelope field. The verifier rejects unsigned/plain events, unknown signers, legacy payload-only envelopes, and any metadata or payload mutation. See Security for server replay-store requirements and migration from unbound headers.
The supplied key map is the trust boundary. A valid signature proves possession of that key, not a real-world identity unless the application established the signer-to-key association under its configured policy.
Agent Document Structure
{
"$schema": "https://hai.ai/schemas/agent/v1/agent.schema.json",
"jacsId": "550e8400-e29b-41d4-a716-446655440000",
"jacsVersion": "123e4567-e89b-12d3-a456-426614174000",
"jacsVersionDate": "2024-01-15T10:30:00Z",
"jacsOriginalVersion": "123e4567-e89b-12d3-a456-426614174000",
"jacsOriginalDate": "2024-01-15T10:30:00Z",
"jacsType": "agent",
"jacsLevel": "config",
"jacsAgentType": "ai",
"jacsAgentDomain": "myagent.example.com",
"name": "Content Creation Agent",
"description": "AI agent for content generation",
"jacsSha256": "hash-of-document",
"jacsSignature": {
"agentID": "550e8400-e29b-41d4-a716-446655440000",
"agentVersion": "123e4567-e89b-12d3-a456-426614174000",
"signature": "base64-encoded-signature",
"signingAlgorithm": "ring-Ed25519",
"publicKeyHash": "hash-of-public-key",
"date": "2024-01-15T10:30:00Z",
"fields": ["jacsId", "jacsVersion", "jacsAgentType", "jacsAgentDomain", "name"]
}
}
Best Practices
- Protect private keys.
- Prefer strong signing algorithms.
- Enable DNS verification for production agents.
- Keep A2A capabilities in the A2A Agent Card.
- Track agent document versions.
Next Steps
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
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-compat document create -f my-document.json
This adds JACS headers and a signature, saves the document, and prints the
exact path. Pass --json for a machine-readable document key/path result.
From a Directory
Process multiple documents at once:
jacs-compat document create -d ./documents/
With Custom Schema
Validate against a custom JSON schema:
jacs-compat document create -f my-document.json -s ./schemas/proposal.schema.json
Output Options
# Save to specific file
jacs-compat document create -f my-document.json -o ./output/signed-doc.json
# Print to stdout instead of saving
jacs-compat document create -f my-document.json --no-save
# Verbose output
jacs-compat 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
| Field | Description | Auto-generated |
|---|---|---|
$schema | JSON Schema reference | Yes |
jacsId | Permanent document UUID | Yes |
jacsVersion | Version UUID (changes on update) | Yes |
jacsVersionDate | When this version was created | Yes |
jacsOriginalVersion | First version UUID | Yes |
jacsOriginalDate | Original creation timestamp | Yes |
jacsType | Document type | Yes |
jacsLevel | Data level (raw, config, artifact, derived) | Yes |
Document Levels
The jacsLevel field indicates the document's purpose:
| Level | Description | Use Case |
|---|---|---|
raw | Original data, should not change | Source documents |
config | Configuration, meant to be updated | Agent definitions, settings |
artifact | Generated output | Reports, summaries |
derived | Computed from other documents | Analysis results |
File Attachments
Attach Files
# Attach a single file
jacs-compat document create -f my-document.json --attach ./report.pdf
# Attach a directory of files
jacs-compat document create -f my-document.json --attach ./attachments/
Embed vs. Reference
# Embed files directly in the document (larger document, self-contained)
jacs-compat document create -f my-document.json --attach ./files/ --embed true
# Reference files (smaller document, files stored separately)
jacs-compat 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-compat document verify -f ./signed-document.json
Verification checks:
- Hash integrity (document hasn't been modified)
- Signature validity (signature matches content)
- Schema compliance (if schema specified)
Verify with Schema
jacs-compat document verify -f ./document.json -s ./schema.json
Verify Directory
jacs-compat document verify -d ./documents/
Verbose Output
jacs-compat document verify -f ./document.json -v
Updating Documents
Updates create a new version while maintaining the same jacsId:
jacs-compat document update -f ./original.json -n ./modified.json
The update process:
- Reads the original document
- Applies changes from the modified file
- Increments
jacsVersion - Links to previous version via
jacsPreviousVersion - Re-signs with agent's key
Update with Attachments
jacs-compat document update -f ./original.json -n ./modified.json --attach ./new-file.pdf
Extracting Embedded Content
Extract attachments from a document:
jacs-compat document extract -f ./document-with-attachments.json
Extract from multiple documents:
jacs-compat document extract -d ./documents/
Document Payloads
Generic Documents
JACS signs arbitrary JSON payloads through the header schema:
{
"$schema": "https://hai.ai/schemas/header/v1/header.schema.json",
"jacsType": "document",
"content": {
"subject": "Status update",
"body": "The requested work is complete."
}
}
Custom Documents
Any JSON can be a JACS document. Create custom schemas:
{
"$schema": "https://example.com/schemas/invoice.schema.json",
"jacsType": "invoice",
"invoiceNumber": "INV-001",
"amount": 1000,
"currency": "USD"
}
Version History
JACS tracks document history through version chains:
Version 1 (jacsOriginalVersion)
↓
Version 2 (jacsPreviousVersion → Version 1)
↓
Version 3 (jacsPreviousVersion → Version 2)
↓
Current Version
Each version is a complete document that can be independently verified.
Working with Multiple Agents
Different Agent Signs Document
# Use a specific agent's keys
jacs-compat document create -f ./document.json -a ./other-agent.json
Verify Document from Unknown Agent
# Verify with strict DNS requirement
jacs-compat document verify -f ./document.json --require-strict-dns
Best Practices
Document Design
- Use appropriate levels: Match
jacsLevelto document purpose - Include context: Add descriptive fields for human readability
- Version control: Keep source files in git alongside JACS documents
Security
- Verify before trusting: Always verify signatures
- Check agent identity: Verify the signing agent
- Validate schemas: Use custom schemas for strict validation
Performance
- External attachments: Use
--embed falsefor large files - Batch processing: Use directory mode for multiple documents
- Selective verification: Verify only when needed
Common Workflows
Create and Share Document
# 1. Create document
jacs-compat document create -f ./proposal.json -o ./signed-proposal.json
# 2. Share the signed document
# The recipient can verify it:
jacs-compat document verify -f ./signed-proposal.json
Track Document Changes
# 1. Create initial version
jacs-compat document create -f ./contract-v1.json
# 2. Make changes and update
jacs-compat document update -f ./contract-v1.json -n ./contract-v2.json
# 3. Continue updating
jacs-compat document update -f ./contract-v2.json -n ./contract-v3.json
Process Multiple Documents
# Create all documents in a directory
jacs-compat document create -d ./input-docs/
# Verify all documents
jacs-compat document verify -d ./signed-docs/
Next Steps
- Agreements - Multi-agent consent
- Custom Schemas - Create your own schemas
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Creating and Using Agreements
Agreement v2 is the native compatibility agreement model. It is a standalone jacsType: "agreement" document that captures terms, parties, transcript references, signature policy, consent signatures, and version lineage.
Use Agreement v2 to inspect existing native agreement signatures and claimed
status. The report always keeps valid=false and policyAccepted=false: party
proofs do not authenticate the complete role, quorum, lineage or finalization
policy. New portable consent uses
Agreement v3.
Agreement v2 Model
The JACS header still owns document identity, versioning, authorship signatures, hashes, registration, files, and visibility. The agreement body owns consent:
title,description,terms,termsFormateffectiveFrom,expiresAtpartiessignaturePolicyagreementSignaturestranscriptallPreviousVersionslinkscontrollers,owners
jacsAgreementHash is the consent hash. It changes when terms, parties, signature policy, or effective/expiry dates change. Transcript appends do not change it; signers bind the transcript state with signedTranscriptHash when transcript entries exist.
Roles
| Role | Meaning |
|---|---|
signer | Consents to the terms and counts toward partyQuorum |
witness | Attests to process, time, or identity and counts toward witnessRequired |
notary | Provides notarial attestation; HAI uses this role and it counts toward notaryRequired |
observer | Listed participant that does not sign |
Minimal Input
{
"title": "Refund approval",
"description": "Agent A and Agent B agree on a bounded refund.",
"terms": "Agent B may issue a refund up to $25 for order 123.",
"termsFormat": "text/plain",
"status": "proposed",
"parties": [
{ "agentId": "00000000-0000-4000-8000-000000000001", "agentType": "ai", "role": "signer" },
{ "agentId": "00000000-0000-4000-8000-000000000002", "agentType": "human", "role": "signer" },
{ "agentId": "00000000-0000-4000-8000-000000000003", "agentType": "ai", "role": "notary" }
],
"signaturePolicy": {
"partyQuorum": "all",
"witnessRequired": 0,
"notaryRequired": 1,
"minimumStrength": "classical"
},
"controllers": [
"00000000-0000-4000-8000-000000000001",
"00000000-0000-4000-8000-000000000002",
"00000000-0000-4000-8000-000000000003"
]
}
Rust Core API
Rust core is the source of truth for Agreement v2 behavior. Binding surfaces call into the same helpers.
#![allow(unused)] fn main() { use jacs::agreements::v2::{ create, sign, verify, AgreementV2Role, CreateAgreementV2, }; let agreement = create(&agent, input)?; let signed = sign(&agent, &agreement.raw, AgreementV2Role::Signer)?; let report = verify(&agent, &signed.raw)?; assert!(report.mathematical_checks_valid); assert!(!report.valid); assert!(!report.policy_accepted); }
Use mutations for successor versions instead of editing JSON by hand. The helpers maintain jacsAgreementHash, jacsPreviousVersion, allPreviousVersions, status, and signature invalidation.
CLI
jacs-compat agreement-v2 create --input agreement-input.json > agreement.json
jacs-compat agreement-v2 sign --agreement agreement.json --role signer > signed.json
jacs-compat agreement-v2 verify --agreement signed.json
Branch handling is also in core and exposed through the CLI:
jacs-compat agreement-v2 detect-conflict --base base.json --left left.json --right right.json
jacs-compat agreement-v2 merge-transcript --base base.json --left left.json --right right.json
jacs-compat agreement-v2 resolve-conflict --base base.json --previous left.json --side-branch right.json --mutation resolution.json
Transcript-only branches can auto-merge. Terms conflicts require an explicit successor mutation.
Legacy Sidecar Agreements
Legacy v1; new work should use Agreement v2. See Migrating from v1 agreements.
The older jacsAgreement field on arbitrary signed documents remains available for simple countersignature workflows:
jacs-compat document create-agreement -f ./document.json -i agent1-uuid,agent2-uuid
jacs-compat document sign-agreement -f ./document-with-agreement.json
jacs-compat document check-agreement -f ./document-with-agreement.json
Use the legacy sidecar only when you need "these keys signed this existing payload (without authenticated consent policy)." Use Agreement v2 when you need standalone terms, lifecycle, transcript evidence, notary signatures, branch handling, or portable SDK parity.
Exporting as a Verifiable Credential
With the agreements feature, SimpleAgent::export_agreement_v2_as_vc(agreement_json)
projects an Agreement-v2 document into a VC 2.0 credential with an
ecdsa-jcs-2019 Data Integrity proof (ES256 compatibility key, Multikey
verification method). It requires the agreement-vc binding scope — content
scopes are never auto-issued — and never mutates the native agreement. See the
Agreement v2 Developer Guide for the full flow.
See Also
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
DNS-Based Agent Verification
Native JACS can publish a raw public-key fingerprint in DNS and retrieve a
candidate key over HTTPS. Discovery is opt-in and does not enroll a trusted
identity. Start with the key discovery and verification guide
for the current source's configuration, exact-version lookup and verifier coverage.
Portable jacs CLI/MCP verification continues to require explicit key material.
Generate publication material
With an existing native identity configured in jacs.config.json:
jacs-compat agent dns --domain agent.example.com --encoding hex
jacs-compat agent dns --domain agent.example.com --provider aws
jacs-compat agent dns --domain agent.example.com --provider azure
jacs-compat agent dns --domain agent.example.com --provider cloudflare
These commands print records or provider-specific publication material. Review
and publish it through your DNS operator; the CLI does not change DNS itself.
--ttl selects the record TTL. The default digest encoding is standard Base64;
--encoding hex selects hexadecimal. Both encode SHA-256 of the raw public key.
The record at _v1.agent.jacs.agent.example.com has this shape:
v=jacs; jacs_agent_id=<agent-uuid>; alg=SHA-256; enc=hex; jac_public_key_hash=<64-hex-raw-key-digest>
An optional uri=https://... identifies supported key-discovery JSON on the
publisher origin or an explicitly configured registry's exact-version endpoint. Without
it, the native resolver requests the domain's /.well-known/jacs-pubkey.json.
The Rust build_agent_dns_txt_with_uri helper validates this field. Publish
only public material. A served current-key document cannot substitute for the
exact historical version named by an older signature.
Configure verification
export JACS_KEY_RESOLUTION=local,dns
export JACS_ALLOW_DNS=true
export JACS_ALLOW_REMOTE_KEY_FETCH=true
export JACS_DNS_KEY_DOMAINS=agent.example.com
A native v2 envelope may also contain a signed agentDomain routing hint.
Until the complete signature verifies, treat that field as untrusted input.
The fetched key must match the DNS fingerprint, signed key reference,
algorithm and identity/version. A domain claim alone is not a trusted identity.
For the configured native agent's own validation:
JACS_ALLOW_DNS=true jacs-compat agent verify --require-strict-dns
Strict mode requires an actual Secure DNSSEC proof; an AD flag by itself does
not establish it. Configure zone signing and the registrar's DS delegation
first. Do not silently weaken strict policy to make a failed check pass.
The DNS trust guide explains the remaining boundaries.
Rotation and troubleshooting
Agent-specific lookup accepts multiple agents and overlapping fingerprints in the same TXT record set. It rejects conflicting URIs for one agent/fingerprint. Retain historical key endpoints where your application needs old documents, and enforce revocation independently. TTLs, caches and retention policy govern your overlap; there is no fixed universal wait period.
Use dig +dnssec _v1.agent.jacs.agent.example.com TXT to inspect records;
requesting DNSSEC records with dig alone does not prove validation succeeded.
On a mismatch, check the decoded raw key and selected agent/version rather than
comparing the encoded DNS digest to jacsSignature.publicKeyHash. Their
hash contracts differ.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
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.16.1"
Feature Flags
[dependencies]
jacs = { version = "=0.16.1", features = ["otlp-logs", "otlp-metrics", "otlp-tracing"] }
| Feature | Description |
|---|---|
sqlite | Lightweight sync SQLite backend (default) |
sqlx-sqlite | Async SQLite backend via sqlx (requires tokio) |
s3 | AWS S3 storage; opt-in because it adds cloud HTTP/XML dependencies |
otlp-logs | OTLP log export support |
otlp-metrics | OTLP metrics export support |
otlp-tracing | OTLP distributed tracing support |
agreements | Agreement lifecycle support |
a2a | Agent-to-Agent protocol support |
attestation | Attestation 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"identifiergetvalue()- Returns reference to the JSON valuegetschema()- Returns the document's schema URLsigning_agent()- Returns the ID of the signing agent
Creating an Agent
Minimal Agent
use jacs::{get_empty_agent, create_minimal_blank_agent}; fn main() -> Result<(), Box<dyn std::error::Error>> { // Create agent JSON let agent_json = create_minimal_blank_agent( "ai".to_string(), // agent type Some("My service".to_string()), // service description Some("Task completed".to_string()), // success description Some("Task failed".to_string()), // failure description )?; // Initialize and load the agent let mut agent = get_empty_agent(); agent.create_agent_and_load(&agent_json, true, None)?; // Save the agent agent.save()?; Ok(()) }
Loading by Configuration
use jacs::get_empty_agent; fn main() -> Result<(), Box<dyn std::error::Error>> { let mut agent = get_empty_agent(); // Load from config file agent.load_by_config("./jacs.config.json".to_string())?; // Or load by agent ID agent.load_by_id("agent-id:version-id".to_string())?; Ok(()) }
DNS Strict Mode
use jacs::load_agent_with_dns_strict; fn main() -> Result<(), Box<dyn std::error::Error>> { // Load agent with strict DNS verification let agent = load_agent_with_dns_strict( "path/to/agent.json".to_string(), true // strict mode )?; Ok(()) }
Working with Documents
Creating Documents
The DocumentTraits trait provides document operations:
use jacs::agent::document::DocumentTraits; use jacs::get_empty_agent; fn main() -> Result<(), Box<dyn std::error::Error>> { let mut agent = get_empty_agent(); agent.load_by_config("./jacs.config.json".to_string())?; // Create a document from JSON let json = r#"{"title": "My Document", "content": "Hello, World!"}"#; let doc = agent.create_document_and_load(json, None, None)?; println!("Document created: {}", doc.getkey()); Ok(()) }
Creating Documents with Attachments
#![allow(unused)] fn main() { use jacs::agent::document::DocumentTraits; // With file attachments let attachments = Some(vec!["./report.pdf".to_string()]); let embed = Some(true); // Embed files in document let doc = agent.create_document_and_load( json, attachments, embed )?; }
Loading Documents
#![allow(unused)] fn main() { use jacs::agent::document::DocumentTraits; // Load a document from JSON string let doc = agent.load_document(&document_json_string)?; // Get a stored document by key let doc = agent.get_document("doc-id:version-id")?; // List all document keys let keys = agent.get_document_keys(); }
Updating Documents
#![allow(unused)] fn main() { use jacs::agent::document::DocumentTraits; // Update creates a new version let updated_doc = agent.update_document( "doc-id:version-id", // original document key &modified_json_string, // new content None, // optional attachments None, // embed flag )?; }
Verifying Documents
#![allow(unused)] fn main() { use jacs::agent::document::DocumentTraits; // Verify document signature with agent's public key agent.verify_document_signature( "doc-id:version-id", None, // signature key (uses default) None, // fields to verify None, // public key (uses agent's) None, // key encoding type )?; // Verify using external public key agent.verify_external_document_signature("doc-id:version-id")?; }
Saving Documents
#![allow(unused)] fn main() { use jacs::agent::document::DocumentTraits; // Save document to filesystem agent.save_document( "doc-id:version-id", Some("output.json".to_string()), // output filename Some(true), // export embedded files None, // extract only )?; }
Signing and Verification
Signing Documents
The agent's signing_procedure method creates cryptographic signatures:
#![allow(unused)] fn main() { use serde_json::json; let document = json!({ "title": "Contract", "terms": "..." }); // Sign the document let signature = agent.signing_procedure( &document, None, // fields to sign (None = all) "jacsSignature" // placement key )?; }
Verification
#![allow(unused)] fn main() { // Verify self-signature (agent document) agent.verify_self_signature()?; // Verify hash integrity agent.verify_hash(&document)?; // Full signature verification agent.signature_verification_procedure( &document, None, // fields "jacsSignature", // signature key public_key, // public key bytes Some("ring-Ed25519".to_string()), // algorithm None, // original public key hash None, // signature override )?; }
Custom Schema Validation
#![allow(unused)] fn main() { // Load custom schemas agent.load_custom_schemas(&[ "./schemas/invoice.schema.json".to_string(), "https://example.com/schemas/contract.schema.json".to_string(), ])?; // Validate document against custom schema agent.validate_document_with_custom_schema( "./schemas/invoice.schema.json", &document_value, )?; }
Configuration
Loading Configuration
#![allow(unused)] fn main() { use jacs::config::{load_config, find_config, Config}; // Load from specific path let config = load_config("./jacs.config.json")?; // Find config in directory let config = find_config("./".to_string())?; // Create programmatically let config = Config::new( Some("false".to_string()), // use_security Some("./jacs_data".to_string()), // data_directory Some("./jacs_keys".to_string()), // key_directory Some("private_key.pem".to_string()), // private key filename Some("public_key.pem".to_string()), // public key filename Some("ring-Ed25519".to_string()), // key algorithm Some("password".to_string()), // private key password None, // agent ID and version Some("fs".to_string()), // storage type ); }
Accessing Configuration
#![allow(unused)] fn main() { // Get key algorithm let algorithm = config.get_key_algorithm()?; // Access config fields let data_dir = config.jacs_data_directory(); let key_dir = config.jacs_key_directory(); let storage_type = config.jacs_default_storage(); }
Observability
Initialize Default Observability
use jacs::init_default_observability; fn main() -> Result<(), Box<dyn std::error::Error>> { // Set up file-based logging init_default_observability()?; // Your application code... Ok(()) }
Custom Observability Configuration
use jacs::{ init_custom_observability, ObservabilityConfig, LogConfig, LogDestination, MetricsConfig, MetricsDestination, }; fn main() -> Result<(), Box<dyn std::error::Error>> { let config = ObservabilityConfig { logs: LogConfig { enabled: true, level: "debug".to_string(), destination: LogDestination::Otlp { endpoint: "http://localhost:4317".to_string(), headers: None, }, headers: None, }, metrics: MetricsConfig { enabled: true, destination: MetricsDestination::Prometheus { endpoint: "http://localhost:9090".to_string(), headers: None, }, export_interval_seconds: Some(30), headers: None, }, tracing: None, }; init_custom_observability(config)?; Ok(()) }
Storage Backends
JACS supports multiple storage backends:
#![allow(unused)] fn main() { use jacs::storage::MultiStorage; // Filesystem storage (default) let storage = MultiStorage::new("fs".to_string())?; // In-memory storage let storage = MultiStorage::new("memory".to_string())?; // AWS object storage (requires the native s3 feature) 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` resolve in the native document service.
Error Handling
Fallible APIs return Result; the exact error type depends on the API (including JacsError and storage-specific errors):
use jacs::get_empty_agent; fn main() { match get_empty_agent().load_by_config("./jacs.config.json".to_string()) { Ok(()) => println!("Agent loaded successfully"), Err(e) => eprintln!("Failed to load agent: {}", e), } }
Thread Safety
The Agent struct uses internal mutexes for thread-safe access to:
- Document schemas (
Arc<Mutex<HashMap<String, Validator>>>) - Storage operations
For concurrent usage:
#![allow(unused)] fn main() { use std::sync::{Arc, Mutex}; use jacs::get_empty_agent; let agent = Arc::new(Mutex::new(get_empty_agent())); // Clone Arc for threads let agent_clone = Arc::clone(&agent); std::thread::spawn(move || { let mut agent = agent_clone.lock().unwrap(); // Use agent... }); }
Complete Example
use jacs::get_empty_agent; use jacs::agent::document::DocumentTraits; use serde_json::json; fn main() -> Result<(), Box<dyn std::error::Error>> { // Initialize agent let mut agent = get_empty_agent(); agent.load_by_config("./jacs.config.json".to_string())?; // Create a document let doc_json = json!({ "title": "Project Proposal", "description": "Q1 development plan", "budget": 50000 }); let doc = agent.create_document_and_load( &doc_json.to_string(), None, None )?; println!("Created document: {}", doc.getkey()); // Verify the document agent.verify_document_signature(&doc.getkey(), None, None, None, None)?; println!("Document verified successfully"); // Save to file agent.save_document(&doc.getkey(), Some("proposal.json".to_string()), None, None)?; Ok(()) }
Next Steps
- Observability - Logging and metrics setup
- Storage Backends - Configure different storage
- Custom Schemas - Define custom document types
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
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.16.1", features = ["otlp-logs", "otlp-metrics", "otlp-tracing"] }
| Feature | Description |
|---|---|
otlp-logs | OTLP log export support |
otlp-metrics | OTLP metrics export support |
otlp-tracing | OTLP 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):
tracedebuginfowarnerror
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 operationsjacs_signature_verifications- Signature verification resultsjacs_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
- Check that logging is enabled:
logs.enabled: true - Verify log level includes your log statements
- For file logging, ensure the directory is writable
Metrics Not Exporting
- Verify
otlp-metricsfeature is enabled - Check endpoint connectivity
- Confirm metrics are enabled:
metrics.enabled: true
Traces Missing
- Verify
otlp-tracingfeature is enabled - Check sampling ratio isn't filtering all traces
- Ensure spans are properly instrumented
Next Steps
- Rust Library API - Use observability in your code
- Configuration Reference - Full config options
- Advanced Topics - Security considerations
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Node.js Installation
Source and published packages: Version 0.16.1 has a recorded release inventory. Version 0.16.0 is the first release whose source includes the native PQ DID/JWK and Agent Card behavior. See the identity integration guide.
@hai.ai/jacs is a native Node.js package. Browser applications use
@hai.ai/jacs-wasm.
Requirements
- Node.js 20 or newer.
- Published addons: macOS x64/arm64, Linux x64/arm64 for glibc and Linux x64 for musl. No native Windows addon is published; WSL needs a matching Linux build.
- Source builds with NAPI-RS CLI 3: Node
^20.17.0,^22.13.0or>=23.5.0, plus Rust 1.97 and a platform C toolchain.
Installation
npm install --save-exact @hai.ai/jacs@0.16.1
The package includes prebuilt bindings. Its optional install script downloads a
checksummed portable CLI; npm install --ignore-scripts skips that download.
The library remains usable without the CLI. See the
package README
for details.
Verify Installation
Save as test.mjs and run node test.mjs:
import * as jacs from '@hai.ai/jacs/simple';
await jacs.quickstart({ name: 'my-agent', domain: 'my-agent.example.com' });
const signed = await jacs.signMessage({ hello: 'world' });
const result = await jacs.verify(signed.raw);
if (!result.valid) throw new Error('signature verification failed');
console.log('Verified 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.
New identities default to pq2025 (ML-DSA-87), without classical fallback.
Ed25519 is an explicit compatibility choice.
Package Structure
TypeScript declarations are included. Choose the API for your application:
| Import | Use |
|---|---|
@hai.ai/jacs/simple | One process-wide signing session |
@hai.ai/jacs/client | Independent JacsClient instances |
@hai.ai/jacs | Lower-level JacsAgent and JacsSimpleAgent APIs |
@hai.ai/jacs/mcp | Signed MCP transports and tool registration |
@hai.ai/jacs/express, @hai.ai/jacs/koa | HTTP framework middleware |
See the simple API, instance usage and API reference for method signatures and async/sync variants.
Configuration and storage
Use quickstart() to create a persistent identity, or load(configPath) for
an existing one. See configuration for paths,
password sources and environment settings, and storage
for backends. Selecting a backend in config does not add a missing build feature;
AWS/S3 requires a custom source build with the Rust s3 feature enabled.
Troubleshooting
- Check
node --versionand match the addon's OS, architecture and libc.npm rebuildcannot supply an unpublished target. For a custom build, usemake build-jacsnpmfrom the repository root; see the native development guide. - Keep config and key directories private and writable by the signing user. For decryption errors, check the password source before replacing keys.
- Use
.mjsor"type": "module"for examples withimportand top-levelawait. Type declarations ship with the package; use a module-resolution mode compatible with your Node/TypeScript toolchain.
Next Steps
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Simplified API
Source and published packages: Version 0.16.1 has a recorded release inventory. Version 0.16.0 is the first release whose source includes the native PQ DID/JWK and Agent Card behavior. See the identity integration guide.
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.
Async and synchronous methods
The standard JacsAgent I/O methods and simple signing helpers are async.
Selected operations also provide Sync variants. Standalone verification,
trust helpers and several low-level JacsSimpleAgent methods are synchronous;
check the API signature for the method you call.
// 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 syncSigned = jacs.signMessageSync({ action: 'approve' });
Quick Start
Quickstart creates or loads an identity. Run this example as an ES module
(.mjs); name and domain are required:
import * as jacs from '@hai.ai/jacs/simple';
const info = await jacs.quickstart({
name: 'my-agent',
domain: 'my-agent.example.com',
});
console.log(info.configPath, info.publicKeyPath, info.privateKeyPath);
const signed = await jacs.signMessage({ action: 'approve', amount: 100 });
const result = await jacs.verify(signed.raw);
console.log(`Valid: ${result.valid}, Signer: ${result.signerId}`);
quickstart(name, domain, ...) creates a persistent agent with keys on disk (default algorithm: pq2025). If ./jacs.config.json already exists, it loads it; otherwise it creates a new agent. Returned AgentInfo includes config/key paths (config_path/public_key_path/private_key_path in Python, configPath/publicKeyPath/privateKeyPath in Node) plus key directory metadata so you can locate key material immediately. Rust/CLI quickstart requires an explicit password source (JACS_PRIVATE_KEY_PASSWORD, or JACS_PASSWORD_FILE in CLI). Python/Node can auto-generate a password if needed; set JACS_SAVE_PASSWORD_FILE=true if you want that generated password persisted to ./jacs_keys/.jacs_password.
Pass { algorithm: 'ed25519' } to override the default (pq2025).
To load an existing agent explicitly, use load() instead:
const agent = await jacs.load('./jacs.config.json');
const signed = await jacs.signMessage({ action: 'approve', amount: 100 });
When to Use the Simplified API
| Simplified API | JacsAgent Class |
|---|---|
| Quick prototyping | Multiple agents in one process |
| Scripts and CLI tools | Complex multi-document workflows |
| MCP tool implementations | Fine-grained control |
| Single-agent applications | Custom error handling |
API Reference
The following operations offer async and explicit synchronous variants:
| Function | Sync Variant | Description |
|---|---|---|
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 |
Synchronous helpers (some call the native binding; no suffix is needed):
| Function | Description |
|---|---|
verifyStandalone(doc, opts?) | Verify without loading an agent |
getPublicKey() | Get public key |
isLoaded() | Check if agent is loaded |
getDnsRecord(domain, ttl?) | Get DNS TXT record |
getWellKnownJson() | Get well-known JSON |
trustAgent(json) | Add agent to trust store |
listTrustedAgents() | List trusted agent IDs |
untrustAgent(id) | Remove from trust store |
isTrusted(id) | Check if agent is trusted |
getTrustedAgent(id) | Get trusted agent's JSON |
quickstart(options)
Create a persistent agent with keys on disk. If ./jacs.config.json already exists, loads it. Otherwise creates a new agent, saving keys and config to disk. If JACS_PRIVATE_KEY_PASSWORD is unset, Node quickstart auto-generates a secure password in-process (JACS_SAVE_PASSWORD_FILE=true persists it to ./jacs_keys/.jacs_password). Call this once before signMessage() or verify().
Parameters:
options(object, required fields):{ name: string, domain: string, description?: string, algorithm?: string, configPath?: string }. Default algorithm:"pq2025". User-facing choices:"ed25519","pq2025";"ring-Ed25519"is accepted as a legacy alias.
Returns: Promise<AgentInfo> (async) or AgentInfo (sync)
const info = await jacs.quickstart({
name: 'my-agent',
domain: 'my-agent.example.com',
});
console.log(`Agent ID: ${info.agentId}`);
console.log(`Config path: ${info.configPath}`);
console.log(`Public key: ${info.publicKeyPath}`);
console.log(`Private key: ${info.privateKeyPath}`);
// Explicit compatibility identity in a separate config directory
const compatibilityInfo = await jacs.quickstart({
name: 'my-agent',
domain: 'my-agent.example.com',
algorithm: 'ed25519',
configPath: './compatibility/jacs.config.json',
});
// Sync variant (blocks event loop)
const syncInfo = jacs.quickstartSync({
name: 'my-agent',
domain: 'my-agent.example.com',
algorithm: 'ed25519',
configPath: './compatibility/jacs.config.json',
});
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 syncInfo = 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 syncSigned = 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 signembed(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 JSONoptions(object, optional):{ keyResolution?, dataDirectory?, keyDirectory? }
Returns: VerificationResult (synchronous native verification)
const result = jacs.verifyStandalone(signedJson, { keyResolution: 'local', keyDirectory: './keys' });
console.log(result.valid, result.signerId);
HTTP protocol helpers (JacsSimpleAgent instances)
The module-level async API focuses on durable documents. Request-bound HTTP auth and signed event methods live on the synchronous instance class:
import { JacsSimpleAgent } from '@hai.ai/jacs';
const agent = JacsSimpleAgent.ephemeral('ed25519');
const body = '{"action":"approve"}';
const authorization = agent.buildRequestAuthHeader(
'POST',
'https://api.example.com/v1/jobs?mode=strict',
body,
'jobs-api',
);
const envelope = agent.signResponse(JSON.stringify({ decision: 'allow' }));
const signerId = JSON.parse(envelope).jacsSignature.agentID;
const keys = JSON.stringify({ [signerId]: agent.getPublicKeyPem() });
const verified = JSON.parse(agent.unwrapSignedEvent(envelope, keys));
if (!authorization.startsWith('JACS v2.') || !verified.verified) {
throw new Error('protocol verification failed');
}
buildRequestAuthHeader(method, url, body, audience)binds the normalized method, absolute URL including query, exact UTF-8 body bytes, audience, signer/key, issue time, and nonce. Stringify the final request body first and send it unchanged.signResponse(payloadJson)returns a2.0.0envelope whosejacs-response-v2signature covers payload and all metadata.unwrapSignedEvent(eventJson, serverKeysJson)returns JSON containing verifieddata,signerId,timestamp,algorithm, anddocumentId. Plain/unsigned input, legacy payload-only envelopes, unknown signers, and any mutation throw; it never successfully returnsverified: false.
The server key map is the application's configured trust decision. Signature verification proves possession of the corresponding key, not a real-world identity by itself.
Legacy no-argument buildAuthHeader() remains available for compatibility and
emits a WARN. Migrate both peers to v2; strict deployments can reject the
legacy method with JACS_REJECT_UNBOUND_AUTH_HEADER=true. See
Security.
signText(path, options?)
Append a YAML-bodied JACS signature block to the end of a markdown or text file. The file content (everything before the first signature block) is preserved byte-for-byte; multi-signer files are unordered. Returns a Promise containing the signing report.
Parameters: path (string): file to sign in place; options may contain
noBackup?: boolean (default false).
Returns: Promise<any> — the native signing report
Throws: MarkerCollision error if the file contains a column-zero -----BEGIN JACS SIGNATURE----- outside a valid block.
await jacs.signText('README.md');
verifyText(path, options?)
Verify all signature blocks in a text file. Permissive by default — missing signatures are a typed status, not an error.
Parameters:
path(string)options(object, optional):{ strict?: boolean, keyDir?: string }. Whenstrictis true, the Promise rejects with aMissingSignature-bearing error if the file is unsigned.keyDiris a directory of<signer_id>.public.pemfiles used in addition to the local trust store.
Returns: Promise<VerifyTextResult> — .status is 'signed' | 'missing_signature' | 'malformed'; .signers lists per-signer outcomes.
const result = await jacs.verifyText('README.md');
console.log(result.status); // 'signed'
// Strict — Promise rejects on missing signature
try {
await jacs.verifyText('UNSIGNED.md', { strict: true });
} catch (err) {
if (/MissingSignature/.test(err.message)) {
console.log('not signed');
} else {
throw err;
}
}
See the inline text signing guide for the full feature set.
signImage(path, out, options?)
Embed a JACS signature inside a PNG, JPEG, or WebP image. The signature lives in a metadata chunk (PNG iTXt / JPEG APP11 / WebP XMP).
Parameters:
path(string): input imageout(string): output pathoptions(object, optional):{ robust?: boolean, refuseOverwrite?: boolean }.robustopts in to LSB fallback for PNG/JPEG (off by default; WebP deferred).refuseOverwriterejects withAlreadySignedif the input already carries a signature.
Returns: Promise<any> — the native signing report
await jacs.signImage('photo.png', 'signed.png');
verifyImage(path, options?)
Verify the embedded signature on an image. Same permissive / strict model as verifyText.
Parameters: as for verifyText.
Returns: Promise<VerifyImageResult> — .status is 'valid' | 'invalid' | 'missing_signature'; .signerId, .signedAt, .algorithm populated when valid.
const v = await jacs.verifyImage('signed.png');
console.log(v.status); // 'valid'
See the media signing guide for the full feature set.
extractMediaSignature(path, options?)
Extract the embedded signature payload without verifying it.
Parameters:
path(string)options(object, optional):{ rawPayload?: boolean }— whentrue, returns the base64url wire form instead of decoded JSON.
Returns: Promise<string | null> — serialized JSON by default, base64url
when rawPayload: true, or null when no payload is found.
const payloadJson = await jacs.extractMediaSignature('signed.png');
if (payloadJson !== null) console.log(JSON.parse(payloadJson));
// Extraction does not verify the signature.
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(documentKey, newDocumentData, attachments?, embed?)
Update an existing document with new data and re-sign it.
Parameters:
documentKey(string): Stored document key,${jacsId}:${jacsVersion}newDocumentData(object|string): Updated document as JSON string or objectattachments(string[], optional): Array of file paths to attachembed(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 documentKey = `${doc.jacsId}:${doc.jacsVersion}`;
const updated = await jacs.updateDocument(documentKey, doc);
exportAgent()
Export the current agent document for sharing or inspection.
Returns: string -- The agent JSON document (synchronous, 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. Synchronous, no suffix needed.
Parameters: domain (string), ttl (number, optional, default 3600)
Returns: string
getWellKnownJson()
Return the well-known JSON object for the loaded agent. Synchronous, no suffix needed.
Returns: object
getPublicKey()
Get the loaded agent's public key in PEM format. Synchronous, 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
import * as jacs from '@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
Use the simplified API inside a real MCP SDK handler. The MCP chapter
shows ListToolsRequestSchema and CallToolRequestSchema, transport setup, and
peer policy. A tool can return signed.raw as MCP text content, but signing a
tool result alone does not authenticate the transport or authorize its caller.
Error Handling
import * as jacs from '@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);
}
// With a loaded agent, invalid documents return valid:false; setup failures may throw
const result = await jacs.verify('invalid json');
if (!result.valid) {
console.error('Verification errors:', result.errors);
}
See Also
- Basic Usage - JacsAgent class usage
- API Reference - Complete JacsAgent API
- MCP Integration - Model Context Protocol
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Basic Usage
Source and published packages: Version 0.16.1 has a recorded release inventory. Version 0.16.0 is the first release whose source includes the native PQ DID/JWK and Agent Card behavior. See the identity integration guide.
This chapter covers fundamental JACS operations in Node.js, including agent initialization, document creation, signing, and verification.
Async and synchronous methods
The standard JacsAgent I/O methods and simple signing helpers are async.
Selected operations also provide Sync variants. Standalone verification,
trust helpers and several low-level JacsSimpleAgent methods are synchronous;
check the API signature for the method you call.
// 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 syncDoc = 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 an identity with quickstart first. A configuration only points to existing keys and an agent document; writing placeholder IDs does not initialize an agent. A generated config contains fields like:
{
"$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": "pq2025",
"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
// Use a complete document previously created and stored by this agent.
const updatedData = JSON.parse(signedDocument);
const documentKey = `${updatedData.jacsId}:${updatedData.jacsVersion}`;
updatedData.title = 'Updated Proposal';
updatedData.budget = 75000;
const updatedDocument = await agent.updateDocument(
documentKey,
JSON.stringify(updatedData)
);
console.log('Updated document:', updatedDocument);
Update with New Attachments
const updatedDocument = await agent.updateDocument(
documentKey,
JSON.stringify(updatedData),
['./new-report.pdf'], // new attachments
true // embed files
);
Signing and Verification
Sign Arbitrary Data
// Sign any string data
const signature = await agent.signString('Important message to sign');
console.log('Signature:', signature);
Verify Arbitrary Data
// Verify a signature on string data
const isValid = await agent.verifyString(
'Important message to sign', // original data
signatureBase64, // base64 signature
publicKeyBuffer, // public key as Buffer
'ring-Ed25519' // algorithm
);
Working with Agreements
The methods below inspect the legacy jacsAgreement sidecar. Its participant,
role and quorum policy is not authenticated; complete, policy_authenticated
and policy_accepted remain false. Agreement v2 also provides inspection, not
application consent or execution permission. See Agreement v2.
Create an Agreement
// Add agreement requiring multiple agent signatures
const documentWithAgreement = await agent.createAgreement(
signedDocumentJson,
[JSON.parse(agent.exportAgent()).jacsId], // local signer for this example
'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 agentDocument = JSON.parse(agent.exportAgent());
agentDocument.jacsAgentType = 'hybrid';
const updatedAgentJson = await agent.updateAgent(JSON.stringify(agentDocument));
Sign External Agent
// Sign another agent's document with registration signature
const signedAgentJson = await agent.signAgent(
externalAgentJson,
publicKeyBuffer,
'ring-Ed25519'
);
Request/Response Signing
These legacy instance methods sign generic documents. They do not bind HTTP
request context or authorize a route. Use request-bound HTTP credentials
for that purpose. 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
'pq2025', // key algorithm
undefined, // private key password
undefined, // agent id and version
'fs' // default storage
);
console.log('Config:', configJson);
Error Handling
import { JacsAgent } from '@hai.ai/jacs';
const agent = new JacsAgent();
try {
await agent.load('./jacs.config.json');
} catch (error) {
console.error('Failed to load agent:', error.message);
}
try {
const doc = await agent.createDocument(JSON.stringify({ data: 'test' }));
console.log('Document created');
} catch (error) {
console.error('Failed to create document:', error.message);
}
try {
const isValid = await agent.verifyDocument(invalidJson);
} catch (error) {
console.error('Verification failed:', error.message);
}
Complete Example
import { JacsAgent, hashString } from '@hai.ai/jacs';
async function main() {
// Initialize agent
const agent = new JacsAgent();
await agent.load('./jacs.config.json');
// Create a generic proposal document
const proposal = {
title: 'Project Proposal',
description: 'Q1 development plan',
budget: 50000
};
const signedProposal = await agent.createDocument(JSON.stringify(proposal));
console.log('Document created');
// Verify the document
if (await agent.verifyDocument(signedProposal)) {
console.log('Document signature valid');
}
// Create a sidecar for signature inspection
const proposalWithAgreement = await agent.createAgreement(
signedProposal,
[JSON.parse(agent.exportAgent()).jacsId],
'Do you approve this proposal?'
);
// Sign the agreement
const signedAgreement = await agent.signAgreement(proposalWithAgreement);
console.log('Agreement signed');
// Check agreement status
const status = await agent.checkAgreement(signedAgreement);
console.log('Status:', status);
// Hash some data for reference
const proposalHash = hashString(signedProposal);
console.log('Document hash:', proposalHash);
}
main().catch(console.error);
Next Steps
- MCP Integration - Model Context Protocol support
- HTTP Server - Create HTTP APIs
- Express Middleware - Express.js integration
- API Reference - Complete API documentation
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
MCP Integration (Node.js)
Source and published packages: Version 0.16.1 has a recorded release inventory. Version 0.16.0 is the first release whose source includes the native PQ DID/JWK and Agent Card behavior. See the identity integration guide.
MCP is the primary integration for external model clients. For the portable
server, install jacs-cli and run jacs mcp; it starts in verify-only mode.
See the current MCP guide. The separate native compatibility
server is jacs-compat mcp from jacs-cli-compat.
Inside a Node application, choose between signed tool results on ordinary MCP and the JACS transport extension. Signing an output does not authenticate its recipient or authorize a tool call.
npm install @hai.ai/jacs @modelcontextprotocol/sdk
Signed tool results on ordinary MCP
This ES module (server.mjs) registers a read-only echo tool that signs its
result. The SDK uses request schemas, not string method names, for handlers:
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { JacsClient } from '@hai.ai/jacs/client';
const client = await JacsClient.quickstart({
name: 'echo-server', domain: 'mcp.local',
configPath: './server/jacs.config.json',
});
const server = new Server(
{ name: 'signed-echo', version: '1.0.0' },
{ capabilities: { tools: {} } },
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [{
name: 'echo', description: 'Return signed echo data',
inputSchema: {
type: 'object', properties: { message: { type: 'string' } },
required: ['message'], additionalProperties: false,
},
}],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name !== 'echo') throw new Error('Unknown tool');
const message = request.params.arguments?.message;
if (typeof message !== 'string') throw new Error('message must be a string');
const signed = await client.signMessage({ echo: message });
return { content: [{ type: 'text', text: signed.raw }] };
});
await server.connect(new StdioServerTransport());
An ordinary MCP client can call this tool. Its relying application must verify the returned document against its own trusted keys before using the contents. Keep stdio diagnostics on stderr; stdout carries protocol messages only.
Signed transport between JACS peers
Both peers must use the JACS transport wrapper. This is a local-only extension,
not a transparent way to connect arbitrary MCP clients. The implementation
rejects localOnly: false.
Provision each peer's identity and verification key through a trusted channel
before connecting. Set one exact expectedPeerAgentId, or an intentional
allowedPeerAgentIds allowlist, on each wrapper. A missing policy rejects
incoming authenticated messages. An optional expectedPeerPublicKeyHash
additionally pins the key for one exact peer.
Replace the final connection in the server above with:
import { createJACSTransportProxy } from '@hai.ai/jacs/mcp';
const expectedPeerAgentId = process.env.JACS_EXPECTED_CLIENT_ID;
if (!expectedPeerAgentId) throw new Error('Configure the trusted client agent ID');
const transport = createJACSTransportProxy(
new StdioServerTransport(), client, 'server', { expectedPeerAgentId },
);
await server.connect(transport);
The matching client wraps its StdioClientTransport the same way, using role
'client' and the trusted server ID. Use separate configuration directories
for server and client so quickstart does not reload the same identity. Load
verification keys into the local trust/key store before starting; these options
select an allowed identity, they do not fetch its key.
When only an existing config path is available:
import { createJACSTransportProxyAsync } from '@hai.ai/jacs/mcp';
const transport = await createJACSTransportProxyAsync(
baseTransport, './client/jacs.config.json', 'client',
{ expectedPeerAgentId: trustedServerId },
);
The synchronous factory takes a loaded JacsClient or JacsAgent, never a
config path. Signing, signature checks, peer checks and message dispatch fail
closed by default. Do not use allowUnsignedFallback or
dangerouslyAllowAnyValidSigner when authenticated peer identity is required.
Signatures provide integrity and key possession, not encryption. Application
permissions and replay policy remain separate from signature validity.
Registering compatibility tools
registerJacsTools(server, client) installs the Node compatibility tool list
and SDK handlers on a low-level Server. It replaces the list/call handlers,
so do not combine it with the echo handlers above. The helper includes signing,
agreement and trust-store mutations as well as inspection; it does not inherit
the portable server's verify-only profile. Expose only operations authorized
for your callers.
For a selected tool list, use getJacsMcpToolDefinitions() and
handleJacsMcpToolCall(client, name, args) behind your own SDK schema handlers
and allowlist. Some setup/registry entries are stubs; listing an operation
does not establish that it implements the full Rust server contract.
For a model already running inside Node, see LangChain.js or Vercel AI SDK. For protocol-wide concepts, see the native MCP guide.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
LangChain.js Integration
Source and published packages: Version 0.16.1 has a recorded release inventory. Version 0.16.0 is the first release whose source includes the native PQ DID/JWK and Agent Card behavior. See the identity integration guide.
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 13 tools:
jacs_signjacs_verifyjacs_create_agreementjacs_sign_agreementjacs_check_agreementjacs_verify_selfjacs_trust_agentjacs_trust_agent_with_keyjacs_list_trustedjacs_is_trustedjacs_share_public_keyjacs_share_agentjacs_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
Signed wrappers fail closed by default: signing failures withhold the output.
The explicit allowUnsignedOutput: true compatibility option permits unsigned
fallback; strict: true overrides it. Tool-operation error reporting is separate:
const jacsTools = createJacsTools({ client, strict: true });
Examples In This Repo
archive/native/jacsnpm/examples/langchain/basic-agent.tsarchive/native/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
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Vercel AI SDK
Source and published packages: Version 0.16.1 has a recorded release inventory. Version 0.16.0 is the first release whose source includes the native PQ DID/JWK and Agent Card behavior. See the identity integration guide.
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. Use the nonstreaming generateText path with a compatible V3 provider.
The current streaming path has an SDK shape limitation described below.
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; // Enforce signing; override unsigned-output opt-ins
maxBufferedStreamBytes?: number; // Default 1 MiB; maximum 16 MiB
metadata?: Record<string, unknown>; // Extra metadata in provenance records
}
Streaming limitation
Use generateText for this guide. The current stream adapter reads
text-delta chunks from a textDelta property and throws if it is absent.
The repository tests supply that mocked shape; they do not qualify actual
provider streams. The package declares broad optional ai >=4 and
@ai-sdk/provider >=1 peer ranges rather than a tested pinned V3 provider.
Do not assume a provider emitting a different delta shape is supported.
When compatible chunks are supplied, the adapter buffers the whole stream,
signs it, and only then releases chunks. The default bound is 1 MiB;
maxBufferedStreamBytes can raise it to at most 16 MiB. Overflow or signing
failure withholds output. Explicit unsigned/post-hoc options weaken that
boundary and are not a remedy for an incompatible SDK shape.
Tool Call Signing
When signToolResults is true (default), tool results in the prompt are signed:
import { generateText, tool, stepCountIs } from 'ai';
import { z } from 'zod';
const { text, providerMetadata } = await generateText({
model: withProvenance(openai('gpt-4'), { client }),
tools: {
getWeather: tool({
inputSchema: z.object({ city: z.string() }),
execute: async ({ city }) => `Weather in ${city}: sunny, 72F`,
}),
},
prompt: 'What is the weather in Paris?',
stopWhen: stepCountIs(2),
});
// Tool results are signed when fed back into a subsequent model step
console.log(providerMetadata?.jacs?.text?.signed);
console.log(providerMetadata?.jacs?.toolResults?.signed);
Provenance Record
Each signed output produces a ProvenanceRecord:
interface SignedProvenanceRecord {
signed: true;
documentId: string; // JACS document ID
agentId: string; // Signing agent's ID
timestamp: string; // ISO 8601 timestamp
signedDocument: string; // Full portable signed JACS document
metadata?: Record<string, unknown>;
}
ProvenanceRecord is a union of that signed shape and an unsigned failure
shape (signed: false, empty IDs/timestamp, and error). Unsigned records are
only returned through explicit compatibility options. Verify signedDocument
and compare its signed output and metadata; the other fields alone are not proof.
Access records from providerMetadata.jacs:
const { providerMetadata } = await generateText({ model, prompt: '...' });
const textProvenance = providerMetadata?.jacs?.text;
const toolProvenance = providerMetadata?.jacs?.toolResults;
Strict Mode
Signing fails closed by default: a failed signature withholds the output.
strict: true also overrides explicit unsigned-output compatibility opt-ins.
Do not enable unsigned or post-hoc streaming options when signed output is required:
const model = withProvenance(openai('gpt-4'), {
client,
strict: true, // Throws if signing fails
});
Next Steps
- Express Middleware - Sign HTTP API responses
- MCP Integration - Secure MCP transport
- API Reference - Complete API documentation
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Express Middleware
Source and published packages: Version 0.16.1 has a recorded release inventory. Version 0.16.0 is the first release whose source includes the native PQ DID/JWK and Agent Card behavior. See the identity integration guide.
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. Response signing intercepts res.json() when enabled.
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
interface JacsMiddlewareOptions {
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/invalid bodies through (default: false)
authReplay?: boolean | { // Signed-body freshness/deduplication (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): A supplied 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 });
}
});
Signed-body replay checks
This option adds freshness and deduplication to signed bodies. It does not authorize a route or bind the HTTP method, URL, body bytes, and audience. Use request-bound authorization and a configured signer policy for that boundary:
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.
Response signing fails closed by default: errors withhold the unsigned body.
The explicit allowUnsignedOutput: true compatibility option relaxes this;
strict: true overrides that option. Routes requiring a signed body should
also reject a missing payload, since body verification is not route authorization.
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' }));
// Routes requiring signed document bodies
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 and separate config directories per route group. Quickstart loads an existing config before creating an identity; changing name alone does not create another agent:
const adminClient = await JacsClient.quickstart({
name: 'admin-agent',
domain: 'admin.example.com',
configPath: './admin/jacs.config.json',
algorithm: 'pq2025',
});
const userClient = await JacsClient.quickstart({
name: 'user-agent',
domain: 'user.example.com',
configPath: './user/jacs.config.json',
algorithm: 'pq2025',
});
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 @hai.ai/jacs/http adapter calls removed root-module helpers. Migrate to the maintained adapter:
| Old | New |
|---|---|
import { JACSExpressMiddleware } from '@hai.ai/jacs/http' | import { jacsMiddleware } from '@hai.ai/jacs/express' |
JACSExpressMiddleware({ configPath: '...' }) | jacsMiddleware({ configPath: '...' }) |
| Per-request agent init | Shared client, lazy-loaded once |
res.send() monkey-patch | res.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 - Same pattern for Koa
- HTTP Server - Core HTTP integration concepts
- Vercel AI SDK - AI model provenance signing
- API Reference - Complete API documentation
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Koa Middleware
Source and published packages: Version 0.16.1 has a recorded release inventory. Version 0.16.0 is the first release whose source includes the native PQ DID/JWK and Agent Card behavior. See the identity integration guide.
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
interface JacsKoaMiddlewareOptions {
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/invalid bodies through (default: false)
authReplay?: boolean | { // Signed-body freshness/deduplication (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: A supplied 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 defined and not a Buffer, it is signed (including JSON scalars) before the response is sent.
Signed-body replay checks
This option adds freshness and deduplication to signed bodies. It does not authorize a route or bind the HTTP method, URL, body bytes, and audience. Use request-bound authorization and a configured signer policy for that boundary:
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.
Response signing fails closed by default: errors withhold the unsigned body.
The explicit allowUnsignedOutput: true compatibility option relaxes this;
strict: true overrides that option. Routes requiring a signed body should
also reject a missing payload, since body verification is not route authorization.
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
| Feature | Express | Koa |
|---|---|---|
| Import | jacsMiddleware from @hai.ai/jacs/express | jacsKoaMiddleware from @hai.ai/jacs/koa |
| Client access | req.jacsClient | ctx.state.jacsClient |
| Payload | req.jacsPayload | ctx.state.jacsPayload |
| Auto-sign target | res.json() interception | ctx.body after next() |
Next Steps
- Express Middleware - Express version
- Vercel AI SDK - AI model provenance signing
- API Reference - Complete API documentation
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
HTTP Signing and Verification
Source and published packages: Version 0.16.1 has a recorded release inventory. Version 0.16.0 is the first release whose source includes the native PQ DID/JWK and Agent Card behavior. See the identity integration guide.
HTTP integration has two separate jobs: verifying signed documents and
binding authorization to the actual request. The maintained
Express and Koa adapters verify document bodies and
optionally sign responses. They do not implement request-bound authorization.
The old @hai.ai/jacs/http adapter calls removed root-module helpers; migrate
to those framework adapters instead.
Request-bound credentials
Use the synchronous JacsSimpleAgent instance API. Run this client as an ES
module (.mjs), with an existing client configuration and a trusted server-key
map provisioned independently of the response:
import { readFile } from 'node:fs/promises';
import { JacsSimpleAgent } from '@hai.ai/jacs';
const agent = JacsSimpleAgent.load('./client/jacs.config.json');
const serverKeysJson = await readFile('./trusted-server-keys.json', 'utf8');
const url = 'https://api.example.com/v1/jobs?mode=strict';
const body = JSON.stringify({ action: 'inspect', jobId: '42' });
const authorization = agent.buildRequestAuthHeader('POST', url, body, 'jobs-api');
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: authorization },
body,
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const event = JSON.parse(agent.unwrapSignedEvent(await response.text(), serverKeysJson));
console.log(event.data);
The key map is a JSON object mapping expected server agent IDs to trusted PEM public keys. Do not construct it from an unverified response. The credential binds method, absolute URL including query, exact body bytes, audience, signer/key, issue time, and nonce. Send the final body unchanged.
The server must verify the same request context with a trusted key, enforce
its caller policy, and atomically consume the nonce before executing work.
Neither a generic signed body nor the legacy buildAuthHeader() provides
that binding. See request-bound HTTP authorization
for the server contract.
Signed response events
A server with an initialized JacsSimpleAgent creates the v2 envelope with:
const envelope = agent.signResponse(JSON.stringify({ result: 42 }));
unwrapSignedEvent(envelope, serverKeysJson) verifies the complete envelope,
including payload and metadata. It throws on unsigned input, legacy
payload-only envelopes, unknown signers, and mutations. Its result contains
verified, data, signerId, timestamp, algorithm, and documentId.
Verification alone does not consume an event for replay prevention or bind it
to an outstanding request; enforce those application constraints before use.
A signature proves possession of the selected key and integrity of the signed scope. Real-world identity, authorization, freshness, and permitted use come from the verifier's trust and application policies. Signatures do not encrypt the payload; use HTTPS for transport confidentiality.
Signed document bodies
For document exchange, use JacsClient.signMessage() and verify() or the
framework adapters. Configure sender trust and route authorization separately.
Express's authReplay and Koa's matching option add freshness and in-process
deduplication for signed bodies; they do not bind method, URL, or audience.
- Express middleware:
jacsMiddlewareandreq.jacsPayload - Koa middleware:
jacsKoaMiddlewareandctx.state.jacsPayload - MCP integration: signed messages between configured peers
- Simplified API: documents and transport helper reference
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
API Reference
Source and published packages: Version 0.16.1 has a recorded release inventory. Version 0.16.0 is the first release whose source includes the native PQ DID/JWK and Agent Card behavior. See the identity integration guide.
Native API reference for the @hai.ai/jacs Node.js package.
Installation
npm install @hai.ai/jacs
Async and synchronous methods
The standard JacsAgent I/O methods and simple signing helpers are async.
Selected operations also provide Sync variants. Standalone verification,
trust helpers and several low-level JacsSimpleAgent methods are synchronous;
check the API signature for the method you call.
// 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 syncDoc = 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 syncAgentJson = 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 contentcustomSchema(string, optional): Path to a custom JSON Schema for validationoutputFilename(string, optional): Filename to save the documentnoSave(boolean, optional): If true, don't save to storage (default: false)attachments(string, optional): Path to file attachmentsembed(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 on success.
Invalid documents can reject/throw; handle errors rather than relying on false.
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 stringsignatureField(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 stringattachments(Array, optional): Array of attachment file paths embed(boolean, optional): If true, embed attachments
Returns: Promise<string> (async) or string (sync)
Example:
const doc = JSON.parse(signedDoc);
const documentKey = `${doc.jacsId}:${doc.jacsVersion}`;
const updatedDoc = await agent.updateDocument(
documentKey,
JSON.stringify({ ...doc, title: 'Updated Title' })
);
agent.createAgreement(...) / agent.createAgreementSync(...)
Legacy sidecar API for mathematical signature inspection. The report keeps
complete, policy_authenticated, and policy_accepted false: participant and
role policy are not authenticated. Agreement v2 is also inspection-only; see
its guide before interpreting reports.
Add an agreement requiring multiple agent signatures to a document.
Parameters:
documentString(string): The document JSON stringagentIds(Array): Array of agent IDs required to sign question(string, optional): The agreement questioncontext(string, optional): Additional contextagreementFieldName(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 stringagreementFieldName(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 stringagreementFieldName(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 signartifactType(string): Type of artifact (e.g.,"artifact","result")parentSignaturesJson(string, optional): JSON string of parent signatures for chain of custody
Returns: Promise<string> (async) or string (sync) -- The signed, wrapped artifact as a JSON string
Example:
const signed = await agent.signArtifact(
JSON.stringify({ action: 'classify', input: 'hello' }),
'artifact'
);
agent.wrapA2aArtifact(...) / agent.wrapA2aArtifactSync(...)
Deprecated since 0.9.0. Use
signArtifact()/signArtifactSync()instead. This method will be removed in 1.0.0.Set
JACS_SHOW_DEPRECATIONS=1to 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 datasignatureBase64(string): The base64-encoded signaturepublicKey(Buffer): The public key as a BufferpublicKeyEncType(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 signpublicKey(Buffer): The public key as a BufferpublicKeyEncType(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(...fields)
Create a JACS configuration JSON string using the positional arguments below. This does not generate keys or create an agent. Never persist a private-key password in the generated configuration.
Parameters:
jacsUseSecurity(string, optional)jacsDataDirectory(string, optional)jacsKeyDirectory(string, optional)jacsAgentPrivateKeyFilename(string, optional)jacsAgentPublicKeyFilename(string, optional)jacsAgentKeyAlgorithm(string, optional)jacsPrivateKeyPassword(legacy slot; leave undefined)jacsAgentIdAndVersion(string, optional)jacsDefaultStorage(string, optional)
Returns: string -- Configuration as JSON string
HTTP integration
Use jacsMiddleware from @hai.ai/jacs/express or jacsKoaMiddleware from
@hai.ai/jacs/koa for signed document bodies. The old /http adapter calls
removed root helpers. Request-bound authorization and pinned event verification
use JacsSimpleAgent instance methods; see HTTP.
MCP module
createJACSTransportProxy(transport, loadedClientOrAgent, role, peerPolicy)
createJACSTransportProxyAsync(transport, configPath, role, peerPolicy)
The first returns JACSTransportProxy; the second returns a Promise after
loading the agent. role is 'client' or 'server'. Configure
peerPolicy.expectedPeerAgentId or peerPolicy.allowedPeerAgentIds on both
ends and provision trusted verification keys. The transport is local-only.
See MCP integration for actual SDK handlers, tool registration and
failure behavior.
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');
Root exports and legacy functions
Root exports include JacsAgent, JacsSimpleAgent, hashString, and the
positional createConfig helper. Historical module-level entry points now
have explicit names such as legacyLoad and legacyCreateDocument. The old
jacs.load() and jacs.createDocument() globals are not exported.
Use instance methods or the separate @hai.ai/jacs/simple module:
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' }));
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
- Installation - Getting started
- Basic Usage - Common usage patterns
- MCP Integration - Model Context Protocol
- HTTP Server - HTTP integration
- Express Middleware - Express.js patterns
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Browser WASM Package
@hai.ai/jacs-wasm is the browser WebAssembly package for JACS. Use it when a
web page needs to create a browser-local JACS agent, sign JSON, verify
signed JACS documents, or collect multi-party agreement signatures
without calling a backend service.
Published 0.16.1: See the release inventory for provenance and installed-browser checks. Later source APIs require a separate release record; use a local build for current checkout behavior.
For Node.js server applications, use the native @hai.ai/jacs package
instead. @hai.ai/jacs-wasm is browser-only: no filesystem access, no DNS
trust lookup, no MCP server, and no CLI process.
Install
npm install --save-exact @hai.ai/jacs-wasm@0.16.1
To build this checkout instead:
git clone https://github.com/HumanAssisted/JACS.git
cd JACS
make build-wasm
# From your consuming project:
npm install /absolute/path/to/JACS/jacs-wasm/pkg
Bundlers must be able to load an ES module plus the generated
jacs_wasm_bg.wasm asset. The source package is smoke-tested with Vite and
Chromium.
Quick Start
import { createEphemeral, initJacsWasm } from "@hai.ai/jacs-wasm";
await initJacsWasm();
const agent = await createEphemeral(); // pq2025; no classical fallback
const signed = agent.signMessageJson(JSON.stringify({ hello: "world" }));
const result = JSON.parse(agent.verifyJson(signed));
console.log(result.valid); // true
initJacsWasm() is idempotent. The async constructors also initialize
the module, so applications can call createEphemeral,
importEncryptedAgent, importEncryptedAgentFiles, or createVerifier
directly.
Public API
Constructors:
createEphemeral(algorithm?: "ed25519" | "pq2025" | "es256"): Promise<CoreAgentHandle>
importEncryptedAgent(materialJson: string, password: string): Promise<CoreAgentHandle>
importEncryptedAgentFiles(files: EncryptedAgentFiles, password: string): Promise<CoreAgentHandle>
createVerifier(publicKeyBase64: string, algorithm: "ed25519" | "pq2025" | "es256"): Promise<CoreAgentHandle>
importEncryptedAgentFiles is shaped for browser file pickers:
type EncryptedAgentFiles = {
configText: string;
agentText: string;
publicKeyBytes: Uint8Array;
encryptedPrivateKeyBytes: Uint8Array;
};
The wrapper infers the algorithm from the raw public key length: 32 bytes
for ed25519, 2592 bytes for pq2025, and 65-byte uncompressed SEC1 for
explicit ES256 compatibility. New identities default to pq2025.
Core handle methods:
agent.signMessageJson(dataJson: string): string
agent.verifyJson(signedJson: string): string
agent.verifyWithKeyJson(
signedJson: string,
publicKeyBase64: string,
algorithm: "ed25519" | "pq2025" | "es256",
): string
agent.exportAgent(): string
agent.getPublicKeyBase64(): string
agent.algorithm(): "ed25519" | "pq2025" | "es256"
agent.isUnlocked(): boolean
agent.clearSecrets(): void
agent.metrics(): string
clearSecrets() zeroes the in-memory private key. Signing then fails
with a Locked error, while verification continues to work.
Agreement signature inspection
Browser callers can collect and inspect v1 sidecar signatures. This local example constructs both handles and pins their actual IDs and keys before receiving an agreement. In an application, obtain the signer-key mapping through independently trusted enrollment, never from the submitted document.
import { createEphemeral, createVerifier, createAgreementJson } from "@hai.ai/jacs-wasm";
const alice = await createEphemeral();
const bob = await createEphemeral();
const aliceId = JSON.parse(alice.exportAgent()).jacsId;
const bobId = JSON.parse(bob.exportAgent()).jacsId;
const trustedSigners = [
{ agentId: aliceId, publicKeyBase64: alice.getPublicKeyBase64(), algorithm: alice.algorithm() },
{ agentId: bobId, publicKeyBase64: bob.getPublicKeyBase64(), algorithm: bob.algorithm() },
];
const verifier = await createVerifier(alice.getPublicKeyBase64(), alice.algorithm());
const agreement = createAgreementJson(
JSON.stringify({ action: "review", version: "1.2.3" }),
JSON.stringify([aliceId, bobId]),
"Have you reviewed this release?",
"Signature inspection example",
);
const signedByAlice = alice.signAgreementJson(agreement, "reviewer");
const signedByBoth = bob.signAgreementJson(signedByAlice, "reviewer");
const outcome = JSON.parse(
verifier.verifyAgreementJson(signedByBoth, JSON.stringify(trustedSigners)),
);
console.log(outcome);
The v1 signature does not authenticate participant, role or quorum policy;
role can be edited without invalidating its signature. The report's
mathematical outcomes are not consent, policy acceptance or permission to
execute. Missing signer keys are reported per signer. Keep application
acceptance policy separate from these inspection results.
Browser Persistence
localStore wraps window.localStorage and stores values under the
jacs: namespace.
import { localStore } from "@hai.ai/jacs-wasm";
localStore.saveEncryptedAgent("alice", encryptedMaterialJson);
localStore.saveDocument("doc-1", signed);
const restored = localStore.loadEncryptedAgent("alice");
const keys = localStore.listKeys();
localStore.remove("doc-1");
localStore.clearAll();
saveEncryptedAgent refuses obvious plaintext secrets, top-level or
nested password-like fields, and raw private-key-shaped payloads. The
browser package should persist encrypted agent material and signed
documents only.
Workers
Post-quantum key generation can block the main thread. The worker subpath moves key generation and signing to a Web Worker:
import {
createEphemeralInWorker,
} from "@hai.ai/jacs-wasm/worker";
const workerAgent = await createEphemeralInWorker("pq2025");
const signed = await workerAgent.signMessage(JSON.stringify({ hello: "world" }));
Observability
Browsers do not expose stdout or a Prometheus endpoint for package code.
@hai.ai/jacs-wasm exposes per-handle counters instead:
const snapshot = JSON.parse(agent.metrics());
// { signCount, verifyCount, lastSignDurationMs, lastVerifyDurationMs }
For local debugging, set globalThis.JACS_WASM_DEBUG = true before a
sign, verify, or clearSecrets call. Debug logging is off by default.
Security Notes
WebAssembly memory is visible to JavaScript running on the same page. That means any XSS, compromised dependency, malicious browser extension, or third-party script on the origin can potentially inspect key material while an agent is unlocked.
Use @hai.ai/jacs-wasm for browser-local signing when that risk is acceptable.
For hardware-backed or high-assurance signing, use a hardware key,
secure-enclave-backed WebCrypto flow, or server-side signer. Call
clearSecrets() as soon as the user is done signing.
Local Build and Chromium Smoke Test
Install the Rust WASM target once:
rustup target add wasm32-unknown-unknown
cargo install wasm-pack
Build the package and run the browser smoke fixture:
make build-wasm
cd jacs-wasm/examples/vite-smoke
npm install
npx playwright install chromium
npx playwright test --project=chromium
The Chromium smoke test builds the Vite app, serves the static output,
loads it in Chromium, creates an ephemeral @hai.ai/jacs-wasm agent, signs a
JSON payload, verifies it, and waits for SMOKE OK in the page output.
CI runs the same browser path in the WASM PR and release workflows.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Python Installation
Install the published native package:
python -m pip install jacs==0.16.1
Published 0.16.1 wheels support Python 3.11–3.14 on macOS arm64/x86_64, Linux arm64/x86_64 glibc, and Linux x86_64 musl; this checkout has the same Python requirement. A compatible wheel needs no Rust compiler. See the release inventory for platform evidence; later source changes are not automatically in the wheel.
Try signing
from jacs.client import JacsClient
client = JacsClient.ephemeral()
signed = client.sign_message({"message": "Hello from Python"})
assert client.verify(signed.raw).valid
For an identity that survives process restarts:
client = JacsClient.quickstart(name="my-agent", domain="my-agent.example.com")
signed = client.sign_message({"message": "Persistent identity"})
assert client.verify(signed.raw).valid
New agents default to pq2025 (ML-DSA-87). Quickstart loads an existing
configuration or creates encrypted keys. Preserve the password through a host
secret source or keychain; an automatically generated password may otherwise
survive only in-process. See
native password bootstrap.
Optional integrations
python -m pip install "jacs[mcp]"
python -m pip install "jacs[fastapi]"
python -m pip install "jacs[langchain,langgraph]"
python -m pip install "jacs[anthropic]"
python -m pip install "jacs[a2a-server]"
Install only the extras you need. The a2a extra adds no dependencies;
a2a-server supplies FastAPI and Uvicorn.
CLI and source builds
The Python jacs command downloads and caches the matching CLI release on
first use and verifies its SHA-256. Importing the library downloads nothing.
Unsupported platforms fail; no musl CLI archive is published. Alternatively
install the portable CLI with cargo install jacs-cli and start its
verify-only MCP server with jacs mcp. If the Python shim shadows Cargo's
binary, invoke the binary in Cargo's bin directory explicitly.
Native configuration commands belong to the separate compatibility CLI:
cargo install jacs-cli-compat installs jacs-compat. Prefer the Python
quickstart above to create a Python agent.
For source builds, follow the native development guide from the complete repository. Native bindings use a separate Cargo workspace.
Continue with Basic Usage, MCP, or Framework Adapters.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Simplified API
jacs.simple provides one process-wide session for signing and verification.
Quick Start
Create or load a persistent agent:
import jacs.simple as jacs
info = jacs.quickstart(name="my-agent", domain="my-agent.example.com")
print(info.config_path, info.public_key_path, info.private_key_path)
signed = jacs.sign_message({"action": "approve", "amount": 100})
result = jacs.verify(signed.raw)
print(f"Valid: {result.valid}, Signer: {result.signer_id}")
quickstart(name, domain, ...) creates a persistent agent with keys on disk (default algorithm: pq2025). If ./jacs.config.json already exists, it loads it; otherwise it creates a new agent. Returned AgentInfo includes config/key paths (config_path/public_key_path/private_key_path in Python, configPath/publicKeyPath/privateKeyPath in Node) plus key directory metadata so you can locate key material immediately. Rust/CLI quickstart requires an explicit password source (JACS_PRIVATE_KEY_PASSWORD, or JACS_PASSWORD_FILE in CLI). Python/Node can auto-generate a password if needed; set JACS_SAVE_PASSWORD_FILE=true if you want that generated password persisted to ./jacs_keys/.jacs_password.
Pass algorithm="ed25519" to override the default (pq2025).
To load an existing agent explicitly, use load() instead:
agent = jacs.load("./jacs.config.json")
signed = jacs.sign_message({"action": "approve", "amount": 100})
When to Use the Simplified API
Use jacs.simple for one agent per process, JacsClient for independent agents,
and lower-level JacsAgent when you need direct document/config control.
API Reference
quickstart(name, domain, description=None, algorithm=None, config_path=None)
Create or load a persistent agent. See the Quick Start above for password handling; call this before signing or verifying.
Parameters:
name(str, required): Agent name used for first-time creation.domain(str, required): Agent domain used for DNS/public-key verification workflows.description(str, optional): Human-readable description.algorithm(str, optional): Signing algorithm. Default:"pq2025". User-facing choices:"ed25519","pq2025";"ring-Ed25519"is accepted as a legacy alias.config_path(str, optional): Config path (default:"./jacs.config.json").
Returns: AgentInfo dataclass
info = jacs.quickstart(name="my-agent", domain="my-agent.example.com")
print(f"Agent ID: {info.agent_id}")
print(f"Config path: {info.config_path}")
print(f"Public key: {info.public_key_path}")
print(f"Private key: {info.private_key_path}")
# Or with a specific algorithm
info = jacs.quickstart(
name="my-agent",
domain="my-agent.example.com",
algorithm="ed25519",
)
load(config_path=None, strict=None)
Load a persistent agent from a configuration file. Use this instead of quickstart(name=..., domain=..., ...) when you want to load a specific config file explicitly.
Parameters:
config_path(str, optional): Path to jacs.config.json (default: "./jacs.config.json")strict(bool, optional): If True, verification failures raise; if None, usesJACS_STRICT_MODEenv 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 signembed(bool, optional): If True, embed file content in the document (default: False)
Returns: SignedDocument
Raises: JacsError if file not found or no agent loaded
# Reference only (stores hash)
signed = jacs.sign_file("contract.pdf", embed=False)
# Embed content (creates portable document)
embedded = jacs.sign_file("contract.pdf", embed=True)
verify(signed_document)
Verify a signed document and extract its content.
Parameters:
signed_document(str|dict|SignedDocument): The signed document as JSON string, dict, or SignedDocument
Returns: VerificationResult
Raises: AgentNotLoadedError if no agent is loaded
result = jacs.verify(signed_json)
if result.valid:
print(f"Signed by: {result.signer_id}")
print(f"Timestamp: {result.timestamp}")
else:
print(f"Invalid: {', '.join(result.errors)}")
verify_standalone(document, key_resolution="local", data_directory=None, key_directory=None)
Verify a signed document without loading an agent. Use when you only need to verify (e.g. a lightweight API).
Parameters: document (str|dict), key_resolution (str), data_directory (str, optional), key_directory (str, optional)
Returns: VerificationResult
HTTP protocol helpers (SimpleAgent instances)
The module-level API focuses on durable documents. Request-bound HTTP auth and
signed event helpers live on the instance-based native SimpleAgent:
import json
from jacs import SimpleAgent
agent, _ = SimpleAgent.ephemeral() # default pq2025
body = '{"action":"approve"}'
authorization = agent.build_request_auth_header(
"POST",
"https://api.example.com/v1/jobs?mode=strict",
body,
"jobs-api",
)
envelope = agent.sign_response(json.dumps({"decision": "allow"}))
signer_id = json.loads(envelope)["jacsSignature"]["agentID"]
keys = json.dumps({signer_id: agent.get_public_key_pem()})
verified = json.loads(agent.unwrap_signed_event(envelope, keys))
assert authorization.startswith("JACS v2.")
assert verified["verified"] is True
assert verified["data"]["decision"] == "allow"
build_request_auth_header(method, url, body="", audience="hai.ai")binds the normalized method, absolute URL including query, exact UTF-8 body bytes, audience, signer/key, issue time, and nonce. Serialize the final request body before calling it and transmit that string unchanged.sign_response(payload_json)returns a2.0.0envelope whosejacs-response-v2signature covers payload and all metadata.unwrap_signed_event(event_json, server_keys_json)returns JSON containing verifieddata,signerId,timestamp,algorithm, anddocumentId. Plain/unsigned input, legacy payload-only envelopes, unknown signers, and any mutation raise; it never successfully returnsverified: false.
server_keys_json is a map from signer ID to a pinned base64 key or PEM string.
It represents the application's configured trust decision. Cryptographic
success proves possession of that key, not a real-world identity by itself.
Legacy no-argument build_auth_header() remains available for compatibility
and emits a WARN. Migrate both peers to v2; strict deployments can reject the
legacy method with JACS_REJECT_UNBOUND_AUTH_HEADER=true. See
Security.
sign_text(path)
Append a YAML-bodied JACS signature block to the end of a markdown or text file. The file content (everything before the first signature block) is preserved byte-for-byte; multi-signer files are unordered.
Parameters: path (str): file to sign in place
Returns: SignTextResult
Raises: RuntimeError if the file contains a column-zero -----BEGIN JACS SIGNATURE----- outside a valid block — see the inline-text-signing guide for documented workarounds.
jacs.sign_text("README.md")
verify_text(path, *, strict=False, key_dir=None)
Verify all signature blocks in a text file. Permissive by default — missing signatures are a typed status, not an error.
Parameters:
path(str)strict(bool, keyword-only): whenTrue, raisesMissingSignatureErrorif the file is unsigned. DefaultFalse.key_dir(str, keyword-only): directory of<signer_id>.public.pemfiles used in addition to the local trust store.
Returns: VerifyTextResult — .status is 'signed' | 'missing_signature' | 'malformed'; .signatures lists per-signer outcomes.
result = jacs.verify_text("README.md")
print(result.status) # 'signed'
# Strict — missing signature raises
try:
jacs.verify_text("UNSIGNED.md", strict=True)
except MissingSignatureError:
print("not signed")
See the inline text signing guide for the full feature set.
sign_image(input_path, output_path, *, robust=False, refuse_overwrite=False)
Embed a JACS signature inside a PNG, JPEG, or WebP image. The signature lives in a metadata chunk (PNG iTXt / JPEG APP11 / WebP XMP).
Parameters:
input_path(str): input imageoutput_path(str): required output path.robust(bool, keyword-only): opt-in LSB embedding fallback for PNG/JPEG (off by default; WebP deferred).refuse_overwrite(bool, keyword-only): raise an error if the input already carries a signature.
Returns: SignImageResult
jacs.sign_image("photo.png", "signed.png")
verify_image(path, *, strict=False, key_dir=None)
Verify the embedded signature on an image. Same permissive / strict model as verify_text.
Parameters: as for verify_text.
Returns: VerifyImageResult with .status, optional .signer_id and
.format. Status values include valid, invalid_signature, hash_mismatch,
missing_signature, key_not_found, unsupported_format and malformed.
v = jacs.verify_image("signed.png")
print(v.status) # 'valid'
See the media signing guide for the full feature set.
extract_media_signature(path, *, raw_payload=False)
Extract the embedded signature payload without verifying it.
Parameters:
path(str)raw_payload(bool, keyword-only): emit the base64url wire form instead of decoded JSON. DefaultFalse.
Returns: Optional[str]: a JSON string, or the base64url wire form when
raw_payload=True; None if no signature is present.
import json
payload = jacs.extract_media_signature("signed.png")
if payload is not None:
print(json.loads(payload)) # Extraction alone does not verify the payload.
verify_by_id(document_id)
Verify a document by its storage ID (uuid:version) without passing the full JSON. Requires the document to be stored locally (e.g. in the agent's data directory).
Parameters: document_id (str): Document ID in format "uuid:version"
Returns: VerificationResult
reencrypt_key(old_password, new_password)
Re-encrypt the loaded agent's private key with a new password.
Parameters: old_password (str), new_password (str)
Raises: AgentNotLoadedError if no agent loaded; JacsError if password is wrong
audit(config_path=None, recent_n=None)
Run a read-only security audit and health checks. Returns a dict with risks, health_checks, summary, and overall_status. Does not require a loaded agent; does not modify state.
See Security Model — Security Audit for full details and options.
result = jacs.audit()
print(f"Risks: {len(result['risks'])}, Status: {result['overall_status']}")
update_agent(new_agent_data)
Update the agent document with new data and re-sign it.
This function expects a complete agent document (not partial updates). Use export_agent() to get the current document, modify it, then pass it here.
Parameters:
new_agent_data(dict|str): Complete agent document as JSON string or dict
Returns: str - The updated and re-signed agent document
Raises: AgentNotLoadedError if no agent loaded, JacsError if validation fails
import json
# Get current agent document
agent_doc = json.loads(jacs.export_agent())
# Modify fields
agent_doc["jacsAgentType"] = "hybrid"
agent_doc["description"] = "Hybrid signing agent"
# Update (creates new version, re-signs, re-hashes)
updated = jacs.update_agent(agent_doc)
new_doc = json.loads(updated)
print(f"New version: {new_doc['jacsVersion']}")
print(f"Previous: {new_doc['jacsPreviousVersion']}")
Valid jacsAgentType values: "human", "human-org", "hybrid", "ai"
update_document(document_id, new_document_data, attachments=None, embed=False)
Update an existing document with new data and re-sign it.
The original must be available in the agent's document store. no_save controls
file export; it does not prevent in-memory storage.
Parameters:
document_id(str): FulljacsId:jacsVersionlookup keynew_document_data(dict|str): Complete updated document, preserving its headersattachments(list, optional): List of file paths to attachembed(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
import jacs as native
# Use a native stored document and its full id:version lookup key.
agent = native.JacsAgent()
agent.load("./jacs.config.json")
original = json.loads(agent.create_document(
json.dumps({"content": {"status": "pending"}}), None, None, False, None, None
))
document_key = f"{original['jacsId']}:{original['jacsVersion']}"
original["content"]["status"] = "approved"
updated = json.loads(agent.update_document(document_key, json.dumps(original), None, None))
print(f"New version: {updated['jacsVersion']}")
print(f"Previous: {updated['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=jacs; ...".
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
These result types provide attributes for common operations:
| Type | Key attributes |
|---|---|
AgentInfo | agent_id, version, config_path, public_key_path |
SignedDocument | document_id, version, signer_id, payload, raw_json; raw aliases raw_json |
VerificationResult | valid, signer_id, timestamp, errors, attachments |
Attachment | filename, mime_type, content_hash, optional content, size_bytes |
See the type definitions for constructors, defaults and aliases, and error handling for exceptions. Signer timestamps are claims, not an independent time authority.
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:
print(f"Payment verified from: {verification.signer_id}")
print(json.loads(signed.raw)) # Inspect the document only after verification.
else:
print(f"Verification failed: {', '.join(verification.errors)}")
# Sign a file
contract_signed = jacs.sign_file("./contract.pdf", embed=True)
print(f"Contract signed: {contract_signed.document_id}")
# Update agent metadata
agent_doc = json.loads(jacs.export_agent())
agent_doc["jacsAgentType"] = "ai"
agent_doc["description"] = "AI signing agent"
updated_agent = jacs.update_agent(agent_doc)
print("Agent metadata updated")
# Share public key
public_key = jacs.get_public_key()
print("Share this public key for verification:")
print(public_key)
MCP Integration
The simplified API works well with FastMCP tool implementations:
from fastmcp import FastMCP
import jacs.simple as jacs
mcp = FastMCP("My Server")
# Load agent once at startup
jacs.load("./jacs.config.json")
@mcp.tool()
def approve_request(request_id: str) -> dict:
"""Approve a request with cryptographic signature."""
signed = jacs.sign_message({
"action": "approve",
"request_id": request_id,
"approved_by": jacs.get_agent_info().agent_id
})
return {"signed_approval": signed.raw}
@mcp.tool()
def verify_approval(signed_json: str) -> dict:
"""Verify a signed approval."""
result = jacs.verify(signed_json)
return {
"valid": result.valid,
"signer": result.signer_id,
"errors": result.errors
}
Error Handling
import jacs.simple as jacs
from jacs.types import JacsError, AgentNotLoadedError
try:
jacs.load("./missing-config.json")
except JacsError as e:
print(f"Config not found: {e}")
try:
# Will fail if no agent loaded
jacs.sign_message({"data": "test"})
except AgentNotLoadedError as e:
print(f"No agent: {e}")
try:
jacs.sign_file("/nonexistent/file.pdf")
except JacsError as e:
print(f"File not found: {e}")
# With an agent loaded and strict mode disabled, inspect result.valid.
# An unloaded agent raises AgentNotLoadedError; strict mode raises VerificationError.
result = jacs.verify("invalid json")
if not result.valid:
print(f"Verification errors: {result.errors}")
See Also
- Basic Usage - Independent JacsClient instances
- API Reference - Client APIs and native argument requirements
- MCP Integration - Model Context Protocol
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Python Basic Usage
Use JacsClient for independent agents, or jacs.simple
for one process-wide session.
Sign and verify
from jacs.client import JacsClient
client = JacsClient.quickstart(name="my-agent", domain="my-agent.example.com")
signed = client.sign_message({"event": "tool-result", "answer": 42})
result = client.verify(signed.raw)
if not result.valid:
raise ValueError(result.errors)
print(result.signer_id, signed.payload)
Quickstart loads an existing config or creates encrypted keys with pq2025.
See password bootstrap
for preserving access across process restarts. To load explicitly:
client = JacsClient(config_path="./jacs.config.json", strict=True)
With strict=True, verification failures raise VerificationError; with
strict mode disabled, inspect result.valid and result.errors.
Configuration and signing errors still raise.
Verification authenticates bytes under the resolved key. Separately require the expected signer, trusted identity binding, allowed action and freshness before executing an operation.
Save and exchange signed JSON
from pathlib import Path
Path("signed.json").write_text(signed.raw, encoding="utf-8")
received = Path("signed.json").read_text(encoding="utf-8")
result = client.verify(received)
if not result.valid:
raise ValueError(result.errors)
Send the complete signed.raw, including signature and metadata. To verify
another agent, configure its public key under your trust policy. A key embedded
in a submission or fetched from a URL is not an identity endorsement.
Files and public identity
signed_file = client.sign_file("report.pdf", embed=True)
assert client.verify(signed_file.raw).valid
agent_json = client.export_agent()
public_key_pem = client.get_public_key()
The file must already exist. Embedding carries its bytes; embed=False
records a reference/hash. Apply an external-file verification policy when
receiving referenced files. Share public identity material through an
authenticated channel.
Independent agents
alice = JacsClient.quickstart(
name="alice", domain="alice.example.com", config_path="./alice/jacs.config.json"
)
bob = JacsClient.quickstart(
name="bob", domain="bob.example.com", config_path="./bob/jacs.config.json"
)
For short-lived tests, JacsClient.ephemeral() keeps its identity in memory.
Do not assume separate ephemeral agents can resolve one another's public keys.
Agreements and integrations
Legacy create_agreement(), sign_agreement() and check_agreement()
inspect sidecar evidence; inspection cannot establish application consent.
Agreement v2 is available on jacs.simple and native SimpleAgent; its
valid and policyAccepted remain false even when
mathematicalChecksValid is true.
See MCP, Framework Adapters, and the API reference for direct native calls.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
MCP Integration (Python)
For the primary ready-made JACS tool server, install jacs-cli and run
jacs mcp (ordinary stdio MCP, verify-only by default). The native Python
integrations below serve different purposes. Install their optional dependency:
python -m pip install "jacs[mcp]==0.16.1"
When building this checkout, use its mcp extra and source Python requirements.
Python exposes two different MCP stories:
- Secure a local FastMCP transport with
jacs.mcp - 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.
The signed local HTTP transport requires JACS wrappers at both endpoints and trusted peer keys. A stock MCP client cannot consume its signed carriers directly. Registering tools on ordinary FastMCP instead preserves ordinary MCP transport; it does not add caller authentication or permission to sign.
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(), andregister_trust_tools()
Important Constraints
JACSMCPClient,JACSMCPServer, andjacs_call()enforce loopback-only URLs- Unsigned fallback is disabled by default
- Clients must pin the expected server agent ID; servers must allowlist client agent IDs
stdiois rejected because it bypasses the HTTP signing middleware- JSON POST and response bodies default to a 2 MiB hard limit (
JACS_MCP_MAX_MESSAGE_BYTES) strict=Trueis about config loading and failure behavior, not an opt-in to security
A valid signature proves possession of a key, not that the signer is the intended
peer. The peer-ID policy below is what binds that proof to your deployment. An
optional expected_peer_public_key_hash also pins one exact server key.
On the server, the agent-ID allowlist is combined with JACS trust/key
resolution; operators must maintain rotation and revocation policy for those
allowed identities.
1. Secure A FastMCP Server
The shortest path is the factory:
from jacs.mcp import create_jacs_mcp_server
mcp = create_jacs_mcp_server(
"My Server",
"./jacs.config.json",
allowed_peer_agent_ids=["CLIENT_AGENT_ID"],
)
@mcp.tool()
def hello(name: str) -> str:
return f"Hello, {name}!"
mcp.run(transport="sse")
If you already have a FastMCP instance:
from fastmcp import FastMCP
from jacs.mcp import JACSMCPServer
mcp = JACSMCPServer(
FastMCP("Secure Server"),
"./jacs.config.json",
allowed_peer_agent_ids=["CLIENT_AGENT_ID"],
)
app = mcp.http_app(transport="sse")
2. Secure A FastMCP Client
from jacs.mcp import JACSMCPClient
client = JACSMCPClient(
"http://localhost:8000/sse",
"./jacs.config.json",
expected_peer_agent_id="SERVER_AGENT_ID",
# Optional stronger rotation policy:
# expected_peer_public_key_hash="SERVER_PUBLIC_KEY_HASH",
)
async with client:
result = await client.call_tool("hello", {"name": "World"})
To accept any valid signer without authenticating which peer signed, use the separate dangerous compatibility switch:
client = JACSMCPClient(
"http://localhost:8000/sse",
"./jacs.config.json",
allow_any_verified_peer=True,
)
This is proof-of-possession only and is not peer authentication. To allow fully unsigned fallback explicitly:
client = JACSMCPClient(
"http://localhost:8000/sse",
"./jacs.config.json",
expected_peer_agent_id="SERVER_AGENT_ID",
allow_unsigned_fallback=True,
)
3. Register JACS As MCP Tools
This is the better fit when the model should be able to ask for signatures, agreements, A2A cards, or trust-store operations directly.
from fastmcp import FastMCP
from jacs.client import JacsClient
from jacs.adapters.mcp import (
register_jacs_tools,
register_a2a_tools,
register_trust_tools,
)
client = JacsClient.quickstart(name="mcp-agent", domain="mcp.local")
mcp = FastMCP("JACS Tools")
register_jacs_tools(mcp, client=client)
register_a2a_tools(mcp, client=client)
register_trust_tools(mcp, client=client)
The core tool set includes document/file signing, verification, legacy sidecar agreement inspection, agent-info, public sharing and media helpers; it does not register an audit tool. A2A and trust helpers are separate opt-in registrations. Expose signing and trust mutations only to callers authorized by your host policy. Agreement inspection does not establish application consent.
Useful Helper APIs
From jacs.mcp:
jacs_toolto sign a specific tool's responsejacs_middleware()for explicit Starlette middlewarejacs_call()for one-off authenticated local MCP calls
jacs_tool, jacs_middleware(), and JacsSSETransport refuse unsigned output
when no agent is loaded or signing fails. The dangerous
allow_unsigned_fallback=True option restores legacy passthrough explicitly.
Authenticated transport messages use a schema-valid JSON-RPC carrier whose
params.envelope is a complete portable-v2 JACS document. Requests receive a
fresh unpredictable wire ID per session, and responses with unknown IDs fail
closed, preventing a signed response from being transplanted into another
client session.
Example Paths In This Repo
archive/native/jacspy/examples/mcp/server.pyarchive/native/jacspy/examples/mcp/client.pyarchive/native/jacspy/examples/mcp_server.pyarchive/native/jacspy/tests/test_adapters_mcp.py
When To Use Adapters Instead
Choose Python Framework Adapters instead of MCP when:
- the model and tools already live in the same Python process
- you only need signed LangChain, LangGraph, or FastAPI boundaries
- you do not need MCP clients to connect from outside the app
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
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... | API | Start here |
|---|---|---|
| Signed LangChain tool results | jacs_signing_middleware, signed_tool | LangChain / LangGraph section below |
Signed LangGraph ToolNode outputs | jacs_wrap_tool_call, with_jacs_signing | LangChain / LangGraph section below |
| Signed FastAPI responses and verification of signed POST bodies | JacsMiddleware, jacs_route | FastAPI section below |
| Signed Anthropic tool return values | jacs.adapters.anthropic.signed_tool | Anthropic section below |
Install only the extra you need:
pip install jacs[langchain]
pip install jacs[fastapi]
pip install jacs[anthropic]
Optional: jacs[langgraph] (LangGraph ToolNode), jacs[ws] (WebSockets). See pyproject.toml for the full list.
LangChain / LangGraph
This is the smallest JACS path if your model already lives in LangChain.
LangChain middleware
from langchain.agents import create_agent
from jacs.client import JacsClient
from jacs.adapters.langchain import jacs_signing_middleware
client = JacsClient.quickstart(name="langchain-agent", domain="langchain.local")
agent = create_agent(
model="openai:gpt-4o",
tools=[search_tool, calc_tool],
middleware=[jacs_signing_middleware(client=client)],
)
LangGraph ToolNode
from jacs.adapters.langchain import with_jacs_signing
tool_node = with_jacs_signing([search_tool, calc_tool], client=client)
Wrap one tool instead of the whole graph
from jacs.adapters.langchain import signed_tool
signed_search = signed_tool(search_tool, client=client)
The executable example to start from in this repo is archive/native/jacspy/examples/langchain/signing_callback.py.
FastAPI / Starlette
Use this when the trust boundary is an API route instead of an MCP transport.
from fastapi import FastAPI
from jacs.client import JacsClient
from jacs.adapters.fastapi import JacsMiddleware
client = JacsClient.quickstart(name="api-agent", domain="api.local")
app = FastAPI()
app.add_middleware(JacsMiddleware, client=client)
Useful options:
- Signing failures and invalid signed POST bodies fail closed by default
sign_responses=Falseorverify_requests=Falseto narrow the behaviora2a=Trueexposes discovery documents cached at middleware initialization; use the A2A serving guide for renewal-aware routes
Request verification only examines POST JSON objects containing jacsSignature.
Unsigned, empty and non-JSON bodies, and other HTTP methods, continue to the
application even with strict=True. This middleware verifies documents; it
does not authenticate every request or bind the signature to HTTP method, URL
and audience. Enforce required authentication separately.
Legacy compatibility options are explicit and dangerous:
allow_unsigned_output=Truepermits raw output after signing failsallow_unverified_passthrough=Truepermits unverifiable input to continueallow_plain_signature_fallback=Truepermits an attestation request to downgrade to a plain signature, dropping attestation claims
strict=True always disables all three fallbacks. Do not use these options at
a trust boundary unless the caller separately marks the data as unsigned or
unverified.
For auth-style endpoints, replay protection is available:
app.add_middleware(
JacsMiddleware,
client=client,
strict=True,
auth_replay_protection=True,
auth_max_age_seconds=30,
auth_clock_skew_seconds=5,
)
This checks freshness and duplicate signatures only for verified signed POST bodies. Its replay cache is local to the middleware instance; it is lost on restart and is not shared across workers or replicas. It does not make unsigned requests fail closed. See the request authentication and replay guide for a complete authentication boundary.
To sign only one route:
from jacs.adapters.fastapi import jacs_route
@app.get("/signed")
@jacs_route(client=client)
async def signed_endpoint():
return {"ok": True}
Anthropic / Claude SDK
Use the Anthropic adapter when you want signed return values from normal Python tool functions:
from jacs.adapters.anthropic import signed_tool
@signed_tool(client=client)
def get_weather(location: str) -> str:
return f"Weather in {location}: sunny"
When To Use MCP Instead
Choose Python MCP Integration instead of adapters when:
- the model is outside your process and talks over MCP
- you want an MCP tool suite for JACS operations
- you need the same server to be usable by external MCP clients
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Python API Reference
| API | Use |
|---|---|
jacs.client.JacsClient | Independent clients with Python result types |
jacs.simple | One process-wide session |
jacs.JacsAgent / jacs.SimpleAgent | Distinct direct native APIs |
Start with Basic Usage. Native arguments accepting None
are not necessarily optional at the call site.
JacsClient
from jacs.client import JacsClient
client = JacsClient.quickstart(name="my-agent", domain="my-agent.example.com")
signed = client.sign_message({"message": "hello"})
assert client.verify(signed.raw).valid
| Method | Result |
|---|---|
JacsClient(config_path=None, algorithm=None, strict=None) | Client; loads only when a path is supplied |
JacsClient.quickstart(name, domain, description=None, algorithm=None, config_path=None, strict=None) | Persistent client |
JacsClient.ephemeral(algorithm=None, strict=None) | In-memory client |
sign_message(data) | SignedDocument |
sign_file(path, embed=False) | SignedDocument |
verify(document) / verify_self() | VerificationResult |
verify_by_id(doc_id) | Verification of stored uuid:version |
export_agent() / get_public_key() | Agent JSON / public PEM string |
update_agent(data) | Updated JSON; supply the complete agent document |
update_document(doc_id, data, attachments=None, embed=False) | Updated document; supply its full id:version key and complete replacement |
reset() | Clear the client session |
The constructor's algorithm argument does not create an agent; select an
algorithm through a factory. New agents default to pq2025.
verify() accepts JSON, a dict or SignedDocument; strict mode raises on
verification failures, otherwise inspect valid and errors.
See the client source for trust-store, attestation, media and A2A helpers.
Result types
| Type | Common attributes |
|---|---|
AgentInfo | agent_id, version, algorithm, config_path, key paths |
SignedDocument | document_id, version, signer_id, payload, raw_json |
VerificationResult | valid, signer_id, timestamp, errors |
raw aliases raw_json, agent_id aliases signer_id, and
timestamp aliases signed_at on SignedDocument.
Signer timestamps are claims; verification does not grant permission to act.
Low-level JacsAgent
Every argument below without a default must be supplied, even when its value is
None. Native operation errors normally raise RuntimeError; Python
argument conversion can raise TypeError.
import json
from jacs import JacsAgent
agent = JacsAgent()
agent.load("./jacs.config.json")
document = agent.create_document(
json.dumps({"jacsDocument": {"type": "message", "content": {"answer": 42}}}),
None, # custom_schema
None, # outputfilename
True, # no_save
None, # attachments
None, # embed
)
assert agent.verify_document(document)
assert agent.verify_agent(None)
| Native method | Return |
|---|---|
load(config_path) | JSON string |
create_document(document_string, custom_schema, outputfilename, no_save, attachments, embed) | Signed JSON string |
verify_document(document_string) | Boolean |
verify_signature(document_string, signature_field) | Boolean; None selects default field |
verify_agent(agentfile) | Boolean; None selects loaded agent |
update_document(document_key, new_document_string, attachments, embed) | Updated JSON string |
update_agent(new_agent_string) | Updated agent JSON string |
export_agent() / get_public_key_pem() | Agent JSON / public PEM string |
create_agreement(document_string, agentids, question, context, agreement_fieldname) | Agreement JSON |
sign_agreement(document_string, agreement_fieldname) | Countersigned JSON |
check_agreement(document_string, agreement_fieldname) | Inspection report JSON |
create_document.attachments accepts an optional string through the native
attachment parser; update_document.attachments accepts an optional list of
paths. Use sign_file() for the common single-file workflow.
Document updates require the original to be available in the agent's document
store. Parse the complete document, derive jacsId:jacsVersion, modify its
application fields, and submit the complete replacement. no_save controls
file export; it does not mean the document is absent from the in-memory store.
Legacy agreement parameters are exactly agentids and
agreement_fieldname on JacsAgent. JacsClient.create_agreement() uses
agent_ids and field_name instead. Inspection reports include
verified_signature_count, pending, and mathematical_checks_valid;
complete and policy_accepted remain false. Never infer consent from them.
Configuration helper
jacs.create_config() returns configuration JSON and does not create keys.
All nine arguments are required, though each accepts None:
create_config(jacs_use_security, jacs_data_directory, jacs_key_directory,
jacs_agent_private_key_filename, jacs_agent_public_key_filename,
jacs_agent_key_algorithm, jacs_private_key_password,
jacs_agent_id_and_version, jacs_default_storage)
Prefer JacsClient.quickstart() for setup. Keep passwords in host secret
handling rather than examples, logs or persisted config.
Native SimpleAgent
SimpleAgent provides instance-based Agreement v2, request authentication,
signed events and public human-approval verification. See the
simplified API guide and
native binding source.
Do not assume methods on one native class exist on the other.
See MCP, Framework Adapters, and Python examples.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Go (jacsgo) Installation and Quick Start
jacsgo provides Go bindings for signing and verifying JACS documents in services, APIs, and agent runtimes.
Go bindings are supported native packages; Python and Node.js have broader
framework adapter coverage. For the primary verify-only MCP server, install
jacs-cli and run jacs mcp. The extended native server is jacs-compat mcp
from jacs-cli-compat; Go MCP examples are demos.
Install
Version 0.16.1 publishes the Go module and matching macOS/Linux native
libraries. go get alone does not provide the CGo library. In a module that
imports github.com/HumanAssisted/JACS/jacsgo:
go get github.com/HumanAssisted/JACS/jacsgo@v0.16.1
go mod vendor
go run github.com/HumanAssisted/JACS/jacsgo/cmd/jacsgo-install@v0.16.1 -version v0.16.1
The installer writes a checksum-verified library into the vendored package's
build/ directory. See the Go package guide
for platform coverage, provenance and source builds. From the full repository,
make build-jacsgo builds the current native library. Published artifacts do
not imply that later source APIs have been released.
Minimal Sign + Verify
Create an agent first (CLI: jacs-compat quickstart --name my-agent --domain my-agent.example.com, or programmatically with jacs.Create(name, opts) 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-compat quickstart --name my-agent --domain my-agent.example.com")
}
signed, err := jacs.SignMessage(map[string]interface{}{
"event": "tool-result",
"status": "ok",
})
if err != nil {
log.Fatal(err)
}
result, err := jacs.Verify(signed.Raw)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Valid: %t signer=%s\n", result.Valid, result.SignerID)
}
Programmatic agent creation
Use jacs.Create(name, &jacs.CreateAgentOptions{...}). Password must be set in options or via JACS_PRIVATE_KEY_PASSWORD. See the jacsgo README for the full API table and options.
Concurrent use
For multiple agents in one process, use NewJacsAgent(), then agent.Load(path)
and agent methods; call agent.Close() when done. Attestation and A2A helpers
also have package-level wrappers. Request authentication and signed-event
helpers require an agent instance; use JacsSimpleAgent for the shared simple
binding contract.
Request authentication and signed events
For the cross-language simple contract, use JacsSimpleAgent:
agent, _, err := jacs.EphemeralSimpleAgent(nil) // default pq2025
if err != nil {
log.Fatal(err)
}
defer agent.Close()
body := []byte(`{"action":"approve"}`)
authorization, err := agent.BuildRequestAuthHeader(
"POST",
"https://api.example.com/v1/jobs?mode=strict",
body,
"jobs-api",
)
if err != nil {
log.Fatal(err)
}
envelope, err := agent.SignResponse(`{"decision":"allow"}`)
if err != nil {
log.Fatal(err)
}
BuildRequestAuthHeader binds method, absolute URL including query, exact body bytes,
audience, signer/key, issue time, and nonce. SignResponse creates a fully
bound 2.0.0 response envelope. Verify an incoming envelope with
UnwrapSignedEvent(eventJSON, serverKeysJSON), where serverKeysJSON is a JSON
map from signer ID to a pinned base64 key or PEM string. Plain events, unknown
signers, legacy payload-only envelopes, and mutations return errors without
releasing data.
The key map is a configured trust decision. A valid signature proves key
possession, not a real-world identity by itself. Both JacsAgent.BuildAuthHeader()
and JacsSimpleAgent.BuildAuthHeader() retain the legacy no-argument form and
emit a warning. Use BuildRequestAuthHeader for v2; deployments can reject the
legacy form with JACS_REJECT_UNBOUND_AUTH_HEADER=true. See the
security guide.
Common Go Use Cases
- Sign outbound API/MCP payloads before crossing trust boundaries
- Verify inbound signed payloads before executing sensitive actions
- Sign files (
SignFile) for portable chain-of-custody workflows - Generate DNS TXT fingerprints (
GetDnsRecord) for public identity verification
MCP and HTTP Patterns
The Go repository includes runnable examples for transport-level signing:
archive/native/jacsgo/examples/mcp/main.gofor MCP-style request/response signingarchive/native/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).
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
JSON Schemas
JACS schemas now describe a small portable signing surface instead of many narrow workflow document types. The core model is a generic signed JSON document: canonical JSON bytes, a common header, cryptographic signatures, signer identity, hashes, optional files, and optional agreement semantics.
Application-specific payloads belong in the document body or in a custom schema layered on top of the header schema.
Preserved Schemas
Document and Agent Schemas
| Schema | Purpose |
|---|---|
header/v1/header.schema.json | Generic signature wrapper for signed JSON documents |
agent/v1/agent.schema.json | Agent identity and public signing metadata |
agreement/v2/agreement.schema.json | Standalone compatibility agreement document for signature inspection |
a2a-verification-result.schema.json | Cross-language A2A artifact verification result |
Component Schemas
| Schema | Purpose |
|---|---|
components/signature/v1/signature.schema.json | Cryptographic signatures |
components/agreement/v1/agreement.schema.json | Legacy sidecar agreement metadata and co-signatures |
components/files/v1/files.schema.json | File attachments and content hashes |
Configuration Schema
| Schema | Purpose |
|---|---|
jacs.config.schema.json | Agent configuration file format |
Schema Locations
Schemas are available as HTTPS URLs and local files:
https://hai.ai/schemas/header/v1/header.schema.json
https://hai.ai/schemas/agent/v1/agent.schema.json
https://hai.ai/schemas/agreement/v2/agreement.schema.json
https://hai.ai/schemas/components/signature/v1/signature.schema.json
https://hai.ai/schemas/components/agreement/v1/agreement.schema.json
https://hai.ai/schemas/components/files/v1/files.schema.json
https://hai.ai/schemas/jacs.config.schema.json
Generic Signed Documents
Every signed document includes a $schema field. Generic payloads use the header schema directly:
{
"$schema": "https://hai.ai/schemas/header/v1/header.schema.json",
"jacsId": "550e8400-e29b-41d4-a716-446655440000",
"jacsVersion": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"jacsType": "document",
"jacsLevel": "artifact",
"content": {
"invoice_id": "INV-001",
"amount": 100.0
}
}
Custom schemas may extend the header when an integration needs stronger payload validation:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://example.com/schemas/invoice.schema.json",
"allOf": [
{ "$ref": "https://hai.ai/schemas/header/v1/header.schema.json" },
{
"type": "object",
"properties": {
"content": {
"type": "object",
"required": ["invoice_id", "amount"]
}
}
}
]
}
Agreements
Native Agreement v2 represents standalone terms, parties and signature records, but its verifier reports mathematical checks only: valid=false, policyAccepted=false, overallScope=consent_signatures_only. Legacy jacsAgreement v1 countersignatures likewise do not authenticate participant, role, quorum, question or context policy.
For authenticated portable consent policy, use Agreement v3. Application authorization remains separate. The native agreement reference documents compatibility inspection.
HAI Extensions
The schema's custom hai property categorizes fields as meta, base or agent. These annotations participate in native schema-driven hashing; they do not supply an authorization policy.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Agent Schema
The Agent Schema defines signed identity documents for entities that can sign and verify JACS documents. Agents are cryptographic identities, not catalogs of services or contact records.
Schema Location
https://hai.ai/schemas/agent/v1/agent.schema.json
Overview
Agent documents describe:
- Identity and versioning
- Agent type
- Optional DNS-based verification domain
- Public-key and signature metadata
Capabilities for A2A interoperability are represented in A2A Agent Cards instead of the JACS identity schema.
Agent Types
The jacsAgentType field classifies the agent:
| Type | Description |
|---|---|
human | Individual person |
human-org | Organization or group |
hybrid | Combination of human and AI components |
ai | Fully artificial intelligence |
Agent Fields
| Field | Type | Required | Description |
|---|---|---|---|
jacsId | string (UUID) | Yes | Unique agent identifier |
jacsVersion | string (UUID) | Yes | Current version identifier |
jacsVersionDate | string (date-time) | Yes | Version timestamp |
jacsType | string | Yes | Set to agent |
jacsOriginalVersion | string (UUID) | Yes | First version identifier |
jacsOriginalDate | string (date-time) | Yes | Creation timestamp |
jacsLevel | string | Yes | Document level |
jacsAgentType | string | Yes | Agent classification |
jacsAgentDomain | string | No | Domain for DNS verification |
jacsSignature | object | No | Cryptographic signature |
jacsSha256 | string | No | Content hash |
DNS Verification
Agents can link to a domain for DNSSEC-validated verification:
{
"jacsAgentDomain": "example.com"
}
The domain should have a DNS TXT record at _v1.agent.jacs.example.com. containing the agent's public key fingerprint.
Example
{
"$schema": "https://hai.ai/schemas/agent/v1/agent.schema.json",
"jacsId": "550e8400-e29b-41d4-a716-446655440000",
"jacsVersion": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"jacsVersionDate": "2024-01-15T10:30:00Z",
"jacsType": "agent",
"jacsOriginalVersion": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"jacsOriginalDate": "2024-01-15T10:30:00Z",
"jacsLevel": "artifact",
"jacsAgentType": "ai",
"jacsAgentDomain": "agent.example.com",
"jacsSignature": {
"agentID": "550e8400-e29b-41d4-a716-446655440000",
"agentVersion": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"date": "2024-01-15T10:30:00Z",
"signature": "base64-encoded-signature...",
"publicKeyHash": "sha256-hash-of-public-key",
"signingAlgorithm": "pq2025",
"fields": ["jacsId", "jacsVersion", "jacsAgentType", "jacsAgentDomain"]
},
"jacsSha256": "document-hash..."
}
See Also
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Document Schema
The Document Schema is the generic header/signature wrapper for JACS documents. It is intentionally payload-agnostic: applications can sign arbitrary JSON content while preserving the same hashing, signature, agreement, and verification contracts across languages.
Schema Location
https://hai.ai/schemas/header/v1/header.schema.json
Overview
The header schema provides:
- Unique document and version identifiers
- Version history through previous-version references
- Cryptographic signatures and hashes
- Optional file attachments
- Optional multi-party agreements
Core Fields
| Field | Type | Required | Description |
|---|---|---|---|
$schema | string | Yes | Schema URL for validation |
jacsId | string (UUID) | Yes | Unique document identifier |
jacsType | string | Yes | Document type, commonly document for generic payloads |
jacsVersion | string (UUID) | Yes | Current version identifier |
jacsVersionDate | string (date-time) | Yes | Version creation timestamp |
jacsOriginalVersion | string (UUID) | Yes | First version identifier |
jacsOriginalDate | string (date-time) | Yes | Document creation timestamp |
jacsLevel | string | Yes | Intended document level |
jacsSignature | object | No | Creator signature |
jacsSha256 | string | No | Canonical content hash |
Document Level
| Level | Description |
|---|---|
raw | Raw data that should not change |
config | Configuration meant to be updated |
artifact | Generated content that may be updated |
derived | Computed from other documents |
Signatures
jacsSignature contains the signing agent, timestamp, algorithm, public key hash, fields signed, and signature bytes.
{
"jacsSignature": {
"agentID": "550e8400-e29b-41d4-a716-446655440000",
"agentVersion": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"date": "2024-01-15T10:30:00Z",
"signature": "base64-encoded-signature-string",
"publicKeyHash": "sha256-hash-of-public-key",
"signingAlgorithm": "pq2025",
"fields": ["jacsId", "jacsVersion", "jacsType", "content"]
}
}
Agreements
Any signed document can include jacsAgreement for multi-party approval.
{
"jacsAgreement": {
"agentIDs": [
"agent-1-uuid",
"agent-2-uuid"
],
"question": "Do you approve this payload?",
"context": "Deployment approval",
"quorum": 2,
"signatures": []
},
"jacsAgreementHash": "hash-of-content-at-agreement-time"
}
File Attachments
Documents can include file attachments using jacsFiles:
{
"jacsFiles": [
{
"mimetype": "application/pdf",
"path": "./documents/contract.pdf",
"embed": false,
"sha256": "file-content-hash"
}
]
}
Complete Example
{
"$schema": "https://hai.ai/schemas/header/v1/header.schema.json",
"jacsId": "550e8400-e29b-41d4-a716-446655440000",
"jacsType": "document",
"jacsVersion": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"jacsVersionDate": "2024-01-15T10:30:00Z",
"jacsOriginalVersion": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"jacsOriginalDate": "2024-01-15T10:30:00Z",
"jacsLevel": "artifact",
"content": {
"title": "Sample Document",
"body": "This is arbitrary signed JSON content."
},
"jacsSignature": {
"agentID": "550e8400-e29b-41d4-a716-446655440000",
"agentVersion": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"date": "2024-01-15T10:30:00Z",
"signature": "signature-base64...",
"publicKeyHash": "public-key-hash...",
"signingAlgorithm": "pq2025",
"fields": ["$schema", "jacsId", "jacsType", "jacsVersion", "content"]
},
"jacsSha256": "document-hash..."
}
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
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
Use quickstart() to create and sign a persistent config. A creation template
must include the schema's six required settings:
{
"$schema": "https://hai.ai/schemas/jacs.config.schema.json",
"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"
}
This unsigned template does not load an existing identity. Existing-agent configs also need the generated identity/version and valid config signature; do not reconstruct them by editing this template.
Fields
| Field | Type | Description |
|---|---|---|
jacs_data_directory | string | Path to store documents and agents |
jacs_key_directory | string | Path to store cryptographic keys |
jacs_agent_private_key_filename | string | Private key filename |
jacs_agent_public_key_filename | string | Public key filename |
jacs_agent_key_algorithm | string | Signing algorithm |
jacs_default_storage | string | Storage backend |
jacs_keychain_backend | string | OS keychain backend: "auto", "macos-keychain", "linux-secret-service", "disabled" |
Configuration Options
Key Configuration
jacs_agent_key_algorithm
Specifies the cryptographic algorithm for signing:
| Value | Description |
|---|---|
ring-Ed25519 | Ed25519 signatures (explicit compatibility) |
pq2025 | FIPS-204 ML-DSA-87 post-quantum signatures |
{
"jacs_agent_key_algorithm": "pq2025"
}
jacs_agent_private_key_filename
Name of the private key file in the key directory:
{
"jacs_agent_private_key_filename": "private.pem"
}
Use the filename produced by agent creation; do not rename encrypted key files independently of their config.
jacs_agent_public_key_filename
Name of the public key file:
{
"jacs_agent_public_key_filename": "public.pem"
}
jacs_private_key_password
Supply the password through the host's JACS_PRIVATE_KEY_PASSWORD, a protected password file or the OS keychain. Do not persist plaintext passwords in JSON config. See password sources.
Storage Configuration
jacs_default_storage
Specifies where documents are stored:
| Value | Description |
|---|---|
fs | Local filesystem |
memory | In-memory storage |
aws | AWS S3; requires a build with the s3 feature |
rusqlite | Indexed SQLite; requires the sqlite feature |
sqlite | SQLx SQLite storage; requires the sqlx-sqlite feature |
{
"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
DNS compares a published fingerprint with the selected native key; strict mode also validates DNSSEC. A domain does not independently authorize an agent.
jacs_agent_domain
The domain used for DNS fingerprint checks. Configure it during agent creation, then publish the required TXT record.
jacs_dns_validate
Controls whether native verification attempts DNS, with the domain and explicit CLI options determining the effective behavior. See the CLI DNS options.
jacs_dns_strict
When true, shared native key discovery (the dns key source) requires a Secure DNSSEC proof, as does Agent::set_dns_strict(true). Agent verification does not read this field: use jacs-compat agent verify --require-strict-dns or AgentBuilder::dns_strict(true) to require strict validation there. A verified DNS claim also forces strict validation.
jacs_dns_required
Controls whether a domain and successful DNS validation are required. Select CLI flags or builder settings explicitly when this is part of the application's policy.
Security
jacs_keychain_backend
OS keychain backend for password storage. Controls whether JACS looks up the private key password from the OS credential store.
{
"jacs_keychain_backend": "auto"
}
| Value | Description |
|---|---|
"auto" | Detect platform default (macOS Keychain or Linux Secret Service) |
"macos-keychain" | macOS Keychain |
"linux-secret-service" | Linux D-Bus Secret Service |
"disabled" | Never consult OS keychain (recommended for CI/headless) |
Override with env var: JACS_KEYCHAIN_BACKEND=disabled
See OS Keychain Configuration for full details.
jacs_use_security
Enable strict security features:
{
"jacs_use_security": "1"
}
Values: "0", "1", or "false", "true"
Observability Fields
The observability object supports logs, metrics, and tracing sub-objects. For full details on all destinations, sampling options, and production patterns, see the Configuration Reference.
Environment Variables
Configuration can be overridden with environment variables:
| Variable | Config Field |
|---|---|
JACS_PRIVATE_KEY_PASSWORD | Host password source; do not write it into config |
JACS_DATA_DIRECTORY | jacs_data_directory |
JACS_KEY_DIRECTORY | jacs_key_directory |
JACS_KEYCHAIN_BACKEND | jacs_keychain_backend |
See Also
- Configuration Reference - Full configuration guide with examples
- JSON Schemas Overview - Schema architecture
- Observability (Rust API) - Rust observability API
- Observability & Monitoring Guide - Structured events, OTEL collector setup
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
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
| Feature | JACS | in-toto / SLSA | Sigstore / cosign | SCITT | IETF RATS / EAT |
|---|---|---|---|---|---|
| Primary domain | AI agent runtime | Build provenance | Artifact signing | Transparency logs | Hardware/platform attestation |
| Identity model | Decentralized (key pairs) | Build system certs | Keyless (OIDC) | Issuer certs | Platform certs |
| Agent-native | Yes | No | No | No | Partial |
| Offline verification | Yes | Yes (with keys) | No (requires Rekor) | No (requires log) | Depends |
| Multi-agent quorum | Yes (M-of-N) | No | No | No | No |
| Evidence normalization | Yes (A2A, email, JWT, custom) | No | No | No | Partial (EAT claims) |
| Transform receipts | Yes (derivation chains) | Yes (build steps) | No | No | No |
| Probabilistic claims | Yes (confidence + assurance) | No | No | No | No |
| Post-quantum | Yes (ML-DSA-87) | No | No | No | Depends |
| Central infrastructure | Not required | Not required | Required (Fulcio + Rekor) | Required (transparency log) | Depends |
| Schema format | JSON Schema + JCS | in-toto layout | Sigstore bundle | SCITT receipt | CBOR/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 modeldraft-messous-eat-ai-00-- EAT profile for AI agentsdraft-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 Level | JACS Equivalent | Verification |
|---|---|---|
| None | No JACS | No signing |
| Basic | Open | Valid signature accepted |
| Standard | Verified | Trust store + DNS verification |
| Enhanced | Strict | Attestation-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
| Scenario | JACS + ... |
|---|---|
| CI/CD pipeline with AI agents | JACS (agent actions) + SLSA (build provenance) |
| Enterprise with compliance requirements | JACS (signing) + SCITT (transparency log) |
| IoT/edge with hardware attestation | JACS (agent layer) + RATS/EAT (hardware layer) |
| Container-based agent deployment | JACS (runtime signing) + cosign (image signing) |
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Security Model
JACS verifies signed data under selected keys. Applications establish identity, trust and authorization around that cryptographic evidence.
Security Model (v0.6.0+)
- Passwords: The private key password can be provided via
JACS_PRIVATE_KEY_PASSWORDenvironment variable,JACS_PASSWORD_FILE, or the OS keychain (macOS Keychain / Linux Secret Service). It is never stored in config files. - Keys: New private-key envelopes use AES-256-GCM with Argon2id. Legacy PBKDF2 envelopes remain readable; public keys and config may be stored on disk.
- Path validation: All paths built from untrusted input (e.g.
publicKeyHash, filenames) are validated viarequire_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 safeUUID:VERSION_UUIDidentifier 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_DIRECTORYand/orJACS_SCHEMA_DIRECTORY) after normalized/canonical path checks. - Network endpoint policy: Registry verification requires HTTPS for
JACS_REGISTRY_URL(legacy alias:HAI_API_URL). Localhost HTTP is allowed for local testing only. - Strict JSON ingress: Raw JSON crossing a signing, verification, hashing,
schema, or protocol boundary must pass the shared
jacs-corestrict decoder. Duplicate decoded object names are rejected recursively before aserde_json::Valuecan collapse them; escape-equivalent names are duplicates, while repeated array elements remain valid. - No secrets in config: Config files must not contain passwords or other secrets. The example config (
jacs.config.example.json) does not includejacs_private_key_password. - Dependency auditing: Run
cargo audit(Rust),npm audit(Node.js), orpip audit(Python) to check for known vulnerabilities.
Core Security Principles
1. Cryptographic Identity
Every JACS agent has a unique cryptographic identity:
- Key Pair: Each agent possesses a private/public key pair
- Agent ID: Unique UUID identifying the agent
- Public Key Hash: SHA-256 hash of the public key for verification
{
"jacsSignature": {
"agentID": "550e8400-e29b-41d4-a716-446655440000",
"publicKeyHash": "sha256-of-public-key",
"signingAlgorithm": "pq2025"
}
}
Native signing defaults to post-quantum pq2025 (ML-DSA-87 / FIPS-204).
Explicit ed25519 creation is supported and emits the canonical
ring-Ed25519 label. Routine rotation preserves the current algorithm;
Ed25519 can explicitly upgrade to pq2025, while PQ-to-Ed25519 downgrade is rejected.
2. Document Integrity
Signatures and hashes protect the signed bytes under the selected public key. Version IDs do not establish a complete history, and signer-supplied timestamps do not prove an independent signing time or real-world authorship.
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 tojacs.config.json(default: 12-factor load)data_directory/key_directory: Override pathsrecent_verify_count: Number of recent documents to re-verify (default 10, max 100)
Return structure: AuditResult with overall_status, risks (list of AuditRisk), health_checks (list of ComponentHealth), summary, checked_at, and optional quarantine_entries / failed_entries.
Rust:
#![allow(unused)] fn main() { use jacs::audit::{audit, AuditOptions}; let result = audit(AuditOptions::default())?; println!("{}", jacs::format_audit_report(&result)); }
Python:
import jacs.simple as jacs
result = jacs.audit() # dict with risks, health_checks, summary, overall_status
print(f"Risks: {len(result['risks'])}, Status: {result['overall_status']}")
Node.js:
import * as jacs from '@hai.ai/jacs/simple';
const result = jacs.audit({ recentN: 5 });
console.log(`Risks: ${result.risks.length}, Status: ${result.overall_status}`);
Available in the language bindings for local diagnostics and automation.
Threat Model
Protected Against
| Threat | Protection |
|---|---|
| Tampering | Content hashes detect modifications |
| Impersonation | Cryptographic signatures verify identity |
| Replay Attacks | HTTP/RPC payload and W3C request proofs use timestamp validation plus atomic nonce consumption; document signatures remain durable by design |
| Man-in-the-Middle | DNS verification via DNSSEC; TLS certificate validation |
| Key Compromise | Key rotation through versioning |
| Weak Passwords | Minimum 28-bit entropy enforcement (35-bit for single class) |
Trust Assumptions
- Private keys are kept secure
- Cryptographic algorithms are sound
- DNS infrastructure (when used) is trustworthy
Replay Store Deployment
The default replay backend is a bounded, atomic, process-local Moka cache. It accepts exactly one concurrent use of a nonce within one process and is suitable for local development or a genuinely single-process service.
Multi-replica production services must install an implementation of the Rust
ReplayStore trait backed by a shared atomic primitive (for example Redis
SET key value NX EX ttl or a database unique insert), then set
JACS_REQUIRE_SHARED_REPLAY_STORE=true. With that setting JACS fails closed
if the installed backend reports itself as process-local or returns an error.
The library never silently falls back to memory after a shared-store failure.
Replay decisions emit structured events without nonce contents and increment
jacs_replay_checks_total{backend,outcome}. Use
JACS_PAYLOAD_MAX_REPLAY_SECONDS to configure the default payload window. For
an accepted future-skewed payload, JACS retains its nonce through the signed
timestamp plus that window, not merely for a fixed interval from verification.
Remote JWKS Retrieval
A2A Agent Card verification keeps JWKS network access disabled unless
JACS_ALLOW_JWKS_FETCH=true (or the broader JACS_ALLOW_NETWORK=true) is set.
Even after that opt-in, JACS accepts only HTTPS origins, resolves and validates
every redirect hop, rejects private/reserved IP answers, pins the validated DNS
answer into the HTTP client, disables proxy resolution, allows at most three
same-origin redirects, and requires a JSON Content-Type. The whole operation is
limited to five seconds and 256 KiB.
Loopback HTTP remains available for deliberate local development only when
both the network capability and JACS_ALLOW_PRIVATE_JWKS=true are set. Do not
enable the private-JWKS override in a service that assesses Agent Cards supplied
by untrusted callers.
Request-Bound HTTP Authorization
Use the request builder exposed by your API with the actual HTTP method, absolute URL, exact body bytes (empty bytes for no body), and a service-specific audience:
| API | Request-bound builder |
|---|---|
Rust SimpleAgent | build_request_auth_header(method, url, body, audience) |
Python SimpleAgent | build_request_auth_header(method, url, body, audience) |
Node JacsSimpleAgent | buildRequestAuthHeader(method, url, body, audience) |
Go JacsSimpleAgent | BuildRequestAuthHeader(method, url, body, audience) |
The resulting JACS v2... header authenticates the signer/key ID, normalized
scheme/authority/path/query, SHA-256 content digest, audience, timestamp, and
nonce. verify_request_auth_header requires the trusted key and the actual
request context; any method, endpoint, query, body, signer, or audience
substitution fails before replay state is consumed.
Build the header after the final body serialization and send those same bytes.
On verification, reconstruct the absolute URL from trusted server/framework
configuration rather than trusting an attacker-controlled forwarding header.
The standard Rust verifier atomically consumes replay state after all other
checks pass. Servers with an application-owned shared store use
verify_request_auth_header_with_trusted_key_without_replay, then grant access
only if an atomic insert such as Redis SET ... NX EX ... accepts the returned
nonce. Use request_auth_replay_ttl(&verified_claims, max_age) for the Redis
expiry (or retain it longer). A fixed max_age TTL is insufficient when the
signer's accepted clock skew is positive: the credential's absolute expiry is
issued_at + max_age.
The older no-argument Rust build_auth_header and equivalent legacy class
methods sign only identity, time, and nonce. They remain available for source
compatibility and emit a WARN because a first-use credential could be moved to
a different request. The additive request-bound methods require method, URL,
body, and audience and emit v2. Strict deployments can reject legacy
construction with JACS_REJECT_UNBOUND_AUTH_HEADER=true.
Encrypted-Key KDF Resource Policy
V2 private-key envelopes accept only the versioned Argon2id resource profile:
8,192–19,456 KiB memory, one or two passes, and parallelism one. JACS validates
these values before base64 allocation or Argon2 execution, and also caps the
serialized envelope/ciphertext and enforces exact salt/nonce sizes. A policy
rejection emits encrypted_key_kdf_policy_rejected at WARN and increments
jacs_kdf_policy_rejections_total{kdf="Argon2id",profile="v2"}; neither signal
contains passwords, key bytes, or ciphertext.
Signed Response and Event Contract
sign_response emits response envelope version 2.0.0 with signature scope
jacs-response-v2. Its domain-separated signature authenticates the complete
envelope except for the signature bytes themselves: payload, protocol
version/type, issuer, document ID, payload hash, timestamp, signer, algorithm,
public-key hash, and any additional fields. This is a protocol-specific
contract; do not replace it with generic document verification or verification
of data alone.
Use verify_response_json_with_key in Rust or unwrap_signed_event /
unwrapSignedEvent in a native binding with a trusted map from signer ID to
public key. Trusted keys may be raw bytes/base64 or canonical PEM, depending on
the binding. On success the bindings return only verified data plus provenance:
{
"data": {"decision": "allow"},
"verified": true,
"status": "verified",
"signerId": "agent-id:version",
"timestamp": "2026-07-09T12:00:00Z",
"algorithm": "pq2025",
"documentId": "2f1e2bdb-55f4-4c99-8f42-d45249808f4b"
}
Plain events, legacy payload-only response envelopes, unknown signers, and
invalid signatures are errors. There is no successful verified: false result;
callers must never dispatch an event by catching that error and reparsing its
untrusted input.
These protocols authenticate control of the private key corresponding to the
public key that the verifier selected. Signed agentID, name, domain, and time
fields are not independent proof of a person, organization, service, or DNS
identity. Establish that association through a pinned key, strict local trust
configuration, or another explicitly chosen verification policy before making
an authorization decision.
Signature Process
Signing a Document
- Field Selection: Determine which fields to sign
- Canonicalization: Serialize fields deterministically
- Signature Generation: Sign with private key
- 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
- Hash Verification: Recompute hash and compare
- Signature Verification: Verify signature with public key
- Agent Verification: Optionally verify agent identity via DNS
is_valid = agent.verify_document(doc_json)
is_signature_valid = agent.verify_signature(doc_json)
Key Management
Compatibility key binding (P2)
An agent's ES256 ecosystem_signing key is authorized per-export by a
compatibility key binding: a compatibilityKeyBinding JACS document
signed by the current native root, persisted as one canonical-JSON file
(jacs_keys/jacs.compat-binding.json). Lifecycle rules:
- granting or widening a scope always requires the native root's signature — the ES256 holder cannot self-escalate;
- re-issue replaces the file (latest
issuedAtwins; no version chains); - native key rotation supersedes the binding — verification fails with a "re-issue" error until a new binding is signed by the current root;
- an expired binding denies export;
expiresAt: nullis permitted in P2; - default issuance grants only the identity scopes (
jwks,did,a2a-agent-card,w3c-agent-identity); content scopes (ap2-mandate,agreement-vc) require an explicit re-issue.
Be precise about what this is: scope checks are a locally enforced authorization policy and an auditable delegation record for relying parties — not isolation. Both private keys live in the same directory under the same password; a compromised host is outside this model. The sound property is non-self-escalation via the native-root signature requirement.
The ES256 private key is encrypted at rest with the same AES-256-GCM + Argon2id envelope as the native root key — the post-quantum library is a signing primitive, not an encryption primitive, so compatibility keys reuse the existing audited envelope rather than inventing a PQ-encrypted key format.
Targeted content exports (P2)
Content exporters (the AP2 mandate export, the Agreement-v2-as-VC export) sign ecosystem artifacts with the ES256 compatibility key through purpose-built, schema-pinned paths — there is no generic "sign this document with algorithm X" API. Two properties matter here:
- Classical verification is not PQ trust. A stock JOSE/Data
Integrity verifier can check the ES256 signature with nothing but the
public key; that proves possession of the compatibility key, not the
agent's native identity. Tracing the export to that root additionally
requires verifying the native-root-signed compatibility key
binding (
jacs-compat agent export-compat-binding). - Native documents are never mutated. Exports are derived views;
the native
jacsSignaturekeeps the agent's selected algorithm and the source document's bytes and verification are unchanged after every export. These are targeted ecosystem artifacts, not JACS projections — native documents never gainproofor other projection fields.
Exports are one-way in P2: JACS emits these artifacts, but verifying incoming AP2 mandates or third-party VCs is out of scope — use the target ecosystem's stock verifiers for that.
Key Generation
JACS generates cryptographic key pairs during agent creation:
# Keys are created in the configured key directory
jacs_keys/
├── private.pem # Private key (keep secure!)
└── public.pem # Public key (can be shared)
Key Protection
Encryption at Rest:
New keys use the V2 AES-256-GCM/Argon2id envelope; PBKDF2 envelopes are read-only compatibility. Never store the password in config files.
# Option 1: Environment variable (recommended for CI/servers)
export JACS_PRIVATE_KEY_PASSWORD="secure-password"
# Option 2: OS keychain (recommended for developer workstations)
jacs-compat keychain set --agent-id <YOUR_AGENT_UUID>
Important: The CLI can prompt for the password during
jacs-compat init, but scripts and servers must setJACS_PRIVATE_KEY_PASSWORDas an environment variable or use the OS keychain.
OS Keychain Integration:
On macOS and Linux desktops, JACS can store and retrieve the private key password from the OS credential store, eliminating the need for environment variables or plaintext password files during day-to-day development:
- macOS: Uses Security.framework (Keychain Access)
- Linux: Uses the freedesktop.org D-Bus Secret Service API (GNOME Keyring, KDE Wallet, KeePassXC)
Each password is keyed by agent ID, so multiple agents can coexist on the same machine without overwriting each other. Store your password once with jacs-compat keychain set --agent-id <ID>, and all subsequent JACS operations will find it automatically. The password resolution order is:
JACS_PRIVATE_KEY_PASSWORDenv var (highest priority -- explicit always wins)JACS_PASSWORD_FILE/ legacy.jacs_passwordfile- OS keychain keyed by agent ID (lowest priority among explicit sources)
To disable keychain lookups (recommended for CI and headless environments):
export JACS_KEYCHAIN_BACKEND=disabled
Or in jacs.config.json:
{
"jacs_keychain_backend": "disabled"
}
Keychain entries are selected by agent ID; a generic shared default entry is
not the current per-agent password contract.
Security note: The OS keychain is encrypted at rest by the OS and unlocked by the user's login session. It is the same mechanism used by
git,ssh-agent,docker, and other CLI tools. Thekeychainfeature is optional and not compiled into thejacscore crate by default -- it is enabled by default injacs-cli-compat.
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 predictable12345678- Insufficient character diversityabc- Too short
File Permissions:
chmod 700 ./jacs_keys
chmod 600 ./jacs_keys/private.pem
Key Rotation
Update agent version to rotate keys:
- Generate new key pair
- Create new agent version
- Sign new version with old key
- Update configuration to use new keys
Native rotation preserves the current algorithm unless explicitly overridden.
Use jacs-compat agent rotate-keys --algorithm pq2025 to upgrade Ed25519;
a PQ identity cannot downgrade to Ed25519. Rotation also supersedes the
ES256 compatibility key binding: re-issue it under the new native root
(jacs-compat agent issue-compat-binding) before the next ecosystem export.
TLS Certificate Validation
JACS includes configurable TLS certificate validation for secure network communication.
Default Behavior (Development)
TLS certificate validation is strict by default. JACS_STRICT_TLS=false is an
explicit insecure development override for the schema-fetching client; do not
use it in production. Omitting the variable or setting it to true retains
certificate-chain validation. This setting does not describe every network
client in the repository.
Registry verification requires HTTPS (JACS_REGISTRY_URL, legacy alias
HAI_API_URL); localhost HTTP is restricted to test/development endpoints.
Signature Timestamp Validation
A document's signed timestamp records the signer's claimed signing time. It is not an independently trusted timestamp or proof that a later request is fresh. JACS document and agent-identity signatures are archival: the generic signature timestamp validator does not impose a maximum age.
What the Timestamp Check Establishes
validate_signature_timestamp parses an RFC 3339 timestamp and rejects a date
more than five minutes in the future relative to the verifier's clock.
Older dates pass this timestamp check; they do not need to fall within five
minutes of the current time. The signature, content hash and applicable trust
policy must still be checked separately.
JACS_MAX_SIGNATURE_AGE_SECONDS is not a supported document-signature setting
in the current runtime. Applications that need an expiration or current-authority
policy must evaluate it explicitly; successful archival verification alone does
not authorize a new action.
Freshness and Replay Protection
A valid timestamp does not prevent copying a signed document or replaying a request. Use the request-bound HTTP authorization contract for actual HTTP actions, with the expected method, URL, body and atomic nonce consumption. The replay store must cover the service's deployment scope; a process-local store does not coordinate multiple replicas.
HTTP/RPC payload verification has a separate replay window, controlled by
JACS_PAYLOAD_MAX_REPLAY_SECONDS (default 300 seconds). That window does not
change archival document-signature age semantics. Strict A2A admission separately
checks the compatibility binding's issued time and expiry; see
Compatibility key binding.
Clock Synchronization
Keep verifier clocks synchronized. The five-minute future tolerance permits bounded clock skew; it does not establish when an artifact was actually created, prevent pre-signing, or prove that a past authorization remains current.
Verification Claims
Agents can claim a verification level that determines security requirements. This follows the principle: "If you claim it, you must prove it."
Claim Levels
| Claim | Required Conditions | Behavior |
|---|---|---|
unverified (default) | None | Relaxed DNS/TLS settings allowed; self-asserted identity |
verified | Domain with DNSSEC | Strict TLS, strict DNS with DNSSEC validation required |
verified-registry | Above + registry verification | Must be registered and verified by a JACS registry |
verified-hai.ai (legacy alias) | Same as verified-registry | Backward-compatible alias |
Setting a Verification Claim
Add the jacsVerificationClaim field to your agent definition:
{
"jacsAgentType": "ai",
"jacsVerificationClaim": "verified",
"jacsAgentDomain": "myagent.example.com"
}
Claim Enforcement
When an agent claims verified, verified-registry, or legacy verified-hai.ai:
- Domain Required: The
jacsAgentDomainfield must be set - Strict DNS: DNS lookup uses DNSSEC validation (no insecure fallback)
- DNS Required: Public key fingerprint must match DNS TXT record
- Strict TLS: TLS certificate validation is mandatory (no self-signed certs)
For verified-registry (or legacy verified-hai.ai) claims, additional enforcement:
- Registry Registration: Agent must be registered with the configured registry (for HAI-hosted registry, hai.ai)
- Public Key Match: Registered public key must match the agent's key
- Network Required: Verification fails if the registry API is unreachable
Backward Compatibility
- Agents without
jacsVerificationClaimare treated asunverified - 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
- No Downgrade: Once an agent claims
verified, it cannot be verified with relaxed settings - Claim Changes: Changing the claim requires creating a new agent version
- Network Dependency:
verified-registryrequires network access to the registry endpoint - Audit Trail: Verification claim and enforcement results are logged
DNS-Based Verification
JACS supports DNSSEC-validated identity verification:
How It Works
- Agent publishes public key fingerprint in DNS TXT record
- Verifier queries DNS for
_v1.agent.jacs.<domain>. - Strict mode validates DNSSEC; optional modes have different guarantees
- Fingerprint is compared against agent's public key
Configuration
Select strict validation explicitly:
jacs-compat agent verify --require-strict-dns
For Rust, configure AgentBuilder::dns_strict(true) together with required
DNS validation. Do not rely on a jacs_dns_strict JSON field being applied by
the native loader. Persisted validate/required settings and domain presence
control the ordinary defaults; verified claims force strict, required checks.
See DNS trust anchoring for the full flag matrix.
Trust Store Management
JACS maintains a trust store for managing trusted agent relationships.
Trusting Agents
First enrollment requires a full self-signed agent document and an independently authenticated public key. Use client.trust_agent_with_key(agent_json, public_key_pem); the older trust_agent(agent_json) only refreshes an enrolled identity. Follow the runnable enrollment example.
Untrusting Agents
untrust_agent() persists a stable-identity distrust marker while retaining historical public documents and keys. It does not delete verification history. Application admission must honor the distrust state.
Trust Store Security
Native trust operations validate identity/path inputs and verify enrollment signatures under the supplied key. Protect the OS/XDG trusted_agents directory from unauthorized writes. A matching self-signature or fetched fingerprint alone does not establish who owns the key or which actions it may authorize.
Best Practices
- Verify Before Trust: Always verify an agent's public key hash through an out-of-band channel before trusting
- Audit Trust Changes: Log all trust store modifications for security auditing
- Periodic Review: Regularly review and prune the trust store
Agreement Security
Legacy v1 sidecars inspect present payload signatures without authenticating participant, role, quorum, question or context policy. Native Agreement v2 also reports inspection only; mathematical validity is not consent-policy acceptance. See the Agreement v2 boundary. New portable consent uses Agreement v3, followed by application authorization.
Request/Response Security
For HTTP, use the request-bound authorization and signed response/event contracts above. Generic document signing does not bind an HTTP method, URL, audience, or request body, and generic verification of only the response payload does not authenticate the v2 envelope metadata.
MCP messages that need portable artifact integrity can carry ordinary signed JACS documents. That is separate from HTTP request authentication: the MCP stdio transport has no HTTP target to bind, and authorization remains the MCP host's responsibility.
Algorithm Security
Supported Algorithms
| Algorithm | Type | Role |
|---|---|---|
pq2025 | Post-Quantum (FIPS-204 ML-DSA-87) | Default native root; cannot rotate down to Ed25519 |
ring-Ed25519 | Elliptic Curve | Supported native creation, signing, and verification; user-facing input alias: ed25519 |
| ES256 | Elliptic Curve (P-256) | Compatibility key for targeted ecosystem exports only — never a valid native jacsSignature algorithm |
Algorithm Selection
Native JACS signing defaults to pq2025; explicit Ed25519 creation is
supported without substitution. Routine rotation preserves the algorithm;
an Ed25519-to-PQ upgrade is explicit.
An ES256 content signature does not create native JACS trust: classical verification proves possession of the compatibility key only. The native-root-signed compatibility key binding is what ties that key to the agent — see Compatibility key binding (P2).
Security Best Practices
1. Key Storage
# Never commit keys to version control
echo "jacs_keys/" >> .gitignore
# Secure file permissions
chmod 700 ./jacs_keys
chmod 600 ./jacs_keys/private.pem
2. Password Handling
# Option A: Use environment variables (CI, servers)
export JACS_PRIVATE_KEY_PASSWORD="$(pass show jacs/key-password)"
# Option B: Use OS keychain (developer workstations)
jacs-compat keychain set --agent-id <AGENT_UUID> # stores password securely in OS credential store
# Option C: Disable keychain for headless/CI environments
export JACS_KEYCHAIN_BACKEND=disabled
3. Transport Security
Always use TLS for network communication:
# HTTPS for web transport
client = JACSMCPClient(
"https://localhost:8000/sse",
expected_peer_agent_id="SERVER_AGENT_ID",
) # TLS plus an explicit signer pin
# Plain HTTP remains loopback-only and should not be exposed remotely.
4. Verification Policies
{
"jacs_dns_strict": true,
"jacs_dns_required": true,
"jacs_enable_filesystem_quarantine": "true"
}
5. Audit Logging
Enable observability for security auditing:
{
"observability": {
"logs": {
"enabled": true,
"level": "info"
}
}
}
Security Checklist
Development
- Generate unique keys for each environment
- Never commit private keys
- Use test keys separate from production
Production
- Encrypt private keys at rest
-
Use environment variables or OS keychain for secrets (never store
jacs_private_key_passwordin config) -
Set
JACS_KEYCHAIN_BACKEND=disabledon CI/headless servers - Enable DNS verification
- Configure strict security mode
- Enable audit logging
- Use TLS for all network transport
- Restrict key file permissions (0600 for keys, 0700 for key directory)
- Implement key rotation policy
-
Set
JACS_STRICT_TLS=truefor 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 auditregularly 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:
- Register your agent with your configured registry (for HAI-hosted registry, hai.ai)
- Or use
verifiedfor 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:
- Keep the current claim level
- Create a new agent with the desired claim level
- If this is a test/development scenario, start fresh
# Create a new agent instead
jacs-compat quickstart --name replacement-agent --domain replacement.example.com
"DNS fingerprint mismatch"
Problem: The public key hash in DNS doesn't match your agent's key.
Solution:
- Regenerate the DNS record with your current keys:
jacs-compat agent dns --domain example.com --provider plain - Update your DNS TXT record with the new value
- Wait for DNS propagation (can take up to 48 hours)
"Strict DNSSEC validation failed"
Problem: Your domain doesn't have DNSSEC enabled.
Solution:
- Enable DNSSEC with your domain registrar
- Publish DS records at the parent zone
- Verified claims require strict DNSSEC; fix DNSSEC before verifying them.
Claim Level Reference
| Claim | Security Level | Requirements |
|---|---|---|
unverified | 0 (lowest) | None - self-asserted identity |
verified | 1 | Domain + DNS TXT record + DNSSEC |
verified-registry | 2 (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 aliasverified-hai.aiis 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-compat agent verify
# Verify DNS record is correct
jacs-compat agent verify --require-strict-dns
# Test verification
jacs-compat agent verify
See Also
- Cryptographic Algorithms - Algorithm details
- DNS Verification - DNS-based identity
- Configuration - Security configuration
- Agreements - Multi-party agreements
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Key Rotation
Key rotation is the process of replacing an agent's cryptographic keys while preserving the ability to verify documents signed with previous keys. JACS implements version-aware key management to support secure key lifecycle operations.
Why Key Rotation Matters
Key Compromise Recovery
When a private key is compromised, the agent must be able to:
- Generate new keys and continue operating
- Revoke trust in the compromised key
- Maintain verifiability of documents signed before the compromise
Cryptographic Agility
Cryptographic algorithms evolve. Key rotation enables:
- Migration from older algorithms to newer ones
- Transition to post-quantum cryptography when needed
- Algorithm upgrades without breaking existing signatures
Migrating Ed25519 Agents to pq2025
Native rotation without --algorithm preserves the current algorithm. Upgrade
an Ed25519 identity explicitly:
jacs-compat agent rotate-keys --algorithm pq2025
- An Ed25519 identity can replace its key with Ed25519 or explicitly upgrade
to
pq2025; the transition proof is signed by its previous key. - A
pq2025identity cannot downgrade to Ed25519. - Until it rotates, an Ed25519 agent keeps signing normally with Ed25519.
- Documents signed by the old Ed25519 key remain verifiable after migration (see Cross-Algorithm Verification in the Algorithm Selection Guide).
- Rotation supersedes the ES256 compatibility key binding: the binding
is signed by the native root, so after rotation ecosystem exports fail
with a "re-issue" error until
jacs-compat agent issue-compat-bindingruns under the new native root. - Rotation applies to the native root only — rotating the ES256 compatibility key itself is out of scope in P2.
Compliance Requirements
Many security standards require periodic key rotation:
- PCI-DSS mandates regular key changes
- SOC 2 requires key management policies
- NIST guidelines recommend rotation schedules
Agent Versioning
JACS uses a versioned identity model where each key rotation creates a new agent version.
Version Format
Agent identifiers follow the format: {agent_id}:{version_uuid}
- jacsId: The stable agent identity (UUID v4) - never changes
- jacsVersion: Current version UUID - changes on each update
- jacsPreviousVersion: Links to the prior version
- jacsOriginalVersion: The first version ever created
{
"jacsId": "550e8400-e29b-41d4-a716-446655440000",
"jacsVersion": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"jacsPreviousVersion": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"jacsOriginalVersion": "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
}
Version Chain
Each version forms a linked chain back to the original:
Original (v1) <-- Previous (v2) <-- Current (v3)
| | |
key-A key-B key-C
This chain provides an audit trail of all key changes and allows verification of any version.
Version-Aware Verification
The critical insight enabling key rotation is that signatures contain both the agent ID and the version that created them.
Signature Structure
{
"jacsSignature": {
"agentID": "550e8400-e29b-41d4-a716-446655440000",
"agentVersion": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"publicKeyHash": "sha256-of-public-key-A",
"signingAlgorithm": "ring-Ed25519",
"signature": "base64-encoded-signature",
"date": "2024-01-15T10:00:00Z"
}
}
Key Resolution Process
When verifying a signature:
- Extract
agentVersionandpublicKeyHashfrom the signature - Look up the public key that was active for that version
- 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
Native external-document verification uses the configured JACS_KEY_RESOLUTION
sources (default local,registry). An opted-in dns source fetches the exact
signed version's key over HTTPS and pins it to the raw-key DNS fingerprint, so
old documents verify through DNS only while their fingerprint and exact-version
key endpoint stay published.
Key Rotation Process
Step-by-Step Rotation
- Generate new key pair with the desired algorithm
- Create new agent version with updated key information
- Sign new version with old key (transition signature)
- Update DNS records to include new key fingerprint
- Store old public key for future verifications
CLI Commands
# Rotate keys with default algorithm
jacs-compat agent rotate-keys
# Rotate to post-quantum algorithm
jacs-compat agent rotate-keys --algorithm pq2025
# List key history (active and archived keys)
jacs-compat agent keys-list
# Repair config after a crash during rotation
jacs-compat agent repair
Transition Signature
During key rotation, JACS produces a cryptographic transition proof that binds the old key to the new key. This proof is embedded in the agent document as jacsKeyRotationProof:
{
"jacsKeyRotationProof": {
"transitionMessage": "JACS_KEY_ROTATION:{agent_id}:{old_key_hash}:{new_key_hash}:{timestamp}",
"signature": "base64-encoded-signature-with-old-key",
"signingAlgorithm": "ring-Ed25519",
"oldPublicKeyHash": "sha256-of-old-key",
"newPublicKeyHash": "sha256-of-new-key",
"timestamp": "2026-04-07T10:00:00Z"
}
}
The transition message is signed with the old private key before it is archived. This proves:
- The rotation was authorized by the holder of the previous key
- The old and new keys are cryptographically linked
- An attacker cannot forge a rotation without the old private key
Agent::verify_transition_proof() validates the detached old-key signature.
Registries and directories must use
Agent::verify_transition_proof_for_rotation(proof, expected_agent_id, old_key, new_key)
instead. That stricter API also binds the proof to the expected stable identity
and exact candidate new key, preventing proof transplantation.
Crash Recovery
JACS uses a write-ahead journal to recover from crashes during key rotation. Before rotation begins, a journal file is written to {key_directory}/.jacs_rotation_journal.json. The journal tracks the rotation stage:
started- Rotation initiatedkeys_rotated- New keys generated, old keys archivedagent_saved- New agent version saved to diskconfig_signed- Config re-signed with new key (journal deleted on success)
If the process crashes mid-rotation, the next agent load detects the journal and automatically repairs the config by re-signing it with the current key. No manual intervention is required for the common case.
For manual recovery: jacs-compat agent repair
Cross-Algorithm Rotation
An Ed25519 root can explicitly upgrade to pq2025. Rotation without an
override preserves the current algorithm; PQ-to-Ed25519 downgrade is rejected:
# Rotate from Ed25519 to post-quantum
jacs-compat agent rotate-keys --algorithm pq2025
#![allow(unused)] fn main() { // Rust API let result = advanced::rotate(&agent, Some("pq2025"))?; }
# Python
result = client.rotate_keys(algorithm="pq2025")
// Node.js
const result = await client.rotateKeys({ algorithm: 'pq2025' });
After cross-algorithm rotation, the config file's jacs_agent_key_algorithm field is updated atomically. Documents signed before the rotation remain verifiable using the archived old key.
Version transition
Rotation retains jacsId, assigns a new jacsVersion and jacsVersionDate,
links jacsPreviousVersion, and embeds jacsKeyRotationProof. A document signed
before rotation still identifies its original agent version and key hash.
Retain that independently trusted public key to verify the older document;
there is no on-document jacsKeyHistory revocation registry.
Trust Store with Version History
Retain independently trusted public keys for each exact agent_id:version.
UUID versions are identifiers, not ordered ranges; do not infer key validity
from UUID comparison.
DNS and key versions
Agent-specific native lookup accepts overlapping fingerprints for one agent and records for multiple agents. Each record uses the actual format:
v=jacs; jacs_agent_id=<agent-uuid>; alg=SHA-256; enc=hex; jac_public_key_hash=<raw-key-sha256>; uri=https://keys.example.com/jacs/v1/agents/<agent-uuid>/keys/<version-uuid>
A cross-origin URI requires an explicitly configured JACS_KEYS_BASE_URL.
Publish distinct raw-key fingerprints and exact-version endpoints during an
overlap. Conflicting URIs for the same agent and fingerprint are rejected.
jacs-compat agent dns generates the configured identity's record; it does not
publish changes or provide an --all-keys export. The DNS record does not
implement ver, valid_until or revocation semantics. Keep historical keys
available and enforce revocation in application policy. There is no universal
rotation interval of twice the TTL: account for caches and retained documents.
See DNS trust anchoring and the
key discovery guide
for signed-reference compatibility and verifier boundaries.
Security Considerations
Key Revocation
When a key is compromised:
- Revoke the key in the relying application's trust policy
- Update DNS to remove discovery records for the compromised key; DNS has no revocation-status field
- Reject revoked keys in verification policy; cryptographic signature validity alone does not enforce revocation
- Notify trusted peers if possible
Secure Deletion
After rotation:
- Old private keys should be securely deleted
- Only public keys are retained for verification
- Key metadata must be protected from modification
Best Practices
Rotation Schedule
- Regular rotation: Quarterly or annually for compliance
- Algorithm upgrade: When transitioning to stronger cryptography
- Incident response: Immediately after suspected compromise
Pre-Rotation Checklist
- Backup current agent state
- Verify all systems can handle new key format
- Plan DNS propagation time
- Notify dependent systems of upcoming change
Post-Rotation Checklist
- Verify new key is active
- Confirm old documents still verify
- Update DNS records
- Securely delete old private key
- Test signing with new key
-
Re-issue the compatibility key binding (
jacs-compat agent issue-compat-binding) if the agent has an ES256 compatibility key — ecosystem exports fail until the new native root signs a fresh binding
See Also
- Security Model - Overall security architecture
- Cryptographic Algorithms - Algorithm details
- DNS Verification - DNS-based identity verification
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Cryptographic Algorithms
JACS supports multiple cryptographic algorithms for digital signatures, providing flexibility for different security requirements and future-proofing against quantum computing threats.
pq2025 (ML-DSA-87 / FIPS-204) is the native default. Explicit
ed25519 selection creates genuine Ed25519 keys and emits the canonical
ring-Ed25519 wire label; JACS never silently substitutes one supported
algorithm for another. Native rotation preserves the current algorithm unless explicitly upgraded.
ES256 exists in JACS only as an ecosystem
compatibility key for targeted exports; it is never a native
jacsSignature algorithm. See the
Algorithm Selection Guide.
Supported Algorithms
| Algorithm | Config Value | Type | Key Size | Signature Size | Role |
|---|---|---|---|---|---|
| PQ2025 | pq2025 | ML-DSA-87 | 2,592 bytes | 4,627 bytes | Native root default; post-quantum |
| Ed25519 | ring-Ed25519 (input: ed25519) | Elliptic Curve | 32 bytes | 64 bytes | Supported native creation, signing, and verification |
Ed25519 (ring-Ed25519)
Supported for creation, signing, and verification across native bindings.
Overview
Ed25519 is an elliptic curve signature scheme using Curve25519. JACS uses the ring cryptographic library implementation.
Characteristics
- Speed: Extremely fast signing and verification
- Key Size: 32-byte private key, 32-byte public key
- Signature Size: 64 bytes
- Security Level: ~128 bits (classical)
Configuration
{
"jacs_agent_key_algorithm": "ring-Ed25519"
}
For user-facing CLI and SDK input, prefer ed25519. Configuration and signed
documents store the canonical ring-Ed25519 wire label.
Use Cases
- Bandwidth-sensitive or high-throughput signing
- Interoperability with existing Ed25519 identities
- Shorter-lived signatures where post-quantum resistance is not required
Example
import jacs
import json
agent, info = jacs.SimpleAgent.ephemeral(algorithm="ed25519")
assert info["algorithm"] == "ring-Ed25519"
signed = agent.sign_message({"message": "Hello, World!"})
assert json.loads(signed["raw"])["jacsSignature"]["signingAlgorithm"] == "ring-Ed25519"
PQ2025
Overview
PQ2025 uses ML-DSA-87, the FIPS-204 post-quantum signature algorithm currently supported by JACS. It is the default native root algorithm.
Characteristics
- Speed: Moderate (slower than Ed25519)
- Key Size: 2,592-byte public keys
- Signature Size: 4,627 bytes
- Security Level: NIST Level 3 (quantum-resistant)
Configuration
{
"jacs_agent_key_algorithm": "pq2025"
}
Use Cases
- Long-term document security
- Protection against future quantum attacks
- High-security applications
- Government/defense requirements
Considerations
- Larger signatures and keys than Ed25519
- Use for compliance or long-lived signatures where post-quantum readiness matters
- May be required for future compliance
ES256 (Compatibility Key — Never Native)
ES256 (ECDSA P-256) appears in JACS only as the ecosystem compatibility
key used by the targeted exporters (JWKS, DID/W3C identity, A2A agent
card, AP2 mandate, Agreement-v2 Verifiable Credential). It is never a
valid native jacsSignature algorithm: the signature schema permits only
ring-Ed25519 and pq2025, and native verification rejects ES256.
An ES256 signature on an exported artifact proves possession of the compatibility key, nothing more — it does not by itself establish native JACS trust. The native-root-signed compatibility key binding is what ties the ES256 key to the agent; see the Security Model for the binding lifecycle. Verifying incoming AP2 mandates or third-party VCs is out of scope in P2.
Algorithm Selection Guide
Choose the native algorithm at agent creation. pq2025 is the default;
ed25519 is an explicit size/performance tradeoff.
Decision Matrix
| Requirement | Algorithm |
|---|---|
| New agent requiring post-quantum assurance | pq2025 (default) |
| New agent prioritizing small keys/signatures | ed25519 (stored/emitted as ring-Ed25519) |
| Verifying Ed25519 documents | ring-Ed25519 (verification is automatic) |
| JOSE/W3C ecosystem interop (JWKS, AP2, VC) | ES256 compatibility key — export-only, never native |
By Use Case
Web APIs and MCP:
{
"jacs_agent_key_algorithm": "pq2025"
}
Native signing defaults to pq2025; explicitly selected Ed25519 agents remain Ed25519 until an explicit algorithm upgrade.
Legal/Financial Documents:
{
"jacs_agent_key_algorithm": "pq2025"
}
Long-term validity requires quantum resistance.
Enterprise Integration:
{
"jacs_agent_key_algorithm": "pq2025"
}
Use pq2025 where cross-organization retention or compliance expectations favor post-quantum signatures.
High-Security:
{
"jacs_agent_key_algorithm": "pq2025"
}
Belt-and-suspenders approach for maximum protection.
Key Generation
Keys are generated automatically when creating an agent:
# Directory structure after agent creation
jacs_keys/
├── private.pem # Algorithm-specific private key
└── public.pem # Algorithm-specific public key
Key Formats
| Algorithm | Private Key Format | Public Key Format |
|---|---|---|
| ring-Ed25519 | PEM (PKCS#8) | PEM (SPKI) |
| pq2025 | Raw ML-DSA-87 bytes | Raw ML-DSA-87 bytes |
Signature Structure
Signatures in JACS documents include algorithm metadata:
{
"jacsSignature": {
"agentID": "550e8400-e29b-41d4-a716-446655440000",
"agentVersion": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"date": "2024-01-15T10:30:00Z",
"signature": "base64-encoded-signature",
"publicKeyHash": "sha256-of-public-key",
"signingAlgorithm": "pq2025",
"fields": ["jacsId", "jacsVersion", "content"]
}
}
The signingAlgorithm field enables verifiers to use the correct verification method. The native signature schema permits only ring-Ed25519 and pq2025; a native document whose signingAlgorithm is anything else (for example ES256) fails verification.
Hashing
JACS uses SHA-256 for all hash operations:
- Document content hashing (
jacsSha256) - Public key fingerprints (
publicKeyHash) - Legacy agreement payload hashing (
jacsAgreementHash), without authenticated consent policy
{
"jacsSha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
Algorithm Migration
Key rotation is the designated Ed25519 → pq2025 migration path.
Rotation without an override preserves the current algorithm. Explicit
Ed25519-to-pq2025 upgrades are supported; PQ-to-Ed25519 downgrades are rejected.
-
Rotate Keys
jacs-compat agent rotate-keys --algorithm pq2025 -
Re-issue the Compatibility Key Binding (if present)
The ES256 compatibility key binding is signed by the native root, so rotation supersedes it. Ecosystem exports fail with a "re-issue" error until a new binding is signed by the current native root:
jacs-compat agent issue-compat-binding -
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):
| Algorithm | Sign (ops/sec) | Verify (ops/sec) | Key Gen (ms) |
|---|---|---|---|
| ring-Ed25519 | ~50,000 | ~20,000 | <1 |
| pq2025 | ~4,000 | ~8,000 | ~2 |
Security Considerations
Algorithm Agility
JACS documents include the signing algorithm, enabling:
- Verification with correct algorithm
- Graceful algorithm transitions
- Multi-algorithm environments
Forward Secrecy
Signatures don't provide forward secrecy. For confidentiality:
- Use TLS for transport
- Consider additional encryption layers
Key Compromise
If a private key is compromised:
- Generate new key pair
- Create new agent version
- Revoke trust in compromised version
- Re-sign critical documents
See Also
- Security Model - Overall security architecture
- Configuration - Algorithm configuration
- DNS Verification - Public key fingerprint verification
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Algorithm Selection Guide
Choosing the right signing algorithm affects key size, signature size, verification speed, and compliance posture. This guide helps you pick the right one.
New-Agent Selection
pq2025 (ML-DSA-87 / FIPS-204) is the secure default. Pass ed25519
explicitly when its smaller keys and signatures are required. JACS returns the
canonical wire label ring-Ed25519; the requested label, generated key shape,
persisted configuration, and emitted signature algorithm always agree.
ring-Ed25519 remains accepted as a legacy input alias. ES256 is an ecosystem
compatibility key, never a native jacsSignature algorithm.
Native rotation preserves the current algorithm unless an Ed25519 identity
explicitly upgrades to pq2025; PQ-to-Ed25519 downgrade is rejected. See
Key Rotation.
Supported Algorithms
| Algorithm | Config Value | Public Key | Signature | Status |
|---|---|---|---|---|
| ML-DSA-87 | pq2025 | 2,592 bytes | 4,627 bytes | Default; post-quantum (FIPS-204) |
| Ed25519 | ring-Ed25519 (input alias: ed25519) | 32 bytes | 64 bytes | Supported for creation, signing, and verification |
When to Choose Post-Quantum
Choose pq2025 (ML-DSA-87, FIPS-204) when:
- Your compliance team asks about quantum readiness
- Government or defense contracts require FIPS-204
- You need long-lived signatures that must remain valid for 10+ years
- You want to avoid a future algorithm migration
JACS supports ML-DSA-87 (FIPS-204) for post-quantum digital signatures. When your compliance team asks about quantum readiness, JACS already has the answer.
The tradeoff is size: ML-DSA-87 public keys are 2,592 bytes and signatures are 4,627 bytes -- roughly 80x larger than Ed25519. For most applications this is negligible, but if you're signing millions of small messages and bandwidth matters, consider Ed25519.
Cross-Algorithm Verification
JACS verification works across supported algorithms. An agreement can contain signatures from Ed25519 and ML-DSA agents and all verify correctly. This heterogeneous verification is important for cross-organization scenarios where different parties choose different supported algorithms.
Each agent uses one algorithm (chosen at creation time), but can verify signatures from all supported algorithms.
Configuration
Set the algorithm in your jacs.config.json:
{
"jacs_agent_key_algorithm": "pq2025"
}
Or via environment variable:
export JACS_AGENT_KEY_ALGORITHM=pq2025
User-facing values for new keys: ed25519, pq2025. The historical
ring-Ed25519 input remains accepted and is the canonical signature/config
label emitted for Ed25519.
In Python and Node.js, pass the algorithm to quickstart(...):
from jacs.client import JacsClient
client = JacsClient.quickstart(
name="algo-agent",
domain="algo.example.com",
algorithm="pq2025",
)
import { JacsClient } from "@hai.ai/jacs/client";
const client = await JacsClient.quickstart({
name: "algo-agent",
domain: "algo.example.com",
algorithm: "pq2025",
});
Current Limitations
- Each active native key has one algorithm. Changing it requires an authorized key rotation; editing the config does not convert an existing key.
- Algorithm negotiation between agents is planned but not yet implemented.
- Use
pq2025for ML-DSA-87 post-quantum signatures and verification hints.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
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
DocumentServiceread verifies the stored JACS document before returning it. - Every
create()andupdate()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.
Native Backends
| Backend | Config Value | Native Surface | Notes |
|---|---|---|---|
| Filesystem | fs | MultiStorage + DocumentService | Default. Signed JSON files on disk. |
| Local indexed SQLite | rusqlite | DocumentService + SearchProvider | Stores signed documents in a local SQLite DB with FTS search. |
| AWS object storage | aws | MultiStorage | Object-store backend. |
| Memory | memory | MultiStorage | Non-persistent, useful for tests and temporary flows. |
| Browser local storage | local | MultiStorage | WASM-only. |
For local indexed document search in native JACS, 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 indexed document backend used by the bindings and MCP search path when built with the sqlite feature (enabled by the native crate's defaults).
{
"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
DocumentServicereads 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 is an object-store backend for lower-level storage operations and requires a source build with the native jacs crate's s3 feature. Selecting aws in config alone cannot add this support to a prebuilt package.
{
"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-postgresqljacs-duckdbjacs-redbjacs-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
| Scenario | Recommendation |
|---|---|
| Default local usage | fs |
| Local search + filtering | rusqlite |
| Ephemeral tests | memory |
| Remote object storage | aws |
| Postgres / vector / multi-model needs | Use an extracted backend crate |
Migration Notes
Switching backends does not migrate data automatically.
When you change jacs_default_storage:
- Export the signed documents you need to keep.
- Update the config value.
- Create/import the new backend’s data store.
- Re-run verification on imported documents as part of migration validation.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
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
- 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"]
}
]
}
- 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
}
}
}
Group Related Fields
{
"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"]
}
]
}
Legal Contract
{
"$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
- Create new schema version
- Update application to support both versions
- Migrate existing documents
- Deprecate old version
See Also
- JSON Schemas Overview - Built-in schemas
- Document Schema - Header fields
- Configuration - Schema configuration
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Trust Store Operations
The native trust store retains explicitly enrolled public identities for selection by verification and admission workflows. First enrollment requires both the full self-signed agent document and an independently authenticated public key. A fetched Agent Card or its same-origin JWKS does not establish that trust.
Enroll a partner
Obtain partner-agent.json and partner-public.pem through an authenticated channel, then:
from pathlib import Path
from jacs.client import JacsClient
client = JacsClient.quickstart(name="receiver", domain="receiver.example.com")
agent_json = Path("partner-agent.json").read_text()
public_key_pem = Path("partner-public.pem").read_text()
lookup_id = client.trust_agent_with_key(agent_json, public_key_pem)
assert client.is_trusted(lookup_id)
The partner can export these public values with share_agent() and share_public_key(). Enrollment checks the document's self-signature against the supplied key and returns the exact agent_id:version lookup ID. Protect the local trust directory and its writes; native storage uses the OS/XDG JACS data location under trusted_agents, not a universal ~/.jacs/trust_store path.
The Rust equivalent is jacs::trust::trust_agent_with_key(&agent_json, Some(&public_key_pem))?. The older trust_agent(agent_json) refreshes an already enrolled identity; it cannot bootstrap an unknown key.
Use and revoke trust
Check the exact identity/version and verify the signed document before applying application authorization. With local in JACS_KEY_RESOLUTION (the default), the shared native resolver for documents, A2A artifacts and attestations uses this enrollment for v2 signatures by the exact enrolled agent_id:version, and refuses a key that conflicts with it. Other signers and versions need a cached local key or opt-in registry or DNS/HTTPS key discovery. Trust-store membership alone does not validate arbitrary input or approve an action. Retain independently trusted historical keys when old signatures must remain verifiable; UUID versions are identifiers, not ordered ranges.
untrust_agent() records stable-identity distrust and retains historical documents and keys. It does not erase public history. Subsequent trust decisions must honor that marker; re-enrollment is an explicit administrative decision.
DNS TXT records publish a public-key fingerprint, not the key; opt-in discovery fetches the key over HTTPS and checks it against that fingerprint. See DNS trust, verification, and A2A discovery for the separate resolution and admission steps.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Signing at Integration Boundaries
Use explicit signing calls for selected artifacts, or an adapter to apply signing at a framework boundary. Adapters cover their documented message/body types; they do not guarantee that every application path is signed or authorized.
MCP transports
The ready-made Rust server exposes ordinary MCP tools to standard clients. Node/Python signed transport wrappers instead require a matching JACS wrapper at both endpoints, trusted peer keys and expected identities. They sign custom carriers over supported local transports; they do not encrypt them. See MCP integration.
HTTP middleware
Express, Koa and FastAPI adapters sign or inspect supported document bodies. Coverage differs by adapter: FastAPI's request verifier handles signed JSON objects on POST, while unsigned/non-JSON bodies and other methods can reach the application even with strict mode. Do not treat middleware installation as caller authentication or route authorization.
For request-bound authentication, bind method, URL, exact body bytes and audience, verify under independently trusted keys, enforce replay protection and apply application policy. See HTTP protocol, Express, Koa and Python adapters.
A2A discovery
JACS publishes signed cards and public evidence using a persisted native PQ root or explicit ES256 compatibility. Peers need support for the chosen algorithm and independently trusted key selection. Discovery routers do not implement advertised message/task handlers or grant permission to call them. Follow the A2A how-to.
Choose the integration that covers the actual boundary, then test unsigned, malformed and unauthorized input as well as successful signing. Signature validity establishes integrity under a key; the host owns identity attribution, claim evaluation and authorization.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
DNS Trust Anchoring
DNS can associate a public-key fingerprint with a domain. It does not by itself prove the identity of a person or organization, authorize an action, or enroll an agent in your trust store. The current native resolver's configuration and verifier coverage are distinct from portable CLI/MCP explicit-key verification and A2A card policy.
Fingerprints and routing
Records at _v1.agent.jacs.<domain> use:
v=jacs; jacs_agent_id=<agent-uuid>; alg=SHA-256; enc=base64; jac_public_key_hash=<raw-key-digest>
jac_public_key_hash encodes SHA-256 of raw public-key bytes. It is not the
signed envelope's historical publicKeyHash string or a hash of its PEM armor.
An optional HTTPS uri points to supported discovery JSON; otherwise the
resolver uses https://<domain>/.well-known/jacs-pubkey.json.
A signed v2 agentDomain is authenticated only after the envelope verifies.
Before then it is a routing hint. Network requests require explicit capability
opt-in, bounded secure transport and key/reference checks. Existing identity
conflicts or distrust are not overridden by discovering a new key.
DNSSEC and local trust
Strict lookup requires the validating resolver to return a Secure DNSSEC
proof. A remote server's AD bit is not accepted as proof on its own. The zone
must be signed and its DS delegation correctly published. Non-strict lookup
has no equivalent cryptographic DNS authentication.
DNSSEC protects records in transit; it does not prevent a compromised or authorized DNS administrator from publishing a new key. Short TTLs do not fix that threat. Keep application identity pins, authorization and revocation policy separate from discovery. HTTPS and DNSSEC support also do not make the entire channel post-quantum secure.
Rotation limits
Agent-specific lookup selects the relevant agent's records and matches a candidate's raw digest. Multiple agents and overlapping keys can coexist; conflicting locations for the same agent and fingerprint are rejected. Publishing an old key does not certify that it remains authorized. JACS does not automatically publish DNS changes, maintain a transparency history, or provide a DNS revocation feed. Plan overlap around actual caches and retained signed documents, and retain exact historical-version key endpoints as needed.
See DNS publication, key rotation, and the current key-discovery guide.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Failure Modes
This page documents the error messages you will see when multi-agent agreements fail, plus the P2 compatibility and export failure table for operators. The legacy diagnostics below inspect supplied metadata and signatures; they do not authenticate consent policy. Even a mathematically valid v1 report returns complete=false and policy_accepted=false. Native v2 also remains inspection-only.
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. Additional signatures can be collected, but this does not turn unauthenticated legacy policy into authorization.
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: Treat this as a diagnostic of the supplied legacy quorum metadata, not an authenticated consent decision. 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 message varies by algorithm and the first failed check. Legacy checking evaluates supplied quorum/missing-signer metadata before individual signatures, so an incomplete document may report that earlier diagnostic even if a signature is also corrupted. Do not infer that unexamined signatures are valid.
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
sign_agreement stores the resulting document before returning. Storage errors make the operation fail; a separate later save() is not required to make a successful return persistent in the configured backend. An in-memory backend remains nonpersistent by design.
After an error, inspect the actual stored state before retrying or broadcasting. Do not assume a failed call committed a complete signature update.
P2 Compatibility and Export Failures
Beyond agreements, the P2 trust model (native signing with a post-quantum default, the ES256 compatibility key, and targeted ecosystem exports) emits structured log events with matching Prometheus counters. This is the sysadmin fix-table: what fired, which metric to alert on, which part of the model is involved, and the usual fix.
Note on the Metric column: the counters exist only in builds with the
otlp-metrics feature enabled — it is not in the default feature set of
the jacs crate or of cargo install jacs-cli-compat, so in a default build the
counters compile to no-ops. The log events are always emitted once a
tracing subscriber is installed (see
Observability).
| Log event | Metric | Failure class | Likely fix |
|---|---|---|---|
compatibility_key_missing (WARN) | jacs_compatibility_export_error_total{format,reason="missing_key"} | compatibility binding | An export was attempted (requested_export in the event; format on the metric) but the agent has no ES256 compatibility key — run jacs-compat agent add-compat-key. |
compatibility_key_unreadable (WARN) | — (log event only, no counter) | compatibility binding | A gate-and-enrich export (did, w3c-agent-identity) found the ES256 key state corrupt or inconsistent — jacs.keyring.json unparseable, or the keyring and the key files disagree (reason in the event). The export still succeeds in its native-only shape (a corrupt keyring must not take down DID serving), so this WARN is the only signal — inspect jacs.keyring.json and the jacs.ecosystem.* key files in the key directory. Distinct from compatibility_key_missing, which is the quiet never-migrated state. |
compatibility_binding_verify_failed (WARN) | jacs_compatibility_binding_verify_failed_total{reason} | compatibility binding | The native-root-signed binding no longer verifies (jacs_id + compat kid in the event) — re-issue after key rotation (jacs-compat agent issue-compat-binding) and check expiresAt. Fixed reason values: schema_invalid (also covers a jacsSha256 content-hash mismatch — the document lies about itself), signature_invalid, rotated_root, root_kid_mismatch, compat_kid_mismatch, jwk_mismatch, identity_mismatch, version_mismatch (identity discovery safely reissues an otherwise-authentic same-root stale-version binding), superseded (an older validly-signed binding was restored over a re-issue; latest issuedAt wins), and expired (also an unparseable expiresAt, which fails closed). |
content_export_scope_denied (WARN) | jacs_content_export_scope_denied_total{format} | ecosystem export | The binding (binding_hash in the event) lacks the requested scope (ap2-mandate, agreement-vc are never auto-issued) — re-issue the binding with that scope; the current native root signs the grant. |
compatibility_binding_created (INFO) | — (log event only, no counter) | compatibility binding | Success, not a failure — the current native root (re-)issued the compatibility key binding (jacs_id, compat kid, binding_hash, and granted scopes in the event). Expect one per jacs-compat agent issue-compat-binding or first identity-export auto-issue; its absence after a key rotation means the required re-issue has not happened yet. |
ecosystem_export_generated (INFO) | jacs_compatibility_export_total{format} | ecosystem export | Success, not a failure — six formats, exactly the six binding scopes: jwks, did, a2a-agent-card, w3c-agent-identity, ap2-mandate, agreement-vc. |
Metric label values are fixed, low-cardinality strings (reason,
format) — never raw error text.
Two boundary notes for dashboard authors:
- The
didandw3c-agent-identityexports are gate-and-enrich views: without a valid binding granting the scope they still succeed in their pre-P2 native-only shape and emit no event or counter. Only the authorized (compat-enriched) exports appear underjacs_compatibility_export_total. One exception to the silence: when the compat key state exists but is unreadable (corrupt keyring, missing key file), the native-only fallback emits thecompatibility_key_unreadableWARN above. - Exporting the compatibility key binding itself (
jacs-compat agent export-compat-binding) is the trust artifact behind the six scoped exports, not one of them — it logs a plain INFO line and never appears under theformatlabel.
See Also
- Creating and Using Agreements - Agreement creation and signing workflow
- Security Model - Overall security architecture, including the compatibility key binding lifecycle
- Key Rotation - Rotation, Ed25519 migration, and binding re-issue
- Cryptographic Algorithms - Algorithm details and signature verification
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Testing JACS Integrations
Test real signing and verification at the boundary where your application accepts data. Keep cryptographic validity, identity trust, authorization and replay policy as separate assertions.
A real Python fixture
An empty config does not create an agent or keys. Use an ephemeral agent for isolated tests:
import json
import pytest
from jacs.client import JacsClient
@pytest.fixture
def client():
return JacsClient.ephemeral()
def test_signed_result_and_tamper_rejection(client):
signed = client.sign_message({"answer": 42})
assert client.verify(signed.raw).valid
tampered = json.loads(signed.raw)
tampered["content"] = {"answer": 43}
assert not client.verify(tampered).valid
def test_invalid_json_is_rejected(client):
assert not client.verify("not JSON").valid
Install jacs and pytest, save this as test_signing.py, then run:
python -m pytest -q test_signing.py
New ephemeral agents default to pq2025. For persistent-agent tests use
JacsClient.quickstart(name=..., domain=..., config_path=...) in a private
temporary directory with a test-only password supplied through host secret
handling. An existing config must refer to a real agent and keys. Do not rely on
undocumented JACS_TEST_MODE or JACS_TEST_CONFIG switches.
Test the trust boundary
Cover valid signatures, changed payloads and metadata, malformed input, wrong keys, unexpected signer IDs, and missing signatures. For multiple agents, explicitly provision trusted public keys; creating two agents does not automatically enroll one in the other's trust store.
A passing signature test does not prove authorization. Test denied actions, revoked or expired trust, and mismatched purpose/audience separately. Legacy agreement inspection deliberately leaves consent-policy acceptance false; never write a test that grants permission merely because a signature count or claimed agreement status looks complete.
For request authentication, mutate method, URL/query, exact body bytes and audience independently. For replay protection, test duplicate delivery, expiry, cache/store failure and concurrent attempts against the same shared store. A process-local cache does not establish protection across workers.
Test framework behavior explicitly
Python FastAPI middleware checks signed POST JSON bodies. Unsigned or non-JSON
requests and other methods can pass through even with strict=True; test your
separate authentication requirement. Test signing failure, verification failure
and any explicitly enabled fallback without returning unmarked trusted data.
MCP tests should start the actual chosen server, negotiate a session, list tools,
call a tool and inspect its returned payload. The portable jacs mcp starts
verify-only by default. Python's signed local HTTP wrapper requires both peers
to use JACS wrappers and trust policy; a normal MCP client is not a substitute
in that transport test. Test unexpected peers and response correlation failures.
Await asynchronous Node APIs such as load() and createDocument().
See the maintained Python tests,
Node tests
and Go tests
for executable fixtures.
Mocks and release validation
Mocks help test application error handling, but cannot establish canonicalization, signature interoperability or key-resolution behavior. Retain real negative tests and cross-language fixtures. Test the installed package in a clean consumer separately from the source checkout; published artifacts may differ from later source fixes.
For repository changes, use the root contributor instructions and native development guide for current commands. Select focused tests first, then the required workspace and release gates. Keep trust stores, configuration, environment and filesystem state isolated; never use a production private key in tests.
See security, trust store, and custom evidence adapters.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
MCP Overview
Use MCP when the boundary is model-to-tool inside an application or local workstation. Use A2A when the boundary is agent-to-agent across organizations or services.
Choose The MCP Path
JACS offers three MCP integration surfaces:
- Run
jacs-compat mcpfor local document verification without signing keys. Select--profile local-signwith an existing signed config to let the local client sign as that agent. - Wrap an existing MCP transport when you already have an MCP server or client and want signed JSON-RPC.
- Register JACS as MCP tools when you want the model to call signing, verification, agreement, A2A, or trust operations directly.
Best Fit By Runtime
| Runtime | Best starting point | What it gives you |
|---|---|---|
| Rust CLI | jacs-compat mcp | Verification by default; explicit local signing of documents and Agreement-v2 artifacts, with optional scoped text/image tools |
| Python | jacs.mcp or jacs.adapters.mcp | Local SSE transport security or FastMCP tool registration |
| Node.js | @hai.ai/jacs/mcp | Transport proxy or MCP tool registration for existing SDK-based servers |
Important Constraints
- Signed transport wrappers are local-only. Python uses loopback SSE;
Node allows stdio or loopback transports and rejects
localOnly: false. - Wrap both ends. Signed carriers need matching JACS wrappers and independently selected peer IDs/keys; a stock MCP client cannot authenticate them unaided. Ordinary Rust tool servers and tool registration can use standard MCP clients.
- 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 loadedJacsClientorJacsAgent;createJACSTransportProxyAsync()is the config-path variant. - The native server's profiles do not configure the Python or Node adapters. Those integrations have their own transport, key-loading and authorization requirements; the examples below do not inherit the native server's closed local-sign inventory.
- Signatures are provenance, not human approval. Supplied-key verification checks integrity; it does not establish truth, identity, authorization, publication permission or Agreement policy acceptance.
1. Ready-Made Server: jacs-compat mcp
Install the unified binary and start a verification-only server. No identity creation or private-key password is required:
cargo install jacs-cli-compat --locked
jacs-compat mcp
Configure a compatible MCP client with:
{
"mcpServers": {
"jacs": {
"command": "jacs-compat",
"args": ["mcp"]
}
}
}
The default verify-only profile exposes only jacs_verify_document; callers
supply the public key and algorithm. An explicitly supplied --config or
JACS_CONFIG loads public-only configuration without unlocking private keys.
To sign, reuse an existing signed config, or run jacs-compat init once in the
directory where you want to keep the agent identity. Keep using its normal
keychain/password source and select it explicitly:
jacs-compat mcp --profile local-sign --config /absolute/path/to/jacs.config.json
Use those same arguments in the MCP client:
{
"mcpServers": {
"jacs": {
"command": "jacs-compat",
"args": ["mcp", "--profile", "local-sign", "--config", "/absolute/path/to/jacs.config.json"]
}
}
}
JACS_CONFIG can supply the config path instead. Local signing requires a
valid signed config and persistent encrypted keys in filesystem storage; it
does not discover config implicitly or create a replacement identity. Never
send a password as a tool argument or embed it in client JSON. Prefer an
owner-readable JACS_PASSWORD_FILE or the configured OS keychain.
This profile exposes nine JSON/Agreement-v2 tools when Agreement tools are compiled. To add exactly five text/image tools, grant an existing content directory explicitly at process startup:
JACS_MCP_BASE_DIR=/absolute/path/to/content jacs-compat mcp --profile local-sign --config /absolute/path/to/jacs.config.json
There is no implicit working-directory grant. File tools reject traversal and
symlinks and stay beneath the selected directory. Replacing a distinct existing
output requires JACS_MCP_OVERWRITE_OK=1; explicit in-place signing remains
available. Normal backups contain the original plaintext file, so protect them
alongside the content. A content root never expands verify-only.
Signing persists ordinary JACS documents under the config directory's
documents/ directory and grants agent signing authority, not human approval.
An explicit --profile overrides JACS_MCP_PROFILE; unknown values fail
startup. Historical core/full recipes are obsolete; trust-admin and
compatibility-only legacy-core remain unavailable. Key administration,
trust management, A2A and attestation tools are not enabled by local-sign.
The compiled 42-tool contract is not a runtime grant: use tools/list to see
the process's actual authorized inventory.
The native server uses stdio only: stdout carries MCP JSON-RPC and diagnostics
go to stderr. It uses RMCP 3.3.0, supporting date-versioned MCP 2026-07-28
and legacy 2025-11-25 initialization; these are distinct from SDK major
versions such as the TypeScript SDK v2. See the
crate MCP README
for the complete local-signing and response-error contract.
2. Transport Security Around Your Existing MCP Code
Python
Use jacs.mcp when you already have a FastMCP server or client and want transparent signing around the SSE transport:
from fastmcp import FastMCP
from jacs.mcp import JACSMCPServer
mcp = JACSMCPServer(
FastMCP("Secure Server"),
"./jacs.config.json",
allowed_peer_agent_ids=["CLIENT_AGENT_ID"],
)
For clients:
from jacs.mcp import JACSMCPClient
client = JACSMCPClient(
"http://localhost:8000/sse",
"./jacs.config.json",
expected_peer_agent_id="SERVER_AGENT_ID",
)
Helpful utilities in the same module:
create_jacs_mcp_server()for a one-line FastMCP serverjacs_middleware()for explicit Starlette middleware wiringjacs_call()for one-off authenticated local calls
See Python MCP Integration for the detailed patterns.
Node.js
Source and published packages: Version 0.16.1 has a recorded release inventory. Version 0.16.0 is the first release whose source includes the native PQ DID/JWK and Agent Card behavior. See the identity integration guide.
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', {
allowedPeerAgentIds: ['CLIENT_AGENT_ID'], // Independently selected peer.
});
If you only have a config path:
import { createJACSTransportProxyAsync } from '@hai.ai/jacs/mcp';
const secureTransport = await createJACSTransportProxyAsync(
transport,
'./jacs.config.json',
'server',
{ allowedPeerAgentIds: ['CLIENT_AGENT_ID'] },
);
See Node.js MCP Integration for examples and tool registration.
3. Register JACS Operations As MCP Tools
This is different from transport security. Here the model gets explicit MCP tools such as jacs_sign_document, jacs_verify_document, agreement helpers, and trust helpers.
Python
from fastmcp import FastMCP
from jacs.client import JacsClient
from jacs.adapters.mcp import (
register_jacs_tools,
register_a2a_tools,
register_trust_tools,
)
client = JacsClient.quickstart(name="mcp-agent", domain="mcp.local")
mcp = FastMCP("JACS Tools")
register_jacs_tools(mcp, client=client)
register_a2a_tools(mcp, client=client)
register_trust_tools(mcp, client=client)
Node.js
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { JacsClient } from '@hai.ai/jacs/client';
import { registerJacsTools } from '@hai.ai/jacs/mcp';
const server = new Server(
{ name: 'jacs-tools', version: '1.0.0' },
{ capabilities: { tools: {} } },
);
const client = await JacsClient.quickstart({
name: 'mcp-agent',
domain: 'mcp.local',
});
registerJacsTools(server, client);
Choose the native jacs-compat mcp server for its explicit local-signing boundary.
Tool registration in an existing Node or Python server is a separate
integration; review that server's exposed operations and authorization.
Provenance MCP tools
With local-sign and an explicit JACS_MCP_BASE_DIR, the native server adds
the following tools for inline text and image provenance. They cover the
same surface as the sign-text and
sign-image CLI verbs:
jacs_sign_text— append a YAML-bodied signature block to a markdown / text file.jacs_verify_text— verify all signature blocks in a file (permissive default;strictopt-in).jacs_sign_image— embed a signature in PNG iTXt / JPEG APP11 / WebP XMP.jacs_verify_image— verify the embedded image signature (permissive default;strictopt-in).jacs_extract_media_signature— read out the embedded signature payload (decoded JSON by default;raw_payloadopt-in for the base64url wire form).
Examples
Use the complete Python MCP guide or Node MCP guide for matched peers and host-held secrets. The repository's historical example scripts are not a replacement for those current authentication requirements.
Related Guides
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
A2A Interoperability
The default discovery adapters retain the legacy A2A 0.4.0 card shape.
The separate typed jacs::a2a::v1 API uses the A2A 1.0 shape for a bounded
JSON-RPC profile; see the
current source quickstart
and the v1 example below.
Source and published packages: Version 0.16.1 has a recorded release inventory. Version 0.16.0 is the first release whose source includes the native PQ DID/JWK and Agent Card behavior. See the identity integration guide.
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-taskora2a-message - Trust policy for deciding whether another agent is acceptable
- Chain of custody via parent signatures
The Core Flow
1. Create An Unsigned Card Model
These convenience methods return unsigned models. For signed discovery use the serving APIs below or the A2A how-to.
Python:
from jacs.client import JacsClient
client = JacsClient.quickstart(name="my-agent", domain="my-agent.example.com")
card = client.export_agent_card()
Node.js:
import { JacsClient } from '@hai.ai/jacs/client';
const client = await JacsClient.quickstart({
name: 'my-agent',
domain: 'my-agent.example.com',
});
const card = client.exportAgentCard();
2. Serve Discovery Documents
Python and Node provide discovery-only server helpers. These mounts publish
well-known documents; they do not implement A2A message/task handling or the
legacy optional /jacs/sign and /jacs/verify host endpoint examples in the
extension descriptor. A host application must implement and authorize any
callable interface separately. Discovery does not grant remote signing access.
Quick demo server:
from jacs.a2a import JACSA2AIntegration
JACSA2AIntegration.quickstart(
name="my-agent",
domain="my-agent.example.com",
).serve(port=8080)
Production FastAPI mounting:
from jacs.a2a_server import create_a2a_app, jacs_a2a_routes
app = create_a2a_app(client, title="My A2A Agent")
# or:
# app.include_router(jacs_a2a_routes(client))
Node.js has two discovery helpers:
client.getA2A().listen(port)for a minimal demo serverjacsA2AMiddleware(client, options)for mounting discovery routes in an existing Express app
import express from 'express';
import { jacsA2AMiddleware } from '@hai.ai/jacs/a2a-server';
const app = express();
app.use(jacsA2AMiddleware(client));
app.listen(3000);
The local listening port does not rewrite the signed public interface URL. Configure the advertised domain before generating the signed card and provide the host service that interface describes. A reachable discovery route alone does not prove that message/task operations exist.
3. Sign And Verify Artifacts
Python:
signed = client.sign_artifact({"artifactId": "a-1", "operation": "classify"}, "artifact")
result = client.get_a2a().verify_wrapped_artifact(signed)
assert result["valid"]
Node.js:
const signed = await client.signArtifact(
{ artifactId: 'a-1', operation: 'classify' },
'artifact',
);
const result = await client.verifyArtifact(signed);
console.log(result.valid);
Both SDKs require the canonical native A2A verifier for an affirmative result.
The generic verify_response() / verifyResponse() document API is not an A2A
fallback: even a literal success boolean yields an invalid result with blank
signer, version, type, timestamp, and payload provenance, and cannot elevate
trust. This prevents parsed attacker-controlled wrapper fields from being
presented as authenticated A2A evidence.
The Node signing wrapper also validates the native return contract before releasing it: the document must carry complete portable-v2 signature metadata, its payload and type must match the request, and its ordered parent chain must exactly match the caller's input. If no parents were requested, only an absent or empty returned chain is accepted. This prevents a malformed native result from injecting, dropping, reordering, or changing chain-of-custody evidence.
Trust Policies
Trust policy answers a different question from cryptographic verification.
- Trust policy: should this remote agent be admitted?
- Artifact verification: is this specific signed payload valid?
The current policy meanings are:
| Policy | Behavior |
|---|---|
open | Accept all agents without verification |
verified | Require a valid Agent Card JWS against the same-origin JWKS and a durable jacsId:jacsVersion key pin (default). This is origin/key continuity, not proof of the claimed native JACS identity. |
strict | Require an explicitly trusted native JACS root. Direct ML-DSA-87 cards must use that exact key. ES256 cards must also carry the fixed-path compatibility binding, whose native signature, identity/version, binding hash, compatibility JWK/kid, scope, expiry, and signed issuance time all verify. Bindings are accepted for at most seven days after issuedAt (with five minutes of future clock skew), and the verifier durably pins the latest observed hash, kid, and issuedAt; a newer root-signed binding advances the pin and older replays fail closed. |
Lifecycle boundary: The absolute seven-day freshness check also applies on first contact, independently of
expiresAt. Local discovery generation refreshes an authentic binding after six days under the shared issuance lock, preserving its scopes and explicit expiry. Once a newer native-root-signed binding is observed, JACS also rejects rollback to the old hash/kid. Use an earlierexpiresAtwhen old compatibility keys must stop working sooner, and distribute the refreshed binding promptly after rotation or compromise.
verified means the card signature and a durable same-origin key pin verified.
It proves origin/key continuity, not that the self-asserted jacsId is a
real-world or explicitly trusted identity. strict is the identity-bound mode.
Direct-PQ verification checks the version/root-key metadata before TOFU pinning
and now uses raw-key SHA-256 fingerprints. Pins from an earlier unpublished
direct-PQ build may use a lossy fingerprint; they fail closed and are not
automatically reset. Independently verify the identity and exact root before
deliberately replacing such a pin. ES256 pins and explicit trusted-root checks
are unchanged. Native artifact-envelope key discovery
is a separate verification path, not an automatic A2A policy upgrade.
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.
Native post-quantum discovery
Current source defaults to the persisted native ML-DSA-87 root for
generate_well_known_documents. It publishes that root's RFC 9964 AKP JWK and
five discovery documents, with no delegated-key binding. The card's JWS signs
canonical card bytes and its metadata.jacsRootKid identifies the root's JWK
thumbprint. Loading or exporting does not create another signing key. The
legacy card wire shape remains unchanged.
Explicit ES256 compatibility
Agent cards can also be exported with an ES256 signature from the agent's
compatibility key (export_a2a_agent_card in the Rust core and bindings,
feature a2a). This is a card-typed exporter, not a generic JWS API:
- The JWS protected header is pinned to
{"alg": "ES256", "typ": "JOSE", "kid": "<compat-key kid>"}—typisJOSE, notJWT. - The export is gated by the
a2a-agent-cardscope of the native-root-signed compatibility key binding (an identity scope, granted by default). - The card's
metadatacarriesjacsCompatKid,jacsCompatBindingHash, and the fixed same-originjacsCompatBindingPath(/.well-known/jacs-compat-binding.json).
A stock JOSE verifier can check the ES256 signature against the agent's
compatibility JWKS (jacs-compat agent export-jwks), but that proves possession of the
compatibility key only. Tracing the card to the agent's native root requires
verifying the native-root-signed compatibility key binding
(jacs-compat agent export-compat-binding). Calling native
generate_well_known_documents with explicit ES256 publishes the binding at
the fixed path and reuses the same persisted ES256 key
across calls and restarts. An obsolete explicit ring-Ed25519 discovery-key
choice is rejected rather than silently ignored — see the
Security Model.
Under strict, the verifier checks the trusted native root signature, expected
jacsId and jacsVersion, binding content hash, compatibility kid and JWK,
the a2a-agent-card scope, expiry, and a signed issuedAt no more than seven
days old or five minutes in the future. This absolute freshness check applies
on first contact; local discovery generation refreshes an authentic binding at
six days while preserving its scopes and explicit expiry. A self-advertised
JWKS key by itself can never establish the claimed JACS identity.
Current Runtime Differences
- Python and Node.js: the FastAPI and Express routers serve five native
discovery documents by default, including the signed card and JWKS. Explicit
ES256 adds a compatibility binding. They are discovery-only;
serve()andlisten()are convenience hosts, not complete A2A task/message services. - The mounted routers refresh their complete snapshots lazily at six days from local generation (direct root) or signed binding issuance (ES256). HTTP freshness ends at renewal or earlier explicit expiry; separate fetches can still straddle replacement. Verify the related documents together and refetch on a mismatch. See Serve Your Agent Card.
Example Paths In This Repo
archive/native/jacspy/tests/test_a2a_server.pyarchive/native/jacsnpm/src/a2a-server.jsarchive/native/jacsnpm/examples/a2a-agent-example.jsarchive/native/jacs/tests/a2a_cross_language_tests.rs
Opt-in A2A 1.0 peer example
Current source's typed jacs::a2a::v1::export_agent_card signs directly with the
loaded ML-DSA-87 root. It uses the A2A 1.0 supportedInterfaces model and
standard detached signatures[].protected / signature fields, with typed
protobuf presence handling and RFC 8785 canonicalization. Its
verify_root_card verifies the signature, exact trusted root and expected
identity/version for the supported profile. Multiple signatures are accepted:
the helper selects the expected root kid regardless of order, rejects
duplicate kids or invalid entry/header shapes, and grants no authority to the
other signatures. It does not cryptographically verify those other entries or
fetch keys over the network. See the
source library guide.
export_agent_card_es256 is the explicit compatibility alternative. The
retained native-fixture/Python vector harness selects ES256. The current
JavaScript SDK smoke instead tests direct ML-DSA-87; the current Python SDK
smoke uses explicit ES256 plus a separately verified native-root binding. verify_card_binding remains the
compatibility binding check, and its caller must additionally verify the JWS.
Public Node and Python discovery APIs accept explicit A2A 1.0 options with
caller-provided skills, input/output modes and a messaging URL. Without those
options, language discovery remains the legacy 0.4.0 format. See the
current public API examples.
The current official SDK smokes exercise those public APIs with stock JavaScript SDK 1.3.0 (direct ML-DSA-87, Node 24.7+) and stock Python SDK 1.3.0 (explicit ES256 compatibility, Python 3.11+). Each uses actual SDK discovery, signature verification and a finite JSON-RPC report exchange. SDK version and wire version are different numbers. The Python backend does not provide ML-DSA verification.
The broader retained native-fixture/Python vector harness also uses the exact
a2a-sdk==1.3.0 registry package and hashed transitive dependencies. From the
repository root, with Python 3.11+ and uv installed:
uv venv --python python3 /tmp/jacs-a2a-peer-env
uv pip sync --python /tmp/jacs-a2a-peer-env/bin/python archive/native/examples/a2a_v1_peer_requirements.txt
cargo build --locked --manifest-path archive/native/Cargo.toml -p jacs \
--no-default-features --features a2a --example a2a_v1_peer --target-dir target/a2a-v1-example
/tmp/jacs-a2a-peer-env/bin/python archive/native/examples/a2a_v1_peer.py \
--native-bin target/a2a-v1-example/debug/examples/a2a_v1_peer
A disposable native helper creates an encrypted signing identity, a card,
its native-root-signed compatibility binding, and a pre-signed synthetic report.
It removes its private workspace before a separate recipient starts. A finite
loopback host serves the card and a real SDK JSON-RPC interface. The official
client follows that returned interface and receives the report as raw
application/json artifact bytes. The recipient checks exact bytes, uses the
native public-key-only verifier for the document, and separately validates the
native binding against an explicitly supplied trusted root before the official
SDK verifies the ES256 card signature. No email or external network is used.
The new profile puts native identity and binding references in
urn:jacs:provenance-v1 extension parameters, using the current card's standard
signature fields. It preserves explicitly supplied optional false booleans and
omits absent optional fields and ordinary proto defaults. Required strings and
arrays must be nonempty. It rejects caller-supplied binding parameters,
duplicate extension URIs, unsupported capability claims, non-object extension
parameter roots, and empty/null or unsafe numeric extension values instead of silently changing them. Production
interface URLs require HTTPS; explicit loopback HTTP is permitted for this
example. The caller must implement every declared skill; the example implements one report skill.
This deliberately bounded profile records a pinned-peer limitation: its canonicalizer removes explicit optional empty strings, although the protocol's presence rules preserve them. The peer is not patched. Unsupported values are rejected by the new typed boundary, and the example does not claim general A2A conformance. It tests independent SDK card/JWS handling and message transport; native document and root-binding verification still use JACS. The existing legacy Strict discovery parser is not a v1 parser, and this stateless bridge does not establish rollback history. Key attribution, human approval, current contact authority and Agreement-policy acceptance remain separate decisions.
Historical verification on September 15, 2026 used macOS arm64, Rust 1.98.1, Python
3.14.7, SDK 1.1.4 at commit 2d4d3048b245d2af854bad804f0e722ea9febc08,
and the reviewed Rustls 0.23.45 lock. The native profile
regression and existing legacy discovery/restart regression passed. The actual
peer verified three card vectors and received all 1,201 report bytes unchanged;
14 peer/native refusals and seven typed-profile refusals passed, along with
private-workspace and host cleanup. This is selected-feature (a2a, without
default features) example evidence, not an all-platform or general-conformance
certification.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
DID integration
JACS exports did:wba identifiers and public DID documents using the persisted
native signing identity. It has no general-purpose DID resolver. See the
current identity guide
for source installation, ML-DSA-87 JWK exports, checked standards and release
boundaries. The extended CLI is named jacs-compat:
jacs-compat w3c did --origin https://agent.example.com
jacs-compat w3c did-document --origin https://agent.example.com
These commands load the existing native agent configuration. The DID is
did:wba:<host>:agent:<jacsId> and its document is hosted at
/agent/<jacsId>/did.json. The domain comes from configured jacs_agent_domain
or an explicit --origin; publish under an origin you control. The exporter
does not prove that control or deploy the document.
Current source represents the native pq2025 root as a JsonWebKey verification
method with an RFC 9964 ML-DSA-87 AKP publicKeyJwk. DID v1 and Controlled
Identifiers v1 contexts describe that representation. Exporting a different
format does not rotate the key or create another root identity. A valid
signature must still be tied to independently trusted identity evidence and
application authorization. No blockchain is required.
Generic did:web and did:key mappings are possible integration designs;
they are not implemented exporters in this library.
ES256 compatibility entries
When the agent has an ES256 compatibility key and a valid
native-root-signed compatibility key binding granting the did scope,
generated DID documents (jacs-compat w3c did-document) include two
verification-method entries for that key:
- a
JsonWebKeyentry (publicKeyJwk) for JOSE consumers, and - a
Multikeyentry (publicKeyMultibase) — the formecdsa-jcs-2019Data Integrity proofs reference.
If the compatibility key is absent or the binding is missing, invalid, or
lacks the did scope, the DID document is still exported — it simply
omits the ES256 entries. The ES256 entries prove possession of the
compatibility key only; native-root trust comes from the binding
(jacs-compat agent export-compat-binding) — see the
Security Model.
JACS-Specific Request Proofs
The request-proof helpers (jacs_w3c_sign_request and
jacs_w3c_verify_request in the compiled MCP inventory, backed by the Rust
w3c::auth module) use an experimental JACS-specific JSON format:
type: "JacsW3cRequestProof", scheme: "DIDWba". This label does not make the
JSON object an ANP HTTP Message Signatures request or demonstrate interoperability
with an ANP peer. Existing helper names and fields describe the current JACS
contract; an ANP wire adapter is a separate integration.
The proof signs canonical request fields, including method, target, claimed creation time, nonce and an optional body digest. Verification checks the supplied DID document and signing key, timestamp policy and nonce use. To bind verification to the request actually received, provide its expected method and URL and its body; inspecting only the proof's own method and URL does not establish that match. A supplied DID document is not, by itself, trusted identity evidence or permission to act. Apply trusted resolution and current authorization policy at the receiving application boundary.
The request-proof verifier infers ML-DSA-87 or Ed25519 from a standard public
JWK. An optional jacsSigningAlgorithm must agree with that key. DID document
method IDs and authentication references may use absolute local DID URLs or
#fragment references, with matching controller and no ambiguous IDs. The
proof itself must name an absolute local DID URL. Scoped ES256 entries are not
supported by this JACS-specific request-proof protocol.
JACS's ML-DSA-87 public AKP profile uses a thumbprint kid; Ed25519 DID exports
retain the existing method URL and use its fragment as kid. CID thumbprint
naming is recommended, not a requirement to rename every existing method.
Where to Combine With Other Standards
- A2A Interoperability: cross-organization agent discovery and artifact exchange
- MCP Overview: signed tool invocation flows
- DNS-Based Verification: decentralized public key anchoring
- Storage Backends: durable storage for signed artifacts and identity metadata
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
AP2 (Agent Payments Protocol)
JACS can export the merchant/business-authorization artifact of the
UCP AP2-Mandates extension (dated spec revision 2026-01-23) as a
detached ES256 JWS, signed with the agent's ecosystem_signing
compatibility key.
Scope is deliberately narrow:
- Exported role: merchant/business authorization only. User-side AP2 Checkout Mandates are SD-JWT-VC and are not produced by JACS — this is not full AP2 conformance, and the docs do not claim it.
- Outgoing only. JACS does not verify incoming AP2 mandates.
- CLI + binding only. There is no MCP tool for this export.
Wire format (pinned)
- Protected header: exactly
{"alg":"ES256","kid":"<compat-key kid>"}— notyp, no RFC 7797b64:false, nocrit. - Detached compact serialization:
<BASE64URL(header)>..<BASE64URL(signature)>. - Signing input:
BASE64URL(header) + "." + BASE64URL(JCS(payload)), where the payload is the checkout object excluding theap2field, canonicalized per RFC 8785 (JCS).
The exporter validates its input against the named schema
jacs/schemas/compatibility/ap2-mandate/v1/ap2-mandate.schema.json and
rejects anything that does not conform — typed input is a schema check,
not a naming convention.
The schema is deliberately open (additionalProperties: true) to match
UCP checkout extensibility: extra checkout fields beyond the required
core are signed as-is, so relying parties must not assume JACS validated
anything beyond the schema's required core fields. Operators granting the
ap2-mandate scope are authorizing signatures over extensible checkout
payloads.
Usage
The ap2-mandate binding scope is a content scope: it is never
auto-issued. Grant it explicitly first (this requires the native root's
signature):
jacs-compat agent issue-compat-binding \
--scopes jwks,did,a2a-agent-card,w3c-agent-identity,ap2-mandate
# inline JSON, a file path, or '-' for stdin all work:
jacs-compat ap2 export-mandate --input checkout.json > export.json
cat checkout.json | jacs-compat ap2 export-mandate --input - > export.json
The export carries the detached JWS, the checkout with
ap2.merchant_authorization filled in, and the binding reference as a
content hash.
Verification
Any stock JOSE verifier can check the ES256 signature against the agent's JWKS — see the committed reference verifier:
jacs-compat agent export-jwks > jwks.json
npm install jose canonicalize
node scripts/smoke/verify_ap2_jws.mjs export.json jwks.json
Classical verification is not native-root trust. The ES256 check proves
possession of the compatibility key only. To trace the mandate to the
agent's native root, a JACS-aware relying party additionally verifies the
native-root-signed compatibility key binding
(jacs-compat agent export-compat-binding) whose content hash the export
references. Native JACS documents are never touched by this export —
signing a mandate does not create or modify any jacsSignature.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
A2A Quickstart
Start with the A2A how-to for source installation, local discovery, A2A 1.0 export and a real official-SDK message exchange. Direct PQ A2A signing first shipped in the published 0.16.0 packages; see the release inventory.
| Guide | Task |
|---|---|
| Serve | Publish signed Agent Cards and public discovery documents |
| Discover and trust | Assess a remote legacy card against an explicit policy |
| Exchange | Sign artifacts and verify their linked parent signatures |
Cards, keys and handlers
Default native discovery exports the legacy 0.4.0 card format. Explicit A2A 1.0 options produce typed cards with detached signatures; the interoperability reference describes the supported profile and official SDK tests. SDK version numbers and wire protocol versions are separate.
The persisted ML-DSA-87 root signs discovery by default: five public documents, with no compatibility binding required. Explicit ES256 uses a persisted compatibility key and adds a sixth, root-signed binding document. Agent creation may generate that optional key; loading or exporting does not mint an unrelated signer.
The discovery servers do not implement message/task handlers. The host must implement and authorize the advertised service. A2A already defines card signatures; JACS adds native-root attribution, signed artifact envelopes and explicit verification policies.
Sign an artifact
from jacs.client import JacsClient
client = JacsClient.quickstart(name="my-agent", domain="my-agent.example.com")
signed = client.sign_artifact({"result": "ready"}, "result")
The wire payload is signed["a2aArtifact"]; jacsType is a2a-result here. Extracting the payload discards its signed envelope, so a recipient must verify before relying on provenance.
Troubleshooting
- Valid signature, rejected policy: independently enroll the full native agent document and public key before using
strict. A self-advertised card cannot enroll itself. - No agent loaded: create/load a client before signing or discovery assessment. Different local agents need different
config_path/configPathvalues. - Missing skills: provide skills before generating signed discovery. Post-signing overrides are rejected.
- Missing signer key: native key resolution defaults to
local,registry; setJACS_KEY_RESOLUTION=localfor offline lookup. Opt-in DNS discovery fetches the key over HTTPS and accepts it only if it matches the DNS fingerprint (key discovery). Obtaining a key is separate from trusting it. - Python installation: published 0.16.1 wheels and current source require Python 3.11–3.14. The
a2a-serverextra adds FastAPI and uvicorn.
Continue with trust-store enrollment and the A2A reference.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Serve Your Agent Card
By default, these discovery helpers export the legacy A2A 0.4.0 card shape. For the
explicit A2A 1.0 options and their limits, see the
current quickstart.
Source and published packages: Version 0.16.1 has a recorded release inventory. Version 0.16.0 is the first release whose source includes the native PQ DID/JWK and Agent Card behavior. See the identity integration guide.
Make your JACS agent discoverable by other A2A agents.
These helpers serve discovery documents only. They do not implement message/task
handlers or the legacy optional /jacs/sign and /jacs/verify host examples in
the extension descriptor. Configure the signed public interface for a service
your host actually implements; a local discovery listener does not create that
service or grant remote signing access.
Prerequisites: The current-source PQ examples require source-built bindings, plus Python's
a2a-serverextra or Express for Node.js. Registry installation alone does not establish that these newer exports are available. Node.js snippets below run inside an async function.
from jacs.a2a import JACSA2AIntegration
JACSA2AIntegration.quickstart(name="my-agent", domain="my-agent.example.com").serve(port=8080)
Your agent is now discoverable at http://localhost:8080/.well-known/agent-card.json.
This local listener does not rewrite the card's signed public interface URL.
Production: Mount into Your Own FastAPI App
from fastapi import FastAPI
from jacs.client import JacsClient
from jacs.a2a_server import jacs_a2a_routes
app = FastAPI()
client = JacsClient.quickstart(name="my-agent", domain="my-agent.example.com")
router = jacs_a2a_routes(client)
app.include_router(router)
const express = require('express');
const { JacsClient } = require('@hai.ai/jacs/client');
const { jacsA2AMiddleware } = require('@hai.ai/jacs/a2a-server');
const client = await JacsClient.quickstart({
name: 'my-agent',
domain: 'my-agent.example.com',
});
const app = express();
app.use(jacsA2AMiddleware(client));
app.listen(8080);
Your agent is now discoverable at http://localhost:8080/.well-known/agent-card.json.
What Gets Served
The native generator returns five public discovery documents by default. Explicit ES256 compatibility adds the sixth, a delegated-key binding:
| Endpoint | Purpose |
|---|---|
/.well-known/agent-card.json | A2A Agent Card with JWS signature |
/.well-known/jwks.json | Stable ML-DSA-87 root JWK; ES256 only when explicitly selected |
/.well-known/jacs-compat-binding.json | Only for explicit ES256 compatibility: native-root-signed binding to the claimed JACS identity |
/.well-known/jacs-agent.json | JACS agent descriptor |
/.well-known/jacs-pubkey.json | JACS public key |
/.well-known/jacs-extension.json | JACS 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.
Current source signs the card with the persisted ML-DSA-87 root and publishes
its AKP JWK. The key remains stable across calls and restarts; randomized
signatures can differ. This path needs no compatibility key or binding.
For a peer requiring ES256, select a2a_algorithm="ES256" in the Python
jacs_a2a_routes / create_a2a_app options or a2aAlgorithm: "ES256" in
Node's jacsA2AMiddleware options. The existing compatibility path uses a
persisted ES256 key and a native-root-signed binding. Provision that key with
jacs-compat agent add-compat-key when needed; loading alone never creates it.
Strict verification of a direct root card requires the exact explicitly trusted
native key. For ES256 it additionally verifies the scoped compatibility binding
referenced by fixed path and content hash. Local loopback trust tests require
JACS_ALLOW_PRIVATE_JWKS=true; private-address JWKS fetching is otherwise denied.
Mounted routers refresh complete snapshots lazily after six days: measured from
local generation for direct root cards, or signed issuedAt for ES256 bindings.
The seven-day snapshot serving limit for a direct root card is a local cache
policy, not a signed expiry or revocation signal. For ES256, strict verifiers
accept a binding for at most seven days after issuedAt, with five minutes of
future skew. Generation refreshes an authentic binding after six days while
preserving its scopes and explicit expiresAt.
HTTP freshness ends by renewal or earlier expiry; failed renewal can serve a
still-valid snapshot with no-store, while hard expiry returns 503. An explicit
binding expiry requires renewed authorization and remounting. Separate resource
fetches can straddle replacement; verify related documents together and refetch
if their references differ.
Next Steps
- Discover & Trust Remote Agents -- Find other agents and assess their trustworthiness
- Exchange Signed Artifacts -- Sign and verify A2A artifacts
- A2A Interoperability Reference -- Full API reference
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Discover & Trust Remote Agents
Find other A2A agents and apply an explicit admission policy. Node.js snippets below run inside an async function. Remote evidence variables denote values obtained through an independently authenticated channel.
from jacs.a2a_discovery import discover_and_assess_sync
from jacs.client import JacsClient
client = JacsClient.quickstart(name="my-agent", domain="my-agent.example.com")
result = discover_and_assess_sync("https://agent.example.com", client=client)
if result["allowed"]:
print(f"Trusted: {result['card']['name']} ({result['trust_level']})")
Add to Your Trust Store
For strict policy, establish native JACS trust from an authenticated agent
document and explicit native public key. An Agent Card is self-advertised and
cannot create strict identity trust by itself:
from jacs.client import JacsClient
from jacs.a2a import JACSA2AIntegration
client = JacsClient.quickstart(name="my-agent", domain="my-agent.example.com")
a2a = JACSA2AIntegration(client, trust_policy="strict")
# Enroll independently authenticated evidence before assessment
a2a.trust_a2a_agent(remote_native_agent_json, remote_native_public_key_pem)
# 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']}")
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 { JacsClient } = require('@hai.ai/jacs/client');
const client = await JacsClient.quickstart({ name: 'my-agent', domain: 'my-agent.example.com' });
const result = await discoverAndAssess('https://agent.example.com', { client });
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');
// Enroll independently authenticated evidence before assessment.
a2a.trustA2AAgent(remoteNativeAgentDocument, remoteNativePublicKeyPem);
// Assess a remote agent
const assessment = a2a.assessRemoteAgent(remoteCardJson);
console.log(`JACS registered: ${assessment.jacsRegistered}`);
console.log(`Allowed: ${assessment.allowed}`);
Trust Policies
| Policy | Behavior |
|---|---|
open | Accept all agents without verification |
verified | Require a valid Agent Card JWS against the same-origin JWKS and a durable jacsId:jacsVersion key pin (default). This is origin/key continuity, not proof of the claimed native JACS identity. |
strict | Require an explicitly trusted native JACS root. Direct ML-DSA-87 cards must use that exact key. ES256 cards must also carry the fixed-path compatibility binding, whose native signature, identity/version, binding hash, compatibility JWK/kid, scope, expiry, and signed issuance time all verify. Bindings are accepted for at most seven days after issuedAt (with five minutes of future clock skew), and the verifier durably pins the latest observed hash, kid, and issuedAt; a newer root-signed binding advances the pin and older replays fail closed. |
Lifecycle boundary: The absolute seven-day freshness check also applies on first contact, independently of
expiresAt. Local discovery generation refreshes an authentic binding after six days under the shared issuance lock, preserving its scopes and explicit expiry. Once a newer native-root-signed binding is observed, JACS also rejects rollback to the old hash/kid. Use an earlierexpiresAtwhen old compatibility keys must stop working sooner, and distribute the refreshed binding promptly after rotation or compromise.
How Trust Flows
1. Discover -- Fetch /.well-known/agent-card.json from a remote URL
2. Assess -- Check for JACS extension, verify signatures
3. Decide -- Trust policy determines if the agent is allowed
4. Authorize -- Apply application policy; strict enrollment precedes assessment
With open, all agents pass step 3 without identity assurance. With
verified, the Agent Card JWS must verify against the same-origin JWKS and the
signing key must match its durable TOFU pin; this proves origin/key continuity,
not native identity. With strict, the native root must be explicitly trusted
and a directly signed card must use that exact root key. ES256 compatibility
additionally requires a signed binding authorizing the exact card key, identity,
version, scope, and current binding hash. These policies parse the legacy card
format; they are not an A2A 1.0 verifier.
Next Steps
- Exchange Signed Artifacts -- Sign and verify artifacts with trusted agents
- Serve Your Agent Card -- Make your agent discoverable
- Trust Store -- Managing the local trust store
CLI a2a discover is an inspection command: --json skips trust assessment, and a disallowed assessment can still exit successfully. Use the assessor's explicit allowed result for admission; an exit code or fetched card is not authentication.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Exchange Signed Artifacts
Sign artifacts with cryptographic provenance and verify artifacts from other agents. Node.js snippets run inside an async function. For multi-agent examples, create separate clients/configurations and independently authenticate each signer's public evidence and configure the verifier's key resolver before verification. Native artifact verification uses a trust-store enrollment for v2 artifacts signed by the exact enrolled agent_id:version; otherwise it uses its local public-key cache or opt-in registry or DNS/HTTPS key discovery. See cross-agent setup.
Sign and Verify
from jacs.client import JacsClient
client = JacsClient.quickstart(name="my-agent", domain="my-agent.example.com")
# Sign an artifact
signed = client.sign_artifact({"action": "classify", "input": "data"}, "artifact")
# Verify it (with trust assessment)
a2a = client.get_a2a()
result = a2a.verify_wrapped_artifact(signed, assess_trust=True)
print(f"Valid: {result['valid']}, Allowed: {result['trust']['allowed']}")
Chain of Custody
When multiple agents process data in sequence, link artifacts into a verifiable chain:
# Agent A signs step 1
step1 = client_a.sign_artifact({"step": 1, "data": "raw"}, "artifact")
# Agent B signs step 2, referencing step 1 as parent
step2 = client_b.sign_artifact(
{"step": 2, "data": "processed"},
"artifact",
parent_signatures=[step1],
)
# Verify the full chain
result = a2a.verify_wrapped_artifact(step2)
assert result["valid"]
assert result["parent_signatures_valid"]
Build an Audit Trail
chain = a2a.create_chain_of_custody([step1, step2])
# Metadata only: chainOfCustody, created, totalArtifacts.
# This helper does not verify signatures, ordering or 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:
| Type | Use Case |
|---|---|
task | Task assignments, work requests |
message | Inter-agent messages |
result | Task results, responses |
You can use any string -- these are conventions, not enforced types.
What Gets Signed
Every signed artifact includes:
| Field | Description |
|---|---|
jacsId | Unique document ID |
jacsSignature | Signer ID, algorithm, timestamp, and base64 signature |
jacsSha256 | Content hash for integrity verification |
jacsType | a2a- followed by the caller's artifact type label |
jacsParentSignatures | Parent artifacts for chain of custody (if any) |
a2aArtifact | The original artifact data |
Extract a2aArtifact only after the required signature and trust checks. Discarding the envelope also discards its provenance.
Next Steps
- Serve Your Agent Card -- Make your agent discoverable
- Discover & Trust Remote Agents -- Find and assess other agents
- A2A Interoperability Reference -- Full API reference
- Hero Demo (Python) -- 3-agent trust verification example
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
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?
-
"This data hasn't been tampered with"
- Use
sign_message()/signMessage() - This gives you a cryptographic signature and integrity hash.
- Use
-
"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.
- Use
-
"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.
- Use
-
"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.
- Use
-
"I need to send signed data to another agent or service"
- Use
sign_artifact()/signArtifact()via the A2A integration. - This wraps your data in a JACS provenance envelope for cross-boundary exchange.
- See A2A Interoperability and A2A + Attestation Composition.
- Use
Quick Reference
| Scenario | API | Output |
|---|---|---|
| Log an AI action | sign_message() | Signed document |
| Record a human review decision | create_attestation() | Attestation with claims |
| Attach evidence from another system | create_attestation() with evidence | Attestation with evidence refs |
| Wrap an existing signed doc with trust context | lift_to_attestation() | New attestation referencing original |
| Export for SLSA/Sigstore | export_attestation_dsse() | DSSE envelope |
| Verify signature only | verify() | Valid/invalid + signer |
| Verify signature + claims + evidence | verify_attestation(full=True) | Full verification result |
| Exchange artifact with another agent | sign_artifact() / A2A | Signed wrapped artifact |
Native Signing vs. Compatibility Exports
Every API above signs natively: the output carries a JACS
jacsSignature from the agent's selected native root (pq2025 by default,
or ring-Ed25519 when explicitly selected). Separately, the CLI offers
targeted ecosystem exporters
(jacs-compat agent export-jwks, jacs-compat ap2 export-mandate,
jacs-compat agreement-v2 export-vc, the A2A agent card) that sign derived
artifacts with the agent's ES256 compatibility key. Exports are one-way
views: the native document never gains proof or projection fields, and an
ES256 signature by itself proves possession of the compatibility key only
— the native-root-signed compatibility key binding is what ties that key to the
agent. There is no generic "sign this document with algorithm X" API, and
verifying incoming AP2/VC artifacts is out of scope. See the
Security Model.
Examples
Just need integrity? Use signing.
signed = client.sign_message({"action": "approve"})
result = client.verify(signed.raw_json)
# result["valid"] == True
Need trust context? Use attestation.
att = client.create_attestation(
subject={"type": "artifact", "id": "doc-001", "digests": {"sha256": "..."}},
claims=[{"name": "reviewed", "value": True, "confidence": 0.95}],
)
result = client.verify_attestation(att.raw_json, full=True)
# result["valid"] == True, result["evidence"] == [...]
Already signed? Lift to attestation.
signed = client.sign_message({"content": "original"})
att = client.lift_to_attestation(signed, [{"name": "approved", "value": True}])
# att now has attestation metadata referencing the original document
Common Patterns
AI Agent Action Logging
Use sign_message() for each tool call or action. The signature proves the
agent took the action and the data hasn't been modified.
Human Review Attestation
Use create_attestation() with claims like reviewed_by: human and
confidence: 0.95. This creates an auditable record that a human reviewed
and approved the output.
Multi-step Pipeline
Use create_attestation() with a derivation field to capture input/output
relationships. Each step attests to its own transformation with references
to upstream attestations.
Cross-system Verification
Use export_attestation_dsse() to generate an in-toto DSSE envelope that
external systems (SLSA verifiers, Sigstore) can validate independently.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Agreement v2 Developer Guide
Agreement v2 is a standalone jacsType: "agreement" document with consent-signature and signed-transcript-prefix inspection. Its party proofs do not authenticate the full role, quorum, lineage, or finalization policy. A successful mathematical check is not permission to act on the agreement.
Rust owns the protocol checks; Python, Node.js, Go, CLI, MCP, and WASM expose related JSON workflows. Native verify_agreement_v2 reports mathematicalChecksValid separately and always returns valid: false, policyAccepted: false, and overallScope: "consent_signatures_only". The portable inspection report likewise does not accept application policy. Do not interpret either result as proof of human approval or authority.
Mental Model
- The JACS header owns document identity, versioning, authorship signatures, content hash, registration, files, and visibility.
- The agreement body records terms and consent-signature inputs:
title,description,terms,parties,signaturePolicy,agreementSignatures,transcript,links,controllers, andowners. jacsAgreementHashis the consent hash. It changes when terms, parties, policy, effective dates, or expiry dates change.- Transcript appends do not change
jacsAgreementHash. They change the transcript hash that later agreement signatures bind. - Links are intentionally slim: only
jacsIdandjacsVersion. Put relationship meaning in the successor agreement's terms and status.
Minimal Input
{
"title": "Refund approval",
"description": "Agent A and Agent B agree on the refund action.",
"terms": "Agent B may issue a refund up to $25 for order 123.",
"termsFormat": "text/plain",
"status": "proposed",
"parties": [
{ "agentId": "00000000-0000-4000-8000-000000000001", "agentType": "ai", "role": "signer" },
{ "agentId": "00000000-0000-4000-8000-000000000002", "agentType": "human", "role": "signer" },
{ "agentId": "00000000-0000-4000-8000-000000000003", "agentType": "ai", "role": "notary" }
],
"signaturePolicy": {
"partyQuorum": "all",
"witnessRequired": 0,
"notaryRequired": 1,
"minimumStrength": "classical"
},
"controllers": [
"00000000-0000-4000-8000-000000000001",
"00000000-0000-4000-8000-000000000002",
"00000000-0000-4000-8000-000000000003"
]
}
The schema labels are signer, witness, notary, and observer; observers do not sign. V2 helpers check membership against supplied roles, but that does not establish portable role authority. An agentType: "human" label is metadata: an automated signature with that label is not evidence that a person reviewed or approved the terms.
Workflow
- Create the agreement from the input JSON.
- Append transcript references as negotiation messages, statements, or evidence are emitted as separate JACS documents.
- Update terms only when the actual agreement language changes. This clears prior agreement signatures because
jacsAgreementHashchanges. - Sign as
signer,witness, ornotary. - Inspect signatures and transcript tamper evidence. Native role counts and
expectedStatusare structural diagnostics over the supplied document; neither they nor storedstatusauthorize an action. - Resolve concurrent branches in Rust core: transcript-only branches auto-merge; terms conflicts require an explicit successor mutation.
Prerequisites
Before the first agreement-v2 call you need a loaded agent with keys on disk and a config:
- CLI: run
jacs-compat quickstart --name my-agent --domain example.comonce. It creates./jacs.config.json, a key pair, and (if missing) a generated password under./jacs_keys. SetJACS_PRIVATE_KEY_PASSWORDor let quickstart manage it. - Python / Node.js: load or create a
SimpleAgent(SimpleAgent.create_agent(...)/JacsSimpleAgent.create(...)), or useephemeral()for throwaway single-agent demos.
Multiple distinct agents must share a data_directory (or exchange public keys out of band) so a verifier can resolve every signer's key. Ephemeral agents keep keys in memory only and cannot verify each other's signatures.
Roles, Quorum, and Notaries
signaturePolicy.partyQuorumisallor an integer M (M-of-N signer parties).witnessRequiredandnotaryRequiredare separate inputs to structural status calculation; their presence does not authenticate a witness or notary role.controllersis the supplied list checked by mutation helpers; the header signature does not prove that the parties authorized that controller list.ownersare soft copyright claims. Keep both distinct fromparties.
Mutations
All public surfaces accept the same mutation JSON:
{ "type": "appendTranscript", "entry": { "jacsId": "...", "jacsVersion": "...", "jacsSha256": "..." } }
{ "type": "updateTerms", "terms": "Updated agreement text." }
{ "type": "setStatus", "status": "proposed" }
{ "type": "addLink", "link": { "jacsId": "...", "jacsVersion": "..." } }
updateTerms also accepts optional title, description, termsFormat, effectiveFrom, and expiresAt.
Python
from jacs import SimpleAgent
agent, info = SimpleAgent.ephemeral(algorithm="ed25519")
agent_id = info["agent_id"]
agreement = agent.create_agreement_v2({
"title": "Refund approval",
"description": "Approval for a bounded refund.",
"terms": "Refund up to $25 for order 123.",
"status": "proposed",
"parties": [{"agentId": agent_id, "agentType": "ai", "role": "signer"}],
"signaturePolicy": {"partyQuorum": "all", "witnessRequired": 0, "notaryRequired": 0},
"controllers": [agent_id],
})
signed = agent.sign_agreement_v2(agreement, "signer")
report = agent.verify_agreement_v2(signed)
assert report["mathematicalChecksValid"] is True
assert report["valid"] is False
assert report["policyAccepted"] is False
Node.js
This single-agent example self-signs and self-verifies, so the agent resolves its own key. For multiple distinct agents, see the multi-agent walkthrough, which shares key storage so each agent can resolve the others' public keys.
import { JacsSimpleAgent } from "@hai.ai/jacs";
async function main() {
const agent = JacsSimpleAgent.ephemeral("ed25519");
const agentId = agent.getAgentId();
const agreement = await agent.createAgreementV2(JSON.stringify({
title: "Refund approval",
description: "Approval for a bounded refund.",
terms: "Refund up to $25 for order 123.",
status: "proposed",
parties: [{ agentId, agentType: "ai", role: "signer" }],
signaturePolicy: { partyQuorum: "all", witnessRequired: 0, notaryRequired: 0 },
controllers: [agentId],
}));
const signed = await agent.signAgreementV2(agreement, "signer");
const report = await agent.verifyAgreementV2(signed);
console.log("mathematical checks:", report.mathematicalChecksValid);
console.log("policy accepted:", report.policyAccepted); // always false for v2
}
main();
CLI
jacs-compat quickstart --name agent-a --domain example.com
jacs-compat agreement-v2 create --input agreement-input.json > agreement.json
jacs-compat agreement-v2 sign --agreement agreement.json --role signer > signed.json
jacs-compat agreement-v2 verify --agreement signed.json
The verify command emits the inspection report and exits non-zero because v2 does not produce an accepted policy verdict. This is the expected result even when the mathematical checks pass.
For branch handling:
jacs-compat agreement-v2 detect-conflict --base base.json --left left.json --right right.json
jacs-compat agreement-v2 merge-transcript --base base.json --left left.json --right right.json
jacs-compat agreement-v2 resolve-conflict --base base.json --previous left.json --side right.json --mutation resolution.json
Branch Merge and Conflicts
Two agents can emit successor versions from the same prior version. Resolve them in core, never by editing JSON by hand:
- Transcript-only branches (each side only appended transcript entries) auto-merge. Detect with
detect-conflict, thenmerge-transcript. - Any terms, party, policy, status, signature, controller, or link divergence is a real conflict and requires an explicit successor via
resolve-conflict(CLI) orresolveAgreementV2BranchConflict/resolve_agreement_v2_branch_conflict(Node/Python).
jacs-compat agreement-v2 detect-conflict --base base.json --left left.json --right right.json
jacs-compat agreement-v2 merge-transcript --base base.json --left left.json --right right.json
jacs-compat agreement-v2 resolve-conflict --base base.json --previous left.json --side right.json --mutation resolution.json
Golden Example
The repository includes a three-party Python example of the v2 consent-signature workflow:
python examples/agreement_v2_three_party.py
It creates Agent A and Agent B as signer parties, a local demo agent labeled notary, and Agent X as an outsider. The example appends transcript references, rejects outsider mutation/signing, collects two signer signatures plus the demo notary signature, and inspects their mathematical validity. The resulting v2 report does not establish a portable finalization or human-approval verdict. The example checks mathematicalChecksValid: true while requiring valid: false and policyAccepted: false, and rejects transcript and signature tampering.
MCP and WASM
MCP tools mirror the CLI:
jacs_create_agreement_v2jacs_apply_agreement_v2jacs_sign_agreement_v2jacs_verify_agreement_v2jacs_detect_agreement_v2_branch_conflictjacs_merge_agreement_v2_transcript_branchesjacs_resolve_agreement_v2_branch_conflict
WASM exposes the same flow as JSON-string methods: createAgreementV2Json, applyAgreementV2Json, signAgreementV2Json, verifyAgreementV2Json, detectAgreementV2BranchConflictJson, mergeAgreementV2TranscriptBranchesJson, and resolveAgreementV2BranchConflictJson.
DevEx Rules
- Use core helpers instead of hand-editing agreement JSON. The helpers maintain
jacsAgreementHash,allPreviousVersions,jacsPreviousVersion, and status transitions. - Keep
parties,controllers, andownersseparate. Party signatures record agent provenance; controllers are checked by version helpers; owners are soft copyright claims. None of those labels establishes human approval. - Treat stored
statusas a cache. V2 inspection cannot establish authority to act on that status; the application must separately establish its authorization policy. - Use transcript entries for process evidence and links for agreement lineage. Links stay
{jacsId, jacsVersion}by design. - For post-final terms changes, create a successor agreement or explicit conflict resolution rather than mutating a final agreement in place.
- Principal delegation (one agent signing on behalf of another party) is unsupported and unscheduled. The signing agent must be listed in
partieswith the matching role;delegatedByanddelegationChainare rejected. This does not change ES256 export-key delegation through compatibility key bindings, or SDK forwarding of calls to Rust.
Migrating from v1 agreements
JACS still ships the original v1 "sidecar" agreement: a jacsAgreement field attached to an existing signed document, with separate create_agreement / sign_agreement / check_agreement calls and an AgreementOptions struct. V1 and v2 both permit mathematical signature inspection without proving the complete authorization policy. V2 additionally supplies a standalone identity, terms hash, transcript-prefix binding, and version/branch helpers. The presence of parties, roles, and signaturePolicy fields does not mean all of those claims are authenticated by each party proof.
V1 and v2 remain available for compatibility and inspection. Moving a payload to v2 does not make it an actionable authorization.
Conceptual difference
- v1 sidecar: signatures over a referenced document. The agreement is a field bolted onto some other JSON. No standalone identity, no roles beyond "must sign", no transcript, no notary, no branch handling.
- v2 document: a first-class agreement document. Parties carry
role(signer/witness/notary/observer), thesignaturePolicyexpresses quorum and witness/notary requirements, atranscriptrecords process evidence, and successor versions / branches are reconciled by core helpers.
Operation mapping
| v1 operation | v2 equivalent |
|---|---|
create_agreement (binding) / jacs-compat document create-agreement (CLI) | create_agreement_v2 / jacs-compat agreement-v2 create |
sign_agreement (binding) / jacs-compat document sign-agreement (CLI) | sign_agreement_v2 / jacs-compat agreement-v2 sign --role signer |
check_agreement (binding) / jacs-compat document check-agreement (CLI) | verify_agreement_v2 / jacs-compat agreement-v2 verify |
AgreementOptions.quorum (M-of-N) | signaturePolicy.partyQuorum (all, majority, or integer M) |
AgreementOptions.timeout | signaturePolicy.timeout |
AgreementOptions.required_algorithms | signaturePolicy.requiredAlgorithms |
AgreementOptions.minimum_strength | signaturePolicy.minimumStrength |
There is no v1 equivalent for v2 mutations (apply_agreement_v2 / jacs-compat agreement-v2 apply) or branch handling (jacs-compat agreement-v2 detect-conflict / merge-transcript / resolve-conflict) - these are new in v2.
What stays the same
- The cryptographic signing model: the same agent identity and keys sign in both versions, and signatures are JACS signatures over canonical content.
- You still load an agent with keys on disk (or
ephemeral()for single-agent demos) before any agreement call.
What is new in v2
- Roles:
signer,witness,notary,observer(v1 has only "agents that must sign"). - Notary and witness requirements (
signaturePolicy.notaryRequired,signaturePolicy.witnessRequired), counted independently of signer quorum. - A
transcriptwith tamper evidence for each signature's covered prefix. - Branch merge / conflict resolution for concurrent successor versions.
- A diagnostic report from
verify_agreement_v2:mathematicalChecksValid,expectedStatus, errors, and chain coverage. The retainedvalidandpolicyAcceptedfields are always false. - Fail-closed CLI:
jacs-compat agreement-v2 verifyexits non-zero for v2 because no accepted policy verdict is produced.
For the full v2 walkthrough, see the Workflow, Python, Node.js, and CLI sections above and the Rust core / legacy comparison.
Verification Matrix
This is a source-test inventory, not evidence that every published SDK has been exercised together. It does not establish all-language discovery parity.
| Scenario | Coverage |
|---|---|
| Create standalone Agreement v2 | Rust core tests, binding parity fixture, CLI/MCP/WASM tests |
| Structural signer-count/quorum calculation; not portable policy acceptance | Rust core tests and shared parity fixture |
Structural notaryRequired calculation; not authenticated notary authority | Rust core tests and language parity tests |
Human agentType labels; not human-approval evidence | Rust core tests |
| Outsider cannot mutate | Rust core authorization tests |
| Outsider cannot sign | Rust core role-membership tests |
Transcript append preserves jacsAgreementHash | Rust core tests |
| Signed-prefix tamper/reorder/substitution detection; no per-entry hash chain | Rust core tests |
Terms edit changes jacsAgreementHash and clears signatures | Rust core tests |
effectiveFrom and expiresAt | Rust core tests |
allPreviousVersions chain reconciliation | Rust core tests |
Links are only {jacsId, jacsVersion} | Rust core tests and parity fixture |
| Transcript-only branch auto-merge | Rust core, binding parity, CLI, MCP, WASM tests |
| Terms conflict requires explicit resolution | Rust core, binding parity, CLI, MCP, WASM tests |
Key rotation / agentVersion matching | Rust core tests |
| Shared agreement workflow fixtures | Python, Node.js, Go, CLI, MCP, and WASM parity tests |
The fixture binding-core/tests/fixtures/agreement_v2_scenarios.json is the portable workflow source of truth. Update it when an exposed workflow changes so every binding stays aligned.
Exporting an Agreement as a Verifiable Credential
jacs-compat agreement-v2 export-vc (or SimpleAgent::export_agreement_v2_as_vc /
export_agreement_v2_as_vc_json in the bindings) projects an Agreement-v2
document into a W3C Verifiable Credential 2.0 carrying an ecdsa-jcs-2019
Data Integrity proof, signed with the agent's ES256 compatibility key:
# the agreement-vc content scope is never auto-issued — grant it first
jacs-compat agent issue-compat-binding \
--scopes jwks,did,a2a-agent-card,w3c-agent-identity,agreement-vc
jacs-compat agreement-v2 export-vc --agreement agreement.json > vc_export.json
The VC's @context is ["https://www.w3.org/ns/credentials/v2", "https://hai.ai/ns/credentials/jacs-agreement/v1"], its type is
["VerifiableCredential", "JacsAgreementCredential"], and
credentialSubject.jacsAgreementV2 embeds the agreement verbatim. The proof's
verificationMethod references the Multikey entry of the agent's DID
document (conformant ecdsa-jcs-2019 verifiers reject JWK-typed methods).
The export is a derived view: the native agreement's bytes, jacsSignature
(using the agent's selected native algorithm), and verification are unchanged.
A stock Data Integrity verifier can check the proof classically — see
scripts/smoke/verify_di_vc.mjs. That proves possession of the ES256 key
only; tracing the credential to the agent's native root requires the
native-root-signed compatibility key binding (jacs-compat agent export-compat-binding).
Verifying incoming VCs is out of scope for JACS in P2.
Troubleshooting
report.validandreport.policyAcceptedare always false for v2. To diagnose mathematical failures, inspectmathematicalChecksValidanderrors; a missing signer key is one possible failure, not the reason for the unconditional policy rejection.- "outsider" rejection on sign/apply: the acting agent is not listed in
partieswith the matching role. Add it toparties(and tocontrollersfor mutations). - Status looks wrong:
expectedStatusis a structural diagnostic. Neither it nor storedstatusis an accepted authorization verdict.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Create and Verify an Attestation
An attestation signs claims about a subject. Verification authenticates the issuer's statement; the truth of a claim and permission to act require separate evidence and application policy.
The Python example below uses an ephemeral pq2025 agent. It requires the
native attestation feature; source builds enable it with the attestation
Cargo feature. See Python installation.
Create a claim about a signed document
import hashlib
from jacs.client import JacsClient
client = JacsClient.ephemeral()
signed = client.sign_message({"tool": "calculator", "answer": 42})
content_hash = hashlib.sha256(signed.raw.encode("utf-8")).hexdigest()
attestation = client.create_attestation(
subject={
"type": "artifact",
"id": signed.document_id,
"digests": {"sha256": content_hash},
},
claims=[{
"name": "purpose",
"value": "calculation example",
"assuranceLevel": "self-asserted",
}],
)
result = client.verify_attestation(attestation.raw)
assert result["valid"]
assert result["crypto"]["signatureValid"]
assert result["crypto"]["hashValid"]
The subject digest here covers the exact UTF-8 signed-document JSON. Preserve those bytes when exchanging the subject. A subject reference alone does not make the referenced artifact available to a verifier.
Local and full verification
verify_attestation() extracts the document's id:version and checks the
copy already in this agent's document store. It is not an arbitrary inbound JSON
verification API. Full verification additionally examines evidence and derivation:
full = client.verify_attestation(attestation.raw, full=True)
print(full["valid"], full.get("evidence"), full.get("chain"))
Inspect the complete report and errors before accepting it. Packages before 0.16.0 check the signature with the loaded agent's key, so they cannot fully verify another agent's attestation. Since 0.16.0, verification resolves the signer's exact signed key: local verification uses own, enrolled or cached keys and stays offline; full verification also follows the opt-in key discovery settings.
Full verification does not fetch URI-only evidence. Non-embedded evidence whose
bytes are unavailable reports digestValid=false; an HTTPS URL and a claimed
digest do not form a passing evidence check by themselves. Built-in A2A and
Email evidence adapters only record and compare digests, not protocol signatures
or signer trust. See custom evidence adapters.
Claims such as reviewed_by=human, a confidence score or an assurance label
are assertions until supported by independently verified reviewer evidence.
Never use a self-selected label as a substitute for that evidence.
DSSE export
envelope = client.export_attestation_dsse(attestation.raw)
print(envelope["payloadType"], len(envelope["signatures"]))
This produces a DSSE-shaped export. Acceptance by in-toto, SLSA or Sigstore consumers depends on their verifier, algorithms and policy; export alone does not demonstrate interoperability or authorization.
Continue with framework attestations, sign versus attest, or the attestation error catalog.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Writing a Custom Evidence Adapter
Evidence adapters normalize external bytes into claims and evidence references.
They belong to the retained native Rust jacs crate with the attestation
feature enabled.
The built-in A2A and Email adapters are digest-only: they do not verify
A2A/JACS signatures, DKIM, S/MIME, PGP, sender identity or signer trust. Their
claims use self-asserted assurance and names ending in
evidence-digest-recorded. Use the corresponding protocol verifier before
asserting protocol or identity verification.
Trait contract
External implementations import the public types from jacs:
#![allow(unused)] fn main() { use jacs::attestation::adapters::EvidenceAdapter; use jacs::attestation::types::{Claim, EvidenceRef, EvidenceVerificationResult}; use jacs::error::JacsError; use serde_json::Value; }
The required method signatures are:
fn kind(&self) -> &str;
fn normalize(&self, raw: &[u8], metadata: &Value)
-> Result<(Vec<Claim>, EvidenceRef), JacsError>;
fn verify_evidence(&self, evidence: &EvidenceRef)
-> Result<EvidenceVerificationResult, JacsError>;
An implementation must also implement Send + Sync + Debug.
See the trait and built-in implementations
for complete types and executable examples.
Normalize and register explicitly
Call your adapter's normalize() on the raw bytes and metadata, then supply
the returned claims and evidence to attestation creation. The raw creation API
takes already-normalized values; it does not automatically call every adapter.
Register a custom adapter on a mutable native Agent with
agent.register_adapter(Box::new(adapter)). This API exists now. Registration
makes matching evidence kinds available during full attestation verification;
it does not require editing JACS's default adapter list.
Preserve the evidence's meaning
Compute digests over an explicitly defined byte representation and preserve that representation for later verification. Reformatting JSON, decoding strings or redacting fields can change the bytes. Keep parsing separate from authentication: decoding a JWT payload does not verify its signature, issuer, audience or expiry.
normalize() receives untrusted data and metadata. Do not promote a caller's
verified=true or a successful digest computation into verified assurance.
A confidence value is an assertion, not proof. State exactly which protocol
checks your implementation performs and which trust policy supplies its keys.
verify_evidence() returns digest_valid, freshness_valid and a
human-readable detail in Rust. Implement actual checks before reporting
success. If required bytes, trust anchors or time context are unavailable, return
a failure or an explicit error rather than a fabricated passing result. The
built-in full verifier does not fetch URI-only evidence.
Privacy and testing
Sensitivity labels do not encrypt evidence or grant access control. JWT subject, issuer and audience fields may identify people or organizations; minimize them and avoid embedding bearer tokens or other credentials. Select URI resolution, retention and access policy in the application.
Test malformed input, digest mutations, unavailable evidence, expired or future-dated evidence, untrusted keys, incorrect issuer/audience, and round trips that preserve exact bytes. Include assertions that an unauthenticated source never produces a claim of verified identity.
See attestation creation and verification and testing.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Framework Adapter Attestations
Python adapters accept attest=True to produce an attestation about their
output. A signed claim records what its issuer asserted; it does not prove a
human reviewed the output or that an action is authorized.
Install the corresponding framework extra. These examples require a native build with attestation support.
LangChain and LangGraph
Wrap a LangChain tool instance:
from langchain_core.tools import tool
from jacs.client import JacsClient
from jacs.adapters.langchain import signed_tool
client = JacsClient.ephemeral()
@tool
def lookup_customer(customer_id: str) -> str:
"""Return the customer status."""
return "active"
attested_lookup = signed_tool(lookup_customer, client=client, attest=True)
For LangGraph, with_jacs_signing([lookup_customer], client=client, attest=True)
constructs a ToolNode. jacs_wrap_tool_call(client=client, attest=True)
returns a (request, execute) hook for a tool node; it is not a decorator for
an ordinary Python function. For LangChain agents use
jacs_signing_middleware(client=client, attest=True).
FastAPI
from fastapi import FastAPI
from jacs.client import JacsClient
from jacs.adapters.fastapi import JacsMiddleware
client = JacsClient.quickstart(name="api", domain="api.example.com")
app = FastAPI()
app.add_middleware(JacsMiddleware, client=client, attest=True)
For an individual route, use @jacs_route(client=client, attest=True).
These constructors do not accept claims or default_claims.
Request verification only checks signed POST JSON bodies; unsigned requests
still pass through, including in strict mode. See
FastAPI verification limits.
Anthropic tools
from jacs.client import JacsClient
from jacs.adapters.anthropic import signed_tool
client = JacsClient.ephemeral()
@signed_tool(client=client, attest=True)
def search_database(query: str) -> str:
return "Found 3 results"
attestation_json = search_database("example")
JacsToolHook(client=client, attest=True) is an async post-tool hook accepting
an input dictionary containing tool_response. It does not register or wrap a
list of tool schemas.
Custom claims and verification
Use JacsClient.create_attestation(subject=..., claims=..., evidence=...)
when you need explicit claims and evidence. The lower-level
BaseJacsAdapter also accepts default_claims, and its
sign_output(data, extra_claims=...) adds per-output claims. Framework wrappers
do not expose all of those arguments.
client.verify_attestation(attestation_json) returns valid, crypto,
evidence, chain and errors; it does not return a top-level claims
list. The attestation must already be available in this agent's document store.
Read the attestation tutorial for exact result keys
and full-verification limits.
Signing and attestation failures raise by default, even when strict mode is off.
The explicit compatibility options allow_unsigned_output=True and
allow_plain_signature_fallback=True respectively permit unsigned output or
dropping attestation claims. strict=True disables those fallbacks.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
A2A + Attestation: Using Both Together
A2A artifact signatures cover an exchanged payload and its linked parent artifacts. Attestations record claims, evidence and derivation. Neither mechanism proves that a claim is true or that a human reviewed it.
Create distinct identities
Different names alone do not create different agents: quickstart loads an existing config. Use distinct paths:
from jacs.client import JacsClient
scanner = JacsClient.quickstart(
name="scanner", domain="scanner.example.com", config_path="scanner/jacs.config.json"
)
reviewer = JacsClient.quickstart(
name="reviewer", domain="reviewer.example.com", config_path="reviewer/jacs.config.json"
)
The Node equivalent is JacsClient.quickstart({name, domain, configPath}) with a separate configPath for each agent. Preserve each identity's encrypted keys and host password source.
Configure verification before exchanging artifacts
Independently authenticate the sender's agent document and public key, then record them with trust-store enrollment. With local in JACS_KEY_RESOLUTION (the default), the native A2A artifact verifier uses that enrollment for v2 artifacts signed by the exact enrolled agent_id:version, and refuses a key that conflicts with it. Other signers, and other versions that keep the enrolled key, need a cached local key or opt-in registry or DNS/HTTPS key discovery. Neither enrollment nor discovery authorizes the sender. Test rejection of a wrong key before enabling the exchange.
The following is an application workflow, not a complete cross-agent program:
- The scanner signs its result with
sign_artifact(result, "result")and sends the full envelope. - The reviewer resolves the authenticated scanner key, verifies the artifact and required parent signatures, then applies admission policy. Fetching a key beside a payload is not independent authentication.
- After performing its own checks, the reviewer creates an attestation about the exact artifact bytes, with explicit digest conventions and supporting evidence. A claim such as
reviewed=trueremains the reviewer's signed assertion. - The reviewer may sign an A2A artifact referencing that attestation. The receiver verifies the exchanged envelope and separately evaluates the attestation's signature, evidence and claim policy.
A create_chain_of_custody summary collects metadata; it does not verify signatures or establish ordering. Attestation full verification likewise has specific evidence/resolver limits; it is not automatic cross-agent trust evaluation.
Continue with artifact exchange, the attestation tutorial, and the cross-language A2A tests.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
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
| Event | Level | Fields | Source |
|---|---|---|---|
document_signed | info | algorithm, duration_ms | crypt/mod.rs |
batch_signed | info | algorithm, batch_size, duration_ms | crypt/mod.rs |
signing_procedure_complete | info | agent_id, algorithm, timestamp, placement_key | agent/mod.rs |
Verification Events
| Event | Level | Fields | Source |
|---|---|---|---|
signature_verified | info | algorithm, valid, duration_ms | crypt/mod.rs |
verification_complete | info / error | document_id, signer_id, algorithm, timestamp, valid, duration_ms | agent/mod.rs |
verification_complete emits at info when valid=true and at error when valid=false.
Agreement Events
| Event | Level | Fields | Source |
|---|---|---|---|
agreement_created | info | document_id, agent_count, quorum, has_timeout | agent/agreement.rs |
signature_added | info | document_id, signer_id, current, total, required | agent/agreement.rs |
quorum_reached | info | document_id, signatures, required, total | agent/agreement.rs |
agreement_expired | warn | document_id, deadline | agent/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 --manifest-path archive/native/Cargo.toml -p jacs --features otlp-logs,otlp-metrics,otlp-tracing
# Or enable just tracing
cargo build --manifest-path archive/native/Cargo.toml -p jacs --features otlp-tracing
| Feature | What it adds |
|---|---|
otlp-logs | OTLP log export (opentelemetry, opentelemetry-otlp, opentelemetry-appender-tracing, tokio) |
otlp-metrics | OTLP metrics export (opentelemetry, opentelemetry-otlp, opentelemetry_sdk, tokio) |
otlp-tracing | Distributed 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
This illustrative OpenTelemetry Collector configuration receives OTLP over HTTP, batches events and exports to selected backends. Pin a collector distribution/version with the configured exporters and validate its schema; exporter availability and configuration vary by distribution.
# 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" }
}
}
}
}
For a host configured to read standard OpenTelemetry environment variables, the corresponding deployment settings are below. They do not enable JACS exporters by themselves; the host must initialize the observability pipelines and select the endpoint.
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
- Deploy the OTEL Collector with the
datadogexporter (see config above). - Set
DD_API_KEYin the collector's environment. - In Datadog, JACS events appear under Logs > Search with
source:opentelemetry. - Create a monitor on
event:verification_complete AND valid:falseto 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
- Deploy the OTEL Collector with the
splunkhecexporter. - Set
SPLUNK_HEC_TOKENin the collector's environment. - Events arrive in Splunk with
sourcetype=jacs:events. - Search:
sourcetype="jacs:events" event="verification_complete" valid=false
Agreement Monitoring
Agreement events give you a complete lifecycle view: creation, each signature, quorum, and expiry. Here are practical queries.
Agreements Approaching Timeout
Filter for agreement_created events where has_timeout=true, then correlate with quorum_reached. Any agreement_created without a matching quorum_reached within the timeout window is at risk.
Failed Quorum Detection
event="signature_added" | stats max(current) as sigs, max(required) as needed by document_id
| where sigs < needed
Signature Velocity
Track signature_added events over time to see how quickly agents sign after agreement creation:
event="signature_added" | timechart count by document_id
Expiry Alerts
The agreement_expired event (level warn) fires when an agent attempts to sign or verify an expired agreement. Alert on this directly:
event="agreement_expired" | alert
Latency Tracking
Both document_signed and signature_verified include duration_ms. Use these to track signing and verification performance:
event="document_signed" | stats avg(duration_ms) as avg_sign_ms, p99(duration_ms) as p99_sign_ms by algorithm
event="signature_verified" | stats avg(duration_ms) as avg_verify_ms, p99(duration_ms) as p99_verify_ms by algorithm
Post-quantum signing with pq2025 will show higher latency than ring-Ed25519. Use these metrics to decide whether the security/performance tradeoff is acceptable for your workload.
Next Steps
- Observability (Rust API) -- Full API: ObservabilityConfig, LogDestination, MetricsConfig, TracingConfig
- Algorithm Selection Guide -- Latency implications of algorithm choice
- Failure Modes -- What events to expect when things go wrong
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Email Signing and Verification
JACS signs selected canonicalized fields from an RFC 5322 .eml input and
returns email bytes with a jacs-signature.json MIME attachment. That attachment
is an ordinary signed JACS document containing the email hash payload. The
recipient verifies it with an explicitly supplied public key, then compares
the received email's covered fields and attachments against the signed hashes.
This does not assert byte-identical preservation of the entire .eml, mailbox
ownership, truth, human approval or permission to contact a recipient.
JACS also exposes migration helpers for HAI's HTML-inline signed email transport. In that mode, the signature material travels in the HTML body and inline logo instead of as user-visible signature attachments. Core signing and verification still stay in JACS; SDKs and servers should call these helpers rather than reimplementing email hashing, MIME parsing, or media extraction.
The primary attachment-mode APIs are:
| Action | Function | What you supply | What you get back |
|---|---|---|---|
| Sign | jacs::email::sign_email() | raw .eml bytes + a JacsSigner | .eml bytes with jacs-signature.json |
| Verify | jacs::email::verify_email() | signed .eml bytes + sender's public key + verifier | ContentVerificationResult (pass/fail per field) |
Runnable local example
From a source checkout, run:
JACS_KEYCHAIN_BACKEND=disabled JACS_ALLOW_NETWORK=false cargo run --locked --manifest-path archive/native/Cargo.toml -p jacs --no-default-features --example email_signing
The example creates two disposable SimpleAgent::ephemeral instances and a
synthetic multipart email with one attachment. It passes the sender's public
key directly to the recipient, signs and verifies the email, prints the actual
JACS attachment, checks body tampering and a wrong key, and inspects a forwarding
chain. It sends no mail, reads no user keys and writes no files. jacs::email
is available without an optional email feature. This is a local JACS-to-JACS
example, not an independent implementation or mailbox-identity test.
//! Inspect real attachment-mode email signing with disposable in-memory keys. //! No config files, mail delivery, remote key lookup or user keys are used. use jacs::email::{FieldStatus, get_jacs_attachment, sign_email, verify_email}; use jacs::simple::SimpleAgent; use serde_json::Value; fn main() -> Result<(), Box<dyn std::error::Error>> { let (sender, _) = SimpleAgent::ephemeral(Some("ed25519"))?; let (recipient, _) = SimpleAgent::ephemeral(Some("ed25519"))?; // In this local example the key is passed directly. A real recipient must // establish the key's attribution separately from signature verification. let public_key = sender.get_public_key()?; let raw_email = concat!( "From: sender@example.test\r\n", "To: recipient@example.test\r\n", "Subject: Synthetic email provenance\r\n", "Date: Mon, 14 Sep 2026 12:00:00 +0000\r\n", "Message-ID: <synthetic-provenance@example.test>\r\n", "MIME-Version: 1.0\r\n", "Content-Type: multipart/mixed; boundary=synthetic-boundary\r\n\r\n", "--synthetic-boundary\r\n", "Content-Type: text/plain; charset=utf-8\r\n\r\n", "Synthetic body for envelope inspection.\r\n", "--synthetic-boundary\r\n", "Content-Type: application/octet-stream; name=receipt.txt\r\n", "Content-Disposition: attachment; filename=receipt.txt\r\n\r\n", "Synthetic receipt\r\n", "--synthetic-boundary--\r\n", ); let signed_email = sign_email(raw_email.as_bytes(), &sender)?; let result = verify_email(&signed_email, &recipient, &public_key)?; assert!(result.valid, "synthetic email must verify: {result:?}"); let attachment = get_jacs_attachment(&signed_email)?; let document: Value = serde_json::from_slice(&attachment)?; assert!(document["content"]["headers"].is_object()); assert_eq!( document["content"]["attachments"][0]["filename"], "receipt.txt" ); assert!(document["jacsSignature"]["signature"].is_string()); assert!(document.get("payload").is_none()); println!("Actual signed attachment (synthetic data only):"); println!("{}", std::str::from_utf8(&attachment)?); println!( "Field verification: {}", serde_json::to_string_pretty(&result)? ); let tampered = std::str::from_utf8(&signed_email)?.replacen( "Synthetic body for envelope inspection.", "Changed body for envelope inspection.", 1, ); assert_ne!(tampered.as_bytes(), signed_email.as_slice()); let tampered_result = verify_email(tampered.as_bytes(), &recipient, &public_key)?; assert!(!tampered_result.valid); assert!( tampered_result .field_results .iter() .any(|field| { field.field == "body_plain" && field.status == FieldStatus::Fail }) ); assert!(verify_email(&signed_email, &recipient, &recipient.get_public_key()?).is_err()); // Re-signing links the previous attachment bytes. The one-key email API // does not verify ancestors, even when this example reuses the same sender. let forwarded = sign_email(&signed_email, &sender)?; let forwarding_result = verify_email(&forwarded, &recipient, &public_key)?; assert!(!forwarding_result.valid); assert_eq!(forwarding_result.chain.len(), 2); assert!(forwarding_result.chain[0].valid); assert!(!forwarding_result.chain[1].valid); assert!( forwarding_result .field_results .iter() .all(|field| field.status != FieldStatus::Fail) ); println!( "Forwarding inspection: {}", serde_json::to_string_pretty(&forwarding_result)? ); println!("Signature checks do not establish mailbox identity, truth or human approval."); Ok(()) }
Signing an email
#![allow(unused)] fn main() { use jacs::email::sign_email; // 1. Load raw email bytes (RFC 5322 format) let raw_eml = std::fs::read("outgoing.eml")?; // 2. Sign — SimpleAgent implements JacsSigner let signed_eml = sign_email(&raw_eml, &my_agent)?; // 3. Send signed_eml — it is a valid .eml with the JACS attachment std::fs::write("outgoing_signed.eml", &signed_eml)?; }
The JacsSigner trait
sign_email accepts any type that implements JacsSigner. SimpleAgent implements it out of the box.
#![allow(unused)] fn main() { pub trait JacsSigner { /// Create a signed JACS document from the email hash payload. fn sign_message(&self, data: &serde_json::Value) -> Result<SignedDocument, JacsError>; /// Verify a signed JACS document using the sender's public key. fn verify_with_key( &self, signed_document: &str, public_key: Vec<u8>, ) -> Result<VerificationResult, JacsError>; } }
The signing algorithm is read from your JACS agent at runtime and recorded in the jacs-signature.json document. Do not hardcode it in email code.
What sign_email does internally
- Parses and canonicalizes the email headers and body
- Computes SHA-256 hashes for each header, body part, and attachment
- Builds the JACS email signature payload
- Places the hash payload in
contentviasign_message() - Uses the normal JACS document canonicalization, hashing and signing pipeline
- 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:
- Renames the existing signature to
jacs-signature-0.json(or-1,-2, ...) - Computes a
parent_signature_hashlinking to the previous signature - Signs the email with a new
jacs-signature.json
The new signature covers the hash link and the retained attachment bytes.
It does not verify the earlier signers' cryptographic signatures. The current
one-key verifier records ancestors with chain[].valid: false because their
keys were not supplied; this also keeps the overall result false even if the
current signature and all covered fields pass. Treat those ancestors as
unchecked, not as proven tampering. Verify their signatures with independently
selected keys before an application relies on the chain; do not turn these
flags true based only on a matching hash or claimed signer label.
Verifying an email
One-call API (recommended)
#![allow(unused)] fn main() { use jacs::email::verify_email; use jacs::simple::SimpleAgent; let signed_eml = std::fs::read("incoming_signed.eml")?; let sender_public_key: Vec<u8> = /* fetch from local trust, DNS, or another trusted source */; // Any agent can verify — the sender's public key is passed explicitly let (agent, _) = SimpleAgent::ephemeral(None)?; let result = verify_email(&signed_eml, &agent, &sender_public_key)?; if result.valid { println!("Covered email fields match a JACS signature checked with the supplied key"); } else { // Inspect field failures and any unchecked forwarding ancestors for field in &result.field_results { println!("{}: {:?}", field.field, field.status); } println!("Chain inspection: {:?}", result.chain); } }
verify_email does everything in one call:
- Extracts
jacs-signature.jsonfrom the email - Removes it (the signature covers the email without itself)
- Verifies the JACS document signature against the sender's public key
- Compares covered canonicalized headers, body parts, attachments and MIME hashes
- Returns per-field and forwarding-chain results; Message-ID is stored but not verified
Two-step API (when you need the JACS document)
If you need a parsed email DTO and JACS metadata (issuer, timestamps) before doing the content comparison:
#![allow(unused)] fn main() { use jacs::email::{verify_email_document, verify_email_content}; use jacs::simple::SimpleAgent; let (agent, _) = SimpleAgent::ephemeral(None)?; // Step 1: Verify the current JACS signature, then project its email DTO let (doc, parts) = verify_email_document(&signed_eml, &agent, &sender_public_key)?; // Inspect the document println!("Signed by: {}", doc.metadata.issuer); println!("Created at: {}", doc.metadata.created_at); // Step 2: Compare content hashes let result = verify_email_content(&doc, &parts); assert!(result.valid); }
All cryptographic operations are handled by the JACS agent via
SimpleAgent::verify_with_key(). The agent's own key is not used --
the sender's public key is passed explicitly.
Field-level results
The ContentVerificationResult contains a field_results vector with one
entry per field:
| Status | Meaning |
|---|---|
Pass | Covered hash matches the signed value; no independent mailbox attribution |
Modified | From, To or Cc names the same mailboxes, differing only in quoting, whitespace, RFC 2047 encoding, order or address case |
Fail | Content does not match the signed hash |
Unverifiable | Field absent or not verifiable (e.g. Message-ID may change in transit) |
Headers covered include from, to, subject, date and, when present, cc,
in_reply_to and references. message_id is stored in the signed payload but
always reported Unverifiable, since transit can change it. Inspect every
field status; overall valid does not mean every field was checked.
Most clients show a display name instead of the address, so a changed, added or
removed display name (or comment) in From, To or Cc fails. A From, To, Cc,
Subject, Date, Message-ID, In-Reply-To or References header that appears more
than once fails as that field (current_value such as 2 subject headers),
whether or not it was signed: clients choose which copy to show. Signing
already refuses such mail.
Body hashing uses decoded/canonicalized text, including charset, line-ending and trailing-whitespace normalization. Attachment hashes include the normalized filename, lowercased content type and extracted content bytes; body and attachment MIME structural headers have separate hashes. Equivalent normalized content can verify despite changes to its wire representation. Uncovered headers and transport metadata are outside this claim.
MIME parts outside the signature
A signature covers the first text/plain part not marked as an attachment, the
first such text/html part (attachment mode only), and every attachment: a part
with a filename, or a part of another type marked inline or attachment.
Mail clients can also show other parts: a second text or HTML alternative, an
image without a filename, a text part marked as an attachment without a
filename, an embedded message/rfc822, or a part without a Content-Type.
extract_email_parts lists these in ParsedEmailParts::unsigned_parts.
Content verification reports each as Fail, named unsigned_parts[0],
unsigned_parts[1] and so on, with current_value describing its type,
disposition, Content-ID and size. A body that is present but not signed, such
as an HTML part added to text-only mail, fails as body_plain or body_html
with current_value present but not signed.
sign_email and build_html_inline_email_signature_payload refuse such mail
with EmailError::UnsupportedFeature, because the signature could never
verify. HTML-inline verification excludes the generated HTML body and exactly
one HAI logo part. A second logo part or HTML body fails; the V3 picture and
calendar invitations remain signed attachments.
Mailing lists and gateways that append a footer or disclaimer as a separate MIME part therefore make verification fail, as a footer added inside a signed body already did.
HTML-inline signed email migration helpers
The HTML-inline transport is being added for HAI email while attachment mode
remains compatible. Use verify_signed_email when a caller may receive either
transport:
#![allow(unused)] fn main() { use jacs::email::{verify_signed_email, VerificationMode}; let result = verify_signed_email( &raw_eml, &verifier_agent, &sender_public_key, VerificationMode::Strict, )?; }
The verifier detects SignedEmailTransport::AttachmentJacs or
SignedEmailTransport::HtmlInline and returns SignedEmailVerificationResult
with Verified, PartiallyVerified, or Failed.
Canonical presentation has three explicit versions. Historical V1 escapes
plaintext without links and omits inline_template_version from the signed
payload. V2 uses render_inline_email_message_body with
InlineEmailTemplateVersion::V2: a complete, unpadded HTTPS URL line becomes an
anchor whose visible URL and destination are the exact original text, including
query and fragment. Credentials, controls, whitespace and markup delimiters
remain ordinary escaped text. The helper is generic and does not recognize
application routes or invitation tokens. render_inline_email_message_main
adds the canonical container: V2 uses dir="auto", deriving displayed direction
from the first strong character of signed text so Arabic and Hebrew render
naturally; V1 retains its historical container. Direction attributes are part
of the reconstructed presentation and cannot be changed independently.
V3 is V2 after one picture: render_inline_email_message_main puts
<img src="cid:mediationbench-judge@hai.ai" alt="MediationBench judge" width="96" height="96"><br> before the container (HAI_BANNER_* constants).
The picture is an ordinary inline MIME part, so the signed payload hashes its
bytes with the other attachments; its <img> is part of the reconstructed
presentation, so removing, moving or resizing it fails presentation
verification. Content-IDs are not hashed, as for the logo: a part without a
signature that takes the picture's Content-ID fails as an unsigned part, but
relabelling another signed image part is not detected. V3 binds
inline_template_version: "v3"; verifiers that know only V1 and V2 reject it.
V2 binds inline_template_version: "v2" inside the signed email payload.
In every version, the strict verifier rebuilds the whole HTML document from the
signed plaintext and requires exactly one supported template marker matching
the signed version. The plaintext must end with HAI's footer line: a blank
line, then This email is sent from an AI agent. Verify at <URL>. The expected
document is the marker, the message from render_inline_email_message_main,
HAI's logo, an empty application/jacs+json envelope script, and the footer
<footer data-hai-verify-footer="v1">, whose text and single link both name
that URL. The logo may take any shape a HAI producer has emitted, each with alt
text HAI verification logo: the picture linked to the same URL, that link
with <br><br> before the picture, or the unlinked picture.
The logo part must show HAI's picture, HAI_LOGO_PNG, carrying the compact
header of the signed document: the size and decoded pixels of
embed_jacs_header_in_logo_png(HAI_LOGO_PNG, header). Dropped metadata and
re-encoding still verify; another picture, even one carrying the genuine
header, or an animated PNG fails with LogoSignatureMismatch in every mode.
Only the contents of the first envelope script are ignored; the verifier checks
that envelope separately. It must be a JSON object with no members other than
compactHeader (matched against the logo), jacsEnvelope (the signed JACS
document) and agentEnvelope (the hosted agent's document on HAI-countersigned
mail), and must not contain <!--, which changes where HTML parsers end a
script. Otherwise verification fails with MissingInlineJacsEnvelope; a signed
value containing <!--, such as a subject, fails safe unless the producer
escapes < as \u003c. Nothing else is stripped: elements carrying
the footer, envelope or logo markers are compared like any other markup, and
any comment (including Outlook conditional comments), a second envelope script
or an HTML tokenizer error fails. Tag and attribute-name case, attribute order
and quoting, and character references are normalized. Altered hrefs, labels,
fragments, extra or moved markup, a missing plaintext footer line and unsigned
version changes fail presentation verification with HtmlEquivalenceFailed:
Failed in VerificationMode::Strict, PartiallyVerified in
VerificationMode::Degraded. Edits to the signed version fail cryptographic
verification.
Link rewriting in transit, such as Microsoft Defender Safe Links, therefore changes the verified presentation. Any email whose footer or logo link was rewritten now fails presentation verification: V1 mail, and V2 or V3 mail whose message has no URL lines, as well as V2 and V3 mail with rewritten message links, which already failed.
Readers implementing only V1 reject V2 presentation. Upgrade producers and
verifying delivery services together before emitting V2; SDKs may continue to
produce V1. V1 historical serialized payloads and verification remain supported.
The optional Rust payload field is an additive serialized contract; downstream
Rust struct literals must supply inline_template_version: None when using V1
or attachment mode. These source changes do not constitute a published release.
HTML-inline helpers include:
build_html_inline_email_signature_payloadfor the inline signed pre-image. It signs the existing email header scope, the text body, and user attachments. Generated HTML and signature artifacts are excluded.embed_jacs_header_in_logo_pngandextract_jacs_header_from_logo_pngfor the signed inline PNG logo transport, withHAI_LOGO_PNGas the picture.HAI_LOGO_ALTandHAI_VERIFY_FOOTER_TEXT_PREFIX, the logo alt text and the footer text before the verify URL, for producers and their tests.extract_topmost_inline_jacs_envelopefor reply-safe hidden envelope selection.inline_email_presentation_matchesfor the noncryptographic whole-document presentation check, also suitable for ingress safety.remove_inline_signature_artifacts,strip_inline_signature_artifacts_from_html, andhtml_bodies_equivalentfor parser-based artifact removal and HTML comparison, for example to display or index a message. Strict presentation verification does not use them.verify_html_inline_email_contentfor content-hash verification after removing inline transport artifacts while keeping user attachments signed.
Attachment mode remains available through sign_email, verify_email, and
the verify_email_* compatibility APIs.
The JACS signature document
The primary signed attachment uses the normal JACS envelope. The runnable example above prints its actual generated JSON; do not construct a signature by filling in a sample object.
| Path in the signed attachment | Meaning |
|---|---|
content.headers | Selected canonicalized header values and hashes |
content.body_plain, content.body_html | Present body parts' content and MIME-header hashes |
content.attachments | Attachment filenames, content hashes and MIME-header hashes |
content.parent_signature_hash | When forwarding, hash of the previous signature attachment bytes |
jacsId, jacsVersion, jacsSha256 | Normal JACS document identifiers and document hash |
jacsSignature | Normal signature metadata, including agentID, date, signingAlgorithm and signature |
verify_email_document() first verifies this envelope, then returns a
JacsEmailSignatureDocument DTO with payload, metadata and signature
fields for compatibility. That DTO is not the serialized attachment format:
payload comes from content; metadata.issuer comes from
jacsSignature.agentID; metadata.hash comes from jacsSha256. Its legacy
signature.key_id, signature.algorithm and signature.signature fields are
empty in this projection. Inspect the original attachment with
get_jacs_attachment() for the actual signature metadata or re-verification.
The historical version: "1.0" / payload / metadata / signature layout is
not what current sign_email() emits. Parsing such a layout in an ancestor is
compatibility inspection, not cryptographic verification. Neither the DTO nor
the attachment establishes that a key belongs to the address in From; that
attribution and any human/contact authority require separate evidence.
Public API summary
All items are re-exported from jacs::email:
#![allow(unused)] fn main() { // Signing jacs::email::sign_email(raw_email: &[u8], signer: &impl JacsSigner) -> Result<Vec<u8>, EmailError> jacs::email::build_html_inline_email_signature_payload(raw_email: &[u8]) jacs::email::JacsSigner // trait implemented by SimpleAgent // Verification jacs::email::verify_email(raw, &agent, pubkey) // one-call: crypto + content check jacs::email::verify_signed_email(raw, &agent, pubkey, mode) jacs::email::verify_email_document(raw, &agent, pk) // step 1: crypto only jacs::email::verify_email_content(&doc, &parts) // step 2: content hash comparison jacs::email::verify_html_inline_email_content(&doc, &parts) jacs::email::normalize_algorithm(...) // algorithm name normalization // Types jacs::email::ContentVerificationResult // overall result with field_results jacs::email::SignedEmailVerificationResult jacs::email::SignedEmailTransport // AttachmentJacs | HtmlInline jacs::email::VerificationMode // Strict | Degraded jacs::email::FieldResult // per-field status jacs::email::FieldStatus // Pass | Modified | Fail | Unverifiable jacs::email::JacsEmailSignatureDocument // parsed compatibility DTO, not raw attachment JSON jacs::email::ParsedEmailParts // parsed bodies, attachments and unsigned_parts jacs::email::UnsignedPart // a MIME part no signature covers 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 }
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Signing Streaming Output
A signature authenticates the bytes it covers. Data displayed before its final signature arrives is provisional; do not treat it as verified output.
Complete-response signing
For bounded text output, accumulate chunks and sign after successful completion. This application-level example accepts an async iterator of strings:
from jacs.client import JacsClient
async def signed_completion(chunks, client: JacsClient, max_bytes=1_000_000):
parts = []
size = 0
async for chunk in chunks:
size += len(chunk.encode("utf-8"))
if size > max_bytes:
raise ValueError("response exceeds signing buffer limit")
parts.append(chunk)
return client.sign_message({"response": "".join(parts)})
The caller decides how to display provisional chunks, handle cancellation and deliver the final signed document. Enforce a size limit appropriate to the host; LLM responses and tool streams are not guaranteed to fit in memory.
Framework boundaries
- LangChain and LangGraph adapters sign completed tool results. They do not authenticate every token in the model's output stream.
- The Vercel AI adapter can attach provenance after stream completion; see its adapter guide for the current chunk contract.
- HTTP JSON-signing middleware is not a generic SSE or chunked-response signing protocol. For signed events, sign complete event payloads and verify them before consumption.
For per-event signing, also authenticate stream identity, ordering and completion under your application protocol. Independently valid events do not by themselves prove that none were removed, reordered or replayed. See signed responses and events for the existing response envelope and replay-store requirements.
Large files
Use the file-signing API for file provenance, but measure memory and I/O costs for your backend and embedding mode. This guide does not promise constant-memory hashing or automatic streaming of multi-gigabyte attachments. Chunking large artifacts needs an explicit manifest and completeness policy understood by both producer and verifier.
See Python adapters and Node adapters.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Inline Text Signatures
Why this matters
jacs-compat sign-text appends a YAML signature footer to a text file. Verification binds canonicalized content and a signer-supplied timestamp to a key. Independent evidence must establish that key's identity; a signature does not prove authorship, human approval or legal ownership.
At a glance
| Action | CLI verb | Python | Node | Go | Rust |
|---|---|---|---|---|---|
| Sign | jacs-compat sign-text <file> | jacs.sign_text(path) | jacs.signText(path) | agent.SignText(path, nil) | jacs::simple::advanced::sign_text_file(&agent, path, opts) |
| Verify | jacs-compat verify-text <file> | jacs.verify_text(path) | jacs.verifyText(path) | agent.VerifyText(path, nil) | jacs::simple::advanced::verify_text_file(&agent, path, opts) |
Python examples use from jacs import simple as jacs; Node examples use @hai.ai/jacs/simple and return Promises. Go calls are methods on a loaded *JacsSimpleAgent. Python, Go and Rust calls are synchronous.
The signature block
sign-text appends a YAML footer at the end of the file. The markers stay
small and stable; the body is a full signed JACS document so markdown can carry
the same jacsId / jacsVersion chain as JSON-envelope documents:
-----BEGIN JACS SIGNATURE-----
jacsId: 5f7e...
jacsVersion: 7c6d...
jacsVersionDate: "2026-05-01T12:00:00Z"
jacsType: inline-md
jacsLevel: artifact
content:
inlineSignatureVersion: 1
canonicalization: jacs-text-v1
hashAlgorithm: sha256
signedContentHash: no5d...
jacsSignature:
agentID: agent-abc123
signingAlgorithm: pq2025
publicKeyHash: 3b2f...
signature: ...
jacsSha256: 9a8b...
-----END JACS SIGNATURE-----
Multi-signer files contain multiple back-to-back blocks. The signers are unordered (matches the existing JACS agreement model) — you can reorder the blocks on disk and verification still passes. The file content (the bytes before the first block) is preserved byte-for-byte. When the same signer re-signs edited content, JACS replaces that signer's footer with a new version of the same inline document: jacsId stays the same, jacsVersion changes, and jacsPreviousVersion points to the prior version.
Field reference
| Field | What it is |
|---|---|
jacsId | Stable identity for this signer's inline markdown document across edits. |
jacsVersion | Version ID for this footer. Changes whenever the signer re-signs edited content. |
jacsPreviousVersion | Previous jacsVersion for same-ID updates; absent or null on first signature. |
jacsVersionDate | RFC 3339 / ISO 8601 timestamp the signer claims. This is a claim, not a notarised proof. |
jacsType | inline-md for inline markdown/text footers. |
content.signedContentHash | Base64url SHA-256 of the canonicalised markdown/text body. |
content.canonicalization | Tag identifying how the bytes are normalised before hashing (currently jacs-text-v1). |
jacsSignature.agentID | Agent ID used by the inline key resolver described below. |
jacsSignature.publicKeyHash | Native key-hash field; not a substitute for independently trusted key selection. |
jacsSignature.signature | Normal JACS document signature over the footer document fields. |
jacsSha256 | Normal JACS document hash for the signed footer. |
The body is valid YAML 1.2; any YAML 1.2 parser can read it, and JACS converts it back through the same yaml_to_jacs path used by email YAML signing. Legacy v0.10.0 mini-blocks still verify, but new writes use the full JACS footer. A file may contain at most 256 signature blocks.
Generic document verification also recognizes inline-signed markdown/text: when
the body has a signature footer, jacs-compat verify <file> and
SimpleAgent::verify() dispatch to the inline verifier instead of treating the
input as JSON. When the MIME type is known, callers should pass
text/markdown or text/plain and use the same inline verification path.
Canonicalization (jacs-text-v1)
The bytes hashed by content.signedContentHash are derived from the file content (everything before the first signature block) by:
- Removing every carriage-return (
\r) character. CRLF becomes LF; CR-only separators disappear. - Stripping trailing spaces, tabs, and blank lines at the end of the content.
The file on disk is NOT modified by canonicalization — the original bytes are preserved. Canonicalization only affects the hash input.
Permissive vs strict verification
By default, verification is permissive: a missing signature is a typed status, not an error. Strict mode opts in to treating missing signatures as a real failure.
| Outcome | CLI exit | Library result |
|---|---|---|
| All blocks valid | 0 | Signed result; every signature entry is valid |
| A block is invalid or its key is unavailable | 1 | Per-block status in signatures; not necessarily an exception |
| No signature, permissive mode | 2 | Missing-signature status |
| No signature, strict mode | 1 | Missing-signature error |
A signed result only means blocks were found. Inspect every signature entry and require the expected signer set; otherwise a valid block can hide a failed or missing required signer. Rust returns VerifyTextResult::Signed { signatures } with SignatureStatus entries. File-level malformed input and I/O can fail separately. Strict mode does not convert every per-block failure into an exception.
Worked example — countersigned README
Initial state: README.md is one paragraph, unsigned.
# Agent A signs
JACS_CONFIG=./agent-a.config.json jacs-compat sign-text README.md
# Agent B counter-signs
JACS_CONFIG=./agent-b.config.json jacs-compat sign-text README.md
# Anyone verifies
jacs-compat verify-text README.md
# - agent-a (ed25519) valid
# - agent-b (pq2025) valid
After both signers, the on-disk file is:
This README documents the v1 release.
-----BEGIN JACS SIGNATURE-----
jacsId: ...
jacsType: inline-md
... (agent-a)
jacsSignature:
signature: ...
-----END JACS SIGNATURE-----
-----BEGIN JACS SIGNATURE-----
jacsId: ...
jacsType: inline-md
... (agent-b)
jacsSignature:
signature: ...
-----END JACS SIGNATURE-----
You can swap the order of the two blocks on disk and jacs-compat verify-text still reports both as valid — multi-signer is unordered.
--key-dir override
When verifying a remote signer, provide a directory of authenticated <signer_id>.public.pem files:
jacs-compat verify-text README.md --key-dir ./trusted-keys/
Use independently authenticated keys in --key-dir for remote signers. A matching file is considered after the local signer's own key; version-qualified IDs encode : as %3A in filenames. Legacy compact blocks resolve only locally: the signer's own key, --key-dir, then a trust-store agent document that embeds its public key. A full JACS footer that none of those match falls back to the shared native resolver, which uses a trust-store enrollment for the exact signed agentID/agentVersion and, when enabled, opt-in registry or DNS/HTTPS key discovery. DNS discovery requires DNSSEC when jacs_dns_strict is set; a discovered key is not independent identity authorization.
Marker collision — what happens if your file mentions the marker
sign-text refuses (hard error) to sign input that already contains a column-zero -----BEGIN JACS SIGNATURE----- line outside a valid block. Authors writing about the format in a markdown file have three workarounds:
- Indent the marker by two spaces — the parser only matches column-zero markers.
- Use a zero-width space prefix — the literal string differs by one byte.
- Use inline emphasis — wrap the marker in backticks or asterisks.
There is intentionally no --force-overwrite-markers escape hatch. Its semantics under the first-marker-splits-content parser are ambiguous — easier to fix the input file than to ship a footgun.
Caps and rejected inputs
The parser limits each footer body to 16 KiB and each file to 256 blocks. Oversized or malformed blocks and unsupported YAML constructs produce validation failures/statuses; inspect the returned result rather than matching invented error-type names. Stray column-zero signature markers prevent signing. See the parser contract for exact framing and accepted fields.
Next step
If you need the same provenance for image bytes (PNG / JPEG / WebP), see Image and Media Signatures.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Image and Media Signatures
Why this matters
jacs-compat sign-image embeds a signed JACS document inside PNG, JPEG or WebP metadata. Verification binds the covered image content and claimed time to a key. Independent trust must establish who controls that key; the signature does not prove first creation, ownership or human approval.
At a glance
| Action | CLI verb | Python | Node | Go | Rust |
|---|---|---|---|---|---|
| Sign | jacs-compat sign-image <in> --out <out> | jacs.sign_image(input_path, output_path) | jacs.signImage(inputPath, outputPath) | agent.SignImage(inputPath, outputPath, nil) | jacs::simple::advanced::sign_image(&agent, in, out, opts) |
| Verify | jacs-compat verify-image <file> | jacs.verify_image(path) | jacs.verifyImage(path) | agent.VerifyImage(path, nil) | jacs::simple::advanced::verify_image(&agent, path, opts) |
| Extract | jacs-compat extract-media-signature <file> | jacs.extract_media_signature(path) | jacs.extractMediaSignature(path) | agent.ExtractMediaSignature(path, nil) | jacs::simple::advanced::extract_media_signature_with_options(path, opts) |
Python examples use jacs.simple; Node examples use @hai.ai/jacs/simple and return Promises. Go calls are methods on *JacsSimpleAgent; extraction returns (payload string, present bool, err error). Python/Node extraction returns a JSON string or no payload, not a parsed object.
Per-format embedding
| Format | Where the signature lives | Notes |
|---|---|---|
| PNG | iTXt chunk with keyword JACS-Signature | Inserted before IEND. Lossless; existing chunks (incl. tEXt, pHYs, iCCP) are preserved. |
| JPEG | APP11 segment with marker JACS\0 | Inserted immediately after SOI. Existing markers preserved. |
| WebP | XMP chunk inside RIFF container | The RIFF header size field is updated. Lossless and lossy variants are both supported. |
The embedded payload is base64url-encoded JACS signed-document JSON (deliberately JSON, not YAML — images are binary containers and YAML's whitespace sensitivity adds no value). The general payload cap is 64 KiB; JPEG has a slightly smaller 65,528-byte segment cap. Oversized payloads fail validation.
Signed claim shape
{
"mediaSignatureVersion": 1,
"format": "png",
"canonicalization": "jacs-media-v1",
"hashAlgorithm": "sha256",
"contentHash": "BASE64URL_SHA256_DIGEST",
"embeddingChannels": ["metadata"],
"robust": false
}
Wrapped in a JACS signed document, the same shape supplies the content claims inspected by verify-image. Successful checks do not independently establish signer identity or a trusted timestamp.
Inline text vs media — why the asymmetry
Inline text uses YAML because humans skim it next to the markdown. Image payloads are read by tooling, not humans — JSON is what every JACS binding already canonicalises and signs. Both paths now carry full signed JACS documents; text renders that document as YAML between footer markers, while images embed the signed document JSON in the media container.
Robust mode (--robust) — opt-in LSB fallback
The default mode embeds in metadata chunks, which survives most pipelines but does not survive a deliberate "strip all metadata" pass. Robust mode uses an LSB (least-significant-bit) channel in the pixel data which can survive metadata stripping if the embedded pixel data remains intact. Resizing or lossy re-encoding can destroy it; verification must still check the result.
| Format | Robust support |
|---|---|
| PNG | Yes (--robust) |
| JPEG | Yes (--robust) |
| WebP | Deferred to a future release |
Robust claims use embeddingChannels: ["lsb"], canonicalization jacs-media-v1-robust and a pixelHash with the sha256-b64url: prefix. Capacity depends on image dimensions and encoding overhead; small images may not fit.
Robust mode is OFF by default because LSB embedding mutates pixel values. Opt in only when the metadata-strip threat model justifies the change.
After metadata stripping, enable LSB scanning explicitly with jacs-compat verify-image signed.png --robust or jacs-compat extract-media-signature signed.png --robust. Default verification/extraction does not scan this channel.
extract-media-signature — getting the payload back out
# Default — decoded JSON (ready to read or pipe to jq)
jacs-compat extract-media-signature signed.png
# Full signed document: jacsId, content, jacsSignature, ...
# Wire form — raw base64url payload (useful for re-embedding or transport)
jacs-compat extract-media-signature signed.png --raw-payload
# BASE64URL_OF_SIGNED_JACS_DOCUMENT
# Pipeline-friendly — extract the signer's claimed timestamp
jacs-compat extract-media-signature signed.png | jq -r '.jacsSignature.date'
# 2026-04-24T18:00:00Z
extract-media-signature does NOT verify the signature; it only decodes the embedded payload. Use verify-image for verification.
Single-signer and overwrite policy
Images are single-signer: re-running sign-image overwrites the embedded signature with the new one. Use --refuse-overwrite to opt into first-signer-wins:
# Default: overwrite if already signed
jacs-compat sign-image photo.png --out signed.png
# Refuse to overwrite — exit non-zero if already signed
jacs-compat sign-image photo.png --out signed.png --refuse-overwrite
This is intentionally narrower than inline text: text is reviewed and counter-signed by humans / agents; images are typically signed once at the point of capture or generation. If you need a multi-signer flow over images, stage the signatures off-image (e.g. as a JACS agreement that references contentHash).
Permissive vs strict verification
Require the returned media status to be valid. Other statuses include invalid_signature, hash_mismatch, key_not_found, unsupported_format and missing_signature; not every failure throws. Strict mode makes a missing signature an error. See text verification for the related per-block contract.
.bak policy
When sign-image overwrites an existing file, it writes a .bak next to the output (mode 0o600 on Unix — owner-read/write only). .bak files contain the unsigned original, which may include sensitive metadata. Treat them like any other plaintext source file: they are not encrypted, do not commit them, and remove them when no longer needed.
A new, nonexistent output path avoids an output backup. Overwriting an existing output can create a .bak even when its path differs from the input.
--key-dir override
Use independently authenticated <signer_id>.public.pem files, with the same filename encoding and resolver limitations as inline text:
jacs-compat verify-image signed.png --key-dir ./trusted-keys/
Caps and rejected inputs
Payloads above the format's size limit, unsupported formats, refused overwrites and unsupported robust-mode formats fail validation. Error representation varies by binding; use its documented result/error API rather than assuming a shared ErrorKind constructor.
Clean-room provenance
The jacs-media crate is licensed under Apache-2.0. Consult its manifest and the repository dependency notices for current licensing information.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
CLI Examples
This chapter provides practical examples of using the JACS CLI for common workflows.
Quick Reference
jacs-compat init # Initialize JACS (config + agent + keys)
jacs-compat agent create # Create a new agent
jacs-compat document create # Create a signed document
jacs-compat document verify # Verify a document signature
jacs-compat 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-compat 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-compat config read
# Verify your agent
jacs-compat 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-compat document create -f invoice.json
# Output shows the exact saved document path, for example:
# Saved signed document: jacs_data/<uuid>:<version>
Create with custom output:
# Specify output filename
jacs-compat document create -f invoice.json -o signed-invoice.json
# Machine-readable document key and path
jacs-compat document create -f invoice.json --json
# Print to stdout (don't save)
jacs-compat document create -f invoice.json --no-save
Create with file attachments:
# Create document with PDF attachment
jacs-compat document create -f contract.json --attach ./contract.pdf
# Embed attachment content in document
jacs-compat document create -f contract.json --attach ./contract.pdf --embed true
# Attach entire directory
jacs-compat document create -f report.json --attach ./attachments/
Create with custom schema:
# Use a custom schema for validation
jacs-compat document create -f order.json -s ./schemas/order.schema.json
Verifying Documents
Basic verification:
# Verify a document
jacs-compat 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-compat 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-compat document verify -d ./documents/
# With custom schema
jacs-compat 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-compat 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-compat document update -f project-v1.json -n updated.json -o project-v2.json
# Verify the updated document
jacs-compat document verify -f project-v2.json -v
Extracting Embedded Content
# Extract embedded files from a document
jacs-compat document extract -f ./document-with-attachments.json
# Extracts to: jacs_data/extracted/[document-id]/
# Extract from multiple documents
jacs-compat document extract -d ./documents/
Agreement Workflows
This section shows the legacy jacsAgreement sidecar commands for legacy signature inspection of an existing signed document. Native jacs-compat agreement-v2 also provides mathematical inspection without consent-policy acceptance; see Agreement v2 Developer Guide. New portable consent uses Agreement v3.
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-compat document create -f service-agreement.json -o agreement.json
# Create agreement requiring signatures from two agents
# (Use actual agent UUIDs)
jacs-compat 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-compat document sign-agreement -f agreement-pending.json -o agreement-signed-1.json
# Check status
jacs-compat 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=./agent2.config.json \
jacs-compat document sign-agreement -f agreement-signed-1.json -o agreement-complete.json
# Verify completion
jacs-compat document check-agreement -f agreement-complete.json
# Output:
# Inspect mathematical signatures; claimed completion is not policy acceptance.
# 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-compat document create -f contract.json -o contract-signed.json
# Step 2: Get agent IDs
SELLER_AGENT=$(jacs-compat 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-compat document create-agreement \
-f contract-signed.json \
-i "$SELLER_AGENT,$BUYER_AGENT" \
-o contract-agreement.json
# Step 4: Seller signs
echo "Seller signing..."
jacs-compat document sign-agreement \
-f contract-agreement.json \
-o contract-seller-signed.json
# Step 5: Check intermediate status
echo "Checking status..."
jacs-compat document check-agreement -f contract-seller-signed.json
# Step 6: Buyer signs
echo "Buyer signing..."
JACS_CONFIG=./buyer.config.json \
jacs-compat document sign-agreement \
-f contract-seller-signed.json \
-o contract-complete.json
# Step 7: Verify complete agreement
echo "Final verification..."
jacs-compat document verify -f contract-complete.json -v
jacs-compat 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-compat agent create --create-keys true -f my-agent.json
# Create agent using existing keys
jacs-compat agent create --create-keys false -f my-agent.json
DNS-Based Identity
Generate DNS record commands:
# Generate TXT record for your domain
jacs-compat 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-compat agent dns --domain myagent.example.com --provider aws
jacs-compat agent dns --domain myagent.example.com --provider cloudflare
jacs-compat agent dns --domain myagent.example.com --provider azure
# Custom TTL
jacs-compat agent dns --domain myagent.example.com --ttl 7200
Verify DNS-published agent:
# Look up agent by domain
jacs-compat agent lookup partner.example.com
# Require strict DNSSEC validation
jacs-compat agent lookup partner.example.com --strict
# Verify local agent file against DNS
jacs-compat agent verify -a ./partner-agent.json --require-strict-dns
Agent Verification
# Basic verification
jacs-compat agent verify
# Verify another agent's file
jacs-compat agent verify -a ./other-agent.json
# With DNS requirements
jacs-compat agent verify --require-dns # Require DNS (not strict)
jacs-compat agent verify --require-strict-dns # Require DNSSEC
jacs-compat agent verify --no-dns # Attempt optional DNS; fall back to embedded fingerprint
jacs-compat agent verify --ignore-dns # Skip DNS checks
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-compat 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-compat 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-compat 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=./production.config.json
jacs-compat document create -f invoice.json
# Override specific settings
export JACS_DATA_DIRECTORY=./custom-data
export JACS_KEY_DIRECTORY=./secure-keys
jacs-compat agent create --create-keys true
# One-time override
JACS_CONFIG=./test.config.json jacs-compat document verify -f test-doc.json
Multiple Configurations
# Development
alias jacs-dev='JACS_CONFIG=./dev.config.json jacs-compat'
jacs-dev document create -f test.json
# Production
alias jacs-prod='JACS_CONFIG=./prod.config.json jacs-compat'
jacs-prod document verify -f important.json
# Different agents
alias jacs-alice='JACS_CONFIG=./alice.config.json jacs-compat'
alias jacs-bob='JACS_CONFIG=./bob.config.json jacs-compat'
Error Handling
Understanding Exit Codes
jacs-compat document verify -f document.json
exit_code=$?
case $exit_code in
0) echo "Success" ;;
1) echo "General error" ;;
2) echo "Invalid arguments" ;;
*) echo "Unknown error: $exit_code" ;;
esac
Handling Failures
#!/bin/bash
# robust-signing.sh
sign_document() {
local input=$1
local output=$2
if ! jacs-compat document create -f "$input" -o "$output" 2>/dev/null; then
echo "Error: Failed to sign $input" >&2
return 1
fi
if ! jacs-compat 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
- CLI Command Reference - Complete command reference
- Configuration Reference - Configuration options
- Rust CLI Usage - Detailed CLI documentation
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Node.js Examples
Source and published packages: Version 0.16.1 has a recorded release inventory. Version 0.16.0 is the first release whose source includes the native PQ DID/JWK and Agent Card behavior. See the identity integration guide.
These examples use the native @hai.ai/jacs library. Run JavaScript snippets
with top-level await as ES modules (.mjs). Signing and document I/O offer
async methods; trust helpers and JacsSimpleAgent methods are synchronous.
npm install @hai.ai/jacs
Create, verify, and update a document
Quickstart creates a persistent identity or loads the config that already
exists. Set JACS_PRIVATE_KEY_PASSWORD securely in the process environment
for an identity you must reopen; do not put the password in source or command
arguments. See quickstart persistence.
import * as jacs from '@hai.ai/jacs/simple';
await jacs.quickstart({
name: 'invoice-agent', domain: 'billing.example.com',
configPath: './invoice-agent/jacs.config.json',
});
const original = await jacs.signMessage({
invoiceNumber: 'INV-001', amount: 1500, status: 'pending',
});
const result = await jacs.verify(original.raw);
if (!result.valid) throw new Error(result.errors.join(', '));
console.log(result.signerId, result.data);
// Update the full stored envelope, addressed by BOTH ID and version.
const envelope = JSON.parse(original.raw);
const key = `${envelope.jacsId}:${envelope.jacsVersion}`;
envelope.content.status = 'paid';
const updated = await jacs.updateDocument(key, envelope);
console.log(updated.documentId, JSON.parse(updated.raw).jacsVersion);
SignedDocument.documentId contains only jacsId. It is not the versioned
key accepted by updateDocument(). Passing a bare ID, the original JSON string
as the key, or a partial envelope does not implement a valid update.
Low-level document operations
Use JacsAgent when you need custom schemas, attachments, or explicit storage
control. This snippet loads the identity created above:
import { JacsAgent } from '@hai.ai/jacs';
const agent = new JacsAgent();
await agent.load('./invoice-agent/jacs.config.json');
const signed = await agent.createDocument(JSON.stringify({
title: 'Meeting notes', content: 'Review the quarterly budget.',
}));
try {
await agent.verifyDocument(signed);
console.log('Signature verified');
} catch (error) {
console.error('Verification failed:', error.message);
throw error;
}
const document = JSON.parse(signed);
const key = `${document.jacsId}:${document.jacsVersion}`;
document.title = 'Updated meeting notes';
const updated = await agent.updateDocument(key, JSON.stringify(document));
The native verifyDocument() can reject on invalid signatures. The simplified
verify() instead returns valid: false for document verification failures;
loading or initialization failures can still throw.
Sign and verify a file
With the simplified module initialized:
const signedFile = await jacs.signFile('./contract.pdf', true);
const verification = await jacs.verify(signedFile.raw);
if (!verification.valid) throw new Error(verification.errors.join(', '));
true embeds the attachment; false records a reference/hash. A portable
signature does not make referenced bytes available to another verifier.
For inline text/image signatures, see the simplified API. Extraction alone does not verify embedded media:
const payloadJson = await jacs.extractMediaSignature('./signed.png');
if (payloadJson !== null) console.log(JSON.parse(payloadJson));
const image = await jacs.verifyImage('./signed.png', { strict: true });
if (image.status !== 'valid') throw new Error('Image verification failed');
Express document exchange
Install Express, then run this as a separate server module:
import express from 'express';
import { JacsClient } from '@hai.ai/jacs/client';
import { jacsMiddleware } from '@hai.ai/jacs/express';
const client = await JacsClient.quickstart({
name: 'echo-server', domain: 'echo.example.com',
configPath: './echo-server/jacs.config.json',
});
const app = express();
app.get('/health', (_req, res) => res.json({ status: 'ok' }));
app.use('/api', express.text({ type: 'application/json' }));
app.use('/api', jacsMiddleware({ client, verify: true, sign: true }));
app.post('/api/echo', (req, res) => {
if (req.jacsPayload === undefined) return res.sendStatus(400);
res.json({ echo: req.jacsPayload });
});
app.listen(3000);
Provision signer verification keys on the receiving side. This example
verifies document bodies and signs res.json() output; it does not authenticate
an HTTP request's method, URL or audience or grant access to a route. For that
boundary, use request-bound HTTP authorization with caller
policy and atomic nonce consumption. The old /http adapter is not used by
this flow.
MCP
The Node MCP guide contains a complete stdio server using real SDK request schemas. It shows both signed tool outputs for ordinary MCP clients and signed transport between two JACS peers.
For signed transport, each peer needs its own identity/configuration,
locally provisioned verification keys, and expectedPeerAgentId or
allowedPeerAgentIds. Both ends need the wrapper. Signatures do not encrypt
MCP messages, and an output signature alone does not authorize tool execution.
Agreement signature inspection
This example uses the legacy sidecar API with a real local signer ID. Continue from the initialized simplified API in the first example:
const signerId = jacs.getAgentInfo().agentId;
const proposal = await jacs.signMessage({ task: 'review release notes' });
const agreement = await jacs.createAgreement(
proposal.raw, [signerId], 'Have you reviewed these notes?',
'Signature inspection example',
);
const signed = await jacs.signAgreement(agreement.raw);
const report = await jacs.checkAgreement(signed.raw);
console.log(report.signers, report.pending);
console.log(report.complete); // false: no authenticated agreement policy
The report can inspect mathematical signatures but keeps complete,
policy_authenticated, and policy_accepted false. It cannot establish
participant, role, quorum or consent policy. Agreement v2
is also inspection-only; an application must make its own acceptance and
execution decisions from trusted policy.
Store portable evidence
Store the complete signed document, not just an ID or a valid boolean:
import { writeFile, readFile } from 'node:fs/promises';
const signed = await jacs.signMessage({ title: 'Meeting notes' });
await writeFile('./meeting-notes.jacs.json', signed.raw, { flag: 'wx' });
const stored = await readFile('./meeting-notes.jacs.json', 'utf8');
const verification = await jacs.verify(stored);
if (!verification.valid) throw new Error(verification.errors.join(', '));
console.log(verification.data);
A storage filename or database row is not a trust decision. Select keys and expected signer policy independently, verify before use, and retain external attachments if your application needs them later.
Test tamper detection without disk configuration
This uses Node's built-in test runner and a disposable in-memory identity.
Save it as jacs.test.mjs and run node --test jacs.test.mjs:
import test from 'node:test';
import assert from 'node:assert/strict';
import { JacsClient } from '@hai.ai/jacs/client';
test('verifies its own document and rejects changed contents', async () => {
const client = await JacsClient.ephemeral(); // pq2025
const signed = await client.signMessage({ value: 42 });
const valid = await client.verify(signed.raw);
assert.equal(valid.valid, true);
const changed = JSON.parse(signed.raw);
changed.content.value = 99;
const invalid = await client.verify(JSON.stringify(changed));
assert.equal(invalid.valid, false);
});
A config file without generated keys and an agent document cannot be loaded as a test identity. Use ephemeral agents or quickstart with a unique temporary configuration directory.
Related references
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Python Examples
These recipes use JacsClient. See installation
for the native package and platform requirements.
Sign a tool result and reject tampering
import json
from jacs.client import JacsClient
client = JacsClient.ephemeral()
signed = client.sign_message({"tool": "calculator", "answer": 42})
assert client.verify(signed.raw).valid
tampered = json.loads(signed.raw)
tampered["content"] = {"answer": 43}
assert not client.verify(tampered).valid
This identity exists only in memory. Before acting on a verified result, also check the expected signer and your application's authorization policy.
Persist an identity and signed document
from pathlib import Path
from jacs.client import JacsClient
client = JacsClient.quickstart(name="publisher", domain="publisher.example.com")
signed = client.sign_message({"event": "report-ready", "report_id": "report-123"})
Path("report.json").write_text(signed.raw, encoding="utf-8")
received = Path("report.json").read_text(encoding="utf-8")
result = client.verify(received)
if not result.valid:
raise ValueError(result.errors)
print(result.signer_id)
Quickstart creates encrypted pq2025 keys or loads existing configuration.
Preserve its password through a host secret source or keychain; see
password bootstrap.
Sign an existing file
from jacs.client import JacsClient
client = JacsClient(config_path="./jacs.config.json")
signed = client.sign_file("report.pdf", embed=True)
assert client.verify(signed.raw).valid
Share the complete signed JSON. With embed=False, the envelope records the
file reference/hash; the receiver also needs the referenced bytes and an
external-file verification policy.
Return signed content from an MCP tool
Install jacs[mcp], then run this as an ordinary stdio MCP server:
from fastmcp import FastMCP
from jacs.client import JacsClient
client = JacsClient.quickstart(name="tools", domain="tools.example.com")
mcp = FastMCP("Signed tools")
@mcp.tool()
def calculate() -> dict:
"""Return a signed calculation result."""
signed = client.sign_message({"answer": 6 * 7})
return {"signed_document": signed.raw}
if __name__ == "__main__":
mcp.run(transport="stdio")
This signs result content; it does not authenticate the transport or caller.
The receiver verifies signed_document under its own trust policy.
For the primary JACS tool server use jacs mcp; for custom native tools and
the separate signed local HTTP transport, see Python MCP.
More recipes
- Framework adapters: LangChain, LangGraph, FastAPI and Anthropic.
- A2A serving: discovery documents and trust requirements.
- DID integration: identity binding without a blockchain.
- Simplified API: request authentication, signed events and agreement inspection.
- Native API reference: exact low-level arguments.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Integration Examples
Choose a maintained guide for the integration you need. Each guide identifies its supported surface and trust requirements.
| Integration | Walkthrough |
|---|---|
| Standard MCP clients | Portable MCP setup and extended profiles |
| Signed MCP transport | Node.js or Python; matching wrappers and peer trust are required at both ends |
| A2A discovery and messaging | A2A how-to and official SDK examples |
| W3C DID export and request proofs | W3C DID how-to |
| HTTP request authentication | Node.js HTTP protocol; bind method, URL, body and audience, then authorize independently |
| Document middleware | Express, Koa, FastAPI; document verification is not route authorization |
| LangChain / LangGraph | Node.js and Python adapters |
Source examples and tests describe this checkout. Consult the release inventory before relying on an API from an installed package.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
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-compat --version
Prints version and build information for the JACS installation.
jacs-compat --version
jacs-compat 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-compat quickstart --name my-agent --domain my-agent.example.com
# Sign JSON from stdin
echo '{"action":"approve"}' | jacs-compat quickstart --name my-agent --domain my-agent.example.com --sign
# Sign a file
jacs-compat quickstart --name my-agent --domain my-agent.example.com --sign --file mydata.json
Options:
--name <name>- Agent name used for first-time quickstart creation (required)--domain <domain>- Agent domain used for DNS/public-key verification workflows (required)--algorithm <algo>- Signing algorithm:pq2025(default) ored25519; the legacy input aliasring-Ed25519is also accepted and the canonical Ed25519 wire label remainsring-Ed25519--sign- Sign input (from stdin or--file) instead of printing info--file <path>- Read JSON input from file instead of stdin (requires--sign)
jacs-compat 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-compat verify signed-document.json
# JSON output (for scripting)
jacs-compat verify signed-document.json --json
# Verify a remote document
jacs-compat verify --remote https://example.com/signed-doc.json
# Specify a directory of public keys
jacs-compat verify signed-document.json --key-dir ./trusted-keys/
Options:
<file>- Path to the signed JACS JSON file (positional, required unless--remoteis 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-compat keychain
Manage private key passwords in the OS keychain (macOS Keychain or Linux Secret Service via D-Bus). This allows JACS to retrieve your private key password without environment variables or password files.
Requires the
keychainfeature (enabled by default injacs-cli-compat). SetJACS_KEYCHAIN_BACKEND=disabledto skip keychain lookups in CI/headless environments.
Every keychain command requires --agent-id so that each agent's password is stored separately. This prevents collisions when multiple agents coexist on the same machine.
# Store a password interactively (prompts for input)
jacs-compat keychain set --agent-id <AGENT_UUID>
# Retrieve the stored password (prints to stdout, for piping)
export JACS_PRIVATE_KEY_PASSWORD=$(jacs-compat keychain get --agent-id <AGENT_UUID>)
# Remove the stored password
jacs-compat keychain delete --agent-id <AGENT_UUID>
# Check keychain availability and whether a password is stored
jacs-compat keychain status --agent-id <AGENT_UUID>
| Subcommand | Description |
|---|---|
jacs-compat keychain set --agent-id <ID> | Store a password for an agent (prompts interactively) |
jacs-compat keychain get --agent-id <ID> | Print stored password to stdout |
jacs-compat keychain delete --agent-id <ID> | Remove stored password from OS keychain |
jacs-compat keychain status --agent-id <ID> | Check keychain availability and storage state |
Output conventions: Human-friendly messages go to stderr; machine-friendly data goes to stdout. This means jacs-compat keychain get output can be safely piped or captured in a variable.
Password resolution order: When JACS needs the private key password, it checks sources in this order:
JACS_PRIVATE_KEY_PASSWORDenvironment variable (highest priority)JACS_PASSWORD_FILE/ legacy.jacs_passwordfile- OS keychain keyed by agent ID (if
keychainfeature is enabled and not disabled)
jacs-compat init
Initialize JACS by creating both configuration and agent (with cryptographic keys). Use this for persistent agent setup. New agents get the selected native root (pq2025 by default) plus an ES256 ecosystem compatibility key; pass --no-compat-key to skip the compatibility key (minimal/air-gapped agents).
jacs-compat init
jacs-compat init --no-compat-key
jacs-compat --help
Print help information for JACS commands.
jacs-compat [COMMAND] --help
Configuration Commands
jacs-compat config
Work with JACS configuration settings.
jacs-compat config [SUBCOMMAND]
Note: Specific subcommands for config are not detailed in the current help output.
Agent Commands
jacs-compat agent
Work with JACS agents - the cryptographic identities that sign and verify documents.
jacs-compat agent [SUBCOMMAND]
jacs-compat agent add-compat-key
Add the ES256 ecosystem_signing compatibility key to an existing agent (explicit migration — loading never creates key material). New agents mint this key at creation unless --no-compat-key was used. Errors if the key already exists.
jacs-compat agent add-compat-key [--config ./jacs.config.json]
jacs-compat agent issue-compat-binding
Issue (or re-issue) the native-root-signed compatibility key binding. Content scopes (ap2-mandate, agreement-vc) are never auto-issued — grant them here explicitly. This is also the re-issue path after agent rotate-keys (a binding signed by a previous root no longer authorizes exports). The same grant is available from language bindings as issue_compat_binding_json(scopes_json, expires_at) (scopes as a JSON array, e.g. ["jwks","did","ap2-mandate"]), so Python/Node/Go callers can enable content exports without shelling out to the CLI.
# default: identity scopes only (jwks,did,a2a-agent-card,w3c-agent-identity)
jacs-compat agent issue-compat-binding
# grant a content scope explicitly
jacs-compat agent issue-compat-binding --scopes jwks,did,a2a-agent-card,w3c-agent-identity,ap2-mandate
# optional expiry (RFC 3339)
jacs-compat agent issue-compat-binding --expires-at 2027-01-01T00:00:00Z
Agreement VC Export
jacs-compat agreement-v2 export-vc
Export an Agreement-v2 document as a W3C Verifiable Credential with an ecdsa-jcs-2019 Data Integrity proof (W3C vc-di-ecdsa, Multikey verification method). Requires the agreement-vc binding scope. The native agreement is unchanged; incoming VC verification is out of scope.
jacs-compat agreement-v2 export-vc --agreement agreement.json # file path
cat agreement.json | jacs-compat agreement-v2 export-vc --agreement - # stdin
AP2 Commands
jacs-compat ap2 export-mandate
Export the AP2 merchant-authorization mandate for a UCP checkout as a detached ES256 JWS (UCP AP2-Mandates extension, rev 2026-01-23). Requires the ap2-mandate binding scope. Input is validated against the named ap2-mandate schema; user-side AP2 Checkout Mandates (SD-JWT-VC) are out of scope. See AP2 Mandate Export.
jacs-compat ap2 export-mandate --input checkout.json # file path
jacs-compat ap2 export-mandate --input '{"id":"c1", ... }' # inline JSON
cat checkout.json | jacs-compat ap2 export-mandate --input - # stdin
Document Commands
The jacs-compat document command provides comprehensive document management capabilities.
jacs-compat document create
Create a new JACS document, either by embedding or parsing a document with optional file attachments.
Usage:
jacs-compat document create [OPTIONS]
Options:
-a <agent-file>- Path to the agent file. If not specified, uses configjacs_agent_id_and_version-f <filename>- Path to input file. Must be JSON format-o, --output <output>- Output filename for the created document (cannot be combined with--directory)-d <directory>- Path to directory of files. Files should end with.json-v, --verbose- Enable verbose output--json- Emit a machine-readable result containing each document key and saved path-n, --no-save- Instead of saving files, print to stdout-s, --schema <schema>- Path to JSON schema file to use for validation--attach <attach>- Path to file or directory for file attachments-e, --embed <embed>- Embed documents or keep them external [possible values: true, false]-h, --help- Print help information
Examples:
# Create document from JSON file
jacs-compat document create -f my-document.json
# Prints: Saved signed document: <actual path>
# Create document with embedded attachment
jacs-compat document create -f document.json --attach ./image.jpg --embed true
# Create document with referenced attachment
jacs-compat document create -f document.json --attach ./data.csv --embed false
# Create from directory of JSON files
jacs-compat document create -d ./documents/
# Create with custom schema validation
jacs-compat document create -f document.json -s custom-schema.json
# Print to stdout instead of saving
jacs-compat document create -f document.json --no-save
jacs-compat document update
Create a new version of an existing document. Requires both the original JACS file and the modified JACS metadata.
Usage:
jacs-compat 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-compat document update -f original.json -n modified.json -o updated.json
# Update and add new attachments
jacs-compat document update -f original.json -n modified.json --attach ./new-file.pdf --embed false
jacs-compat document verify
Verify a document's hash, signatures, and schema compliance.
Usage:
jacs-compat 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-compat document verify -f signed-document.json
# Verify all documents in directory
jacs-compat document verify -d ./documents/
# Verify with custom schema
jacs-compat document verify -f document.json -s custom-schema.json
Verification Process:
- Hash verification - Confirms document integrity
- Signature verification - Validates cryptographic signatures
- Schema validation - Ensures document structure compliance
- File integrity - Checks SHA256 checksums of attached files
jacs-compat document extract
Extract embedded file contents from documents back to the filesystem.
Usage:
jacs-compat 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-compat document extract -f document-with-embedded-files.json
# Extract from all documents in directory
jacs-compat document extract -d ./documents/
Extract Process:
- Reads embedded file contents from document
- Decodes base64-encoded data
- Writes files to their original paths
- Creates backup of existing files (with timestamp)
Agreement Commands
The jacs-compat agreement-v2 commands inspect native compatibility agreement
documents; mathematical validity is separate from application policy acceptance.
The jacs-compat document *-agreement commands below inspect legacy sidecar
signatures without authenticating their participant or quorum policy. New
portable consent uses
Agreement v3.
jacs-compat document check-agreement
Given a document, provide a list of agents that should sign the document.
Usage:
jacs-compat document check-agreement [OPTIONS]
jacs-compat document create-agreement
Create an agreement structure for a document that requires multiple agent signatures.
Usage:
jacs-compat document create-agreement [OPTIONS]
jacs-compat document sign-agreement
Sign the agreement section of a document with the current agent's cryptographic signature.
Usage:
jacs-compat document sign-agreement [OPTIONS]
jacs-compat agreement-v2
Standalone Agreement v2 documents provide compatibility inspection, not consent-policy acceptance. Each is a self-contained, cryptographically signed jacsType: "agreement" artifact — terms, parties, signature policy (quorum, required roles), an optional transcript, links, controllers, and owners — with its own content hash and version chain. Running jacs-compat agreement-v2 with no subcommand prints help and exits non-zero.
Typical order: create → sign → verify. Use apply to evolve an agreement via a typed mutation; use the branch commands when two agents diverge from the same prior version.
jacs-compat agreement-v2 create
Create a standalone agreement v2 document from a CreateAgreementV2 JSON input.
jacs-compat agreement-v2 create --input <CreateAgreementV2 JSON | path | ->
jacs-compat agreement-v2 sign
Add this agent's signature as a signer, witness, or notary.
jacs-compat agreement-v2 sign --agreement <JSON | path | -> [--role signer|witness|notary]
--role defaults to signer.
jacs-compat agreement-v2 verify
Recompute the agreement and transcript hashes and re-check quorum, roles, status, and every signature. Exits non-zero when the agreement is invalid.
jacs-compat agreement-v2 verify --agreement <JSON | path | ->
jacs-compat agreement-v2 apply
Apply a typed mutation (appendTranscript, updateTerms, setStatus, addLink, setParties, setPolicy, setOwners) and emit a successor version. Mutation fields are camelCase.
jacs-compat agreement-v2 apply --agreement <JSON | path | -> --mutation <mutation JSON | path>
jacs-compat agreement-v2 detect-conflict
Report whether two successor versions of --base are transcript-only (auto-mergeable) or conflict on consent-scope fields.
jacs-compat agreement-v2 detect-conflict --base <common ancestor> --left <branch> --right <branch>
jacs-compat agreement-v2 merge-transcript
Auto-merge two transcript-only branches of --base into one successor version.
jacs-compat agreement-v2 merge-transcript --base <common ancestor> --left <branch> --right <branch>
jacs-compat agreement-v2 resolve-conflict
Resolve a consent-scope conflict by rebasing an explicit mutation onto --previous; the divergent --side-branch is recorded as a link on the result.
jacs-compat agreement-v2 resolve-conflict \
--base <common ancestor> \
--previous <branch you keep> \
--side-branch <divergent branch> \
--mutation <resolving mutation JSON | path>
--side-branch replaces the older --side flag; --side still works as a hidden alias for backward compatibility.
Provenance Commands
The provenance commands sign and verify content in place — text files keep a YAML-bodied signature block at the end of the file, images carry the signature in a metadata chunk. See the Inline Text Signatures and Image and Media Signatures guides for the full feature set.
jacs-compat sign-text
Append a YAML-bodied JACS signature block to the end of a markdown or text file. The file content (everything before the first signature block) is preserved byte-for-byte.
Usage:
jacs-compat sign-text <file>
The command refuses (hard error) to sign input that contains a column-zero -----BEGIN JACS SIGNATURE----- line outside a valid block — see the inline-text-signing guide for documented workarounds.
jacs-compat verify-text
Verify all signature blocks in a text file. Permissive by default — missing signatures are exit code 2, not a thrown error. Per-signer status is printed to stdout.
Usage:
jacs-compat verify-text <file> [--strict] [--key-dir <dir>]
Options:
--strict— treat missing signature as exit1(instead of2); stderr:no JACS signature found.--key-dir <dir>— directory of<signer_id>.public.pemfiles used in addition to the local trust store.
jacs-compat sign-image
Embed a JACS signature inside a PNG, JPEG, or WebP image. The signature lives in a metadata chunk (PNG iTXt / JPEG APP11 / WebP XMP).
Usage:
jacs-compat sign-image <input> --out <output> [--robust] [--refuse-overwrite]
Options:
--out <path>— output file (required).--robust— opt-in LSB embedding fallback for PNG/JPEG (off by default; WebP deferred).--refuse-overwrite— exit non-zero if the input already carries a JACS signature (default is overwrite).
jacs-compat verify-image
Verify the embedded signature on an image. Same permissive / strict model as verify-text.
Usage:
jacs-compat verify-image <file> [--strict] [--key-dir <dir>]
jacs-compat extract-media-signature
Extract the embedded signature payload without verifying it.
Usage:
jacs-compat extract-media-signature <file> [--raw-payload]
Options:
--raw-payload— emit the base64url wire form instead of decoded JSON.
Common Patterns
Basic Document Lifecycle
# 1. Initialize JACS
jacs-compat init
# 2. Create document with attachments
jacs-compat document create -f document.json --attach ./files/ --embed true
# 3. Verify document integrity
jacs-compat document verify -f created-document.json
# 4. Update document if needed
jacs-compat document update -f original.json -n modified.json
# 5. Extract embedded files when needed
jacs-compat document extract -f document.json
Working with Attachments
# Embed small files for portability
jacs-compat document create -f doc.json --attach ./small-image.png --embed true
# Reference large files to save space
jacs-compat document create -f doc.json --attach ./large-video.mp4 --embed false
# Attach multiple files from directory
jacs-compat document create -f doc.json --attach ./attachments/ --embed false
Schema Validation Workflow
# Create with schema validation
jacs-compat document create -f document.json -s schema.json
# Verify against specific schema
jacs-compat 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
Exit Codes
See CLI exit codes. Inspect the command's result fields as well as process status; Agreement v2 inspection never grants policy acceptance.
Environment Variables
JACS_CONFIG- Override default configuration file locationJACS_DATA_DIRECTORY- Override default data directory location
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
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
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
JACS_KEY_RESOLUTION is an environment-only comma-separated source list.
The default is local,registry: local public-key cache/trust data, then the
configured registry when network policy permits. dns looks up the publisher's
TXT fingerprint and fetches the exact signed version's key over HTTPS when
JACS_ALLOW_DNS and JACS_ALLOW_REMOTE_KEY_FETCH are set; see the
key discovery guide. hai is not a source name. Use independently trusted key evidence
and the DNS guide for domain anchoring.
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-compat 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
Use quickstart() to create and sign a persistent config. A creation template
must include the schema's six required settings:
{
"$schema": "https://hai.ai/schemas/jacs.config.schema.json",
"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"
}
This unsigned template does not load an existing identity. Existing-agent configs also need the generated identity/version and valid config signature; do not reconstruct them by editing this template.
Signed-config integrity
Persistent agent configs are verified before JACS applies any config-controlled key path, storage backend, database URL, or network setting. A bad hash, signature, signer/identity mismatch, algorithm mismatch, unsafe key path, or invalid rotation journal stops the load; JACS does not continue with the tampered settings.
An unsigned file may be used as a template while creating a new identity. Loading an existing identity from an unsigned file is rejected by default. For a one-time, audited legacy migration only, set JACS_ALLOW_UNSIGNED_AGENT_CONFIG=true, load and immediately re-sign the config, then remove the variable. This compatibility path emits a security_outcome WARN event and increments jacs_security_outcomes_total with source="config", outcome="unverified", and policy="permissive".
Config signatures prove integrity relative to the locally resolved signing key. They do not by themselves prove a real-world identity; deployments that need that guarantee must also pin or independently trust the agent key and protect the config/key directories from replacement.
Complete Example Configuration
{
"$schema": "https://hai.ai/schemas/jacs.config.schema.json",
"jacs_use_security": "false",
"jacs_data_directory": "./jacs_data",
"jacs_key_directory": "./jacs_keys",
"jacs_agent_private_key_filename": "jacs.private.pem.enc",
"jacs_agent_public_key_filename": "jacs.public.pem",
"jacs_agent_key_algorithm": "pq2025",
"jacs_default_storage": "fs",
"observability": {
"logs": {
"enabled": true,
"level": "info",
"destination": {
"type": "file",
"path": "./logs"
},
"headers": {
"Authorization": "Bearer token",
"X-API-Key": "secret"
}
},
"metrics": {
"enabled": true,
"destination": {
"type": "prometheus",
"endpoint": "http://localhost:9090/api/v1/write",
"headers": {
"Authorization": "Basic dXNlcjpwYXNz"
}
},
"export_interval_seconds": 60,
"headers": {
"X-Service": "jacs"
}
},
"tracing": {
"enabled": true,
"sampling": {
"ratio": 0.1,
"parent_based": true,
"rate_limit": 100
},
"resource": {
"service_name": "jacs",
"service_version": "0.4.0",
"environment": "production",
"attributes": {
"team": "platform",
"region": "us-west-2"
}
}
}
}
}
Observability Configuration
JACS supports comprehensive observability through configurable logging, metrics, and tracing. All observability features are optional and can be configured in the jacs.config.json file.
For the P2 compatibility/export WARN events and their jacs_*_total Prometheus counters (what to alert on and the likely fix), see Failure Modes.
Logs Configuration
Controls how JACS generates and outputs log messages.
| Field | Type | Required | Description |
|---|---|---|---|
enabled | boolean | Yes | Whether logging is enabled |
level | string | Yes | Minimum log level: trace, debug, info, warn, error |
destination | object | Yes | Where logs are sent (see destinations below) |
headers | object | No | Additional 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.
| Field | Type | Required | Description |
|---|---|---|---|
enabled | boolean | Yes | Whether metrics collection is enabled |
destination | object | Yes | Where metrics are exported (see destinations below) |
export_interval_seconds | integer | No | How often to export metrics (default: 60) |
headers | object | No | Additional 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.
| Field | Type | Required | Description |
|---|---|---|---|
enabled | boolean | Yes | Whether tracing is enabled |
sampling | object | No | Sampling configuration (see below) |
resource | object | No | Service identification (see below) |
Sampling Configuration
Controls which traces are collected to manage overhead.
| Field | Type | Default | Description |
|---|---|---|---|
ratio | number | 1.0 | Fraction of traces to sample (0.0-1.0) |
parent_based | boolean | true | Whether to respect parent trace sampling decisions |
rate_limit | integer | none | Maximum 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.
| Field | Type | Required | Description |
|---|---|---|---|
service_name | string | Yes | Name of the service |
service_version | string | No | Version of the service |
environment | string | No | Environment (dev, staging, prod) |
attributes | object | No | Custom key-value attributes |
Authentication & Headers
For remote destinations (OTLP, Prometheus), you can specify authentication headers:
Bearer Token Authentication:
"headers": {
"Authorization": "Bearer your-token-here"
}
Basic Authentication:
"headers": {
"Authorization": "Basic dXNlcjpwYXNz"
}
API Key Authentication:
"headers": {
"X-API-Key": "your-api-key",
"X-Auth-Token": "your-auth-token"
}
Common Patterns
Development Configuration
"observability": {
"logs": {
"enabled": true,
"level": "debug",
"destination": { "type": "stderr" }
},
"metrics": {
"enabled": true,
"destination": { "type": "stdout" }
}
}
Production Configuration
"observability": {
"logs": {
"enabled": true,
"level": "info",
"destination": {
"type": "otlp",
"endpoint": "https://logs.example.com:4317",
"headers": {
"Authorization": "Bearer prod-token"
}
}
},
"metrics": {
"enabled": true,
"destination": {
"type": "prometheus",
"endpoint": "https://metrics.example.com/api/v1/write"
},
"export_interval_seconds": 30
},
"tracing": {
"enabled": true,
"sampling": {
"ratio": 0.05,
"rate_limit": 100
},
"resource": {
"service_name": "jacs",
"service_version": "0.4.0",
"environment": "production"
}
}
}
File-based Configuration
"observability": {
"logs": {
"enabled": true,
"level": "info",
"destination": {
"type": "file",
"path": "/var/log/jacs"
}
},
"metrics": {
"enabled": true,
"destination": {
"type": "file",
"path": "/var/log/jacs/metrics.txt"
},
"export_interval_seconds": 60
}
}
Environment Variable Integration
The observability configuration works alongside JACS's core configuration system.
Required Environment Variable
Only one environment variable is truly required (unless using the OS keychain):
JACS_PRIVATE_KEY_PASSWORD- Password for encrypting/decrypting private keys (required for cryptographic operations, unless the password is stored in the OS keychain)
Configuration-Based Settings
All other JACS settings are configuration file fields that have sensible defaults:
jacs_data_directory- Where agent/document data is stored (default:./jacs_data)jacs_key_directory- Where cryptographic keys are stored (default:./jacs_keys)jacs_agent_key_algorithm- The agent's native signing algorithm (default:pq2025). New creation honorspq2025,ed25519, or the legacy input aliasring-Ed25519, then records the canonical value actually minted (pq2025orring-Ed25519). Rotation preserves this value unless an Ed25519 identity explicitly upgrades topq2025; PQ-to-Ed25519 downgrade is rejected.jacs_default_storage- Storage backend (default:fs)jacs_keychain_backend- OS keychain backend for password storage (default:"auto"). See below.jacs_use_security/JACS_ENABLE_FILESYSTEM_QUARANTINE- Enable filesystem quarantine of executable files (default:false). The env varJACS_USE_SECURITYis deprecated; useJACS_ENABLE_FILESYSTEM_QUARANTINEinstead.
OS Keychain Configuration
The jacs_keychain_backend field controls whether JACS looks up the private key password from the OS credential store (macOS Keychain or Linux Secret Service):
{
"jacs_keychain_backend": "auto"
}
| Value | Description |
|---|---|
"auto" | Detect the platform default (macOS Keychain or Linux Secret Service). This is the default when the field is omitted. |
"macos-keychain" | Explicitly use the macOS Keychain |
"linux-secret-service" | Explicitly use the Linux D-Bus Secret Service (GNOME Keyring, KDE Wallet, KeePassXC) |
"disabled" | Never consult the OS keychain. Recommended for CI, headless servers, and containers. |
You can also override via environment variable:
export JACS_KEYCHAIN_BACKEND=disabled # skip keychain in CI
An explicit instance password takes precedence. Otherwise resolution checks the password environment variable, the selected/legacy password file, then the agent-specific OS keychain. The CLI rejects simultaneously configured explicit environment and password-file sources. See the CLI keychain commands for storing and managing passwords.
These can be overridden by environment variables if needed, but they are primarily configured through the jacs.config.json file.
The observability configuration is completely optional - JACS will work without any observability configuration.
Storage Configuration
The jacs_default_storage field determines where JACS stores agent data, documents, and keys. This is a critical configuration that affects how your data is persisted and accessed.
Available Storage Backends
| Backend | Value | Description | Use Case |
|---|---|---|---|
| Filesystem | "fs" | Signed JSON documents on local disk | Default, development, single-node deployments |
| Local Indexed SQLite | "rusqlite" | Signed documents in SQLite with FTS search | Local search, bindings, MCP |
| AWS S3 | "aws" | Amazon S3 object storage (requires the Rust s3 feature) | Remote object storage |
| Memory | "memory" | In-memory object storage (non-persistent) | Testing, temporary data |
| Web Local | "local" | Browser local storage (WASM only) | Web applications |
For local indexed document search in JACS core, use "rusqlite". Additional database backends such as PostgreSQL, DuckDB, Redb, and SurrealDB live in separate crates and are not core jacs_default_storage values.
Backend-Specific Configuration
Filesystem Storage ("fs")
{
"jacs_default_storage": "fs",
"jacs_data_directory": "./jacs_data",
"jacs_key_directory": "./jacs_keys"
}
Requirements: None - works out of the box Data location: Local directories as specified in config Best for: Development, local testing, single-machine deployments
Local Indexed SQLite ("rusqlite")
{
"jacs_default_storage": "rusqlite",
"jacs_data_directory": "./jacs_data",
"jacs_key_directory": "./jacs_keys"
}
Requirements: Built with the default sqlite Cargo feature
Database path: <jacs_data_directory>/jacs_documents.sqlite3
Best for: Local full-text search, MCP/binding document operations, single-machine deployments that want indexed reads
AWS S3 Storage ("aws")
AWS support is not compiled into the default JACS build. Enable it explicitly:
cargo add jacs --features s3
Without that feature, selecting "aws" fails with an actionable configuration
error instead of silently activating the cloud dependency stack.
{
"jacs_default_storage": "aws"
}
Required Environment Variables:
JACS_ENABLE_AWS_BUCKET_NAME- S3 bucket nameAWS_ACCESS_KEY_ID- AWS access keyAWS_SECRET_ACCESS_KEY- AWS secret keyAWS_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 underjacs_data/documents/. - Rusqlite (
rusqlite) stores signed documents injacs_data/jacs_documents.sqlite3and is the indexedDocumentServicepath used by bindings and MCP. - Agent files and keys remain path-based assets under
jacs_data/andjacs_keys/. - Observability data (logs, metrics) can use separate storage via observability configuration.
DocumentService Guarantees
- Every
DocumentServiceread verifies the stored JACS document before returning it. - Every
create()andupdate()verifies the signed document before persisting it. - If an update payload changes a signed JACS document without re-signing it, the write fails.
- Visibility changes create a new signed version instead of mutating metadata in place.
Configuration Examples
Development Setup (Filesystem)
{
"jacs_default_storage": "fs",
"jacs_data_directory": "./dev_data",
"jacs_key_directory": "./dev_keys"
}
Production Setup (AWS S3)
Build the application with jacs's s3 feature before selecting this backend.
{
"jacs_default_storage": "aws"
}
With environment variables:
export JACS_ENABLE_AWS_BUCKET_NAME="my-jacs-production-bucket"
export AWS_ACCESS_KEY_ID="AKIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_REGION="us-west-2"
Security Considerations
- AWS S3: Ensure proper IAM permissions for bucket access
- Rusqlite: Protect the local database file with the same filesystem permissions you use for other signed artifacts
- Filesystem: Ensure proper file system permissions for data and key directories
- Keys: Regardless of storage backend, always set
JACS_PRIVATE_KEY_PASSWORDfor key encryption
Migration Between Storage Backends
When changing storage backends, you'll need to:
- Export existing data from the current backend
- Update the
jacs_default_storageconfiguration - Set any required environment variables for the new backend
- Import data into the new backend
JACS doesn't automatically migrate data between storage backends - this must be done manually or via custom scripts.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Error Codes
This reference documents error codes and messages you may encounter when using JACS.
CLI Exit Codes
| Code | Meaning |
|---|---|
| 0 | Command completed; inspect the operation's result fields before accepting evidence. |
| 1 | Operation/verification error; strict text/image verification also uses this for a missing signature. |
| 2 | Argument parsing error, or permissive text/image inspection found no signature. |
The native CLI does not assign universal file/schema/signature codes 3–5. Use the command's structured output and stderr, not an invented global taxonomy.
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-compat init
# Or specify a custom config path
JACS_CONFIG=./custom.config.json jacs-compat 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-compat 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-compat 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-compat 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-compat document create -f doc.json
Algorithm Mismatch
Error: Key algorithm 'ring-Ed25519' does not match configured algorithm 'pq2025'
Cause: The key file was created with a different algorithm than configured.
Solution:
- Update config to match key algorithm, or
- Regenerate keys with the correct algorithm
Signature Errors
Verification Failed
Error: Document verification failed: signature does not match content
Cause: Document content has been modified after signing.
Solution:
- The document may have been tampered with
- Re-sign the document if you have the original content
Missing jacsSignature Field
Error: Document missing jacsSignature field
Cause: Document was not signed or signature was removed.
Solution:
# Create a signed document
jacs-compat document create -f unsigned-doc.json
Missing Signature (v0.10.0)
Error / Status: MissingSignature — surfaced by verify-text / verify-image (and the corresponding binding methods) when the input file does not carry an embedded JACS signature.
Default behaviour is permissive — missing signature is a typed status, not a thrown error. Strict mode opts in to treating it as an error.
| Surface | Permissive (default) | Strict (--strict / strict=True / { strict: true } / Strict: true) |
|---|---|---|
| CLI | Exit code 2; stdout reports missing_signature | Exit code 1; stderr: no JACS signature found |
Python (jacs.simple / JacsClient) | Returns result with status == "missing_signature" | Raises MissingSignatureError |
Node (@hai.ai/jacs/simple / JacsClient) | Returns result with status === "missing_signature" | Promise rejects with error message containing MissingSignature |
Go (jacsgo) | Returns result with Status == "missing_signature" | Returns error matching errors.Is(err, jacs.ErrMissingSignature) |
Rust (jacs::simple::advanced) | Successful result with a missing-signature variant/status | Err(JacsError::MissingSignature) |
When to pick strict. Pick strict when a missing signature is genuinely a failure (release-gate, post-merge audit, CI on a branch that should always be signed). Pick permissive when verification runs against files that may legitimately be unsigned (the first PR introducing a signature).
Solution: sign the file, or — if missing signatures are expected — switch the call site to permissive mode and inspect result.status.
Invalid Signature Format
Error: Invalid signature format: expected base64 encoded string
Cause: The signature field is malformed.
Solution:
- Re-sign the document
- Verify the document hasn't been corrupted
Unknown Signing Algorithm
Error: Unknown signing algorithm: unknown-algo
Cause: Document was signed with an unsupported algorithm.
Solution:
- Use a supported user-facing algorithm for new keys:
ed25519,pq2025(ring-Ed25519remains a legacy input and canonical wire label)
DNS Verification Errors
DNSSEC Validation Failed
Error: strict DNSSEC validation failed for <owner> (TXT not authenticated). Enable DNSSEC and publish DS at registrar
Cause: DNSSEC mode was requested but the TXT response wasn't authenticated.
Solution:
- Enable DNSSEC for your domain zone
- Publish the DS record at your registrar
- 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:
- Verify the TXT record was created:
dig _v1.agent.jacs.yourdomain.com TXT - Wait for DNS propagation (can take up to 48 hours)
- 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-dnsduring initial setup:jacs-compat 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-compat 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.agentIDscan 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-compat document verify -f doc.json -v
# Environment variable
export JACS_DEBUG=true
Check Configuration
# Display current configuration
jacs-compat config read
Verify Agent
# Verify agent is properly configured
jacs-compat agent verify -v
Test Signing
# Create a test document
echo '{"test": true}' > test.json
jacs-compat document create -f test.json -v
See Also
- Configuration Reference - Configuration options
- CLI Command Reference - CLI usage
- Security Model - Security details
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
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
| Field | Type | Description |
|---|---|---|
valid | boolean | Overall result. true only if all sub-checks pass. |
crypto | object | Cryptographic verification results. |
evidence | array | Per-evidence-ref verification results (full tier only). |
chain | object|null | Derivation chain verification (full tier only, if derivation exists). |
errors | array | Human-readable error messages for any failures. |
crypto Object
| Field | Type | Description |
|---|---|---|
signature_valid | boolean | The cryptographic signature matches the document content and the signer's public key. |
hash_valid | boolean | The 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.
| Field | Type | Description |
|---|---|---|
kind | string | Evidence type (a2a, email, jwt, tlsnotary, custom). |
digest_valid | boolean | The evidence digest matches the expected value. |
freshness_valid | boolean | The collectedAt timestamp is within acceptable bounds. |
errors | array | Error 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. CheckcollectedAtand your freshness policy.
chain Object (Full Tier Only)
Present only when the attestation has a derivation field.
| Field | Type | Description |
|---|---|---|
depth | number | Number of links in the derivation chain. |
all_links_valid | boolean | Every derivation link verified successfully. |
links | array | Per-link verification details. |
Each link in links:
| Field | Type | Description |
|---|---|---|
input_digests_valid | boolean | Input digests match the referenced documents. |
output_digests_valid | boolean | Output digests match the transformation result. |
transform | object | Transform 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.
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
CLI Reference: jacs-compat attest
The jacs-compat attest command creates and verifies attestation documents from
the command line. Attestation extends basic signing with structured claims,
evidence references, and derivation chains.
jacs-compat attest create
Create a signed attestation document.
Synopsis
jacs-compat attest create --claims '<JSON>' [options]
Options
| Flag | Required | Description |
|---|---|---|
--claims '<JSON>' | Yes | JSON array of claims. Each claim must have name and value fields. |
--subject-type <TYPE> | No | Type of subject: agent, artifact, workflow, identity. Default: derived from context. |
--subject-id <ID> | No | Identifier of the subject being attested. |
--subject-digest <SHA256> | No | SHA-256 digest of the subject content. |
--evidence '<JSON>' | No | JSON array of evidence references. |
--from-document <FILE> | No | Lift an existing signed JACS document into an attestation. Overrides subject flags. |
-o, --output <FILE> | No | Write attestation to file instead of stdout. |
Examples
Create a basic attestation:
jacs-compat 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-compat 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-compat attest create \
--from-document mydata.signed.json \
--claims '[{"name": "approved", "value": true}]'
With evidence references:
jacs-compat 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-compat attest create \
--subject-type artifact \
--subject-id "doc-001" \
--subject-digest "abc123..." \
--claims '[{"name": "ok", "value": true}]' \
-o attestation.json
jacs-compat attest verify
Verify an attestation document.
Synopsis
jacs-compat attest verify <FILE> [options]
Arguments
| Argument | Required | Description |
|---|---|---|
<FILE> | Yes | Path to the attestation JSON file to verify. |
Options
| Flag | Required | Description |
|---|---|---|
--full | No | Use full verification (evidence + derivation chain). Default: local verification (crypto + hash only). |
--json | No | Output the verification result as JSON. |
--key-dir <DIR> | No | Directory containing public keys for verification. |
--max-depth <N> | No | Maximum derivation chain depth. Default: 10. |
Examples
Basic verification (local tier):
jacs-compat attest verify attestation.json
Output:
Attestation verification: VALID
Signature: OK
Hash: OK
Signer: agent-id-abc123
Algorithm: ring-Ed25519
Full verification:
jacs-compat 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-compat 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-compat attest verify attestation.json --key-dir ./trusted_keys/
Pipe through jq:
jacs-compat attest verify attestation.json --json | jq '.crypto'
Piping and Scripting Patterns
Create and verify in one pipeline
jacs-compat attest create \
--subject-type artifact \
--subject-id "doc-001" \
--subject-digest "abc..." \
--claims '[{"name": "ok", "value": true}]' \
-o att.json && \
jacs-compat attest verify att.json --json | jq '.valid'
Check validity in a script
#!/bin/bash
set -e
RESULT=$(jacs-compat 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-compat attest verify "$file" --json | jq -r '.valid'
done
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success (create: attestation created; verify: attestation valid) |
| 1 | Failure (create: error creating attestation; verify: attestation invalid or error) |
Environment Variables
| Variable | Description |
|---|---|
JACS_PRIVATE_KEY_PASSWORD | Password for the agent's private key |
JACS_MAX_DERIVATION_DEPTH | Override maximum derivation chain depth (default: 10) |
JACS_DATA_DIRECTORY | Directory for JACS data files |
JACS_KEY_DIRECTORY | Directory containing keys |
JACS_AGENT_ID_AND_VERSION | Agent identity for signing |
See Also
- Sign vs. Attest Decision Guide
- Attestation Tutorial
- Attestation Verification Results
- CLI Command Reference
Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
Migration Guide
Source and published packages: Version 0.16.1 has a recorded release inventory. Version 0.16.0 is the first release whose source includes the native PQ DID/JWK and Agent Card behavior. See the identity integration guide.
Version-numbered sections below are historical migration notes, not the current install or API reference. Use the package guide and current language chapters for supported calls, defaults and profiles. The algorithm migration section describes current explicit upgrades.
Version Compatibility
JACS maintains backward compatibility for document verification:
- Documents signed with older versions can be verified with newer versions
- Older JACS versions cannot verify documents using newer cryptographic algorithms
0.9 → 0.10
v0.10.0 adds inline text signatures, image / media embedding, and the new MissingSignature error variant. No existing API changes behaviour.
Additive surface — no opt-in required
Five new methods per binding, five new CLI verbs, five new MCP tools. Pre-existing call sites are unaffected.
| Domain | New surface |
|---|---|
| CLI | jacs-compat sign-text, jacs-compat verify-text, jacs-compat sign-image, jacs-compat verify-image, jacs-compat extract-media-signature |
Python (jacs.simple / JacsClient) | sign_text, verify_text, sign_image, verify_image, extract_media_signature |
Node (@hai.ai/jacs/simple / JacsClient) | signText, verifyText, signImage, verifyImage, extractMediaSignature |
Go (jacsgo) | SignText, VerifyText, SignImage, VerifyImage, ExtractMediaSignature |
| MCP | jacs_sign_text, jacs_verify_text, jacs_sign_image, jacs_verify_image, jacs_extract_media_signature |
Breaking consideration: ErrorKind exhaustive matches
The new MissingSignature variant on jacs::error::ErrorKind will trigger an unreachable-pattern warning (or a compile error in -D warnings) on Rust call sites that exhaustively match without a catch-all.
#![allow(unused)] fn main() { // before — does not compile under -D warnings in v0.10.0 match err.kind() { ErrorKind::InvalidSignature => ..., ErrorKind::DocumentNotFound => ..., } // after — option 1: explicit arm match err.kind() { ErrorKind::InvalidSignature => ..., ErrorKind::DocumentNotFound => ..., ErrorKind::MissingSignature => ..., } // after — option 2: catch-all match err.kind() { ErrorKind::InvalidSignature => ..., ErrorKind::DocumentNotFound => ..., _ => ..., } }
Python / Node / Go bindings are unaffected — their error types are not exhaustive enums.
Verify semantics change is opt-in via strict
verify-text and verify-image are new commands in v0.10.0. There is no existing caller to break. The default mode is permissive (missing-signature is a typed status, not an error); --strict opts in to error-on-missing.
No existing command (jacs-compat verify, jacs-compat document verify, etc.) changes its exit codes or its return shape.
Migrating Node.js from 0.6.x to 0.7.0
Breaking Change: Async-First API
In v0.7.0, all NAPI operations return Promises by default. Sync variants are available with a Sync suffix, following the Node.js convention (like fs.readFile vs fs.readFileSync).
Before (v0.6.x):
const agent = new JacsAgent();
agent.load('./jacs.config.json');
const doc = agent.createDocument(JSON.stringify(content));
const isValid = agent.verifyDocument(doc);
After (v0.7.0, async -- recommended):
const agent = new JacsAgent();
await agent.load('./jacs.config.json');
const doc = await agent.createDocument(JSON.stringify(content));
const isValid = await agent.verifyDocument(doc);
After (v0.7.0, sync -- for scripts/CLI):
const agent = new JacsAgent();
agent.loadSync('./jacs.config.json');
const doc = agent.createDocumentSync(JSON.stringify(content));
const isValid = agent.verifyDocumentSync(doc);
Method Renaming Summary
| v0.6.x | v0.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)-- unchangedagent.verifyResponse(doc)-- unchangedagent.verifyResponseWithAgentId(doc)-- unchanged
Simplified API (Module-Level)
Historical module-level comparison; each block is a separate version-specific example.
Before (v0.6.x):
await jacs.quickstart({ name: 'my-agent', domain: 'agent.example.com' });
const signed = jacs.signMessage({ action: 'approve' });
After (v0.7.0 asynchronous API):
await jacs.quickstart({ name: 'my-agent', domain: 'agent.example.com' });
const signed = await jacs.signMessage({ action: 'approve' });
Alternative (v0.7.0 synchronous API):
jacs.quickstartSync({ name: 'my-agent', domain: 'agent.example.com' });
const signed = jacs.signMessageSync({ action: 'approve' });
Pure Sync Functions (No Change)
Historical synchronous API list (current methods may call native code; consult the current API reference):
hashString(),createConfig(),getPublicKey(),isLoaded(),exportAgent(),getAgentInfo()getDnsRecord(),getWellKnownJson(),verifyStandalone()- Trust store:
trustAgent(),listTrustedAgents(),untrustAgent(),isTrusted(),getTrustedAgent()
Migration Steps
- Update dependency:
npm install @hai.ai/jacs@0.7.0 - Add
awaitto all NAPI method calls, or appendSyncto method names - Update test assertions to handle Promises (use
async/awaitin test functions) - V8-thread-only methods (
signRequest,verifyResponse,verifyResponseWithAgentId) need no changes
Migrating from 0.5.1 to 0.5.2
Migration Notes
Historical encryption change: 0.5.2 increased PBKDF2 to 600,000 iterations. Current new writes use the V2 Argon2id/AES-GCM envelope and retain legacy reads. Use the current jacs-compat key reencrypt workflow to change at-rest encryption; generating a new signing key is a separate identity rotation.
Deprecated Environment Variables
JACS_USE_SECURITYis nowJACS_ENABLE_FILESYSTEM_QUARANTINE. The old name still works with a deprecation warning.
New Environment Variables
| Variable | Default | Description |
|---|---|---|
JACS_MAX_SIGNATURE_AGE_SECONDS | 0 (no expiration) | Maximum age of valid signatures. Set to a positive value to enable (e.g., 7776000 for 90 days). |
JACS_REQUIRE_EXPLICIT_ALGORITHM | false | When true, reject verification if signingAlgorithm is missing. |
JACS_ENABLE_FILESYSTEM_QUARANTINE | false | Enable 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
| SDK | Deprecated Method | Canonical Replacement | Since | Removal |
|---|---|---|---|---|
| Python (binding) | agent.wrap_a2a_artifact() | agent.sign_artifact() | 0.9.0 | 1.0.0 |
| Python (A2A) | a2a.wrap_artifact_with_provenance() | a2a.sign_artifact() | 0.9.0 | 1.0.0 |
| Node.js (binding) | agent.wrapA2aArtifact() | agent.signArtifact() | 0.9.0 | 1.0.0 |
| Node.js (binding) | agent.wrapA2aArtifactSync() | agent.signArtifactSync() | 0.9.0 | 1.0.0 |
| Node.js (A2A) | a2a.wrapArtifactWithProvenance() | a2a.signArtifact() | 0.9.0 | 1.0.0 |
| Rust (core) | agent.wrap_a2a_artifact() | agent.sign_artifact() | 0.9.0 | 1.0.0 |
| Go | SignA2AArtifact() (simple API) | Uses sign_artifact internally | -- | -- |
All aliases behave identically to their canonical replacements. No behavioral changes are needed when migrating -- only rename the method call.
Migration Examples
Python:
# Before (deprecated)
wrapped = agent.wrap_a2a_artifact(artifact_json, "artifact")
# After (canonical)
signed = agent.sign_artifact(artifact_json, "artifact")
Node.js:
// Before (deprecated)
const wrapped = await agent.wrapA2aArtifact(artifactJson, 'artifact');
// After (canonical)
const signed = await agent.signArtifact(artifactJson, 'artifact');
Python A2A integration:
# Before (deprecated)
wrapped = a2a.wrap_artifact_with_provenance(artifact, "artifact")
# After (canonical)
signed = a2a.sign_artifact(artifact, "artifact")
Node.js A2A integration:
// Before (deprecated)
const wrapped = await a2a.wrapArtifactWithProvenance(artifact, 'artifact');
// After (canonical)
const signed = await a2a.signArtifact(artifact, 'artifact');
Module-Level Function Deprecation (Reminder)
Module-level functions (e.g., jacs.load(), jacs.sign_request() in Python; load(), signRequest() in Node.js) were deprecated in earlier releases. Use JacsAgent instance methods instead. See the Python and Node.js API references for the full list.
Migrating from 0.2.x to 0.3.x
Configuration Changes
New Configuration Fields:
{
"observability": {
"logs": { "enabled": true, "level": "info" },
"metrics": { "enabled": false },
"tracing": { "enabled": false }
}
}
Deprecated Fields:
jacs_log_level→ Useobservability.logs.leveljacs_log_file→ Useobservability.logs.destination
Migration Steps
-
Update Configuration:
# Backup current config cp jacs.config.json jacs.config.json.backup # Update to new format # Add observability section if needed -
Update Dependencies:
# Node.js npm install @hai.ai/jacs@latest # Python pip install --upgrade jacs -
Verify Existing Documents:
jacs-compat document verify -d ./jacs_data/documents/
Migrating Storage Backends
Filesystem to AWS S3
-
Create S3 Bucket:
aws s3 mb s3://my-jacs-bucket -
Update Configuration:
{ "jacs_default_storage": "aws", "jacs_data_directory": "s3://my-jacs-bucket/data" } -
Set Environment Variables:
export AWS_ACCESS_KEY_ID="your-key" export AWS_SECRET_ACCESS_KEY="your-secret" export AWS_REGION="us-east-1" -
Migrate Documents:
# Upload existing documents aws s3 sync ./jacs_data/ s3://my-jacs-bucket/data/ -
Verify Migration:
jacs-compat document verify -d s3://my-jacs-bucket/data/documents/
AWS S3 to Filesystem
-
Download Documents:
aws s3 sync s3://my-jacs-bucket/data/ ./jacs_data/ -
Update Configuration:
{ "jacs_default_storage": "fs", "jacs_data_directory": "./jacs_data" } -
Verify Documents:
jacs-compat document verify -d ./jacs_data/documents/
Migrating Cryptographic Algorithms
Ed25519 to Post-Quantum
Native JACS signing defaults to pq2025 (ML-DSA-87 / FIPS-204). Explicit
ed25519 creation remains supported; agents using it continue to load and
sign with the canonical ring-Ed25519 label until they rotate.
Key rotation is the designated migration path. Rotation without an override preserves the current algorithm. Explicit
Ed25519-to-pq2025 upgrades are supported; PQ-to-Ed25519 downgrades are rejected.
-
Rotate Keys:
jacs-compat agent rotate-keys --algorithm pq2025 -
Re-issue the Compatibility Key Binding (if present):
The ES256 compatibility key binding is signed by the native root, so rotation supersedes it. Ecosystem exports fail with a "re-issue" error until a new binding is signed by the current native root:
jacs-compat agent issue-compat-binding -
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:
-
Export Agent Files:
jacs_keys/ ├── private.pem └── public.pem jacs.config.json -
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
-
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 -
Update Configuration:
{ "jacs_agent_private_key_filename": "private.pem" } -
Set Password:
export JACS_PRIVATE_KEY_PASSWORD="your-secure-password"
Database Migration
Adding Database Storage
If migrating from filesystem to include database storage:
-
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) ); -
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
-
Install JACS:
npm install @hai.ai/jacs -
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); -
Update Client:
// Client also needs JACS const baseTransport = new StdioClientTransport({ command: 'node', args: ['server.js'] }); const secureTransport = createJACSTransportProxy( baseTransport, './jacs.client.config.json', 'client' ); await client.connect(secureTransport);
HTTP API Migration
Migrating unbound request credentials to JACS v2
Legacy request credentials signed only signer ID, timestamp, and nonce. They did not authenticate the HTTP method, target, query, body, or receiving service, so a fresh credential could be transplanted to another request. Legacy builders remain available for compatibility and emit a WARN; migrate callers to v2.
- Serialize the final request body once.
- Build a v2 header from the actual method, absolute URL (including query), those exact body bytes, and a non-empty service-specific audience.
- Send the body without reformatting it.
- On the server, resolve the expected signer to a trusted public key and verify against the actual request context.
- Atomically consume the nonce. Use a shared store for multiple replicas and
retain it through
issued_at + max_age; Rust callers should userequest_auth_replay_ttl(&verified_claims, max_age). - Reject legacy headers after every client is migrated.
| API | Legacy | JACS v2 |
|---|---|---|
| Rust | protocol::build_auth_header(&mut agent) | SimpleAgent::build_request_auth_header(method, url, body, audience) |
| Python | SimpleAgent.build_auth_header() | SimpleAgent.build_request_auth_header(method, url, body, audience) |
| Node.js | JacsSimpleAgent.buildAuthHeader() | JacsSimpleAgent.buildRequestAuthHeader(method, url, body, audience) |
Go JacsAgent | BuildAuthHeader() | BuildRequestAuthHeader(method, url, body, audience) |
Go JacsSimpleAgent | BuildAuthHeader() | BuildRequestAuthHeader(method, url, body, audience) |
The APIs are additive: the legacy name takes no request context, while the
request-bound name requires the request values and emits
JACS v2.<claims>.<signature>. A header without that prefix is not the new
contract.
Legacy construction emits a structured WARN on every use. Strict deployments
can set JACS_REJECT_UNBOUND_AUTH_HEADER=true after migrating callers. Verification
policy should distinguish legacy and v2 explicitly, never silently fall back
after a v2 verification failure.
For signed responses, migrate payload-only or generic document verification to
sign_response / signResponse and strict event unwrapping. The v2 response
signature covers payload and all metadata. Unknown signers, plain events,
legacy envelopes, and mutations are errors; do not catch the error and parse
the untrusted payload anyway.
Verification proves possession of the public key selected under your trust policy. It does not establish a real-world identity merely because the signed envelope contains a name, agent ID, domain, or timestamp.
Adding JACS to Existing Express API
-
Install Middleware:
npm install @hai.ai/jacs -
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()); -
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:
-
Verify Configuration:
jacs-compat config read -
Verify Agent:
jacs-compat agent verify -
Verify Sample Document:
jacs-compat document verify -f ./sample-doc.json -
Test Document Creation:
echo '{"test": true}' > test.json jacs-compat document create -f test.json -
Verify Version:
jacs-compat --version
Rollback Procedures
If migration fails:
-
Restore Configuration:
cp jacs.config.json.backup jacs.config.json -
Restore Keys:
cp -r jacs_keys.backup/* jacs_keys/ -
Restore Dependencies:
# Node.js npm install @hai.ai/jacs@previous-version # Python pip install jacs==previous-version -
Verify Rollback:
jacs-compat agent verify jacs-compat document verify -d ./jacs_data/documents/
See Also
- Configuration Reference - Configuration options
- Cryptographic Algorithms - Algorithm details
- Storage Backends - Storage options