metalins 0.4.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.
@@ -0,0 +1,19 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+
9
+ # Virtual environments
10
+ .venv/
11
+ venv/
12
+ env/
13
+
14
+ # Test / tooling caches
15
+ .pytest_cache/
16
+ .mypy_cache/
17
+ .ruff_cache/
18
+ .coverage
19
+ htmlcov/
metalins-0.4.0/LICENSE ADDED
@@ -0,0 +1,2 @@
1
+ Apache License Version 2.0 — see ../LICENSE-Apache-2.0.txt at the monorepo root.
2
+ Copyright 2026 Jose Hernandez (Metalins).
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.4
2
+ Name: metalins
3
+ Version: 0.4.0
4
+ Summary: SDK for Metalins — identity verification for AI agents.
5
+ Project-URL: Homepage, https://metalins.ai
6
+ Project-URL: Repository, https://github.com/Metalins/metalins-python-sdk
7
+ Author-email: Metalins <support@metalins.ai>
8
+ License: Apache-2.0
9
+ License-File: LICENSE
10
+ Keywords: agents,ai,identity,metalins,verification
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: httpx>=0.27.0
20
+ Provides-Extra: dev
21
+ Requires-Dist: langchain-core>=0.3.0; extra == 'dev'
22
+ Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
23
+ Requires-Dist: pytest>=8.0; extra == 'dev'
24
+ Requires-Dist: respx>=0.21.0; extra == 'dev'
25
+ Requires-Dist: starlette>=0.37.0; extra == 'dev'
26
+ Provides-Extra: fastapi
27
+ Requires-Dist: fastapi>=0.110.0; extra == 'fastapi'
28
+ Provides-Extra: langchain
29
+ Requires-Dist: langchain-core>=0.3.0; extra == 'langchain'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # metalins
33
+
34
+ **Zero Trust identity verification for AI agents.**
35
+
36
+ Your agents in production are black boxes. Metalins verifies they're still the same agents you deployed — same model, same behavior, continuously. It's the behavioral verification layer in the Zero Trust stack for AI agents.
37
+
38
+ ## How it works
39
+
40
+ 1. The SDK hashes your agent's inputs and outputs **locally** — raw prompts and responses never leave your infrastructure.
41
+ 2. Signed hashes are sent to `api.metalins.ai`, where the behavioral engine runs.
42
+ 3. The engine returns a continuous verification status: `verified`, `caution`, or `not_verified`.
43
+
44
+ Your data stays in your infra. We only see fingerprints.
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ pip install metalins
50
+ ```
51
+
52
+ ## Quick start
53
+
54
+ Three lines to start verifying your agent:
55
+
56
+ ```python
57
+ import metalins
58
+
59
+ agent = metalins.Agent(api_key="ml_live_...", name="my-agent")
60
+ agent.start()
61
+
62
+ # Log each turn — hashing happens locally, automatically
63
+ agent.log(input=user_message, output=agent_reply)
64
+
65
+ # Check verification status at any time
66
+ status = agent.get_status() # "verified" | "caution" | "not_verified"
67
+ ```
68
+
69
+ Or as a context manager:
70
+
71
+ ```python
72
+ with metalins.Agent(api_key="ml_live_...", name="my-agent") as agent:
73
+ agent.log(input=user_message, output=agent_reply)
74
+ ```
75
+
76
+ Get your API key at [metalins.ai](https://metalins.ai).
77
+
78
+ ## Integrations
79
+
80
+ ### LangChain
81
+
82
+ ```python
83
+ from metalins import Agent
84
+ from metalins.integrations.langchain import MetalinsCallbackHandler
85
+
86
+ agent = Agent(api_key="ml_live_...", name="my-bot").start()
87
+ handler = MetalinsCallbackHandler(agent)
88
+
89
+ chain.invoke(user_input, config={"callbacks": [handler]})
90
+ ```
91
+
92
+ Every chain and LLM call is logged automatically — no manual `agent.log()` needed.
93
+
94
+ ### FastAPI / Starlette
95
+
96
+ ```python
97
+ import metalins
98
+ from metalins.integrations.fastapi import MetalinsMiddleware
99
+
100
+ agent = metalins.Agent(api_key="ml_live_...", name="my-api").start()
101
+ app.add_middleware(MetalinsMiddleware, agent=agent)
102
+ ```
103
+
104
+ Every request/response pair is logged automatically. Bodies are hashed locally and never buffered in full (1 MiB cap by default). Skip noisy endpoints with `exclude_paths=["/health"]`.
105
+
106
+ ### Anthropic SDK
107
+
108
+ ```python
109
+ import metalins
110
+
111
+ agent = metalins.Agent(api_key="ml_live_...", name="my-claude-agent").start()
112
+
113
+ with metalins.trace(agent):
114
+ response = client.messages.create(...)
115
+ ```
116
+
117
+ Or use the `@metalins.monitor` decorator on any function that calls the Anthropic SDK.
118
+
119
+ ## What leaves your infrastructure
120
+
121
+ Only hashed fingerprints — never raw text:
122
+
123
+ | What we receive | What stays with you |
124
+ |-----------------|---------------------|
125
+ | SHA-256 hash of input | Raw prompt text |
126
+ | SHA-256 hash of output | Raw response text |
127
+ | Timestamp + agent ID | Your users' data |
128
+ | HMAC-signed event chain | Your model config |
129
+
130
+ The behavioral engine compares fingerprint patterns over time. It does not reconstruct your prompts or responses.
131
+
132
+ ## State persistence
133
+
134
+ The SDK persists the agent session (ID, secret, hash chain) to `~/.metalins/<name>.json` with `0600` permissions by default. To store it elsewhere — a database, a secrets manager — pass any object with `load()` and `save()`:
135
+
136
+ ```python
137
+ agent = metalins.Agent(api_key="ml_live_...", name="my-bot", store=my_store)
138
+ ```
139
+
140
+ ## License
141
+
142
+ Apache 2.0. See [LICENSE](LICENSE).
@@ -0,0 +1,111 @@
1
+ # metalins
2
+
3
+ **Zero Trust identity verification for AI agents.**
4
+
5
+ Your agents in production are black boxes. Metalins verifies they're still the same agents you deployed — same model, same behavior, continuously. It's the behavioral verification layer in the Zero Trust stack for AI agents.
6
+
7
+ ## How it works
8
+
9
+ 1. The SDK hashes your agent's inputs and outputs **locally** — raw prompts and responses never leave your infrastructure.
10
+ 2. Signed hashes are sent to `api.metalins.ai`, where the behavioral engine runs.
11
+ 3. The engine returns a continuous verification status: `verified`, `caution`, or `not_verified`.
12
+
13
+ Your data stays in your infra. We only see fingerprints.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pip install metalins
19
+ ```
20
+
21
+ ## Quick start
22
+
23
+ Three lines to start verifying your agent:
24
+
25
+ ```python
26
+ import metalins
27
+
28
+ agent = metalins.Agent(api_key="ml_live_...", name="my-agent")
29
+ agent.start()
30
+
31
+ # Log each turn — hashing happens locally, automatically
32
+ agent.log(input=user_message, output=agent_reply)
33
+
34
+ # Check verification status at any time
35
+ status = agent.get_status() # "verified" | "caution" | "not_verified"
36
+ ```
37
+
38
+ Or as a context manager:
39
+
40
+ ```python
41
+ with metalins.Agent(api_key="ml_live_...", name="my-agent") as agent:
42
+ agent.log(input=user_message, output=agent_reply)
43
+ ```
44
+
45
+ Get your API key at [metalins.ai](https://metalins.ai).
46
+
47
+ ## Integrations
48
+
49
+ ### LangChain
50
+
51
+ ```python
52
+ from metalins import Agent
53
+ from metalins.integrations.langchain import MetalinsCallbackHandler
54
+
55
+ agent = Agent(api_key="ml_live_...", name="my-bot").start()
56
+ handler = MetalinsCallbackHandler(agent)
57
+
58
+ chain.invoke(user_input, config={"callbacks": [handler]})
59
+ ```
60
+
61
+ Every chain and LLM call is logged automatically — no manual `agent.log()` needed.
62
+
63
+ ### FastAPI / Starlette
64
+
65
+ ```python
66
+ import metalins
67
+ from metalins.integrations.fastapi import MetalinsMiddleware
68
+
69
+ agent = metalins.Agent(api_key="ml_live_...", name="my-api").start()
70
+ app.add_middleware(MetalinsMiddleware, agent=agent)
71
+ ```
72
+
73
+ Every request/response pair is logged automatically. Bodies are hashed locally and never buffered in full (1 MiB cap by default). Skip noisy endpoints with `exclude_paths=["/health"]`.
74
+
75
+ ### Anthropic SDK
76
+
77
+ ```python
78
+ import metalins
79
+
80
+ agent = metalins.Agent(api_key="ml_live_...", name="my-claude-agent").start()
81
+
82
+ with metalins.trace(agent):
83
+ response = client.messages.create(...)
84
+ ```
85
+
86
+ Or use the `@metalins.monitor` decorator on any function that calls the Anthropic SDK.
87
+
88
+ ## What leaves your infrastructure
89
+
90
+ Only hashed fingerprints — never raw text:
91
+
92
+ | What we receive | What stays with you |
93
+ |-----------------|---------------------|
94
+ | SHA-256 hash of input | Raw prompt text |
95
+ | SHA-256 hash of output | Raw response text |
96
+ | Timestamp + agent ID | Your users' data |
97
+ | HMAC-signed event chain | Your model config |
98
+
99
+ The behavioral engine compares fingerprint patterns over time. It does not reconstruct your prompts or responses.
100
+
101
+ ## State persistence
102
+
103
+ The SDK persists the agent session (ID, secret, hash chain) to `~/.metalins/<name>.json` with `0600` permissions by default. To store it elsewhere — a database, a secrets manager — pass any object with `load()` and `save()`:
104
+
105
+ ```python
106
+ agent = metalins.Agent(api_key="ml_live_...", name="my-bot", store=my_store)
107
+ ```
108
+
109
+ ## License
110
+
111
+ Apache 2.0. See [LICENSE](LICENSE).
@@ -0,0 +1,257 @@
1
+ # Anthropic SDK Integration — metalins.trace and @metalins.monitor
2
+
3
+ The Anthropic integration provides two primitives for recording Metalins
4
+ verification events when calling the Anthropic SDK directly (no LangChain
5
+ required):
6
+
7
+ - **`metalins.trace(agent)`** — a context manager that wraps any block
8
+ containing Anthropic API calls.
9
+ - **`@metalins.monitor(agent)`** — a decorator that wraps a function whose
10
+ first argument is the messages input and whose return value is the output.
11
+
12
+ Both work for sync and async code, capture input/output, and call
13
+ `agent.log(...)` exactly once per turn.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pip install metalins anthropic
19
+ ```
20
+
21
+ No extra `metalins` optional-dependency is needed — the Anthropic integration
22
+ module imports nothing from the `anthropic` package at load time. It works by
23
+ duck-typing the response object, so it is compatible with any version of the
24
+ Anthropic SDK.
25
+
26
+ ## Context Manager — `metalins.trace`
27
+
28
+ ### Sync Usage
29
+
30
+ ```python
31
+ import anthropic
32
+ import metalins
33
+
34
+ agent = metalins.Agent(api_key="ml_live_...", name="my-claude-agent").start()
35
+ client = anthropic.Anthropic()
36
+
37
+ messages = [{"role": "user", "content": "Summarise this article: ..."}]
38
+
39
+ with metalins.trace(agent) as t:
40
+ t.set_input(messages) # capture what you're sending
41
+ response = client.messages.create(
42
+ model="claude-opus-4-5",
43
+ max_tokens=1024,
44
+ messages=messages,
45
+ )
46
+ t.set_output(response) # capture the Anthropic response
47
+ ```
48
+
49
+ ### Async Usage
50
+
51
+ ```python
52
+ import anthropic
53
+ import metalins
54
+
55
+ agent = metalins.Agent(api_key="ml_live_...", name="my-async-agent").start()
56
+ async_client = anthropic.AsyncAnthropic()
57
+
58
+ async def handle_request(messages: list[dict]) -> str:
59
+ async with metalins.trace(agent) as t:
60
+ t.set_input(messages)
61
+ response = await async_client.messages.create(
62
+ model="claude-haiku-4-5",
63
+ max_tokens=512,
64
+ messages=messages,
65
+ )
66
+ t.set_output(response)
67
+ return response.content[0].text
68
+ ```
69
+
70
+ ### How the Context Manager Works
71
+
72
+ ```python
73
+ with metalins.trace(agent) as t:
74
+ ...
75
+ ```
76
+
77
+ `t` is a `TraceContext` object with two methods:
78
+
79
+ | Method | Purpose |
80
+ |--------|---------|
81
+ | `t.set_input(value)` | Record the input for this turn. Accepts a list of message dicts, a string, bytes, or any JSON-serialisable object. |
82
+ | `t.set_output(value)` | Record the output. Accepts an Anthropic `Message`, a plain string, or any JSON-serialisable object. Text is automatically extracted from `response.content[0].text`. |
83
+
84
+ If the block raises an exception, **nothing is logged** — the exception
85
+ propagates normally and the turn is treated as failed.
86
+
87
+ ## Decorator — `@metalins.monitor`
88
+
89
+ Use `monitor` when you have a function dedicated to calling Claude. The first
90
+ positional argument is captured as the input and the return value as the output.
91
+
92
+ ### Sync Decorator
93
+
94
+ ```python
95
+ import anthropic
96
+ import metalins
97
+
98
+ agent = metalins.Agent(api_key="ml_live_...", name="classification-agent").start()
99
+ client = anthropic.Anthropic()
100
+
101
+ @metalins.monitor(agent)
102
+ def classify_ticket(messages: list[dict]) -> str:
103
+ response = client.messages.create(
104
+ model="claude-haiku-4-5",
105
+ max_tokens=256,
106
+ messages=messages,
107
+ )
108
+ return response.content[0].text
109
+
110
+ # Call normally — Metalins logs input + output automatically.
111
+ result = classify_ticket([{"role": "user", "content": "My order never arrived."}])
112
+ ```
113
+
114
+ ### Async Decorator
115
+
116
+ ```python
117
+ import anthropic
118
+ import metalins
119
+
120
+ agent = metalins.Agent(api_key="ml_live_...", name="refund-agent").start()
121
+ async_client = anthropic.AsyncAnthropic()
122
+
123
+ @metalins.monitor(agent)
124
+ async def process_refund(messages: list[dict]) -> str:
125
+ response = await async_client.messages.create(
126
+ model="claude-opus-4-5",
127
+ max_tokens=1024,
128
+ messages=messages,
129
+ )
130
+ return response.content[0].text
131
+
132
+ # In your async route / task:
133
+ reply = await process_refund([{"role": "user", "content": "Refund order #42"}])
134
+ ```
135
+
136
+ ### What `monitor` Captures
137
+
138
+ | Logged field | Source |
139
+ |---|---|
140
+ | `input` | The first positional argument (JSON-serialised if it's a list/dict) |
141
+ | `output` | The return value — text extracted if it's an Anthropic Message, otherwise JSON-serialised |
142
+
143
+ If the function raises, **nothing is logged**.
144
+
145
+ ## Configuration Options
146
+
147
+ Both `trace` and `monitor` accept the same keyword arguments:
148
+
149
+ ```python
150
+ metalins.trace(
151
+ agent,
152
+
153
+ # Extra key/values attached to the Metalins event.
154
+ metadata={"service": "refund-agent", "env": "prod"},
155
+
156
+ # Re-raise agent.log() failures. Default False — log failures are
157
+ # silenced so a Metalins outage never breaks your application.
158
+ raise_on_error=False,
159
+ )
160
+
161
+ @metalins.monitor(
162
+ agent,
163
+ metadata={"model": "claude-haiku-4-5"},
164
+ raise_on_error=False,
165
+ )
166
+ async def run(messages): ...
167
+ ```
168
+
169
+ ## Choosing Between `trace` and `monitor`
170
+
171
+ | Scenario | Recommended primitive |
172
+ |---|---|
173
+ | Ad-hoc calls spread across a function body | `metalins.trace` context manager |
174
+ | A dedicated function that calls Claude and returns its result | `@metalins.monitor` decorator |
175
+ | Async FastAPI route that calls Claude directly | Either — `trace` gives more granular control |
176
+ | Multi-step reasoning where you want one event per turn | `metalins.trace` — call `set_input`/`set_output` once per conversation turn |
177
+
178
+ ## Full Example — FastAPI Route with Async Anthropic
179
+
180
+ ```python
181
+ import anthropic
182
+ import metalins
183
+ from fastapi import FastAPI
184
+ from pydantic import BaseModel
185
+ from contextlib import asynccontextmanager
186
+
187
+ metalins_agent = metalins.Agent(api_key="ml_live_...", name="support-bot")
188
+ anthropic_client = anthropic.AsyncAnthropic()
189
+
190
+
191
+ @asynccontextmanager
192
+ async def lifespan(app):
193
+ metalins_agent.start()
194
+ yield
195
+ metalins_agent.stop()
196
+
197
+
198
+ app = FastAPI(lifespan=lifespan)
199
+
200
+
201
+ class ChatRequest(BaseModel):
202
+ messages: list[dict]
203
+
204
+
205
+ @app.post("/chat")
206
+ async def chat(req: ChatRequest):
207
+ async with metalins.trace(metalins_agent) as t:
208
+ t.set_input(req.messages)
209
+ response = await anthropic_client.messages.create(
210
+ model="claude-haiku-4-5",
211
+ max_tokens=1024,
212
+ messages=req.messages,
213
+ )
214
+ t.set_output(response)
215
+ return {"reply": response.content[0].text}
216
+ ```
217
+
218
+ ## Full Example — Decorator Pattern with Metadata
219
+
220
+ ```python
221
+ import anthropic
222
+ import metalins
223
+
224
+ agent = metalins.Agent(api_key="ml_live_...", name="email-triage").start()
225
+ client = anthropic.Anthropic()
226
+
227
+ SYSTEM_PROMPT = "Classify the following support email into: billing, technical, general."
228
+
229
+
230
+ @metalins.monitor(agent, metadata={"pipeline": "email-triage", "model": "claude-haiku-4-5"})
231
+ def triage_email(messages: list[dict]) -> str:
232
+ response = client.messages.create(
233
+ model="claude-haiku-4-5",
234
+ max_tokens=128,
235
+ system=SYSTEM_PROMPT,
236
+ messages=messages,
237
+ )
238
+ return response.content[0].text
239
+
240
+
241
+ category = triage_email([{"role": "user", "content": "I was charged twice this month."}])
242
+ print(category) # "billing"
243
+ ```
244
+
245
+ ## Error Handling
246
+
247
+ ```python
248
+ # Default: swallow log errors (recommended for production)
249
+ with metalins.trace(agent) as t:
250
+ ... # agent.log() failure is silenced — your app keeps working
251
+
252
+ # Opt in to raising: useful during local development
253
+ with metalins.trace(agent, raise_on_error=True) as t:
254
+ ... # will raise if agent.log() fails
255
+ ```
256
+
257
+ The same option is available on `@metalins.monitor(agent, raise_on_error=True)`.