forgedev 1.2.0 → 1.3.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.
Files changed (171) hide show
  1. package/README.md +57 -10
  2. package/bin/chainproof.js +126 -0
  3. package/package.json +25 -7
  4. package/src/chainproof-bridge.js +330 -0
  5. package/src/ci-mode.js +85 -0
  6. package/src/claude-configurator.js +86 -49
  7. package/src/cli.js +30 -7
  8. package/src/composer.js +159 -34
  9. package/src/doctor-checks-chainproof.js +106 -0
  10. package/src/doctor-checks.js +39 -20
  11. package/src/doctor-prompts.js +9 -9
  12. package/src/doctor.js +37 -4
  13. package/src/guided.js +3 -3
  14. package/src/index.js +31 -10
  15. package/src/init-mode.js +64 -11
  16. package/src/menu.js +178 -0
  17. package/src/prompts.js +5 -12
  18. package/src/recommender.js +134 -10
  19. package/src/scanner.js +57 -2
  20. package/src/uat-generator.js +204 -189
  21. package/src/update-check.js +9 -4
  22. package/src/update.js +1 -1
  23. package/src/utils.js +64 -5
  24. package/templates/ai/guardrails-py/backend/app/ai/__init__.py +29 -0
  25. package/templates/ai/guardrails-py/backend/app/ai/audit_log.py +133 -0
  26. package/templates/ai/guardrails-py/backend/app/ai/client.py.template +323 -0
  27. package/templates/ai/guardrails-py/backend/app/ai/health.py.template +157 -0
  28. package/templates/ai/guardrails-py/backend/app/ai/input_guard.py +98 -0
  29. package/templates/ai/guardrails-ts/src/lib/ai/audit-log.ts.template +164 -0
  30. package/templates/ai/guardrails-ts/src/lib/ai/client.ts.template +403 -0
  31. package/templates/ai/guardrails-ts/src/lib/ai/health.ts.template +165 -0
  32. package/templates/ai/guardrails-ts/src/lib/ai/index.ts.template +17 -0
  33. package/templates/ai/guardrails-ts/src/lib/ai/input-guard.ts.template +124 -0
  34. package/templates/auth/nextauth/src/lib/auth.ts.template +12 -7
  35. package/templates/backend/express/Dockerfile.template +18 -0
  36. package/templates/backend/express/package.json.template +33 -0
  37. package/templates/backend/express/src/index.ts.template +34 -0
  38. package/templates/backend/express/src/routes/health.ts.template +27 -0
  39. package/templates/backend/express/tsconfig.json +17 -0
  40. package/templates/backend/fastapi/backend/Dockerfile.template +5 -0
  41. package/templates/backend/fastapi/backend/app/api/health.py.template +1 -1
  42. package/templates/backend/fastapi/backend/app/core/config.py.template +1 -1
  43. package/templates/backend/fastapi/backend/app/core/errors.py +1 -1
  44. package/templates/backend/fastapi/backend/app/main.py.template +3 -1
  45. package/templates/backend/fastapi/backend/requirements.txt.template +2 -0
  46. package/templates/backend/hono/Dockerfile.template +18 -0
  47. package/templates/backend/hono/package.json.template +31 -0
  48. package/templates/backend/hono/src/index.ts.template +32 -0
  49. package/templates/backend/hono/src/routes/health.ts.template +27 -0
  50. package/templates/backend/hono/tsconfig.json +18 -0
  51. package/templates/base/docs/uat/UAT_TEMPLATE.md.template +1 -1
  52. package/templates/chainproof/base/.chainproof/config.json.template +11 -0
  53. package/templates/chainproof/base/.chainproof/mcp-server.mjs +310 -0
  54. package/templates/chainproof/base/.mcp.json +9 -0
  55. package/templates/chainproof/fastapi/.chainproof/middleware.json.template +14 -0
  56. package/templates/chainproof/nextjs/.chainproof/hooks.json.template +19 -0
  57. package/templates/chainproof/polyglot/.chainproof/config.json.template +21 -0
  58. package/templates/claude-code/agents/architect.md +25 -11
  59. package/templates/claude-code/agents/build-error-resolver.md +19 -5
  60. package/templates/claude-code/agents/chief-of-staff.md +42 -8
  61. package/templates/claude-code/agents/code-quality-reviewer.md +14 -0
  62. package/templates/claude-code/agents/database-reviewer.md +15 -1
  63. package/templates/claude-code/agents/deep-reviewer.md +191 -0
  64. package/templates/claude-code/agents/doc-updater.md +19 -5
  65. package/templates/claude-code/agents/docs-lookup.md +19 -5
  66. package/templates/claude-code/agents/e2e-runner.md +26 -12
  67. package/templates/claude-code/agents/enforcement-gate.md +102 -0
  68. package/templates/claude-code/agents/frontend-builder.md +188 -0
  69. package/templates/claude-code/agents/harness-optimizer.md +36 -1
  70. package/templates/claude-code/agents/loop-operator.md +27 -13
  71. package/templates/claude-code/agents/planner.md +21 -7
  72. package/templates/claude-code/agents/product-strategist.md +24 -10
  73. package/templates/claude-code/agents/production-readiness.md +14 -0
  74. package/templates/claude-code/agents/prompt-auditor.md +115 -0
  75. package/templates/claude-code/agents/refactor-cleaner.md +22 -8
  76. package/templates/claude-code/agents/security-reviewer.md +14 -0
  77. package/templates/claude-code/agents/spec-validator.md +15 -1
  78. package/templates/claude-code/agents/tdd-guide.md +21 -7
  79. package/templates/claude-code/agents/uat-validator.md +14 -0
  80. package/templates/claude-code/claude-md/base.md +14 -7
  81. package/templates/claude-code/claude-md/fastapi.md +8 -8
  82. package/templates/claude-code/claude-md/fullstack.md +6 -6
  83. package/templates/claude-code/claude-md/hono.md +18 -0
  84. package/templates/claude-code/claude-md/nextjs.md +5 -5
  85. package/templates/claude-code/claude-md/remix.md +18 -0
  86. package/templates/claude-code/commands/audit-security.md +14 -0
  87. package/templates/claude-code/commands/audit-spec.md +14 -0
  88. package/templates/claude-code/commands/audit-wiring.md +14 -0
  89. package/templates/claude-code/commands/build-fix.md +28 -0
  90. package/templates/claude-code/commands/build-ui.md +59 -0
  91. package/templates/claude-code/commands/code-review.md +53 -31
  92. package/templates/claude-code/commands/fix-loop.md +211 -0
  93. package/templates/claude-code/commands/full-audit.md +36 -8
  94. package/templates/claude-code/commands/generate-prd.md +1 -1
  95. package/templates/claude-code/commands/generate-sdd.md +74 -0
  96. package/templates/claude-code/commands/generate-uat.md +107 -35
  97. package/templates/claude-code/commands/help.md +68 -0
  98. package/templates/claude-code/commands/live-uat.md +268 -0
  99. package/templates/claude-code/commands/optimize-claude-md.md +15 -1
  100. package/templates/claude-code/commands/plan.md +3 -3
  101. package/templates/claude-code/commands/pre-pr.md +57 -19
  102. package/templates/claude-code/commands/product-strategist.md +21 -0
  103. package/templates/claude-code/commands/resume-session.md +10 -10
  104. package/templates/claude-code/commands/run-uat.md +59 -2
  105. package/templates/claude-code/commands/save-session.md +10 -10
  106. package/templates/claude-code/commands/simplify.md +36 -0
  107. package/templates/claude-code/commands/tdd.md +17 -18
  108. package/templates/claude-code/commands/verify-all.md +24 -0
  109. package/templates/claude-code/commands/verify-intent.md +55 -0
  110. package/templates/claude-code/commands/workflows.md +52 -40
  111. package/templates/claude-code/hooks/polyglot.json +10 -1
  112. package/templates/claude-code/hooks/python.json +10 -1
  113. package/templates/claude-code/hooks/scripts/autofix-polyglot.mjs +2 -2
  114. package/templates/claude-code/hooks/scripts/autofix-python.mjs +1 -1
  115. package/templates/claude-code/hooks/scripts/autofix-typescript.mjs +1 -1
  116. package/templates/claude-code/hooks/scripts/code-hygiene.mjs +293 -0
  117. package/templates/claude-code/hooks/scripts/pre-commit-gate.mjs +207 -0
  118. package/templates/claude-code/hooks/typescript.json +10 -1
  119. package/templates/claude-code/skills/ai-prompts/SKILL.md +119 -41
  120. package/templates/claude-code/skills/git-workflow/SKILL.md +5 -5
  121. package/templates/claude-code/skills/nextjs/SKILL.md +1 -1
  122. package/templates/claude-code/skills/playwright/SKILL.md +5 -5
  123. package/templates/claude-code/skills/security-api/SKILL.md +1 -1
  124. package/templates/claude-code/skills/security-web/SKILL.md +1 -1
  125. package/templates/claude-code/skills/testing-patterns/SKILL.md +9 -9
  126. package/templates/database/prisma-postgres/{.env.example → .env.example.template} +1 -0
  127. package/templates/database/sqlalchemy-postgres/{.env.example → .env.example.template} +1 -0
  128. package/templates/docs-portal/fastapi/backend/app/portal/__pycache__/docs_reader.cpython-314.pyc +0 -0
  129. package/templates/docs-portal/fastapi/backend/app/portal/docs_reader.py +201 -0
  130. package/templates/docs-portal/fastapi/backend/app/portal/html_renderer.py +229 -0
  131. package/templates/docs-portal/fastapi/backend/app/portal/router.py.template +35 -0
  132. package/templates/docs-portal/nextjs/src/app/portal/[category]/[slug]/page.tsx +81 -0
  133. package/templates/docs-portal/nextjs/src/app/portal/[category]/page.tsx +65 -0
  134. package/templates/docs-portal/nextjs/src/app/portal/layout.tsx.template +54 -0
  135. package/templates/docs-portal/nextjs/src/app/portal/page.tsx +85 -0
  136. package/templates/docs-portal/nextjs/src/components/portal/markdown-renderer.tsx +101 -0
  137. package/templates/docs-portal/nextjs/src/components/portal/mobile-portal-nav.tsx +81 -0
  138. package/templates/docs-portal/nextjs/src/components/portal/portal-nav.tsx +86 -0
  139. package/templates/docs-portal/nextjs/src/lib/docs.ts +139 -0
  140. package/templates/frontend/nextjs/package.json.template +3 -1
  141. package/templates/frontend/react/index.html.template +12 -0
  142. package/templates/frontend/react/package.json.template +34 -0
  143. package/templates/frontend/react/src/App.tsx.template +10 -0
  144. package/templates/frontend/react/src/index.css +1 -0
  145. package/templates/frontend/react/src/main.tsx +10 -0
  146. package/templates/frontend/react/tsconfig.json +17 -0
  147. package/templates/frontend/react/vite.config.ts.template +15 -0
  148. package/templates/frontend/react/vitest.config.ts +9 -0
  149. package/templates/frontend/remix/app/root.tsx.template +31 -0
  150. package/templates/frontend/remix/app/routes/_index.tsx.template +19 -0
  151. package/templates/frontend/remix/app/routes/api.health.ts.template +10 -0
  152. package/templates/frontend/remix/app/tailwind.css +1 -0
  153. package/templates/frontend/remix/package.json.template +39 -0
  154. package/templates/frontend/remix/tsconfig.json +18 -0
  155. package/templates/frontend/remix/vite.config.ts.template +7 -0
  156. package/templates/infra/github-actions/.github/workflows/ci.yml.template +3 -0
  157. package/docs/00-README.md +0 -310
  158. package/docs/01-universal-prompt-library.md +0 -1049
  159. package/docs/02-claude-code-mastery-playbook.md +0 -283
  160. package/docs/03-multi-agent-verification.md +0 -565
  161. package/docs/04-errata-and-verification-checklist.md +0 -284
  162. package/docs/05-universal-scaffolder-vision.md +0 -452
  163. package/docs/06-confidence-assessment-and-repo-prompt.md +0 -407
  164. package/docs/errata.md +0 -58
  165. package/docs/multi-agent-verification.md +0 -66
  166. package/docs/playbook.md +0 -95
  167. package/docs/prompt-library.md +0 -160
  168. package/docs/uat/UAT_CHECKLIST.csv +0 -9
  169. package/docs/uat/UAT_TEMPLATE.md +0 -163
  170. package/templates/claude-code/commands/done.md +0 -19
  171. /package/{docs/plans/.gitkeep → templates/docs-portal/fastapi/backend/app/portal/__init__.py} +0 -0
