Metadata-Version: 2.4
Name: watchlight-agent-sdk
Version: 0.5.0
Summary: Watchlight Agent SDK — build governed AI-agent plugins for any framework
Author-email: Watchlight AI <team@watchlight.ai>
License: Apache-2.0
Project-URL: Homepage, https://www.watchlight.ai
Project-URL: Documentation, https://docs.watchlight.ai
Project-URL: Repository, https://github.com/watchlight-ai-beacon/watchlight
Project-URL: Plugin SDK Guide, https://github.com/watchlight-ai-beacon/watchlight/tree/main/plugins
Keywords: watchlight,ai-governance,agent-runtime,ai-agents,agent-sdk,policy-decision-point,cedar-policy
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Monitoring
Classifier: Topic :: Security
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.0
Requires-Dist: typing-extensions>=4.5; python_version < "3.12"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
Requires-Dist: mypy>=1.8; extra == "dev"
Requires-Dist: cryptography>=42; extra == "dev"
Requires-Dist: requests>=2.32; extra == "dev"
Provides-Extra: otlp
Requires-Dist: opentelemetry-api>=1.20; extra == "otlp"
Requires-Dist: opentelemetry-sdk>=1.20; extra == "otlp"
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20; extra == "otlp"

# watchlight-core

Shared HTTP client and governance primitives used by every Watchlight
AI framework plugin (`watchlight-adk`, `watchlight-langgraph`,
`watchlight-bedrock`, …).

## Why this exists

Until #312, the same APDP HTTP client lived as three near-identical
copies under each framework plugin. As the contract stabilised across
ADK / LangGraph / Bedrock, three copies became the right trigger for
extraction. This package is the single source of truth for:

- The HTTP wire format with WL-APDP (`ApdpClient`)
- Plan-validation / preflight result wrappers (`PlanResult`,
  `PreflightResult`)
- The plan-normalisation contract (`normalize_plan`)
- The framework-agnostic `GovernanceState` shape consumed by the
  Beacon Dashboard / Streamlit governance card
- The base run-handle lifecycle (`BaseRunHandle`)
- Shared error types and the `governance_mode` env reader
- Plugin telemetry contract (`GovernanceTelemetry`)

## What's intentionally NOT here

Framework-specific behaviour stays in the plugin packages:

- ADK lifecycle callbacks + `PlanReActPlanner` parser + `BaseToolset`
  — `watchlight-adk`
- LangGraph plan-and-execute primitives — `watchlight-langgraph`
- AWS Bedrock event parsing + Lambda action-group decorator +
  `bedrock_session_attributes()` helper — `watchlight-bedrock`

## Install

```bash
# Minimal — APDP client + governance primitives only
pip install watchlight-core

# With OpenTelemetry export support (adds ~50MB of OTel SDK deps)
pip install 'watchlight-core[otlp]'

# Or in this monorepo:
pip install -e plugins/watchlight-core
pip install -e 'plugins/watchlight-core[otlp]'
```

Plugins depend on it explicitly via their `pyproject.toml`; the core
package depends only on `httpx` and (on Python 3.10) `typing-extensions`.
The `[otlp]` extra pulls in `opentelemetry-api`, `opentelemetry-sdk`, and
`opentelemetry-exporter-otlp-proto-http` — optional so cold-start-
sensitive environments (Lambda, Cloudflare Workers) don't pay the install
weight when they're not exporting to OTel.

## Public API

```python
from watchlight_core import (
    # HTTP client
    ApdpClient,

    # Errors
    WatchlightError,
    AgentNotRegistered,
    GovernanceUnavailable,

    # Result wrappers
    PlanResult,
    PreflightResult,

    # Plan / state
    normalize_plan,
    GovernanceState,
    GovernanceTelemetry,
    state_from_attrs,

    # Lifecycle
    BaseRunHandle,
    GovernedPlugin,

    # Custom-agent SDK (EN.6)
    watchlight,

    # OpenTelemetry export (EN.6) — requires [otlp] extra at construction
    OtlpConfig,
    OtlpProfile,
    WatchlightOtlpExporter,

    # Env
    governance_mode,
)
```

