kifaru 1.0.198 → 1.0.200
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/extensions/kifaru-finding-classification.ts +102 -0
- package/kifaru-pi.js +1101 -237
- package/package.json +2 -2
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** Read-only TypeSafe experiment over agent findings in the Pi target dossier. */
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
|
3
|
+
import { Type } from "typebox"
|
|
4
|
+
import fs from "node:fs/promises"
|
|
5
|
+
import path from "node:path"
|
|
6
|
+
import { targetSlug } from "./kifaru-dossier"
|
|
7
|
+
|
|
8
|
+
export type Severity = "critical" | "high" | "medium" | "low" | "info" | "unknown"
|
|
9
|
+
export interface AgentFinding { text: string; severity?: Severity }
|
|
10
|
+
const rank: Record<Severity, number> = { critical: 5, high: 4, medium: 3, low: 2, info: 1, unknown: 0 }
|
|
11
|
+
|
|
12
|
+
export function severityFromText(text: string): Severity {
|
|
13
|
+
const match = text.match(/(?:^|\n)\s*(?:\[|severity\s*[:=-]\s*)(critical|high|medium|low|info)(?:\]|\b)/i)
|
|
14
|
+
return (match?.[1]?.toLowerCase() as Severity | undefined) ?? "unknown"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function redactFindingText(value: string): string {
|
|
18
|
+
return value
|
|
19
|
+
.replace(/\b(?:sk|pk)_[A-Za-z0-9_-]{12,}\b/g, "[REDACTED_KEY]")
|
|
20
|
+
.replace(/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, "[REDACTED_KEY]")
|
|
21
|
+
.replace(/\bBearer\s+\S+/gi, "Bearer [REDACTED]")
|
|
22
|
+
.replace(/\b(?:password|passwd|api[_-]?key|token|secret)\s*[:=]\s*\S+/gi, "[REDACTED_SECRET]")
|
|
23
|
+
.replace(/\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[REDACTED_JWT]")
|
|
24
|
+
.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, "[REDACTED_EMAIL]")
|
|
25
|
+
.replace(/https?:\/\/[^\s)]+/gi, "[REDACTED_URL]")
|
|
26
|
+
.slice(0, 1800)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const detailQuestion = {
|
|
30
|
+
type: "score",
|
|
31
|
+
instructions: "How complete is this security finding for an independent reviewer to understand and reproduce the claimed issue? Judge only the supplied report text. Do not infer missing evidence or judge severity.",
|
|
32
|
+
criteria: [
|
|
33
|
+
"Thin: the issue or affected behavior is unclear, with no usable reproduction or impact explanation.",
|
|
34
|
+
"Partial: the issue and impact are understandable, but reproduction, evidence, or remediation detail is missing.",
|
|
35
|
+
"Actionable: the issue, impact, reproduction evidence, and remediation are clear enough for independent triage.",
|
|
36
|
+
],
|
|
37
|
+
} as const
|
|
38
|
+
|
|
39
|
+
export function classifyFinding(finding: AgentFinding, score: number, confidence: number) {
|
|
40
|
+
const severity = finding.severity ?? severityFromText(finding.text)
|
|
41
|
+
const detail = score < 0.75 ? "thin" : score < 1.5 ? "partial" : "actionable"
|
|
42
|
+
const review = confidence < 0.5 || severity === "unknown"
|
|
43
|
+
? "manual_review"
|
|
44
|
+
: rank[severity] >= 4 && detail !== "actionable" ? "needs_detail" : "ready_for_triage"
|
|
45
|
+
return { severity, detail, detailScore: score, confidence, review }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function dossierFindings(target: string, cwd = process.cwd()): Promise<AgentFinding[]> {
|
|
49
|
+
const file = path.join(cwd, ".kifaru", "dossiers", `${targetSlug(target)}.json`)
|
|
50
|
+
const data = await fs.readFile(file, "utf8").then((raw) => JSON.parse(raw) as { findings?: unknown }).catch(() => null)
|
|
51
|
+
return Array.isArray(data?.findings)
|
|
52
|
+
? data.findings.filter((item): item is string => typeof item === "string").map((text) => ({ text }))
|
|
53
|
+
: []
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default function (pi: ExtensionAPI) {
|
|
57
|
+
pi.registerTool({
|
|
58
|
+
name: "kifaru_finding_classification",
|
|
59
|
+
label: "Finding Classification Experiment",
|
|
60
|
+
description: "Read-only TypeSafe experiment: classify agent findings by recorded severity and report detail. Supply findings or a target whose Pi dossier contains findings. Requires TYPESAFE_API_KEY. Advisory only; does not change the dossier or report.",
|
|
61
|
+
promptSnippet: "Experimentally classify recorded findings by severity and report detail",
|
|
62
|
+
parameters: Type.Object({
|
|
63
|
+
target: Type.Optional(Type.String({ description: "Target whose .kifaru/dossiers/<slug>.json findings to read" })),
|
|
64
|
+
findings: Type.Optional(Type.Array(Type.Object({
|
|
65
|
+
text: Type.String({ description: "Finding report text" }),
|
|
66
|
+
severity: Type.Optional(Type.Union(["critical", "high", "medium", "low", "info", "unknown"].map((s) => Type.Literal(s)))),
|
|
67
|
+
}), { description: "Agent findings to classify directly; overrides target dossier findings" })),
|
|
68
|
+
}),
|
|
69
|
+
async execute(_toolCallId, params: { target?: string; findings?: AgentFinding[] }) {
|
|
70
|
+
const result = (text: string, details: Record<string, unknown> = {}) => ({ content: [{ type: "text" as const, text }], details })
|
|
71
|
+
const key = process.env.TYPESAFE_API_KEY
|
|
72
|
+
if (!key) return result("Experiment unavailable: set TYPESAFE_API_KEY in the Pi process environment.", { ok: false })
|
|
73
|
+
if (!params.findings && !params.target) return result("Supply findings or target.", { ok: false })
|
|
74
|
+
const findings = params.findings ?? await dossierFindings(params.target!)
|
|
75
|
+
if (!findings.length) return result("No agent findings to classify.", { ok: true, findings: [] })
|
|
76
|
+
const classifications = []
|
|
77
|
+
for (const [index, finding] of findings.entries()) {
|
|
78
|
+
let response: Response
|
|
79
|
+
try {
|
|
80
|
+
response = await fetch("https://api.typesafe.ai/v1/systemone", {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
|
|
83
|
+
body: JSON.stringify({ model: "jev-latest", state: { report: redactFindingText(finding.text) }, questions: { report_detail: detailQuestion } }),
|
|
84
|
+
signal: AbortSignal.timeout(15_000),
|
|
85
|
+
})
|
|
86
|
+
} catch {
|
|
87
|
+
return result(`TypeSafe request failed at finding ${index + 1}; no dossier data changed.`, { ok: false })
|
|
88
|
+
}
|
|
89
|
+
if (!response.ok) return result(`TypeSafe returned HTTP ${response.status} at finding ${index + 1}; no dossier data changed.`, { ok: false })
|
|
90
|
+
let data: { model?: string; answers?: { report_detail?: { type?: string; score?: number; confidence?: number } } }
|
|
91
|
+
try { data = await response.json() } catch { return result(`Invalid TypeSafe JSON at finding ${index + 1}.`, { ok: false }) }
|
|
92
|
+
const answer = data.answers?.report_detail
|
|
93
|
+
if (answer?.type !== "score" || typeof answer.score !== "number" || !Number.isFinite(answer.score) || answer.score < 0 || answer.score > 2 || typeof answer.confidence !== "number" || !Number.isFinite(answer.confidence) || answer.confidence < 0 || answer.confidence > 1) {
|
|
94
|
+
return result(`Invalid TypeSafe score at finding ${index + 1}.`, { ok: false })
|
|
95
|
+
}
|
|
96
|
+
classifications.push({ index: index + 1, ...classifyFinding(finding, answer.score, answer.confidence), model: data.model })
|
|
97
|
+
}
|
|
98
|
+
classifications.sort((a, b) => rank[b.severity] - rank[a.severity] || a.detailScore - b.detailScore)
|
|
99
|
+
return result(JSON.stringify({ advisory: true, classifications }, null, 2), { ok: true, classifications })
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
}
|