Metadata-Version: 2.4
Name: nyx-monitor
Version: 0.1.0
Summary: Agent-embedded security monitor. Instruments the four agent boundaries (model-client, framework tool-dispatch, MCP, effect) and reports findings to a control plane.
Author: Nyx
License-Expression: Apache-2.0
Keywords: agents,instrumentation,llm,observability,security
Requires-Python: >=3.9
Provides-Extra: anthropic
Requires-Dist: anthropic>=0.30; extra == 'anthropic'
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: wrapt>=1.15; extra == 'dev'
Provides-Extra: litellm
Requires-Dist: litellm>=1.0; extra == 'litellm'
Provides-Extra: mcp
Requires-Dist: mcp>=1.0; extra == 'mcp'
Provides-Extra: openai
Requires-Dist: openai>=1.0; extra == 'openai'
Provides-Extra: wrapt
Requires-Dist: wrapt>=1.15; extra == 'wrapt'
Description-Content-Type: text/markdown

# Nyx Agentic Detection & Response (ADR)

> Part of the **Nyx Security** platform ([root README](../../README.md)) — the ADR
> capability: the in-process instrument SDK (`nyx/`) + the control plane / agentic
> SIEM (`control-plane/`). Its sibling is Agentic Security Posture at
> [`products/posture/`](../posture). Integration flows one-way, **posture → ADR**.

An **agent-embedded security monitor**. Nyx instruments the boundaries of an
agent process in-place and reports findings to a control plane. It runs inside
the agent — no proxy, no sidecar required — and is designed to *do no harm*: a
bug in instrumentation must never take down the host agent.

## The four boundaries

| Boundary | What it wraps | What you learn |
|---|---|---|
| **model_client** | `openai` chat.completions, **`anthropic` messages**, `litellm`, **`bedrock`** (boto3 Converse/InvokeModel), **`vertex`** (Gemini) | **Intent**: system prompt, messages, tool schemas *offered*, tool_calls *chosen* |
| **tool_dispatch** | LangChain `BaseTool.run/arun`, LlamaIndex `FunctionTool.call`, CrewAI `BaseTool.run`, AutoGen `FunctionTool.run` | **Action**: this agent, this run, this tool, these args |
| **mcp** | client `ClientSession.call_tool` + server `FastMCP.call_tool` / low-level `Server` handlers + protocol metadata (transport/server) | **Action + protocol**: tool, args, both sides of the call, and the assessment point for authz / confused-deputy |
| **effect** | `httpx`, `requests`, **`aiohttp`**, `subprocess.Popen`, `os.system` | **Ground truth**: what actually happened, even from raw code that bypassed every framework |

The effect boundary is the backstop: without it, coverage is only as good as
the framework-adapter list. With it, Nyx catches the agent writing and running
raw code that bypasses every abstraction.

## Status

Built and tested, default **observe-only**:

- ✅ **L1 capture** — `model_client` (OpenAI + Anthropic + litellm + Bedrock + Vertex), `tool_dispatch` (LangChain + LlamaIndex + CrewAI + AutoGen), `mcp` (client + server: FastMCP & low-level `Server`, + protocol metadata), `effect` (httpx/requests/aiohttp/subprocess/os.system)
- ✅ **L2 attribution** — stack/line capture (gated) **and identity/NHI capture** (cred type, source, fingerprint — never the secret)
- ✅ **L3 correlation** — `contextvars` span chain: effects nest under the tool span that triggered them; whole chain shares a `trace_id`
- ✅ **L4/L5 policy** — decision evaluated on every call and recorded; shadow-mode rollup; enforcement gated behind `mode="enforce"`
- ✅ **Metadata-only by default** (principle 6) — payloads (messages/tool args/response text) are hashed at ship time; metadata always sent. Detectors still see **raw** content in-process (principle 3: capture in-process, decide locally, ship metadata).
- ✅ **Coverage honesty** (principle 7) — subprocess / `os.system` events carry an explicit `coverage_gap` (execution leaves the interpreter)
- ✅ **Framework mapping** — findings carry version-stamped ATLAS / OWASP Agentic ids
- ✅ **Hardened L1 patching** — uses `wrapt` when installed (descriptor/signature-safe, survives version drift), hardened `functools`+`setattr` fallback otherwise; backend-agnostic, idempotent, never crashes import. Guarded by a per-adapter **conformance suite**.
- ✅ **Self-observability** (§9) — `nyx.metrics()`: events per boundary, coverage gaps, enforced vs. would-act, patch failures, reporter queue health
- ✅ **Co-located engine split** (§5/§6.6) — optional `UDSSink` ships telemetry to a separate `python -m nyx.engine` process over a Unix socket (fail-open); the blocking decision stays local in-process
- ✅ Non-blocking reporter (drop-on-overflow), zero hard dependencies (wrapt optional), self-traffic suppression
- ✅ Overhead **measured, not asserted**: ~14µs/call wrapper overhead worst-case (trivial callee); ~0.003% of a 500ms model call. Run `python benchmarks/overhead.py`.

