Files
ECC/.omp/extensions/tools.ts
sakuradairong e08ee4a4f9 chore: snapshot backup before rainycy push (20260624-032434)
Auto-committed by MiMo for migration to git.rainycy.top
2026-06-24 03:24:34 +08:00

225 lines
7.6 KiB
TypeScript

import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
/**
* ECC Custom Tools — LLM-callable tools for security audit, test running, etc.
*
* Registered alongside slash commands for the model to invoke autonomously.
*/
export function registerTools(pi: ExtensionAPI): void {
const { z } = pi.zod;
// ── Security audit tool ───────────────────────────────────────────────
pi.registerTool({
name: "ecc_security_audit",
label: "ECC Security Audit",
description:
"Run a security audit on agent configuration: scan for hardcoded secrets, broad permissions, and unsafe MCP servers",
parameters: z.object({
path: z
.string()
.optional()
.default(".")
.describe("Project root path to scan"),
format: z
.enum(["text", "json", "markdown"])
.optional()
.default("text")
.describe("Output format"),
minSeverity: z
.enum(["low", "medium", "high", "critical"])
.optional()
.default("medium")
.describe("Minimum severity to report"),
}),
hidden: false,
defaultInactive: false,
deferrable: false,
async execute(_id, params, signal, _onUpdate, ctx) {
if (signal?.aborted) {
return { content: [{ type: "text", text: "Cancelled" }] };
}
const scanPath = params.path ?? ".";
const fmt = params.format ?? "text";
const minSev = params.minSeverity ?? "medium";
const sevOrder = ["low", "medium", "high", "critical"] as const;
const minIdx = sevOrder.indexOf(minSev);
// Use AgentShield if available
let output = `Security scan of ${scanPath} (min severity: ${minSev})\n`;
try {
const result = await pi.exec("npx", [
"ecc-agentshield",
"scan",
"--path",
scanPath,
"--format",
fmt,
]);
output = `Security scan completed.\n\n${result.stdout ?? ""}`;
} catch {
// AgentShield not installed — do best-effort text scan
output +=
"AgentShield not available. Run `npx ecc-agentshield scan` for full analysis.\n";
}
return {
content: [{ type: "text", text: output }],
details: { scanPath, format: fmt, minSeverity: minSev },
};
},
});
// ── Run tests tool ────────────────────────────────────────────────────
pi.registerTool({
name: "ecc_run_tests",
label: "ECC Run Tests",
description:
"Detect project type and run the appropriate test suite. Supports Node/TypeScript, Rust, Go, Python",
parameters: z.object({
path: z
.string()
.optional()
.default(".")
.describe("Project root path"),
filter: z
.string()
.optional()
.describe("Optional test name filter (passed to test runner)"),
}),
hidden: false,
defaultInactive: false,
deferrable: false,
async execute(_id, params, _signal, _onUpdate, _ctx) {
const projectPath = params.path ?? ".";
const filter = params.filter ?? "";
// Detect project type
try {
const files = await pi.exec("ls", [projectPath]);
const ls = files.stdout ?? "";
if (ls.includes("package.json")) {
const cmd = filter
? `cd "${projectPath}" && npx jest --testNamePattern="${filter}" 2>/dev/null || npm test -- --testPathPattern="${filter}"`
: `cd "${projectPath}" && npm test 2>/dev/null`;
const result = await pi.exec("bash", ["-c", cmd]);
return {
content: [{ type: "text", text: result.stdout || "Tests passed" }],
details: { exitCode: result.exitCode },
};
}
if (ls.includes("Cargo.toml")) {
const cmd = filter
? `cd "${projectPath}" && cargo test "${filter}" 2>&1`
: `cd "${projectPath}" && cargo test 2>&1`;
const result = await pi.exec("bash", ["-c", cmd]);
return {
content: [{ type: "text", text: result.stdout || "Tests passed" }],
details: { exitCode: result.exitCode },
};
}
if (ls.includes("go.mod")) {
const cmd = filter
? `cd "${projectPath}" && go test -run "${filter}" ./... 2>&1`
: `cd "${projectPath}" && go test ./... 2>&1`;
const result = await pi.exec("bash", ["-c", cmd]);
return {
content: [{ type: "text", text: result.stdout || "Tests passed" }],
details: { exitCode: result.exitCode },
};
}
return {
content: [
{
type: "text",
text: "Could not detect project type (package.json, Cargo.toml, go.mod). Run tests manually.",
},
],
};
} catch (err) {
return {
content: [
{
type: "text",
text: `Test execution failed: ${err instanceof Error ? err.message : String(err)}`,
},
],
isError: true,
};
}
},
});
// ── Checklist / quality gate tool ─────────────────────────────────────
pi.registerTool({
name: "ecc_quality_checklist",
label: "ECC Quality Checklist",
description:
"Return the ECC code quality checklist for human review before marking work complete",
parameters: z.object({
area: z
.enum(["all", "security", "coding", "testing"])
.optional()
.default("all")
.describe("Which checklist to return"),
}),
hidden: false,
defaultInactive: false,
deferrable: false,
async execute(_id, params, _signal, _onUpdate, _ctx) {
const area = params.area ?? "all";
const securityItems = [
"No hardcoded secrets (API keys, passwords, tokens)",
"All user inputs validated",
"SQL injection prevention (parameterized queries)",
"XSS prevention (sanitized HTML)",
"CSRF protection enabled",
"Authentication/authorization verified",
"Rate limiting on all endpoints",
"Error messages don't leak sensitive data",
];
const codingItems = [
"Code is readable and well-named",
"Functions are small (<50 lines)",
"Files are focused (<800 lines)",
"No deep nesting (>4 levels)",
"Proper error handling",
"No hardcoded values (use constants or config)",
"No mutation (immutable patterns used)",
];
const testingItems = [
"All public functions have unit tests",
"All API endpoints have integration tests",
"Critical user flows have E2E tests",
"Edge cases covered (null, empty, invalid)",
"Error paths tested (not just happy path)",
"Tests are independent (no shared state)",
"Coverage is 80%+",
];
const text: string[] = [];
if (area === "all" || area === "security") {
text.push("## Security Checklist", ...securityItems.map((s) => `- [ ] ${s}`), "");
}
if (area === "all" || area === "coding") {
text.push("## Coding Checklist", ...codingItems.map((s) => `- [ ] ${s}`), "");
}
if (area === "all" || area === "testing") {
text.push("## Testing Checklist", ...testingItems.map((s) => `- [ ] ${s}`), "");
}
return {
content: [{ type: "text", text: text.join("\n") }],
details: { area, itemCount: text.length - 1 },
};
},
});
}