Metadata-Version: 2.4
Name: llama-index-tools-enigmagent
Version: 0.1.0
Summary: LlamaIndex tools integration for EnigmAgent — resolve {{PLACEHOLDER}} secrets at the LLM boundary so models never see real API keys
Project-URL: Homepage, https://github.com/Agnuxo1/llama-index-tools-enigmagent
Project-URL: Bug Tracker, https://github.com/Agnuxo1/llama-index-tools-enigmagent/issues
Project-URL: Main Project, https://github.com/Agnuxo1/EnigmAgent
Author-email: Francisco Angulo de Lafuente <agnuxo1@gmail.com>
License: MIT
License-File: LICENSE
Keywords: agent,credentials,enigmagent,llama-index,llamaindex,mcp,secrets,security,tools,vault
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
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: Topic :: Security :: Cryptography
Requires-Python: >=3.10
Requires-Dist: httpx>=0.24
Requires-Dist: llama-index-core>=0.11
Requires-Dist: pydantic>=2
Description-Content-Type: text/markdown

# llama-index-tools-enigmagent

[![CI](https://github.com/Agnuxo1/llama-index-tools-enigmagent/actions/workflows/ci.yml/badge.svg)](https://github.com/Agnuxo1/llama-index-tools-enigmagent/actions/workflows/ci.yml)
[![PyPI version](https://img.shields.io/pypi/v/llama-index-tools-enigmagent?label=pypi&color=blue)](https://pypi.org/project/llama-index-tools-enigmagent/)
[![PyPI downloads](https://img.shields.io/pypi/dm/llama-index-tools-enigmagent.svg)](https://pypi.org/project/llama-index-tools-enigmagent/)
[![Python](https://img.shields.io/pypi/pyversions/llama-index-tools-enigmagent.svg)](https://pypi.org/project/llama-index-tools-enigmagent/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![LlamaIndex](https://img.shields.io/badge/LlamaIndex-0.11+-green.svg)](https://github.com/run-llama/llama_index)
[![EnigmAgent](https://img.shields.io/badge/EnigmAgent-MCP-purple.svg)](https://github.com/Agnuxo1/EnigmAgent)
[![GitHub stars](https://img.shields.io/github/stars/Agnuxo1/llama-index-tools-enigmagent?style=social)](https://github.com/Agnuxo1/llama-index-tools-enigmagent)

> **Last week I asked a LlamaIndex agent to push a fix to a private GitHub repo. To do that, the agent needed my personal access token. I had three options, and all three were terrible: paste the token into the prompt (and into the provider's logs forever), give the agent a long-lived token it could reuse on its own at 3 a.m., or give up and do it by hand.**

`llama-index-tools-enigmagent` is option four.

Your LlamaIndex agent emits `{{GITHUB_TOKEN}}`. The placeholder leaves the model and travels through the prompt, the QueryPipeline, the tool inputs, the trace — and only at the moment your tool actually needs the credential does EnigmAgent intercept the call, decrypt the real token locally with AES-256-GCM, and inject it. The plaintext exists for one event-loop tick. The model never sees it. The provider never sees it. Your trace never sees it.

```bash
pip install llama-index-tools-enigmagent
```

In another terminal, next to your app:

```bash
npx enigmagent-mcp --mode rest --port 3737
```

That's the entire install. The Python package talks to the local EnigmAgent REST server over loopback; secrets stay in the encrypted vault on disk.

> Star [the main project](https://github.com/Agnuxo1/EnigmAgent) if you've ever pasted a token you regretted.

---

## The problem (in LlamaIndex terms)

When you build a LlamaIndex agent that needs to authenticate against a real API — GitHub, OpenAI, Stripe, your own backend — you face the same impossible choice every framework faces:

| Option | What happens |
|---|---|
| Put the secret into the prompt | It lands in the trace, in the model's context, possibly in provider logs |
| Bake the token into the tool at construction time | The model can call the tool with arbitrary inputs and exfiltrate the secret indirectly |
| Use a separate HSM / vault per tool | Works but every tool has to be rewritten |

**`llama-index-tools-enigmagent` is option D.** Your prompt, your QueryPipeline, your trace all carry only `{{PLACEHOLDER}}` strings. The real value is resolved at the boundary, by a process the model cannot see, against a vault on the user's machine.

---

## How it works

```
+------------------+  emits {{GITHUB_TOKEN}}  +---------------------+
| LlamaIndex agent | -----------------------> |  Tool input / call  |
|  (any LLM)       |                          |  (github.com / ...) |
+------------------+                          +----------+----------+
                                                         | before invoke (intercepted)
                                                         v
                                          +-------------------------+
                                          |      EnigmAgent         |
                                          |  detects placeholder,   |
                                          |  checks origin match,   |
                                          |  decrypts -> ghp_xxx    |
                                          +----------+--------------+
                                                     | real value
                                                     v
                                          +-------------------------+
                                          |  HTTP request to the    |
                                          |  upstream API           |
                                          +-------------------------+
```

The model emits a placeholder. The placeholder lives in the prompt, the pipeline, and the trace. A `BaseToolSpec` (or `FnComponent`) in your pipeline sees the placeholder right before the request leaves your process and asks the local EnigmAgent REST server to swap it for the real value — but only if the request's `origin` matches the domain that secret was bound to. Wrong domain → the resolver refuses.

---

## Three usage patterns

### 1. `EnigmAgentToolSpec` — agent-callable tools (recommended)

Expose vault operations as first-class LlamaIndex tools the agent can call:

```python
from llama_index.core.agent import ReActAgent
from llama_index.llms.openai import OpenAI
from llama_index.tools.enigmagent import EnigmAgentClient, EnigmAgentToolSpec

spec = EnigmAgentToolSpec(
    client=EnigmAgentClient(),
    default_origin="https://api.github.com",
)

agent = ReActAgent.from_tools(
    spec.to_tool_list(),
    llm=OpenAI(model="gpt-4o-mini"),
    verbose=True,
)

# The agent emits {{GITHUB_TOKEN}}. The tool resolves it at call time.
# The model NEVER sees the value.
agent.chat("Use the GitHub API with header Authorization: Bearer {{GITHUB_TOKEN}} to list my repos")
```

`EnigmAgentToolSpec` exposes three operations:

| Tool | Purpose |
|---|---|
| `resolve_placeholder(name, origin)` | Decrypt one secret |
| `substitute_placeholders(text, origin)` | Walk a string, swap every `{{NAME}}` |
| `list_placeholders(text)` | Extract placeholder names without resolving (useful for dry-runs) |

### 2. `enigma_substitute` — QueryPipeline transformer

Drop into a `QueryPipeline` so every value flowing through gets `{{PLACEHOLDER}}` resolved before the LLM stage:

```python
from llama_index.core.query_pipeline import QueryPipeline, FnComponent
from llama_index.llms.openai import OpenAI
from llama_index.tools.enigmagent import EnigmAgentClient, enigma_substitute

sub = FnComponent(
    fn=enigma_substitute(EnigmAgentClient(), origin="https://api.openai.com"),
)

pipe = QueryPipeline(chain=[sub, OpenAI(model="gpt-4o-mini")])

# {{OPENAI_KEY}} is resolved right before OpenAI is invoked.
pipe.run(input="Bearer {{OPENAI_KEY}}")
```

### 3. `enigma_secret` — drop-in `SecretStr` replacement

For LlamaIndex components that take an API key directly, resolve once at construction time:

```python
from llama_index.llms.openai import OpenAI
from llama_index.tools.enigmagent import enigma_secret

api_key = enigma_secret("OPENAI_KEY", origin="https://api.openai.com")

llm = OpenAI(api_key=api_key.get_secret_value())
```

The plaintext lives only inside the `SecretStr` and only inside the `OpenAI` instance — never in your source, never in your env, never in the prompt.

---

## Configuration

`EnigmAgentClient` defaults to `http://localhost:3737`. Override:

```python
client = EnigmAgentClient(
    base_url="http://127.0.0.1:9999",      # custom port
    timeout=5.0,                            # in seconds
    shared_secret="my-loopback-token",      # sent as X-EnigmAgent-Auth header
)
```

To run the EnigmAgent REST server with a shared secret:

```bash
npx enigmagent-mcp --mode rest --port 3737 --auth my-loopback-token
```

---

## The vault

This package is a thin client. The real work — Argon2id key derivation, AES-256-GCM encryption, origin binding, audit logging — lives in **[EnigmAgent](https://github.com/Agnuxo1/EnigmAgent)**, the npm package that backs it. To create or edit your vault, see the main README. A typical workflow:

```bash
# Create a vault interactively (one-time)
npx enigmagent-mcp --new-vault ./my.vault.json

# Add a secret bound to a domain
npx enigmagent-mcp --vault ./my.vault.json --add GITHUB_TOKEN ghp_xxx --origin https://api.github.com

# Run as REST server next to your LlamaIndex app
npx enigmagent-mcp --mode rest --port 3737 --vault ./my.vault.json
```

---

## Security model

- **Loopback only.** The REST server binds to `127.0.0.1`. Only processes on the same machine can reach it.
- **Origin binding.** Every secret is bound to one or more origins (e.g. `https://api.github.com`). Resolving a secret for a different origin is refused.
- **Argon2id + AES-256-GCM.** The vault file is encrypted at rest with a passphrase-derived key.
- **No plaintext in logs.** Resolved values exist only in the memory of the process making the upstream HTTP call, for the duration of that call.
- **Optional shared secret.** Pass `--auth` to require an `X-EnigmAgent-Auth` header on every REST call, so unauthorised local processes can't query the vault.

Full threat model: [EnigmAgent THREAT_MODEL.md](https://github.com/Agnuxo1/EnigmAgent/blob/main/docs/THREAT_MODEL.md)

---

## Compatibility

- Python: 3.10, 3.11, 3.12, 3.13
- `llama-index-core >= 0.11`
- `pydantic >= 2`
- Any LLM provider (OpenAI, Anthropic, Mistral, local), any tool

---

## Roadmap

- [ ] Auto-rewrite tool inputs in the agent middleware (currently the agent must explicitly call `substitute_placeholders` or wrap calls in `enigma_substitute`)
- [ ] Workflow event helper for the new LlamaIndex `Workflow` API
- [ ] Sister package for LlamaIndex `Settings` (vault-backed default LLM keys)
- [ ] Upstream proposal to LlamaHub once this package has real users

PRs welcome.

---

## See also

- **LangChain integration:** [`langchain-enigmagent`](https://github.com/Agnuxo1/langchain-enigmagent) — same idea, LangChain-native (`Runnable`, `Callback`)

---

## License

MIT (c) 2026 Francisco Angulo de Lafuente

## Links

- Main project: [github.com/Agnuxo1/EnigmAgent](https://github.com/Agnuxo1/EnigmAgent)
- npm package: [enigmagent-mcp](https://www.npmjs.com/package/enigmagent-mcp)
- Issues: [github.com/Agnuxo1/llama-index-tools-enigmagent/issues](https://github.com/Agnuxo1/llama-index-tools-enigmagent/issues)