### Posture ladder

Nyx walks a trust ladder, set via `mode` (or `NYX_MODE`):

The global `mode` is a **master switch** over **per-rule** modes:

| Global `mode` | Behavior |
|---|---|
| `observe` *(default)* | decisions evaluated + recorded on each event; nothing acted on, nothing shadow-tagged |
| `shadow` | every rule forced to shadow: would-act events tagged + rolled up, nothing acted on |
| `enforce` | per-rule honored — rules with `mode="enforce"` **act**; rules left in `mode="shadow"` are tagged would-act |

Each **rule** carries its own `mode` (`shadow`/`enforce`) and `fail` (`open`/`closed`), so you can flip one rule live while the rest stay in shadow. Gate actions: **block**, **redact** (mask args before the call), **modify**, and **hold** (synchronous approval via a registered approver).

### Roadmap

> **Feature freeze — the umbrella merge is the single active workstream.** New
> features are frozen in both products while ASPM + ADR converge into one repo, one
> design system, and one security graph. The merged roadmap (merge phases R1–R5 /
> P1–P5 + both product backlogs) is [`docs/roadmap.md`](../../docs/roadmap.md).

The live, **canonical roadmap is the [Nyx Roadmap project](https://github.com/users/kgalappatti-aegis/projects/6)** — that board is the source of truth for the current item list, status, and priorities. ADR phases to date:

- **Phase 1–3** ✅ — single-language spine · correlation & attribution · policy in shadow
- **Phase 4** 🟡 — enforcement: per-rule modes, `block`/`redact`/`modify`/`hold`, engine split, **MCP** (client + server) / Anthropic / **Bedrock** / **Vertex** / **LlamaIndex** / **CrewAI** / **AutoGen** / aiohttp *(next: Node/TS stack)*
- **Phase 5** ✅ — intelligence: **behavioral baselines** (per-agent novelty — `NYX_BASELINE=1`), **criticality tagging** (`NYX_AGENT_CRITICALITY` / `NYX_AGENT_FUNCTION`, findings ranked by blast radius), **taint tracking** (untrusted ingress → exfil sink, trace-scoped — `NYX_TAINT=1`), **downstream emission schema** (`nyx.emission/1` → ASPM / AEGIS via `POST /api/emit`)
- **Phase 6** ✅ — agentic SIEM: **smart policy from baselines** (`nyx.allowlist_rule` + control-plane allowlist proposals with anti-poisoning), **data-driven correlation/detection rules** (`lib/correlate.ts`, multi-event SIEM over the stream — `NYX_CORRELATION_RULES`) with a **durable alert store** (Nyx as system-of-record — Alerts screen, dedup + acknowledge/resolve, survives event aging), **Agentic Entity Behaviour Analytics (AEBA)** (per-agent fleet risk profile, Entities screen) — built from reviewed plans in [`docs/plans/`](../../docs/plans/)

See the [project board](https://github.com/users/kgalappatti-aegis/projects/6) for the detailed, current item list and status, and [`docs/plans/`](../../docs/plans/) for version-controlled, adversarially-reviewed implementation plans.

## Shadow mode

```python
nyx.install(mode="shadow")              # evaluate + tag + aggregate, never block
nyx.policy().add_precall(egress_allowlist)
...
nyx.shadow_summary()                     # {"total_would_act": 3, "by_policy": {...}, "examples": {...}}
```

Every non-allow decision is tagged onto its event (`event["shadow"]`) and rolled
up; a `shadow_summary` event is emitted every `shadow_summary_interval` seconds
and on shutdown. See `examples/shadow_demo.py`.

## Quick start

```python
import nyx

nyx.install()  # ConsoleSink: events to stderr
# or: nyx.install(endpoint="https://cp.example/ingest", api_key="...")

with nyx.run(agent_id="research-agent", session_id="sess-42"):
    ...  # run your agent; every event shares a trace_id
```

Everything is env-overridable for zero-code-change container drop-in:
`NYX_ENDPOINT`, `NYX_API_KEY`, `NYX_ENFORCE`, `NYX_CAPTURE_CONTENT`,
`NYX_CAPTURE_STACK`, `NYX_SYNC`, `NYX_SERVICE`, `NYX_ENV`.

## Writing policy

Nyx core ships the *mechanism*; rules are registered on the shared engine.

```python
from nyx import Finding, Decision, Rule

# a rule that ENFORCES (acts) while everything else stays shadow:
nyx.policy().add_rule(Rule(
    name="block-metadata",
    check=lambda ctx: Decision.block("cloud metadata egress", policy="block-metadata")
        if (ctx.request.get("host") or "").startswith("169.254.") else None,
    mode="enforce", fail="open",
))

# a rule that only watches (would-block logged, never acted on):
nyx.policy().add_rule(Rule(
    name="model-allowlist",
    check=lambda ctx: Decision.block("disallowed model", policy="model-allowlist")
        if ctx.request.get("model") not in ALLOWED else None,
    mode="shadow",
))

# HOLD requires synchronous approval:
nyx.set_approver(lambda ctx, decision: ask_human(ctx))   # -> bool

# detector: annotates the assembled event with findings (sees RAW content; never blocks)
def metadata_egress(event):
    host = (event.get("request") or {}).get("host") or ""
    if event["boundary"] == "effect" and host.startswith("169.254."):
        return Finding("egress.cloud_metadata", severity="critical",
                       message="agent reached cloud metadata endpoint",
                       data={"host": host}, atlas="AML.T0051", owasp="ASI05")
nyx.policy().add_detector(metadata_egress)
```

## See it work

```bash
PYTHONPATH=. python examples/chain_demo.py    # prints the reconstructed trace tree
PYTHONPATH=. python examples/shadow_demo.py   # shadow-mode would-block rollup
PYTHONPATH=. python examples/enforce_demo.py  # per-rule: one rule live, one shadow
PYTHONPATH=. python examples/mcp_demo.py       # MCP client + server boundary (needs `pip install mcp`)
PYTHONPATH=. python benchmarks/overhead.py    # measures per-call overhead
PYTHONPATH=. python benchmarks/policy.py      # policy hot path: linear scan vs boundary index
python -m pytest -q                           # 77 tests (two real-MCP tests skip without the SDK)

# co-located engine split: run the engine, point the shim at it
python -m nyx.engine --socket /tmp/nyx.sock --forward console &
NYX_ENGINE_SOCKET=/tmp/nyx.sock python your_agent.py
```

`chain_demo.py` output (the trace tree the control plane reconstructs):

```
trace 2628fa…  (agent=research-agent session=sess-42)
  └─ [model_client]  chose=['fetch_url']                                  @ main (chain_demo.py:90)
  └─ [tool_dispatch]  tool=fetch_url                                      @ main (chain_demo.py:96)
    └─ [effect]  GET 169.254.169.254  !! critical:egress.cloud_metadata   @ tool_body (chain_demo.py:81)
```

## Design guarantees

- **Do no harm.** Extractors, hooks, and detectors are sandboxed; the only exception Nyx raises into agent code is `NyxBlocked`, and only under `enforce=True`.
- **Fail open.** Errors in the enforcement path allow the call (`fail_open=True`).
- **No recursion.** The reporter posts via stdlib `urllib` inside `suppress_instrumentation()`, so the effect boundary never observes Nyx's own traffic.
- **Privacy / residency.** Metadata-only by default: payloads stay in the deployment boundary (hashed at ship time, opt in per deployment with `capture_content=True`); secret-keyed values are always redacted; identity is recorded as a fingerprint hash, never the credential.
```
