computer-agent-py 0.1.0__py3-none-any.whl
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.
- computer_agent_py-0.1.0.dist-info/METADATA +307 -0
- computer_agent_py-0.1.0.dist-info/RECORD +25 -0
- computer_agent_py-0.1.0.dist-info/WHEEL +4 -0
- computeragent/__init__.py +90 -0
- computeragent/_proxy/__init__.py +8 -0
- computeragent/_proxy/client.py +225 -0
- computeragent/_proxy/query.py +165 -0
- computeragent/policy/__init__.py +59 -0
- computeragent/policy/authorizer.py +161 -0
- computeragent/policy/cedar.py +182 -0
- computeragent/policy/opa.py +124 -0
- computeragent/policy/types.py +121 -0
- computeragent/py.typed +0 -0
- computeragent/telemetry/__init__.py +24 -0
- computeragent/telemetry/config.py +176 -0
- computeragent/telemetry/event.py +355 -0
- computeragent/telemetry/middleware/__init__.py +8 -0
- computeragent/telemetry/middleware/guardrails.py +127 -0
- computeragent/telemetry/middleware/pii.py +158 -0
- computeragent/telemetry/pipeline.py +160 -0
- computeragent/telemetry/sinks/__init__.py +28 -0
- computeragent/telemetry/sinks/agentos.py +442 -0
- computeragent/telemetry/sinks/message_archive.py +136 -0
- computeragent/telemetry/sinks/otel.py +375 -0
- computeragent/types.py +13 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: computer-agent-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Drop-in replacement for claude-agent-sdk that adds a proxied telemetry pipeline (PII redaction + guardrails) with OpenTelemetry and AgentOS sinks.
|
|
5
|
+
Keywords: computeragent,claude-agent-sdk,claude,agent,telemetry,otel,opentelemetry,agentos,pii
|
|
6
|
+
Author: Abhi Bhat
|
|
7
|
+
Author-email: Abhi Bhat <abhishek.bhat@lyzr.ai>
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Dist: claude-agent-sdk>=0.2,<0.3
|
|
21
|
+
Requires-Dist: typing-extensions>=4.12
|
|
22
|
+
Requires-Dist: motor>=3.5 ; extra == 'agentos'
|
|
23
|
+
Requires-Dist: opentelemetry-api>=1.27 ; extra == 'all'
|
|
24
|
+
Requires-Dist: opentelemetry-sdk>=1.27 ; extra == 'all'
|
|
25
|
+
Requires-Dist: opentelemetry-exporter-otlp>=1.27 ; extra == 'all'
|
|
26
|
+
Requires-Dist: opentelemetry-semantic-conventions>=0.48b0 ; extra == 'all'
|
|
27
|
+
Requires-Dist: motor>=3.5 ; extra == 'all'
|
|
28
|
+
Requires-Dist: cedarpy>=4,<5 ; extra == 'all'
|
|
29
|
+
Requires-Dist: cedarpy>=4,<5 ; extra == 'cedar'
|
|
30
|
+
Requires-Dist: opentelemetry-api>=1.27 ; extra == 'otel'
|
|
31
|
+
Requires-Dist: opentelemetry-sdk>=1.27 ; extra == 'otel'
|
|
32
|
+
Requires-Dist: opentelemetry-exporter-otlp>=1.27 ; extra == 'otel'
|
|
33
|
+
Requires-Dist: opentelemetry-semantic-conventions>=0.48b0 ; extra == 'otel'
|
|
34
|
+
Requires-Python: >=3.10
|
|
35
|
+
Project-URL: Changelog, https://github.com/open-gitagent/computer-agent-py/blob/main/CHANGELOG.md
|
|
36
|
+
Project-URL: Homepage, https://github.com/open-gitagent/computer-agent-py
|
|
37
|
+
Project-URL: Issues, https://github.com/open-gitagent/computer-agent-py/issues
|
|
38
|
+
Provides-Extra: agentos
|
|
39
|
+
Provides-Extra: all
|
|
40
|
+
Provides-Extra: cedar
|
|
41
|
+
Provides-Extra: otel
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
|
|
44
|
+
# computer-agent-py
|
|
45
|
+
|
|
46
|
+
[](https://pypi.org/project/computer-agent-py/)
|
|
47
|
+
[](https://pypi.org/project/computer-agent-py/)
|
|
48
|
+
[](LICENSE)
|
|
49
|
+
|
|
50
|
+
**Drop-in replacement for [`claude-agent-sdk`](https://pypi.org/project/claude-agent-sdk/)** — change the import line, get a proxied telemetry pipeline with PII redaction, configurable OTel export, policy-based tool authorization, and full AgentOS integration for free.
|
|
51
|
+
|
|
52
|
+
```diff
|
|
53
|
+
- from claude_agent_sdk import ClaudeAgentOptions, query
|
|
54
|
+
- from claude_agent_sdk.types import ResultMessage
|
|
55
|
+
+ from computeragent import ClaudeAgentOptions, query
|
|
56
|
+
+ from computeragent.types import ResultMessage
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Every other line stays identical. `isinstance(msg, ResultMessage)` still works. The `claude` CLI subprocess, AWS Bedrock auth, MCP servers, permission modes, `cwd`, `add_dirs` — all behave exactly as the upstream SDK does.
|
|
60
|
+
|
|
61
|
+
> **Package vs import name** — PyPI distribution is `computer-agent-py` (hyphens for the wheel); the import name is `computeragent` (Python doesn't allow hyphens). So `pip install computer-agent-py` then `from computeragent import …`.
|
|
62
|
+
|
|
63
|
+
## Why use this
|
|
64
|
+
|
|
65
|
+
Adopting `computer-agent-py` in place of `claude-agent-sdk` gives you, without rewriting your agent code:
|
|
66
|
+
|
|
67
|
+
- **OpenTelemetry traces** following the GenAI Semantic Conventions, vendor-neutral (New Relic, Datadog, ClickHouse, Honeycomb, Tempo, Jaeger — one env-var change).
|
|
68
|
+
- **PII redaction** at the package boundary — email, phone, SSN, credit cards, AWS keys redacted before anything reaches a sink.
|
|
69
|
+
- **Generic guardrails** — attribute truncation, tool allowlists, per-session cost ceilings, content filters.
|
|
70
|
+
- **Policy-based tool-use authorization** — gate every tool call through OPA (remote) or Cedar (in-process). Fail-closed by default.
|
|
71
|
+
- **AgentOS visibility** — write to the same Mongo collections (`agent_registry`, `agent_logs`, `agent_messages`, `sessions`, `slack_threads`) the AgentOS frontend already reads. Library-mode agents show up in the Agents list, Logs tab, Chat transcript, and (with the harness running) live chat sandboxes.
|
|
72
|
+
- **Per-message archive** — every message, tool call, and policy decision archived to `agent_messages` for replay, RAG, and forensic audit.
|
|
73
|
+
|
|
74
|
+
## Install
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install computer-agent-py # core drop-in + OPA policy engine
|
|
78
|
+
pip install 'computer-agent-py[otel]' # + OpenTelemetry sink
|
|
79
|
+
pip install 'computer-agent-py[agentos]' # + AgentOS Mongo sinks
|
|
80
|
+
pip install 'computer-agent-py[cedar]' # + Cedar policy engine (in-process)
|
|
81
|
+
pip install 'computer-agent-py[all]' # everything
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Prerequisite** — same as upstream: the `claude` CLI binary on `PATH` and Anthropic / Bedrock credentials in the environment.
|
|
85
|
+
|
|
86
|
+
## Quickstart
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import asyncio
|
|
90
|
+
from computeragent import ClaudeAgentOptions, query
|
|
91
|
+
from computeragent.types import ResultMessage
|
|
92
|
+
|
|
93
|
+
async def main():
|
|
94
|
+
options = ClaudeAgentOptions(
|
|
95
|
+
model="claude-sonnet-4-5",
|
|
96
|
+
system_prompt="You are a helpful assistant.",
|
|
97
|
+
allowed_tools=["Read", "Glob", "Grep"],
|
|
98
|
+
permission_mode="bypassPermissions",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
async for message in query(prompt="Summarize the README.md in this directory.", options=options):
|
|
102
|
+
if isinstance(message, ResultMessage):
|
|
103
|
+
print(f"answer ({message.num_turns} turns, ${message.total_cost_usd:.4f}):")
|
|
104
|
+
print(message.result)
|
|
105
|
+
|
|
106
|
+
asyncio.run(main())
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
That's it — same shape as `claude-agent-sdk`. Telemetry is configured from env vars; without anything set, nothing leaves your process.
|
|
110
|
+
|
|
111
|
+
## Configure telemetry
|
|
112
|
+
|
|
113
|
+
### Env-driven (zero code change)
|
|
114
|
+
|
|
115
|
+
| Variable | Effect |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Enables OTLP/HTTP export. Unset → console exporter (debug). |
|
|
118
|
+
| `OTEL_EXPORTER_OTLP_HEADERS` | Comma-separated `key=value` (e.g. `api-key=NRRX-...` for New Relic). |
|
|
119
|
+
| `OTEL_SERVICE_NAME` | `service.name` attribute on every span. Default: `computeragent`. |
|
|
120
|
+
| `COMPUTERAGENT_OTEL` | `disabled` to suppress OtelSink. |
|
|
121
|
+
| `AGENTOS_MONGO_URL` | When set + `[agentos]` installed, attaches AgentOS sinks (registry, logs, sessions, agent_messages, slack_threads). |
|
|
122
|
+
| `AGENTOS_MONGO_DB` | Mongo database name. Default: `agentos`. |
|
|
123
|
+
| `COMPUTERAGENT_CAPTURE_CONTENT` | `1` to include prompts/responses on OTel spans. Default: off. |
|
|
124
|
+
| `COMPUTERAGENT_CAPTURE_CONTENT_MODE` | `events` (default) \| `attributes` \| `both`. |
|
|
125
|
+
|
|
126
|
+
### Programmatic
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from computeragent import configure, PiiRedactor, GuardrailFilter
|
|
130
|
+
from computeragent.telemetry.sinks import OtelSink, AgentRegistrySink, MongoMessageSink
|
|
131
|
+
|
|
132
|
+
configure(
|
|
133
|
+
middleware=[
|
|
134
|
+
PiiRedactor(strategy="hash", extra_patterns=[r"BADGE-\d{6}"]),
|
|
135
|
+
GuardrailFilter(
|
|
136
|
+
max_attribute_length=4096,
|
|
137
|
+
tool_name_allowlist={"Read", "Glob", "Grep", "mcp__nordassist-tools__*"},
|
|
138
|
+
cost_ceiling_usd=1.50,
|
|
139
|
+
),
|
|
140
|
+
],
|
|
141
|
+
sinks=[
|
|
142
|
+
OtelSink(), # picks up env
|
|
143
|
+
AgentRegistrySink(mongo_url="mongodb://..."), # registry + logs + sessions + slack_threads
|
|
144
|
+
MongoMessageSink(mongo_url="mongodb://..."), # per-message archive
|
|
145
|
+
],
|
|
146
|
+
)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Vendor-neutral OTel destinations
|
|
150
|
+
|
|
151
|
+
The package emits standard OTLP — point it at any backend by setting two env vars. No code change.
|
|
152
|
+
|
|
153
|
+
| Destination | `OTEL_EXPORTER_OTLP_ENDPOINT` | `OTEL_EXPORTER_OTLP_HEADERS` |
|
|
154
|
+
|---|---|---|
|
|
155
|
+
| **New Relic** | `https://otlp.nr-data.net` | `api-key=<NR_LICENSE_KEY>` |
|
|
156
|
+
| **Datadog** (via DD Agent OTLP) | `http://localhost:4318` | _(unset; agent handles auth)_ |
|
|
157
|
+
| **Honeycomb** | `https://api.honeycomb.io` | `x-honeycomb-team=<KEY>` |
|
|
158
|
+
| **Grafana Cloud Tempo** | `https://tempo-prod-...grafana.net:443` | `authorization=Basic <base64>` |
|
|
159
|
+
| **Self-hosted Jaeger / Tempo / SigNoz** | `http://<host>:4318` | _(unset)_ |
|
|
160
|
+
| **Local console (debug)** | _(unset)_ | _(unset)_ |
|
|
161
|
+
|
|
162
|
+
Full recipe table — including direct New Relic / Datadog without an OTel collector — is in [`examples/e2e/destinations.md`](examples/e2e/destinations.md).
|
|
163
|
+
|
|
164
|
+
## Policy-based tool-use authorization
|
|
165
|
+
|
|
166
|
+
For agents that need stronger guardrails than `permission_mode`, attach an external policy engine. Activation is a single new option-field; the rest of the worker code is unchanged.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from computeragent import ClaudeAgentOptions, PolicyPrincipal, PolicyResource, query
|
|
170
|
+
from computeragent.policy import OpaPolicyEngine, PolicyToolAuthorizer
|
|
171
|
+
|
|
172
|
+
opa = OpaPolicyEngine(
|
|
173
|
+
url="http://opa.platform:8181",
|
|
174
|
+
policy_path="computeragent/tools/allow",
|
|
175
|
+
fail_mode="deny", # default — engine errors deny the call
|
|
176
|
+
)
|
|
177
|
+
authorizer = PolicyToolAuthorizer(
|
|
178
|
+
engine=opa,
|
|
179
|
+
principal_resolver=lambda ctx: PolicyPrincipal(id="alice", groups=["engineer"]),
|
|
180
|
+
resource_resolver=lambda ctx: PolicyResource(agent_name="nordassist", model="claude-sonnet-4-5"),
|
|
181
|
+
context_resolver=lambda ctx: {"env": "prod"},
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
options = ClaudeAgentOptions(
|
|
185
|
+
...,
|
|
186
|
+
permission_mode="default", # was "bypassPermissions"
|
|
187
|
+
can_use_tool=authorizer,
|
|
188
|
+
)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Swap `OpaPolicyEngine` for `CedarPolicyEngine` (install with `pip install 'computer-agent-py[cedar]'`) and the worker code is identical:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from computeragent.policy import CedarPolicyEngine
|
|
195
|
+
|
|
196
|
+
cedar = CedarPolicyEngine(
|
|
197
|
+
policies=open("policies/computeragent.cedar").read(),
|
|
198
|
+
fail_mode="deny",
|
|
199
|
+
)
|
|
200
|
+
authorizer = PolicyToolAuthorizer(engine=cedar, principal_resolver=..., ...)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Sample policies are in [`examples/policies/`](examples/policies/) — one Rego file for OPA, one Cedar file. Each policy receives a canonical `PolicyInput` shape (`principal`, `action`, `resource`, `context`) so the engine choice is purely operational.
|
|
204
|
+
|
|
205
|
+
Every authorization decision emits a `policy_decision` telemetry event — `OtelSink` annotates the active `execute_tool` span with `policy.decision`, `policy.reason`, `policy.engine`, and `policy.latency_ms` so security audits and span queries co-locate.
|
|
206
|
+
|
|
207
|
+
## AgentOS integration
|
|
208
|
+
|
|
209
|
+
When `[agentos]` is installed and `AGENTOS_MONGO_URL` is set, every agent run writes to the Mongo collections the AgentOS frontend already reads:
|
|
210
|
+
|
|
211
|
+
| Collection | Per | What's in it |
|
|
212
|
+
|---|---|---|
|
|
213
|
+
| `agent_registry` | agent name | Identity + harness + model + last-seen; idempotent upsert |
|
|
214
|
+
| `agent_logs` | run | Rolled-up query/reply + tokens + cost + ok/error — drives the Logs tab |
|
|
215
|
+
| `sessions` | session | `entries[]` of `{type, text}` chat-bubble messages — drives the Chat tab transcript |
|
|
216
|
+
| `slack_threads` | session | TS parity row that drives the per-agent `sessionCount` + `lastActivity` aggregates |
|
|
217
|
+
| `agent_messages` | message | Per-event archive (`user_message`, `assistant_message`, `tool_use`, `tool_result`, `usage_snapshot`, `policy_decision`, `system_message`) for replay, RAG, audit |
|
|
218
|
+
|
|
219
|
+
The doc shapes are byte-for-byte compatible with the TypeScript `@open-gitagent/agent-registry-mongo` package — Python-driven agents show up in the same AgentOS UI that hosted TS agents do, with no frontend change.
|
|
220
|
+
|
|
221
|
+
### Live chat for library-mode agents
|
|
222
|
+
|
|
223
|
+
When the AgentRegistrySink writes its `agent_registry.source` row, it includes a full inline `IdentitySource` with `files: {agent.yaml, CLAUDE.md}` derived from your `ClaudeAgentOptions`. If a user clicks "New Chat" on the agent in the AgentOS SPA, the harness can clone those files into a sandbox workdir and spawn a live conversation — same UX as hosted (git-sourced) agents. The historical transcript stays available in the Chat tab regardless.
|
|
224
|
+
|
|
225
|
+
### DocumentDB compatibility
|
|
226
|
+
|
|
227
|
+
Prod deployments on AWS DocumentDB work without changes. The sinks use only operators DocumentDB supports — `$set`, `$setOnInsert`, `$push`, `update_one(upsert=True)`, `insert_one`. No aggregation pipelines, transactions, change streams, or TTL indexes. Set `MONGO_URL` with the standard `tls=true&tlsCAFile=...` params and mount the DocumentDB CA bundle.
|
|
228
|
+
|
|
229
|
+
## Architecture
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
user code: from computeragent import query, ClaudeAgentOptions
|
|
233
|
+
│
|
|
234
|
+
▼
|
|
235
|
+
computeragent._proxy.query ──┐
|
|
236
|
+
│ │
|
|
237
|
+
▼ │ PolicyToolAuthorizer
|
|
238
|
+
claude_agent_sdk → claude CLI subprocess → Bedrock│ (OPA / Cedar)
|
|
239
|
+
│ │ via can_use_tool
|
|
240
|
+
▼ │
|
|
241
|
+
yielded messages │
|
|
242
|
+
│ │
|
|
243
|
+
▼ │
|
|
244
|
+
TelemetryPipeline (taps stream) │
|
|
245
|
+
│ │
|
|
246
|
+
┌──── middleware ─────┐ │
|
|
247
|
+
│ PiiRedactor │ │
|
|
248
|
+
│ GuardrailFilter │ ◄────────────────────┘
|
|
249
|
+
│ <user-defined> │
|
|
250
|
+
└────────┬────────────┘
|
|
251
|
+
▼
|
|
252
|
+
┌──── fan-out to sinks ───────────────────────────┐
|
|
253
|
+
│ │
|
|
254
|
+
▼ ▼ ▼ ▼
|
|
255
|
+
OtelSink AgentRegistrySink MongoMessageSink <user>
|
|
256
|
+
│ │ │
|
|
257
|
+
▼ ▼ ▼
|
|
258
|
+
OTLP backend agent_registry agent_messages
|
|
259
|
+
(NR / DD / agent_logs (per-message
|
|
260
|
+
ClickHouse / sessions archive)
|
|
261
|
+
Tempo …) slack_threads
|
|
262
|
+
(drives AgentOS UI)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The proxy is a pure tap — messages are never modified or reordered. Sinks run as background tasks so a slow exporter never stalls the agent hot path; `query()`'s `finally` block awaits them with a 5 s default timeout. Telemetry never breaks an agent run: middleware and sink exceptions are absorbed and logged.
|
|
266
|
+
|
|
267
|
+
## Live e2e against AgentOS
|
|
268
|
+
|
|
269
|
+
[`examples/e2e/`](examples/e2e/) contains a recipe for standing up the full TypeScript stack (mongo + clickhouse + otel-collector + harness + agentos-server + SPA) via docker-compose and running this package against it. After ~60s of warm-up plus a 30s Python script run, you'll see the agent appear in the SPA's Agents list with `logCount`, `sessionCount`, `lastActivity`, and `activeSandboxes` populated; the Logs tab will show the rollup; the Chat tab will show the per-message transcript; the Observability tab will show the OTel trace tree. See [`examples/e2e/README.md`](examples/e2e/README.md).
|
|
270
|
+
|
|
271
|
+
## Examples
|
|
272
|
+
|
|
273
|
+
| File | Demonstrates |
|
|
274
|
+
|---|---|
|
|
275
|
+
| [`examples/pdf_drop_in.py`](examples/pdf_drop_in.py) | The minimum drop-in change |
|
|
276
|
+
| [`examples/with_otel.py`](examples/with_otel.py) | OTel pointed at a local collector |
|
|
277
|
+
| [`examples/with_new_relic.py`](examples/with_new_relic.py) | OTel pointed at New Relic (just env vars) |
|
|
278
|
+
| [`examples/with_datadog.py`](examples/with_datadog.py) | OTel pointed at Datadog |
|
|
279
|
+
| [`examples/with_agentos.py`](examples/with_agentos.py) | AgentOS Mongo writes |
|
|
280
|
+
| [`examples/with_message_archive.py`](examples/with_message_archive.py) | Per-message archive |
|
|
281
|
+
| [`examples/with_pii_redaction.py`](examples/with_pii_redaction.py) | PII middleware in front of every sink |
|
|
282
|
+
| [`examples/with_opa_policy.py`](examples/with_opa_policy.py) | OPA-gated tool use |
|
|
283
|
+
| [`examples/with_cedar_policy.py`](examples/with_cedar_policy.py) | Cedar-gated tool use (in-process) |
|
|
284
|
+
| [`examples/multi_sink.py`](examples/multi_sink.py) | All sinks + all guardrails together |
|
|
285
|
+
| [`examples/e2e/run_live_demo.py`](examples/e2e/run_live_demo.py) | Full live demo against the AgentOS docker-compose stack |
|
|
286
|
+
|
|
287
|
+
## Upstream pin
|
|
288
|
+
|
|
289
|
+
This release tracks **`claude-agent-sdk` 0.2.x**. The pinned upstream version is recorded in [`CHANGELOG.md`](CHANGELOG.md). Bump deliberately — wire-protocol field additions in upstream get re-exported automatically (identity-preserving), but any behavioral changes need a passthrough audit.
|
|
290
|
+
|
|
291
|
+
## Development
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
git clone https://github.com/open-gitagent/computer-agent-py
|
|
295
|
+
cd computer-agent-py
|
|
296
|
+
uv sync --all-extras --dev
|
|
297
|
+
uv run ruff check src tests
|
|
298
|
+
uv run ruff format --check src tests
|
|
299
|
+
uv run mypy src
|
|
300
|
+
uv run pytest -q # 120+ unit tests
|
|
301
|
+
uv run pytest -q -m integration # requires ANTHROPIC_API_KEY + claude CLI
|
|
302
|
+
uv build
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## License
|
|
306
|
+
|
|
307
|
+
MIT — see [`LICENSE`](LICENSE).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
computeragent/__init__.py,sha256=w_MWC-nCutEhDdnHx4SiKFToiVex8lHHKyIG1DHexzA,2939
|
|
2
|
+
computeragent/_proxy/__init__.py,sha256=7x5lm5tt4NhW8-4LvlM1grmdH0D_H5Ijc7a_BQjmMU8,218
|
|
3
|
+
computeragent/_proxy/client.py,sha256=FxRfA8mUdVTjQhlf4xTHCbUddZruvTPLzUOLWs5hzx8,8768
|
|
4
|
+
computeragent/_proxy/query.py,sha256=VIKMkbUPVHL3dUF4wggbXb0Gu35cTcu2HYd6-yL1nUg,6107
|
|
5
|
+
computeragent/policy/__init__.py,sha256=pKiV2ulBZW8khTa57UfqXGnicMviYxqL3CTO37Xo2js,1814
|
|
6
|
+
computeragent/policy/authorizer.py,sha256=PlkIcKtnSwl-B9f2FspX3-nVfzNLa43itV8EgnjW0BU,6310
|
|
7
|
+
computeragent/policy/cedar.py,sha256=mofBDW3HntADaim2WcMLUAXeZ9_OKaXuhMsaLRqzw10,6959
|
|
8
|
+
computeragent/policy/opa.py,sha256=Pho2_a97UhEpFsdvHJ71dXTnytqBYCmYb2GYpR7dABM,4920
|
|
9
|
+
computeragent/policy/types.py,sha256=sAGXyqT_-I-u9y2Mp583vkg86X9EOuI4YVBnaElnFVo,3488
|
|
10
|
+
computeragent/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
computeragent/telemetry/__init__.py,sha256=VZFpiN6hJHu5w8ISROlrRvotKyGdGlbSWjmXdu6mTEk,647
|
|
12
|
+
computeragent/telemetry/config.py,sha256=luSsIftLniXQ0n9rDY3oFaA19I3D5tZHTL9wFS9iExk,6475
|
|
13
|
+
computeragent/telemetry/event.py,sha256=qsBvY3q4ORUSaUC49pzHWnH_27Qy9eujT4KCHMlimgM,13819
|
|
14
|
+
computeragent/telemetry/middleware/__init__.py,sha256=4poNMyaJuiVhE0KSvYcTwCbcwbNAqOUJb6HeSkn160M,222
|
|
15
|
+
computeragent/telemetry/middleware/guardrails.py,sha256=elcT3AdvOTddwY-Lp0rQKchTlCAq3Y2-ky3cp9n2lbo,5435
|
|
16
|
+
computeragent/telemetry/middleware/pii.py,sha256=xD2JRR97GNw_5TMz1PhYLcl2HkLuLS4BQrfhOjsJMVg,6453
|
|
17
|
+
computeragent/telemetry/pipeline.py,sha256=bxuYJ1oNzRyT992I6SUF-K65MJywKpFZBp6-zcc3HOI,5667
|
|
18
|
+
computeragent/telemetry/sinks/__init__.py,sha256=sDBRPu9-9pDWFeavxzs6mo3EXIL8jJyElB8hC03e4uc,1009
|
|
19
|
+
computeragent/telemetry/sinks/agentos.py,sha256=vOZq-kwzGFHCiaMj82AOQqKGxcDLePngwlos7SMhs4Y,17707
|
|
20
|
+
computeragent/telemetry/sinks/message_archive.py,sha256=N5kEjCHjph0oevY7n0EeRd2c0bFppSrlpHm3Ijcu4ao,5339
|
|
21
|
+
computeragent/telemetry/sinks/otel.py,sha256=xOUufyiJpZsuPCmUp0bi7onCcEUPmGE3xXfyj9le5Y8,15914
|
|
22
|
+
computeragent/types.py,sha256=ttjh1Ovb4zWWUv68jhCtb14lH83YsapP8ailmpo7PbY,475
|
|
23
|
+
computer_agent_py-0.1.0.dist-info/WHEEL,sha256=eycQt0QpYmJMLKpE3X9iDk8R04v2ZF0x82ogq-zP6bQ,79
|
|
24
|
+
computer_agent_py-0.1.0.dist-info/METADATA,sha256=WsCcBdn8DSTEreP49T1ir1c2T35nwprXYXyZYLkcfg8,16897
|
|
25
|
+
computer_agent_py-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""``computeragent`` — drop-in replacement for ``claude_agent_sdk``.
|
|
2
|
+
|
|
3
|
+
Every public symbol from ``claude_agent_sdk`` is re-exported here with identity
|
|
4
|
+
preserved (``computeragent.ResultMessage is claude_agent_sdk.ResultMessage``).
|
|
5
|
+
``query`` and ``ClaudeSDKClient`` are wrapped so that every message flowing
|
|
6
|
+
through the upstream stream also flows through a telemetry pipeline with
|
|
7
|
+
configurable PII-redaction / guardrail middleware and pluggable sinks
|
|
8
|
+
(OpenTelemetry, AgentOS registry).
|
|
9
|
+
|
|
10
|
+
Drop-in usage::
|
|
11
|
+
|
|
12
|
+
# before
|
|
13
|
+
from claude_agent_sdk import ClaudeAgentOptions, query
|
|
14
|
+
from claude_agent_sdk.types import ResultMessage
|
|
15
|
+
|
|
16
|
+
# after — every other line stays identical
|
|
17
|
+
from computeragent import ClaudeAgentOptions, query
|
|
18
|
+
from computeragent.types import ResultMessage
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import claude_agent_sdk as _cas
|
|
24
|
+
|
|
25
|
+
# Re-export every public symbol from the upstream package by walking its
|
|
26
|
+
# `__all__`. This is identity-preserving — `computeragent.ResultMessage is
|
|
27
|
+
# claude_agent_sdk.ResultMessage` evaluates True — so user-side
|
|
28
|
+
# `isinstance(msg, ResultMessage)` keeps working regardless of import origin.
|
|
29
|
+
# Skip `__version__` so our own version (set below) wins.
|
|
30
|
+
_upstream_all = [n for n in getattr(_cas, "__all__", []) if n != "__version__"]
|
|
31
|
+
for _name in _upstream_all:
|
|
32
|
+
globals()[_name] = getattr(_cas, _name)
|
|
33
|
+
|
|
34
|
+
__version__ = "0.1.0"
|
|
35
|
+
upstream_version = getattr(_cas, "__version__", "unknown")
|
|
36
|
+
|
|
37
|
+
# Override the two entry points with the proxied versions. Same signatures,
|
|
38
|
+
# same yielded types — but each yielded message also flows through the
|
|
39
|
+
# telemetry pipeline.
|
|
40
|
+
from ._proxy.client import ClaudeSDKClient # noqa: E402
|
|
41
|
+
from ._proxy.query import query # noqa: E402
|
|
42
|
+
|
|
43
|
+
# Policy surface (v0.2 — tool-use authorization). Engines themselves
|
|
44
|
+
# (OpaPolicyEngine, CedarPolicyEngine) stay in ``computeragent.policy`` so
|
|
45
|
+
# users opt into Cedar's extra deliberately.
|
|
46
|
+
from .policy import ( # noqa: E402
|
|
47
|
+
PolicyAction,
|
|
48
|
+
PolicyDecision,
|
|
49
|
+
PolicyEngine,
|
|
50
|
+
PolicyInput,
|
|
51
|
+
PolicyPrincipal,
|
|
52
|
+
PolicyResource,
|
|
53
|
+
PolicyResult,
|
|
54
|
+
PolicyToolAuthorizer,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Telemetry surface (additions beyond the drop-in mirror).
|
|
58
|
+
from .telemetry import ( # noqa: E402
|
|
59
|
+
GuardrailFilter,
|
|
60
|
+
PiiRedactor,
|
|
61
|
+
Pipeline,
|
|
62
|
+
TelemetryEvent,
|
|
63
|
+
TelemetryMiddleware,
|
|
64
|
+
TelemetrySink,
|
|
65
|
+
configure,
|
|
66
|
+
get_pipeline,
|
|
67
|
+
shutdown,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
__all__ = sorted({*_upstream_all, "__version__"}) + [
|
|
71
|
+
# telemetry + policy add-ons (alphabetical at the tail so the public
|
|
72
|
+
# symbol list stays scannable when grepped against `claude_agent_sdk.__all__`)
|
|
73
|
+
"GuardrailFilter",
|
|
74
|
+
"PiiRedactor",
|
|
75
|
+
"Pipeline",
|
|
76
|
+
"PolicyAction",
|
|
77
|
+
"PolicyDecision",
|
|
78
|
+
"PolicyEngine",
|
|
79
|
+
"PolicyInput",
|
|
80
|
+
"PolicyPrincipal",
|
|
81
|
+
"PolicyResource",
|
|
82
|
+
"PolicyResult",
|
|
83
|
+
"PolicyToolAuthorizer",
|
|
84
|
+
"TelemetryEvent",
|
|
85
|
+
"TelemetryMiddleware",
|
|
86
|
+
"TelemetrySink",
|
|
87
|
+
"configure",
|
|
88
|
+
"get_pipeline",
|
|
89
|
+
"shutdown",
|
|
90
|
+
]
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""``ClaudeSDKClient`` — wraps upstream's class with a telemetry tap.
|
|
2
|
+
|
|
3
|
+
The wrapper holds an instance of the upstream client and forwards every
|
|
4
|
+
public method. ``receive_messages`` and ``receive_response`` re-yield messages
|
|
5
|
+
through the telemetry pipeline so multi-turn callers get the same
|
|
6
|
+
observability as :func:`computeragent.query`.
|
|
7
|
+
|
|
8
|
+
Control methods (``set_model``, ``get_context_usage``, etc.) pass straight
|
|
9
|
+
through — no telemetry for those today (can be added later as
|
|
10
|
+
``operation_started`` / ``operation_ended`` events).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import time
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
import claude_agent_sdk
|
|
20
|
+
from claude_agent_sdk.types import AssistantMessage, ResultMessage, TextBlock
|
|
21
|
+
|
|
22
|
+
from ..telemetry import TelemetryEvent, get_pipeline, new_session_id
|
|
23
|
+
from .query import _derive_agent_name
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from collections.abc import AsyncIterable, AsyncIterator
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger("computeragent")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ClaudeSDKClient:
|
|
32
|
+
"""Drop-in for :class:`claude_agent_sdk.ClaudeSDKClient`.
|
|
33
|
+
|
|
34
|
+
Same surface, same semantics, plus telemetry. The wrapper does not hold
|
|
35
|
+
extra state beyond the upstream instance and a per-session id used to
|
|
36
|
+
correlate emitted events.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, options: Any = None, transport: Any = None) -> None:
|
|
40
|
+
kwargs: dict[str, Any] = {}
|
|
41
|
+
if options is not None:
|
|
42
|
+
kwargs["options"] = options
|
|
43
|
+
if transport is not None:
|
|
44
|
+
kwargs["transport"] = transport
|
|
45
|
+
self._inner = claude_agent_sdk.ClaudeSDKClient(**kwargs)
|
|
46
|
+
self._options = options
|
|
47
|
+
self._session_id: str = new_session_id()
|
|
48
|
+
# Upstream's own session id is captured for join-by-id but is NOT
|
|
49
|
+
# used as the sink/event session_id (see _proxy.query for the
|
|
50
|
+
# placeholder-vs-upstream rationale).
|
|
51
|
+
self._upstream_session_id: str | None = None
|
|
52
|
+
self._agent_name = _derive_agent_name(options)
|
|
53
|
+
self._started_at: float | None = None
|
|
54
|
+
self._last_result: ResultMessage | None = None
|
|
55
|
+
self._last_assistant_text: str = ""
|
|
56
|
+
self._connected = False
|
|
57
|
+
|
|
58
|
+
# ── lifecycle ----------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
async def __aenter__(self) -> ClaudeSDKClient:
|
|
61
|
+
await self.connect()
|
|
62
|
+
return self
|
|
63
|
+
|
|
64
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool:
|
|
65
|
+
try:
|
|
66
|
+
await self.disconnect()
|
|
67
|
+
except Exception: # noqa: BLE001
|
|
68
|
+
logger.debug("disconnect in __aexit__ failed", exc_info=True)
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
async def connect(self, prompt: str | AsyncIterable[dict[str, Any]] | None = None) -> None:
|
|
72
|
+
pipeline = get_pipeline()
|
|
73
|
+
self._started_at = time.monotonic()
|
|
74
|
+
await pipeline.emit(
|
|
75
|
+
TelemetryEvent.session_started(
|
|
76
|
+
session_id=self._session_id,
|
|
77
|
+
agent_name=self._agent_name,
|
|
78
|
+
options=self._options,
|
|
79
|
+
prompt=prompt if prompt is not None else "<connect>",
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
if prompt is None:
|
|
83
|
+
await self._inner.connect()
|
|
84
|
+
else:
|
|
85
|
+
await self._inner.connect(prompt)
|
|
86
|
+
self._connected = True
|
|
87
|
+
|
|
88
|
+
async def disconnect(self) -> None:
|
|
89
|
+
if not self._connected:
|
|
90
|
+
return
|
|
91
|
+
try:
|
|
92
|
+
await self._emit_session_ended()
|
|
93
|
+
finally:
|
|
94
|
+
self._connected = False
|
|
95
|
+
try:
|
|
96
|
+
await self._inner.disconnect()
|
|
97
|
+
finally:
|
|
98
|
+
try:
|
|
99
|
+
await get_pipeline().flush()
|
|
100
|
+
except Exception: # noqa: BLE001
|
|
101
|
+
logger.debug("flush in disconnect failed", exc_info=True)
|
|
102
|
+
|
|
103
|
+
# ── streaming ----------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
async def query(
|
|
106
|
+
self, prompt: str | AsyncIterable[dict[str, Any]], session_id: str = "default"
|
|
107
|
+
) -> None:
|
|
108
|
+
await self._inner.query(prompt, session_id=session_id)
|
|
109
|
+
|
|
110
|
+
async def receive_messages(self) -> AsyncIterator[Any]:
|
|
111
|
+
pipeline = get_pipeline()
|
|
112
|
+
async for msg in self._inner.receive_messages():
|
|
113
|
+
real_sid = getattr(msg, "session_id", None)
|
|
114
|
+
if (
|
|
115
|
+
isinstance(real_sid, str)
|
|
116
|
+
and real_sid
|
|
117
|
+
and real_sid != self._session_id
|
|
118
|
+
and self._upstream_session_id is None
|
|
119
|
+
):
|
|
120
|
+
# Capture but don't rebind — see _proxy.query for rationale.
|
|
121
|
+
self._upstream_session_id = real_sid
|
|
122
|
+
for ev in TelemetryEvent.from_message(
|
|
123
|
+
msg, session_id=self._session_id, agent_name=self._agent_name
|
|
124
|
+
):
|
|
125
|
+
await pipeline.emit(ev)
|
|
126
|
+
if isinstance(msg, AssistantMessage):
|
|
127
|
+
for block in getattr(msg, "content", []) or []:
|
|
128
|
+
if isinstance(block, TextBlock):
|
|
129
|
+
self._last_assistant_text = block.text
|
|
130
|
+
if isinstance(msg, ResultMessage):
|
|
131
|
+
self._last_result = msg
|
|
132
|
+
yield msg
|
|
133
|
+
|
|
134
|
+
async def receive_response(self) -> AsyncIterator[Any]:
|
|
135
|
+
pipeline = get_pipeline()
|
|
136
|
+
async for msg in self._inner.receive_response():
|
|
137
|
+
real_sid = getattr(msg, "session_id", None)
|
|
138
|
+
if (
|
|
139
|
+
isinstance(real_sid, str)
|
|
140
|
+
and real_sid
|
|
141
|
+
and real_sid != self._session_id
|
|
142
|
+
and self._upstream_session_id is None
|
|
143
|
+
):
|
|
144
|
+
self._upstream_session_id = real_sid
|
|
145
|
+
for ev in TelemetryEvent.from_message(
|
|
146
|
+
msg, session_id=self._session_id, agent_name=self._agent_name
|
|
147
|
+
):
|
|
148
|
+
await pipeline.emit(ev)
|
|
149
|
+
if isinstance(msg, AssistantMessage):
|
|
150
|
+
for block in getattr(msg, "content", []) or []:
|
|
151
|
+
if isinstance(block, TextBlock):
|
|
152
|
+
self._last_assistant_text = block.text
|
|
153
|
+
if isinstance(msg, ResultMessage):
|
|
154
|
+
self._last_result = msg
|
|
155
|
+
yield msg
|
|
156
|
+
|
|
157
|
+
async def interrupt(self) -> None:
|
|
158
|
+
await self._inner.interrupt()
|
|
159
|
+
|
|
160
|
+
# ── pass-through control methods ---------------------------------------
|
|
161
|
+
|
|
162
|
+
async def set_model(self, model: str | None = None) -> None:
|
|
163
|
+
await self._inner.set_model(model)
|
|
164
|
+
|
|
165
|
+
async def set_permission_mode(self, mode: Any) -> None:
|
|
166
|
+
await self._inner.set_permission_mode(mode)
|
|
167
|
+
|
|
168
|
+
async def get_context_usage(self) -> Any:
|
|
169
|
+
return await self._inner.get_context_usage()
|
|
170
|
+
|
|
171
|
+
async def get_mcp_status(self) -> Any:
|
|
172
|
+
return await self._inner.get_mcp_status()
|
|
173
|
+
|
|
174
|
+
async def get_server_info(self) -> dict[str, Any] | None:
|
|
175
|
+
# Upstream's `get_server_info` is async; the proxy mirrors that.
|
|
176
|
+
result: dict[str, Any] | None = await self._inner.get_server_info()
|
|
177
|
+
return result
|
|
178
|
+
|
|
179
|
+
async def reconnect_mcp_server(self, server_name: str) -> None:
|
|
180
|
+
await self._inner.reconnect_mcp_server(server_name)
|
|
181
|
+
|
|
182
|
+
async def toggle_mcp_server(self, server_name: str, enabled: bool) -> None:
|
|
183
|
+
await self._inner.toggle_mcp_server(server_name, enabled)
|
|
184
|
+
|
|
185
|
+
async def stop_task(self, task_id: str) -> None:
|
|
186
|
+
await self._inner.stop_task(task_id)
|
|
187
|
+
|
|
188
|
+
async def rewind_files(self, user_message_id: str) -> None:
|
|
189
|
+
await self._inner.rewind_files(user_message_id)
|
|
190
|
+
|
|
191
|
+
# ── internals ----------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
async def _emit_session_ended(self) -> None:
|
|
194
|
+
if self._started_at is None:
|
|
195
|
+
return
|
|
196
|
+
duration_ms = (time.monotonic() - self._started_at) * 1000.0
|
|
197
|
+
pipeline = get_pipeline()
|
|
198
|
+
if self._last_result is not None:
|
|
199
|
+
ev = TelemetryEvent.session_ended_ok(
|
|
200
|
+
session_id=self._session_id,
|
|
201
|
+
agent_name=self._agent_name,
|
|
202
|
+
result=self._last_result,
|
|
203
|
+
duration_ms=duration_ms,
|
|
204
|
+
last_assistant_text=self._last_assistant_text,
|
|
205
|
+
)
|
|
206
|
+
if self._upstream_session_id is not None:
|
|
207
|
+
ev.payload["upstream_session_id"] = self._upstream_session_id
|
|
208
|
+
await pipeline.emit(ev)
|
|
209
|
+
else:
|
|
210
|
+
await pipeline.emit(
|
|
211
|
+
TelemetryEvent(
|
|
212
|
+
kind="session_ended",
|
|
213
|
+
session_id=self._session_id,
|
|
214
|
+
agent_name=self._agent_name,
|
|
215
|
+
payload={
|
|
216
|
+
"is_error": False,
|
|
217
|
+
"subtype": "no_result",
|
|
218
|
+
"result": self._last_assistant_text,
|
|
219
|
+
"duration_ms": duration_ms,
|
|
220
|
+
"num_turns": 0,
|
|
221
|
+
"total_cost_usd": 0.0,
|
|
222
|
+
"usage": {},
|
|
223
|
+
},
|
|
224
|
+
)
|
|
225
|
+
)
|