SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
import type { AiProvider , Prompt } from "@workspace/db" ;
import type { ParsedFile , RawFinding , Severity , Axis } from "./ruleCatalog" ;
const SEVERITIES : Severity [ ] = [ "critical" , "high" , "medium" , "low" , "info" ] ;
const AXES : Axis [ ] = [ "security" , "privacy" ] ;
2026-06-10 13:56:15 +00:00
export type AiRuleConfig = {
ruleId : string ;
title : string ;
description : string ;
axis : Axis ;
severity : Severity ;
} ;
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
export type AiResult = {
findings : RawFinding [ ] ;
error : string | null ;
} ;
const FETCH_TIMEOUT_MS = 60000 ;
function redactSecrets ( text : string , token : string | null | undefined ) : string {
let out = text ;
if ( token && token . length >= 4 ) {
out = out . split ( token ) . join ( "[REDACTED]" ) ;
}
out = out . replace ( /(Bearer\s+)[A-Za-z0-9._\-]+/gi , "$1[REDACTED]" ) ;
out = out . replace ( /\bsk-[A-Za-z0-9._\-]{8,}\b/g , "[REDACTED]" ) ;
return out ;
}
async function fetchWithTimeout (
url : string ,
init : RequestInit ,
) : Promise < Response > {
const controller = new AbortController ( ) ;
const timer = setTimeout ( ( ) = > controller . abort ( ) , FETCH_TIMEOUT_MS ) ;
try {
return await fetch ( url , { . . . init , signal : controller.signal } ) ;
} finally {
clearTimeout ( timer ) ;
}
}
function buildSkillPayload ( files : ParsedFile [ ] ) : string {
const parts : string [ ] = [ ] ;
let budget = 60000 ;
for ( const f of files ) {
if ( f . content === "" ) continue ;
const header = ` \ n===== DATEI: ${ f . path } ( ${ f . kind } ) ===== \ n ` ;
const body = f . content . slice ( 0 , 16000 ) ;
if ( header . length + body . length > budget ) {
parts . push ( header + body . slice ( 0 , Math . max ( 0 , budget - header . length ) ) ) ;
break ;
}
parts . push ( header + body ) ;
budget -= header . length + body . length ;
}
return parts . join ( "\n" ) ;
}
2026-06-10 13:56:15 +00:00
function coerceFinding (
raw : unknown ,
allowed : Map < string , AiRuleConfig > ,
) : RawFinding | null {
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
if ( ! raw || typeof raw !== "object" ) return null ;
const o = raw as Record < string , unknown > ;
const title = typeof o . title === "string" ? o . title . slice ( 0 , 200 ) : null ;
if ( ! title ) return null ;
2026-06-10 13:56:15 +00:00
const rule = typeof o . ruleId === "string" ? allowed . get ( o . ruleId ) : undefined ;
if ( ! rule ) {
return null ;
}
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
return {
2026-06-10 13:56:15 +00:00
ruleId : rule.ruleId ,
axis : rule.axis ,
severity : rule.severity ,
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
title ,
description :
typeof o . description === "string" ? o . description . slice ( 0 , 2000 ) : title ,
remediation :
typeof o . remediation === "string" ? o . remediation . slice ( 0 , 2000 ) : null ,
file : typeof o . file === "string" ? o . file . slice ( 0 , 400 ) : null ,
line : typeof o . line === "number" ? o.line : null ,
snippet : typeof o . snippet === "string" ? o . snippet . slice ( 0 , 400 ) : null ,
detectedBy : "ai" ,
} ;
}
function extractJson ( text : string ) : unknown {
const fence = text . match ( /```(?:json)?\s*([\s\S]*?)```/i ) ;
const candidate = fence ? fence [ 1 ] : text ;
const start = candidate . indexOf ( "{" ) ;
const end = candidate . lastIndexOf ( "}" ) ;
if ( start === - 1 || end === - 1 || end <= start ) {
throw new Error ( "Keine JSON-Antwort von der KI erhalten." ) ;
}
return JSON . parse ( candidate . slice ( start , end + 1 ) ) ;
}
async function callOpenAiCompatible (
provider : AiProvider ,
system : string ,
user : string ,
) : Promise < string > {
const base = provider . baseUrl . replace ( /\/$/ , "" ) ;
const url = ` ${ base } /chat/completions ` ;
const res = await fetchWithTimeout ( url , {
method : "POST" ,
headers : {
"Content-Type" : "application/json" ,
Authorization : ` Bearer ${ provider . apiToken ? ? "" } ` ,
} ,
body : JSON.stringify ( {
model : provider.model ,
messages : [
{ role : "system" , content : system } ,
{ role : "user" , content : user } ,
] ,
} ) ,
} ) ;
if ( ! res . ok ) {
const body = await res . text ( ) ;
throw new Error (
` HTTP ${ res . status } : ${ redactSecrets ( body . slice ( 0 , 300 ) , provider . apiToken ) } ` ,
) ;
}
const data = ( await res . json ( ) ) as {
choices ? : { message ? : { content? : string } } [ ] ;
} ;
return data . choices ? . [ 0 ] ? . message ? . content ? ? "" ;
}
async function callAnthropic (
provider : AiProvider ,
system : string ,
user : string ,
) : Promise < string > {
const base = provider . baseUrl . replace ( /\/$/ , "" ) ;
const url = ` ${ base } /messages ` ;
const res = await fetchWithTimeout ( url , {
method : "POST" ,
headers : {
"Content-Type" : "application/json" ,
"x-api-key" : provider . apiToken ? ? "" ,
"anthropic-version" : "2023-06-01" ,
} ,
body : JSON.stringify ( {
model : provider.model ,
max_tokens : 4096 ,
system ,
messages : [ { role : "user" , content : user } ] ,
} ) ,
} ) ;
if ( ! res . ok ) {
const body = await res . text ( ) ;
throw new Error (
` HTTP ${ res . status } : ${ redactSecrets ( body . slice ( 0 , 300 ) , provider . apiToken ) } ` ,
) ;
}
const data = ( await res . json ( ) ) as { content ? : { text? : string } [ ] } ;
return data . content ? . [ 0 ] ? . text ? ? "" ;
}
export async function callProvider (
provider : AiProvider ,
system : string ,
user : string ,
) : Promise < string > {
if ( provider . apiType === "anthropic" ) {
return callAnthropic ( provider , system , user ) ;
}
return callOpenAiCompatible ( provider , system , user ) ;
}
Guided AI provider setup with model discovery
Task: Replace free-text model entry in Admin → Providers with a guided
flow (Name → API type → API endpoint → API token → Test connection) that
auto-discovers available models after a successful connection test and
presents them in a Select positioned right after the API endpoint field.
Model-independent connection test (key fix):
- The setup connection test no longer requires a model, removing the
chicken-and-egg where discovery could never run. test-connection's model
is now optional: when a model is supplied it does a full chat round-trip;
when omitted it verifies credentials via the provider's models endpoint and
reports how many models are available. The form sends no model on the
initial test, so a successful test now reliably triggers discovery.
Backend:
- aiAnalysis.ts: added listProviderModels(provider) — GETs {baseUrl}/models
using Bearer auth for openai/custom and x-api-key + anthropic-version for
anthropic. Normalizes data[].id (falls back to models[].id/.name),
dedupes + sorts, and redacts secrets in error messages via the existing
redactSecrets helper.
- providers.ts: added POST /providers/list-models accepting ad-hoc config
(apiType, baseUrl, optional apiToken, optional providerId). Falls back to
the stored token by providerId when token omitted; returns { ok, models,
message } and never leaks the token.
API contract:
- openapi.yaml: added /providers/list-models path, ProviderListModelsInput
and ProviderModelsResult schemas. Regenerated zod + react-query client via
the api-spec codegen workflow (orval).
Admin UI (admin.tsx):
- New ModelField component renders a loading state, a Select when models are
discovered, or a manual free-text input fallback (with hint) when discovery
returns nothing — so saving always works for custom endpoints.
- Field order follows the guided flow: Name → API type → API endpoint →
API token → Test connection, with the model selector appearing after the
token once discovery succeeds. A successful test automatically triggers
discovery; editing endpoint or token resets discovery state.
Verified: workspace typecheck passes, api-server tests 59/59 pass, live curl
of the new endpoint returns graceful errors without leaking the token.
Replit-Task-Id: 8d300a47-0b45-4677-9e9e-aa041bf03e98
2026-06-10 21:13:35 +00:00
export async function listProviderModels (
provider : AiProvider ,
) : Promise < string [ ] > {
const base = provider . baseUrl . replace ( /\/$/ , "" ) ;
const url = ` ${ base } /models ` ;
const headers : Record < string , string > =
provider . apiType === "anthropic"
? {
"x-api-key" : provider . apiToken ? ? "" ,
"anthropic-version" : "2023-06-01" ,
}
: {
Authorization : ` Bearer ${ provider . apiToken ? ? "" } ` ,
} ;
const res = await fetchWithTimeout ( url , { method : "GET" , headers } ) ;
if ( ! res . ok ) {
const body = await res . text ( ) ;
throw new Error (
` HTTP ${ res . status } : ${ redactSecrets ( body . slice ( 0 , 300 ) , provider . apiToken ) } ` ,
) ;
}
const data = ( await res . json ( ) ) as {
data ? : { id? : unknown } [ ] ;
models ? : { id? : unknown ; name? : unknown } [ ] ;
} ;
const rows = Array . isArray ( data . data )
? data . data
: Array . isArray ( data . models )
? data . models
: [ ] ;
const ids = rows
. map ( ( m ) = >
typeof m . id === "string"
? m . id
: typeof ( m as { name? : unknown } ) . name === "string"
? ( ( m as { name : string } ) . name )
: null ,
)
. filter ( ( id ) : id is string = > ! ! id ) ;
return Array . from ( new Set ( ids ) ) . sort ( ( a , b ) = > a . localeCompare ( b ) ) ;
}
2026-06-10 13:56:15 +00:00
function buildRuleMenu ( aiRules : AiRuleConfig [ ] ) : string {
const lines = aiRules . map (
( r ) = > ` - ${ r . ruleId } ( ${ r . axis } ): ${ r . title } — ${ r . description } ` ,
) ;
return [
"" ,
"Ordne jeden Befund GENAU EINER der folgenden aktiven Kategorien zu und gib deren Kennung im Pflichtfeld \"ruleId\" zurück. Verwende ausschließlich diese Kennungen:" ,
. . . lines ,
'Befunde, die zu keiner dieser Kategorien passen, lasse weg. Das Feld "severity" wird serverseitig festgelegt und kann von dir ignoriert werden.' ,
] . join ( "\n" ) ;
}
2026-06-10 21:13:51 +00:00
export async function generateSkillDescription (
provider : AiProvider ,
prompts : Prompt [ ] ,
files : ParsedFile [ ] ,
) : Promise < string | null > {
const descriptionPrompt =
prompts . find ( ( p ) = > p . key === "description" ) ? . content ? ? "" ;
if ( ! descriptionPrompt ) return null ;
const systemPrompt = prompts . find ( ( p ) = > p . key === "system" ) ? . content ? ? "" ;
const payload = buildSkillPayload ( files ) ;
const user = ` ${ descriptionPrompt } \ n \ nHier ist das zu beschreibende Skill: \ n ${ payload } ` ;
try {
const content = await callProvider ( provider , systemPrompt , user ) ;
const parsed = extractJson ( content ) as { description? : unknown } ;
const description =
typeof parsed . description === "string" ? parsed . description . trim ( ) : "" ;
return description ? description . slice ( 0 , 2000 ) : null ;
} catch {
return null ;
}
}
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
export async function runAiAnalysis (
provider : AiProvider ,
prompts : Prompt [ ] ,
files : ParsedFile [ ] ,
2026-06-10 13:56:15 +00:00
aiRules : AiRuleConfig [ ] ,
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
) : Promise < AiResult > {
2026-06-10 13:56:15 +00:00
if ( aiRules . length === 0 ) {
return { findings : [ ] , error : null } ;
}
const allowed = new Map ( aiRules . map ( ( r ) = > [ r . ruleId , r ] ) ) ;
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
const systemPrompt = prompts . find ( ( p ) = > p . key === "system" ) ? . content ? ? "" ;
const analysisPrompt =
prompts . find ( ( p ) = > p . key === "analysis" ) ? . content ? ? "" ;
const payload = buildSkillPayload ( files ) ;
2026-06-10 13:56:15 +00:00
const user = ` ${ analysisPrompt } \ n ${ buildRuleMenu ( aiRules ) } \ n \ nHier ist das zu prüfende Skill: \ n ${ payload } ` ;
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
try {
const content = await callProvider ( provider , systemPrompt , user ) ;
const parsed = extractJson ( content ) as { findings? : unknown [ ] } ;
const findingsRaw = Array . isArray ( parsed . findings ) ? parsed . findings : [ ] ;
const findings = findingsRaw
2026-06-10 13:56:15 +00:00
. map ( ( f ) = > coerceFinding ( f , allowed ) )
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
. filter ( ( f ) : f is RawFinding = > f !== null ) ;
return { findings , error : null } ;
} catch ( err ) {
return {
findings : [ ] ,
error : err instanceof Error ? err . message : "Unbekannter KI-Fehler" ,
} ;
}
}