33session-start fetches the tenant's resolved guardrail config from the platform and writes it
44here; scans only read. Per-agent modes and sensitive-path globs are platform-owned - local
55policy files never carry them. An absent or corrupt cache means built-in defaults (Report
6- everywhere + the default globs), always synchronous.
6+ everywhere + the default globs, with the unauthorized MCP server guardrail Off ), always synchronous.
77"""
88
99import json
2323
2424GUARDRAILS_CONFIG_FILE_NAME = 'ai-guardrails-config.json'
2525
26- _DEFAULT_TTL_SECONDS = 900
26+ DEFAULT_TTL_SECONDS = 900
2727
2828# Guardrail keys are the CLI's block-reason vocabulary. Anything else in the payload (a future
2929# guardrail this CLI doesn't implement) is ignored - unknown config must never fail closed.
3434 BlockReason .SECRETS_IN_FILE ,
3535 BlockReason .SENSITIVE_PATH ,
3636 BlockReason .SECRETS_IN_MCP_ARGS ,
37+ BlockReason .UNAUTHORIZED_MCP_SERVER ,
3738 )
3839)
3940
41+ # Off by default (not Report), so they never start enforcing on a tenant that hasn't configured them.
42+ _DEFAULT_OFF_GUARDRAIL_KEYS = frozenset ((BlockReason .UNAUTHORIZED_MCP_SERVER .value ,))
43+
4044
4145def get_config_cache_path () -> Path :
4246 return Path .home () / CYCODE_CONFIGURATION_DIRECTORY / GUARDRAILS_CONFIG_FILE_NAME
4347
4448
49+ def default_mode_for (guardrail_key : str ) -> str :
50+ if guardrail_key in _DEFAULT_OFF_GUARDRAIL_KEYS :
51+ return GuardrailCellMode .OFF .value
52+ return GuardrailCellMode .REPORT .value
53+
54+
4555def _default_sensitive_globs () -> list :
4656 return list (DEFAULT_SENSITIVE_PATH_GLOBS )
4757
@@ -60,10 +70,20 @@ def __post_init__(self) -> None:
6070 if guardrail .get ('key' ) in _KNOWN_GUARDRAIL_KEYS
6171 }
6272
73+ @property
74+ def ttl_seconds (self ) -> float :
75+ return self .payload .get ('ttl_seconds' ) or DEFAULT_TTL_SECONDS
76+
77+ def _agents (self , guardrail_key : str ) -> dict :
78+ return (self ._guardrails .get (guardrail_key ) or {}).get ('agents' ) or {}
79+
6380 def mode_for (self , guardrail_key : str , ide_name : Optional [str ]) -> str :
6481 """The platform keys the cells by our --ide names, so the lookup is direct."""
65- agents = (self ._guardrails .get (guardrail_key ) or {}).get ('agents' ) or {}
66- return str (agents .get ((ide_name or '' ).lower (), GuardrailCellMode .REPORT .value )).lower ()
82+ agents = self ._agents (guardrail_key )
83+ return str (agents .get ((ide_name or '' ).lower (), default_mode_for (guardrail_key ))).lower ()
84+
85+ def is_off_for_every_agent (self , guardrail_key : str ) -> bool :
86+ return all (str (mode ).lower () == GuardrailCellMode .OFF for mode in self ._agents (guardrail_key ).values ())
6787
6888 def _modes_for_event (self , event_name : str , ide_name : Optional [str ]) -> list :
6989 return [
@@ -87,8 +107,7 @@ def sensitive_globs(self) -> list:
87107 return globs if isinstance (globs , list ) and globs else _default_sensitive_globs ()
88108
89109 def is_expired (self ) -> bool :
90- ttl = self .payload .get ('ttl_seconds' ) or _DEFAULT_TTL_SECONDS
91- return time .time () - self .fetched_at > ttl
110+ return time .time () - self .fetched_at > self .ttl_seconds
92111
93112 def needs_refresh (self , tenant_id : Optional [str ]) -> bool :
94113 """Expired, or fetched for another tenant (the user switched tenants since)."""
@@ -99,14 +118,14 @@ def apply_platform_config(policy: dict, config: Optional[GuardrailConfig], ide_n
99118 """Overlay the platform-owned enforcement config onto the local knobs-only policy.
100119
101120 The platform is the only mode source: no cache (cold start) means the built-in defaults -
102- Report everywhere with the default globs - which equal an unconfigured tenant's platform
103- config, so behaviour is uniform either way. Each matrix cell lands on its own per-feature
104- action, so the two FileRead guardrails (content scan vs. sensitive path ) keep independent modes.
121+ Report everywhere (unauthorized MCP server Off) with the default globs - which equal an unconfigured
122+ tenant's platform config, so behaviour is uniform either way. Each matrix cell lands on its own
123+ per-feature action, so guardrails sharing an event (e.g. the two FileRead ones ) keep independent modes.
105124 An all-Off event never reaches here at all: scan_command skips it.
106125 """
107126
108127 def cell (guardrail_key : str ) -> str :
109- return config .mode_for (guardrail_key , ide_name ) if config is not None else GuardrailCellMode . REPORT . value
128+ return config .mode_for (guardrail_key , ide_name ) if config is not None else default_mode_for ( guardrail_key )
110129
111130 def action (guardrail_key : str ) -> str :
112131 return PolicyMode .BLOCK .value if cell (guardrail_key ) == GuardrailCellMode .BLOCK else PolicyMode .WARN .value
@@ -123,7 +142,11 @@ def action(guardrail_key: str) -> str:
123142 )
124143 file_read ['path_action' ] = action (BlockReason .SENSITIVE_PATH )
125144
126- policy .setdefault ('mcp' , {})['action' ] = action (BlockReason .SECRETS_IN_MCP_ARGS )
145+ mcp = policy .setdefault ('mcp' , {})
146+ mcp ['scan_args' ] = cell (BlockReason .SECRETS_IN_MCP_ARGS ) != GuardrailCellMode .OFF
147+ mcp ['action' ] = action (BlockReason .SECRETS_IN_MCP_ARGS )
148+ mcp ['check_server' ] = cell (BlockReason .UNAUTHORIZED_MCP_SERVER ) != GuardrailCellMode .OFF
149+ mcp ['server_action' ] = action (BlockReason .UNAUTHORIZED_MCP_SERVER )
127150
128151
129152def save_guardrail_config (payload : dict , tenant_id : Optional [str ]) -> None :
0 commit comments