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.
Files changed (73) hide show
  1. watchlight_agent_sdk-0.5.0/PKG-INFO +221 -0
  2. watchlight_agent_sdk-0.5.0/README.md +182 -0
  3. watchlight_agent_sdk-0.5.0/pyproject.toml +101 -0
  4. watchlight_agent_sdk-0.5.0/setup.cfg +4 -0
  5. watchlight_agent_sdk-0.5.0/tests/test_arg_key_summary.py +84 -0
  6. watchlight_agent_sdk-0.5.0/tests/test_attenuate_decision.py +279 -0
  7. watchlight_agent_sdk-0.5.0/tests/test_canonical_signature.py +123 -0
  8. watchlight_agent_sdk-0.5.0/tests/test_conformance_invariant_11.py +402 -0
  9. watchlight_agent_sdk-0.5.0/tests/test_conformance_invariant_12.py +528 -0
  10. watchlight_agent_sdk-0.5.0/tests/test_conformance_invariant_13.py +554 -0
  11. watchlight_agent_sdk-0.5.0/tests/test_conformance_invariant_14.py +706 -0
  12. watchlight_agent_sdk-0.5.0/tests/test_connect_proxy.py +643 -0
  13. watchlight_agent_sdk-0.5.0/tests/test_core.py +1611 -0
  14. watchlight_agent_sdk-0.5.0/tests/test_credentials.py +728 -0
  15. watchlight_agent_sdk-0.5.0/tests/test_cross_plugin_conformance.py +777 -0
  16. watchlight_agent_sdk-0.5.0/tests/test_declare_intent.py +466 -0
  17. watchlight_agent_sdk-0.5.0/tests/test_decorator.py +217 -0
  18. watchlight_agent_sdk-0.5.0/tests/test_escalate_decision.py +204 -0
  19. watchlight_agent_sdk-0.5.0/tests/test_event_emission_credential.py +202 -0
  20. watchlight_agent_sdk-0.5.0/tests/test_events_schema.py +199 -0
  21. watchlight_agent_sdk-0.5.0/tests/test_in_process_client.py +156 -0
  22. watchlight_agent_sdk-0.5.0/tests/test_kill_signal.py +397 -0
  23. watchlight_agent_sdk-0.5.0/tests/test_lifecycle_emit.py +513 -0
  24. watchlight_agent_sdk-0.5.0/tests/test_lineage_contract_897.py +457 -0
  25. watchlight_agent_sdk-0.5.0/tests/test_lineage_invariant.py +242 -0
  26. watchlight_agent_sdk-0.5.0/tests/test_lineage_token_adoption.py +122 -0
  27. watchlight_agent_sdk-0.5.0/tests/test_observe_approval_decision.py +246 -0
  28. watchlight_agent_sdk-0.5.0/tests/test_otlp.py +425 -0
  29. watchlight_agent_sdk-0.5.0/tests/test_publish_observation.py +497 -0
  30. watchlight_agent_sdk-0.5.0/tests/test_publish_observation_for_execution.py +502 -0
  31. watchlight_agent_sdk-0.5.0/tests/test_publish_observation_lineage_contract_b.py +529 -0
  32. watchlight_agent_sdk-0.5.0/tests/test_quarantine_decision.py +575 -0
  33. watchlight_agent_sdk-0.5.0/tests/test_quarantine_kill_signal.py +347 -0
  34. watchlight_agent_sdk-0.5.0/tests/test_revoke_decision.py +308 -0
  35. watchlight_agent_sdk-0.5.0/tests/test_scope.py +443 -0
  36. watchlight_agent_sdk-0.5.0/tests/test_sever_subtree_decision.py +409 -0
  37. watchlight_agent_sdk-0.5.0/tests/test_sidecar.py +163 -0
  38. watchlight_agent_sdk-0.5.0/tests/test_spawn_subagent.py +662 -0
  39. watchlight_agent_sdk-0.5.0/tests/test_subagent_invariant.py +99 -0
  40. watchlight_agent_sdk-0.5.0/tests/test_subagent_registry.py +526 -0
  41. watchlight_agent_sdk-0.5.0/tests/test_terminate_decision.py +350 -0
  42. watchlight_agent_sdk-0.5.0/watchlight_agent_sdk.egg-info/PKG-INFO +221 -0
  43. watchlight_agent_sdk-0.5.0/watchlight_agent_sdk.egg-info/SOURCES.txt +71 -0
  44. watchlight_agent_sdk-0.5.0/watchlight_agent_sdk.egg-info/dependency_links.txt +1 -0
  45. watchlight_agent_sdk-0.5.0/watchlight_agent_sdk.egg-info/entry_points.txt +2 -0
  46. watchlight_agent_sdk-0.5.0/watchlight_agent_sdk.egg-info/requires.txt +17 -0
  47. watchlight_agent_sdk-0.5.0/watchlight_agent_sdk.egg-info/top_level.txt +1 -0
  48. watchlight_agent_sdk-0.5.0/watchlight_core/__init__.py +355 -0
  49. watchlight_agent_sdk-0.5.0/watchlight_core/__main__.py +18 -0
  50. watchlight_agent_sdk-0.5.0/watchlight_core/apdp_client.py +925 -0
  51. watchlight_agent_sdk-0.5.0/watchlight_core/conformance.py +2066 -0
  52. watchlight_agent_sdk-0.5.0/watchlight_core/connect_proxy.py +656 -0
  53. watchlight_agent_sdk-0.5.0/watchlight_core/credentials.py +652 -0
  54. watchlight_agent_sdk-0.5.0/watchlight_core/decision.py +1170 -0
  55. watchlight_agent_sdk-0.5.0/watchlight_core/declare_intent.py +360 -0
  56. watchlight_agent_sdk-0.5.0/watchlight_core/decorator.py +416 -0
  57. watchlight_agent_sdk-0.5.0/watchlight_core/env.py +22 -0
  58. watchlight_agent_sdk-0.5.0/watchlight_core/errors.py +276 -0
  59. watchlight_agent_sdk-0.5.0/watchlight_core/events_schema.py +808 -0
  60. watchlight_agent_sdk-0.5.0/watchlight_core/in_process_client.py +317 -0
  61. watchlight_agent_sdk-0.5.0/watchlight_core/kill_signal.py +479 -0
  62. watchlight_agent_sdk-0.5.0/watchlight_core/otlp.py +903 -0
  63. watchlight_agent_sdk-0.5.0/watchlight_core/plan.py +91 -0
  64. watchlight_agent_sdk-0.5.0/watchlight_core/plan_event.py +186 -0
  65. watchlight_agent_sdk-0.5.0/watchlight_core/py.typed +0 -0
  66. watchlight_agent_sdk-0.5.0/watchlight_core/results.py +207 -0
  67. watchlight_agent_sdk-0.5.0/watchlight_core/run_handle.py +3690 -0
  68. watchlight_agent_sdk-0.5.0/watchlight_core/scope.py +472 -0
  69. watchlight_agent_sdk-0.5.0/watchlight_core/sidecar.py +297 -0
  70. watchlight_agent_sdk-0.5.0/watchlight_core/state.py +113 -0
  71. watchlight_agent_sdk-0.5.0/watchlight_core/subagent_registry.py +443 -0
  72. watchlight_agent_sdk-0.5.0/watchlight_core/telemetry.py +83 -0
  73. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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)