Skip to content
Merged
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
29 changes: 29 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,32 @@ GITPILOT_MCP_ENABLE_DURING_TEST_GENERATION=true
GITPILOT_MCP_REQUIRE_APPROVAL_FOR_MUTATIONS=true
GITPILOT_MCP_MAX_CALLS_PER_REQUEST=20
GITPILOT_MCP_TIMEOUT_SECONDS=30

# =============================================================================
# GitPilot Repair Flow (generic patch generation via OllaBridge)
# =============================================================================
# GitPilot is the ONLY component that GENERATES patches. It is GENERIC and
# provider-neutral: it calls OllaBridge (or any OpenAI-compatible endpoint)
# via OPENAI_BASE_URL + OPENAI_API_KEY. It NEVER reads HF_TOKEN.

# Caller identity (Agent-Matrix is just a default client_id, not hardcoded).
GITPILOT_CLIENT_ID=agent-matrix
GITPILOT_WORKSPACE_ID=default

# OpenAI-compatible endpoint (OllaBridge). Key format: ob_test_xxx / ob_live_xxx
OPENAI_BASE_URL=http://localhost:11434
OPENAI_API_KEY=ob_test_changeme

# Model aliases served by OllaBridge.
GITPILOT_MODEL_FAST=code-fast
GITPILOT_MODEL_CODER=code-coder
GITPILOT_MODEL_REVIEWER=code-reviewer

# Sandbox (MatrixLab) for patch validation.
GITPILOT_SANDBOX_PROVIDER=matrixlab
MATRIXLAB_URL=http://localhost:8765
MATRIXLAB_TOKEN=change-me

# Safety: draft PRs are a no-op stub in the first wave; demo mode runs offline.
GITPILOT_DRAFT_PR_ENABLED=false
GITPILOT_DEMO_MODE=true
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,7 @@ frontend/.vite/
reports/
.mcp.env
mcp-stack/

# GitPilot dry-run/CLI output artifacts
repair-response.json
repair-plan.json
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- **Coder API** (`POST /repair` + `GET /repair/health`), bearer-token gated (`GITPILOT_API_TOKEN`), mounted into the main app — turns a repair-plan into a dry-run patch preview for SelfRepair / matrix-maintainer over HTTPS.

### Changed — `make run` now starts the MCP Context Forge stack by default

**Heads-up for upgraders.** Until this release, `make run` started only the
Expand Down
95 changes: 95 additions & 0 deletions docs/contracts/repair-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# GitPilot Repair API contract

GitPilot is the **only** component that generates patches. The repair flow is
**generic** — usable by SelfRepair, Agent-Matrix, CI, or a developer — and is
provider-neutral. It calls OllaBridge (or any OpenAI-compatible endpoint) via
`OPENAI_BASE_URL` + `OPENAI_API_KEY` and **never reads `HF_TOKEN`**.

## Repair request (== SelfRepair `repair-plan.json`)

```jsonc
{
"client_id": "agent-matrix",
"workspace_id": "ws-123",
"task_id": "fix-health-test",
"repo_url": "https://github.com/acme/app",
"branch": "main",
"mode": "dry_run", // "dry_run" | "draft_pr" | "apply"
"issues": [
{
"id": "missing-health-test",
"severity": "medium", // low | medium | high | critical
"description": "No smoke test for /health",
"recommended_action": "Add tests/test_health.py"
}
],
"allowed_paths": ["tests/**"], // globs; EMPTY => fail-closed (no changes)
"forbidden_paths": [".env", "secrets/**", "**/*token*", "**/*secret*"],
"coder": { "provider": "ollabridge", "model": "code-coder" },
"sandbox": { "provider": "matrixlab", "profile": "default", "required": false },
"human_approval": false // required to proceed on risk_level=high
}
```

Pydantic model: `gitpilot.repair.schema.RepairRequest`.

## Repair response (`repair-response.json`)

```jsonc
{
"task_id": "fix-health-test",
"status": "ok", // ok | blocked | error | needs_approval
"mode": "dry_run",
"patch_preview": "--- /dev/null\n+++ b/tests/test_health.py\n...",
"changed_files": [{ "path": "tests/test_health.py", "change_type": "added" }],
"review": "Review: change is small, additive ... Risk: low.",
"sandbox_result": null, // or MatrixLab result / {skipped, reason}
"risk_level": "low", // low | medium | high
"pr_url": null,
"messages": ["Dry-run: patch preview only. No PR opened."],
"warnings": []
}
```

Pydantic model: `gitpilot.repair.schema.RepairResponse`.

## Pipeline (`gitpilot.repair.service.RepairService.run`)

