Metadata-Version: 2.4
Name: langchain-eisfi
Version: 0.1.0
Summary: LangChain middleware for the EISFI Weaver integrity chain: a tool call whose integrity precondition fails is never executed.
Author: Aaron Brown
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/Eru-Iluvatar-the-One/weaver/tree/main/libs/langchain-eisfi
Project-URL: Repository, https://github.com/Eru-Iluvatar-the-One/weaver
Project-URL: Issues, https://github.com/Eru-Iluvatar-the-One/weaver/issues
Keywords: langchain,middleware,agents,integrity,provenance,ai-safety,auditability
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: eisfi-weaver>=0.1.1
Requires-Dist: langchain>=1.3
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Dynamic: license-file

# langchain-eisfi

LangChain middleware for the [EISFI Weaver](https://pypi.org/project/eisfi-weaver/)
integrity chain.

**A tool call whose integrity precondition fails is not executed. The blocked operation is
never reached.**

Licensed under Apache-2.0.

---

## Why middleware and not a callback

A `BaseCallbackHandler` observes. It is explicitly forbidden from raising into the
framework and it has no mechanism to stop anything, so the strongest thing it can offer is
a record written *after* the act. That is an enforcement surface, and an enforcement
surface is something an optimizing system can learn to route around.

Weaver's claim is the opposite. A transition that fails its integrity precondition writes
**no state** — there is no rejected-entry record, no error slot, no partial credit. There
is nothing to route around, because the operation was never invoked.

LangChain's `wrap_tool_call` hands the middleware the `handler` that executes the tool and
lets the middleware decide whether to call it. Returning without calling `handler` means
the tool never runs. Not observed, not caught, not rolled back — **never reached.**

## Install

```bash
pip install langchain-eisfi
```

### On dependencies, stated precisely

This package requires `eisfi-weaver>=0.1.1` and `langchain>=1.3`. The middleware
interface lives in the **`langchain`** package; `langchain-core` alone is not sufficient
and contains no middleware API at all. `langchain` brings a substantial transitive tree,
which is what a LangChain integration package costs.

Both floors are set to what was actually exercised rather than to what probably works.
Verification ran against `langchain` 1.3.15 and `langchain-core` 1.5.6; 1.0.x was never
run, so it is not claimed. `eisfi-weaver` 0.1.0 shipped a reducer that was not total, and
this package depends on that reducer being total.

**`eisfi-weaver` itself has zero required runtime dependencies, and that remains true.**
It is a separate package and you can use it without any of this. That property belongs to
`eisfi-weaver` and it is not a property of `langchain-eisfi`. We say so plainly rather
than letting the two blur together.

## Use

```python
from langchain.agents import create_agent
from langchain_eisfi import WeaverMiddleware


def integrity_precondition(payload: dict) -> bool:
    """Return False to VOID the transition. The tool will not run."""
    if payload.get("event") != "tool_call":
        return True
    return payload.get("args", {}).get("path", "").startswith("/workspace/")


agent = create_agent(
    model=my_model,
    tools=my_tools,
    middleware=[
        WeaverMiddleware(
            chain_path="run.jsonl",
            precondition=integrity_precondition,
            on_void=lambda v: print("VOIDED:", v.reason),
        )
    ],
)
```

When the precondition fails, the agent receives a `ToolMessage` with `status="error"`
explaining the refusal, the tool is not invoked, and the chain does not advance.

### What lands in the chain

| hook | event |
|---|---|
| `before_model` | `before_model` |
| `wrap_tool_call` | `tool_call` — carries `tool`, `args`, `tool_call_id` |
| `after_model` | `after_model` |

The full message history is deliberately **not** recorded. The chain is an integrity
spine, not a transcript store; copying every message into every entry would grow the
chain quadratically with the run.

## When the integrity layer itself fails

`on_bookkeeping_error` controls what happens if the chain cannot record — a payload shape
that survives reduction but that canonicalization still refuses, a full disk, an
unavailable store.

- `"refuse"` (**default**) — the tool call is refused, exactly as a failed precondition
  would be.
- `"allow"` — the tool call proceeds, unrecorded.

The default is deliberate. **A guard that silently fails open is indistinguishable from no
guard, and it is worse than none, because the operator believes they are covered.** That
is precisely the failure this project exists to name: a detector reporting clean while
blind. Failing closed is visible, costs one refused tool call, and leaves no unrecorded
gap in the chain. `"allow"` exists because availability is sometimes genuinely the higher
duty — but choosing it should be an explicit act, never a default someone drifted into.

## Terminating the whole run

Not implemented, on purpose. LangChain middleware can end an agent run outright via
`@hook_config(can_jump_to=["end"])`, but whether a single integrity failure *should*
terminate an entire run is a policy decision for the operator deploying this, not a
default this package should make on anyone's behalf. Refusing the individual call is the
conservative behaviour and composes with whatever the host decides.

## Reducing arbitrary objects

Tool call arguments are arbitrary user objects. `eisfi_weaver.canonical.canonicalize`
deliberately refuses anything it cannot represent deterministically — including sets, NaN
and infinities — because silently coercing them would corrupt content addressing.

`eisfi_weaver.safe` is the total reducer that sits between the two, re-exported here as
`langchain_eisfi.safe`. It closes five hazards that otherwise raise mid-flight in a
production agent, or corrupt content addressing: non-finite floats, reference cycles,
unbounded nesting depth, objects whose `__repr__` raises, and unordered containers whose
`repr` varies with the per-process hash seed. Every reduction that loses information
leaves a visible marker (`<eisfi:nan>`, `<eisfi:cycle>`, `<eisfi:max-depth-exceeded>`,
`<eisfi:unreprable>`); none of them are silent.

The reducer is **not** reimplemented here. It is owned by `eisfi-weaver` and imported.
This package briefly carried its own copy, and a test asserts that
`langchain_eisfi.safe is eisfi_weaver.safe.safe` so a copy cannot return unnoticed — two
copies of a load-bearing boundary is the subject of corpus specimen 0006.

## Relationship to `eisfi-weaver`

This package does not reimplement the chain. `TransitionChain`, `canonicalize` and the
void semantics live in `eisfi-weaver` and are used as published. This is a thin shell that
adapts them to LangChain's agent loop.

## Standard tests

LangChain's `langchain-tests` package publishes standard test suites for chat models,
embeddings, tools, vector stores, retrievers, caches, stores and sandboxes. As of
`langchain-tests` 1.1.9 **there is no standard test suite for middleware** — the string
does not appear anywhere in the package. There is therefore nothing to conform to here,
and this is recorded so nobody spends an afternoon looking for it.
