merkl-sdk 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. merkl_sdk-0.1.0/.gitignore +1 -0
  2. merkl_sdk-0.1.0/CLAUDE.md +64 -0
  3. merkl_sdk-0.1.0/INSTALL.md +93 -0
  4. merkl_sdk-0.1.0/LICENSE +21 -0
  5. merkl_sdk-0.1.0/PKG-INFO +94 -0
  6. merkl_sdk-0.1.0/README.md +63 -0
  7. merkl_sdk-0.1.0/docs/recording.png +0 -0
  8. merkl_sdk-0.1.0/docs/verification.png +0 -0
  9. merkl_sdk-0.1.0/merkl/__init__.py +1 -0
  10. merkl_sdk-0.1.0/merkl/cli/__init__.py +1 -0
  11. merkl_sdk-0.1.0/merkl/cli/disclose.py +104 -0
  12. merkl_sdk-0.1.0/merkl/cli/main.py +239 -0
  13. merkl_sdk-0.1.0/merkl/hooks/__init__.py +1 -0
  14. merkl_sdk-0.1.0/merkl/hooks/claude_code.py +897 -0
  15. merkl_sdk-0.1.0/merkl/integrations/__init__.py +0 -0
  16. merkl_sdk-0.1.0/merkl/integrations/_common.py +52 -0
  17. merkl_sdk-0.1.0/merkl/integrations/crewai.py +73 -0
  18. merkl_sdk-0.1.0/merkl/integrations/google_adk.py +327 -0
  19. merkl_sdk-0.1.0/merkl/integrations/langchain.py +118 -0
  20. merkl_sdk-0.1.0/merkl/integrations/openai.py +93 -0
  21. merkl_sdk-0.1.0/merkl/sdk/__init__.py +6 -0
  22. merkl_sdk-0.1.0/merkl/sdk/client.py +86 -0
  23. merkl_sdk-0.1.0/merkl/sdk/decorators.py +167 -0
  24. merkl_sdk-0.1.0/merkl/sdk/session_context.py +153 -0
  25. merkl_sdk-0.1.0/merkl/sdk/transport.py +141 -0
  26. merkl_sdk-0.1.0/merkl/shared/__init__.py +38 -0
  27. merkl_sdk-0.1.0/merkl/shared/enums.py +57 -0
  28. merkl_sdk-0.1.0/merkl/shared/errors.py +46 -0
  29. merkl_sdk-0.1.0/merkl/shared/events.py +18 -0
  30. merkl_sdk-0.1.0/merkl/shared/hashing.py +49 -0
  31. merkl_sdk-0.1.0/merkl/shared/ids.py +64 -0
  32. merkl_sdk-0.1.0/merkl/shared/timestamps.py +30 -0
  33. merkl_sdk-0.1.0/pyproject.toml +73 -0
  34. merkl_sdk-0.1.0/tests/__init__.py +0 -0
  35. merkl_sdk-0.1.0/tests/cli/__init__.py +0 -0
  36. merkl_sdk-0.1.0/tests/cli/test_disclose.py +99 -0
  37. merkl_sdk-0.1.0/tests/cli/test_install.py +59 -0
  38. merkl_sdk-0.1.0/tests/conftest.py +32 -0
  39. merkl_sdk-0.1.0/tests/hooks/__init__.py +0 -0
  40. merkl_sdk-0.1.0/tests/hooks/test_claude_code.py +453 -0
  41. merkl_sdk-0.1.0/tests/integrations/__init__.py +0 -0
  42. merkl_sdk-0.1.0/tests/integrations/test_google_adk.py +281 -0
  43. merkl_sdk-0.1.0/tests/sdk/__init__.py +0 -0
  44. merkl_sdk-0.1.0/tests/sdk/test_client.py +52 -0
  45. merkl_sdk-0.1.0/tests/sdk/test_decorators.py +251 -0
  46. merkl_sdk-0.1.0/tests/sdk/test_session_context.py +298 -0
  47. merkl_sdk-0.1.0/tests/sdk/test_transport.py +61 -0
  48. merkl_sdk-0.1.0/tests/shared/__init__.py +0 -0
  49. merkl_sdk-0.1.0/tests/shared/test_enums.py +83 -0
  50. merkl_sdk-0.1.0/tests/shared/test_errors.py +61 -0
  51. merkl_sdk-0.1.0/tests/shared/test_events.py +34 -0
  52. merkl_sdk-0.1.0/tests/shared/test_hashing.py +103 -0
  53. merkl_sdk-0.1.0/tests/shared/test_ids.py +94 -0
  54. merkl_sdk-0.1.0/tests/shared/test_timestamps.py +37 -0
  55. merkl_sdk-0.1.0/uv.lock +682 -0
