Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
a9feadc
feat(openant-core,openant-cli): pluggable LLM adapter framework + mul…
ar7casper May 24, 2026
f3590b6
docs: README + CHANGELOG for multi-provider work
ar7casper May 24, 2026
8ce9756
fix(openant-core): resolve PR #69 review findings in the LLM adapter …
ar7casper Jun 8, 2026
dc18c28
fix(openant-cli): resolve PR #69 review findings in the CLI
ar7casper Jun 8, 2026
c67f653
fix(openant-core): PR #69 round-2 — Gemini tokens/empty-candidate + v…
ar7casper Jun 8, 2026
c1c65e0
fix(openant-cli): PR #69 round-2 — wizard env-key probe + redaction p…
ar7casper Jun 8, 2026
c6afdcb
fix(openant-core,openant-cli): harden agent file-read traversal + Mas…
ar7casper Jun 8, 2026
da83685
fix(openant-core): PR #69 round-3 — report model, coercion, config, e…
ar7casper Jun 10, 2026
d39ab52
fix(openant-cli): PR #69 round-3 — wizard o1-mini, report --llm-confi…
ar7casper Jun 10, 2026
232da3e
docs: PR #69 round-3 — adapter onboarding wording + contract-test count
ar7casper Jun 10, 2026
8e2e89a
fix(openant-core): PR #69 r4/r5 — harden LLM adapter error & response…
ar7casper Jun 16, 2026
5aa1743
fix(openant-core): PR #69 r4/r5 — verifier fail-safe + "unverified" v…
ar7casper Jun 16, 2026
01f6f43
fix(openant-core): PR #69 r4/r5 — prevent prompt-fence breakout in pr…
ar7casper Jun 16, 2026
7580637
fix(openant-cli): PR #69 r4 — no-echo API-key entry in wizard & set-a…
ar7casper Jun 16, 2026
dbf9a7c
chore(ci): allowlist fake key fixtures for gitleaks (PR #69)
ar7casper Jun 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# gitleaks configuration for OpenAnt.
#
# Keeps gitleaks' full default ruleset (useDefault) and adds a NARROW allowlist
# for the deliberately-fake API-key fixtures used to exercise the secret-redaction
# helper (utilities/llm/_redact.py). Those tests must feed key-shaped inputs, so
# the scanner flags them as generic-api-keys even though no real secret exists.
#
# The allowlist is intentionally tight: it only suppresses secrets that carry an
# unmistakable FAKE marker (the digit run 1234567890, or LEAKED/EXAMPLE/FAKE) AND
# live in the two redaction-test files. Any real-looking key in those files — or a
# fake marker anywhere else — is still reported. New fake fixtures must include one
# of these markers to be allowlisted.

title = "OpenAnt"

[extend]
useDefault = true

[[allowlists]]
description = "Fake key fixtures for redact_secrets() tests (marker-gated, path-scoped)"
condition = "AND"
paths = [
'''libs/openant-core/tests/test_llm_round4_fixes\.py''',
'''libs/openant-core/tests/test_llm_round5_fixes\.py''',
]
regexes = [
'''(1234567890|LEAKED|EXAMPLE|FAKE)''',
]
117 changes: 117 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,124 @@

# Changelog

All notable changes to OpenAnt are documented in this file.

## [2026-05-24] — Pluggable LLM providers (per-phase llm-configs)

### Added

- **LLM adapter plugin layer.** OpenAnt's pipeline used to hardcode
`anthropic.Anthropic` calls in 15+ files. All LLM IO now flows
through `libs/openant-core/utilities/llm/`, a Protocol-based
adapter layer with one provider plugin per file in
`utilities/llm/providers/`. Three adapters ship today —
**Anthropic** (reference), **OpenAI** (Chat Completions), and
**Google Gemini** (`google-genai` SDK) — all supporting tool
calling. Adding more (Ollama, vLLM, OpenRouter-native, etc.) is
a small Python adapter recipe — plus a few Go wizard/probe
touch-points if you want it offered by `openant setup llm`; see
`docs/features/llm-providers/HOW_TO_ADD_AN_ADAPTER.md`. The
surface is deliberately minimal — one `complete()` method, one
`validate()` method, a closed set of three content-block kinds,
a five-class error taxonomy. Closes #65.

- **Per-phase llm-configs.** `~/.config/openant/config.json` now
accepts an `llm_configs` section that maps each of the seven
pipeline phases (`analyze`, `enhance`, `verify`, `report`,
`dynamic_test`, `llm_reach`, `app_context`) to a
`{provider, model}` pair. Users pick an llm-config via
`openant scan --llm-config <name>`. The built-in `openant-default`
config (source-defined, frozen) pins today's per-phase Claude
defaults, so existing users see no behavior change: a fresh
install with no `llm_configs` resolves to `openant-default` and
runs against Anthropic with the same model IDs as before.

- **`openant setup llm` interactive wizard.** Walks the user
through creating a named llm-config without hand-editing JSON.
Per-phase per-provider model defaults (e.g. `gpt-4o` for
analyze, `gpt-4o-mini` for app_context, `gemini-1.5-pro` for
verify), known-models hint shown once per provider per session,
overwrite confirmation, and a 1-token probe per unique
(provider, model) pair before save so a typo'd key surfaces
immediately. Includes a heads-up that ChatGPT / Codex
subscriptions don't grant OpenAI API quota.

- **Eager provider validation.** When a scan starts, the registry
instantiates one adapter per unique provider in the resolved
llm-config and exposes a `validate()` method that probes each
unique `(provider, model)` pair with a 1-token call. Catches
typo'd model IDs, revoked keys, and broken endpoints before the
user starts a paid scan. Standalone step verbs (`openant analyze`,
`verify`, etc.) probe their own registry at startup too.

- **Tool-support gating at config-validation time.** Phases that
use tool calling (`enhance`, `verify`) refuse to bind to a
provider whose adapter sets `supports_tools = False`. Error
message names the phase, the offending provider, and what to do
about it — fails at registry-build time, never at first call.

- **Contract test harness.** A 12-test parametrised suite runs
against every shipped adapter (36 cases across Anthropic, OpenAI,
Google; one tool-related case skips per adapter depending on
`supports_tools`, so all three tool-capable shipped adapters
execute 11 and skip 1) pinning each one's behaviour for text
completion, tool-use round trips, and error mapping. Adding an
adapter means adding one scenario factory file and one row in
`tests/test_llm_adapter_contract.py::ADAPTERS`.

### Changed

- **`--model opus|sonnet` removed.** Both Go and Python CLIs replace
it with `--llm-config <name>` across `scan`, `analyze`, `enhance`,
`verify`, `dynamic-test`, and `report`. Backwards compatibility:
`~/.config/openant/config.json` files that only have the legacy
top-level `api_key` field auto-migrate in memory to a synthetic
`llm_providers["anthropic"]` entry, so `openant scan` keeps
working unchanged for upgrade users.

- **JSON-correction calls now inherit the parent phase's binding.**
The legacy code hardcoded Sonnet for JSON correction regardless
of the analyze phase's model. With per-phase configs this stops
generalising — correction calls now use the same provider+model
as the call whose response failed to parse. For all-Anthropic
users this is a small cost bump on Opus-phase corrections; for
non-Anthropic users it's the only correct behavior.

- **Unknown-model cost reporting is honest.** The pricing table
used to fall back to Sonnet rates for any unknown model ID,
which produced plausible-but-wrong totals on OpenRouter runs.
Unknown IDs now report `$0` with a one-time stderr warning.
Each adapter ships its own per-model pricing table; add entries
locally if you scan against a newer model the adapter doesn't
list yet.

### Fixed

- **Reporter no longer crashes on non-string response fields.**
Some non-Anthropic models return structured dicts where the
analyze prompt asked for plain strings (e.g. `attack_vector` as
a JSON object instead of a quoted attack description). The
reporter's `"\n\n".join(parts)` then raised
`TypeError: sequence item 0: expected str instance, dict found`
mid-scan. `core/reporter.py:_coerce_to_str` now defensively
serialises non-string values at every consumption site; the
analyze prompt has been tightened to require string types
explicitly.

### Removed

- **`AnthropicClient` class deleted** from
`libs/openant-core/utilities/llm_client.py`. The file remains
for `TokenTracker` (still shared across all adapter call sites)
but the LLM-wrapper class is gone — every caller now uses
`simple_text(binding, prompt, ...)` (for text-only phases) or
`binding.adapter.complete(...)` (for tool-using phases) from
`utilities.llm`.

- **`OPENANT_LLM_BASE_URL` / `OPENANT_LLM_API_KEY` /
`OPENANT_LLM_MODEL` env vars are gone** (they were never in a
release). Provider configuration lives in `config.json` only.

## [2026-05-12] — Parser depth, dependency UX, and LLM reachability (opt-in)

### Fixed
Expand Down
80 changes: 77 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,75 @@ ln -sf "$(pwd)/apps/openant-cli/bin/openant" /usr/local/bin/openant

_Note: run this from the repo root so `$(pwd)` resolves to the correct absolute path._

Set your Anthropic API key (required for analyze, verify, and scan):
### Setting up an LLM

OpenAnt routes each pipeline phase through a configurable (provider, model) pair. The fastest path is the interactive wizard:

```bash
openant setup llm
```

You name the config (e.g. `my-llm`), pick a provider per pipeline phase (`anthropic`, `openai`, or `google`), enter an API key once per provider, and the wizard probes each unique provider+model pair with a 1-token request before writing `~/.config/openant/config.json`. Run a scan against it with `--llm-config`:

```bash
openant scan /path/to/repo --llm-config my-llm
```

Wizard defaults reflect the project's per-phase recommendations (stronger reasoning models for detection / verification / reachability review; lighter models for context, report, and test generation) — override any answer to taste.

#### Shipped adapters

| Provider type | API key from | Notes |
|---|---|---|
| `anthropic` | [console.anthropic.com](https://console.anthropic.com/settings/keys) | Reference adapter. NOT included in Claude Pro / Max subscriptions — separate billing. |
| `openai` | [platform.openai.com](https://platform.openai.com/api-keys) | NOT included in ChatGPT / Codex subscriptions — separate billing. |
| `google` | [aistudio.google.com](https://aistudio.google.com/apikey) | NOT included in Gemini Advanced — separate billing. |

All three support tool calling, so any of them can drive the `enhance` and `verify` phases that use the agentic tool-use loop.

#### Quick path for Anthropic-only setups

If you want today's per-phase Claude defaults and nothing else, skip the wizard:

```bash
openant set-api-key <your-key>
openant set-api-key sk-ant-...
openant scan /path/to/repo
```

**The key must have access to the Claude Opus 4.6 model.** Get a key at [console.anthropic.com](https://console.anthropic.com/settings/keys).
This uses the built-in `openant-default` config (compiled into the binary, no `config.json` needed) — Claude Opus 4.6 for detection phases, Sonnet 4 for the rest.

#### Hand-authored config

The wizard writes `~/.config/openant/config.json` for you, but you can edit it directly too. Every llm-config must list all seven pipeline phases:

```json
{
"$schema_version": 2,
"default_llm": "my-llm",
"llm_providers": {
"anthropic": {"type": "anthropic", "api_key": "sk-ant-..."},
"openai": {"type": "openai", "api_key": "sk-proj-..."},
"google": {"type": "google", "api_key": "AIza..."}
},
"llm_configs": {
"my-llm": {
"app_context": {"provider": "openai", "model": "gpt-4o-mini"},
"llm_reach": {"provider": "anthropic", "model": "claude-opus-4-6"},
"enhance": {"provider": "openai", "model": "gpt-4o-mini"},
"analyze": {"provider": "anthropic", "model": "claude-opus-4-6"},
"verify": {"provider": "anthropic", "model": "claude-opus-4-6"},
"dynamic_test": {"provider": "google", "model": "gemini-2.0-flash"},
"report": {"provider": "google", "model": "gemini-2.0-flash"}
}
}
}
```

Providers accept a custom `base_url` for OpenAI-compatible / Anthropic-compatible proxies (OpenRouter, vLLM, Bedrock, internal gateways). The `openant-default` config (Claude across all phases) is built in and always available regardless of file contents.

#### Adding a new provider adapter

OpenAnt's adapter layer is a small Python recipe — one Python file implementing the `LLMAdapter` Protocol, one factory for the contract-test harness, plus a registry entry — and that alone is enough to run the adapter from a hand-authored config. To also have it offered by the `openant setup llm` wizard and pass its pre-save probe, add a few Go touch-points in `apps/openant-cli/cmd/setup.go` (the supported-provider list, a probe `case`, the per-phase default-model maps) plus a Go probe function. The 12 contract tests run automatically against your adapter once it's wired in. See [`docs/features/llm-providers/HOW_TO_ADD_AN_ADAPTER.md`](docs/features/llm-providers/HOW_TO_ADD_AN_ADAPTER.md) for the full recipe.

### Python runtime

Expand Down Expand Up @@ -148,6 +210,18 @@ openant project show # details of active project
openant project switch <org/repo> # switch active project
```

## Roadmap

Things on the list, in no particular order:

- **More provider adapters.** Ollama (local models), vLLM, Cohere, Mistral, Groq, Amazon Bedrock, Azure OpenAI — each is a small Python adapter recipe (plus a few Go wizard/probe touch-points if you want it offered by `openant setup llm`) per the contributor guide. Lower the barrier to local / on-prem inference.
- **Subscription-based auth.** ChatGPT / Codex, Claude Pro / Max, and Gemini Advanced subscriptions don't currently grant API quota — users have to maintain a separate API-tier key per provider. OAuth-based adapters that ride the consumer subscription would close that gap.
- **Cross-provider tool-call quirks.** All three shipped adapters support tool calling, but the long tail (parallel tool calls, strict-mode schema enforcement, retry semantics on partial JSON) behaves differently per provider. Real-world scans surface these — PRs welcome.
- **More languages.** The supported-languages list above is current coverage. Rust, Java, C#, and Swift come up frequently.
- **Hosted scan service.** Knostic offers free scans for OSS projects today via the form linked above; a self-serve API for trusted partners is a future possibility.

PRs welcome on any of these — open an issue first if the scope is non-trivial so we can align before you build.

## LICENSE

This project is licensed under Apache 2. See the LICENSE file for details.
Expand Down
8 changes: 4 additions & 4 deletions apps/openant-cli/cmd/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ var (
analyzeRepoPath string
analyzeExploitOnly bool
analyzeLimit int
analyzeModel string
analyzeLLMConfig string
analyzeWorkers int
analyzeCheckpoint string
analyzeBackoff int
Expand All @@ -45,7 +45,7 @@ func init() {
analyzeCmd.Flags().StringVar(&analyzeRepoPath, "repo-path", "", "Path to the repository (for context correction)")
analyzeCmd.Flags().BoolVar(&analyzeExploitOnly, "exploitable-only", false, "Only analyze units classified as exploitable by enhancer")
analyzeCmd.Flags().IntVar(&analyzeLimit, "limit", 0, "Max units to analyze (0 = no limit)")
analyzeCmd.Flags().StringVar(&analyzeModel, "model", "opus", "Model: opus or sonnet")
analyzeCmd.Flags().StringVar(&analyzeLLMConfig, "llm-config", "", "Name of the llm-config in ~/.config/openant/config.json (defaults to the file's default_llm, or the built-in 'openant-default' if no config file exists).")
analyzeCmd.Flags().IntVar(&analyzeWorkers, "workers", 8, "Number of parallel workers for LLM steps (default: 8)")
analyzeCmd.Flags().StringVar(&analyzeCheckpoint, "checkpoint", "", "Path to checkpoint directory for save/resume")
analyzeCmd.Flags().IntVar(&analyzeBackoff, "backoff", 30, "Seconds to wait when rate-limited (default: 30)")
Expand Down Expand Up @@ -111,8 +111,8 @@ func runAnalyze(cmd *cobra.Command, args []string) {
if analyzeLimit > 0 {
pyArgs = append(pyArgs, "--limit", fmt.Sprintf("%d", analyzeLimit))
}
if analyzeModel != "opus" {
pyArgs = append(pyArgs, "--model", analyzeModel)
if analyzeLLMConfig != "" {
pyArgs = append(pyArgs, "--llm-config", analyzeLLMConfig)
}
if analyzeWorkers != 8 {
pyArgs = append(pyArgs, "--workers", fmt.Sprintf("%d", analyzeWorkers))
Expand Down
5 changes: 5 additions & 0 deletions apps/openant-cli/cmd/dynamictest.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ If no path is given, the active project's pipeline_output.json is used.`,
var (
dynamicTestOutput string
dynamicTestMaxRetries int
dynamicTestLLMConfig string
)

func init() {
dynamicTestCmd.Flags().StringVarP(&dynamicTestOutput, "output", "o", "", "Output directory")
dynamicTestCmd.Flags().IntVar(&dynamicTestMaxRetries, "max-retries", 3, "Max retries per finding on error")
dynamicTestCmd.Flags().StringVar(&dynamicTestLLMConfig, "llm-config", "", "Name of the llm-config in ~/.config/openant/config.json (defaults to the file's default_llm, or the built-in 'openant-default' if no config file exists).")
}

func runDynamicTest(cmd *cobra.Command, args []string) {
Expand Down Expand Up @@ -85,6 +87,9 @@ func runDynamicTest(cmd *cobra.Command, args []string) {
if ctx != nil && ctx.Project != nil && ctx.RepoPath != "" {
pyArgs = append(pyArgs, "--repo-path", ctx.RepoPath)
}
if dynamicTestLLMConfig != "" {
pyArgs = append(pyArgs, "--llm-config", dynamicTestLLMConfig)
}

result, err := python.Invoke(rt.Path, pyArgs, "", quiet, requireAPIKey())
if err != nil {
Expand Down
5 changes: 5 additions & 0 deletions apps/openant-cli/cmd/enhance.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ var (
enhanceCheckpoint string
enhanceWorkers int
enhanceBackoff int
enhanceLLMConfig string
)

func init() {
Expand All @@ -42,6 +43,7 @@ func init() {
enhanceCmd.Flags().StringVar(&enhanceCheckpoint, "checkpoint", "", "Path to save/resume checkpoint (agentic mode)")
enhanceCmd.Flags().IntVar(&enhanceWorkers, "workers", 8, "Number of parallel workers for LLM steps (default: 8)")
enhanceCmd.Flags().IntVar(&enhanceBackoff, "backoff", 30, "Seconds to wait when rate-limited (default: 30)")
enhanceCmd.Flags().StringVar(&enhanceLLMConfig, "llm-config", "", "Name of the llm-config in ~/.config/openant/config.json (defaults to the file's default_llm, or the built-in 'openant-default' if no config file exists).")
}

func runEnhance(cmd *cobra.Command, args []string) {
Expand Down Expand Up @@ -104,6 +106,9 @@ func runEnhance(cmd *cobra.Command, args []string) {
if enhanceBackoff != 30 {
pyArgs = append(pyArgs, "--backoff", fmt.Sprintf("%d", enhanceBackoff))
}
if enhanceLLMConfig != "" {
pyArgs = append(pyArgs, "--llm-config", enhanceLLMConfig)
}

result, err := python.Invoke(rt.Path, pyArgs, "", quiet, requireAPIKey())
if err != nil {
Expand Down
Loading
Loading