feat: add opt-in write access for all token-visible repos

This commit is contained in:
2026-02-14 16:35:03 +01:00
parent e22a8d37e4
commit 8504a95a11
10 changed files with 74 additions and 10 deletions

View File

@@ -43,6 +43,7 @@ POLICY_FILE_PATH=policy.yaml
# Write mode (disabled by default)
WRITE_MODE=false
WRITE_REPOSITORY_WHITELIST=
WRITE_ALLOW_ALL_TOKEN_REPOS=false
# Automation mode (disabled by default)
AUTOMATION_ENABLED=false

View File

@@ -9,7 +9,7 @@ AegisGitea-MCP exposes controlled read and optional write capabilities to AI age
- Security-first defaults (localhost bind, write mode disabled, no stack traces in production errors).
- YAML policy engine with global/per-repository tool allow/deny and optional path restrictions.
- Expanded read tools for repositories, commits, diffs, issues, PRs, labels, tags, and releases.
- Strict write mode (opt-in + repository whitelist + policy enforcement).
- Strict write mode (opt-in + policy enforcement, with whitelist by default).
- Tamper-evident audit logging with hash-chain integrity validation.
- Secret detection/sanitization for outbound payloads.
- Structured JSON logging + Prometheus metrics.
@@ -57,7 +57,7 @@ Server defaults to `127.0.0.1:8080`.
- Authorization: policy engine (`policy.yaml`) evaluated before tool execution.
- Rate limiting: per-IP and per-token.
- Output controls: bounded response size and optional secret masking/blocking.
- Write controls: `WRITE_MODE=false` by default, repository whitelist required when enabled.
- Write controls: `WRITE_MODE=false` by default; when enabled, use whitelist or opt into `WRITE_ALLOW_ALL_TOKEN_REPOS=true`.
## Documentation

View File

@@ -36,7 +36,7 @@ Startup validates:
- Required Gitea settings.
- API keys (when auth enabled).
- Insecure bind opt-in.
- Write whitelist when write mode enabled.
- Write whitelist when write mode enabled (unless `WRITE_ALLOW_ALL_TOKEN_REPOS=true`).
## Production Recommendations

View File

@@ -10,7 +10,9 @@ Aegis uses a YAML policy engine to authorize tool execution before any Gitea API
- Per-repository tool allow/deny supported.
- Optional repository path allow/deny supported.
- Write operations are denied by default.
- Write operations also require `WRITE_MODE=true` and `WRITE_REPOSITORY_WHITELIST` match.
- Write operations also require `WRITE_MODE=true` and either:
- `WRITE_REPOSITORY_WHITELIST` match, or
- `WRITE_ALLOW_ALL_TOKEN_REPOS=true`.
## Example Configuration

View File

@@ -7,7 +7,8 @@ Write mode introduces mutation risk (issue/PR changes, metadata updates). Risks
## Default Posture
- `WRITE_MODE=false` by default.
- Even when enabled, writes require repository whitelist membership.
- When enabled, writes require repository whitelist membership by default.
- Optional opt-in: `WRITE_ALLOW_ALL_TOKEN_REPOS=true` allows writes to any repo the token can access.
- Policy engine remains authoritative and may deny specific write tools.
## Supported Write Tools
@@ -24,7 +25,9 @@ Not supported (explicitly forbidden): merge actions, branch deletion, force push
## Enablement Steps
1. Set `WRITE_MODE=true`.
2. Set `WRITE_REPOSITORY_WHITELIST=owner/repo,...`.
2. Choose one:
- `WRITE_REPOSITORY_WHITELIST=owner/repo,...` (recommended)
- `WRITE_ALLOW_ALL_TOKEN_REPOS=true` (broader scope)
3. Review policy file for write-tool scope.
4. Verify audit logging and alerting before rollout.

View File

@@ -1,6 +1,6 @@
defaults:
read: allow
write: deny
write: allow
tools:
deny: []

View File

