Metadata-Version: 2.5
Name: artemis-provenance-sdk
Version: 0.2.0
Summary: Thin client for the Artemis Provenance data plane — mark and verify AI-generated media (EU AI Act Article 50(2)).
Project-URL: Homepage, https://github.com/Star-48/artemis-provenance-sdk-python
Project-URL: Repository, https://github.com/Star-48/artemis-provenance-sdk-python.git
Project-URL: Issues, https://github.com/Star-48/artemis-provenance-sdk-python/issues
Author: Star48
License-Expression: MIT
License-File: LICENSE
Keywords: ai-act,c2pa,content-credentials,provenance,watermark
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# artemis-provenance-sdk

Thin Python client for the **Artemis Provenance data plane** — mark and verify
AI-generated media (image · video · audio) for EU AI Act Article 50(2)
compliance. The SDK contains **no marking logic**; it serializes calls to the
data-plane container the customer runs in their own network, so content never
leaves the VPC.

```bash
pip install artemis-provenance-sdk
```

## Usage

```python
from artemis_provenance_sdk import Client, MarkingFailedError, MarkingUnavailableError

pv = Client(endpoint="http://provenance-dp.internal:8080", api_key="...")

# Mark an asset at the end of your generation pipeline.
try:
    marked = pv.mark_image(image_bytes, app_id="avatar-studio",
                           context={"title": "Generated avatar"})
    # marked.bytes  -> the marked output to ship
    # marked.event_id, marked.payload_id, marked.sha256, marked.marks
except MarkingFailedError as err:
    # The data plane ran and could NOT produce the required mark.
    # err.required_mark, err.event_id, err.reason
    # err.unmarked is the unmarked output — not None only if your tenant
    # policy is `return_unmarked`. Shipping it is a compliance gap.
    ...
except MarkingUnavailableError:
    # Decide fail-open (ship unmarked — a compliance gap) vs fail-closed.
    ...

# Verify locally — content never leaves your network; only the id is resolved.
result = pv.verify(image_bytes, content_type="image")
# result["result"] == "matched" | "no-match", result["event"], result["checks"], result["local"]
```

There is also a file convenience wrapper:

```python
marked = pv.mark_image_file("out/hero.png", app_id="avatar-studio")
```

## Text — attested, not watermarked

There is no robust post-hoc text watermark: statistical watermarks exist only
at generation time, and invisible-Unicode marks are strippable by design.
`mark_text` therefore **attests** text — canonical hashes plus a detached,
KMS-signed manifest recorded on your provenance chain — with an *optional*,
explicitly strippable zero-width soft binding.

```python
attested = pv.mark_text(article_text, app_id="newsroom",
                        context={"title": "Q3 results", "model": "my-llm"})
# attested.text          -> the text to publish (identical unless soft_binding=True)
# attested.manifest_jws  -> detached signed manifest (portable proof)
# attested.text_canonical_hash, attested.event_id, attested.payload_id

result = pv.verify_text(some_text)
# result["verdict"] -> "exact-match" | "canonical-match" | "softbinding-recovered" | "no-match"
```

The optional soft binding (`soft_binding=True`) only makes the record
auto-discoverable in byte-preserving copy flows — it is best-effort and removed
by normalization, sanitizers, retyping, or one free paste-through tool. A
`no-match` verdict proves **nothing** about origin: unmarked, edited,
paraphrased, translated, or third-party text all produce it, and no
AI-vs-human inference is ever made.

### Declaring AI generation

The signed manifest declares the content **AI-generated by default**: this SDK
sits in your generation pipeline, and the AI disclosure is the obligation it
exists to discharge. `ai_generated` (on `mark_text` and `mark_image`) sets the
C2PA/IPTC `digital_source_type`:

```python
pv.mark_text(draft, app_id="newsroom")                      # trainedAlgorithmicMedia (default)
pv.mark_text(draft, app_id="newsroom", ai_generated=True)   # trainedAlgorithmicMedia
pv.mark_text(draft, app_id="newsroom", ai_generated=False)  # digitalCreation
```

Pass `ai_generated=False` to mark human-made, non-generative content — that is
the only way to get `digitalCreation`. The declaration is yours to make:
nothing detects AI generation, and the manifest is signed with your key. A
malformed value is rejected (HTTP 400); it never falls back to the default.

There is also a file convenience wrapper:
`pv.mark_text_file("post.md", app_id="newsroom")`.

## Fail modes

Marking has exactly two failure modes, and they are the **same compliance
outcome**: nothing was marked. So they reach you the same way — raised, never
returned. Both are catchable so your pipeline chooses fail-open vs fail-closed
**consciously**; document the compliance implications of each.

| Exception | Means | HTTP |
| --- | --- | --- |
| `MarkingUnavailableError` | The data plane is unreachable, or could not accept the request (e.g. payload ids exhausted). | connect error / `503` |
| `MarkingFailedError` (a **subclass** of the above) | The data plane ran and could not produce the **required mark**. | `422` with `{"code": "marking_failed", ...}` |

`MarkingFailedError` subclasses `MarkingUnavailableError` on purpose: if your
pipeline already handles an unreachable data plane, it handles a failed mark
identically with no code change. Catch `MarkingFailedError` first when you want
the detail.

### The required mark is modality-aware

* **image · video · audio** — the invisible **watermark**. A C2PA manifest is
  metadata (strippable) and is never the required mark, so a manifest that
  fails on a correctly watermarked asset does **not** raise.
* **text** — the detached **signed manifest**. Text is attested, not
  watermarked; the optional zero-width soft binding is best-effort and its
  failure never raises.

```python
try:
    marked = pv.mark_image(image_bytes, app_id="avatar-studio")
    publish(marked.bytes)
except MarkingFailedError as err:
    err.required_mark        # "watermark" | "signed-manifest"
    err.event_id             # the evidence event recording the failure
    err.reason               # e.g. "ValueError: no video frames decoded"
    err.on_marking_failure   # "reject" | "return_unmarked"
    err.unmarked             # see below — None under `reject`
    raise                    # fail closed: publish nothing
```

### Tenant policy: `on_marking_failure`

Set on your provenance policy (`rules.onMarkingFailure`), it decides what the
data plane does when the required mark fails.

* **`reject` (default)** — fail closed. The data plane records the failure on
  your evidence chain, returns HTTP 422 `marking_failed`, and returns **no
  asset**. `err.unmarked` is `None`, because nothing exists to ship.
* **`return_unmarked`** — availability over compliance. The data plane returns
  the **unmarked** output, flagged (`markingFailed: true`, a `markingFailure`
  block, and the `x-provenance-marking-failed` header). The SDK still
  **raises**: the unmarked output is reachable only as `err.unmarked`, so
  publishing it is always a deliberate act. Your regulator report names this
  choice, so an auditor sees it was yours and deliberate.

There is no configuration in which unmarked output is returned to your code as
a successful result.

## Typing

The package ships a `py.typed` marker; `Client`, `MarkedAsset`, `MarkedText`,
`MarkingFailedError`, `VerifyResult` and `VerifyTextResult` are fully typed for
editor/mypy support.

## Releasing (maintainers)

See [SETUP.md](./SETUP.md) for the one-time PyPI Trusted Publishing setup and
the per-release flow.

## License

MIT
