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
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ owner.
<!-- Contributors who have signed the CLA are added here (alphabetical by GitHub
handle). To be credited, contribute a PR and sign the CLA. -->

- **ADITYA** ([@adity982](https://github.com/adity982)) — Aider executor adapter behind the Executor protocol ([#55](https://github.com/Signetry/core/pull/55), ported in [#96](https://github.com/Signetry/core/pull/96))
- **Advait Varhade** ([@AdvaitVarhade](https://github.com/AdvaitVarhade)) — SSRF (CWE-918) detection rule for JavaScript/Node ([#73](https://github.com/Signetry/core/pull/73)); SSRF URL-argument resolution for keyword + positional calls, plus httpx/aiohttp coverage ([#89](https://github.com/Signetry/core/pull/89))
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ one admission pipeline and adapted behind a single interface:
Executor (protocol)
├── CodexExecutor → codex exec (disposable checkout, no push/merge)
├── ClaudeCodeExecutor → claude -p (--bare: no CLAUDE.md auto-read, push/merge tools denied)
├── AiderExecutor → aider --message (--no-auto-commits: edits the tree, never commits)
└── <your agent> → one adapter, no pipeline change
```

Expand Down Expand Up @@ -83,10 +84,9 @@ check and nothing merges without a signed receipt. `auto_merge` is always false.
`signetry-core` also ships a **layered SAST detection engine**: a deterministic,
offline floor (Python AST taint + cross-file/interprocedural taint, plus rules and
line-based taint for Go, Java, PHP, Ruby, C#, and rules for Kotlin) covering the
OWASP set — SQL/command/
code injection, unsafe deserialization, path traversal, XSS, weak crypto, insecure
randomness, SSRF, SSTI, JWT-none, NoSQL, XXE, hardcoded secrets, TLS-off, debug
mode. Optional, non-fatal layers add Semgrep, tree-sitter AST, and advisory LLM
OWASP set — SQL/command/code injection, unsafe deserialization, path traversal,
XSS, weak crypto, insecure randomness, SSRF, SSTI, JWT-none, NoSQL, XXE,
hardcoded secrets, TLS-off, debug mode. Optional, non-fatal layers add Semgrep, tree-sitter AST, and advisory LLM
triage (which can only reduce noise — never strengthen or self-approve).

```bash
Expand Down Expand Up @@ -144,7 +144,7 @@ agent = resolve_available(["claude-code", "codex-cli"])
agent = get_executor("claude-code")

result = agent.propose("bump the vulnerable dependency", repo_path=checkout)
print(result.executor) # "claude-code" | "codex-cli" | "unavailable"
print(result.executor) # "claude-code" | "codex-cli" | "aider" | "unavailable"
print(result.diff) # recomputed from git on the final tree
print(result.model_identity) # honest provenance for the receipt
```
Expand All @@ -153,6 +153,8 @@ Enable agents via environment flags (off by default, fail-closed):

- `SIGNETRY_ENABLE_CODEX_CLI=true` (+ `codex login`)
- `SIGNETRY_ENABLE_CLAUDE_CODE=true` (+ authenticated `claude` CLI)
- `SIGNETRY_ENABLE_AIDER=true` (+ an Aider model provider; optionally
`SIGNETRY_AIDER_MODEL=<model>`)

## The admission pipeline

Expand Down
2 changes: 2 additions & 0 deletions signetry_core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""signetry-core — an agent-agnostic change-control plane for coding agents."""
from .executors.aider import AiderExecutor
from .executors.base import ExecutionResult, Executor
from .executors.claude_code import ClaudeCodeExecutor
from .executors.codex import CodexExecutor
Expand Down Expand Up @@ -72,6 +73,7 @@
"Executor",
"ExecutionResult",
"CodexExecutor",
"AiderExecutor",
"ClaudeCodeExecutor",
"NullExecutor",
"available_executors",
Expand Down
2 changes: 2 additions & 0 deletions signetry_core/executors/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .aider import AiderExecutor
from .base import ExecutionResult, Executor
from .claude_code import ClaudeCodeExecutor
from .codex import CodexExecutor
Expand All @@ -8,6 +9,7 @@
"Executor",
"ExecutionResult",
"CodexExecutor",
"AiderExecutor",
"ClaudeCodeExecutor",
"NullExecutor",
"available_executors",
Expand Down
170 changes: 170 additions & 0 deletions signetry_core/executors/aider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
"""Aider CLI executor — adapts ``aider --message`` to the Executor protocol.

Ported from @adity982's implementation in PR #55, which predated the
``umbra_core`` → ``signetry_core`` rename and so could no longer be rebased. The
adapter design (flag set, diff-derived result, prompt redaction) is theirs; this
port updates the package path, the env-var prefix, and the model allowlist.

Aider is run against a checkout with commit and push authority deliberately
withheld: ``--no-auto-commits`` / ``--no-dirty-commits`` mean Aider edits the
working tree but never creates a commit, so the change stays governable by the
admission pipeline. The result is derived from the repository diff, not from
Aider's own claim of success.
"""
from __future__ import annotations

import logging
import os
import subprocess
from datetime import UTC, datetime
from pathlib import Path
from typing import Any

from ._shared import (
Runner,
bounded_prompt,
changed_files,
reason_prompt,
sanitize_paths,
unified_diff,
)
from .base import ExecutionResult

logger = logging.getLogger("signetry.executor.aider")


class AiderExecutor:
"""Draft changes with Aider while withholding commit and push authority."""

name = "aider"

def __init__(
self,
runner: Runner = subprocess.run,
model: str | None = None,
) -> None:
self.runner = runner
configured = model if model is not None else os.getenv("SIGNETRY_AIDER_MODEL")
self.model = (configured or "").strip() or None

# --- capability ---------------------------------------------------------
def available(self) -> bool:
"""Fail closed: opt-in via env AND the CLI must actually respond."""
if os.getenv("SIGNETRY_ENABLE_AIDER", "false").lower() != "true":
return False
return self._cli_version() is not None

def _cli_version(self) -> str | None:
try:
result = self.runner(
["aider", "--version"],
text=True,
capture_output=True,
timeout=15,
check=False,
)
except (OSError, subprocess.SubprocessError):
return None
if result is None:
return None
output = (
(getattr(result, "stdout", "") or "") + (getattr(result, "stderr", "") or "")
).strip()
return output.splitlines()[0].strip() if output else None

def model_identity(self) -> dict[str, Any]:
"""Report what we know, and mark what we cannot verify as unavailable.

Aider does not echo the resolved provider model, so ``model_resolved`` is
honestly ``unavailable`` rather than being back-filled from the request.
"""
return {
"executor": self.name,
"cli_version": self._cli_version() or "unavailable",
"model_configured": self.model or "aider-default",
"model_resolved": "unavailable",
"model_evidence": "cli-argument" if self.model else "aider-default",
}

# --- execution ----------------------------------------------------------
def propose(self, prompt: str, repo_path: Path, *, read_only: bool = False) -> ExecutionResult:
if not self.available():
return ExecutionResult.disabled(
prompt,
self.name,
"Aider is disabled. Set SIGNETRY_ENABLE_AIDER=true and configure an "
"Aider model provider.",
)
if repo_path is None or not repo_path.is_dir():
raise RuntimeError("A checked-out repository is required for AiderExecutor.propose()")

cli_prompt = reason_prompt(prompt, "Aider") if read_only else bounded_prompt(prompt, "Aider")
command = [
"aider",
"--message",
cli_prompt,
"--yes-always",
# Commit authority stays with the pipeline, not the agent: Aider edits
# the working tree and never commits, so the change remains governable.
"--no-auto-commits",
"--no-dirty-commits",
"--no-gitignore",
"--no-suggest-shell-commands",
]
if read_only:
command.append("--dry-run")
if self.model:
command.extend(["--model", self.model])

# The prompt can contain the mission verbatim; keep it out of the replay
# command recorded on the receipt.
redacted_command = (
command[:2] + ["<agent prompt redacted from command replay>"] + command[3:]
)
try:
completed = self.runner(
command,
text=True,
capture_output=True,
timeout=900,
check=False,
cwd=str(repo_path),
)
except (OSError, subprocess.SubprocessError) as exc:
return ExecutionResult.failed(
prompt,
self.name,
str(exc)[:300],
command=redacted_command,
)

returncode = getattr(completed, "returncode", 1)
stdout = getattr(completed, "stdout", "") or ""
stderr = getattr(completed, "stderr", "") or ""
# Ground truth is the repository diff, not Aider's own summary.
diff = unified_diff(repo_path)
files = changed_files(repo_path)
summary = stdout.strip()
if not summary:
if returncode == 0:
summary = (
"Aider completed; see the diff below." if diff
else "Aider ran and produced no changes."
)
else:
summary = f"Aider failed (exit {returncode})."

return ExecutionResult(
prompt=prompt,
summary=sanitize_paths(summary, repo_path),
diff=diff,
tests_passed=returncode == 0,
files=files,
# Honesty rule: only claim the executor produced output when it did.
executor=self.name if returncode == 0 else "unavailable",
created_at=datetime.now(UTC).isoformat(),
command=redacted_command,
stdout=sanitize_paths(stdout[-12000:], repo_path),
error=sanitize_paths(stderr[-4000:], repo_path) or None,
model_identity=self.model_identity(),
)
2 changes: 2 additions & 0 deletions signetry_core/executors/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import subprocess
from typing import Callable

from .aider import AiderExecutor
from .base import Executor
from .claude_code import ClaudeCodeExecutor
from .codex import CodexExecutor
Expand All @@ -20,6 +21,7 @@
_REGISTRY: dict[str, Callable[[Runner], Executor]] = {
"codex-cli": lambda runner: CodexExecutor(runner=runner),
"claude-code": lambda runner: ClaudeCodeExecutor(runner=runner),
"aider": lambda runner: AiderExecutor(runner=runner),
"none": lambda runner: NullExecutor(), # govern an existing working-tree change
}

Expand Down
Loading
Loading