watchlight-agent-sdk 0.5.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.
- watchlight_agent_sdk-0.5.0/PKG-INFO +221 -0
- watchlight_agent_sdk-0.5.0/README.md +182 -0
- watchlight_agent_sdk-0.5.0/pyproject.toml +101 -0
- watchlight_agent_sdk-0.5.0/setup.cfg +4 -0
- watchlight_agent_sdk-0.5.0/tests/test_arg_key_summary.py +84 -0
- watchlight_agent_sdk-0.5.0/tests/test_attenuate_decision.py +279 -0
- watchlight_agent_sdk-0.5.0/tests/test_canonical_signature.py +123 -0
- watchlight_agent_sdk-0.5.0/tests/test_conformance_invariant_11.py +402 -0
- watchlight_agent_sdk-0.5.0/tests/test_conformance_invariant_12.py +528 -0
- watchlight_agent_sdk-0.5.0/tests/test_conformance_invariant_13.py +554 -0
- watchlight_agent_sdk-0.5.0/tests/test_conformance_invariant_14.py +706 -0
- watchlight_agent_sdk-0.5.0/tests/test_connect_proxy.py +643 -0
- watchlight_agent_sdk-0.5.0/tests/test_core.py +1611 -0
- watchlight_agent_sdk-0.5.0/tests/test_credentials.py +728 -0
- watchlight_agent_sdk-0.5.0/tests/test_cross_plugin_conformance.py +777 -0
- watchlight_agent_sdk-0.5.0/tests/test_declare_intent.py +466 -0
- watchlight_agent_sdk-0.5.0/tests/test_decorator.py +217 -0
- watchlight_agent_sdk-0.5.0/tests/test_escalate_decision.py +204 -0
- watchlight_agent_sdk-0.5.0/tests/test_event_emission_credential.py +202 -0
- watchlight_agent_sdk-0.5.0/tests/test_events_schema.py +199 -0
- watchlight_agent_sdk-0.5.0/tests/test_in_process_client.py +156 -0
- watchlight_agent_sdk-0.5.0/tests/test_kill_signal.py +397 -0
- watchlight_agent_sdk-0.5.0/tests/test_lifecycle_emit.py +513 -0
- watchlight_agent_sdk-0.5.0/tests/test_lineage_contract_897.py +457 -0
- watchlight_agent_sdk-0.5.0/tests/test_lineage_invariant.py +242 -0
- watchlight_agent_sdk-0.5.0/tests/test_lineage_token_adoption.py +122 -0
- watchlight_agent_sdk-0.5.0/tests/test_observe_approval_decision.py +246 -0
- watchlight_agent_sdk-0.5.0/tests/test_otlp.py +425 -0
- watchlight_agent_sdk-0.5.0/tests/test_publish_observation.py +497 -0
- watchlight_agent_sdk-0.5.0/tests/test_publish_observation_for_execution.py +502 -0
- watchlight_agent_sdk-0.5.0/tests/test_publish_observation_lineage_contract_b.py +529 -0
- watchlight_agent_sdk-0.5.0/tests/test_quarantine_decision.py +575 -0
- watchlight_agent_sdk-0.5.0/tests/test_quarantine_kill_signal.py +347 -0
- watchlight_agent_sdk-0.5.0/tests/test_revoke_decision.py +308 -0
- watchlight_agent_sdk-0.5.0/tests/test_scope.py +443 -0
- watchlight_agent_sdk-0.5.0/tests/test_sever_subtree_decision.py +409 -0
- watchlight_agent_sdk-0.5.0/tests/test_sidecar.py +163 -0
- watchlight_agent_sdk-0.5.0/tests/test_spawn_subagent.py +662 -0
- watchlight_agent_sdk-0.5.0/tests/test_subagent_invariant.py +99 -0
- watchlight_agent_sdk-0.5.0/tests/test_subagent_registry.py +526 -0
- watchlight_agent_sdk-0.5.0/tests/test_terminate_decision.py +350 -0
- watchlight_agent_sdk-0.5.0/watchlight_agent_sdk.egg-info/PKG-INFO +221 -0
- watchlight_agent_sdk-0.5.0/watchlight_agent_sdk.egg-info/SOURCES.txt +71 -0
- watchlight_agent_sdk-0.5.0/watchlight_agent_sdk.egg-info/dependency_links.txt +1 -0
- watchlight_agent_sdk-0.5.0/watchlight_agent_sdk.egg-info/entry_points.txt +2 -0
- watchlight_agent_sdk-0.5.0/watchlight_agent_sdk.egg-info/requires.txt +17 -0
- watchlight_agent_sdk-0.5.0/watchlight_agent_sdk.egg-info/top_level.txt +1 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/__init__.py +355 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/__main__.py +18 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/apdp_client.py +925 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/conformance.py +2066 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/connect_proxy.py +656 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/credentials.py +652 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/decision.py +1170 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/declare_intent.py +360 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/decorator.py +416 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/env.py +22 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/errors.py +276 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/events_schema.py +808 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/in_process_client.py +317 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/kill_signal.py +479 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/otlp.py +903 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/plan.py +91 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/plan_event.py +186 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/py.typed +0 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/results.py +207 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/run_handle.py +3690 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/scope.py +472 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/sidecar.py +297 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/state.py +113 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/subagent_registry.py +443 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/telemetry.py +83 -0
- watchlight_agent_sdk-0.5.0/watchlight_core/testing.py +729 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: watchlight-agent-sdk
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Watchlight Agent SDK — build governed AI-agent plugins for any framework
|
|
5
|
+
Author-email: Watchlight AI <team@watchlight.ai>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://www.watchlight.ai
|
|
8
|
+
Project-URL: Documentation, https://docs.watchlight.ai
|
|
9
|
+
Project-URL: Repository, https://github.com/watchlight-ai-beacon/watchlight
|
|
10
|
+
Project-URL: Plugin SDK Guide, https://github.com/watchlight-ai-beacon/watchlight/tree/main/plugins
|
|
11
|
+
Keywords: watchlight,ai-governance,agent-runtime,ai-agents,agent-sdk,policy-decision-point,cedar-policy
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: System :: Monitoring
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
Requires-Dist: httpx>=0.27
|
|
27
|
+
Requires-Dist: pydantic>=2.0
|
|
28
|
+
Requires-Dist: typing-extensions>=4.5; python_version < "3.12"
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
32
|
+
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
33
|
+
Requires-Dist: cryptography>=42; extra == "dev"
|
|
34
|
+
Requires-Dist: requests>=2.32; extra == "dev"
|
|
35
|
+
Provides-Extra: otlp
|
|
36
|
+
Requires-Dist: opentelemetry-api>=1.20; extra == "otlp"
|
|
37
|
+
Requires-Dist: opentelemetry-sdk>=1.20; extra == "otlp"
|
|
38
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20; extra == "otlp"
|
|
39
|
+
|
|
40
|
+
# watchlight-core
|
|
41
|
+
|
|
42
|
+
Shared HTTP client and governance primitives used by every Watchlight
|
|
43
|
+
AI framework plugin (`watchlight-adk`, `watchlight-langgraph`,
|
|
44
|
+
`watchlight-bedrock`, …).
|
|
45
|
+
|
|
46
|
+
## Why this exists
|
|
47
|
+
|
|
48
|
+
Until #312, the same APDP HTTP client lived as three near-identical
|
|
49
|
+
copies under each framework plugin. As the contract stabilised across
|
|
50
|
+
ADK / LangGraph / Bedrock, three copies became the right trigger for
|
|
51
|
+
extraction. This package is the single source of truth for:
|
|
52
|
+
|
|
53
|
+
- The HTTP wire format with WL-APDP (`ApdpClient`)
|
|
54
|
+
- Plan-validation / preflight result wrappers (`PlanResult`,
|
|
55
|
+
`PreflightResult`)
|
|
56
|
+
- The plan-normalisation contract (`normalize_plan`)
|
|
57
|
+
- The framework-agnostic `GovernanceState` shape consumed by the
|
|
58
|
+
Beacon Dashboard / Streamlit governance card
|
|
59
|
+
- The base run-handle lifecycle (`BaseRunHandle`)
|
|
60
|
+
- Shared error types and the `governance_mode` env reader
|
|
61
|
+
- Plugin telemetry contract (`GovernanceTelemetry`)
|
|
62
|
+
|
|
63
|
+
## What's intentionally NOT here
|
|
64
|
+
|
|
65
|
+
Framework-specific behaviour stays in the plugin packages:
|
|
66
|
+
|
|
67
|
+
- ADK lifecycle callbacks + `PlanReActPlanner` parser + `BaseToolset`
|
|
68
|
+
— `watchlight-adk`
|
|
69
|
+
- LangGraph plan-and-execute primitives — `watchlight-langgraph`
|
|
70
|
+
- AWS Bedrock event parsing + Lambda action-group decorator +
|
|
71
|
+
`bedrock_session_attributes()` helper — `watchlight-bedrock`
|
|
72
|
+
|
|
73
|
+
## Install
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Minimal — APDP client + governance primitives only
|
|
77
|
+
pip install watchlight-core
|
|
78
|
+
|
|
79
|
+
# With OpenTelemetry export support (adds ~50MB of OTel SDK deps)
|
|
80
|
+
pip install 'watchlight-core[otlp]'
|
|
81
|
+
|
|
82
|
+
# Or in this monorepo:
|
|
83
|
+
pip install -e plugins/watchlight-core
|
|
84
|
+
pip install -e 'plugins/watchlight-core[otlp]'
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Plugins depend on it explicitly via their `pyproject.toml`; the core
|
|
88
|
+
package depends only on `httpx` and (on Python 3.10) `typing-extensions`.
|
|
89
|
+
The `[otlp]` extra pulls in `opentelemetry-api`, `opentelemetry-sdk`, and
|
|
90
|
+
`opentelemetry-exporter-otlp-proto-http` — optional so cold-start-
|
|
91
|
+
sensitive environments (Lambda, Cloudflare Workers) don't pay the install
|
|
92
|
+
weight when they're not exporting to OTel.
|
|
93
|
+
|
|
94
|
+
## Public API
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from watchlight_core import (
|
|
98
|
+
# HTTP client
|
|
99
|
+
ApdpClient,
|
|
100
|
+
|
|
101
|
+
# Errors
|
|
102
|
+
WatchlightError,
|
|
103
|
+
AgentNotRegistered,
|
|
104
|
+
GovernanceUnavailable,
|
|
105
|
+
|
|
106
|
+
# Result wrappers
|
|
107
|
+
PlanResult,
|
|
108
|
+
PreflightResult,
|
|
109
|
+
|
|
110
|
+
# Plan / state
|
|
111
|
+
normalize_plan,
|
|
112
|
+
GovernanceState,
|
|
113
|
+
GovernanceTelemetry,
|
|
114
|
+
state_from_attrs,
|
|
115
|
+
|
|
116
|
+
# Lifecycle
|
|
117
|
+
BaseRunHandle,
|
|
118
|
+
GovernedPlugin,
|
|
119
|
+
|
|
120
|
+
# Custom-agent SDK (EN.6)
|
|
121
|
+
watchlight,
|
|
122
|
+
|
|
123
|
+
# OpenTelemetry export (EN.6) — requires [otlp] extra at construction
|
|
124
|
+
OtlpConfig,
|
|
125
|
+
OtlpProfile,
|
|
126
|
+
WatchlightOtlpExporter,
|
|
127
|
+
|
|
128
|
+
# Env
|
|
129
|
+
governance_mode,
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Custom-agent SDK (`@watchlight`)
|
|
134
|
+
|
|
135
|
+
For agents NOT built on a framework with a Watchlight plugin (LangGraph,
|
|
136
|
+
ADK, Bedrock), the `@watchlight` decorator emits the canonical lineage
|
|
137
|
+
events for any Python function:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from watchlight_core import ApdpClient, watchlight
|
|
141
|
+
|
|
142
|
+
client = ApdpClient(base_url="http://localhost:8081")
|
|
143
|
+
|
|
144
|
+
@watchlight(agent_id="custom-research", apdp_client=client)
|
|
145
|
+
async def run_research(query: str) -> str:
|
|
146
|
+
# Your agent logic — emits execution_started / completed / failed
|
|
147
|
+
return result
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### OpenTelemetry export
|
|
151
|
+
|
|
152
|
+
Opt-in customer observability — sends a deliberate **subset** of the
|
|
153
|
+
lineage stream to your OTel collector. Default is `profile=off`; the
|
|
154
|
+
exporter is a no-op until you turn it on.
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from watchlight_core import OtlpConfig, WatchlightOtlpExporter
|
|
158
|
+
|
|
159
|
+
# Profile ladder: off ⊂ metrics ⊂ standard ⊂ governance
|
|
160
|
+
otlp = WatchlightOtlpExporter(OtlpConfig.from_env())
|
|
161
|
+
# WL_OTLP_PROFILE=standard
|
|
162
|
+
# WL_OTLP_ENDPOINT=http://otel-collector:4318
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Tool arguments, LLM prompts, ABR raw scores, drift baselines, and
|
|
166
|
+
security-internal anomalies never leave Watchlight. The exported subset
|
|
167
|
+
is documented in `dev-docs/architecture/otlp-export.md`.
|
|
168
|
+
|
|
169
|
+
The package exposes a `py.typed` marker so type checkers (mypy, pyright)
|
|
170
|
+
pick up the inline annotations directly.
|
|
171
|
+
|
|
172
|
+
## Contract stability
|
|
173
|
+
|
|
174
|
+
Anything in `watchlight_core/__init__.py`'s `__all__` is a **stable
|
|
175
|
+
public API** under SemVer minor-version compatibility. Anything else
|
|
176
|
+
is internal and may move between minor versions.
|
|
177
|
+
|
|
178
|
+
When changing the wire format with WL-APDP (e.g. adding a route or
|
|
179
|
+
field), update `ApdpClient` here — the plugins inherit it for free.
|
|
180
|
+
|
|
181
|
+
## Building a new plugin
|
|
182
|
+
|
|
183
|
+
Implement the framework integration surface (lifecycle hooks, native
|
|
184
|
+
plan format, etc.) in your plugin package and use this package for
|
|
185
|
+
everything else:
|
|
186
|
+
|
|
187
|
+
1. Inherit `GovernanceTelemetry` to satisfy the `last_*` contract.
|
|
188
|
+
2. For frameworks with explicit primitives (LangGraph, Bedrock):
|
|
189
|
+
subclass `BaseRunHandle` and add framework-specific helpers; expose
|
|
190
|
+
`start_run(slug)` returning your subclass.
|
|
191
|
+
3. For frameworks with lifecycle callbacks (ADK): use `ApdpClient`
|
|
192
|
+
directly and populate the `last_*` attributes from your callbacks.
|
|
193
|
+
4. Provide a one-line `state_from_<framework>_plugin` state helper
|
|
194
|
+
using `state_from_attrs(plugin, framework="<name>")`.
|
|
195
|
+
|
|
196
|
+
The `GovernanceState` shape parity test in your plugin's test suite
|
|
197
|
+
guarantees the dashboard renders consistently across frameworks.
|
|
198
|
+
|
|
199
|
+
## Cross-references
|
|
200
|
+
|
|
201
|
+
- Multi-framework contract: `dev-docs/architecture/proxy-plugin-interop.md` §6
|
|
202
|
+
- Threat model: `dev-docs/security/HARDENING.md` (entries #301, #312, plus this PR's entry)
|
|
203
|
+
|
|
204
|
+
## Open-source by design — the plugin is glue
|
|
205
|
+
|
|
206
|
+
This package (and the framework plugins built on it) ships as readable,
|
|
207
|
+
Apache-2.0 source. That's safe because **all authorization decisions, scope
|
|
208
|
+
strict-subset attenuation, lineage/audit signing, and drift/anomaly scoring are
|
|
209
|
+
server-side** — in the compiled Rust core (`wl-apdp`) and the governed platform.
|
|
210
|
+
The plugin registers hooks, shapes a request, round-trips it to the PDP, and
|
|
211
|
+
projects the answer.
|
|
212
|
+
|
|
213
|
+
There is **no differentiating IP to strip** for a community build: the
|
|
214
|
+
difference between Developer Edition and Enterprise is the *backend* the plugin
|
|
215
|
+
talks to (the in-process DE engine vs. the full governed plane), never the
|
|
216
|
+
plugin code. And because any decision made client-side is *bypassable*, keeping
|
|
217
|
+
enforcement server-side is a security rule, not just an IP one.
|
|
218
|
+
|
|
219
|
+
This boundary is documented in [`plugins/CONTRIBUTING.md`](../CONTRIBUTING.md)
|
|
220
|
+
and enforced in CI by the plugin IP-boundary gate
|
|
221
|
+
(`scripts/ci/check_plugin_ip_boundary.py`).
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# watchlight-core
|
|
2
|
+
|
|
3
|
+
Shared HTTP client and governance primitives used by every Watchlight
|
|
4
|
+
AI framework plugin (`watchlight-adk`, `watchlight-langgraph`,
|
|
5
|
+
`watchlight-bedrock`, …).
|
|
6
|
+
|
|
7
|
+
## Why this exists
|
|
8
|
+
|
|
9
|
+
Until #312, the same APDP HTTP client lived as three near-identical
|
|
10
|
+
copies under each framework plugin. As the contract stabilised across
|
|
11
|
+
ADK / LangGraph / Bedrock, three copies became the right trigger for
|
|
12
|
+
extraction. This package is the single source of truth for:
|
|
13
|
+
|
|
14
|
+
- The HTTP wire format with WL-APDP (`ApdpClient`)
|
|
15
|
+
- Plan-validation / preflight result wrappers (`PlanResult`,
|
|
16
|
+
`PreflightResult`)
|
|
17
|
+
- The plan-normalisation contract (`normalize_plan`)
|
|
18
|
+
- The framework-agnostic `GovernanceState` shape consumed by the
|
|
19
|
+
Beacon Dashboard / Streamlit governance card
|
|
20
|
+
- The base run-handle lifecycle (`BaseRunHandle`)
|
|
21
|
+
- Shared error types and the `governance_mode` env reader
|
|
22
|
+
- Plugin telemetry contract (`GovernanceTelemetry`)
|
|
23
|
+
|
|
24
|
+
## What's intentionally NOT here
|
|
25
|
+
|
|
26
|
+
Framework-specific behaviour stays in the plugin packages:
|
|
27
|
+
|
|
28
|
+
- ADK lifecycle callbacks + `PlanReActPlanner` parser + `BaseToolset`
|
|
29
|
+
— `watchlight-adk`
|
|
30
|
+
- LangGraph plan-and-execute primitives — `watchlight-langgraph`
|
|
31
|
+
- AWS Bedrock event parsing + Lambda action-group decorator +
|
|
32
|
+
`bedrock_session_attributes()` helper — `watchlight-bedrock`
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Minimal — APDP client + governance primitives only
|
|
38
|
+
pip install watchlight-core
|
|
39
|
+
|
|
40
|
+
# With OpenTelemetry export support (adds ~50MB of OTel SDK deps)
|
|
41
|
+
pip install 'watchlight-core[otlp]'
|
|
42
|
+
|
|
43
|
+
# Or in this monorepo:
|
|
44
|
+
pip install -e plugins/watchlight-core
|
|
45
|
+
pip install -e 'plugins/watchlight-core[otlp]'
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Plugins depend on it explicitly via their `pyproject.toml`; the core
|
|
49
|
+
package depends only on `httpx` and (on Python 3.10) `typing-extensions`.
|
|
50
|
+
The `[otlp]` extra pulls in `opentelemetry-api`, `opentelemetry-sdk`, and
|
|
51
|
+
`opentelemetry-exporter-otlp-proto-http` — optional so cold-start-
|
|
52
|
+
sensitive environments (Lambda, Cloudflare Workers) don't pay the install
|
|
53
|
+
weight when they're not exporting to OTel.
|
|
54
|
+
|
|
55
|
+
## Public API
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from watchlight_core import (
|
|
59
|
+
# HTTP client
|
|
60
|
+
ApdpClient,
|
|
61
|
+
|
|
62
|
+
# Errors
|
|
63
|
+
WatchlightError,
|
|
64
|
+
AgentNotRegistered,
|
|
65
|
+
GovernanceUnavailable,
|
|
66
|
+
|
|
67
|
+
# Result wrappers
|
|
68
|
+
PlanResult,
|
|
69
|
+
PreflightResult,
|
|
70
|
+
|
|
71
|
+
# Plan / state
|
|
72
|
+
normalize_plan,
|
|
73
|
+
GovernanceState,
|
|
74
|
+
GovernanceTelemetry,
|
|
75
|
+
state_from_attrs,
|
|
76
|
+
|
|
77
|
+
# Lifecycle
|
|
78
|
+
BaseRunHandle,
|
|
79
|
+
GovernedPlugin,
|
|
80
|
+
|
|
81
|
+
# Custom-agent SDK (EN.6)
|
|
82
|
+
watchlight,
|
|
83
|
+
|
|
84
|
+
# OpenTelemetry export (EN.6) — requires [otlp] extra at construction
|
|
85
|
+
OtlpConfig,
|
|
86
|
+
OtlpProfile,
|
|
87
|
+
WatchlightOtlpExporter,
|
|
88
|
+
|
|
89
|
+
# Env
|
|
90
|
+
governance_mode,
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Custom-agent SDK (`@watchlight`)
|
|
95
|
+
|
|
96
|
+
For agents NOT built on a framework with a Watchlight plugin (LangGraph,
|
|
97
|
+
ADK, Bedrock), the `@watchlight` decorator emits the canonical lineage
|
|
98
|
+
events for any Python function:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from watchlight_core import ApdpClient, watchlight
|
|
102
|
+
|
|
103
|
+
client = ApdpClient(base_url="http://localhost:8081")
|
|
104
|
+
|
|
105
|
+
@watchlight(agent_id="custom-research", apdp_client=client)
|
|
106
|
+
async def run_research(query: str) -> str:
|
|
107
|
+
# Your agent logic — emits execution_started / completed / failed
|
|
108
|
+
return result
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### OpenTelemetry export
|
|
112
|
+
|
|
113
|
+
Opt-in customer observability — sends a deliberate **subset** of the
|
|
114
|
+
lineage stream to your OTel collector. Default is `profile=off`; the
|
|
115
|
+
exporter is a no-op until you turn it on.
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from watchlight_core import OtlpConfig, WatchlightOtlpExporter
|
|
119
|
+
|
|
120
|
+
# Profile ladder: off ⊂ metrics ⊂ standard ⊂ governance
|
|
121
|
+
otlp = WatchlightOtlpExporter(OtlpConfig.from_env())
|
|
122
|
+
# WL_OTLP_PROFILE=standard
|
|
123
|
+
# WL_OTLP_ENDPOINT=http://otel-collector:4318
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Tool arguments, LLM prompts, ABR raw scores, drift baselines, and
|
|
127
|
+
security-internal anomalies never leave Watchlight. The exported subset
|
|
128
|
+
is documented in `dev-docs/architecture/otlp-export.md`.
|
|
129
|
+
|
|
130
|
+
The package exposes a `py.typed` marker so type checkers (mypy, pyright)
|
|
131
|
+
pick up the inline annotations directly.
|
|
132
|
+
|
|
133
|
+
## Contract stability
|
|
134
|
+
|
|
135
|
+
Anything in `watchlight_core/__init__.py`'s `__all__` is a **stable
|
|
136
|
+
public API** under SemVer minor-version compatibility. Anything else
|
|
137
|
+
is internal and may move between minor versions.
|
|
138
|
+
|
|
139
|
+
When changing the wire format with WL-APDP (e.g. adding a route or
|
|
140
|
+
field), update `ApdpClient` here — the plugins inherit it for free.
|
|
141
|
+
|
|
142
|
+
## Building a new plugin
|
|
143
|
+
|
|
144
|
+
Implement the framework integration surface (lifecycle hooks, native
|
|
145
|
+
plan format, etc.) in your plugin package and use this package for
|
|
146
|
+
everything else:
|
|
147
|
+
|
|
148
|
+
1. Inherit `GovernanceTelemetry` to satisfy the `last_*` contract.
|
|
149
|
+
2. For frameworks with explicit primitives (LangGraph, Bedrock):
|
|
150
|
+
subclass `BaseRunHandle` and add framework-specific helpers; expose
|
|
151
|
+
`start_run(slug)` returning your subclass.
|
|
152
|
+
3. For frameworks with lifecycle callbacks (ADK): use `ApdpClient`
|
|
153
|
+
directly and populate the `last_*` attributes from your callbacks.
|
|
154
|
+
4. Provide a one-line `state_from_<framework>_plugin` state helper
|
|
155
|
+
using `state_from_attrs(plugin, framework="<name>")`.
|
|
156
|
+
|
|
157
|
+
The `GovernanceState` shape parity test in your plugin's test suite
|
|
158
|
+
guarantees the dashboard renders consistently across frameworks.
|
|
159
|
+
|
|
160
|
+
## Cross-references
|
|
161
|
+
|
|
162
|
+
- Multi-framework contract: `dev-docs/architecture/proxy-plugin-interop.md` §6
|
|
163
|
+
- Threat model: `dev-docs/security/HARDENING.md` (entries #301, #312, plus this PR's entry)
|
|
164
|
+
|
|
165
|
+
## Open-source by design — the plugin is glue
|
|
166
|
+
|
|
167
|
+
This package (and the framework plugins built on it) ships as readable,
|
|
168
|
+
Apache-2.0 source. That's safe because **all authorization decisions, scope
|
|
169
|
+
strict-subset attenuation, lineage/audit signing, and drift/anomaly scoring are
|
|
170
|
+
server-side** — in the compiled Rust core (`wl-apdp`) and the governed platform.
|
|
171
|
+
The plugin registers hooks, shapes a request, round-trips it to the PDP, and
|
|
172
|
+
projects the answer.
|
|
173
|
+
|
|
174
|
+
There is **no differentiating IP to strip** for a community build: the
|
|
175
|
+
difference between Developer Edition and Enterprise is the *backend* the plugin
|
|
176
|
+
talks to (the in-process DE engine vs. the full governed plane), never the
|
|
177
|
+
plugin code. And because any decision made client-side is *bypassable*, keeping
|
|
178
|
+
enforcement server-side is a security rule, not just an IP one.
|
|
179
|
+
|
|
180
|
+
This boundary is documented in [`plugins/CONTRIBUTING.md`](../CONTRIBUTING.md)
|
|
181
|
+
and enforced in CI by the plugin IP-boundary gate
|
|
182
|
+
(`scripts/ci/check_plugin_ip_boundary.py`).
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
# PyPI distribution name. The IMPORTABLE name stays ``watchlight_core``
|
|
7
|
+
# (see [tool.setuptools.packages.find] below) — keeping it preserves
|
|
8
|
+
# every existing plugin's import path. The distribution name is what
|
|
9
|
+
# customers `pip install` and what surfaces on the PyPI listing.
|
|
10
|
+
name = "watchlight-agent-sdk"
|
|
11
|
+
version = "0.5.0"
|
|
12
|
+
description = "Watchlight Agent SDK — build governed AI-agent plugins for any framework"
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
license = {text = "Apache-2.0"}
|
|
15
|
+
requires-python = ">=3.10"
|
|
16
|
+
authors = [
|
|
17
|
+
{name = "Watchlight AI", email = "team@watchlight.ai"},
|
|
18
|
+
]
|
|
19
|
+
keywords = [
|
|
20
|
+
"watchlight",
|
|
21
|
+
"ai-governance",
|
|
22
|
+
"agent-runtime",
|
|
23
|
+
"ai-agents",
|
|
24
|
+
"agent-sdk",
|
|
25
|
+
"policy-decision-point",
|
|
26
|
+
"cedar-policy",
|
|
27
|
+
]
|
|
28
|
+
classifiers = [
|
|
29
|
+
"Development Status :: 4 - Beta",
|
|
30
|
+
"Intended Audience :: Developers",
|
|
31
|
+
"License :: OSI Approved :: Apache Software License",
|
|
32
|
+
"Operating System :: OS Independent",
|
|
33
|
+
"Programming Language :: Python :: 3",
|
|
34
|
+
"Programming Language :: Python :: 3.10",
|
|
35
|
+
"Programming Language :: Python :: 3.11",
|
|
36
|
+
"Programming Language :: Python :: 3.12",
|
|
37
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
38
|
+
"Topic :: System :: Monitoring",
|
|
39
|
+
"Topic :: Security",
|
|
40
|
+
"Typing :: Typed",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
# Strictly minimal. Pydantic v2 is required (events_schema mirror).
|
|
44
|
+
# Framework plugins add their own deps (langgraph, google-adk, boto3,
|
|
45
|
+
# pydantic-ai, …).
|
|
46
|
+
dependencies = [
|
|
47
|
+
"httpx>=0.27",
|
|
48
|
+
"pydantic>=2.0",
|
|
49
|
+
"typing-extensions>=4.5; python_version<'3.12'",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[project.optional-dependencies]
|
|
53
|
+
# `cryptography` is used only to issue throwaway PEMs for the mTLS
|
|
54
|
+
# round-trip test in test_core.py — it's not a runtime dep. The test
|
|
55
|
+
# auto-skips if the package is missing, so any caller pip-installing
|
|
56
|
+
# without [dev] still gets a working SDK, just without the
|
|
57
|
+
# SSL-context load_cert_chain coverage on their machine.
|
|
58
|
+
#
|
|
59
|
+
# `requests` is used only by `tests/test_connect_proxy.py::
|
|
60
|
+
# test_works_for_requests_library_too` — proves the local sidecar
|
|
61
|
+
# CONNECT proxy intercepts BOTH httpx and requests (universal-
|
|
62
|
+
# coverage claim). The test auto-skips when requests is missing,
|
|
63
|
+
# but we include it in [dev] so CI always exercises the universal-
|
|
64
|
+
# coverage path. Not a runtime dep of watchlight-core itself.
|
|
65
|
+
dev = [
|
|
66
|
+
"pytest>=7",
|
|
67
|
+
"pytest-asyncio>=0.21",
|
|
68
|
+
"mypy>=1.8",
|
|
69
|
+
"cryptography>=42",
|
|
70
|
+
"requests>=2.32",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
# OpenTelemetry export (EN.6) — opt-in customer-facing observability.
|
|
74
|
+
# The OTel SDK is ~50MB installed; customers who don't want OTLP
|
|
75
|
+
# (or run in cold-start-sensitive Lambda environments) install
|
|
76
|
+
# without this extra. `WatchlightOtlpExporter` construction raises
|
|
77
|
+
# ImportError at startup if a non-OFF profile is requested without
|
|
78
|
+
# the extra installed — fail-loud, not silent-no-op.
|
|
79
|
+
otlp = [
|
|
80
|
+
"opentelemetry-api>=1.20",
|
|
81
|
+
"opentelemetry-sdk>=1.20",
|
|
82
|
+
"opentelemetry-exporter-otlp-proto-http>=1.20",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
[project.urls]
|
|
86
|
+
Homepage = "https://www.watchlight.ai"
|
|
87
|
+
Documentation = "https://docs.watchlight.ai"
|
|
88
|
+
Repository = "https://github.com/watchlight-ai-beacon/watchlight"
|
|
89
|
+
"Plugin SDK Guide" = "https://github.com/watchlight-ai-beacon/watchlight/tree/main/plugins"
|
|
90
|
+
|
|
91
|
+
[project.scripts]
|
|
92
|
+
# Allows ``watchlight-conformance my_plugin`` as an alias for
|
|
93
|
+
# ``python -m watchlight_core.conformance my_plugin`` so plugin
|
|
94
|
+
# authors can run the SDK CLI without remembering the module path.
|
|
95
|
+
watchlight-conformance = "watchlight_core.conformance:_main"
|
|
96
|
+
|
|
97
|
+
[tool.setuptools.packages.find]
|
|
98
|
+
include = ["watchlight_core*"]
|
|
99
|
+
|
|
100
|
+
[tool.setuptools.package-data]
|
|
101
|
+
watchlight_core = ["py.typed"]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Tests for the ``arg_key_summary`` behavioural feature — the
|
|
2
|
+
sorted, comma-joined argument KEYS (never values) of a tool call.
|
|
3
|
+
|
|
4
|
+
This is the privacy-safe feature wl-drift's arg-shape scorer consumes.
|
|
5
|
+
The wire contract spans three ends that MUST agree:
|
|
6
|
+
|
|
7
|
+
* Rust canonical (``wl-lineage::events::ToolCallObservedPayload``)
|
|
8
|
+
* this Pydantic mirror (``watchlight_core.events_schema``)
|
|
9
|
+
* ``wl-proxy``'s MCP translator + every SDK plugin emitter
|
|
10
|
+
|
|
11
|
+
If these tests fail, the emitters and wl-drift have drifted on how the
|
|
12
|
+
arg-shape distribution is keyed, and drift scoring will be inconsistent
|
|
13
|
+
across the SDK and proxy vantages.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from watchlight_core import INPUT_SUMMARY_MAX_CHARS, summarize_arg_keys
|
|
19
|
+
from watchlight_core.events_schema import ToolCallObservedPayload
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestSummarizeArgKeys:
|
|
23
|
+
def test_sorted_comma_joined_keys(self) -> None:
|
|
24
|
+
# Order-independent: whatever kwarg order the framework used,
|
|
25
|
+
# the same call shape yields the same summary.
|
|
26
|
+
assert summarize_arg_keys({"text": "a", "format": "b"}) == "format,text"
|
|
27
|
+
assert summarize_arg_keys({"format": "b", "text": "a"}) == "format,text"
|
|
28
|
+
|
|
29
|
+
def test_single_key(self) -> None:
|
|
30
|
+
assert summarize_arg_keys({"query": "anything"}) == "query"
|
|
31
|
+
|
|
32
|
+
def test_values_never_appear(self) -> None:
|
|
33
|
+
out = summarize_arg_keys({"path": "/etc/passwd", "mode": "r"})
|
|
34
|
+
assert out == "mode,path"
|
|
35
|
+
assert "/etc/passwd" not in out
|
|
36
|
+
assert "r" not in out.split(",")
|
|
37
|
+
|
|
38
|
+
def test_empty_mapping_is_none(self) -> None:
|
|
39
|
+
# A no-argument call contributes no shape.
|
|
40
|
+
assert summarize_arg_keys({}) is None
|
|
41
|
+
|
|
42
|
+
def test_non_mapping_is_none(self) -> None:
|
|
43
|
+
# Only structured mappings carry keys; a flat string rendering
|
|
44
|
+
# (e.g. langchain's input_str) must NOT be treated as keys.
|
|
45
|
+
assert summarize_arg_keys(None) is None
|
|
46
|
+
assert summarize_arg_keys("query=hello") is None
|
|
47
|
+
assert summarize_arg_keys(42) is None
|
|
48
|
+
# A raw list (e.g. Bedrock's parameter dicts) is NOT a mapping;
|
|
49
|
+
# the bedrock emitter projects names into a mapping first.
|
|
50
|
+
assert summarize_arg_keys([{"name": "a"}]) is None
|
|
51
|
+
|
|
52
|
+
def test_non_string_keys_are_stringified(self) -> None:
|
|
53
|
+
assert summarize_arg_keys({1: "x", 2: "y"}) == "1,2"
|
|
54
|
+
|
|
55
|
+
def test_bounded_to_input_summary_max(self) -> None:
|
|
56
|
+
big = {f"key_{i:04d}": None for i in range(200)}
|
|
57
|
+
out = summarize_arg_keys(big)
|
|
58
|
+
assert out is not None
|
|
59
|
+
assert len(out) <= INPUT_SUMMARY_MAX_CHARS
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TestToolCallObservedPayloadField:
|
|
63
|
+
def test_field_round_trips(self) -> None:
|
|
64
|
+
p = ToolCallObservedPayload.model_validate(
|
|
65
|
+
{
|
|
66
|
+
"tool": {"resource_type": "tool", "identifier": "web_search"},
|
|
67
|
+
"action": {"action": "invoke"},
|
|
68
|
+
"arg_key_summary": "max_results,query",
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
assert p.arg_key_summary == "max_results,query"
|
|
72
|
+
assert p.model_dump(exclude_none=True)["arg_key_summary"] == "max_results,query"
|
|
73
|
+
|
|
74
|
+
def test_field_is_optional(self) -> None:
|
|
75
|
+
# Absent on the wire (older emitters / no structured args) →
|
|
76
|
+
# no arg-shape contribution, never a validation error.
|
|
77
|
+
p = ToolCallObservedPayload.model_validate(
|
|
78
|
+
{
|
|
79
|
+
"tool": {"resource_type": "tool", "identifier": "t"},
|
|
80
|
+
"action": {"action": "invoke"},
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
assert p.arg_key_summary is None
|
|
84
|
+
assert "arg_key_summary" not in p.model_dump(exclude_none=True)
|