Metadata-Version: 2.4
Name: axiomgate-lint
Version: 0.2.2
Summary: Deterministic AST security and privilege linter for CrewAI multi-agent systems
Author-email: Robin Svensson <228113985+robin-svensson@users.noreply.github.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/robin-svensson/axiomgate-lint
Project-URL: Repository, https://github.com/robin-svensson/axiomgate-lint
Project-URL: Issues, https://github.com/robin-svensson/axiomgate-lint/issues
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
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 :: Quality Assurance
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Dynamic: license-file

# AxiomGate Linter (`axiomgate-lint`)

> Someone on your team wrote `allow_delegation=True`. CrewAI ships that switch off, so turning it on was a decision. This linter finds those decisions and puts them in front of a reviewer.

[![CI](https://github.com/robin-svensson/axiomgate-lint/actions/workflows/ci.yml/badge.svg)](https://github.com/robin-svensson/axiomgate-lint/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![OWASP Top 10 for LLMs](https://img.shields.io/badge/Security-OWASP%20LLM%20Top%2010-emerald.svg)](https://owasp.org/www-project-top-10-for-large-language-model-applications/)

---

## Why

A CrewAI agent with `allow_delegation=True` can hand work to other agents, which can hand it on again. There's no depth limit and no approval step. It's one keyword, easy to add while debugging and easy to forget, and it disappears in a diff full of prompt changes.

Here's the shape it usually takes. A researcher agent delegates drafting to a writer. The writer hits a gap and delegates the missing fact back to the researcher, which delegates part of it onward again. Nothing is broken and nothing raises an error. Every hop is a fresh model call against a real budget, and the first thing anyone notices is the invoice. The second is that no human approved any step after the first one.

That's the cheap version. The expensive version is the same loop with a tool attached that writes to something.

CrewAI defaults to `allow_delegation=False` ([source](https://github.com/crewAIInc/crewAI/blob/main/lib/crewai/src/crewai/agents/agent_builder/base_agent.py)). That's a sensible default, and it's why the explicit `True` is worth finding. Nobody got it by accident. Somebody typed it, and this linter asks whether anyone reviewed that.

---

## What it does

`axiomgate-lint` reads Python with the standard library's `ast` module and reports what it finds, mapped to the [OWASP Top 10 for LLM Applications](https://owasp.org/www-project-top-10-for-large-language-model-applications/).

No dependencies. No network calls. No API keys. Nothing leaves your machine, and it never executes the code it reads, so it's safe to point at a repository you haven't looked at yet.

---

## What it catches

| Rule | Severity | OWASP LLM | Meaning |
|---|---|---|---|
| `AG-DEL-001` | HIGH | LLM06: Excessive Agency | An agent is built with an explicit `allow_delegation=True`, so it can delegate to other agents with no depth limit and no human approval |

One rule so far. It's a real one, and the tool doesn't claim more than it checks.

The rule fires only when the file imports the framework. A class of your own that happens to be called `Agent` — a sales agent, an insurance broker — is not CrewAI's, and reporting it as `LLM06` would be a false alarm about code that has nothing to do with LLMs.

---

## What it misses

A security tool that hides its blind spots is worse than no tool, because people trust it. Here are the current ones.

- **Python files only.** CrewAI's newer pattern puts agent config in `agents.yaml`. Delegation turned on there is invisible to this linter.
- **Literal values only.** It catches `allow_delegation=True` and `allow_delegation=1`. It won't catch `allow_delegation=cfg.delegate`, a value read from the environment, or anything else whose value is only known once the code runs.
- **Configuration held in a variable.** `Agent(**{"allow_delegation": True})` is read, because the dict is right there. `Agent(**config)` is not: what `config` holds is not knowable without running the code, and assuming every `**kwargs` factory delegates would flag every factory in the project.
- **Names it can follow.** It tracks delegation switched on afterwards through a variable, a second name for the same agent, an attribute of the method's own receiver (`self.researcher`), a constant index into a literal list, tuple or dict, a walrus, tuple unpacking, and `setattr(a, "allow_delegation", True)`. It does not follow a computed index (`agents[i]`), a computed attribute name, a container filled in a loop or a comprehension, or an object built in another module.
- **A visible import is required.** The rule needs `from crewai import Agent` (or `import crewai`) in the same file. A factory in that same file is followed; one in another module, a wildcard import, and dynamic construction are not. This is deliberate: without the import there is no evidence the class is the framework's, and claiming `LLM06` about someone's sales agent is worse than saying nothing.
- **CrewAI's parameter only.** `allow_delegation` is a CrewAI concept. AutoGen and OpenAI Swarm have no equivalent, so running this against their code finds nothing. That's a check that didn't apply, not a clean bill of health.
- **Source, not behaviour.** It doesn't run your agents, watch them, or prove anything about permission boundaries.

Every one of these is a place where the honest answer is "not knowable without executing the code". The rule stays quiet there on purpose. A rule that guesses is worse than no rule.

Zero findings means this rule didn't match. It doesn't mean your system is safe.

---

## Install

```bash
pip install axiomgate-lint
```

Or straight from the repository, which is the same code at whatever `main`
currently is:

```bash
pip install git+https://github.com/robin-svensson/axiomgate-lint.git
```

Or clone it:

```bash
git clone https://github.com/robin-svensson/axiomgate-lint.git
cd axiomgate-lint
pip install .
```

Python 3.10 or newer, nothing else.

### Check it works

A linter that silently does nothing looks exactly like a linter that found nothing. The repository ships two small files so you can tell them apart:

```bash
axiomgate-lint examples/vulnerable_crew.py   # one HIGH finding, exit 2
axiomgate-lint examples/safe_crew.py         # nothing,          exit 0
```

If the first one is quiet, something is wrong with the install, not with your code.

---

## Use it

```bash
axiomgate-lint .
```

Nothing matched:

```
=== AxiomGate AI Agent Linter ===
Target: /path/to/your/project
Total findings: 0
```

Something matched:

```
=== AxiomGate AI Agent Linter ===
Target: /path/to/your/project
Total findings: 1

[HIGH] AG-DEL-001 at /path/to/your/project/src/crew.py:29
  Unattenuated agent delegation enabled (allow_delegation=True). Vulnerable to excessive agency loops. (LLM06: Excessive Agency)
  -> Set allow_delegation=False or add a human-in-the-loop gate before delegation.
```

### Options

```
axiomgate-lint [target] [--fail-on critical|high|medium|low|any] [--format text|json|github] [--strict] [--version]
```

| Flag | Default | Does |
|---|---|---|
| `target` | `.` | Directory or file to scan |
| `--fail-on` | `high` | Severity at or above which the run exits 2 |
| `--format` | `text` | `text` to read, `json` for tooling, `github` for CI annotations |
| `--strict` | off | Exit `1` if any file could not be read or parsed |

### Exit codes

| Code | Meaning |
|---|---|
| `0` | Scanned, nothing at or above the threshold |
| `1` | Could not scan: the target doesn't exist, a flag was used wrongly, or `--strict` was set and a file could not be read while nothing was found |
| `2` | Scanned, found something at or above the threshold |

Code `1` earns its place. `axiomgate-lint ./scr` is a typo, and a tool that answers "0 findings, exit 0" to a path it never opened hands you a green pipeline for a scan that never ran. Scanning nothing and finding nothing are different results and only one of them should pass.

Individual files that can't be parsed or read are reported as warnings and don't change the exit code. The count also appears in the summary, so "0 findings" is never the whole story when part of the tree went unread. Pass `--strict` to make that a failure instead.

A usage error — a mistyped flag — exits `1`, not argparse's usual `2`. In a pipeline, `2` means a finding, and a typo should not look like a vulnerability.

A finding outranks an unreadable file: with `--strict`, a run that finds something still exits `2`. Downgrading a real finding to "could not scan" because an unrelated file had a syntax error would send it down a pipeline's retry branch instead of its security branch.

---

## GitHub Actions

As a step in your own workflow:

```yaml
name: AI Agent Security Lint

on: [push, pull_request]

jobs:
  lint-agents:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-python@v7
        with:
          python-version: '3.11'
      - run: pip install git+https://github.com/robin-svensson/axiomgate-lint.git
      - run: axiomgate-lint . --fail-on high --format github
```

Or use the bundled action:

```yaml
      - uses: robin-svensson/axiomgate-lint@v0.2.2
        with:
          target-path: ./src
          fail-on: high
```

With `--format github` the findings show up as annotations on the changed lines. Anything that fails the build is emitted as `::error` and the rest as `::warning`, so what the reviewer sees matches what the pipeline decided.

---

## Contributing

Issues and pull requests are welcome, especially new rules and especially evidence that an existing rule is wrong.

A new rule needs three things: a test that fails before the rule exists, a real pattern it matches, and a plain statement of what it can't see. A rule that guesses is worse than no rule.

False positives are the worst failure this tool has, and false negatives are security bugs, not feature requests — there's an issue template for each. Everyone taking part is expected to follow the [code of conduct](CODE_OF_CONDUCT.md), and vulnerabilities go through [SECURITY.md](SECURITY.md) rather than the issue tracker.

```bash
git clone https://github.com/robin-svensson/axiomgate-lint.git
cd axiomgate-lint
pip install -e ".[dev]"
pytest
```

---

## Where this comes from

`axiomgate-lint` is the open part of **AxiomGate**. The linter does one check, well and for free.

There is more to AxiomGate than the two public repositories here — a commercial auditor that maps agent topologies and capability boundaries and produces evidence-backed reports. That part is closed, so take nothing about it on this page as verified: you cannot read it, and a claim you cannot check is worth what you paid for it. What you *can* check is in this repository and in the kernel.

This linter finds unattenuated delegation in source. It cannot stop it at runtime — nothing that reads code can. That job belongs to [**AxiomGate Kernel**](https://github.com/robin-svensson/axiomgate-kernel), which enforces attenuation when the call is actually made, and writes a hash-chained audit record of the decision. It is readable and runnable, but it is source-available under PolyForm Noncommercial, not MIT like this one. Worth saying plainly rather than leaving you to find it: the two are by the same author and they are not licensed the same way.

Built by [Robin Svensson](https://github.com/robin-svensson).

## License

MIT, see [LICENSE](LICENSE).
