87 lines
3.2 KiB
Python
87 lines
3.2 KiB
Python
"""External security/restriction rules the agent must check every request and
|
|
response against before acting — analogous to core/skills.py, but for
|
|
mandatory guardrails rather than opt-in behaviors. An admin/security team can
|
|
edit this file directly; it's re-read fresh on every turn, so no rebuild or
|
|
even app restart is needed for a change to take effect.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
from ..config import CONFIG_DIR
|
|
|
|
RULES_PATH = CONFIG_DIR / "security_rules.md"
|
|
BUNDLED_RULEBASE = Path(__file__).resolve().parent.parent / "assets" / "RULEBASE.md"
|
|
# The CODE agent uses a SEPARATE rulebase (RULEBASE.md — incl. any "no coding"
|
|
# restriction — applies to the Cowork agent only). This one is intentionally
|
|
# empty for now; the Code agent's safety comes from the sandbox until rules are
|
|
# defined here. CONFIG_DIR override wins over the bundled placeholder.
|
|
BUNDLED_CODE_RULES = Path(__file__).resolve().parent.parent / "assets" / "RULEforCode.md"
|
|
CODE_RULES_PATH = CONFIG_DIR / "RULEforCode.md"
|
|
_HTML_COMMENT_RE = re.compile(r"<!--.*?-->", re.DOTALL)
|
|
# Keep prompt bloat bounded even if someone pastes an entire policy document.
|
|
_MAX_CHARS = 20000
|
|
|
|
|
|
def _resolve_rulebase() -> Path:
|
|
"""Return the rulebase path:
|
|
1. Configured rulebase_path in agent_security settings (rulebase_path key)
|
|
2. CONFIG_DIR/RULEBASE.md (copied from bundled)
|
|
3. Bundled RULEBASE.md in assets/
|
|
4. Old CONFIG_DIR/security_rules.md (legacy fallback)
|
|
"""
|
|
try:
|
|
from ..config import load_config
|
|
cfg = load_config()
|
|
rb_path = (cfg.get("agent_security") or {}).get("rulebase_path", "")
|
|
if rb_path and Path(rb_path).exists():
|
|
return Path(rb_path)
|
|
except Exception:
|
|
pass
|
|
# Copied config rulebase
|
|
copied = CONFIG_DIR / "RULEBASE.md"
|
|
if copied.exists():
|
|
return copied
|
|
# Bundled default
|
|
if BUNDLED_RULEBASE.exists():
|
|
return BUNDLED_RULEBASE
|
|
# Legacy fallback
|
|
if RULES_PATH.exists():
|
|
return RULES_PATH
|
|
return RULES_PATH
|
|
|
|
|
|
def load_rules(path: Path = None) -> str:
|
|
"""Best-effort read of the external rules file.
|
|
|
|
Returns '' when the file is missing/unreadable/empty — the agent must
|
|
keep working with no rules configured rather than ever block on this."""
|
|
if path is None:
|
|
path = _resolve_rulebase()
|
|
try:
|
|
text = path.read_text(encoding="utf-8").strip()
|
|
except OSError:
|
|
return ""
|
|
return text[:_MAX_CHARS]
|
|
|
|
|
|
def _resolve_code_rulebase() -> Path:
|
|
"""CONFIG_DIR/RULEforCode.md if the admin created one, else the bundled
|
|
(empty) placeholder."""
|
|
return CODE_RULES_PATH if CODE_RULES_PATH.exists() else BUNDLED_CODE_RULES
|
|
|
|
|
|
def load_code_rules(path: Path = None) -> str:
|
|
"""Rules for the CODE agent (RULEforCode.md). HTML comments are stripped so
|
|
the placeholder file — which is comment-only — yields NO rules (the Code
|
|
agent is unrestricted beyond the sandbox until real rules are added)."""
|
|
if path is None:
|
|
path = _resolve_code_rulebase()
|
|
try:
|
|
raw = path.read_text(encoding="utf-8")
|
|
except OSError:
|
|
return ""
|
|
text = _HTML_COMMENT_RE.sub("", raw).strip()
|
|
return text[:_MAX_CHARS]
|