1. Validate schema.
2. Clone repo into a temp workspace (stubbed in dry-run/demo or if unreachable).
3. Create local branch `gitpilot/<task_id>`.
4. Refuse forbidden paths (fail-closed).
5. `code-fast` inspects context.
6. `code-coder` generates a unified diff constrained to `allowed_paths`.
7. Apply patch locally (dry-run computes preview only).
8. `code-reviewer` reviews the diff.
9. MatrixLab `validate-patch` (skipped if not required and unreachable;
fail-closed if `required` and unreachable; blocked if validation fails).
10. Build `repair-response.json`.
11. `draft_pr` mode → safe no-op stub (no real PR in the first wave).
12. `dry_run` mode → patch preview only, never a real PR.

## Fail-closed safety (`gitpilot.repair.policy`)

A change is **blocked** when: `allowed_paths` is empty; a changed file is
outside `allowed_paths`; a changed file matches `forbidden_paths`; the patch
touches `.env` or any secret/token material; `sandbox.required=true` and
MatrixLab is unavailable; sandbox validation fails; or `risk_level=high` with
no human approval.

## Environment

`GITPILOT_CLIENT_ID`, `GITPILOT_WORKSPACE_ID`, `OPENAI_BASE_URL`,
`OPENAI_API_KEY`, `GITPILOT_MODEL_FAST` (`code-fast`),
`GITPILOT_MODEL_CODER` (`code-coder`), `GITPILOT_MODEL_REVIEWER`
(`code-reviewer`), `GITPILOT_SANDBOX_PROVIDER` (`matrixlab`), `MATRIXLAB_URL`,
`MATRIXLAB_TOKEN`, `GITPILOT_DRAFT_PR_ENABLED` (`false`),
`GITPILOT_DEMO_MODE` (`true`).

## CLI

```bash
gitpilot repair --repo <url> --plan repair-plan.json --sandbox matrixlab --dry-run
# standalone:
python -m gitpilot.repair.cli --plan repair-plan.json --dry-run --demo
```
68 changes: 68 additions & 0 deletions docs/contracts/sandbox-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Sandbox provider contract

GitPilot validates generated patches in a pluggable sandbox. Providers
implement the `SandboxProvider` ABC
(`gitpilot.sandbox_providers.base.SandboxProvider`).

> The provider package is `gitpilot.sandbox_providers` (not `gitpilot.sandbox`)
> because a `gitpilot/sandbox.py` module already exists for an unrelated
> local-sandbox feature.

## ABC

```python
class SandboxProvider(ABC):
name: str

def health(self) -> bool: ...
def run(self, **payload) -> SandboxResult: ...
def validate_patch(self, **payload) -> SandboxResult: ...
```

`health()` must **degrade gracefully** — return `False` when the sandbox is
unreachable, never raise.

## MatrixLab client

`gitpilot.sandbox_providers.matrixlab_client.MatrixLabClient` implements the
ABC against MatrixLab. Reads `MATRIXLAB_URL` (default `http://localhost:8765`)
and `MATRIXLAB_TOKEN` (sent as `Authorization: Bearer ...`).

### Endpoints

* `GET {MATRIXLAB_URL}/health`
* `POST {MATRIXLAB_URL}/repo/run`
* `POST {MATRIXLAB_URL}/repo/validate-patch`

### Request body

```jsonc
{
"client_id": "...",
"workspace_id": "...",
"repo_url": "...",
"branch": "...",
"profile": "default",
"commands": ["pytest -q"], // optional
"timeout_seconds": 600, // optional
"artifacts": ["coverage.xml"]// optional
}
```

### Response

```jsonc
{
"run_id": "...",
"status": "passed", // passed | failed | error
"exit_code": 0,
"stdout": "...",
"stderr": "...",
"duration_ms": 1234,
"artifacts": [{ "name": "coverage.xml", "url": "https://..." }]
}
```

Normalized into `gitpilot.sandbox_providers.base.SandboxResult`. A
`SandboxResult` with `skipped=True` indicates the sandbox was not run (e.g.
unreachable and not required in a dry-run).
12 changes: 12 additions & 0 deletions gitpilot/api.py → gitpilot/_api_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,18 @@ def _env_bool(name: str, default: bool) -> bool:
except Exception: # noqa: BLE001
logger.exception("MatrixLab admin API failed to mount; install modal will be disabled")

# Coder API (the generic GitPilot repair pipeline over HTTP): POST /repair +
# GET /repair/health, gated by a bearer token (GITPILOT_API_TOKEN). This is
# what SelfRepair / matrix-maintainer call to turn a repair-plan into a
# dry-run patch preview. Non-fatal mount so the UI/chat still work if it fails.
try:
from .repair_router import build_repair_router

app.include_router(build_repair_router())
logger.info("Coder API enabled (mounting /repair + /repair/health)")
except Exception: # noqa: BLE001
logger.exception("Coder API failed to mount; /repair will be unavailable")

