225 lines
7.6 KiB
TypeScript
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 },
|
|
};
|
|
},
|
|
});
|
|
}
|