@@ -128,6 +128,13 @@ class Settings(BaseSettings):
description="Comma-separated repository whitelist for write mode (owner/repo)",
alias="WRITE_REPOSITORY_WHITELIST",
)
write_allow_all_token_repos: bool = Field(
default=False,
description=(
"Allow write-mode operations on any repository the token can access. "
"Disabled by default."
),
)
automation_enabled: bool = Field(
default=False,
description="Enable automation endpoints and workflows",
@@ -221,8 +228,11 @@ class Settings(BaseSettings):
if len(key) < 32:
raise ValueError("API keys must be at least 32 characters long")
if self.write_mode and not write_repositories:
raise ValueError("WRITE_MODE=true requires WRITE_REPOSITORY_WHITELIST to be configured")
if self.write_mode and not self.write_allow_all_token_repos and not write_repositories:
raise ValueError(
"WRITE_MODE=true requires WRITE_REPOSITORY_WHITELIST to be configured "
"unless WRITE_ALLOW_ALL_TOKEN_REPOS=true"
)
return self

View File

@@ -218,7 +218,10 @@ class PolicyEngine:
if not repository:
return PolicyDecision(False, "write operation requires a repository target")
if repository not in self.settings.write_repository_whitelist:
if (
not self.settings.write_allow_all_token_repos
and repository not in self.settings.write_repository_whitelist
):
return PolicyDecision(False, "repository is not in write-mode whitelist")
repo_policy = self.config.repositories.get(repository) if repository else None

View File

@@ -78,3 +78,31 @@ def test_settings_singleton(mock_env: None) -> None:
settings2 = get_settings()
assert settings1 is settings2
def test_write_mode_requires_whitelist_or_allow_all(monkeypatch: pytest.MonkeyPatch) -> None:
"""Write mode without whitelist must be rejected unless allow-all is enabled."""
monkeypatch.setenv("GITEA_URL", "https://gitea.example.com")
monkeypatch.setenv("GITEA_TOKEN", "test-token")
monkeypatch.setenv("MCP_API_KEYS", "a" * 64)
monkeypatch.setenv("WRITE_MODE", "true")
monkeypatch.delenv("WRITE_REPOSITORY_WHITELIST", raising=False)
monkeypatch.setenv("WRITE_ALLOW_ALL_TOKEN_REPOS", "false")
reset_settings()
with pytest.raises(ValidationError):
get_settings()
def test_write_mode_allows_all_token_repos(monkeypatch: pytest.MonkeyPatch) -> None:
"""Allow-all mode should pass validation without explicit repository whitelist."""
monkeypatch.setenv("GITEA_URL", "https://gitea.example.com")
monkeypatch.setenv("GITEA_TOKEN", "test-token")
monkeypatch.setenv("MCP_API_KEYS", "a" * 64)
monkeypatch.setenv("WRITE_MODE", "true")
monkeypatch.delenv("WRITE_REPOSITORY_WHITELIST", raising=False)
monkeypatch.setenv("WRITE_ALLOW_ALL_TOKEN_REPOS", "true")
reset_settings()
settings = get_settings()
assert settings.write_allow_all_token_repos is True

View File

@@ -125,3 +125,20 @@ def test_write_mode_repository_whitelist(monkeypatch: pytest.MonkeyPatch, tmp_pa
assert allowed.allowed
assert denied.allowed is False
def test_write_mode_allow_all_token_repos(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
"""Write mode can be configured to allow all repos accessible to token."""
_set_base_env(monkeypatch)
monkeypatch.setenv("WRITE_MODE", "true")
monkeypatch.setenv("WRITE_ALLOW_ALL_TOKEN_REPOS", "true")
policy_path = tmp_path / "policy.yaml"
policy_path.write_text("defaults:\n write: allow\n", encoding="utf-8")
reset_settings()
_ = get_settings()
engine = PolicyEngine.from_yaml_file(policy_path)
decision = engine.authorize("create_issue", is_write=True, repository="acme/any-repo")
assert decision.allowed