canon-gate 0.5.2__py3-none-any.whl

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.
canon_gate/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # This file makes 'canon_gate' a Python package.
@@ -0,0 +1 @@
1
+ # Boundary interaction layer
@@ -0,0 +1,13 @@
1
+ """
2
+ This package contains adapter implementations that are part of the boundary layer.
3
+
4
+ Adapters are responsible for:
5
+ 1. **Egress Validation:** Applying strict, deterministic checks to the parameters
6
+ of a proposed action before execution.
7
+ 2. **Execution:** Interacting with external systems (e.g., databases, APIs,
8
+ shell commands) to carry out the approved action.
9
+ """
10
+ from .mock_adapter import MockRunQueryAdapter
11
+ from .script_adapter import ScriptAdapter
12
+
13
+ __all__ = ["MockRunQueryAdapter", "ScriptAdapter"]
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING
3
+ from pydantic import BaseModel, Field, ValidationError
4
+ from canon_gate.core.adapters import BaseAdapter
5
+ from canon_gate.core.config import RUN_QUERY_FORBIDDEN_KEYWORDS
6
+ from canon_gate.core.models import (
7
+ ActionProposal,
8
+ ActionRejected,
9
+ AdapterAction,
10
+ AdapterManifest,
11
+ )
12
+
13
+ if TYPE_CHECKING:
14
+ from canon_gate.core.models import ActionProposal
15
+
16
+
17
+ class MockAdapterParameters(BaseModel):
18
+ query: str = Field(..., description="The read-only SQL query to execute.")
19
+
20
+
21
+ class MockRunQueryAdapter(BaseAdapter):
22
+ """
23
+ A mock adapter for demonstration purposes. It handles the 'run_query' action
24
+ and performs its own egress validation on the SQL parameters before execution.
25
+ """
26
+
27
+ def get_manifest(self) -> AdapterManifest:
28
+ """Returns the manifest describing the adapter's capabilities."""
29
+ return AdapterManifest(
30
+ adapter_name="run_query_adapter",
31
+ actions=[
32
+ AdapterAction.from_pydantic_model(
33
+ name="run_query",
34
+ description="Runs a read-only SQL query against a mock database.",
35
+ model=MockAdapterParameters,
36
+ )
37
+ ],
38
+ )
39
+
40
+ def _validate_egress(self, query: str) -> list[str]:
41
+ """
42
+ Performs a final validation check on the SQL query itself.
43
+ This is the Egress Validation step.
44
+ """
45
+ errors = []
46
+ sql_lower = query.lower()
47
+ for keyword in RUN_QUERY_FORBIDDEN_KEYWORDS:
48
+ if keyword in sql_lower:
49
+ errors.append(f"SQL query contains forbidden keyword: '{keyword}'")
50
+ return errors
51
+
52
+ def execute(self, proposal: "ActionProposal") -> dict:
53
+ """
54
+ Validates the egress parameters and then simulates executing a SQL query.
55
+ """
56
+ try:
57
+ params = MockAdapterParameters.model_validate(proposal.parameters)
58
+ except ValidationError as e:
59
+ raise ActionRejected(f"Egress validation failed: Invalid parameters: {e}")
60
+
61
+ # Perform Egress Validation
62
+ egress_errors = self._validate_egress(params.query)
63
+ if egress_errors:
64
+ raise ActionRejected(f"Egress validation failed: {', '.join(egress_errors)}")
65
+
66
+ print("---")
67
+ print(f"[Mock Adapter] INFO: Egress validation passed for action 'run_query'.")
68
+ print(f"[Mock Adapter] INFO: Would execute query against production database: {params.query}")
69
+ print("---")
70
+
71
+ return {
72
+ "status": "success",
73
+ "message": "Query was mock-executed successfully after passing egress validation.",
74
+ "query_sent_to_db": params.query,
75
+ "egress_checks_passed": True,
76
+ }
@@ -0,0 +1,103 @@
1
+ # canon_gate/boundary/adapters/script_adapter.py
2
+ import subprocess
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import Any, Dict
6
+
7
+ from pydantic import BaseModel, Field, ValidationError
8
+
9
+ from canon_gate.core.adapters import BaseAdapter
10
+ from canon_gate.core.config import SCRIPT_ADAPTER_ALLOWED_SCRIPTS
11
+ from canon_gate.core.models import (
12
+ ActionProposal,
13
+ ActionRejected,
14
+ AdapterAction,
15
+ AdapterManifest,
16
+ )
17
+
18
+ # The base path to the directory where the scripts are stored
19
+ SCRIPT_DIRECTORY = Path(__file__).parent.parent.parent.parent / "scripts"
20
+
21
+
22
+ # 1. Define the parameter schema for the adapter's action
23
+ class ScriptAdapterParameters(BaseModel):
24
+ script_name: str = Field(
25
+ ..., description="The name of the script to execute (without the .py extension)."
26
+ )
27
+ script_args: list[str] = Field(
28
+ default=[], description="A list of arguments to pass to the script."
29
+ )
30
+
31
+
32
+ class ScriptAdapter(BaseAdapter):
33
+ """
34
+ An adapter for executing predefined python scripts from the /scripts directory.
35
+ """
36
+
37
+ # 2. Implement get_manifest to make the adapter self-describing
38
+ def get_manifest(self) -> AdapterManifest:
39
+ """Returns the manifest describing the adapter's capabilities."""
40
+ return AdapterManifest(
41
+ adapter_name="execute_script_adapter",
42
+ actions=[
43
+ AdapterAction.from_pydantic_model(
44
+ name="execute_script",
45
+ description="Executes a predefined system script.",
46
+ model=ScriptAdapterParameters,
47
+ )
48
+ ],
49
+ )
50
+
51
+ # 3. Refactor execute to use the parameter schema
52
+ def execute(self, proposal: ActionProposal) -> Dict[str, Any]:
53
+ """
54
+ Executes a script after performing Egress validation.
55
+ """
56
+ try:
57
+ params = ScriptAdapterParameters.model_validate(proposal.parameters)
58
+ except ValidationError as e:
59
+ raise ActionRejected(f"Egress validation failed: Invalid parameters: {e}")
60
+
61
+ # Egress Validation: Policy check
62
+ if params.script_name not in SCRIPT_ADAPTER_ALLOWED_SCRIPTS:
63
+ raise ActionRejected(
64
+ f"Egress validation failed: Script '{params.script_name}' is not in the list of allowed scripts."
65
+ )
66
+
67
+ script_path = SCRIPT_DIRECTORY / f"{params.script_name}.py"
68
+
69
+ # Egress Validation: Existence check
70
+ if not script_path.exists():
71
+ raise ActionRejected(
72
+ f"Egress validation failed: Script '{params.script_name}.py' not found at '{script_path}'."
73
+ )
74
+
75
+ try:
76
+ # Execution
77
+ command = [sys.executable, str(script_path)] + params.script_args
78
+ completed_process = subprocess.run(
79
+ command,
80
+ capture_output=True,
81
+ text=True,
82
+ check=False,
83
+ )
84
+
85
+ stdout = completed_process.stdout.strip()
86
+ stderr = completed_process.stderr.strip()
87
+ exit_code = completed_process.returncode
88
+
89
+ if exit_code == 0:
90
+ return {
91
+ "status": "succeeded",
92
+ "message": stdout or "Script executed successfully with no output.",
93
+ "output": {"stdout": stdout, "stderr": stderr, "exit_code": exit_code},
94
+ }
95
+ else:
96
+ return {
97
+ "status": "failed",
98
+ "message": stderr or stdout or f"Script failed with exit code {exit_code}.",
99
+ "output": {"stdout": stdout, "stderr": stderr, "exit_code": exit_code},
100
+ }
101
+
102
+ except Exception as e:
103
+ raise ActionRejected(f"Execution failed: An unexpected error occurred: {str(e)}")
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from canon_gate.core.models import GateDecision
9
+
10
+ # This basic configuration sends structured JSON logs to stdout.
11
+ # In a real application, you would configure handlers to route this
12
+ # to a dedicated logging service or file.
13
+ logging.basicConfig(level=logging.INFO, format='%(message)s')
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def log_denial(decision: "GateDecision"):
18
+ """
19
+ Logs a denied GateDecision in a structured JSON format.
20
+
21
+ This creates a critical audit trail for security and review purposes.
22
+ """
23
+ if decision.allowed:
24
+ # This function is specifically for logging denials.
25
+ return
26
+
27
+ log_data = {
28
+ "event": "action_denied",
29
+ "decision_id": decision.decision_id,
30
+ "timestamp": decision.timestamp.isoformat(),
31
+ "source": decision.source,
32
+ "action": decision.action,
33
+ "allowed": decision.allowed,
34
+ "errors": decision.errors,
35
+ "failed_checks": decision.failed_checks,
36
+ }
37
+
38
+ # The use of json.dumps creates a structured, machine-readable log entry.
39
+ logger.info(json.dumps(log_data))
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import wraps
4
+ from typing import Any, Callable
5
+
6
+ from canon_gate.core.models import ActionProposal, GateDecision
7
+ from canon_gate.core.policy import validate_agent_output, ValidationResult
8
+
9
+
10
+ def protected_by_canon_gate(action_type: str):
11
+ """
12
+ A decorator to protect a function with Canon Gate's validation.
13
+ """
14
+ def decorator(func: Callable[..., Any]):
15
+ @wraps(func)
16
+ def wrapper(*args, **kwargs):
17
+ # 1. Construct the generic ActionProposal from function arguments
18
+ proposal = ActionProposal(
19
+ action=action_type,
20
+ reason=f"Automated call to protected function '{func.__name__}'",
21
+ parameters={
22
+ "function_name": func.__name__,
23
+ "function_args": kwargs,
24
+ # Note: args are ignored in this simple version
25
+ },
26
+ )
27
+
28
+ # 2. Validate the proposal
29
+ validation_result = validate_agent_output(proposal)
30
+
31
+ # 3. If allowed, execute the original function
32
+ if validation_result.allowed:
33
+ print(f"Canon Gate validated '{proposal.action}'. Executing function.")
34
+ return func(*args, **kwargs)
35
+ else:
36
+ # If not allowed, block execution and return a GateDecision
37
+ print(f"Canon Gate BLOCKED '{proposal.action}'. Reason: {validation_result.errors}")
38
+
39
+ decision = GateDecision(
40
+ source="decorator",
41
+ action=validation_result.action,
42
+ allowed=validation_result.allowed,
43
+ errors=validation_result.errors,
44
+ passed_checks=validation_result.passed_checks,
45
+ failed_checks=validation_result.failed_checks,
46
+ execution=None,
47
+ )
48
+ return decision
49
+ return wrapper
50
+ return decorator
@@ -0,0 +1,162 @@
1
+ import logging
2
+ import os
3
+ from contextlib import asynccontextmanager
4
+ from datetime import datetime, timezone
5
+ from pathlib import Path
6
+ from typing import Annotated, Dict
7
+ import json
8
+ from importlib import metadata, resources
9
+ import toml
10
+
11
+ from fastapi import Depends, FastAPI, Request, status
12
+
13
+ from fastapi.responses import JSONResponse, HTMLResponse
14
+
15
+ # --- Core Imports ---
16
+ from canon_gate.core.policy import validate_agent_output
17
+ from canon_gate.core.models import ActionProposal, GateDecision, Manifest
18
+ from canon_gate.core.adapters import BaseAdapter
19
+ from canon_gate.core.orchestration import run_execute_flow
20
+ from canon_gate.core.manifest_generator import generate_manifest
21
+
22
+ # --- Boundary Imports ---
23
+ from .audit import log_denial
24
+ from .audit import logger as audit_logger
25
+
26
+ from .adapters import MockRunQueryAdapter, ScriptAdapter
27
+ from operator_console.main import router as console_router, SESSIONS_DIR
28
+
29
+ from contextlib import asynccontextmanager
30
+
31
+ # --- Adapter Registry (Boundary-level concern) ---
32
+
33
+ def register_adapters() -> Dict[str, BaseAdapter]:
34
+ """Initializes and registers all available adapters."""
35
+ registry = {}
36
+ adapters_to_register = [MockRunQueryAdapter(), ScriptAdapter()]
37
+
38
+ for adapter in adapters_to_register:
39
+ manifest = adapter.get_manifest()
40
+ for action in manifest.actions:
41
+ if action.name in registry:
42
+ logging.warning(
43
+ f"Action '{action.name}' is already registered. Overwriting with adapter '{manifest.adapter_name}'."
44
+ )
45
+ registry[action.name] = adapter
46
+ logging.info(
47
+ f"Registered action '{action.name}' to adapter '{manifest.adapter_name}'"
48
+ )
49
+ return registry
50
+
51
+ # --- FastAPI Application Factory ---
52
+
53
+ def get_version() -> str:
54
+ """Reads the package version from metadata, with fallbacks for development."""
55
+ try:
56
+ # Primary method: For installed packages
57
+ return metadata.version("canon-gate")
58
+ except metadata.PackageNotFoundError:
59
+ # Fallback for local development: read from pyproject.toml
60
+ try:
61
+ pyproject_path = Path(__file__).parent.parent.parent / "pyproject.toml"
62
+ with open(pyproject_path, "r") as f:
63
+ data = toml.load(f)
64
+ return data["project"]["version"]
65
+ except (FileNotFoundError, KeyError):
66
+ # Final fallback if all else fails
67
+ return "0.0.0-dev"
68
+
69
+ _VERSION = get_version()
70
+
71
+ def get_adapter_registry(request: Request) -> Dict[str, BaseAdapter]:
72
+ """Dependency to get the adapter registry from the app state."""
73
+ return request.app.state.adapter_registry
74
+
75
+ def create_app(env: str | None = None) -> FastAPI:
76
+ @asynccontextmanager
77
+ async def lifespan(app: FastAPI):
78
+ # Create the adapter registry on startup and store it in the app state
79
+ app.state.adapter_registry = register_adapters()
80
+
81
+ # Ensure the directory for operator sessions exists in the user's app data dir
82
+ SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
83
+
84
+ environment = os.getenv("CANON_GATE_ENV", "production")
85
+ startup_log_data = {
86
+ "event": "service_startup",
87
+ "timestamp": datetime.now(timezone.utc).isoformat(),
88
+ "version": _VERSION,
89
+ "environment": environment,
90
+ "message": "Canon Gate service started.",
91
+ }
92
+ audit_logger.info(json.dumps(startup_log_data))
93
+ yield
94
+
95
+ app = FastAPI(title="Canon Gate Framework", version=_VERSION, lifespan=lifespan)
96
+ app.include_router(console_router)
97
+ actual_env = env or os.getenv("CANON_GATE_ENV", "production")
98
+
99
+ @app.exception_handler(Exception)
100
+ async def unhandled_exception_handler(request: Request, exc: Exception):
101
+ error_log_data = {"event": "unhandled_exception", "message": str(exc)}
102
+ audit_logger.error(json.dumps(error_log_data))
103
+ return JSONResponse(status_code=500, content=error_log_data)
104
+
105
+ @app.get("/")
106
+ def root() -> dict:
107
+ endpoints = {
108
+ "message": "Canon Gate Framework operational.",
109
+ "endpoints": ["/validate_output", "/execute", "/manifest"],
110
+ }
111
+ if actual_env == "development":
112
+ endpoints["message"] = "Canon Gate Framework running (Development Mode)"
113
+ endpoints["operator_console"] = "/console"
114
+ return endpoints
115
+
116
+ @app.get("/console", response_class=HTMLResponse)
117
+ def serve_console():
118
+ """Serves the Operator Console main page from package resources."""
119
+ try:
120
+ content = resources.files('operator_console.static').joinpath('console.html').read_text()
121
+ return HTMLResponse(content)
122
+ except (ModuleNotFoundError, FileNotFoundError):
123
+ return HTMLResponse("<h1>Error: Operator Console not found.</h1>", status_code=500)
124
+
125
+
126
+ @app.get("/manifest", response_model=Manifest, tags=["Runtime Discovery"])
127
+ def get_manifest(
128
+ adapter_registry: Annotated[Dict, Depends(get_adapter_registry)]
129
+ ) -> Manifest:
130
+ """
131
+ Provides a discoverable, machine-readable overview of the actions
132
+ and parameters governed by this Canon Gate instance.
133
+ """
134
+ return generate_manifest(adapter_registry)
135
+
136
+ @app.post("/validate_output", response_model=GateDecision)
137
+ def validate_output_endpoint(proposal: ActionProposal) -> GateDecision:
138
+ result = validate_agent_output(proposal)
139
+ decision = GateDecision(
140
+ source="validate",
141
+ action=result.action,
142
+ proposal=proposal,
143
+ allowed=result.allowed,
144
+ errors=result.errors,
145
+ passed_checks=result.passed_checks,
146
+ failed_checks=result.failed_checks,
147
+ execution=None,
148
+ )
149
+ if not decision.allowed:
150
+ log_denial(decision)
151
+ return decision
152
+
153
+ @app.post("/execute", response_model=GateDecision)
154
+ def execute_endpoint(
155
+ proposal: ActionProposal,
156
+ adapter_registry: Annotated[Dict, Depends(get_adapter_registry)],
157
+ ) -> GateDecision:
158
+ return run_execute_flow(proposal, adapter_registry)
159
+
160
+ return app
161
+
162
+ app = create_app()
canon_gate/cli.py ADDED
@@ -0,0 +1,85 @@
1
+ import uvicorn
2
+ import typer
3
+ from typing_extensions import Annotated
4
+
5
+ app = typer.Typer()
6
+
7
+ @app.command()
8
+ def serve(
9
+ host: Annotated[str, typer.Option(help="The host to bind the server to.")] = "127.0.0.1",
10
+ port: Annotated[int, typer.Option(help="The port to run the server on.")] = 8000,
11
+ console: Annotated[bool, typer.Option("--console/--no-console", help="Launch the Operator Console in a browser.")] = True,
12
+ ):
13
+ """
14
+ Starts the Canon Gate server and optionally launches the console.
15
+ """
16
+ url = f"http://{host}:{port}"
17
+ print(f"Canon Gate server starting on {url}")
18
+
19
+ if console:
20
+ print(f"Launching Operator Console in your browser: {url}/console")
21
+ typer.launch(f"{url}/console")
22
+
23
+ from canon_gate.boundary.main import app as fastapi_app
24
+ uvicorn.run(fastapi_app, host=host, port=port)
25
+
26
+ @app.command()
27
+ def demo():
28
+ """
29
+ Runs the full, orchestrated reference demonstration.
30
+ """
31
+ from canon_gate.demo import run_full_demo
32
+ run_full_demo()
33
+
34
+ @app.command()
35
+ def init(
36
+ force: Annotated[bool, typer.Option("--force", "-f", help="Overwrite existing policy.toml and adapter_policy.toml files if they exist.")] = False
37
+ ):
38
+ """
39
+ Initializes a new Canon Gate project with example policy templates.
40
+ """
41
+ from importlib import resources
42
+ from pathlib import Path
43
+
44
+ policy_dest = Path.cwd() / "policy.toml"
45
+ adapter_dest = Path.cwd() / "adapter_policy.toml"
46
+
47
+ # 1. Checks if files exist
48
+ if not force:
49
+ exists_files = []
50
+ if policy_dest.exists():
51
+ exists_files.append("policy.toml")
52
+ if adapter_dest.exists():
53
+ exists_files.append("adapter_policy.toml")
54
+
55
+ if exists_files:
56
+ print(f"❌ ERROR: Project initialization aborted.")
57
+ print(f"The following configuration files already exist: {', '.join(exists_files)}")
58
+ print("Use --force (or -f) to overwrite them.")
59
+ raise typer.Exit(code=1)
60
+
61
+ # 2. Load templates from package resources
62
+ try:
63
+ policy_content = resources.files("canon_gate.resources").joinpath("policy.example.toml").read_text(encoding="utf-8")
64
+ adapter_content = resources.files("canon_gate.resources").joinpath("adapter_policy.example.toml").read_text(encoding="utf-8")
65
+ except Exception as e:
66
+ print(f"❌ ERROR: Failed to load packaged templates: {e}")
67
+ raise typer.Exit(code=1)
68
+
69
+ # 3. Write templates to destination
70
+ try:
71
+ policy_dest.write_text(policy_content, encoding="utf-8")
72
+ adapter_dest.write_text(adapter_content, encoding="utf-8")
73
+ except Exception as e:
74
+ print(f"❌ ERROR: Failed to write policy files to current directory: {e}")
75
+ raise typer.Exit(code=1)
76
+
77
+ print("canon-gate init\n")
78
+ print("↓\n")
79
+ print("Initialized Canon Gate project.")
80
+ print("Created:")
81
+ print(" policy.toml")
82
+ print(" adapter_policy.toml")
83
+
84
+ if __name__ == "__main__":
85
+ app()
@@ -0,0 +1 @@
1
+ # Core validation engine
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+ from abc import ABC, abstractmethod
3
+ from typing import TYPE_CHECKING, Any, Dict
4
+
5
+ if TYPE_CHECKING:
6
+ from .models import ActionProposal, AdapterManifest
7
+
8
+
9
+ class BaseAdapter(ABC):
10
+ """
11
+ The abstract base class for all execution adapters.
12
+
13
+ Each adapter is responsible for handling the execution logic for a single,
14
+ specific action type. It is also responsible for describing its own
15
+ capabilities for runtime discovery via a manifest.
16
+ """
17
+
18
+ @abstractmethod
19
+ def get_manifest(self) -> "AdapterManifest":
20
+ """
21
+ Returns a manifest describing the adapter's capabilities, including
22
+ the actions it supports and their parameter schemas.
23
+ """
24
+ pass
25
+
26
+ @abstractmethod
27
+ def execute(self, proposal: "ActionProposal") -> Dict[str, Any]:
28
+ """
29
+ Executes the validated action.
30
+
31
+ Args:
32
+ proposal: The validated ActionProposal object.
33
+
34
+ Returns:
35
+ A dictionary containing the results of the execution, which will
36
+ be embedded in the final GateDecision. Must include at least
37
+ a "status" and "message" key.
38
+ """
39
+ pass
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ import toml
4
+ from pydantic import BaseModel, Field
5
+ from typing import List, Set
6
+ import logging
7
+ from importlib import resources
8
+ from pathlib import Path
9
+
10
+ # --- Ingress Policy Models ---
11
+
12
+ class ActionsConfig(BaseModel):
13
+ allowed: Set[str] = Field(default_factory=set)
14
+
15
+ class PolicyConfig(BaseModel):
16
+ actions: ActionsConfig = ActionsConfig()
17
+
18
+ # --- Egress Policy Models ---
19
+
20
+ class RunQueryAdapterPolicy(BaseModel):
21
+ forbidden_keywords: Set[str] = Field(default_factory=set)
22
+
23
+ class ScriptAdapterPolicy(BaseModel):
24
+ allowed_scripts: Set[str] = Field(default_factory=set)
25
+
26
+ class AdapterPolicyConfig(BaseModel):
27
+ run_query_adapter: RunQueryAdapterPolicy = RunQueryAdapterPolicy()
28
+ execute_script_adapter: ScriptAdapterPolicy = ScriptAdapterPolicy()
29
+
30
+ # --- Settings Loading ---
31
+
32
+ def _load_local_policy(filename: str) -> dict:
33
+ """
34
+ Loads a policy file from the current working directory.
35
+ Does NOT fall back to package-bundled files to maintain
36
+ strict Default-Deny and preserve explicit operator ownership.
37
+ """
38
+ policy_path = Path.cwd() / filename
39
+ try:
40
+ if policy_path.is_file():
41
+ with open(policy_path, "r", encoding="utf-8") as f:
42
+ return toml.load(f)
43
+ except Exception as e:
44
+ logging.error(f"Error reading local policy file '{filename}': {e}")
45
+ return {}
46
+
47
+ # Load Ingress Policy (policy.toml)
48
+ _policy_config = PolicyConfig()
49
+ try:
50
+ if not (Path.cwd() / "policy.toml").is_file():
51
+ warning_msg = (
52
+ "\n"
53
+ "===============================================================================\n"
54
+ " No local policy.toml was found.\n\n"
55
+ " Canon Gate is operating in Default-Deny mode.\n"
56
+ " No actions will be permitted until a project policy is supplied.\n\n"
57
+ " To create starter policy files run:\n"
58
+ " canon-gate init\n"
59
+ "==============================================================================="
60
+ )
61
+ print(warning_msg)
62
+ logging.warning("No local policy.toml was found. Canon Gate is operating in Default-Deny mode.")
63
+ else:
64
+ policy_data = _load_local_policy("policy.toml")
65
+ if policy_data:
66
+ _policy_config = PolicyConfig.model_validate(policy_data)
67
+ except Exception as e:
68
+ logging.error(f"Failed to load or parse policy.toml: {e}")
69
+ _policy_config = PolicyConfig()
70
+
71
+ # Load Egress Policy (adapter_policy.toml)
72
+ _adapter_policy_config = AdapterPolicyConfig()
73
+ try:
74
+ if (Path.cwd() / "adapter_policy.toml").is_file():
75
+ adapter_policy_data = _load_local_policy("adapter_policy.toml")
76
+ if adapter_policy_data:
77
+ _adapter_policy_config = AdapterPolicyConfig.model_validate(adapter_policy_data)
78
+ except Exception as e:
79
+ logging.error(f"Failed to load or parse adapter_policy.toml: {e}")
80
+ _adapter_policy_config = AdapterPolicyConfig()
81
+
82
+ # --- Publicly Exposed Settings ---
83
+
84
+ # Ingress Settings
85
+ ALLOWED_ACTIONS: Set[str] = _policy_config.actions.allowed
86
+
87
+ # Egress Settings
88
+ RUN_QUERY_FORBIDDEN_KEYWORDS: Set[str] = _adapter_policy_config.run_query_adapter.forbidden_keywords
89
+ SCRIPT_ADAPTER_ALLOWED_SCRIPTS: Set[str] = _adapter_policy_config.execute_script_adapter.allowed_scripts