Metadata-Version: 2.4
Name: straylight-agent
Version: 0.1.0
Summary: Straylight — the open-source, self-hosted gated AI agent (the gate is the only path to tools).
License-Expression: Apache-2.0
License-File: LICENSE
Requires-Python: >=3.14
Requires-Dist: anyio>=4.14.0
Requires-Dist: fastapi>=0.115.0
Requires-Dist: fire>=0.7.1
Requires-Dist: httpx>=0.27.0
Requires-Dist: jinja2>=3.1.0
Requires-Dist: pydantic-settings>=2.4.0
Requires-Dist: pydantic>=2.8.0
Requires-Dist: rich>=15.0.0
Requires-Dist: strahl>=0.1.1
Requires-Dist: uvicorn[standard]>=0.30.0
Description-Content-Type: text/markdown

# Straylight

Straylight is an **open-source, self-hosted, gated AI agent** — a general-purpose, OpenAI-compatible
assistant (à la Claude Code / a Hermes agent) where the [Strahl Prism](../prism/api/README.md)
information-flow gate is the **only** path to tools.

You prompt it; it works **automatically** — multi-step tool use (read files, search, run shell, edit code)
to a result. The difference from any other agent: **every tool call is authorized by the gate before it
runs**, so untrusted data (a fetched web page, a file's contents, command output) can't drive a sensitive
action. Coverage is 100% by construction, and it **fails closed** — if the gate is unreachable, nothing runs.

## Quick start

Needs two things running: an OpenAI-compatible **model** (a tool-capable one — e.g. Qwen2.5 via Ollama or
vLLM) and a **Prism gate**.

```bash
uv sync

# point at your model + gate (or pass --model_url / --gate_url as flags)
export STRAYLIGHT_MODEL_URL=http://localhost:11434/v1      # Ollama's OpenAI endpoint
export STRAYLIGHT_MODEL=qwen2.5
export STRAYLIGHT_GATE_URL=http://localhost:8000
export STRAYLIGHT_API_KEY=sk_live_...                      # if your gate requires a key

uv run straylight                 # interactive REPL (bare command == `chat`)
uv run straylight run "list the python files and summarise the largest one"
uv run straylight tools           # show the toolset + which are active
```

In the REPL: streamed Markdown answers, each tool call and gate **PERMIT / DENY** shown inline. `/reset`
starts a fresh conversation, `/tools` lists tools, `/exit` quits, **Ctrl-C** stops the current turn.

## Models

straylight drives any OpenAI-compatible endpoint, so it works with the leading **open-source** models —
Qwen 2.5/3, Llama 3.1/3.3, Mistral/Mixtral, DeepSeek, Hermes, and Gemma 3. `straylight models` lists them; the
only model-specific thing that matters is the tool-call parser, which the serve scripts pick automatically.
(Gemma 3 tool calling uses vLLM's `pythonic` parser — the serve script auto-fetches its chat template — and is
less robust than Qwen/Llama; Gemma 2 has no native tool calling. Pair Gemma with `STRAYLIGHT_STREAM=false` if
its tool calls don't stream cleanly.)

```bash
uv run straylight models                              # the supported models + how to serve each

# Easiest — Ollama (OpenAI-compatible on :11434/v1, straylight's default STRAYLIGHT_MODEL_URL):
scripts/serve-ollama.sh qwen2.5:7b                    # pulls + serves; prints the env to export

# Self-hosted — vLLM (auto-selects the tool-call parser by family):
LOCAL_LLM_PORT=8020 scripts/serve-model.sh meta-llama/Llama-3.3-70B-Instruct
export STRAYLIGHT_MODEL_URL=http://localhost:8020/v1  STRAYLIGHT_MODEL=Llama-3.3-70B-Instruct
```

Hosted OSS (OpenRouter / Together / Fireworks) also works — just set `STRAYLIGHT_MODEL_URL` +
`STRAYLIGHT_API_KEY` to theirs. If a model's tool calls don't stream cleanly, set `STRAYLIGHT_STREAM=false`
(a non-streaming fallback). The agent needs a **tool-calling-capable** model — that's the only requirement.

## How it works

The loop is a generator: the model streams, proposes tool calls, the gate authorises each, permitted ones
execute, and the loop repeats until the model answers.

```
 prompt ─► model (stream) ─► proposed tool calls ─► [ Prism gate ] ─► permitted run / denied blocked ─► loop
                                                          │
                                                    strahl SDK · POST /v0/analyses
```

- **`agent.py`** — the gated loop, emitting typed `events.py` (streamed text, tool calls, verdicts, results)
  so the CLI (and, later, a `serve` mode) render the same core without touching loop logic.
- **`tools.py`** — the toolset (`web_fetch`, `read_file`, `list_files`, `glob`, `grep`, `write_file`,
  `edit_file`, `bash`, `run_python`), each carrying an **IFC label** (`requires`/`produces`) that is the
  policy the gate enforces. Filesystem/shell tools are confined to a workspace; `web_fetch` blocks non-public
  hosts (SSRF guard).
- **`config.py`** — one `pydantic-settings` object (env / `.env` / CLI flags).
- **`cli.py`** — the Rich REPL + one-shot `run`.

## The gate boundary — HTTP only

Straylight reaches the gate **only over HTTP**, through the published [`strahl`](../prism/client/python)
SDK (`POST /v0/analyses`). It depends on the SDK for the labeled-transcript types and the gate call, and
**never** imports the gate's decision code or the GPU attribution service — that stays behind the HTTP
boundary, which is what keeps the gate tamper-proof and this subtree cleanly open-source. The agent tier
holds no GPU; both GPU workloads (the model, the gate's attribution) sit behind APIs.

```
straylight ──strahl SDK──► analysis ──► attribution   (the Prism gate; GPU)
   └────────/v1/chat/completions──────► model          (the LLM; GPU)
```

## Configuration

All via env (prefix `STRAYLIGHT_`, `.env` supported) or CLI flags; legacy `STRAHL_*` names still work.

| setting | env | default |
|---|---|---|
| model endpoint | `STRAYLIGHT_MODEL_URL` | `http://localhost:11434/v1` |
| model name | `STRAYLIGHT_MODEL` | `local` |
| gate URL | `STRAYLIGHT_GATE_URL` | `http://localhost:8000` |
| gate API key | `STRAYLIGHT_API_KEY` | — |
| workspace (tool root) | `STRAYLIGHT_WORKSPACE` | cwd |
| max tool steps | `STRAYLIGHT_MAX_STEPS` | 25 |
| toolset subset | `STRAYLIGHT_ENABLED_TOOLSETS` | all (`read`,`files`,`code`,`safe` presets) |

## Development

```bash
uv run ruff format --check . && uv run ruff check . && uv run ty check . && uv run python -m pytest -q
```

Tests are hermetic — a fake gate + fake model exercise the permit/deny/fail-closed paths, the streaming
parser, the toolset, and config; no network, model, or gate required. (Use `python -m pytest`: the bare
`pytest` launcher misses the path-based editable `strahl` install.)

## Design docs

The design docs live in [`docs/`](docs/). Start with [`architecture`](docs/architecture.md) — the map: the
module tree, end-to-end data-flow traces, and the invariants that make straylight *gated*. Then the subsystems:
[`trust-model`](docs/trust-model.md) (how provenance labels are synthesized on a bare filesystem),
[`toolset`](docs/toolset.md) (the tools and the policy on each), and [`memory`](docs/memory.md) (persistent,
injection-safe local memory). A browsable site builds with `uv run mkdocs serve` (adds an auto-generated API
reference from the docstrings).

## Conventions

**uv**, Python 3.14, `src/` layout, hatchling, `ruff` (line-length 120), `pytest`. Depends on `strahl` (the SDK).
