Complete snapshot of /opt/microdao-daarion/ from NODE1 (144.76.224.179).
This represents the actual running production code that has diverged
significantly from the previous main branch.
Key changes from old main:
- Gateway (http_api.py): expanded from ~40KB to 164KB with full agent support
- Router: new /v1/agents/{id}/infer endpoint with vision + DeepSeek routing
- Behavior Policy: SOWA v2.2 (3-level: FULL/ACK/SILENT)
- Agent Registry: config/agent_registry.yml as single source of truth
- 13 agents configured (was 3)
- Memory service integration
- CrewAI teams and roles
Excluded from snapshot: venv/, .env, data/, backups, .tgz archives
Co-authored-by: Cursor <cursoragent@cursor.com>
267 lines
6.6 KiB
Markdown
267 lines
6.6 KiB
Markdown
# Privacy Gate Specification
|
||
|
||
**Версія:** 1.0
|
||
**Статус:** Mandatory Middleware
|
||
|
||
---
|
||
|
||
## Принцип
|
||
|
||
Privacy Gate — обов'язковий middleware на Router/Tool Manager рівні, який контролює передачу контенту між сервісами на основі `mode` та `consent`.
|
||
|
||
---
|
||
|
||
## Modes
|
||
|
||
| Mode | Description | Content Access | Logging |
|
||
|------|-------------|----------------|---------|
|
||
| `public` | Публічний контент | Full access | Full logging |
|
||
| `team` | Командний контент | Team members only | Metadata only |
|
||
| `confidential` | Конфіденційний | Sanitized or with consent | No content |
|
||
| `e2ee` | End-to-end encrypted | Never plaintext | No content |
|
||
|
||
---
|
||
|
||
## Privacy Gate Rules
|
||
|
||
### Rule 1: Public Mode
|
||
```python
|
||
if request.mode == "public":
|
||
# Повний доступ
|
||
allow_content = True
|
||
log_content = True
|
||
transform = None
|
||
```
|
||
|
||
### Rule 2: Team Mode
|
||
```python
|
||
if request.mode == "team":
|
||
# Перевірка membership
|
||
if user in request.team.members:
|
||
allow_content = True
|
||
log_content = False # Тільки metadata
|
||
transform = None
|
||
else:
|
||
allow_content = False
|
||
raise AccessDenied("Not a team member")
|
||
```
|
||
|
||
### Rule 3: Confidential Mode
|
||
```python
|
||
if request.mode == "confidential":
|
||
# Тільки sanitized або з consent
|
||
if request.user_consent:
|
||
allow_content = True
|
||
log_content = False
|
||
transform = None
|
||
else:
|
||
allow_content = True
|
||
log_content = False
|
||
transform = sanitize_context # Тільки summary
|
||
```
|
||
|
||
### Rule 4: E2EE Mode
|
||
```python
|
||
if request.mode == "e2ee":
|
||
# НІКОЛИ plaintext
|
||
allow_content = False
|
||
log_content = False
|
||
# Обробка тільки на клієнті
|
||
raise E2EEViolation("Cannot process E2EE content server-side")
|
||
```
|
||
|
||
---
|
||
|
||
## Sanitization Rules
|
||
|
||
### Context Sanitization
|
||
|
||
```python
|
||
def sanitize_context(context: str, max_tokens: int = 100) -> str:
|
||
"""
|
||
Перетворює детальний контекст на узагальнений summary.
|
||
|
||
Правила:
|
||
1. Видалити PII (імена, телефони, email)
|
||
2. Видалити конкретні числа/дати
|
||
3. Узагальнити до категорії
|
||
4. Обмежити довжину
|
||
"""
|
||
# Step 1: Remove PII
|
||
context = remove_pii(context)
|
||
|
||
# Step 2: Summarize
|
||
summary = llm_summarize(context, max_tokens=max_tokens)
|
||
|
||
# Step 3: Validate no sensitive data
|
||
if contains_sensitive(summary):
|
||
return "[Confidential content]"
|
||
|
||
return summary
|
||
```
|
||
|
||
### PII Patterns
|
||
|
||
```python
|
||
PII_PATTERNS = [
|
||
r'\b\d{10,}\b', # Phone numbers
|
||
r'\b[\w.-]+@[\w.-]+\.\w+\b', # Emails
|
||
r'\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b', # Credit cards
|
||
r'\b\d{2,3}-\d{2,3}-\d{2,3}\b', # IDs
|
||
# Додати специфічні для домену
|
||
]
|
||
```
|
||
|
||
---
|
||
|
||
## Integration Points
|
||
|
||
### 1. Router Level
|
||
|
||
```python
|
||
@router.middleware
|
||
async def privacy_gate_middleware(request: Request, call_next):
|
||
gate = PrivacyGate()
|
||
result = gate.check(request)
|
||
|
||
if not result.allow:
|
||
raise AccessDenied(result.reason)
|
||
|
||
if result.transform:
|
||
request.context = result.transform(request.context)
|
||
|
||
response = await call_next(request)
|
||
|
||
if result.log_content:
|
||
await audit_log(request, response)
|
||
else:
|
||
await audit_log_metadata_only(request, response)
|
||
|
||
return response
|
||
```
|
||
|
||
### 2. Tool Manager Level
|
||
|
||
```python
|
||
class ToolManager:
|
||
def execute(self, tool: Tool, context: Context) -> Result:
|
||
# Check privacy before tool execution
|
||
gate = PrivacyGate()
|
||
if context.mode in ["confidential", "e2ee"]:
|
||
if tool.requires_plaintext:
|
||
raise PrivacyViolation(
|
||
f"Tool {tool.name} cannot be used in {context.mode} mode"
|
||
)
|
||
|
||
return tool.execute(context)
|
||
```
|
||
|
||
### 3. Memory Service Level
|
||
|
||
```python
|
||
class MemoryService:
|
||
async def store(self, content: str, metadata: dict) -> str:
|
||
mode = metadata.get("mode", "public")
|
||
|
||
if mode == "confidential":
|
||
# Зберігаємо тільки embedding, не plaintext
|
||
embedding = await self.embed(content)
|
||
return await self.store_embedding_only(embedding, metadata)
|
||
|
||
if mode == "e2ee":
|
||
# Зберігаємо тільки encrypted blob
|
||
return await self.store_encrypted(content, metadata)
|
||
|
||
# Public/team - зберігаємо все
|
||
return await self.store_full(content, metadata)
|
||
```
|
||
|
||
---
|
||
|
||
## Consent Flow
|
||
|
||
### User Consent for Handoff
|
||
|
||
```
|
||
User → DAARWIZZ: "my health data question"
|
||
|
||
DAARWIZZ detects: mode=confidential, target=Nutra
|
||
|
||
DAARWIZZ → User: "Це питання потребує передачі в Nutra.
|
||
Дозволиш передати узагальнений контекст?"
|
||
|
||
User → DAARWIZZ: "так"
|
||
|
||
DAARWIZZ sets: request.user_consent = True
|
||
|
||
DAARWIZZ → Nutra: sanitized_context (with consent)
|
||
|
||
Nutra → DAARWIZZ: response
|
||
|
||
DAARWIZZ → User: response
|
||
```
|
||
|
||
### Consent Storage
|
||
|
||
```sql
|
||
CREATE TABLE user_consents (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
user_id VARCHAR(255) NOT NULL,
|
||
consent_type VARCHAR(100) NOT NULL, -- 'handoff', 'indexing', etc.
|
||
target_agent VARCHAR(100),
|
||
granted_at TIMESTAMPTZ DEFAULT NOW(),
|
||
expires_at TIMESTAMPTZ,
|
||
revoked_at TIMESTAMPTZ,
|
||
context_hash VARCHAR(64) -- Для аудиту, без контенту
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
## Audit Trail
|
||
|
||
### Privacy Events
|
||
|
||
```json
|
||
{
|
||
"event_type": "privacy.check",
|
||
"timestamp": "2026-01-19T10:00:00Z",
|
||
"request_id": "uuid",
|
||
"user_id": "user_123",
|
||
"mode": "confidential",
|
||
"action": "sanitize",
|
||
"target_service": "nutra",
|
||
"consent_given": true,
|
||
"content_hash": "sha256:...", // Без контенту
|
||
"result": "allowed"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Testing
|
||
|
||
### Test Cases
|
||
|
||
```python
|
||
def test_public_mode_allows_full_content():
|
||
request = Request(mode="public", content="Hello")
|
||
result = gate.check(request)
|
||
assert result.allow == True
|
||
assert result.transform is None
|
||
|
||
def test_confidential_mode_requires_consent():
|
||
request = Request(mode="confidential", content="Secret")
|
||
result = gate.check(request)
|
||
assert result.transform == sanitize_context
|
||
|
||
def test_e2ee_mode_blocks_server_processing():
|
||
request = Request(mode="e2ee", content="Encrypted")
|
||
with pytest.raises(E2EEViolation):
|
||
gate.check(request)
|
||
```
|
||
|
||
---
|
||
|
||
**Останнє оновлення:** 2026-01-19
|