# GitPilot-as-MCP-server (turns GitPilot into an MCP server other agents
# can drive). Off by default; mount only when GITPILOT_EXPOSE_MCP_SERVER=true.
try:
Expand Down
27 changes: 27 additions & 0 deletions gitpilot/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""GitPilot HTTP API package.

This package preserves the historical ``gitpilot.api`` module surface: the
main FastAPI application and every public symbol previously importable from
``gitpilot.api`` now live in :mod:`gitpilot._api_app` and are re-exported here
verbatim. This keeps ``from gitpilot.api import app`` (and every other prior
import) working unchanged while allowing additive submodules such as
:mod:`gitpilot.api.repair_server` to live alongside the main app.
"""

from __future__ import annotations

# Re-export the entire historical module surface. ``_api_app`` is the original
# ``gitpilot/api.py`` content moved verbatim (relative imports unchanged), so
# every symbol (``app``, ``api_list_branches``, ``EnvironmentConfig``, …) and
# submodule attribute remains importable as ``gitpilot.api.<name>``.
from gitpilot._api_app import * # noqa: F401,F403
from gitpilot import _api_app as _api_app

# ``import *`` only pulls names listed in ``__all__`` (if defined) or the
# module's public names. To guarantee 100% parity with the old flat module,
# copy across every public attribute that ``import *`` may have skipped.
for _name in dir(_api_app):
if _name.startswith("__"):
continue
globals().setdefault(_name, getattr(_api_app, _name))
del _name
99 changes: 99 additions & 0 deletions gitpilot/api/repair_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Standalone GitPilot HTTP repair API.

This is an *additive* FastAPI application (separate from the main ``gitpilot``
app) that exposes the generic GitPilot repair pipeline over HTTP so that
SelfRepair, matrix-maintainer, CI, or a developer can request a repair plan
to be turned into a (dry-run) patch preview.

Endpoints
---------
``GET /health`` -> liveness + demo-mode + version.
``POST /repair`` -> accepts a repair-plan JSON (the :class:`RepairRequest`
shape), runs the repair pipeline, and returns the
:class:`RepairResponse` as a dict.

Safety
------
* Defaults to **dry-run**: a body with no ``mode`` is treated as ``dry_run``.
* **Never opens a real PR**: ``draft_pr`` is downgraded to ``dry_run`` unless
``GITPILOT_DRAFT_PR_ENABLED=true`` (defaults false).
* Inference reaches models only via OllaBridge (``OPENAI_BASE_URL`` /
``OPENAI_API_KEY``); ``GITPILOT_DEMO_MODE=true`` produces a deterministic
offline stub patch. ``HF_TOKEN`` is never read here.
* Empty / violated ``allowed_paths`` fail closed (``status="blocked"``), never
a 500.

Run
---
``uvicorn gitpilot.api.repair_server:app``
``python -m gitpilot.api.repair_server`` (host 0.0.0.0, port ``$PORT`` or 9000)
"""

from __future__ import annotations

import os
from typing import Any

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

try: # package version is best-effort; never fatal
from gitpilot.version import __version__ as _PKG_VERSION
except Exception: # pragma: no cover - defensive
_PKG_VERSION = "0.1.0"

from gitpilot.repair.schema import RepairMode, RepairRequest
from gitpilot.repair.service import run_repair


def _truthy(value: str | None) -> bool:
return str(value or "").strip().lower() in {"1", "true", "yes", "on"}


def _demo_mode() -> bool:
return _truthy(os.getenv("GITPILOT_DEMO_MODE"))


def _draft_pr_enabled() -> bool:
"""Real draft PRs are disabled by default (fail-safe)."""
return _truthy(os.getenv("GITPILOT_DRAFT_PR_ENABLED"))


def create_app() -> FastAPI:
"""Build the GitPilot repair FastAPI application."""
app = FastAPI(
title="GitPilot Repair API",
description="Generic GitPilot repair pipeline over HTTP (dry-run safe).",
version=_PKG_VERSION or "0.1.0",
)

@app.get("/health")
def health() -> dict[str, Any]:
return {
"status": "ok",
"service": "gitpilot",
"version": _PKG_VERSION or "0.1.0",
"demo_mode": _demo_mode(),
}

# Share the bearer-gated coder router (GET /repair/health + POST /repair).
from gitpilot.repair_router import build_repair_router

app.include_router(build_repair_router())
return app


# Module-level app for ``uvicorn gitpilot.api.repair_server:app``.
app = create_app()


def main() -> None:
"""Run the repair server with uvicorn (``python -m ...``)."""
import uvicorn

port = int(os.getenv("PORT", "9000"))
uvicorn.run(app, host="0.0.0.0", port=port)


if __name__ == "__main__": # pragma: no cover - manual/CLI entrypoint
main()
Loading
Loading