chore(repo): initialize Cowork Local Gitea repository
CI / test (push) Canceled after 0s

This commit is contained in:
thanhnv
2026-08-09 20:12:05 +07:00
commit 414eaddca3
192 changed files with 48160 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Security validation layer for Cowork Local agent execution."""
+13
View File
@@ -0,0 +1,13 @@
"""Action validator — classifies agent actions and enforces policy."""
from __future__ import annotations
from .command_risk_classifier import classify_action
ACTION_DENIED_MESSAGE = "Action denied by security policy."
def validate_action(action_type: str, action_details: dict = None) -> bool:
"""Return True if the action is allowed, False if blocked."""
result = classify_action(action_type, action_details)
return not result.blocked
+11
View File
@@ -0,0 +1,11 @@
"""Attachment validator — inspects attached files for security risks."""
from __future__ import annotations
from .command_risk_classifier import classify_attachment
ATTACHMENT_DENIED_MESSAGE = "Attached content failed security validation."
def validate_attachment(path: str, mime_type: str = "") -> bool:
result = classify_attachment(path, mime_type or None)
return not result.blocked
+101
View File
@@ -0,0 +1,101 @@
"""Audit logger — records every sandbox execution attempt.
Logs: allow, deny, execution events with timestamp, user, project, workspace,
prompt category, risk score, action type, backend selected, command hash,
working directory scope, network blocked, result status, return code, denial reason.
Does NOT log secrets or raw sensitive content.
"""
from __future__ import annotations
import hashlib
import json
import logging
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Optional
logger = logging.getLogger("cowork_local.security.audit")
@dataclass
class AuditEntry:
timestamp: str = ""
user: str = ""
project: str = ""
workspace: str = ""
prompt_category: str = ""
risk_score: int = 0
action_type: str = ""
backend_selected: str = ""
command_hash: str = ""
working_directory_scope: str = ""
network_blocked: bool = False
result_status: str = "" # allowed, denied, executed, error
return_code: int = 0
denial_reason: str = ""
approval_status: str = "" # auto, approved, rejected
def __post_init__(self):
if not self.timestamp:
self.timestamp = datetime.now(timezone.utc).isoformat()
if not self.command_hash and self.action_type:
self.command_hash = hashlib.sha256(
self.action_type.encode()
).hexdigest()[:16]
def _audit_dir() -> Path:
from ..config import CONFIG_DIR
return CONFIG_DIR / "audit"
def record(
action_type: str,
result_status: str,
*,
user: str = "",
project: str = "",
workspace: str = "",
prompt_category: str = "",
risk_score: int = 0,
backend_selected: str = "",
command: str = "",
working_directory: str = "",
network_blocked: bool = False,
return_code: int = 0,
denial_reason: str = "",
approval_status: str = "",
) -> AuditEntry:
"""Create and persist an audit log entry."""
cmd_hash = hashlib.sha256(command.encode()).hexdigest()[:16] if command else ""
entry = AuditEntry(
user=user,
project=project,
workspace=workspace,
prompt_category=prompt_category,
risk_score=risk_score,
action_type=action_type,
backend_selected=backend_selected,
command_hash=cmd_hash,
working_directory_scope=working_directory,
network_blocked=network_blocked,
result_status=result_status,
return_code=return_code,
denial_reason=denial_reason,
approval_status=approval_status,
)
# Write to JSONL file
audit_dir = _audit_dir()
audit_dir.mkdir(parents=True, exist_ok=True)
log_file = audit_dir / "sandbox_audit.jsonl"
with open(log_file, "a", encoding="utf-8") as f:
f.write(json.dumps(asdict(entry)) + "\n")
logger.info(
"AUDIT: action=%s status=%s user=%s risk=%d backend=%s",
action_type, result_status, user, risk_score, backend_selected,
)
return entry
+151
View File
@@ -0,0 +1,151 @@
"""Command risk classifier — scores commands 0-100 and assigns risk level.
This is the first gate in the security validation pipeline. It classifies every
command/tool-call/prompt into a risk bucket so SandboxManager can select the
right isolation backend.
Risk levels:
safe (0-30) Business-safe, read-only, no system impact
moderate (31-60) File writes, trusted internal tools, report generation
high (61-85) Interpreters, untrusted commands, external file access
critical (86-100) Unknown binaries, privilege changes, shell expansion,
system discovery, source-code access, secret access
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from enum import Enum
from typing import List, Optional
class RiskLevel(str, Enum):
SAFE = "safe"
MODERATE = "moderate"
HIGH = "high"
CRITICAL = "critical"
BLOCKED = "blocked"
@dataclass
class RiskResult:
score: int # 0-100
level: RiskLevel # categorized bucket
reasons: List[str] # why this score was assigned
blocked: bool = False
# Patterns that immediately block (score=100, blocked=True)
_BLOCK_PATTERNS = [
r'\bwhoami\b', r'\bgetent\b', r'\bw\b', r'\buname\b', r'\bhostname\b',
r'\bnmap\b', r'\bnetstat\b', r'\bir\b', r'\bpip\s+list\b', r'\bnpm\s+list\b',
r'\bsecret\b', r'\bpassword\b', r'\bapi[_-]?key\b', r'\btoken\b',
r'\b\.env\b', r'\bcredentials?\b', r'\bprivate[_-]?key\b',
r'\bsudo\b', r'\brunsas\b', r'\bpowershell\s+-ep\s+bypass',
r'\bexploit\b', r'\bpayload\b', r'\bshellcode\b',
r'ignore\s+previous\s+instructions?',
r'you\s+are\s+now\s+(\w+)',
r'disabl(e|ed?)\s+(sandbox|security|guardrail|filter)',
r'bypass\s+(security|sandbox|policy)',
]
_HIGH_PATTERNS = [
r'\b(python|node|ruby|perl|php|bash|sh|pwsh|powershell)\b',
r'\bexec\b', r'\beval\b', r'\bsystem\b', r'\bpopen\b',
r'\bcurl\s+.*\|\s*(bash|sh|python|node)',
r'\brm\s+-rf\b', r'\bdeltree\b',
]
_MODERATE_PATTERNS = [
r'\b(touch|mkdir|cp|mv|rename)\b',
r'\b(pip|npm|pnpm|yarn)\s+install\b',
r'\b(make|cmake|gradle|mvn)\b',
r'\b(test|pytest|jest|mocha)\b',
]
def classify_command(command: str, is_cowork_mode: bool = True) -> RiskResult:
score = 0
reasons: List[str] = []
blocked = False
cmd_lower = command.lower()
for pattern in _BLOCK_PATTERNS:
m = re.search(pattern, cmd_lower, re.IGNORECASE)
if m:
reasons.append(f"blocked: matched '{m.group()[:50]}'")
score = 100
blocked = True
break
if not blocked:
high_hits = 0
for pattern in _HIGH_PATTERNS:
m = re.search(pattern, cmd_lower, re.IGNORECASE)
if m:
high_hits += 1
reasons.append(f"high: matched '{m.group()[:50]}'")
score = max(score, min(85, 50 + high_hits * 10))
mod_hits = 0
for pattern in _MODERATE_PATTERNS:
m = re.search(pattern, cmd_lower, re.IGNORECASE)
if m:
mod_hits += 1
reasons.append(f"moderate: matched '{m.group()[:50]}'")
score = max(score, min(60, 20 + mod_hits * 10))
if not reasons:
score = 10
reasons.append("safe: no risky patterns detected")
if blocked:
level = RiskLevel.BLOCKED
elif score >= 86:
level = RiskLevel.CRITICAL
elif score >= 61:
level = RiskLevel.HIGH
elif score >= 31:
level = RiskLevel.MODERATE
else:
level = RiskLevel.SAFE
return RiskResult(score=score, level=level, reasons=reasons, blocked=blocked)
def classify_prompt(prompt: str, is_cowork_mode: bool = True) -> RiskResult:
return classify_command(prompt, is_cowork_mode=is_cowork_mode)
def classify_attachment(path: str, mime_type: Optional[str] = None) -> RiskResult:
import os
_, ext = os.path.splitext(path.lower())
blocked_ext = {
'.py', '.js', '.ts', '.java', '.cs', '.cpp', '.c', '.go', '.rs',
'.php', '.vb', '.sql', '.ps1', '.sh', '.bat', '.cmd', '.vbs',
'.vba', '.exe', '.dll', '.jar',
}
if ext in blocked_ext:
return RiskResult(100, RiskLevel.BLOCKED,
[f"blocked: extension '{ext}'"], blocked=True)
if mime_type:
blocked_mimes = {
'application/x-executable', 'application/x-dosexec',
'application/x-pie-executable', 'application/x-sharedlib',
'application/java-archive', 'application/x-msdownload',
}
if mime_type.lower() in blocked_mimes:
return RiskResult(100, RiskLevel.BLOCKED,
[f"blocked: MIME '{mime_type}'"], blocked=True)
return RiskResult(30, RiskLevel.SAFE, ["safe: allowed file type"], blocked=False)
def classify_action(action_type: str, action_details: Optional[dict] = None) -> RiskResult:
a = action_type.lower()
if any(kw in a for kw in ('execute', 'run', 'shell', 'system')):
return RiskResult(70, RiskLevel.HIGH, [f"high: action '{action_type}'"])
if any(kw in a for kw in ('write', 'create', 'modify', 'delete', 'install')):
return RiskResult(40, RiskLevel.MODERATE, [f"moderate: action '{action_type}'"])
if any(kw in a for kw in ('read', 'list', 'get', 'search', 'query')):
return RiskResult(10, RiskLevel.SAFE, [f"safe: action '{action_type}'"])
return RiskResult(50, RiskLevel.MODERATE, [f"unknown: action '{action_type}'"])
+25
View File
@@ -0,0 +1,25 @@
"""Prompt validator — detects and blocks malicious user prompts.
Checks for prompt injection, jailbreak, policy bypass, role override,
system prompt extraction, secret extraction, source code access,
app architecture discovery, agent discovery, MCP discovery.
"""
from __future__ import annotations
from .command_risk_classifier import RiskResult, RiskLevel, classify_prompt
def validate_prompt(prompt: str, is_cowork_mode: bool = True) -> RiskResult:
"""Validate a user prompt before agent processing.
Returns RiskResult with blocked=True if the prompt must be rejected.
"""
result = classify_prompt(prompt, is_cowork_mode=is_cowork_mode)
if result.blocked:
result.reasons.insert(0, "Prompt denied by security policy")
return result
PROMPT_DENIED_MESSAGE = "Request denied due to security policy."