### Custom-agent SDK (`@watchlight`)

For agents NOT built on a framework with a Watchlight plugin (LangGraph,
ADK, Bedrock), the `@watchlight` decorator emits the canonical lineage
events for any Python function:

```python
from watchlight_core import ApdpClient, watchlight

client = ApdpClient(base_url="http://localhost:8081")

@watchlight(agent_id="custom-research", apdp_client=client)
async def run_research(query: str) -> str:
    # Your agent logic — emits execution_started / completed / failed
    return result
```

### OpenTelemetry export

Opt-in customer observability — sends a deliberate **subset** of the
lineage stream to your OTel collector. Default is `profile=off`; the
exporter is a no-op until you turn it on.

```python
from watchlight_core import OtlpConfig, WatchlightOtlpExporter

# Profile ladder: off ⊂ metrics ⊂ standard ⊂ governance
otlp = WatchlightOtlpExporter(OtlpConfig.from_env())
# WL_OTLP_PROFILE=standard
# WL_OTLP_ENDPOINT=http://otel-collector:4318
```

Tool arguments, LLM prompts, ABR raw scores, drift baselines, and
security-internal anomalies never leave Watchlight. The exported subset
is documented in `dev-docs/architecture/otlp-export.md`.

The package exposes a `py.typed` marker so type checkers (mypy, pyright)
pick up the inline annotations directly.

## Contract stability

Anything in `watchlight_core/__init__.py`'s `__all__` is a **stable
public API** under SemVer minor-version compatibility. Anything else
is internal and may move between minor versions.

When changing the wire format with WL-APDP (e.g. adding a route or
field), update `ApdpClient` here — the plugins inherit it for free.

## Building a new plugin

Implement the framework integration surface (lifecycle hooks, native
plan format, etc.) in your plugin package and use this package for
everything else:

1. Inherit `GovernanceTelemetry` to satisfy the `last_*` contract.
2. For frameworks with explicit primitives (LangGraph, Bedrock):
   subclass `BaseRunHandle` and add framework-specific helpers; expose
   `start_run(slug)` returning your subclass.
3. For frameworks with lifecycle callbacks (ADK): use `ApdpClient`
   directly and populate the `last_*` attributes from your callbacks.
4. Provide a one-line `state_from_<framework>_plugin` state helper
   using `state_from_attrs(plugin, framework="<name>")`.

The `GovernanceState` shape parity test in your plugin's test suite
guarantees the dashboard renders consistently across frameworks.

## Cross-references

- Multi-framework contract: `dev-docs/architecture/proxy-plugin-interop.md` §6
- Threat model: `dev-docs/security/HARDENING.md` (entries #301, #312, plus this PR's entry)

## Open-source by design — the plugin is glue

This package (and the framework plugins built on it) ships as readable,
Apache-2.0 source. That's safe because **all authorization decisions, scope
strict-subset attenuation, lineage/audit signing, and drift/anomaly scoring are
server-side** — in the compiled Rust core (`wl-apdp`) and the governed platform.
The plugin registers hooks, shapes a request, round-trips it to the PDP, and
projects the answer.

There is **no differentiating IP to strip** for a community build: the
difference between Developer Edition and Enterprise is the *backend* the plugin
talks to (the in-process DE engine vs. the full governed plane), never the
plugin code. And because any decision made client-side is *bypassable*, keeping
enforcement server-side is a security rule, not just an IP one.

This boundary is documented in [`plugins/CONTRIBUTING.md`](../CONTRIBUTING.md)
and enforced in CI by the plugin IP-boundary gate
(`scripts/ci/check_plugin_ip_boundary.py`).
