Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/crewai-tools/src/crewai_tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
from crewai_tools.tools.browserbase_load_tool.browserbase_load_tool import (
BrowserbaseLoadTool,
)
from crewai_tools.tools.chronoverify_image_verification_tool.chronoverify_image_verification_tool import (
ChronoVerifyImageVerificationTool,
)
from crewai_tools.tools.code_docs_search_tool.code_docs_search_tool import (
CodeDocsSearchTool,
)
Expand Down Expand Up @@ -237,6 +240,7 @@
"BrightDataWebUnlockerTool",
"BrowserbaseLoadTool",
"CSVSearchTool",
"ChronoVerifyImageVerificationTool",
"CodeDocsSearchTool",
"ComposioTool",
"ContextualAICreateAgentTool",
Expand Down
4 changes: 4 additions & 0 deletions lib/crewai-tools/src/crewai_tools/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
from crewai_tools.tools.browserbase_load_tool.browserbase_load_tool import (
BrowserbaseLoadTool,
)
from crewai_tools.tools.chronoverify_image_verification_tool.chronoverify_image_verification_tool import (
ChronoVerifyImageVerificationTool,
)
from crewai_tools.tools.code_docs_search_tool.code_docs_search_tool import (
CodeDocsSearchTool,
)
Expand Down Expand Up @@ -222,6 +225,7 @@
"BrightDataWebUnlockerTool",
"BrowserbaseLoadTool",
"CSVSearchTool",
"ChronoVerifyImageVerificationTool",
"CodeDocsSearchTool",
"ComposioTool",
"ContextualAICreateAgentTool",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# ChronoVerifyImageVerificationTool

## Description

This tool verifies when a photo was taken and its provenance using the [ChronoVerify](https://chronoverify.com) API. For each image it checks EXIF and XMP metadata, validates C2PA Content Credentials against the official trust lists, and runs classical pixel forensics, returning a single typed verdict with a 0 to 100 confidence score.

The verdict is one of:

| Verdict | Meaning |
|---|---|
| `provenance_confirmed` | Cryptographically valid C2PA Content Credentials from a signer on a recognised trust list |
| `consistent` | Metadata and pixel evidence agree with the claimed capture time and device |
| `inconclusive` | Not enough evidence to support or contradict the claim |
| `metadata_anomaly` | Metadata is missing, contradictory, or altered |
| `manipulation_indicated` | Pixel forensics or metadata indicate editing |

The response also includes the extracted capture time, capture device, capture location, a `c2pa` block, and file integrity hashes. The full response schema is published at `https://chronoverify.com/v1/verify.schema.json`.

**Scope**: ChronoVerify validates provenance. It is not a deepfake or AI-generation detector. Verdicts are investigative triage to support human review, not proof, and should not be the sole basis for any automated decision.

## Installation

Install the crewai_tools package:

```shell
pip install 'crewai[tools]'
```

No extra dependencies are required.

## Authentication (optional)

The tool works without any credentials on ChronoVerify's free keyless path, which is rate limited per IP.

For higher limits, set the `CHRONOVERIFY_API_KEY` environment variable. A free key (100 verifications per month, no card) can be requested with:

```shell
curl -X POST https://chronoverify.com/v1/keys/free -d "email=you@example.com"
```

```shell
export CHRONOVERIFY_API_KEY='cv_live_...'
```

## Example

```python
from crewai import Agent, Task
from crewai_tools import ChronoVerifyImageVerificationTool

tool = ChronoVerifyImageVerificationTool()

verifier = Agent(
role="Image Provenance Analyst",
goal="Verify when submitted photos were taken and where they came from",
backstory=(
"You examine image metadata, Content Credentials, and forensic "
"signals to assess whether a photo's claimed origin holds up."
),
tools=[tool],
verbose=True,
)

task = Task(
description=(
"Verify the capture time and provenance of the image at "
"https://example.com/photo.jpg and summarize the evidence."
),
agent=verifier,
expected_output="A short provenance assessment citing the verdict and confidence.",
)
```

## Arguments

- `image_path` (str, optional): Local path of the image file to verify.
- `image_url` (str, optional): Direct URL of the image to verify. Must be a direct link to the image bytes; redirects and hosts that block automated fetches fail, so prefer `image_path` when the file is available locally.

Provide exactly one of the two.

Constructor options:

- `api_base_url` (str, optional): Base URL of the ChronoVerify API. Defaults to `https://chronoverify.com`.
- `request_timeout` (int, optional): Request timeout in seconds. Defaults to 120.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""ChronoVerify image verification tool for CrewAI."""

from crewai_tools.tools.chronoverify_image_verification_tool.chronoverify_image_verification_tool import (
ChronoVerifyImageVerificationTool,
)


__all__ = ["ChronoVerifyImageVerificationTool"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""ChronoVerify image capture-time and provenance verification tool for CrewAI."""

import json
import os
from pathlib import Path

from crewai.tools import BaseTool, EnvVar
from pydantic import BaseModel, Field
import requests


class ChronoVerifyImageVerificationToolSchema(BaseModel):
"""Input for ChronoVerifyImageVerificationTool."""

image_path: str | None = Field(
default=None,
description=(
"Local filesystem path of the image file to verify. "
"Provide exactly one of image_path or image_url."
),
)
image_url: str | None = Field(
default=None,
description=(
"Direct URL of the image to verify. Must be a direct link to the "
"image bytes; redirects and hosts that block automated fetches fail, "
"so prefer image_path when the file is available locally."
),
)


class ChronoVerifyImageVerificationTool(BaseTool):
"""Verify a photo's capture time and provenance with the ChronoVerify API.

Sends the image to the ChronoVerify verification endpoint, which checks
EXIF and XMP metadata, validates C2PA Content Credentials against the
official trust lists, and runs classical pixel forensics. The response is
a typed verdict with a 0 to 100 confidence plus the extracted capture
time, capture device, capture location, C2PA details, and file hashes.

ChronoVerify is not a deepfake or AI-generation detector. Verdicts are
investigative triage to support human review, not proof.

An API key is optional. Without one, requests use the free keyless path,
which is rate limited per IP. Set ``CHRONOVERIFY_API_KEY`` to meter
requests against a key instead.
"""

name: str = "ChronoVerify Image Verification"
description: str = (
"Verify when a photo was taken and its provenance. Checks EXIF and XMP "
"metadata, validates C2PA Content Credentials against the official "
"trust lists, and runs pixel forensics, returning one verdict "
"(provenance_confirmed, consistent, inconclusive, metadata_anomaly, or "
"manipulation_indicated) with a 0 to 100 confidence, plus the extracted "
"capture time, capture device, and capture location. It does not detect "
"deepfakes or AI generation; verdicts are investigative triage to "
"support human review, not proof. Accepts a local image file path or a "
"direct image URL."
)
args_schema: type[BaseModel] = ChronoVerifyImageVerificationToolSchema
api_base_url: str = Field(
default="https://chronoverify.com",
description="Base URL of the ChronoVerify API.",
)
request_timeout: int = Field(
default=120,
description="Timeout in seconds for the verification request.",
)
env_vars: list[EnvVar] = Field(
default_factory=lambda: [
EnvVar(
name="CHRONOVERIFY_API_KEY",
description=(
"Optional ChronoVerify API key (cv_live_...). Without it, "
"requests use the free keyless path, which is rate limited "
"per IP. A free key is available at "
"https://chronoverify.com/v1/keys/free."
),
required=False,
),
]
)

def _request_headers(self) -> dict[str, str]:
"""Build request headers, adding auth only when an API key is set."""
api_key = os.environ.get("CHRONOVERIFY_API_KEY")
if api_key:
return {"Authorization": f"Bearer {api_key}"}
return {}

def _run(
self,
image_path: str | None = None,
image_url: str | None = None,
) -> str:
"""Verify an image and return the verification result as JSON text.

Args:
image_path: Local path of the image file to verify.
image_url: Direct URL of the image to verify.

Returns:
The ChronoVerify verification response serialized as JSON, or an
error message describing what went wrong.
"""
if not image_path and not image_url:
return "Error: provide either image_path or image_url."
if image_path and image_url:
return "Error: provide only one of image_path or image_url, not both."

endpoint = f"{self.api_base_url.rstrip('/')}/v1/verify"

try:
if image_path:
path = Path(image_path)
if not path.is_file():
return f"Error: image file not found: {image_path}"
with path.open("rb") as image_file:
response = requests.post(
endpoint,
headers=self._request_headers(),
files={"file": (path.name, image_file)},
timeout=self.request_timeout,
)
else:
response = requests.post(
endpoint,
headers=self._request_headers(),
data={"url": image_url},
timeout=self.request_timeout,
)
response.raise_for_status()
return json.dumps(response.json(), indent=2)
except requests.exceptions.HTTPError as e:
status_code = e.response.status_code if e.response is not None else "?"
detail = e.response.text[:500] if e.response is not None else str(e)
return f"Error verifying image (HTTP {status_code}): {detail}"
except requests.exceptions.RequestException as e:
return f"Error communicating with the ChronoVerify API: {e!s}"
except ValueError as e:
return f"Error parsing the ChronoVerify API response: {e!s}"
Loading