<!-- SPDX-License-Identifier: Apache-2.0 -->
**Status:** beta · **Owner:** Developer Relations · **License:** Apache-2.0

# kye-haystack — KYE Authority Components for Haystack

> **the framework decides what to do; KYE decides whether it was allowed to.**

A thin, patent-safe adapter that lets a [Haystack](https://haystack.deepset.ai/)
pipeline emit the KYE §0.3 governance + evidence envelopes and gates each
**consequential** tool call through the one KYE PDP. Sibling of the
[`langchain`](../langchain/README.md), [`llamaindex`](../llamaindex/README.md)
and [`crewai`](../crewai/README.md) adapters — same non-competition lock, same
envelope family, same "invent no new schema" rule.

This is the **Haystack half** of the Haystack + pgvector reference pair; the
retrieval half is [`../pgvector`](../pgvector/README.md). It is **LangChain-agnostic** —
you do not need LangChain to use it.

## The §0 / §0.30 / §32 non-competition lock (permanent)

KYE **governs** the Haystack pipeline as a first-class principal (§0.30); it
does **not** run, replace, or vendor Haystack, and it is **not** a parallel
agent / RAG / vector engine. `haystack` is a peer dependency referenced as an
integration point only. The lock vs LangChain / LlamaIndex / CrewAI / Haystack /
OpenAI Agents SDK / Claude Agent SDK is permanent (§0.25 "integrate, not
compete").

## What it does (five authority components + one drift signal)

| # | Component | Method | Emits (canonical, reused) |
|---|---|---|---|
| 1 | **Agent-manifest helper** | `bind_agent_manifest(...)` | `kye.agent.governance.v1` (§52) with a pinned version fingerprint + integrity digest |
| 2 | **Tool-call admissibility gate** | `admit_tool_call(...)` / `guard_tool(...)` | calls the PDP → `allow` / `deny` / `constrain` / `step_up` / `quarantine`; runs the real tool **only on `allow`**; `kye.evidence.tool_call.v1` on allow, `kye.agent.refusal.v1` on refusal |
| 3 | **Sub-agent delegation attenuation** | `attenuate_for_subagent(...)` | `kye.agent.governance.v1` — child scope **must** ⊆ parent scope; a widening delegation raises |
| 4 | **RAG reliance record** | `record_rag_reliance(...)` | `kye.evidence.tool_call_pin.v1` (§62 source pin) — *recall is not a mandate* |
| 5 | **Evidence Pack emitter** | `emit_evidence_pack()` | `kye.agent.completion.v1` + `kye.evidence.pack.v1` over the run ledger |
| — | **Authority-drift on version change** | `note_pack_version_change(...)` | `kye.agency_drift.event.v1` — an Agent-Pack / package bump requires a re-bind |

**No new schema. No second PDP client.** The decision is made by the one KYE PDP:
`decide` is an injected callable (exactly like the inference PEP's
`guardInference(req, admissibility)`), and `decide_via_sdk(client)` wires the
default to the canonical Python SDK's `KyeClient.authorize()` — swap it, never
fork it.

## Install

```bash
pip install kye-haystack            # authority core, zero required deps
pip install 'kye-haystack[sdk]'     # + the canonical KYE PDP client (kye-sdk)
pip install 'kye-haystack[haystack]'  # + the optional @component integration
```

## Quickstart

```python
from kye_haystack import KyeHaystackAuthority, decide_via_sdk
from kye_sdk import KyeClient   # the ONE PDP client — reused, never re-implemented

authority = KyeHaystackAuthority(
    tenant_id="kye:tenant:acme",
    actor="kye:agent:acme:haystack-rag",
    principal="kye:org:acme",
    decide=decide_via_sdk(KyeClient(base_url="https://api.kyeprotocol.com")),
)

# bind the pipeline to a signed manifest (§52) — pins the version fingerprint
authority.bind_agent_manifest(
    pipeline_name="support-rag",
    model="acme-llm-rev-7",
    versions={"haystack": "2.5.0", "kye-pack": "1.2.0"},
    tools=["retriever", "send_payment"],
)

# gate a consequential tool call — the real tool runs ONLY on allow
gated = authority.admit_tool_call(
    tool_name="send_payment",
    tool_input={"amount": 250},
    run=lambda: charge_customer(250),   # never called unless KYE admits it
)
if gated["outcome"] != "allow":
    handle_refusal(gated["decision"])   # deny / step_up / quarantine / constrain

# bind retrieved context to the action that relied on it — recall is not a mandate
authority.record_rag_reliance(
    source_id="kye:doc:refund-policy", source_version="v3",
    permitted_purpose="answer_customer_query", freshness="2026-07-01",
    classification="internal", downstream_action="draft_reply",
)

# seal the run
authority.emit_evidence_pack()
```

A fully runnable, network-free version is in
[`examples/quickstart.py`](examples/quickstart.py):

```bash
python examples/quickstart.py
```

### Inside a Haystack pipeline

```python
from kye_haystack.component import build_authority_gate

gate = build_authority_gate(authority, tool_name="send_payment")
pipeline.add_component("kye_gate", gate)
pipeline.connect("planner.action", "kye_gate.value")   # gate sits before the act
```

`kye_haystack.component` is the only module that touches `haystack`, and it
imports it lazily — the authority core works with no Haystack install.

## Fail-closed by construction

An authority component with no PDP wired **refuses** to admit a consequential
action (`AuthorityUnavailable`) rather than default-allow (§0.4 banking-grade).
An unrecognised verdict normalises to `deny`.

## Test

```bash
pip install pytest
python -m pytest public/oss/framework-adapters/haystack/tests -q
```

## Files

- `src/kye_haystack/authority.py` — the five authority components + drift signal + verdict normalisation.
- `src/kye_haystack/component.py` — the optional Haystack `@component` gate (lazy `haystack` import).
- `examples/quickstart.py` — a runnable, network-free end-to-end demo.
- `tests/test_authority.py` — pytest unit tests (no network, no Haystack install).
