forgedev 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -10
- package/bin/chainproof.js +126 -0
- package/bin/devforge.js +1 -1
- package/package.json +25 -7
- package/src/chainproof-bridge.js +330 -0
- package/src/ci-mode.js +85 -0
- package/src/claude-configurator.js +171 -78
- package/src/cli.js +30 -7
- package/src/composer.js +242 -214
- package/src/doctor-checks-chainproof.js +106 -0
- package/src/doctor-checks.js +39 -20
- package/src/doctor-prompts.js +9 -9
- package/src/doctor.js +37 -4
- package/src/guided.js +3 -3
- package/src/index.js +31 -10
- package/src/init-mode.js +76 -12
- package/src/menu.js +178 -0
- package/src/prompts.js +5 -12
- package/src/recommender.js +163 -30
- package/src/scanner.js +57 -2
- package/src/uat-generator.js +204 -189
- package/src/update-check.js +9 -4
- package/src/update.js +57 -13
- package/src/utils.js +162 -5
- package/templates/ai/guardrails-py/backend/app/ai/__init__.py +29 -0
- package/templates/ai/guardrails-py/backend/app/ai/audit_log.py +133 -0
- package/templates/ai/guardrails-py/backend/app/ai/client.py.template +323 -0
- package/templates/ai/guardrails-py/backend/app/ai/health.py.template +157 -0
- package/templates/ai/guardrails-py/backend/app/ai/input_guard.py +98 -0
- package/templates/ai/guardrails-ts/src/lib/ai/audit-log.ts.template +164 -0
- package/templates/ai/guardrails-ts/src/lib/ai/client.ts.template +403 -0
- package/templates/ai/guardrails-ts/src/lib/ai/health.ts.template +165 -0
- package/templates/ai/guardrails-ts/src/lib/ai/index.ts.template +17 -0
- package/templates/ai/guardrails-ts/src/lib/ai/input-guard.ts.template +124 -0
- package/templates/auth/nextauth/src/lib/auth.ts.template +12 -7
- package/templates/backend/express/Dockerfile.template +18 -0
- package/templates/backend/express/package.json.template +33 -0
- package/templates/backend/express/src/index.ts.template +34 -0
- package/templates/backend/express/src/routes/health.ts.template +27 -0
- package/templates/backend/express/tsconfig.json +17 -0
- package/templates/backend/fastapi/backend/Dockerfile.template +5 -0
- package/templates/backend/fastapi/backend/app/api/health.py.template +1 -1
- package/templates/backend/fastapi/backend/app/core/config.py.template +1 -1
- package/templates/backend/fastapi/backend/app/core/errors.py +1 -1
- package/templates/backend/fastapi/backend/app/main.py.template +3 -1
- package/templates/backend/fastapi/backend/requirements.txt.template +2 -0
- package/templates/backend/hono/Dockerfile.template +18 -0
- package/templates/backend/hono/package.json.template +31 -0
- package/templates/backend/hono/src/index.ts.template +32 -0
- package/templates/backend/hono/src/routes/health.ts.template +27 -0
- package/templates/backend/hono/tsconfig.json +18 -0
- package/templates/base/.gitignore.template +3 -0
- package/templates/base/docs/uat/UAT_TEMPLATE.md.template +1 -1
- package/templates/chainproof/base/.chainproof/config.json.template +11 -0
- package/templates/chainproof/base/.chainproof/mcp-server.mjs +310 -0
- package/templates/chainproof/base/.mcp.json +9 -0
- package/templates/chainproof/fastapi/.chainproof/middleware.json.template +14 -0
- package/templates/chainproof/nextjs/.chainproof/hooks.json.template +19 -0
- package/templates/chainproof/polyglot/.chainproof/config.json.template +21 -0
- package/templates/claude-code/agents/architect.md +25 -11
- package/templates/claude-code/agents/build-error-resolver.md +19 -5
- package/templates/claude-code/agents/chief-of-staff.md +42 -8
- package/templates/claude-code/agents/code-quality-reviewer.md +14 -0
- package/templates/claude-code/agents/database-reviewer.md +15 -1
- package/templates/claude-code/agents/deep-reviewer.md +191 -0
- package/templates/claude-code/agents/doc-updater.md +19 -5
- package/templates/claude-code/agents/docs-lookup.md +19 -5
- package/templates/claude-code/agents/e2e-runner.md +26 -12
- package/templates/claude-code/agents/enforcement-gate.md +102 -0
- package/templates/claude-code/agents/frontend-builder.md +188 -0
- package/templates/claude-code/agents/harness-optimizer.md +36 -1
- package/templates/claude-code/agents/loop-operator.md +27 -13
- package/templates/claude-code/agents/planner.md +21 -7
- package/templates/claude-code/agents/product-strategist.md +24 -10
- package/templates/claude-code/agents/production-readiness.md +14 -0
- package/templates/claude-code/agents/prompt-auditor.md +115 -0
- package/templates/claude-code/agents/refactor-cleaner.md +22 -8
- package/templates/claude-code/agents/security-reviewer.md +14 -0
- package/templates/claude-code/agents/spec-validator.md +15 -1
- package/templates/claude-code/agents/tdd-guide.md +21 -7
- package/templates/claude-code/agents/uat-validator.md +14 -0
- package/templates/claude-code/claude-md/base.md +14 -7
- package/templates/claude-code/claude-md/fastapi.md +8 -8
- package/templates/claude-code/claude-md/fullstack.md +6 -6
- package/templates/claude-code/claude-md/hono.md +18 -0
- package/templates/claude-code/claude-md/nextjs.md +5 -5
- package/templates/claude-code/claude-md/remix.md +18 -0
- package/templates/claude-code/commands/audit-security.md +14 -0
- package/templates/claude-code/commands/audit-spec.md +14 -0
- package/templates/claude-code/commands/audit-wiring.md +14 -0
- package/templates/claude-code/commands/build-fix.md +28 -0
- package/templates/claude-code/commands/build-ui.md +59 -0
- package/templates/claude-code/commands/code-review.md +53 -31
- package/templates/claude-code/commands/fix-loop.md +211 -0
- package/templates/claude-code/commands/full-audit.md +36 -8
- package/templates/claude-code/commands/generate-prd.md +1 -1
- package/templates/claude-code/commands/generate-sdd.md +74 -0
- package/templates/claude-code/commands/generate-uat.md +107 -35
- package/templates/claude-code/commands/help.md +68 -0
- package/templates/claude-code/commands/live-uat.md +268 -0
- package/templates/claude-code/commands/optimize-claude-md.md +15 -1
- package/templates/claude-code/commands/plan.md +3 -3
- package/templates/claude-code/commands/pre-pr.md +57 -19
- package/templates/claude-code/commands/product-strategist.md +21 -0
- package/templates/claude-code/commands/resume-session.md +10 -10
- package/templates/claude-code/commands/run-uat.md +59 -2
- package/templates/claude-code/commands/save-session.md +10 -10
- package/templates/claude-code/commands/simplify.md +36 -0
- package/templates/claude-code/commands/tdd.md +17 -18
- package/templates/claude-code/commands/verify-all.md +24 -0
- package/templates/claude-code/commands/verify-intent.md +55 -0
- package/templates/claude-code/commands/workflows.md +52 -40
- package/templates/claude-code/hooks/polyglot.json +10 -1
- package/templates/claude-code/hooks/python.json +10 -1
- package/templates/claude-code/hooks/scripts/autofix-polyglot.mjs +2 -2
- package/templates/claude-code/hooks/scripts/autofix-python.mjs +1 -1
- package/templates/claude-code/hooks/scripts/autofix-typescript.mjs +1 -1
- package/templates/claude-code/hooks/scripts/code-hygiene.mjs +293 -0
- package/templates/claude-code/hooks/scripts/pre-commit-gate.mjs +207 -0
- package/templates/claude-code/hooks/typescript.json +10 -1
- package/templates/claude-code/skills/ai-prompts/SKILL.md +119 -41
- package/templates/claude-code/skills/git-workflow/SKILL.md +5 -5
- package/templates/claude-code/skills/nextjs/SKILL.md +1 -1
- package/templates/claude-code/skills/playwright/SKILL.md +5 -5
- package/templates/claude-code/skills/security-api/SKILL.md +1 -1
- package/templates/claude-code/skills/security-web/SKILL.md +1 -1
- package/templates/claude-code/skills/testing-patterns/SKILL.md +9 -9
- package/templates/database/prisma-postgres/{.env.example → .env.example.template} +1 -0
- package/templates/database/sqlalchemy-postgres/{.env.example → .env.example.template} +1 -0
- package/templates/docs-portal/fastapi/backend/app/portal/__pycache__/docs_reader.cpython-314.pyc +0 -0
- package/templates/docs-portal/fastapi/backend/app/portal/docs_reader.py +201 -0
- package/templates/docs-portal/fastapi/backend/app/portal/html_renderer.py +229 -0
- package/templates/docs-portal/fastapi/backend/app/portal/router.py.template +35 -0
- package/templates/docs-portal/nextjs/src/app/portal/[category]/[slug]/page.tsx +81 -0
- package/templates/docs-portal/nextjs/src/app/portal/[category]/page.tsx +65 -0
- package/templates/docs-portal/nextjs/src/app/portal/layout.tsx.template +54 -0
- package/templates/docs-portal/nextjs/src/app/portal/page.tsx +85 -0
- package/templates/docs-portal/nextjs/src/components/portal/markdown-renderer.tsx +101 -0
- package/templates/docs-portal/nextjs/src/components/portal/mobile-portal-nav.tsx +81 -0
- package/templates/docs-portal/nextjs/src/components/portal/portal-nav.tsx +86 -0
- package/templates/docs-portal/nextjs/src/lib/docs.ts +139 -0
- package/templates/frontend/nextjs/package.json.template +3 -1
- package/templates/frontend/react/index.html.template +12 -0
- package/templates/frontend/react/package.json.template +34 -0
- package/templates/frontend/react/src/App.tsx.template +10 -0
- package/templates/frontend/react/src/index.css +1 -0
- package/templates/frontend/react/src/main.tsx +10 -0
- package/templates/frontend/react/tsconfig.json +17 -0
- package/templates/frontend/react/vite.config.ts.template +15 -0
- package/templates/frontend/react/vitest.config.ts +9 -0
- package/templates/frontend/remix/app/root.tsx.template +31 -0
- package/templates/frontend/remix/app/routes/_index.tsx.template +19 -0
- package/templates/frontend/remix/app/routes/api.health.ts.template +10 -0
- package/templates/frontend/remix/app/tailwind.css +1 -0
- package/templates/frontend/remix/package.json.template +39 -0
- package/templates/frontend/remix/tsconfig.json +18 -0
- package/templates/frontend/remix/vite.config.ts.template +7 -0
- package/templates/infra/github-actions/.github/workflows/ci.yml.template +3 -0
- package/templates/infra/k8s/k8s/deployment.yml.template +70 -0
- package/templates/infra/k8s/k8s/hpa.yml.template +24 -0
- package/templates/infra/k8s/k8s/ingress.yml.template +26 -0
- package/templates/infra/k8s/k8s/kustomization.yml.template +13 -0
- package/templates/infra/k8s/k8s/namespace.yml.template +4 -0
- package/templates/infra/k8s/k8s/networkpolicy.yml.template +41 -0
- package/templates/infra/k8s/k8s/secrets.yml.template +10 -0
- package/templates/infra/k8s/k8s/service.yml.template +15 -0
- package/templates/testing/load/k6/README.md.template +48 -0
- package/templates/testing/load/k6/load-test.js.template +57 -0
- package/docs/00-README.md +0 -310
- package/docs/01-universal-prompt-library.md +0 -1049
- package/docs/02-claude-code-mastery-playbook.md +0 -283
- package/docs/03-multi-agent-verification.md +0 -565
- package/docs/04-errata-and-verification-checklist.md +0 -284
- package/docs/05-universal-scaffolder-vision.md +0 -452
- package/docs/06-confidence-assessment-and-repo-prompt.md +0 -407
- package/docs/errata.md +0 -58
- package/docs/multi-agent-verification.md +0 -66
- package/docs/playbook.md +0 -95
- package/docs/prompt-library.md +0 -160
- package/docs/uat/UAT_CHECKLIST.csv +0 -9
- package/docs/uat/UAT_TEMPLATE.md +0 -163
- package/templates/claude-code/commands/done.md +0 -19
- /package/{docs/plans/.gitkeep → templates/docs-portal/fastapi/backend/app/portal/__init__.py} +0 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""AI Health Check — Observability endpoint for AI system metrics.
|
|
2
|
+
|
|
3
|
+
Compliance: NIST AI RMF Manage 3.2 (monitoring),
|
|
4
|
+
EU AI Act Art. 9 (risk management)
|
|
5
|
+
|
|
6
|
+
Mount as: router.include_router(ai_health_router, prefix="/api/ai")
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
from collections import deque
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from fastapi import APIRouter
|
|
15
|
+
|
|
16
|
+
ai_health_router = APIRouter(tags=["ai-health"])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class CallMetric:
|
|
21
|
+
timestamp: float
|
|
22
|
+
model: str
|
|
23
|
+
latency_ms: float
|
|
24
|
+
confidence: float
|
|
25
|
+
success: bool
|
|
26
|
+
token_usage: dict[str, int] | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AIHealthMetrics:
|
|
30
|
+
def __init__(self, max_metrics: int = 5000):
|
|
31
|
+
self._metrics: deque[CallMetric] = deque(maxlen=max_metrics)
|
|
32
|
+
|
|
33
|
+
def record_call(
|
|
34
|
+
self,
|
|
35
|
+
model: str,
|
|
36
|
+
latency_ms: float,
|
|
37
|
+
confidence: float,
|
|
38
|
+
success: bool,
|
|
39
|
+
token_usage: dict[str, int] | None = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
self._metrics.append(CallMetric(
|
|
42
|
+
timestamp=time.time(),
|
|
43
|
+
model=model,
|
|
44
|
+
latency_ms=latency_ms,
|
|
45
|
+
confidence=confidence,
|
|
46
|
+
success=success,
|
|
47
|
+
token_usage=token_usage,
|
|
48
|
+
))
|
|
49
|
+
|
|
50
|
+
def get_health_status(self, window_seconds: int = 3600) -> dict[str, Any]:
|
|
51
|
+
cutoff = time.time() - window_seconds
|
|
52
|
+
recent = [m for m in self._metrics if m.timestamp >= cutoff]
|
|
53
|
+
|
|
54
|
+
if not recent:
|
|
55
|
+
return {
|
|
56
|
+
"status": "ok",
|
|
57
|
+
"ai_available": True,
|
|
58
|
+
"message": "No AI calls in the monitoring window",
|
|
59
|
+
"window": f"{window_seconds // 60}m",
|
|
60
|
+
"metrics": self._empty_metrics(),
|
|
61
|
+
"models": {},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
successes = [m for m in recent if m.success]
|
|
65
|
+
error_rate = 1 - (len(successes) / len(recent))
|
|
66
|
+
avg_confidence = (
|
|
67
|
+
sum(m.confidence for m in successes) / len(successes) if successes else 0
|
|
68
|
+
)
|
|
69
|
+
avg_latency = sum(m.latency_ms for m in recent) / len(recent)
|
|
70
|
+
low_confidence = [m for m in successes if m.confidence < 0.7]
|
|
71
|
+
low_confidence_rate = len(low_confidence) / max(len(successes), 1)
|
|
72
|
+
|
|
73
|
+
# Determine status
|
|
74
|
+
status = "ok"
|
|
75
|
+
warnings: list[str] = []
|
|
76
|
+
|
|
77
|
+
if error_rate > 0.5:
|
|
78
|
+
status = "unhealthy"
|
|
79
|
+
warnings.append(f"High error rate: {error_rate * 100:.1f}%")
|
|
80
|
+
elif error_rate > 0.1:
|
|
81
|
+
status = "degraded"
|
|
82
|
+
warnings.append(f"Elevated error rate: {error_rate * 100:.1f}%")
|
|
83
|
+
|
|
84
|
+
if avg_confidence < 0.5:
|
|
85
|
+
status = "degraded" if status == "ok" else status
|
|
86
|
+
warnings.append(f"Low average confidence: {avg_confidence * 100:.1f}%")
|
|
87
|
+
|
|
88
|
+
if avg_latency > 10_000:
|
|
89
|
+
status = "degraded" if status == "ok" else status
|
|
90
|
+
warnings.append(f"High average latency: {avg_latency:.0f}ms")
|
|
91
|
+
|
|
92
|
+
# Per-model breakdown
|
|
93
|
+
models: dict[str, dict] = {}
|
|
94
|
+
model_names = set(m.model for m in recent)
|
|
95
|
+
for model_name in model_names:
|
|
96
|
+
model_calls = [m for m in recent if m.model == model_name]
|
|
97
|
+
model_successes = [m for m in model_calls if m.success]
|
|
98
|
+
total_tokens = sum(
|
|
99
|
+
(m.token_usage.get("input_tokens", 0) + m.token_usage.get("output_tokens", 0))
|
|
100
|
+
for m in model_calls if m.token_usage
|
|
101
|
+
)
|
|
102
|
+
models[model_name] = {
|
|
103
|
+
"calls": len(model_calls),
|
|
104
|
+
"success_rate": len(model_successes) / len(model_calls),
|
|
105
|
+
"avg_latency_ms": sum(m.latency_ms for m in model_calls) / len(model_calls),
|
|
106
|
+
"avg_confidence": (
|
|
107
|
+
sum(m.confidence for m in model_successes) / len(model_successes)
|
|
108
|
+
if model_successes else 0
|
|
109
|
+
),
|
|
110
|
+
"total_tokens": total_tokens,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
"status": status,
|
|
115
|
+
"ai_available": error_rate < 1,
|
|
116
|
+
"message": "; ".join(warnings) if warnings else "All AI systems operating normally",
|
|
117
|
+
"window": f"{window_seconds // 60}m",
|
|
118
|
+
"metrics": {
|
|
119
|
+
"total_calls": len(recent),
|
|
120
|
+
"success_rate": 1 - error_rate,
|
|
121
|
+
"avg_confidence": avg_confidence,
|
|
122
|
+
"avg_latency_ms": avg_latency,
|
|
123
|
+
"low_confidence_rate": low_confidence_rate,
|
|
124
|
+
"error_rate": error_rate,
|
|
125
|
+
},
|
|
126
|
+
"models": models,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
def _empty_metrics(self) -> dict:
|
|
130
|
+
return {
|
|
131
|
+
"total_calls": 0,
|
|
132
|
+
"success_rate": 1.0,
|
|
133
|
+
"avg_confidence": 0.0,
|
|
134
|
+
"avg_latency_ms": 0.0,
|
|
135
|
+
"low_confidence_rate": 0.0,
|
|
136
|
+
"error_rate": 0.0,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# --- Singleton ---
|
|
141
|
+
|
|
142
|
+
ai_health_metrics = AIHealthMetrics()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# --- FastAPI Health Endpoint ---
|
|
146
|
+
|
|
147
|
+
@ai_health_router.get("/health")
|
|
148
|
+
async def ai_health():
|
|
149
|
+
"""AI system health check.
|
|
150
|
+
|
|
151
|
+
Returns model availability, confidence distribution,
|
|
152
|
+
error rates, and per-model metrics.
|
|
153
|
+
"""
|
|
154
|
+
return ai_health_metrics.get_health_status()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
AIHealthStatus = dict # Type alias for documentation
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""AI Input Guard — Prompt injection detection and input sanitization.
|
|
2
|
+
|
|
3
|
+
Compliance: EU AI Act Art. 15 (robustness), NIST AI RMF Manage 2.2
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class InputValidationResult:
|
|
12
|
+
blocked: bool
|
|
13
|
+
reason: str | None = None
|
|
14
|
+
detected_patterns: list[str] = field(default_factory=list)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# --- Injection Detection Patterns ---
|
|
18
|
+
|
|
19
|
+
INJECTION_PATTERNS: list[tuple[re.Pattern, str]] = [
|
|
20
|
+
# Direct instruction override
|
|
21
|
+
(re.compile(r"ignore\s+(all\s+)?(previous|prior|above)\s+(instructions|prompts|rules)", re.I), "instruction-override"),
|
|
22
|
+
(re.compile(r"disregard\s+(all\s+)?(previous|prior|above|your)\s+(instructions|prompts|rules|training)", re.I), "instruction-override"),
|
|
23
|
+
(re.compile(r"forget\s+(all\s+)?(previous|prior|your)\s+(instructions|context|rules)", re.I), "instruction-override"),
|
|
24
|
+
|
|
25
|
+
# Role manipulation
|
|
26
|
+
(re.compile(r"you\s+are\s+now\s+(a|an|the)\s+", re.I), "role-manipulation"),
|
|
27
|
+
(re.compile(r"act\s+as\s+(if\s+you\s+are|a|an)\s+", re.I), "role-manipulation"),
|
|
28
|
+
(re.compile(r"pretend\s+(to\s+be|you\s+are)\s+", re.I), "role-manipulation"),
|
|
29
|
+
(re.compile(r"from\s+now\s+on\s+(you|your)\s+", re.I), "role-manipulation"),
|
|
30
|
+
|
|
31
|
+
# System prompt extraction
|
|
32
|
+
(re.compile(r"what\s+(is|are)\s+your\s+(system\s+)?(prompt|instructions|rules)", re.I), "prompt-extraction"),
|
|
33
|
+
(re.compile(r"show\s+me\s+your\s+(system\s+)?(prompt|instructions)", re.I), "prompt-extraction"),
|
|
34
|
+
(re.compile(r"repeat\s+(your|the)\s+(system\s+)?(prompt|instructions)", re.I), "prompt-extraction"),
|
|
35
|
+
(re.compile(r"print\s+(your|the)\s+(system\s+)?(prompt|instructions)", re.I), "prompt-extraction"),
|
|
36
|
+
|
|
37
|
+
# Delimiter injection
|
|
38
|
+
(re.compile(r"</?system>", re.I), "delimiter-injection"),
|
|
39
|
+
(re.compile(r"\[INST\]", re.I), "delimiter-injection"),
|
|
40
|
+
(re.compile(r"<\|im_start\|", re.I), "delimiter-injection"),
|
|
41
|
+
(re.compile(r"###\s*(system|instruction|human|assistant)", re.I), "delimiter-injection"),
|
|
42
|
+
|
|
43
|
+
# Data exfiltration
|
|
44
|
+
(re.compile(r"send\s+(this|the|all)\s+(data|info|conversation)\s+to", re.I), "data-exfiltration"),
|
|
45
|
+
(re.compile(r"forward\s+(this|everything)\s+to", re.I), "data-exfiltration"),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
SUSPICIOUS_PATTERNS: list[tuple[re.Pattern, str]] = [
|
|
49
|
+
(re.compile(r"eval\s*\(", re.I), "code-execution"),
|
|
50
|
+
(re.compile(r"exec\s*\(", re.I), "code-execution"),
|
|
51
|
+
(re.compile(r"import\s+(?:os|subprocess)|subprocess\.", re.I), "code-execution"),
|
|
52
|
+
(re.compile(r"[A-Za-z0-9+/]{100,}={0,2}", re.I), "encoded-payload"),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def validate_input(text: str) -> InputValidationResult:
|
|
57
|
+
"""Validate input for prompt injection and safety concerns."""
|
|
58
|
+
import unicodedata
|
|
59
|
+
# Normalize Unicode to defeat homoglyph attacks (e.g., Cyrillic "а" for Latin "a")
|
|
60
|
+
normalized = unicodedata.normalize("NFKC", text)
|
|
61
|
+
detected: list[str] = []
|
|
62
|
+
|
|
63
|
+
for pattern, name in INJECTION_PATTERNS:
|
|
64
|
+
if pattern.search(normalized):
|
|
65
|
+
detected.append(name)
|
|
66
|
+
|
|
67
|
+
if detected:
|
|
68
|
+
unique = list(set(detected))
|
|
69
|
+
return InputValidationResult(
|
|
70
|
+
blocked=True,
|
|
71
|
+
reason=f"Potential prompt injection detected: {', '.join(unique)}",
|
|
72
|
+
detected_patterns=unique,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
suspicious: list[str] = []
|
|
76
|
+
for pattern, name in SUSPICIOUS_PATTERNS:
|
|
77
|
+
if pattern.search(normalized):
|
|
78
|
+
suspicious.append(name)
|
|
79
|
+
|
|
80
|
+
if suspicious:
|
|
81
|
+
return InputValidationResult(
|
|
82
|
+
blocked=False,
|
|
83
|
+
reason=f"Suspicious patterns detected (not blocked): {', '.join(suspicious)}",
|
|
84
|
+
detected_patterns=suspicious,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return InputValidationResult(blocked=False)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def sanitize_input(text: str) -> str:
|
|
91
|
+
"""Remove known dangerous patterns from input."""
|
|
92
|
+
sanitized = re.sub(r"</?system>", "", text, flags=re.I)
|
|
93
|
+
sanitized = re.sub(r"\[INST\]", "", sanitized, flags=re.I)
|
|
94
|
+
sanitized = re.sub(r"<\|im_start\|[^>]*>?", "", sanitized, flags=re.I)
|
|
95
|
+
sanitized = re.sub(r"<\|im_end\|>?", "", sanitized, flags=re.I)
|
|
96
|
+
sanitized = re.sub(r"###\s*(system|instruction|human|assistant)", "", sanitized, flags=re.I)
|
|
97
|
+
sanitized = sanitized.replace("\x00", "")
|
|
98
|
+
return sanitized
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Audit Logger — Structured logging of all AI interactions.
|
|
3
|
+
*
|
|
4
|
+
* Compliance: EU AI Act Art. 12 (logging and traceability),
|
|
5
|
+
* NIST AI RMF Manage 1.3 (monitoring)
|
|
6
|
+
*
|
|
7
|
+
* Every AI call is logged with:
|
|
8
|
+
* - Input preview (truncated, no PII in logs)
|
|
9
|
+
* - Output confidence score
|
|
10
|
+
* - Model version and parameters
|
|
11
|
+
* - Human review decisions
|
|
12
|
+
* - Latency and token usage
|
|
13
|
+
* - Error details
|
|
14
|
+
*
|
|
15
|
+
* Logs are structured JSON for easy ingestion into observability platforms
|
|
16
|
+
* (Datadog, Grafana, ELK, etc.)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface AuditEntry {
|
|
20
|
+
/** Unique interaction ID (matches AIResponse.auditId) */
|
|
21
|
+
id: string;
|
|
22
|
+
/** ISO 8601 timestamp */
|
|
23
|
+
timestamp: string;
|
|
24
|
+
/** Model identifier */
|
|
25
|
+
model: string;
|
|
26
|
+
/** Business purpose of this AI call */
|
|
27
|
+
purpose: string;
|
|
28
|
+
/** Truncated input for traceability (never log full PII) */
|
|
29
|
+
inputPreview: string;
|
|
30
|
+
/** Confidence score (0-1) */
|
|
31
|
+
confidence: number;
|
|
32
|
+
/** Whether human review was triggered */
|
|
33
|
+
needsHumanReview: boolean;
|
|
34
|
+
/** Total latency in milliseconds */
|
|
35
|
+
latencyMs: number;
|
|
36
|
+
/** Token usage for cost tracking */
|
|
37
|
+
tokenUsage?: { inputTokens: number; outputTokens: number };
|
|
38
|
+
/** Whether the call succeeded */
|
|
39
|
+
success: boolean;
|
|
40
|
+
/** Error message if failed */
|
|
41
|
+
error?: string;
|
|
42
|
+
/** Human reviewer action (if reviewed) */
|
|
43
|
+
humanAction?: 'approved' | 'rejected' | 'modified';
|
|
44
|
+
/** Human reviewer ID (if reviewed) */
|
|
45
|
+
humanReviewerId?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface AuditLogConfig {
|
|
49
|
+
/** Where to send logs: 'console' or custom handler */
|
|
50
|
+
destination: 'console' | 'custom';
|
|
51
|
+
/** Custom log handler */
|
|
52
|
+
handler?: (entry: AuditEntry) => void;
|
|
53
|
+
/** Log level filter: only log entries with confidence below this */
|
|
54
|
+
confidenceAlertThreshold?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class AIAuditLog {
|
|
58
|
+
private config: AuditLogConfig;
|
|
59
|
+
private entries: AuditEntry[] = [];
|
|
60
|
+
private maxInMemory = 1000;
|
|
61
|
+
|
|
62
|
+
constructor(config?: Partial<AuditLogConfig>) {
|
|
63
|
+
this.config = {
|
|
64
|
+
destination: config?.destination || 'console',
|
|
65
|
+
handler: config?.handler,
|
|
66
|
+
confidenceAlertThreshold: config?.confidenceAlertThreshold ?? 0.5,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
log(entry: AuditEntry): void {
|
|
71
|
+
// In-memory buffer for health metrics
|
|
72
|
+
this.entries.push(entry);
|
|
73
|
+
if (this.entries.length > this.maxInMemory) {
|
|
74
|
+
this.entries = this.entries.slice(-this.maxInMemory);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Structured log output (exclude inputPreview to avoid PII in logs)
|
|
78
|
+
const { inputPreview: _preview, ...safeEntry } = entry;
|
|
79
|
+
const logEntry = {
|
|
80
|
+
level: entry.success ? 'info' : 'error',
|
|
81
|
+
type: 'ai_interaction',
|
|
82
|
+
...safeEntry,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
switch (this.config.destination) {
|
|
86
|
+
case 'console':
|
|
87
|
+
if (!entry.success || (entry.confidence < (this.config.confidenceAlertThreshold ?? 0.5))) {
|
|
88
|
+
console.warn('[AI_AUDIT]', JSON.stringify(logEntry));
|
|
89
|
+
} else {
|
|
90
|
+
console.log('[AI_AUDIT]', JSON.stringify(logEntry));
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
case 'custom':
|
|
94
|
+
this.config.handler?.(entry);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Record a human review decision against an existing audit entry.
|
|
101
|
+
*/
|
|
102
|
+
recordHumanReview(auditId: string, action: 'approved' | 'rejected' | 'modified', reviewerId?: string): void {
|
|
103
|
+
const entry = this.entries.find(e => e.id === auditId);
|
|
104
|
+
if (!entry) {
|
|
105
|
+
console.warn(`[AI_AUDIT] Cannot record human review: audit entry ${auditId} not found`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
entry.humanAction = action;
|
|
109
|
+
entry.humanReviewerId = reviewerId;
|
|
110
|
+
// Re-emit to log destination without adding duplicate to buffer
|
|
111
|
+
const logEntry = {
|
|
112
|
+
level: 'info',
|
|
113
|
+
type: 'ai_interaction_review',
|
|
114
|
+
...entry,
|
|
115
|
+
};
|
|
116
|
+
if (this.config.destination === 'console') {
|
|
117
|
+
console.log('[AI_AUDIT]', JSON.stringify(logEntry));
|
|
118
|
+
} else if (this.config.destination === 'custom') {
|
|
119
|
+
this.config.handler?.(entry);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get recent entries for monitoring dashboard.
|
|
125
|
+
*/
|
|
126
|
+
getRecentEntries(count = 50): AuditEntry[] {
|
|
127
|
+
return this.entries.slice(-count);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get aggregate stats for AI health reporting.
|
|
132
|
+
*/
|
|
133
|
+
getStats(windowMs = 3600_000): {
|
|
134
|
+
totalCalls: number;
|
|
135
|
+
successRate: number;
|
|
136
|
+
avgConfidence: number;
|
|
137
|
+
avgLatencyMs: number;
|
|
138
|
+
humanReviewRate: number;
|
|
139
|
+
errorRate: number;
|
|
140
|
+
} {
|
|
141
|
+
const cutoff = new Date(Date.now() - windowMs).toISOString();
|
|
142
|
+
const recent = this.entries.filter(e => e.timestamp >= cutoff);
|
|
143
|
+
|
|
144
|
+
if (recent.length === 0) {
|
|
145
|
+
return { totalCalls: 0, successRate: 1, avgConfidence: 0, avgLatencyMs: 0, humanReviewRate: 0, errorRate: 0 };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const successes = recent.filter(e => e.success).length;
|
|
149
|
+
const reviews = recent.filter(e => e.needsHumanReview).length;
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
totalCalls: recent.length,
|
|
153
|
+
successRate: successes / recent.length,
|
|
154
|
+
avgConfidence: recent.reduce((sum, e) => sum + e.confidence, 0) / recent.length,
|
|
155
|
+
avgLatencyMs: recent.reduce((sum, e) => sum + e.latencyMs, 0) / recent.length,
|
|
156
|
+
humanReviewRate: reviews / recent.length,
|
|
157
|
+
errorRate: 1 - (successes / recent.length),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// --- Singleton ---
|
|
163
|
+
|
|
164
|
+
export const aiAuditLog = new AIAuditLog();
|