Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ uv sync
cp .env.example .env
```

`uv sync` installs the `kai-security` command (the distribution is published as
`kai-security`; the command and import package are `kai`).

Common developer commands are available through `make`:

```bash
Expand All @@ -33,6 +36,25 @@ make typecheck
make run REPO_PATH=/path/to/target
```

## Command-line interface

```bash
# Audit a repository you're authorized to test (setup → exploit pipeline)
uv run kai-security audit --repo-path /path/to/target --verbose

# Open a finished run as an interactive HTML report (findings + agent trace)
uv run kai-security view output/state/<run_id> --open

# Render a run's findings — Markdown to stdout, or a styled HTML document
uv run kai-security report output/state/<run_id>
uv run kai-security report output/state/<run_id> --format html -o report.html
```

`kai-security audit` is the friendly alias for the full pipeline; `kai-security pipeline` and
`kai-security agent` expose the complete interface documented under [Usage](#usage)
(equivalently `uv run python -m kai.main ...`). Run `kai-security <command> -h` for
per-command options.

### API keys

| Key | Required | Used by |
Expand Down
14 changes: 13 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,17 @@
requires = ["uv_build>=0.8.17,<0.9.0"]
build-backend = "uv_build"

# The published distribution is `kai-security`, but the import packages stay
# `kai` (domain) and `ra` (framework). Ship BOTH — `kai` imports `ra`, so a
# wheel with only `kai` is broken. Listing them also decouples the wheel from
# the dotted distribution name. The bare `kai` name on PyPI is reserved for the
# future umbrella dispatcher.
[tool.uv.build-backend]
module-name = ["kai", "ra"]
module-root = "src"

[project]
name = "kai"
name = "kai-security"
version = "0.1.0"
description = "Automated vulnerability discovery, verification, and patching using recursive language models."
readme = "README.md"
Expand Down Expand Up @@ -51,6 +60,9 @@ Homepage = "https://github.com/firstbatchxyz/kai-security"
Repository = "https://github.com/firstbatchxyz/kai-security"
Issues = "https://github.com/firstbatchxyz/kai-security/issues"

[project.scripts]
kai-security = "kai.cli:main"

[project.optional-dependencies]
dev = [
"pytest>=9.0.2",
Expand Down
71 changes: 71 additions & 0 deletions src/kai/cli.py
Comment thread
aktasbatuhan marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""The ``kai`` command-line entry point.

A thin dispatcher over the existing modules, giving the friendly verbs the
docs promise:

kai audit <repo> analyze a repository (setup → exploit pipeline)
kai view <run_dir> open a finished run as interactive HTML
kai report <run_dir> render a run's findings (Markdown, or --format html)

``kai pipeline`` / ``kai agent`` remain available as direct aliases into the
full :mod:`kai.main` interface. The distribution is published as
``kai-security``; the command and the import package stay ``kai``.
"""

from __future__ import annotations

import sys

_USAGE = """\
kai-security — automated vulnerability discovery, verification, and patching

usage: kai-security <command> [options]

commands:
audit <repo> Analyze a repository for vulnerabilities (setup → exploit)
view <run_dir> Open a finished run as interactive HTML (findings + trace)
report <run_dir> Render a run's findings as Markdown (default) or HTML

pipeline Full pipeline interface (kai audit is the friendly alias)
agent Run a single agent

Run `kai-security <command> -h` for command-specific options.
"""


def main(argv: list[str] | None = None) -> int:
"""Dispatch a ``kai`` subcommand. Returns a process exit code."""

argv = list(sys.argv[1:] if argv is None else argv)
if not argv or argv[0] in ("-h", "--help", "help"):
sys.stdout.write(_USAGE)
return 0

command, rest = argv[0], argv[1:]

if command in ("audit", "pipeline"):
from kai.main import main as kai_main

kai_main(["pipeline", *rest])
return 0
if command == "agent":
from kai.main import main as kai_main

kai_main(["agent", *rest])
return 0
if command == "view":
from kai.viewer.__main__ import main as view_main

return view_main(rest)
if command == "report":
from kai.report import main as report_main

return report_main(rest)

sys.stderr.write(f"kai-security: unknown command {command!r}\n\n")
sys.stdout.write(_USAGE)
return 2


if __name__ == "__main__":
raise SystemExit(main())
69 changes: 69 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Tests for the unified ``kai`` CLI dispatcher."""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from kai import cli


def _write_run(dir_path: Path) -> None:
exploits = [
{
"exploit_id": "e1", "status": "verified", "confirmed": True,
"hypothesis": "Reentrancy in withdraw drains the vault.",
"file": "Vault.sol", "function": "withdraw", "category": "active_exploit",
"severity": "critical", "cvss_score": 9.1,
}
]
(dir_path / "exploits.json").write_text(json.dumps(exploits), encoding="utf-8")


def test_help_and_no_args_print_usage(capsys: pytest.CaptureFixture[str]) -> None:
assert cli.main([]) == 0
assert "usage: kai <command>" in capsys.readouterr().out
assert cli.main(["--help"]) == 0
assert "audit" in capsys.readouterr().out


def test_unknown_command_returns_2(capsys: pytest.CaptureFixture[str]) -> None:
assert cli.main(["bogus"]) == 2
err = capsys.readouterr().err
assert "unknown command 'bogus'" in err


@pytest.mark.parametrize(
"command,expected",
[
(["audit", "/repo", "--verbose"], ["pipeline", "/repo", "--verbose"]),
(["pipeline", "--recipe", "r.json"], ["pipeline", "--recipe", "r.json"]),
(["agent", "setup", "--input", "{}"], ["agent", "setup", "--input", "{}"]),
],
)
def test_audit_pipeline_agent_delegate_to_kai_main(
command: list[str], expected: list[str], monkeypatch: pytest.MonkeyPatch
) -> None:
captured: list[list[str]] = []
monkeypatch.setattr("kai.main.main", lambda argv: captured.append(argv))
assert cli.main(command) == 0
assert captured == [expected]


def test_view_delegates_and_writes_html(tmp_path: Path) -> None:
_write_run(tmp_path)
out = tmp_path / "v.html"
assert cli.main(["view", str(tmp_path), "-o", str(out)]) == 0
assert out.exists() and out.read_text(encoding="utf-8").startswith("<!DOCTYPE html>")


def test_report_delegates(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
_write_run(tmp_path)
assert cli.main(["report", str(tmp_path)]) == 0
assert "Security findings" in capsys.readouterr().out

out = tmp_path / "r.html"
assert cli.main(["report", str(tmp_path), "--format", "html", "-o", str(out)]) == 0
assert out.exists()
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading