Files
cowork-local/core/security_rules.py
T
2026-08-09 20:12:05 +07:00

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]