@@ -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();
@@ -0,0 +1,403 @@
1
+ /**
2
+ * AI Client — Central wrapper for all LLM interactions.
3
+ *
4
+ * Every AI call goes through this client, which provides:
5
+ * - Input validation and prompt injection detection
6
+ * - Output validation against Zod schemas
7
+ * - Confidence scoring with human review routing
8
+ * - Structured audit logging (EU AI Act Art. 12)
9
+ * - AI disclosure headers (EU AI Act Art. 50)
10
+ * - Health metrics collection (NIST AI RMF Manage 3.2)
11
+ *
12
+ * Compliance: EU AI Act (2024/1689), NIST AI RMF 1.0
13
+ */
14
+
15
+ import Anthropic from '@anthropic-ai/sdk';
16
+ import { z, type ZodSchema } from 'zod';
17
+ import { aiAuditLog, type AuditEntry } from './audit-log.js';
18
+ import { validateInput, type InputValidationResult } from './input-guard.js';
19
+ import { aiHealthMetrics } from './health.js';
20
+
21
+ // --- Configuration ---
22
+
23
+ export interface AIClientConfig {
24
+ /** Anthropic API key (defaults to ANTHROPIC_API_KEY env var) */
25
+ apiKey?: string;
26
+ /** Default model to use */
27
+ model?: string;
28
+ /** Confidence threshold below which human review is required (0-1) */
29
+ confidenceThreshold?: number;
30
+ /** Maximum input length in characters */
31
+ maxInputLength?: number;
32
+ /** Enable prompt injection detection */
33
+ detectInjection?: boolean;
34
+ /** Enable structured audit logging */
35
+ auditLog?: boolean;
36
+ /** Custom moderation function (return true to block) */
37
+ moderator?: (input: string) => Promise<boolean>;
38
+ }
39
+
40
+ const DEFAULT_CONFIG: Required<AIClientConfig> = {
41
+ apiKey: process.env.ANTHROPIC_API_KEY || '',
42
+ model: 'claude-sonnet-4-20250514',
43
+ confidenceThreshold: 0.7,
44
+ maxInputLength: 100_000,
45
+ detectInjection: true,
46
+ auditLog: true,
47
+ moderator: async () => false,
48
+ };
49
+
50
+ // --- Core Client ---
51
+
52
+ export class AIClient {
53
+ private client: Anthropic;
54
+ private config: Required<AIClientConfig>;
55
+
56
+ constructor(config: AIClientConfig = {}) {
57
+ this.config = { ...DEFAULT_CONFIG, ...config };
58
+ if (!this.config.apiKey) {
59
+ throw new Error('ANTHROPIC_API_KEY environment variable is required');
60
+ }
61
+ this.client = new Anthropic({ apiKey: this.config.apiKey });
62
+ }
63
+
64
+ /**
65
+ * Generate a structured response validated against a Zod schema.
66
+ *
67
+ * This is the primary method for AI interactions. It:
68
+ * 1. Validates and sanitizes input
69
+ * 2. Calls the model
70
+ * 3. Parses and validates output against the schema
71
+ * 4. Scores confidence
72
+ * 5. Logs the interaction for audit
73
+ * 6. Routes to human review if confidence is low
74
+ */
75
+ async generate<T>(options: {
76
+ prompt: string;
77
+ schema: ZodSchema<T>;
78
+ systemPrompt?: string;
79
+ context?: string;
80
+ /** Override model for this call */
81
+ model?: string;
82
+ /** Override confidence threshold for this call */
83
+ confidenceThreshold?: number;
84
+ /** Max retries on validation failure */
85
+ maxRetries?: number;
86
+ /** Purpose tag for audit log */
87
+ purpose?: string;
88
+ }): Promise<AIResponse<T>> {
89
+ const startTime = Date.now();
90
+ const model = options.model || this.config.model;
91
+ const threshold = options.confidenceThreshold ?? this.config.confidenceThreshold;
92
+ const maxRetries = options.maxRetries ?? 2;
93
+
94
+ // Step 1: Input validation
95
+ const inputValidation = await this.validateInputs(options.prompt, options.context);
96
+ if (inputValidation.blocked) {
97
+ const result = this.buildBlockedResponse<T>(inputValidation, startTime, model, options.purpose);
98
+ return result;
99
+ }
100
+
101
+ // Step 2: Call model with retries on parse failure
102
+ let lastError: Error | null = null;
103
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
104
+ try {
105
+ const response = await this.callModel(
106
+ options.prompt,
107
+ options.systemPrompt,
108
+ options.context,
109
+ model,
110
+ options.schema,
111
+ );
112
+
113
+ // Step 3: Parse and validate output
114
+ const parsed = options.schema.safeParse(response.content);
115
+ if (!parsed.success) {
116
+ lastError = new Error(`Output validation failed: ${parsed.error.message}`);
117
+ if (attempt < maxRetries) continue;
118
+ break;
119
+ }
120
+
121
+ // Step 4: Score confidence
122
+ const confidence = this.scoreConfidence(response);
123
+ const needsReview = confidence < threshold;
124
+
125
+ // Step 5: Build response
126
+ const result: AISuccessResponse<T> = {
127
+ success: true,
128
+ data: parsed.data,
129
+ confidence,
130
+ needsHumanReview: needsReview,
131
+ model,
132
+ latencyMs: Date.now() - startTime,
133
+ tokenUsage: response.usage,
134
+ aiGenerated: true,
135
+ auditId: crypto.randomUUID(),
136
+ };
137
+
138
+ // Step 6: Audit log
139
+ if (this.config.auditLog) {
140
+ this.logInteraction(result, options.prompt, options.purpose);
141
+ }
142
+
143
+ // Step 7: Health metrics
144
+ aiHealthMetrics.recordCall({
145
+ model,
146
+ latencyMs: result.latencyMs,
147
+ confidence,
148
+ success: true,
149
+ tokenUsage: response.usage,
150
+ });
151
+
152
+ return result;
153
+
154
+ } catch (err) {
155
+ lastError = err instanceof Error ? err : new Error(String(err));
156
+ if (attempt < maxRetries) continue;
157
+ }
158
+ }
159
+
160
+ // All retries exhausted
161
+ aiHealthMetrics.recordCall({
162
+ model,
163
+ latencyMs: Date.now() - startTime,
164
+ confidence: 0,
165
+ success: false,
166
+ });
167
+
168
+ return {
169
+ success: false,
170
+ model,
171
+ latencyMs: Date.now() - startTime,
172
+ aiGenerated: true,
173
+ auditId: crypto.randomUUID(),
174
+ error: lastError?.message || 'AI call failed after retries',
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Simple text generation without schema validation.
180
+ * Still applies input guards, audit logging, and confidence scoring.
181
+ */
182
+ async generateText(options: {
183
+ prompt: string;
184
+ systemPrompt?: string;
185
+ context?: string;
186
+ model?: string;
187
+ purpose?: string;
188
+ }): Promise<AIResponse<string>> {
189
+ return this.generate({
190
+ ...options,
191
+ schema: z.any().transform(String),
192
+ });
193
+ }
194
+
195
+ // --- Private Methods ---
196
+
197
+ private async validateInputs(
198
+ prompt: string,
199
+ context?: string,
200
+ ): Promise<InputValidationResult> {
201
+ const fullInput = context ? `${prompt}\n${context}` : prompt;
202
+
203
+ if (fullInput.length > this.config.maxInputLength) {
204
+ return { blocked: true, reason: `Input exceeds maximum length (${this.config.maxInputLength} chars)` };
205
+ }
206
+
207
+ if (this.config.detectInjection) {
208
+ const result = validateInput(fullInput);
209
+ if (result.blocked) return result;
210
+ }
211
+
212
+ if (this.config.moderator) {
213
+ const blocked = await this.config.moderator(fullInput);
214
+ if (blocked) return { blocked: true, reason: 'Content blocked by moderation policy' };
215
+ }
216
+
217
+ return { blocked: false };
218
+ }
219
+
220
+ private async callModel(
221
+ prompt: string,
222
+ systemPrompt: string | undefined,
223
+ context: string | undefined,
224
+ model: string,
225
+ schema: ZodSchema,
226
+ ) {
227
+ const userContent = context
228
+ ? `${prompt}\n\nContext:\n${context}`
229
+ : prompt;
230
+
231
+ const messages: Anthropic.MessageParam[] = [
232
+ { role: 'user', content: userContent },
233
+ ];
234
+
235
+ const response = await this.client.messages.create({
236
+ model,
237
+ max_tokens: 4096,
238
+ system: systemPrompt || `You are an AI assistant for {{PROJECT_NAME}}. Respond with valid JSON matching the requested schema. Be precise and factual.`,
239
+ messages,
240
+ });
241
+
242
+ const textBlock = response.content.find(
243
+ (block): block is Anthropic.TextBlock => block.type === 'text'
244
+ );
245
+
246
+ return {
247
+ content: this.extractJSON(textBlock?.text || ''),
248
+ usage: {
249
+ inputTokens: response.usage.input_tokens,
250
+ outputTokens: response.usage.output_tokens,
251
+ },
252
+ stopReason: response.stop_reason,
253
+ };
254
+ }
255
+
256
+ private extractJSON(text: string): unknown {
257
+ // Try direct parse first
258
+ try {
259
+ return JSON.parse(text);
260
+ } catch {
261
+ // Extract JSON from markdown code blocks
262
+ const jsonMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
263
+ if (jsonMatch) {
264
+ try {
265
+ return JSON.parse(jsonMatch[1].trim());
266
+ } catch {
267
+ // Fall through to return raw text
268
+ }
269
+ }
270
+ // Return raw text for string schema
271
+ return text;
272
+ }
273
+ }
274
+
275
+ private scoreConfidence(response: {
276
+ stopReason: string | null;
277
+ usage: { inputTokens: number; outputTokens: number };
278
+ }): number {
279
+ let score = 0.85; // Base confidence
280
+
281
+ // Penalize if model was cut off
282
+ if (response.stopReason === 'max_tokens') {
283
+ score -= 0.3;
284
+ }
285
+
286
+ // Penalize very short responses (likely incomplete)
287
+ if (response.usage.outputTokens < 10) {
288
+ score -= 0.2;
289
+ }
290
+
291
+ // Penalize very long responses (may indicate hallucination loops)
292
+ if (response.usage.outputTokens > 3000) {
293
+ score -= 0.1;
294
+ }
295
+
296
+ return Math.max(0, Math.min(1, score));
297
+ }
298
+
299
+ private logInteraction<T>(
300
+ result: AISuccessResponse<T>,
301
+ prompt: string,
302
+ purpose?: string,
303
+ ): void {
304
+ const entry: AuditEntry = {
305
+ id: result.auditId,
306
+ timestamp: new Date().toISOString(),
307
+ model: result.model,
308
+ purpose: purpose || 'unspecified',
309
+ inputPreview: prompt.slice(0, 100) + (prompt.length > 100 ? '...' : ''),
310
+ confidence: result.confidence,
311
+ needsHumanReview: result.needsHumanReview,
312
+ latencyMs: result.latencyMs,
313
+ tokenUsage: result.tokenUsage,
314
+ success: true,
315
+ };
316
+ aiAuditLog.log(entry);
317
+ }
318
+
319
+ private buildBlockedResponse<T>(
320
+ validation: InputValidationResult,
321
+ startTime: number,
322
+ model: string,
323
+ purpose?: string,
324
+ ): AIErrorResponse {
325
+ const result: AIErrorResponse = {
326
+ success: false,
327
+ model,
328
+ latencyMs: Date.now() - startTime,
329
+ aiGenerated: false,
330
+ auditId: crypto.randomUUID(),
331
+ error: `Input blocked: ${validation.reason}`,
332
+ blocked: true,
333
+ };
334
+
335
+ if (this.config.auditLog) {
336
+ aiAuditLog.log({
337
+ id: result.auditId,
338
+ timestamp: new Date().toISOString(),
339
+ model,
340
+ purpose: purpose || 'unspecified',
341
+ inputPreview: '[BLOCKED]',
342
+ confidence: 0,
343
+ needsHumanReview: false,
344
+ latencyMs: result.latencyMs,
345
+ success: false,
346
+ error: validation.reason,
347
+ });
348
+ }
349
+
350
+ return result;
351
+ }
352
+ }
353
+
354
+ // --- Types ---
355
+
356
+ interface AIResponseBase {
357
+ /** Model used for this call */
358
+ model: string;
359
+ /** Total latency in milliseconds */
360
+ latencyMs: number;
361
+ /** EU AI Act Art. 50: flag indicating AI-generated content */
362
+ aiGenerated: boolean;
363
+ /** Unique ID for audit trail */
364
+ auditId: string;
365
+ }
366
+
367
+ export interface AISuccessResponse<T> extends AIResponseBase {
368
+ success: true;
369
+ /** The validated, typed response data */
370
+ data: T;
371
+ /** Confidence score (0-1). Below threshold triggers human review */
372
+ confidence: number;
373
+ /** Whether this response needs human review before acting on it */
374
+ needsHumanReview: boolean;
375
+ /** Token usage for cost tracking */
376
+ tokenUsage?: { inputTokens: number; outputTokens: number };
377
+ }
378
+
379
+ export interface AIErrorResponse extends AIResponseBase {
380
+ success: false;
381
+ /** Error message describing the failure */
382
+ error: string;
383
+ /** Whether input was blocked by guards */
384
+ blocked?: boolean;
385
+ }
386
+
387
+ export type AIResponse<T> = AISuccessResponse<T> | AIErrorResponse;
388
+
389
+ // --- Singleton ---
390
+
391
+ let _defaultClient: AIClient | null = null;
392
+
393
+ export function getAIClient(config?: AIClientConfig): AIClient {
394
+ if (!_defaultClient || config) {
395
+ if (_defaultClient && config) {
396
+ console.warn('AIClient already initialized. Ignoring new config. Use new AIClient(config) for custom instances.');
397
+ }
398
+ if (!_defaultClient) {
399
+ _defaultClient = new AIClient(config);
400
+ }
401
+ }
402
+ return _defaultClient;
403
+ }