aegis-gateway-core 2.0.0a0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aegis_gateway_core-2.0.0a0/.gitignore +100 -0
- aegis_gateway_core-2.0.0a0/PKG-INFO +26 -0
- aegis_gateway_core-2.0.0a0/pyproject.toml +41 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/__init__.py +5 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/config/__init__.py +25 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/config/loader.py +122 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/config/models.py +163 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/errors.py +233 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/guardrails/__init__.py +8 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/guardrails/incremental.py +44 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/guardrails/protocol.py +35 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/guardrails/regex_guard.py +39 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/guardrails/spine.py +93 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/hooks/__init__.py +23 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/hooks/specs.py +75 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/mcp/__init__.py +27 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/mcp/execute_node.py +205 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/mcp/guards.py +96 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/mcp/protocol.py +55 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/mcp/tool_policy.py +34 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/pipeline/__init__.py +21 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/pipeline/assembler.py +440 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/pipeline/checkpointer.py +41 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/pipeline/executor.py +71 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/pipeline/nodes.py +31 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/pipeline/protocol.py +27 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/pipeline/state.py +64 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/pipeline/verdict.py +70 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/providers/__init__.py +28 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/providers/litellm_provider.py +192 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/providers/models.py +84 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/providers/openai_compatible.py +40 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/providers/profiles.py +169 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/providers/protocol.py +41 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/rag/__init__.py +15 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/rag/adapter.py +63 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/rag/chroma_store.py +44 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/rag/chunker.py +72 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/rag/chunking.py +36 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/rag/pgvector_store.py +41 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/rag/protocol.py +58 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/rag/retrieval_node.py +105 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/rag/stores/__init__.py +1 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/rag/stores/chroma.py +82 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/rag/stores/pgvector.py +84 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/registry/__init__.py +8 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/registry/discovery.py +121 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/registry/models.py +28 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/secrets/__init__.py +17 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/secrets/backends/__init__.py +1 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/secrets/backends/env.py +39 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/secrets/backends/keyring.py +81 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/secrets/protocol.py +27 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/secrets/ref.py +52 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/secrets/resolver.py +66 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/testing/__init__.py +6 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/testing/guardrails.py +89 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/testing/providers.py +204 -0
- aegis_gateway_core-2.0.0a0/src/aegis_core/testing/rag.py +107 -0
- aegis_gateway_core-2.0.0a0/tests/test_config.py +288 -0
- aegis_gateway_core-2.0.0a0/tests/test_guardrails.py +299 -0
- aegis_gateway_core-2.0.0a0/tests/test_hooks.py +192 -0
- aegis_gateway_core-2.0.0a0/tests/test_mcp.py +503 -0
- aegis_gateway_core-2.0.0a0/tests/test_pipeline.py +417 -0
- aegis_gateway_core-2.0.0a0/tests/test_placeholder.py +7 -0
- aegis_gateway_core-2.0.0a0/tests/test_providers.py +439 -0
- aegis_gateway_core-2.0.0a0/tests/test_rag.py +331 -0
- aegis_gateway_core-2.0.0a0/tests/test_registry.py +190 -0
- aegis_gateway_core-2.0.0a0/tests/test_secrets.py +195 -0
- aegis_gateway_core-2.0.0a0/tests/test_streaming.py +202 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Node/npm local installs
|
|
2
|
+
node_modules/
|
|
3
|
+
npm-debug.log*
|
|
4
|
+
yarn-debug.log*
|
|
5
|
+
yarn-error.log*
|
|
6
|
+
.pnpm-store/
|
|
7
|
+
|
|
8
|
+
# Local environment files
|
|
9
|
+
.env
|
|
10
|
+
.env.*
|
|
11
|
+
!.env.example
|
|
12
|
+
|
|
13
|
+
## jj
|
|
14
|
+
.jj
|
|
15
|
+
|
|
16
|
+
# Codex local configuration
|
|
17
|
+
.codex/
|
|
18
|
+
AGENTS.md
|
|
19
|
+
|
|
20
|
+
# Claude Flow runtime data
|
|
21
|
+
.claude-flow/data/
|
|
22
|
+
.claude-flow/logs/
|
|
23
|
+
CLAUDE.md
|
|
24
|
+
.claude
|
|
25
|
+
.claude-flow
|
|
26
|
+
|
|
27
|
+
# MCP
|
|
28
|
+
.mcp.json
|
|
29
|
+
ruvector.db
|
|
30
|
+
|
|
31
|
+
# ruflo agents
|
|
32
|
+
.agents
|
|
33
|
+
.swarm
|
|
34
|
+
.agentdb
|
|
35
|
+
agentdb.db
|
|
36
|
+
|
|
37
|
+
# Environment variables
|
|
38
|
+
.env
|
|
39
|
+
.env.local
|
|
40
|
+
.env.*.local
|
|
41
|
+
|
|
42
|
+
# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode
|
|
43
|
+
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode
|
|
44
|
+
|
|
45
|
+
### VisualStudioCode ###
|
|
46
|
+
.vscode/*
|
|
47
|
+
!.vscode/settings.json
|
|
48
|
+
!.vscode/tasks.json
|
|
49
|
+
!.vscode/launch.json
|
|
50
|
+
!.vscode/extensions.json
|
|
51
|
+
!.vscode/*.code-snippets
|
|
52
|
+
.vscode
|
|
53
|
+
|
|
54
|
+
# Local History for Visual Studio Code
|
|
55
|
+
.history/
|
|
56
|
+
|
|
57
|
+
# Built Visual Studio Code Extensions
|
|
58
|
+
*.vsix
|
|
59
|
+
|
|
60
|
+
### VisualStudioCode Patch ###
|
|
61
|
+
# Ignore all local history of files
|
|
62
|
+
.history
|
|
63
|
+
.ionide
|
|
64
|
+
|
|
65
|
+
### Misc files
|
|
66
|
+
gemma4
|
|
67
|
+
.postman/
|
|
68
|
+
.openclaude/
|
|
69
|
+
postman/
|
|
70
|
+
|
|
71
|
+
### OpenClaude
|
|
72
|
+
.openclaude-profile.json
|
|
73
|
+
|
|
74
|
+
# Python
|
|
75
|
+
__pycache__/
|
|
76
|
+
*.py[cod]
|
|
77
|
+
*.egg-info/
|
|
78
|
+
.pytest_cache/
|
|
79
|
+
dist/
|
|
80
|
+
build/
|
|
81
|
+
*.egg
|
|
82
|
+
.venv/
|
|
83
|
+
.uv-cache/
|
|
84
|
+
.tmp/
|
|
85
|
+
.ruff_cache/
|
|
86
|
+
|
|
87
|
+
# Docker
|
|
88
|
+
.docker/
|
|
89
|
+
|
|
90
|
+
### Build plans
|
|
91
|
+
BUILD_PLAN.md
|
|
92
|
+
CLAUDE.md
|
|
93
|
+
PROJECT_SPEC.md
|
|
94
|
+
AMEND_PLAN.md
|
|
95
|
+
SOURCE_OF_TRUTH.md
|
|
96
|
+
repo-seed/
|
|
97
|
+
.import_linter_cache/
|
|
98
|
+
site/
|
|
99
|
+
|
|
100
|
+
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aegis-gateway-core
|
|
3
|
+
Version: 2.0.0a0
|
|
4
|
+
Summary: Aegis v2 — core kernel: plugin registry, typed config, pipeline contracts.
|
|
5
|
+
Project-URL: Homepage, https://github.com/e-choness/aegis
|
|
6
|
+
Project-URL: Repository, https://github.com/e-choness/aegis
|
|
7
|
+
Project-URL: Issues, https://github.com/e-choness/aegis/issues
|
|
8
|
+
License: MIT
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Requires-Dist: keyring>=25.0
|
|
11
|
+
Requires-Dist: langchain-core>=0.3
|
|
12
|
+
Requires-Dist: langchain-mcp-adapters>=0.1.0
|
|
13
|
+
Requires-Dist: langgraph-checkpoint-postgres>=3.1.0
|
|
14
|
+
Requires-Dist: langgraph-checkpoint-sqlite>=3.1.0
|
|
15
|
+
Requires-Dist: langgraph>=0.2
|
|
16
|
+
Requires-Dist: litellm>=1.0
|
|
17
|
+
Requires-Dist: mcp>=1.0
|
|
18
|
+
Requires-Dist: pluggy>=1.4
|
|
19
|
+
Requires-Dist: pydantic-settings>=2.0
|
|
20
|
+
Requires-Dist: pydantic>=2.0
|
|
21
|
+
Requires-Dist: pyyaml>=6.0
|
|
22
|
+
Provides-Extra: rag
|
|
23
|
+
Requires-Dist: chromadb>=0.5; extra == 'rag'
|
|
24
|
+
Requires-Dist: langchain-chroma>=0.1; extra == 'rag'
|
|
25
|
+
Requires-Dist: langchain-postgres>=0.0.12; extra == 'rag'
|
|
26
|
+
Requires-Dist: langchain-text-splitters>=0.3; extra == 'rag'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "aegis-gateway-core"
|
|
3
|
+
version = "2.0.0a0"
|
|
4
|
+
description = "Aegis v2 — core kernel: plugin registry, typed config, pipeline contracts."
|
|
5
|
+
license = { text = "MIT" }
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
"pydantic>=2.0",
|
|
10
|
+
"pydantic-settings>=2.0",
|
|
11
|
+
"pluggy>=1.4",
|
|
12
|
+
"pyyaml>=6.0",
|
|
13
|
+
"keyring>=25.0",
|
|
14
|
+
"litellm>=1.0",
|
|
15
|
+
"langgraph>=0.2",
|
|
16
|
+
"langchain-core>=0.3",
|
|
17
|
+
"langgraph-checkpoint-sqlite>=3.1.0",
|
|
18
|
+
"langgraph-checkpoint-postgres>=3.1.0",
|
|
19
|
+
"mcp>=1.0",
|
|
20
|
+
"langchain-mcp-adapters>=0.1.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/e-choness/aegis"
|
|
25
|
+
Repository = "https://github.com/e-choness/aegis"
|
|
26
|
+
Issues = "https://github.com/e-choness/aegis/issues"
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
rag = [
|
|
30
|
+
"chromadb>=0.5",
|
|
31
|
+
"langchain-chroma>=0.1",
|
|
32
|
+
"langchain-postgres>=0.0.12",
|
|
33
|
+
"langchain-text-splitters>=0.3",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["hatchling"]
|
|
38
|
+
build-backend = "hatchling.build"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/aegis_core"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Aegis config subsystem — typed pydantic models and YAML loader."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from aegis_core.config.loader import load_config
|
|
6
|
+
from aegis_core.config.models import (
|
|
7
|
+
AegisConfig,
|
|
8
|
+
AuthConfig,
|
|
9
|
+
GuardrailConfig,
|
|
10
|
+
PipelineConfig,
|
|
11
|
+
ProviderConfig,
|
|
12
|
+
ResidencyConfig,
|
|
13
|
+
RouteConfig,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"AegisConfig",
|
|
18
|
+
"AuthConfig",
|
|
19
|
+
"GuardrailConfig",
|
|
20
|
+
"PipelineConfig",
|
|
21
|
+
"ProviderConfig",
|
|
22
|
+
"ResidencyConfig",
|
|
23
|
+
"RouteConfig",
|
|
24
|
+
"load_config",
|
|
25
|
+
]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Config loader — reads aegis.yaml and applies env overrides.
|
|
2
|
+
|
|
3
|
+
Loading sequence:
|
|
4
|
+
1. Parse the YAML file into a raw dict.
|
|
5
|
+
2. Walk the raw dict with ``SecretResolver`` to replace ``secret://`` URIs.
|
|
6
|
+
3. Apply pydantic-settings env overrides (``AEGIS__<SECTION>__<KEY>=...``).
|
|
7
|
+
4. Validate with ``AegisConfig``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from aegis_core.config.models import AegisConfig
|
|
17
|
+
from aegis_core.errors import AegisConfigNotFoundError, AegisConfigValidationError
|
|
18
|
+
from aegis_core.secrets.backends.env import EnvSecretProvider
|
|
19
|
+
from aegis_core.secrets.resolver import SecretResolver
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import yaml # type: ignore[import-untyped]
|
|
23
|
+
except ModuleNotFoundError as exc: # pragma: no cover
|
|
24
|
+
raise ImportError(
|
|
25
|
+
"PyYAML is required to load aegis.yaml. "
|
|
26
|
+
"Install it with: uv add pyyaml"
|
|
27
|
+
) from exc
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _build_default_resolver() -> SecretResolver:
|
|
31
|
+
resolver = SecretResolver()
|
|
32
|
+
resolver.register(EnvSecretProvider())
|
|
33
|
+
return resolver
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_config(
|
|
37
|
+
path: str | Path,
|
|
38
|
+
*,
|
|
39
|
+
resolver: SecretResolver | None = None,
|
|
40
|
+
env_prefix: str = "AEGIS",
|
|
41
|
+
) -> AegisConfig:
|
|
42
|
+
"""Load and validate an ``aegis.yaml`` file.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
path: Path to ``aegis.yaml``.
|
|
46
|
+
resolver: Optional :class:`~aegis_core.secrets.SecretResolver`. If not
|
|
47
|
+
supplied a default one (env-only) is constructed.
|
|
48
|
+
env_prefix: Prefix for environment-variable overrides. Variables of the
|
|
49
|
+
form ``AEGIS__ROUTES__DEFAULT__MODEL`` override nested config keys
|
|
50
|
+
(double-underscore as separator).
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Validated :class:`~aegis_core.config.models.AegisConfig` instance.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
AegisConfigNotFoundError: if *path* does not exist.
|
|
57
|
+
AegisConfigValidationError: if the YAML is structurally valid but fails
|
|
58
|
+
Pydantic validation.
|
|
59
|
+
"""
|
|
60
|
+
p = Path(path)
|
|
61
|
+
if not p.exists():
|
|
62
|
+
raise AegisConfigNotFoundError(
|
|
63
|
+
f"Configuration file not found: {p}",
|
|
64
|
+
path=str(p),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
with p.open("rb") as fh:
|
|
68
|
+
raw: Any = yaml.safe_load(fh)
|
|
69
|
+
|
|
70
|
+
if raw is None:
|
|
71
|
+
raw = {}
|
|
72
|
+
|
|
73
|
+
if not isinstance(raw, dict):
|
|
74
|
+
raise AegisConfigValidationError(
|
|
75
|
+
"aegis.yaml must be a YAML mapping at the top level.",
|
|
76
|
+
path=str(p),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Resolve secret:// URIs
|
|
80
|
+
if resolver is None:
|
|
81
|
+
resolver = _build_default_resolver()
|
|
82
|
+
raw = resolver.resolve_dict(raw)
|
|
83
|
+
|
|
84
|
+
# Apply env overrides (double-underscore path separator)
|
|
85
|
+
raw = _apply_env_overrides(raw, prefix=env_prefix)
|
|
86
|
+
|
|
87
|
+
# Validate
|
|
88
|
+
from pydantic import ValidationError
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
return AegisConfig.model_validate(raw)
|
|
92
|
+
except ValidationError as exc:
|
|
93
|
+
raise AegisConfigValidationError(
|
|
94
|
+
f"Configuration validation failed:\n{exc}",
|
|
95
|
+
path=str(p),
|
|
96
|
+
) from exc
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _apply_env_overrides(data: dict[str, Any], prefix: str) -> dict[str, Any]:
|
|
100
|
+
"""Overlay ``<PREFIX>__KEY1__KEY2=value`` env vars onto *data*.
|
|
101
|
+
|
|
102
|
+
Only variables that start with ``<prefix>__`` (case-insensitive) are
|
|
103
|
+
considered. Double-underscore is the nesting separator.
|
|
104
|
+
"""
|
|
105
|
+
prefix_upper = prefix.upper() + "__"
|
|
106
|
+
for key, value in os.environ.items():
|
|
107
|
+
if not key.upper().startswith(prefix_upper):
|
|
108
|
+
continue
|
|
109
|
+
remainder = key[len(prefix_upper):]
|
|
110
|
+
parts = [p.lower() for p in remainder.split("__")]
|
|
111
|
+
_set_nested(data, parts, value)
|
|
112
|
+
return data
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _set_nested(data: dict[str, Any], keys: list[str], value: str) -> None:
|
|
116
|
+
"""Set *data*[keys[0]][keys[1]]… = *value*, creating dicts as needed."""
|
|
117
|
+
node = data
|
|
118
|
+
for k in keys[:-1]:
|
|
119
|
+
if k not in node or not isinstance(node[k], dict):
|
|
120
|
+
node[k] = {}
|
|
121
|
+
node = node[k]
|
|
122
|
+
node[keys[-1]] = value
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Pydantic v2 models for aegis.yaml configuration.
|
|
2
|
+
|
|
3
|
+
All credentials are stored as ``pydantic.SecretStr`` so that they are
|
|
4
|
+
automatically redacted in ``repr``, logs, and serialized output.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field, SecretStr, field_validator, model_validator
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _StrictModel(BaseModel):
|
|
15
|
+
"""Shared base — forbids extra fields so typos surface as errors."""
|
|
16
|
+
|
|
17
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ── Residency ────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ResidencyConfig(_StrictModel):
|
|
24
|
+
region: str
|
|
25
|
+
jurisdiction: str | None = None
|
|
26
|
+
source_url: str | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ── Providers ────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ProviderConfig(_StrictModel):
|
|
33
|
+
"""A single LLM provider profile."""
|
|
34
|
+
|
|
35
|
+
type: str # e.g. "anthropic", "openai_compatible"
|
|
36
|
+
api_key: SecretStr | None = None
|
|
37
|
+
base_url: str | None = None
|
|
38
|
+
model: str | None = None
|
|
39
|
+
residency: ResidencyConfig | None = None
|
|
40
|
+
# Allow extra provider-specific fields without failing validation
|
|
41
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
42
|
+
|
|
43
|
+
@field_validator("api_key", mode="before")
|
|
44
|
+
@classmethod
|
|
45
|
+
def _coerce_secret(cls, v: Any) -> Any:
|
|
46
|
+
# secret:// URIs are resolved later by SecretResolver; pass through.
|
|
47
|
+
return v
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ── Guardrails ────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class GuardrailConfig(_StrictModel):
|
|
54
|
+
pack: str
|
|
55
|
+
mode: str | None = None
|
|
56
|
+
scanners: list[str] | None = None
|
|
57
|
+
threshold: float | None = None
|
|
58
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ── Pipeline ──────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class PipelineConfig(_StrictModel):
|
|
65
|
+
ingress: list[str] = Field(default_factory=list)
|
|
66
|
+
tool_call: list[str] = Field(default_factory=list)
|
|
67
|
+
tool_result: list[str] = Field(default_factory=list)
|
|
68
|
+
egress: list[str] = Field(default_factory=list)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ── Routes ────────────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class RouteConfig(_StrictModel):
|
|
75
|
+
provider: str
|
|
76
|
+
model: str | None = None
|
|
77
|
+
pipeline: PipelineConfig | None = None
|
|
78
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ── Auth ──────────────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class AuthConfig(_StrictModel):
|
|
85
|
+
type: Literal["none", "api_key"] = "none"
|
|
86
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ── Top-level AegisConfig ─────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class AegisConfig(_StrictModel):
|
|
93
|
+
"""Root configuration object for an Aegis gateway instance."""
|
|
94
|
+
|
|
95
|
+
providers: dict[str, ProviderConfig] = Field(default_factory=dict)
|
|
96
|
+
guardrails: dict[str, GuardrailConfig] = Field(default_factory=dict)
|
|
97
|
+
pipeline: PipelineConfig = Field(default_factory=PipelineConfig)
|
|
98
|
+
routes: dict[str, RouteConfig] = Field(default_factory=dict)
|
|
99
|
+
auth: AuthConfig = Field(default_factory=AuthConfig)
|
|
100
|
+
|
|
101
|
+
@model_validator(mode="after")
|
|
102
|
+
def _validate_route_providers(self) -> AegisConfig:
|
|
103
|
+
"""All routes must reference a declared provider."""
|
|
104
|
+
from aegis_core.errors import AegisConfigValidationError
|
|
105
|
+
|
|
106
|
+
for route_name, route in self.routes.items():
|
|
107
|
+
if route.provider not in self.providers:
|
|
108
|
+
raise AegisConfigValidationError(
|
|
109
|
+
f"Route {route_name!r} references unknown provider "
|
|
110
|
+
f"{route.provider!r}. "
|
|
111
|
+
f"Declared providers: {list(self.providers)}",
|
|
112
|
+
route=route_name,
|
|
113
|
+
provider=route.provider,
|
|
114
|
+
)
|
|
115
|
+
return self
|
|
116
|
+
|
|
117
|
+
@model_validator(mode="after")
|
|
118
|
+
def _validate_pipeline_guardrails(self) -> AegisConfig:
|
|
119
|
+
"""Pipeline node references must be declared guardrails or qualified names."""
|
|
120
|
+
from aegis_core.errors import AegisConfigValidationError
|
|
121
|
+
|
|
122
|
+
all_refs: list[str] = (
|
|
123
|
+
self.pipeline.ingress
|
|
124
|
+
+ self.pipeline.tool_call
|
|
125
|
+
+ self.pipeline.tool_result
|
|
126
|
+
+ self.pipeline.egress
|
|
127
|
+
)
|
|
128
|
+
for ref in all_refs:
|
|
129
|
+
# Strip any ".unmask" or similar suffixes for lookup
|
|
130
|
+
base = ref.split(".")[0]
|
|
131
|
+
if base not in self.guardrails:
|
|
132
|
+
raise AegisConfigValidationError(
|
|
133
|
+
f"Pipeline references unknown guardrail {ref!r}. "
|
|
134
|
+
f"Declared guardrails: {list(self.guardrails)}",
|
|
135
|
+
guardrail_ref=ref,
|
|
136
|
+
)
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
def safe_dict(self) -> dict[str, Any]:
|
|
140
|
+
"""Return a JSON-serialisable dict with all secrets redacted.
|
|
141
|
+
|
|
142
|
+
``SecretStr`` fields are replaced with ``"**REDACTED**"`` so the
|
|
143
|
+
output is safe to display in CLI output or logs.
|
|
144
|
+
"""
|
|
145
|
+
raw = self.model_dump(mode="python")
|
|
146
|
+
return _redact(raw)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
_REDACTED = "**REDACTED**"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _redact(node: Any) -> Any:
|
|
155
|
+
if isinstance(node, SecretStr):
|
|
156
|
+
return _REDACTED
|
|
157
|
+
if isinstance(node, dict):
|
|
158
|
+
return {k: _redact(v) for k, v in node.items()}
|
|
159
|
+
if isinstance(node, list):
|
|
160
|
+
return [_redact(item) for item in node]
|
|
161
|
+
return node
|
|
162
|
+
|
|
163
|
+
|