@@ -0,0 +1 @@
1
+ dist/
@@ -0,0 +1,64 @@
1
+ # merkl-sdk
2
+
3
+ Thin Python HTTP client for Merkl. This is what agent developers `pip install` to instrument their agents.
4
+
5
+ Developed in the merkl monorepo (`packages/merkl-sdk`); the standalone `merkl-sdk` repo is a read-only export — change things there via the monorepo, then re-run the subtree split.
6
+
7
+ ## What This Package Does
8
+
9
+ - `MerklClient` — main entry point (endpoint URL, agent_id, API key)
10
+ - `SessionContext` — async context manager for session lifecycle
11
+ - `@trace` and `@guardrail` decorators for auto-recording actions (concurrency-safe via `contextvars`)
12
+ - `merkl` CLI — `merkl install --claude-code [--global]` writes hooks into `settings.json`
13
+ - `HookState` (`merkl/hooks/claude_code.py`) — one tempfile-backed object per Claude Code session, owns session_id, turn rotation, dataflow snippets, sub-agent parent linkage
14
+ - Framework integrations: LangChain, OpenAI, Google ADK, CrewAI — all route through `merkl.integrations._common.record_tool_call` so new action fields plumb through one call site
15
+ - Shared value objects (`SHA256Hash`, `canonical_hash`, `SessionId`, `ActionId`, `Timestamp`, enums, errors) imported by both SDK and merkl-api
16
+
17
+ ## What This Package Does NOT Do
18
+
19
+ No domain logic, no Merkle trees, no batching, no Solana, no persistence. Pure HTTP client.
20
+
21
+ ## Key Files
22
+
23
+ - `merkl/sdk/client.py` — `MerklClient`: creates sessions, holds transport
24
+ - `merkl/sdk/session_context.py` — `SessionContext`: async with, `record_action()`, auto-close; binds itself to the `_current_session` contextvar on enter
25
+ - `merkl/sdk/transport.py` — `AsyncTransport`: httpx with retry + buffering
26
+ - `merkl/sdk/decorators.py` — `@trace`, `@guardrail` + `set_current_session` / `reset_current_session` backed by `contextvars`
27
+ - `merkl/integrations/_common.py` — `record_tool_call()` shared by every framework adapter
28
+ - `merkl/integrations/` — langchain.py, openai.py, google_adk.py, crewai.py
29
+ - `merkl/hooks/claude_code.py` — Claude Code PostToolUse + SessionEnd hook; `HookState` class owns all per-session scratch state
30
+ - `merkl/cli/main.py` — `merkl install --claude-code` CLI
31
+ - `merkl/shared/hashing.py` — `SHA256Hash`, `canonical_hash()`, `canonical_bytes()` (deterministic JSON-sorted-keys hashing shared by SDK, hook, and server-side leaf verification)
32
+ - `merkl/shared/` — ids.py, timestamps.py, enums.py, errors.py, events.py
33
+
34
+ ## Usage
35
+
36
+ ```python
37
+ from merkl.sdk import MerklClient
38
+
39
+ client = MerklClient(endpoint="http://localhost:8000", agent_id="my-agent", api_key="mk_...")
40
+ async with client.session(goal="Process refunds", allowed_tools=["query_db"]) as session:
41
+ result = await session.record_action(tool_name="query_db", input_data="SELECT ...", output_data={...})
42
+ # result includes leaf_index
43
+ ```
44
+
45
+ ## Testing
46
+
47
+ ```bash
48
+ pip install -e ".[dev]"
49
+ pytest # 137 tests
50
+ ```
51
+
52
+ ## Guidelines
53
+
54
+ - Keep it thin. Server logic belongs in the merkl-api package.
55
+ - `merkl/shared/` is imported by both the SDK and the merkl-api server (in the monorepo) — changes affect both, and `canonical_hash` / leaf encodings are proof-format-critical.
56
+ - Framework integrations should end at `record_tool_call()` in `_common.py`, not call `session.record_action()` directly. New action fields flow through one site.
57
+ - Input/output hashing must go through `canonical_hash()`. Raw `str()` is non-deterministic for dicts; the SDK and the Claude Code hook must produce identical leaf hashes for the same logical payload.
58
+ - The SDK must never import from `merkl_api.*`.
59
+ - Dependencies must stay minimal (uuid6, httpx, cryptography). This ships to customers.
60
+
61
+ ## Known Issues
62
+
63
+ - `@trace` and `@guardrail` decorators only wrap async functions; sync is passthrough
64
+ - `@guardrail` runs client-side policy (allowlist or callable); no server-side evaluation yet
@@ -0,0 +1,93 @@
1
+ ---
2
+ name: merkl-install
3
+ description: Install Merkl into the user's Claude Code and verify it records a real session, with minimal prompting. Use for first-time setup, reconnect, or "why isn't Merkl recording?".
4
+ ---
5
+
6
+ # merkl install
7
+
8
+ You are driving this install. Do as much as you can yourself; ask the user only for the one thing you cannot get — their API key — and only when you actually need it. After every step, verify before moving on.
9
+
10
+ Merkl records what an AI agent does, hashes each action into a Merkle tree, and lets anyone verify what happened later. Installing it into Claude Code means registering a hook that fires on every tool call. Full reference: `README.md`.
11
+
12
+ ## Install prompt contract
13
+
14
+ - Prefer running commands over telling the user to run them.
15
+ - The user must do exactly two things you cannot: (1) get an API key from their dashboard, (2) reload hooks (`/hooks` or restart) — Claude Code loads hooks at session start, so a freshly written hook is not live in the current session. Everything else is yours.
16
+ - Do not print the API key back to the user or paste it into any file they can commit. It goes into the hook command and shell profile only.
17
+ - Verify with a real recorded action, not by assuming the config is correct.
18
+
19
+ ## 1. Install the package
20
+
21
+ ```bash
22
+ pip install merkl-sdk
23
+ merkl --help
24
+ ```
25
+
26
+ If `merkl: command not found` after a successful `pip install`, the package installed into an environment whose scripts are not on `PATH`. Escalate from the actual cause:
27
+
28
+ - **A pyenv/conda/system Python mismatch** → install with the interpreter Claude Code's hooks will use. `pipx install merkl-sdk` (or `uvx merkl`) puts a stable global `merkl` on `PATH` and is the most robust choice. Prefer it.
29
+ - **A project venv** → the install is fine, just not global; either activate that venv or use `pipx`.
30
+
31
+ Confirm `command -v merkl` prints a path before continuing.
32
+
33
+ ## 2. Get the API key (the one thing you must ask for)
34
+
35
+ The key authenticates the hook. Only the user can mint it.
36
+
37
+ - If `MERKL_API_KEY` is already set in the environment, use it — do not ask.
38
+ - Otherwise ask the user to: open their Merkl dashboard → create an org if they have not → API Keys → create one → copy the `mk_...` value (shown once). While they do this, wait and re-check; do not move on without it.
39
+ - Self-hosted only: also note their API URL for `--endpoint`. The default is `api.merkl.ai`, hardcoded — do not pass `--endpoint` for the hosted service.
40
+
41
+ ## 3. Install the hook
42
+
43
+ ```bash
44
+ merkl install --claude-code --global
45
+ ```
46
+
47
+ The installer prompts for the API key if `MERKL_API_KEY` is unset and bakes it into the hook command, so no shell-profile editing is needed. If you already have the key, pass it non-interactively:
48
+
49
+ ```bash
50
+ merkl install --claude-code --global --api-key mk_...
51
+ ```
52
+
53
+ This registers five events (PostToolUse, SessionEnd, UserPromptSubmit, PermissionRequest, PermissionDenied) in the global settings and uses the current interpreter, not a bare `python`. Read back the written settings file and confirm the hook command contains `merkl.hooks.claude_code` and the key.
54
+
55
+ ## 4. Load the hook
56
+
57
+ Hooks load at session start, so the hook you just wrote is not active in this session. Tell the user to either run `/hooks` once (reloads config) or start a new Claude Code session. You cannot do this for them — `/hooks` is a user UI action.
58
+
59
+ ## 5. Verify it records — do this, do not assume
60
+
61
+ Fire a synthetic PostToolUse payload through the hook exactly as Claude Code would, then check the session landed. Use the same interpreter the hook uses.
62
+
63
+ ```bash
64
+ echo '{"session_id":"merkl-install-check","hook_event_name":"PostToolUse","tool_name":"Bash","tool_input":{"command":"echo hi"},"tool_response":"hi","transcript_path":"/nonexistent"}' \
65
+ | MERKL_API_KEY=<key> python -m merkl.hooks.claude_code
66
+ echo "hook exit: $?"
67
+ ```
68
+
69
+ Then confirm the server received it:
70
+
71
+ ```bash
72
+ curl -s -H "Authorization: Bearer <key>" https://api.merkl.ai/v1/sessions | head -c 400
73
+ ```
74
+
75
+ A session with `agent_id` `claude-code` (or the `MERKL_AGENT_ID` you set) and `action_count >= 1` means it works. If you see it, you are done — tell the user their next real Claude Code session records automatically, and point them at their dashboard.
76
+
77
+ ## Troubleshooting — read the error, escalate from there
78
+
79
+ Do not restart from step 1. Match the symptom:
80
+
81
+ - **Hook exits 0 but no session appears** → the hook swallows errors (`2>/dev/null || true`), so a silent failure looks like success. Re-run the hook payload *without* the `2>/dev/null || true` suppression to see the real error, then match below.
82
+ - **HTTP 401 / "Invalid API key"** → the key is wrong, revoked, or from a different Merkl instance than the endpoint. Re-copy it from the dashboard; confirm the endpoint matches where the key was minted.
83
+ - **Connection refused / DNS failure on the endpoint** → for self-hosted, the API URL is wrong or the server is down. For the hosted service, confirm `api.merkl.ai` resolves. Never silently fall back to a different endpoint.
84
+ - **`/hooks` shows "0 hooks configured"** → the settings file you wrote is not the one this session reads. Claude Code loads hooks from the config dir for the session's project root; confirm you wrote to the right scope (`--global` writes `~/.claude/settings.json`; a project install writes `<project>/.claude/settings.json`). If the user runs a non-default `CLAUDE_CONFIG_DIR`, write there.
85
+ - **Session is created but `action_count` stays 0** → the session opened (create succeeded) but PostToolUse never fired for a real tool. Have the user run any tool (read a file, run a command) in a hook-loaded session, then re-check.
86
+ - **Hook command runs `python` and that Python lacks merkl** → an older install wrote a bare `python`. Re-run `merkl install` (it now uses the full interpreter path), or edit the hook command to the absolute path from `command -v python` in the install env.
87
+ - **Dev/self-hosted: sessions vanished after a server restart** → the server is running in-memory (no `DATABASE_URL`). That is expected for dev; use Postgres for anything you want to keep.
88
+ - **Everything looks right but still nothing** → clear stale hook state and retry: `rm -f "$TMPDIR"/merkl_hookstate_*.json`. Stale state points at session ids the server may not have.
89
+
90
+ ## What "installed" means
91
+
92
+ - `merkl` on `PATH`, hook registered in the correct settings file with the key baked in, hooks reloaded, and a verification action visible in the dashboard or via the sessions API.
93
+ - Nothing about the user's real payloads left their machine: only hashes and tool names go to Merkl; raw data stays in `~/.merkl/evidence/`. If the user wants zero plaintext at all, that is already the default; previews are opt-in via `MERKL_INCLUDE_PREVIEWS=1`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ricardo Mendez Cavalieri
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.5
2
+ Name: merkl-sdk
3
+ Version: 0.1.0
4
+ Summary: Record AI agent actions as independently verifiable cryptographic proofs
5
+ Project-URL: Homepage, https://merkl.ai
6
+ Project-URL: Documentation, https://merkl.ai
7
+ Project-URL: Repository, https://github.com/ramcav/merkl-sdk
8
+ Project-URL: Bug Tracker, https://github.com/ramcav/merkl-sdk/issues
9
+ Author-email: Merkl <hello@merkl.ai>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: agents,ai,audit,compliance,merkle,verification
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Security
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: cryptography>=42.0
23
+ Requires-Dist: httpx>=0.27
24
+ Requires-Dist: uuid6>=2025.0.1
25
+ Provides-Extra: dev
26
+ Requires-Dist: mypy>=1.8; extra == 'dev'
27
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
28
+ Requires-Dist: pytest>=8.0; extra == 'dev'
29
+ Requires-Dist: ruff>=0.3; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # merkl-sdk
33
+
34
+ The accountability layer for AI agents. Every action is hashed on your machine and committed to a signed, append-only log — independently verifiable proof of what happened. Raw payloads stay on disk; the notary only ever sees hashes, tool names, and metadata.
35
+
36
+ Homepage: [merkl.ai](https://merkl.ai) · Dashboard: [app.merkl.ai](https://app.merkl.ai)
37
+
38
+ ## Install (Claude Code)
39
+
40
+ ```bash
41
+ pip install merkl-sdk
42
+ merkl install --claude-code --global
43
+ ```
44
+
45
+ The installer asks for an API key (`mk_...` from [app.merkl.ai](https://app.merkl.ai)) and wires Claude Code. Restart Claude Code or run `/hooks`. After that, every session records automatically.
46
+
47
+ To have the agent do the install for you, point it at `INSTALL.md`.
48
+
49
+ ## Python
50
+
51
+ ```python
52
+ from merkl.sdk import MerklClient
53
+
54
+ client = MerklClient(
55
+ endpoint="https://api.merkl.ai",
56
+ agent_id="my-agent",
57
+ api_key="mk_...",
58
+ )
59
+ async with client.session(goal="Process refunds") as session:
60
+ await session.record_action(
61
+ tool_name="query_db",
62
+ input_data="SELECT ...",
63
+ output_data={"rows": 42},
64
+ )
65
+ ```
66
+
67
+ The session seals on exit. Plaintext previews are off by default (`include_previews=True` to send them). The Claude Code hook is the same: opt in with `MERKL_INCLUDE_PREVIEWS=1`.
68
+
69
+ LangChain, OpenAI, CrewAI, and Google ADK adapters live in `merkl.integrations.*`. They work; Claude Code is the supported launch path.
70
+
71
+ ## Disclose
72
+
73
+ ```bash
74
+ merkl disclose <action_id>
75
+ ```
76
+
77
+ Writes a folder with `verify.html` and one-line `evidence.jsonl`. An auditor opens the page offline — no account, nothing to install.
78
+
79
+ ## Environment
80
+
81
+ | Variable | What |
82
+ |---|---|
83
+ | `MERKL_API_KEY` | API key (`mk_...`) |
84
+ | `MERKL_ENDPOINT` | API URL (default `https://api.merkl.ai`) |
85
+ | `MERKL_AGENT_ID` | Label in the dashboard (default `claude-code` for the hook) |
86
+ | `MERKL_INCLUDE_PREVIEWS` | `1` to send short plaintext previews |
87
+ | `MERKL_EVIDENCE_DIR` | Local evidence log (default `~/.merkl/evidence/`; `off` to disable) |
88
+
89
+ ## Development
90
+
91
+ ```bash
92
+ pip install -e ".[dev]"
93
+ pytest
94
+ ```
@@ -0,0 +1,63 @@
1
+ # merkl-sdk
2
+
3
+ The accountability layer for AI agents. Every action is hashed on your machine and committed to a signed, append-only log — independently verifiable proof of what happened. Raw payloads stay on disk; the notary only ever sees hashes, tool names, and metadata.
4
+
5
+ Homepage: [merkl.ai](https://merkl.ai) · Dashboard: [app.merkl.ai](https://app.merkl.ai)
6
+
7
+ ## Install (Claude Code)
8
+
9
+ ```bash
10
+ pip install merkl-sdk
11
+ merkl install --claude-code --global
12
+ ```
13
+
14
+ The installer asks for an API key (`mk_...` from [app.merkl.ai](https://app.merkl.ai)) and wires Claude Code. Restart Claude Code or run `/hooks`. After that, every session records automatically.
15
+
16
+ To have the agent do the install for you, point it at `INSTALL.md`.
17
+
18
+ ## Python
19
+
20
+ ```python
21
+ from merkl.sdk import MerklClient
22
+
23
+ client = MerklClient(
24
+ endpoint="https://api.merkl.ai",
25
+ agent_id="my-agent",
26
+ api_key="mk_...",
27
+ )
28
+ async with client.session(goal="Process refunds") as session:
29
+ await session.record_action(
30
+ tool_name="query_db",
31
+ input_data="SELECT ...",
32
+ output_data={"rows": 42},
33
+ )
34
+ ```
35
+
36
+ The session seals on exit. Plaintext previews are off by default (`include_previews=True` to send them). The Claude Code hook is the same: opt in with `MERKL_INCLUDE_PREVIEWS=1`.
37
+
38
+ LangChain, OpenAI, CrewAI, and Google ADK adapters live in `merkl.integrations.*`. They work; Claude Code is the supported launch path.
39
+
40
+ ## Disclose
41
+
42
+ ```bash
43
+ merkl disclose <action_id>
44
+ ```
45
+
46
+ Writes a folder with `verify.html` and one-line `evidence.jsonl`. An auditor opens the page offline — no account, nothing to install.
47
+
48
+ ## Environment
49
+
50
+ | Variable | What |
51
+ |---|---|
52
+ | `MERKL_API_KEY` | API key (`mk_...`) |
53
+ | `MERKL_ENDPOINT` | API URL (default `https://api.merkl.ai`) |
54
+ | `MERKL_AGENT_ID` | Label in the dashboard (default `claude-code` for the hook) |
55
+ | `MERKL_INCLUDE_PREVIEWS` | `1` to send short plaintext previews |
56
+ | `MERKL_EVIDENCE_DIR` | Local evidence log (default `~/.merkl/evidence/`; `off` to disable) |
57
+
58
+ ## Development
59
+
60
+ ```bash
61
+ pip install -e ".[dev]"
62
+ pytest
63
+ ```
Binary file
Binary file
@@ -0,0 +1 @@
1
+ """Merkl SDK — accountability infrastructure for autonomous AI agents."""
@@ -0,0 +1 @@
1
+ """Merkl CLI — install and manage integrations."""
@@ -0,0 +1,104 @@
1
+ """``merkl disclose`` — package one action's evidence for an auditor.
2
+
3
+ The operator side of the disclosure flow. Produces a folder the operator
4
+ can zip and email; the auditor needs no tooling — they open verify.html
5
+ in any browser and drop evidence.jsonl on it.
6
+
7
+ merkl disclose <action_id>
8
+ → disclosure-<action_id[:8]>/
9
+ verify.html standalone verifier with the session's proof bundle
10
+ evidence.jsonl ONLY the disclosed action's raw record
11
+
12
+ Selective by construction: undisclosed actions appear in the bundle as
13
+ hashes and typed metadata only; their raw payloads never leave the
14
+ evidence dir.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import os
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ import httpx
25
+
26
+
27
+ def find_evidence_entry(evidence_dir: Path, action_id: str) -> tuple[dict, str] | None:
28
+ """Scan the evidence dir for an entry with this action_id.
29
+
30
+ Returns (entry, raw_line) so the disclosed line is byte-identical to
31
+ what the hook wrote — re-serializing could change key order and
32
+ confuse a diff, even though hashing is order-independent.
33
+ """
34
+ for path in sorted(evidence_dir.glob("*.jsonl")):
35
+ with open(path, encoding="utf-8") as f:
36
+ for line in f:
37
+ if action_id not in line:
38
+ continue
39
+ try:
40
+ entry = json.loads(line)
41
+ except json.JSONDecodeError:
42
+ continue
43
+ if entry.get("action_id") == action_id:
44
+ return entry, line.rstrip("\n")
45
+ return None
46
+
47
+
48
+ def disclose(
49
+ action_id: str,
50
+ *,
51
+ evidence_dir: Path | None = None,
52
+ endpoint: str | None = None,
53
+ api_key: str | None = None,
54
+ out_dir: Path | None = None,
55
+ ) -> Path:
56
+ """Build a disclosure folder for one action. Returns the folder path.
57
+
58
+ Raises SystemExit with a readable message on any failure — this is a
59
+ CLI entry point, not a library API.
60
+ """
61
+ evidence_dir = evidence_dir or Path(
62
+ os.environ.get("MERKL_EVIDENCE_DIR") or Path.home() / ".merkl" / "evidence"
63
+ )
64
+ endpoint = (endpoint or os.environ.get("MERKL_ENDPOINT", "https://api.merkl.ai")).rstrip("/")
65
+ api_key = api_key or os.environ.get("MERKL_API_KEY", "")
66
+
67
+ if not evidence_dir.is_dir():
68
+ sys.exit(f"Evidence dir not found: {evidence_dir} (set MERKL_EVIDENCE_DIR)")
69
+
70
+ found = find_evidence_entry(evidence_dir, action_id)
71
+ if found is None:
72
+ sys.exit(
73
+ f"No evidence entry for action {action_id} under {evidence_dir}.\n"
74
+ "Evidence is written by the Merkl hook on the machine the agent ran on."
75
+ )
76
+ entry, raw_line = found
77
+ session_id = entry["session_id"]
78
+
79
+ resp = httpx.get(
80
+ f"{endpoint}/v1/sessions/{session_id}/verify.html",
81
+ headers={"Authorization": f"Bearer {api_key}"},
82
+ timeout=30.0,
83
+ )
84
+ if resp.status_code == 422:
85
+ sys.exit(
86
+ f"Session {session_id} is not sealed yet — seal it first:\n"
87
+ f" curl -X POST -H 'Authorization: Bearer <key>' "
88
+ f"{endpoint}/v1/sessions/{session_id}/seal"
89
+ )
90
+ if not resp.is_success:
91
+ sys.exit(f"Failed to fetch verifier ({resp.status_code}): {resp.text[:200]}")
92
+
93
+ out = out_dir or Path(f"disclosure-{action_id[:8]}")
94
+ out.mkdir(parents=True, exist_ok=True)
95
+ (out / "verify.html").write_text(resp.text, encoding="utf-8")
96
+ (out / "evidence.jsonl").write_text(raw_line + "\n", encoding="utf-8")
97
+
98
+ print(f"Disclosure package: {out}/")
99
+ print(f" verify.html session {session_id[:8]}… proof bundle + verifier")
100
+ print(f" evidence.jsonl 1 record: {entry.get('tool_name', '?')} action {action_id[:8]}…")
101
+ print()
102
+ print("Send the folder to the auditor. They open verify.html in a browser")
103
+ print("and drop evidence.jsonl on it — no install, no network, no account.")
104
+ return out