@matyah00/openpi 0.1.5 → 0.2.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.
- package/README.md +29 -11
- package/agents/agent-chain.yaml +50 -0
- package/agents/api-designer.md +58 -0
- package/agents/docs-writer.md +38 -0
- package/agents/migration-expert.md +62 -0
- package/agents/perf-auditor.md +64 -0
- package/agents/teams.yaml +23 -0
- package/damage-control-rules.yaml +153 -0
- package/extensions/agent-chain.ts +101 -12
- package/extensions/agent-team.ts +11 -0
- package/extensions/audit-tools.ts +125 -6
- package/extensions/lib/auditLogger.ts +29 -0
- package/extensions/openpi.ts +169 -21
- package/extensions/search-tools.ts +21 -3
- package/extensions/workflow.ts +77 -5
- package/package.json +7 -3
- package/prompts/docs.md +37 -0
- package/prompts/migrate.md +44 -0
- package/prompts/perf.md +52 -0
- package/prompts/refactor.md +53 -0
- package/scripts/validate-package.mjs +28 -1
- package/skills/perf-auditor/SKILL.md +49 -0
- package/skills/refactor-guide/SKILL.md +39 -0
- package/tsconfig.json +2 -1
|
@@ -8,9 +8,10 @@ import { join, resolve } from "path";
|
|
|
8
8
|
import { parse as parseYaml } from "yaml";
|
|
9
9
|
import { bundledAgentsDir } from "./lib/packagePaths.ts";
|
|
10
10
|
import { arrayField, parseMarkdownFrontmatter, stringField } from "./lib/markdown.ts";
|
|
11
|
+
import { writeAuditLog } from "./lib/auditLogger.ts";
|
|
11
12
|
|
|
12
|
-
type ChainStep = { agent: string; prompt: string };
|
|
13
|
-
type ChainDef = { name: string; description: string; steps: ChainStep[] };
|
|
13
|
+
type ChainStep = { agent: string; prompt: string; timeout?: number };
|
|
14
|
+
type ChainDef = { name: string; description: string; steps: ChainStep[]; continueOnError?: boolean };
|
|
14
15
|
type AgentDef = { name: string; description: string; tools: string; systemPrompt: string };
|
|
15
16
|
type StepState = { agent: string; status: "pending" | "running" | "done" | "error"; elapsed: number; lastWork: string };
|
|
16
17
|
|
|
@@ -24,19 +25,24 @@ function parseChainYaml(raw: string): ChainDef[] {
|
|
|
24
25
|
|
|
25
26
|
return Object.entries(parsed as Record<string, unknown>).flatMap(([name, value]) => {
|
|
26
27
|
if (!value || typeof value !== "object" || Array.isArray(value)) return [];
|
|
27
|
-
const chain = value as { description?: unknown; steps?: unknown };
|
|
28
|
+
const chain = value as { description?: unknown; steps?: unknown; continueOnError?: unknown };
|
|
28
29
|
if (!Array.isArray(chain.steps)) return [];
|
|
29
30
|
const steps = chain.steps.flatMap((step): ChainStep[] => {
|
|
30
31
|
if (!step || typeof step !== "object" || Array.isArray(step)) return [];
|
|
31
|
-
const item = step as { agent?: unknown; prompt?: unknown };
|
|
32
|
+
const item = step as { agent?: unknown; prompt?: unknown; timeout?: unknown };
|
|
32
33
|
if (typeof item.agent !== "string" || typeof item.prompt !== "string") return [];
|
|
33
|
-
return [{
|
|
34
|
+
return [{
|
|
35
|
+
agent: item.agent,
|
|
36
|
+
prompt: item.prompt,
|
|
37
|
+
timeout: typeof item.timeout === "number" ? item.timeout : undefined,
|
|
38
|
+
}];
|
|
34
39
|
});
|
|
35
40
|
if (!steps.length) return [];
|
|
36
41
|
return [{
|
|
37
42
|
name,
|
|
38
43
|
description: typeof chain.description === "string" ? chain.description : "",
|
|
39
44
|
steps,
|
|
45
|
+
continueOnError: typeof chain.continueOnError === "boolean" ? chain.continueOnError : undefined,
|
|
40
46
|
}];
|
|
41
47
|
});
|
|
42
48
|
}
|
|
@@ -115,7 +121,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
115
121
|
}));
|
|
116
122
|
}
|
|
117
123
|
|
|
118
|
-
function runAgent(
|
|
124
|
+
function runAgent(
|
|
125
|
+
agentDef: AgentDef,
|
|
126
|
+
task: string,
|
|
127
|
+
stepIndex: number,
|
|
128
|
+
timeoutSecs: number | undefined,
|
|
129
|
+
ctx: any,
|
|
130
|
+
): Promise<{ output: string; exitCode: number; elapsed: number }> {
|
|
119
131
|
const model = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "";
|
|
120
132
|
const agentKey = agentDef.name.toLowerCase().replace(/\s+/g, "-");
|
|
121
133
|
const agentSessionFile = join(sessionDir, `chain-${agentKey}.json`);
|
|
@@ -137,9 +149,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
137
149
|
const start = Date.now();
|
|
138
150
|
const chunks: string[] = [];
|
|
139
151
|
|
|
152
|
+
const timeoutMs = timeoutSecs ? timeoutSecs * 1000 : 120000;
|
|
153
|
+
|
|
140
154
|
return new Promise((resolveDone) => {
|
|
141
155
|
const cleanupPrompt = () => rmSync(systemPrompt.dir, { recursive: true, force: true });
|
|
142
156
|
const proc = spawn("pi", args, { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env } });
|
|
157
|
+
|
|
158
|
+
let killed = false;
|
|
159
|
+
const killTimer = setTimeout(() => {
|
|
160
|
+
killed = true;
|
|
161
|
+
proc.kill("SIGTERM");
|
|
162
|
+
setTimeout(() => {
|
|
163
|
+
if (proc.exitCode === null) proc.kill("SIGKILL");
|
|
164
|
+
}, 2000);
|
|
165
|
+
}, timeoutMs);
|
|
166
|
+
|
|
143
167
|
const timer = setInterval(() => {
|
|
144
168
|
state.elapsed = Date.now() - start;
|
|
145
169
|
updateWidget();
|
|
@@ -166,13 +190,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
166
190
|
proc.stderr!.setEncoding("utf-8");
|
|
167
191
|
proc.stderr!.on("data", () => {});
|
|
168
192
|
proc.on("close", (code) => {
|
|
193
|
+
clearTimeout(killTimer);
|
|
169
194
|
clearInterval(timer);
|
|
170
195
|
cleanupPrompt();
|
|
171
196
|
state.elapsed = Date.now() - start;
|
|
172
197
|
if (code === 0) agentSessions.set(agentKey, agentSessionFile);
|
|
173
|
-
|
|
198
|
+
const exitCode = killed ? 124 : (code ?? 1);
|
|
199
|
+
const finalOutput = killed ? `${chunks.join("")}\n\n[Agent timeout after ${timeoutSecs}s]` : chunks.join("");
|
|
200
|
+
resolveDone({ output: finalOutput, exitCode, elapsed: state.elapsed });
|
|
174
201
|
});
|
|
175
202
|
proc.on("error", (error) => {
|
|
203
|
+
clearTimeout(killTimer);
|
|
176
204
|
clearInterval(timer);
|
|
177
205
|
cleanupPrompt();
|
|
178
206
|
resolveDone({ output: `Error spawning agent: ${error.message}`, exitCode: 1, elapsed: Date.now() - start });
|
|
@@ -188,28 +216,89 @@ export default function (pi: ExtensionAPI) {
|
|
|
188
216
|
|
|
189
217
|
let input = task;
|
|
190
218
|
const original = task;
|
|
219
|
+
const outputs: string[] = [];
|
|
220
|
+
|
|
191
221
|
for (let i = 0; i < activeChain.steps.length; i++) {
|
|
192
222
|
const step = activeChain.steps[i];
|
|
193
223
|
stepStates[i].status = "running";
|
|
194
224
|
updateWidget();
|
|
195
225
|
|
|
196
|
-
|
|
226
|
+
let prompt = step.prompt.replace(/\$INPUT/g, input).replace(/\$ORIGINAL/g, original);
|
|
227
|
+
// Replace $STEP_N references
|
|
228
|
+
prompt = prompt.replace(/\$STEP_(\d+)/g, (match, numStr) => {
|
|
229
|
+
const num = parseInt(numStr, 10);
|
|
230
|
+
if (num > 0 && num <= outputs.length) {
|
|
231
|
+
return outputs[num - 1];
|
|
232
|
+
}
|
|
233
|
+
return match;
|
|
234
|
+
});
|
|
235
|
+
|
|
197
236
|
const agentDef = allAgents.get(step.agent.toLowerCase());
|
|
198
237
|
if (!agentDef) {
|
|
199
238
|
stepStates[i].status = "error";
|
|
200
|
-
|
|
239
|
+
const errResult = { output: `Agent "${step.agent}" not found`, success: false, elapsed: Date.now() - start };
|
|
240
|
+
writeAuditLog(ctx.cwd, {
|
|
241
|
+
type: "chain_run",
|
|
242
|
+
name: activeChain.name,
|
|
243
|
+
task,
|
|
244
|
+
input: task,
|
|
245
|
+
output: errResult.output,
|
|
246
|
+
exitCode: 1,
|
|
247
|
+
elapsedMs: errResult.elapsed,
|
|
248
|
+
metadata: { stepsCount: activeChain.steps.length, success: false, error: "agent_not_found" }
|
|
249
|
+
});
|
|
250
|
+
return errResult;
|
|
201
251
|
}
|
|
202
252
|
|
|
203
|
-
const result = await runAgent(agentDef, prompt, i, ctx);
|
|
253
|
+
const result = await runAgent(agentDef, prompt, i, step.timeout, ctx);
|
|
254
|
+
writeAuditLog(ctx.cwd, {
|
|
255
|
+
type: "chain_step",
|
|
256
|
+
name: `${activeChain.name}:${step.agent}`,
|
|
257
|
+
task: prompt,
|
|
258
|
+
input: input,
|
|
259
|
+
output: result.output,
|
|
260
|
+
exitCode: result.exitCode,
|
|
261
|
+
elapsedMs: result.elapsed,
|
|
262
|
+
metadata: { stepIndex: i, chainName: activeChain.name, agent: step.agent }
|
|
263
|
+
});
|
|
264
|
+
|
|
204
265
|
if (result.exitCode !== 0) {
|
|
205
266
|
stepStates[i].status = "error";
|
|
206
|
-
|
|
267
|
+
if (activeChain.continueOnError) {
|
|
268
|
+
outputs.push("");
|
|
269
|
+
updateWidget();
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const errResult = { output: result.output, success: false, elapsed: Date.now() - start };
|
|
273
|
+
writeAuditLog(ctx.cwd, {
|
|
274
|
+
type: "chain_run",
|
|
275
|
+
name: activeChain.name,
|
|
276
|
+
task,
|
|
277
|
+
input: task,
|
|
278
|
+
output: errResult.output,
|
|
279
|
+
exitCode: 1,
|
|
280
|
+
elapsedMs: errResult.elapsed,
|
|
281
|
+
metadata: { stepsCount: activeChain.steps.length, success: false, error: "step_failed", failedStep: step.agent }
|
|
282
|
+
});
|
|
283
|
+
return errResult;
|
|
207
284
|
}
|
|
208
285
|
stepStates[i].status = "done";
|
|
209
286
|
input = result.output;
|
|
287
|
+
outputs.push(result.output);
|
|
210
288
|
updateWidget();
|
|
211
289
|
}
|
|
212
|
-
|
|
290
|
+
const finalResult = { output: input, success: true, elapsed: Date.now() - start };
|
|
291
|
+
writeAuditLog(ctx.cwd, {
|
|
292
|
+
type: "chain_run",
|
|
293
|
+
name: activeChain.name,
|
|
294
|
+
task,
|
|
295
|
+
input: task,
|
|
296
|
+
output: finalResult.output,
|
|
297
|
+
exitCode: 0,
|
|
298
|
+
elapsedMs: finalResult.elapsed,
|
|
299
|
+
metadata: { stepsCount: activeChain.steps.length, success: true }
|
|
300
|
+
});
|
|
301
|
+
return finalResult;
|
|
213
302
|
}
|
|
214
303
|
|
|
215
304
|
pi.registerTool({
|
package/extensions/agent-team.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { join, resolve } from "path";
|
|
|
8
8
|
import { parse as parseYaml } from "yaml";
|
|
9
9
|
import { bundledAgentsDir } from "./lib/packagePaths.ts";
|
|
10
10
|
import { arrayField, parseMarkdownFrontmatter, stringField } from "./lib/markdown.ts";
|
|
11
|
+
import { writeAuditLog } from "./lib/auditLogger.ts";
|
|
11
12
|
|
|
12
13
|
type AgentDef = {
|
|
13
14
|
name: string;
|
|
@@ -238,6 +239,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
238
239
|
const { agent, task } = params as { agent: string; task: string };
|
|
239
240
|
onUpdate?.({ content: [{ type: "text", text: `Dispatching to ${agent}...` }], details: { agent, task, status: "running" } });
|
|
240
241
|
const result = await dispatchAgent(agent, task, ctx);
|
|
242
|
+
writeAuditLog(ctx.cwd, {
|
|
243
|
+
type: "team_agent",
|
|
244
|
+
name: agent,
|
|
245
|
+
task: task,
|
|
246
|
+
input: task,
|
|
247
|
+
output: result.output,
|
|
248
|
+
exitCode: result.exitCode,
|
|
249
|
+
elapsedMs: result.elapsed,
|
|
250
|
+
metadata: { teamName: activeTeamName }
|
|
251
|
+
});
|
|
241
252
|
const text = result.output.length > 8000 ? `${result.output.slice(0, 8000)}\n\n... [truncated]` : result.output;
|
|
242
253
|
return {
|
|
243
254
|
content: [{ type: "text", text: `[${agent}] ${result.exitCode === 0 ? "done" : "error"} in ${Math.round(result.elapsed / 1000)}s\n\n${text}` }],
|
|
@@ -31,6 +31,19 @@ const SECRET_PATTERNS = [
|
|
|
31
31
|
["sendgrid-key", /SG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}/g],
|
|
32
32
|
["private-key", /-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g],
|
|
33
33
|
["jwt-like-token", /eyJ[A-Za-z0-9_-]{40,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g],
|
|
34
|
+
// New patterns
|
|
35
|
+
["supabase-key", /sbp_[a-f0-9]{40}/g],
|
|
36
|
+
["twilio-sid", /AC[a-f0-9]{32}/g],
|
|
37
|
+
["twilio-auth", /SK[a-f0-9]{32}/g],
|
|
38
|
+
["mailgun-key", /key-[a-zA-Z0-9]{32}/g],
|
|
39
|
+
["slack-webhook", /hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[a-zA-Z0-9]+/g],
|
|
40
|
+
["digitalocean-token", /dop_v1_[a-f0-9]{64}/g],
|
|
41
|
+
["databricks-token", /dapi[a-f0-9]{32}/g],
|
|
42
|
+
["openai-key", /sk-[a-zA-Z0-9]{20}T3BlbkFJ[a-zA-Z0-9]{20}/g],
|
|
43
|
+
["anthropic-key", /sk-ant-[a-zA-Z0-9_-]{90,}/g],
|
|
44
|
+
["azure-connection-string", /DefaultEndpointsProtocol=https;AccountName=[^;]+;AccountKey=[^;]+/g],
|
|
45
|
+
["vercel-token", /vercel_[A-Za-z0-9_-]{24,}/g],
|
|
46
|
+
["hashicorp-vault-token", /hvs\.[A-Za-z0-9_-]{24,}/g],
|
|
34
47
|
] as const;
|
|
35
48
|
|
|
36
49
|
const GHOST_PATTERNS = [
|
|
@@ -38,6 +51,30 @@ const GHOST_PATTERNS = [
|
|
|
38
51
|
["exit-bypass", /sys\.exit\s*\(\s*0\s*\)|os\._exit\s*\(\s*0\s*\)|process\.exit\s*\(\s*0\s*\)/g],
|
|
39
52
|
["pytest-report-patch", /TestReport\.from_item_and_call|pytest_runtest_makereport|monkeypatch.*TestReport/g],
|
|
40
53
|
["assertion-monkeypatch", /expect\s*=\s*jest\.fn|assert\s*=\s*lambda|monkeypatch.*assert/g],
|
|
54
|
+
// New patterns
|
|
55
|
+
["empty-test-body", /it\s*\(\s*['"][^'"]*['"]\s*,\s*\(\s*\)\s*=>\s*\{\s*\}\s*\)|def\s+test_\w+\s*\([^)]*\)\s*:\s*\n\s*pass\b/g],
|
|
56
|
+
["catch-all-swallow", /catch\s*\([^)]*\)\s*\{\s*\}|except\s*:\s*\n\s*pass\b/g],
|
|
57
|
+
["self-comparison", /expect\s*\(\s*(\w+)\s*\)\s*\.toBe\s*\(\s*\1\s*\)|assert\s+(\w+)\s*==\s*\2\b/g],
|
|
58
|
+
["disabled-tests", /\bxit\s*\(|\bxdescribe\s*\(|\bxtest\s*\(|@pytest\.mark\.skip(?!\(reason)|\.skip\s*\(\s*\)/g],
|
|
59
|
+
["console-only-validation", /(?:it|test)\s*\([^)]*\)\s*(?:=>|{)[\s\S]{0,200}console\.\w+[\s\S]{0,50}(?:\}|\))\s*(?:;|\n)(?![\s\S]{0,50}(?:expect|assert|should))/g],
|
|
60
|
+
["unreachable-assertion", /\breturn\b[\s\S]{0,20}\n\s*(?:expect|assert)\b/g],
|
|
61
|
+
["mock-everything", /jest\.mock\s*\(\s*['"]\.\.?\//g],
|
|
62
|
+
["timeout-zero", /setTimeout\s*\(\s*(?:done|resolve|callback)\s*,\s*0\s*\)/g],
|
|
63
|
+
] as const;
|
|
64
|
+
|
|
65
|
+
const SAST_PATTERNS = [
|
|
66
|
+
["eval-usage", /\beval\s*\(/g],
|
|
67
|
+
["function-constructor", /new\s+Function\s*\(/g],
|
|
68
|
+
["child-process-exec", /child_process.*\.exec\s*\(|exec\s*\(\s*`/g],
|
|
69
|
+
["sql-concat", /['"`]\s*(?:SELECT|INSERT|UPDATE|DELETE)\b[\s\S]{0,80}\$\{|['"`]\s*(?:SELECT|INSERT|UPDATE|DELETE)\b[\s\S]{0,80}\+\s*\w/g],
|
|
70
|
+
["innerhtml-assign", /\.innerHTML\s*=\s*(?!['"`]<)/g],
|
|
71
|
+
["dangerous-react", /dangerouslySetInnerHTML/g],
|
|
72
|
+
["path-traversal", /\.\.\/|\.\.\\|req\.\w+\.\w+.*(?:readFile|writeFile|createReadStream|path\.join|path\.resolve)/g],
|
|
73
|
+
["prototype-pollution", /\[['"`]__proto__['"`]\]|\[['"`]constructor['"`]\]|\[['"`]prototype['"`]\]/g],
|
|
74
|
+
["ssrf-pattern", /fetch\s*\(\s*(?:req\.|request\.|params\.|query\.)|axios\.\w+\s*\(\s*(?:req\.|request\.|params\.|query\.)/g],
|
|
75
|
+
["hardcoded-secret-assign", /(?:password|secret|apiKey|api_key|token|auth)\s*[:=]\s*['"][^'"]{8,}['"]/g],
|
|
76
|
+
["unvalidated-redirect", /res\.redirect\s*\(\s*req\.|response\.redirect\s*\(\s*request\./g],
|
|
77
|
+
["cors-wildcard", /(?:Access-Control-Allow-Origin|origin)\s*[:=]\s*['"]\*['"]/g],
|
|
41
78
|
] as const;
|
|
42
79
|
|
|
43
80
|
function walkFiles(root: string, maxFiles: number): string[] {
|
|
@@ -103,17 +140,64 @@ function detectFrameworks(root: string): string[] {
|
|
|
103
140
|
if (deps.react) found.add("React");
|
|
104
141
|
if (deps.vite) found.add("Vite");
|
|
105
142
|
if (deps["@sveltejs/kit"]) found.add("SvelteKit");
|
|
143
|
+
if (deps.svelte) found.add("Svelte");
|
|
106
144
|
if (deps.vue) found.add("Vue");
|
|
107
145
|
if (deps.express) found.add("Express");
|
|
108
146
|
if (deps.vitest) found.add("Vitest");
|
|
109
147
|
if (deps.jest) found.add("Jest");
|
|
110
148
|
if (deps.typescript) found.add("TypeScript");
|
|
149
|
+
// New framework detections
|
|
150
|
+
if (deps.astro) found.add("Astro");
|
|
151
|
+
if (deps["@remix-run/node"] || deps["@remix-run/react"]) found.add("Remix");
|
|
152
|
+
if (deps.nuxt) found.add("Nuxt");
|
|
153
|
+
if (deps["@angular/core"]) found.add("Angular");
|
|
154
|
+
if (deps["@nestjs/core"]) found.add("NestJS");
|
|
155
|
+
if (deps.fastify) found.add("Fastify");
|
|
156
|
+
if (deps.hono) found.add("Hono");
|
|
157
|
+
if (deps.elysia) found.add("Elysia");
|
|
158
|
+
if (deps.prisma || deps["@prisma/client"]) found.add("Prisma");
|
|
159
|
+
if (deps["drizzle-orm"]) found.add("Drizzle");
|
|
160
|
+
if (deps["@trpc/server"]) found.add("tRPC");
|
|
161
|
+
if (deps.graphql || deps["@apollo/server"]) found.add("GraphQL");
|
|
162
|
+
if (deps.tailwindcss) found.add("Tailwind CSS");
|
|
163
|
+
if (deps.playwright || deps["@playwright/test"]) found.add("Playwright");
|
|
164
|
+
if (deps.cypress) found.add("Cypress");
|
|
165
|
+
if (deps.storybook || deps["@storybook/react"]) found.add("Storybook");
|
|
166
|
+
if (deps.turborepo || deps.turbo) found.add("Turborepo");
|
|
167
|
+
if (deps.electron) found.add("Electron");
|
|
168
|
+
if (deps["react-native"]) found.add("React Native");
|
|
169
|
+
if (deps.expo) found.add("Expo");
|
|
111
170
|
}
|
|
112
171
|
}
|
|
113
|
-
if (fs.existsSync(path.join(root, "pyproject.toml")) || fs.existsSync(path.join(root, "requirements.txt")))
|
|
172
|
+
if (fs.existsSync(path.join(root, "pyproject.toml")) || fs.existsSync(path.join(root, "requirements.txt"))) {
|
|
173
|
+
found.add("Python");
|
|
174
|
+
const pyReqs = readSmallText(path.join(root, "requirements.txt"));
|
|
175
|
+
const pyProject = readSmallText(path.join(root, "pyproject.toml"));
|
|
176
|
+
const pyContent = (pyReqs || "") + (pyProject || "");
|
|
177
|
+
if (pyContent.includes("fastapi") || pyContent.includes("FastAPI")) found.add("FastAPI");
|
|
178
|
+
if (pyContent.includes("django") || pyContent.includes("Django")) found.add("Django");
|
|
179
|
+
if (pyContent.includes("flask") || pyContent.includes("Flask")) found.add("Flask");
|
|
180
|
+
if (pyContent.includes("pytest")) found.add("pytest");
|
|
181
|
+
}
|
|
114
182
|
if (fs.existsSync(path.join(root, "Cargo.toml"))) found.add("Rust");
|
|
115
183
|
if (fs.existsSync(path.join(root, "go.mod"))) found.add("Go");
|
|
116
184
|
if (fs.existsSync(path.join(root, "Dockerfile"))) found.add("Docker");
|
|
185
|
+
if (fs.existsSync(path.join(root, "Gemfile"))) {
|
|
186
|
+
found.add("Ruby");
|
|
187
|
+
const gemfile = readSmallText(path.join(root, "Gemfile"));
|
|
188
|
+
if (gemfile?.includes("rails")) found.add("Rails");
|
|
189
|
+
}
|
|
190
|
+
if (fs.existsSync(path.join(root, "mix.exs"))) {
|
|
191
|
+
found.add("Elixir");
|
|
192
|
+
const mix = readSmallText(path.join(root, "mix.exs"));
|
|
193
|
+
if (mix?.includes("phoenix")) found.add("Phoenix");
|
|
194
|
+
}
|
|
195
|
+
if (fs.existsSync(path.join(root, "pom.xml")) || fs.existsSync(path.join(root, "build.gradle"))) {
|
|
196
|
+
found.add("Java");
|
|
197
|
+
const pom = readSmallText(path.join(root, "pom.xml")) || "";
|
|
198
|
+
const gradle = readSmallText(path.join(root, "build.gradle")) || "";
|
|
199
|
+
if (pom.includes("spring-boot") || gradle.includes("spring-boot")) found.add("Spring Boot");
|
|
200
|
+
}
|
|
117
201
|
return Array.from(found);
|
|
118
202
|
}
|
|
119
203
|
|
|
@@ -131,7 +215,7 @@ function dependencyInventory(root: string): string {
|
|
|
131
215
|
if (loose.length) sections.push(`Loose pins: ${loose.slice(0, 20).map(([name, version]) => `${name}@${version}`).join(", ")}${loose.length > 20 ? `, +${loose.length - 20} more` : ""}`);
|
|
132
216
|
}
|
|
133
217
|
}
|
|
134
|
-
for (const file of ["requirements.txt", "pyproject.toml", "Cargo.toml", "go.mod", "Gemfile"]) {
|
|
218
|
+
for (const file of ["requirements.txt", "pyproject.toml", "Cargo.toml", "go.mod", "Gemfile", "mix.exs", "pom.xml", "build.gradle"]) {
|
|
135
219
|
if (fs.existsSync(path.join(root, file))) sections.push(`Manifest: ${file}`);
|
|
136
220
|
}
|
|
137
221
|
return sections.join("\n") || "No common dependency manifests found.";
|
|
@@ -179,7 +263,7 @@ export default function auditToolsExtension(pi: ExtensionAPI) {
|
|
|
179
263
|
pi.registerTool({
|
|
180
264
|
name: "secret_scan",
|
|
181
265
|
label: "Secret Scan",
|
|
182
|
-
description: "Read-only high-signal secret scan with redacted findings.",
|
|
266
|
+
description: "Read-only high-signal secret scan with redacted findings. Detects 22 secret patterns including cloud provider keys, API tokens, and private keys.",
|
|
183
267
|
promptSnippet: "Use secret_scan before shipping, importing external files, or touching credentials.",
|
|
184
268
|
parameters: Type.Object({
|
|
185
269
|
maxFiles: Type.Optional(Type.Number({ description: "Maximum files to scan. Default 5000." })),
|
|
@@ -202,7 +286,7 @@ export default function auditToolsExtension(pi: ExtensionAPI) {
|
|
|
202
286
|
const envGitignored = !fs.existsSync(path.join(ctx.cwd, ".env")) || (readSmallText(path.join(ctx.cwd, ".gitignore")) || "").includes(".env");
|
|
203
287
|
const verdict = findings.length ? "BLOCKED" : "CLEAN";
|
|
204
288
|
return {
|
|
205
|
-
content: [{ type: "text", text: [`secret_scan verdict: ${verdict}`, `.env gitignored: ${envGitignored ? "yes" : "no"}`, "", ...(findings.length ? findings : ["No high-signal secrets found."])].join("\n") }],
|
|
289
|
+
content: [{ type: "text", text: [`secret_scan verdict: ${verdict}`, `patterns: ${SECRET_PATTERNS.length}`, `files scanned: ${files.length}`, `.env gitignored: ${envGitignored ? "yes" : "no"}`, "", ...(findings.length ? findings : ["No high-signal secrets found."])].join("\n") }],
|
|
206
290
|
details: { findings },
|
|
207
291
|
};
|
|
208
292
|
},
|
|
@@ -214,7 +298,7 @@ export default function auditToolsExtension(pi: ExtensionAPI) {
|
|
|
214
298
|
pi.registerTool({
|
|
215
299
|
name: "ghost_test_scan",
|
|
216
300
|
label: "Ghost Test Scan",
|
|
217
|
-
description: "Static scan for test-suite reward hacking patterns
|
|
301
|
+
description: "Static scan for test-suite reward hacking patterns: always-true equality, exit bypasses, empty test bodies, disabled tests, self-comparisons, and framework patching.",
|
|
218
302
|
promptSnippet: "Use ghost_test_scan when validating test integrity before trusting green tests.",
|
|
219
303
|
parameters: Type.Object({
|
|
220
304
|
maxFiles: Type.Optional(Type.Number({ description: "Maximum files to scan. Default 3000." })),
|
|
@@ -235,7 +319,7 @@ export default function auditToolsExtension(pi: ExtensionAPI) {
|
|
|
235
319
|
}
|
|
236
320
|
}
|
|
237
321
|
return {
|
|
238
|
-
content: [{ type: "text", text: [`ghost_test_scan verdict: ${findings.length ? "SUSPICIOUS" : "CLEAN"}`, `files scanned: ${files.length}`, "", ...(findings.length ? findings : ["No static reward-hack patterns found."])].join("\n") }],
|
|
322
|
+
content: [{ type: "text", text: [`ghost_test_scan verdict: ${findings.length ? "SUSPICIOUS" : "CLEAN"}`, `patterns: ${GHOST_PATTERNS.length}`, `files scanned: ${files.length}`, "", ...(findings.length ? findings : ["No static reward-hack patterns found."])].join("\n") }],
|
|
239
323
|
details: { findings },
|
|
240
324
|
};
|
|
241
325
|
},
|
|
@@ -257,4 +341,39 @@ export default function auditToolsExtension(pi: ExtensionAPI) {
|
|
|
257
341
|
return new Text(theme.fg("toolTitle", theme.bold("dependency_inventory")), 0, 0);
|
|
258
342
|
},
|
|
259
343
|
});
|
|
344
|
+
|
|
345
|
+
pi.registerTool({
|
|
346
|
+
name: "sast_scan",
|
|
347
|
+
label: "SAST Scan",
|
|
348
|
+
description: "Static application security testing: detects eval(), SQL injection, prototype pollution, SSRF, XSS, unvalidated redirects, CORS wildcards, and hardcoded secrets in code.",
|
|
349
|
+
promptSnippet: "Use sast_scan to detect dangerous code patterns before shipping or reviewing external contributions.",
|
|
350
|
+
parameters: Type.Object({
|
|
351
|
+
maxFiles: Type.Optional(Type.Number({ description: "Maximum files to scan. Default 5000." })),
|
|
352
|
+
}),
|
|
353
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
354
|
+
const maxFiles = Math.max(100, Math.min(Number(params.maxFiles ?? 5000), 20000));
|
|
355
|
+
const codeExts = /\.(ts|tsx|js|jsx|mjs|cjs|py|rb|go|rs|java|php)$/i;
|
|
356
|
+
const files = walkFiles(ctx.cwd, maxFiles).filter((file) => codeExts.test(file));
|
|
357
|
+
const findings: string[] = [];
|
|
358
|
+
for (const file of files) {
|
|
359
|
+
const content = readSmallText(file);
|
|
360
|
+
if (!content) continue;
|
|
361
|
+
for (const [rule, pattern] of SAST_PATTERNS) {
|
|
362
|
+
pattern.lastIndex = 0;
|
|
363
|
+
let match: RegExpExecArray | null;
|
|
364
|
+
while ((match = pattern.exec(content))) {
|
|
365
|
+
findings.push(`${relative(ctx.cwd, file)}:${lineOf(content, match.index)} ${rule}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const severity = findings.some((f) => /eval-usage|sql-concat|child-process-exec|prototype-pollution/.test(f)) ? "HIGH" : findings.length ? "MEDIUM" : "CLEAN";
|
|
370
|
+
return {
|
|
371
|
+
content: [{ type: "text", text: [`sast_scan verdict: ${severity}`, `patterns: ${SAST_PATTERNS.length}`, `files scanned: ${files.length}`, "", ...(findings.length ? findings : ["No dangerous code patterns found."])].join("\n") }],
|
|
372
|
+
details: { findings, severity },
|
|
373
|
+
};
|
|
374
|
+
},
|
|
375
|
+
renderCall(_args, theme) {
|
|
376
|
+
return new Text(theme.fg("toolTitle", theme.bold("sast_scan")), 0, 0);
|
|
377
|
+
},
|
|
378
|
+
});
|
|
260
379
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export type AuditLogEntry = {
|
|
5
|
+
timestamp: string;
|
|
6
|
+
type: "workflow" | "team_agent" | "chain_step" | "chain_run";
|
|
7
|
+
name: string;
|
|
8
|
+
task: string;
|
|
9
|
+
input: string;
|
|
10
|
+
output: string;
|
|
11
|
+
exitCode: number;
|
|
12
|
+
elapsedMs: number;
|
|
13
|
+
metadata?: Record<string, any>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function writeAuditLog(cwd: string, entry: Omit<AuditLogEntry, "timestamp">) {
|
|
17
|
+
try {
|
|
18
|
+
const logDir = path.join(cwd, ".pi", "logs");
|
|
19
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
20
|
+
const logFile = path.join(logDir, "openpi-audit.jsonl");
|
|
21
|
+
const line = JSON.stringify({
|
|
22
|
+
timestamp: new Date().toISOString(),
|
|
23
|
+
...entry
|
|
24
|
+
});
|
|
25
|
+
fs.appendFileSync(logFile, `${line}\n`, "utf-8");
|
|
26
|
+
} catch {
|
|
27
|
+
// Silent fail to ensure logger never disrupts runtime in case of file locks
|
|
28
|
+
}
|
|
29
|
+
}
|