Merge pull request #43 from y574444354/feat/migrate-review-agents
feat: migrate review agents to unified costrict-review repo
This commit is contained in:
commit
85aaeb9a7a
9
.github/workflows/ci.yml
vendored
9
.github/workflows/ci.yml
vendored
|
|
@ -31,6 +31,15 @@ jobs:
|
|||
CLAUDE_CODE_SKIP_CHROME_MCP_SETUP: "1"
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Setup SSH for review agent generation
|
||||
uses: webfactory/ssh-agent@v0.9.0
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.CO_STRICT_REVIEW_SSH_KEY }}
|
||||
|
||||
- name: Generate review builtin files
|
||||
run: bun run scripts/generate-review-builtin.ts
|
||||
continue-on-error: true
|
||||
|
||||
- name: Type check
|
||||
run: bun run typecheck
|
||||
|
||||
|
|
|
|||
9
.gitignore
vendored
9
.gitignore
vendored
|
|
@ -41,4 +41,11 @@ data
|
|||
|
||||
.costrict
|
||||
.claude
|
||||
.tmp/
|
||||
.tmp/
|
||||
|
||||
# Auto-generated review builtin files
|
||||
src/costrict/review/agent/builtin.ts
|
||||
src/costrict/review/skill/builtin.ts
|
||||
|
||||
# Review builtin cache
|
||||
packages/builtin-tools/bundled-review/
|
||||
11
build.ts
11
build.ts
|
|
@ -8,6 +8,17 @@ const outdir = 'dist'
|
|||
const { rmSync } = await import('fs')
|
||||
rmSync(outdir, { recursive: true, force: true })
|
||||
|
||||
// Step 1.5: Generate review builtin files
|
||||
console.log('Generating review builtin files...')
|
||||
const { spawnSync: genSpawnSync } = await import('child_process')
|
||||
const genResult = genSpawnSync('bun', ['run', 'scripts/generate-review-builtin.ts'], {
|
||||
stdio: 'inherit',
|
||||
cwd: new URL('.', import.meta.url).pathname,
|
||||
})
|
||||
if (genResult.status !== 0) {
|
||||
console.warn('Warning: generate-review-builtin.ts failed, using existing files')
|
||||
}
|
||||
|
||||
// Collect FEATURE_* env vars → Bun.build features
|
||||
const envFeatures = Object.keys(process.env)
|
||||
.filter(k => k.startsWith('FEATURE_'))
|
||||
|
|
|
|||
|
|
@ -62,7 +62,8 @@
|
|||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.19.0",
|
||||
"@claude-code-best/mcp-chrome-bridge": "^2.0.8",
|
||||
"ws": "^8.20.0"
|
||||
"ws": "^8.20.0",
|
||||
"gray-matter": "^4.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@alcalzone/ansi-tokenize": "^0.3.0",
|
||||
|
|
@ -199,4 +200,4 @@
|
|||
"yaml": "^2.8.3",
|
||||
"zod": "^4.3.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import { feature } from 'bun:bundle'
|
|||
import { getIsNonInteractiveSession } from 'src/bootstrap/state.js'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
|
||||
import { isEnvTruthy } from 'src/utils/envUtils.js'
|
||||
import { REVIEW_AGENTS } from 'src/costrict/review/agent/builtin.js'
|
||||
import { DESIGN_AGENT } from 'src/costrict/agents/designAgent.js'
|
||||
import { QUICK_EXPLORE_AGENT } from 'src/costrict/agents/quickExplore.js'
|
||||
import { REQUIREMENT_AGENT } from 'src/costrict/agents/requirement.js'
|
||||
|
|
@ -111,5 +112,8 @@ export function getBuiltInAgents(): AgentDefinition[] {
|
|||
agents.push(VERIFICATION_AGENT)
|
||||
}
|
||||
|
||||
// Review agents (CoStrictReviewer, CoStrictValidator)
|
||||
agents.push(...REVIEW_AGENTS)
|
||||
|
||||
return agents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,16 @@ const inspectArgs = process.env.BUN_INSPECT
|
|||
// npm, etc.) and on all platforms.
|
||||
const bunCmd = process.execPath;
|
||||
|
||||
// Generate review builtin files before dev launch
|
||||
console.log('[dev] Generating review builtin files...');
|
||||
const genResult = Bun.spawnSync(['bun', 'run', 'scripts/generate-review-builtin.ts'], {
|
||||
stdio: 'inherit',
|
||||
cwd: projectRoot,
|
||||
});
|
||||
if (!genResult.success) {
|
||||
console.warn('[dev] Warning: generate-review-builtin.ts failed, using existing files');
|
||||
}
|
||||
|
||||
const result = Bun.spawnSync(
|
||||
[bunCmd, ...inspectArgs, "run", ...defineArgs, ...featureArgs, cliPath, ...process.argv.slice(2)],
|
||||
{
|
||||
|
|
|
|||
487
scripts/generate-review-builtin.ts
Normal file
487
scripts/generate-review-builtin.ts
Normal file
|
|
@ -0,0 +1,487 @@
|
|||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Downloads builtin review skills & agents from costrict-review repo and generates
|
||||
* src/costrict/review/skill/builtin.ts and src/costrict/review/agent/builtin.ts
|
||||
*
|
||||
* Uses git SSH transport (git ls-remote + git clone).
|
||||
* Reads index.json manifest to discover resources and their per-locale paths.
|
||||
* Compares remote commit SHA with cached version and skips download if unchanged.
|
||||
*
|
||||
* Usage: bun run scripts/generate-review-builtin.ts
|
||||
*/
|
||||
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { spawnSync } from 'child_process'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
const bundledReviewDir = path.resolve(__dirname, '../packages/builtin-tools/bundled-review')
|
||||
const builtinSkillsFile = path.resolve(__dirname, '../src/costrict/review/skill/builtin.ts')
|
||||
const builtinAgentsFile = path.resolve(__dirname, '../src/costrict/review/agent/builtin.ts')
|
||||
|
||||
type IndexJson = {
|
||||
agents: Array<{
|
||||
name: string
|
||||
path: Record<string, string>
|
||||
opencode?: Record<string, unknown>
|
||||
claudecode?: Record<string, unknown>
|
||||
}>
|
||||
skills: Array<{ name: string; path: Record<string, string> }>
|
||||
}
|
||||
|
||||
const REPO = 'zgsm-ai/costrict-review'
|
||||
const BRANCH = 'main'
|
||||
const CLONE_URL = `git@github.com:${REPO}.git`
|
||||
|
||||
function git(...args: string[]): { ok: boolean; stdout: string; stderr: string } {
|
||||
const result = spawnSync('git', args, { encoding: 'utf-8' })
|
||||
return {
|
||||
ok: result.status === 0,
|
||||
stdout: result.stdout?.trim() ?? '',
|
||||
stderr: result.stderr?.trim() ?? '',
|
||||
}
|
||||
}
|
||||
|
||||
function lsRemoteSha(): string | null {
|
||||
const ref = `refs/heads/${BRANCH}`
|
||||
const result = git('ls-remote', '--heads', CLONE_URL, ref)
|
||||
if (!result.ok || !result.stdout) return null
|
||||
const sha = result.stdout.split('\t')[0] ?? ''
|
||||
return sha.length >= 40 ? sha : null
|
||||
}
|
||||
|
||||
async function readCachedSha(targetFile: string): Promise<string | null> {
|
||||
try {
|
||||
const content = await fs.readFile(targetFile, 'utf-8')
|
||||
const match = content.match(/\b([a-f0-9]{40})\b/)
|
||||
return match ? match[1] : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function walk(dir: string, base = ''): Promise<string[]> {
|
||||
try {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true })
|
||||
const files: string[] = []
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name)
|
||||
const relativePath = base ? path.join(base, entry.name) : entry.name
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...await walk(fullPath, relativePath))
|
||||
} else {
|
||||
files.push(relativePath)
|
||||
}
|
||||
}
|
||||
return files
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async function readIndexJson(cloneDir: string): Promise<IndexJson> {
|
||||
const raw = await fs.readFile(path.join(cloneDir, 'index.json'), 'utf-8')
|
||||
return JSON.parse(raw) as IndexJson
|
||||
}
|
||||
|
||||
function collectLocales(index: IndexJson): string[] {
|
||||
const localeSet = new Set<string>()
|
||||
for (const skill of index.skills) {
|
||||
for (const locale of Object.keys(skill.path)) localeSet.add(locale)
|
||||
}
|
||||
for (const agent of index.agents) {
|
||||
for (const locale of Object.keys(agent.path)) localeSet.add(locale)
|
||||
}
|
||||
return [...localeSet].sort()
|
||||
}
|
||||
|
||||
function mergeClaudecodeFrontmatter(
|
||||
mdContent: string,
|
||||
claudecodeFields: Record<string, unknown>,
|
||||
): string {
|
||||
const md = matter(mdContent)
|
||||
const merged = { ...md.data, ...claudecodeFields }
|
||||
return matter.stringify(md.content, merged)
|
||||
}
|
||||
|
||||
async function cloneAndCopy(
|
||||
cloneDir: string,
|
||||
index: IndexJson,
|
||||
): Promise<void> {
|
||||
const locales = collectLocales(index)
|
||||
|
||||
// Clean stale locale directories before copying
|
||||
for (const entry of await fs.readdir(bundledReviewDir).catch(() => [] as string[])) {
|
||||
if (entry === '.clone') continue
|
||||
const entryPath = path.join(bundledReviewDir, entry)
|
||||
if ((await fs.stat(entryPath).catch(() => null))?.isDirectory()) {
|
||||
await fs.rm(entryPath, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
for (const locale of locales) {
|
||||
const outputLocaleDir = path.join(bundledReviewDir, locale)
|
||||
|
||||
// Copy skill dirs for this locale
|
||||
const skillPaths = index.skills
|
||||
.map(s => s.path[locale])
|
||||
.filter(Boolean)
|
||||
|
||||
for (const skillMdPath of skillPaths) {
|
||||
const srcDir = path.join(cloneDir, path.dirname(skillMdPath))
|
||||
const relativeDir = skillMdPath.startsWith(`${locale}/`)
|
||||
? skillMdPath.slice(locale.length + 1).replace(/\/[^/]*$/, '')
|
||||
: path.dirname(skillMdPath)
|
||||
const outputDir = path.join(outputLocaleDir, relativeDir)
|
||||
|
||||
await fs.rm(outputDir, { recursive: true, force: true })
|
||||
await fs.cp(srcDir, outputDir, { recursive: true })
|
||||
|
||||
const skillMd = path.join(outputDir, 'SKILL.md')
|
||||
try {
|
||||
await fs.access(skillMd)
|
||||
} catch {
|
||||
throw new Error(`Skill (${locale}) missing SKILL.md at ${skillMdPath}`)
|
||||
}
|
||||
|
||||
const skillName = path.basename(srcDir)
|
||||
const fileCount = (await walk(outputDir)).length
|
||||
console.log(` ✓ ${locale}/skills/${skillName}: ${fileCount} files`)
|
||||
}
|
||||
|
||||
// Copy agent files for this locale, merging claudecode frontmatter
|
||||
const agentEntries = index.agents
|
||||
.map(a => ({ name: a.name, filePath: a.path[locale], claudecode: a.claudecode }))
|
||||
.filter(e => e.filePath)
|
||||
|
||||
for (const { name, filePath, claudecode } of agentEntries) {
|
||||
const srcFile = path.join(cloneDir, filePath)
|
||||
const outputDir = path.join(outputLocaleDir, 'agents')
|
||||
await fs.mkdir(outputDir, { recursive: true })
|
||||
|
||||
const filename = path.basename(filePath)
|
||||
const destFile = path.join(outputDir, filename)
|
||||
|
||||
if (claudecode) {
|
||||
const rawContent = await fs.readFile(srcFile, 'utf-8')
|
||||
const merged = mergeClaudecodeFrontmatter(rawContent, claudecode)
|
||||
await fs.writeFile(destFile, merged, 'utf-8')
|
||||
} else {
|
||||
await fs.cp(srcFile, destFile)
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.access(destFile)
|
||||
} catch {
|
||||
throw new Error(`Agent "${name}" (${locale}) missing at ${filePath}`)
|
||||
}
|
||||
|
||||
console.log(` ✓ ${locale}/agents/${filename}`)
|
||||
}
|
||||
}
|
||||
|
||||
await fs.rm(cloneDir, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
async function generateBuiltinSkills(
|
||||
commitSha: string,
|
||||
): Promise<void> {
|
||||
const localeEntries = await fs.readdir(bundledReviewDir).catch(() => [] as string[])
|
||||
const locales: string[] = []
|
||||
for (const l of localeEntries) {
|
||||
if ((await fs.stat(path.join(bundledReviewDir, l)).catch(() => null))?.isDirectory()) {
|
||||
locales.push(l)
|
||||
}
|
||||
}
|
||||
|
||||
const allSkillNames: string[] = []
|
||||
|
||||
// Discover skill names from first locale
|
||||
for (const locale of locales) {
|
||||
const skillsDir = path.join(bundledReviewDir, locale, 'skills')
|
||||
const entries = await fs.readdir(skillsDir).catch(() => [] as string[])
|
||||
for (const name of entries) {
|
||||
if (!allSkillNames.includes(name)) allSkillNames.push(name)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
const imports: string[] = []
|
||||
const localeSkillEntries: string[] = []
|
||||
let fileIdx = 0
|
||||
|
||||
for (const locale of [...locales].sort()) {
|
||||
const skillEntries: string[] = []
|
||||
for (const skillName of allSkillNames) {
|
||||
const skillDir = path.join(bundledReviewDir, locale, 'skills', skillName)
|
||||
const files = await walk(skillDir)
|
||||
const fileEntries: string[] = []
|
||||
for (const file of files) {
|
||||
const varName = `SKILL_FILE_${fileIdx++}`
|
||||
const filePath = path.join(skillDir, file)
|
||||
const content = await fs.readFile(filePath, 'utf-8')
|
||||
const normalizedPath = file.replaceAll('\\', '/')
|
||||
imports.push(`const ${varName} = ${JSON.stringify(content)}`)
|
||||
fileEntries.push(` "${normalizedPath}": ${varName}`)
|
||||
}
|
||||
skillEntries.push(` "${skillName}": {\n${fileEntries.join(',\n')}\n }`)
|
||||
}
|
||||
localeSkillEntries.push(` "${locale}": {\n${skillEntries.join(',\n')}\n }`)
|
||||
}
|
||||
|
||||
const content = `// This file is auto-generated by scripts/generate-review-builtin.ts
|
||||
// Do not edit manually
|
||||
|
||||
import { writeFile, mkdir } from "fs/promises"
|
||||
import { join, dirname } from "path"
|
||||
|
||||
${imports.join('\n')}
|
||||
|
||||
const SKILL_FILES: Record<string, Record<string, Record<string, string>>> = {
|
||||
${localeSkillEntries.join(',\n')}
|
||||
}
|
||||
|
||||
const SKILL_VERSIONS: Record<string, string> = {
|
||||
${allSkillNames.map(n => ` "${n}": "${commitSha}"`).join(',\n')}
|
||||
}
|
||||
|
||||
export function listBuiltinSkills(): string[] {
|
||||
return ${JSON.stringify(allSkillNames)}
|
||||
}
|
||||
|
||||
export function getBuiltinSkillVersion(skillName: string): string | undefined {
|
||||
return SKILL_VERSIONS[skillName]
|
||||
}
|
||||
|
||||
export function listSkillFiles(skillName: string, locale: string): string[] {
|
||||
return Object.keys(SKILL_FILES[locale]?.[skillName] || {})
|
||||
}
|
||||
|
||||
export async function extractBundledSkill(skillName: string, targetDir: string, locale: string): Promise<void> {
|
||||
const localeData = SKILL_FILES[locale]
|
||||
if (!localeData) {
|
||||
throw new Error(\`Locale not found: \${locale}\`)
|
||||
}
|
||||
|
||||
const skillFiles = localeData[skillName]
|
||||
if (!skillFiles) {
|
||||
throw new Error(\`Skill not found: \${skillName}\`)
|
||||
}
|
||||
|
||||
await mkdir(targetDir, { recursive: true })
|
||||
for (const [relativePath, content] of Object.entries(skillFiles)) {
|
||||
await mkdir(join(targetDir, dirname(relativePath)), { recursive: true })
|
||||
await writeFile(join(targetDir, relativePath), content, "utf-8")
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
await fs.writeFile(builtinSkillsFile, content, 'utf-8')
|
||||
console.log(`\n✓ Generated ${builtinSkillsFile}`)
|
||||
}
|
||||
|
||||
async function generateBuiltinAgents(
|
||||
commitSha: string,
|
||||
): Promise<void> {
|
||||
const localeEntries = await fs.readdir(bundledReviewDir).catch(() => [] as string[])
|
||||
const locales: string[] = []
|
||||
for (const l of localeEntries) {
|
||||
if ((await fs.stat(path.join(bundledReviewDir, l)).catch(() => null))?.isDirectory()) {
|
||||
locales.push(l)
|
||||
}
|
||||
}
|
||||
|
||||
// Discover agent names from first locale's agents dir
|
||||
const allAgentNames: string[] = []
|
||||
for (const locale of locales) {
|
||||
const agentsDir = path.join(bundledReviewDir, locale, 'agents')
|
||||
const entries = await fs.readdir(agentsDir).catch(() => [] as string[])
|
||||
for (const name of entries) {
|
||||
if (!allAgentNames.includes(name)) allAgentNames.push(name)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
const imports: string[] = []
|
||||
const agentCodeEntries: string[] = []
|
||||
let fileIdx = 0
|
||||
let primaryAgent = ''
|
||||
let subAgent = ''
|
||||
|
||||
for (const agentName of allAgentNames) {
|
||||
const agentKey = agentName.replace(/\.md$/, '')
|
||||
const localeEntriesList: string[] = []
|
||||
const promptVarNames: string[] = []
|
||||
|
||||
for (const locale of [...locales].sort()) {
|
||||
const agentFile = path.join(bundledReviewDir, locale, 'agents', agentName)
|
||||
try {
|
||||
const content = await fs.readFile(agentFile, 'utf-8')
|
||||
const varName = `REVIEW_AGENT_${fileIdx++}`
|
||||
imports.push(`const ${varName} = ${JSON.stringify(content)}`)
|
||||
localeEntriesList.push(`"${locale}": ${varName}`)
|
||||
promptVarNames.push(varName)
|
||||
|
||||
if (content.includes('mode: primary')) primaryAgent = agentKey
|
||||
if (content.includes('mode: subagent')) subAgent = agentKey
|
||||
} catch {
|
||||
// Agent file not found for this locale, skip
|
||||
}
|
||||
}
|
||||
|
||||
if (localeEntriesList.length > 0) {
|
||||
const promptRecordName = `${agentKey.toUpperCase().replace(/-/g, '_')}_PROMPTS`
|
||||
agentCodeEntries.push(`const ${promptRecordName}: Record<string, string> = {\n${localeEntriesList.join(',\n')}\n}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Now generate the BuiltInAgentDefinition array
|
||||
// Parse merged frontmatter from the zh-CN agent files to extract fields
|
||||
const agentDefs: string[] = []
|
||||
|
||||
for (const agentName of allAgentNames) {
|
||||
const agentKey = agentName.replace(/\.md$/, '')
|
||||
const promptRecordName = `${agentKey.toUpperCase().replace(/-/g, '_')}_PROMPTS`
|
||||
|
||||
// Read zh-CN version to parse frontmatter for fields
|
||||
const zhAgentFile = path.join(bundledReviewDir, 'zh-CN', 'agents', agentName)
|
||||
let frontmatterData: Record<string, unknown> = {}
|
||||
try {
|
||||
const content = await fs.readFile(zhAgentFile, 'utf-8')
|
||||
const parsed = matter(content)
|
||||
frontmatterData = parsed.data as Record<string, unknown>
|
||||
} catch {
|
||||
// fallback
|
||||
}
|
||||
|
||||
// Extract fields from frontmatter
|
||||
const whenToUse = String(frontmatterData['whenToUse'] ?? frontmatterData['description'] ?? '')
|
||||
const toolsStr = String(frontmatterData['tools'] ?? '')
|
||||
const tools = toolsStr ? toolsStr.split(',').map((t: string) => t.trim()).filter(Boolean) : undefined
|
||||
const permissionMode = String(frontmatterData['permissionMode'] ?? 'default')
|
||||
const model = String(frontmatterData['model'] ?? 'inherit')
|
||||
const mode = String(frontmatterData['mode'] ?? '')
|
||||
|
||||
// Determine disallowedTools based on tools availability
|
||||
const toolNameSet = new Set(tools ?? [])
|
||||
const disallowedTools: string[] = []
|
||||
if (!toolNameSet.has('Agent')) disallowedTools.push('Agent')
|
||||
if (!toolNameSet.has('FileEdit')) disallowedTools.push('FileEdit')
|
||||
if (!toolNameSet.has('FileWrite')) disallowedTools.push('FileWrite')
|
||||
if (!toolNameSet.has('NotebookEdit')) disallowedTools.push('NotebookEdit')
|
||||
|
||||
// Determine visibleTo based on mode
|
||||
const visibleTo = mode === 'primary' ? undefined : ['CoStrictReviewer']
|
||||
|
||||
const toolsLiteral = tools ? JSON.stringify(tools) : 'undefined'
|
||||
const disallowedLiteral = disallowedTools.length > 0
|
||||
? `[${disallowedTools.map((t: string) => `'${t}'`).join(', ')}]`
|
||||
: 'undefined'
|
||||
const visibleToLiteral = visibleTo ? JSON.stringify(visibleTo) : 'undefined'
|
||||
|
||||
agentDefs.push(` {
|
||||
agentType: '${agentKey}',
|
||||
whenToUse: ${JSON.stringify(whenToUse)},
|
||||
tools: ${toolsLiteral} as string[] | undefined,
|
||||
disallowedTools: ${disallowedLiteral} as string[] | undefined,
|
||||
permissionMode: '${permissionMode}',
|
||||
model: '${model}',
|
||||
source: 'built-in',
|
||||
baseDir: 'built-in',
|
||||
visibleTo: ${visibleToLiteral} as string[] | undefined,
|
||||
getSystemPrompt: (_params) => {
|
||||
const lang = getResolvedLanguage()
|
||||
const locale = LOCALE_MAP[lang] ?? 'zh-CN'
|
||||
return ${promptRecordName}[locale] ?? ${promptRecordName}['zh-CN']
|
||||
},
|
||||
}`)
|
||||
}
|
||||
|
||||
const content = `// This file is auto-generated by scripts/generate-review-builtin.ts
|
||||
// Do not edit manually
|
||||
// Agents are downloaded from zgsm-ai/costrict-review repository
|
||||
|
||||
import type { BuiltInAgentDefinition } from '@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js'
|
||||
import { getResolvedLanguage } from 'src/utils/language.js'
|
||||
|
||||
const LOCALE_MAP: Record<string, string> = { zh: 'zh-CN', en: 'en' }
|
||||
|
||||
${imports.join('\n')}
|
||||
|
||||
${agentCodeEntries.join('\n\n')}
|
||||
|
||||
export const REVIEW_AGENTS: BuiltInAgentDefinition[] = [
|
||||
${agentDefs.join(',\n')}
|
||||
]
|
||||
|
||||
export const AGENT_VERSIONS: Record<string, string> = {
|
||||
${allAgentNames.map(n => ` "${n.replace(/\.md$/, '')}": "${commitSha}"`).join(',\n')}
|
||||
}
|
||||
|
||||
export const PRIMARY_REVIEW_AGENT = ${JSON.stringify(primaryAgent)}
|
||||
export const SUB_REVIEW_AGENT = ${JSON.stringify(subAgent)}
|
||||
`
|
||||
|
||||
await fs.writeFile(builtinAgentsFile, content, 'utf-8')
|
||||
console.log(`✓ Generated ${builtinAgentsFile}`)
|
||||
}
|
||||
|
||||
async function generateBuiltinReview() {
|
||||
console.log('\n🚀 CSC — Downloading Builtin Review Resources\n')
|
||||
|
||||
await fs.mkdir(bundledReviewDir, { recursive: true })
|
||||
|
||||
const remoteSha = lsRemoteSha()
|
||||
if (!remoteSha) {
|
||||
throw new Error(`git ls-remote failed for ${CLONE_URL} (branch: ${BRANCH})`)
|
||||
}
|
||||
console.log(`Remote commit: ${remoteSha.slice(0, 7)}`)
|
||||
|
||||
const cachedSha = await readCachedSha(builtinSkillsFile)
|
||||
const hasCachedFiles = (await walk(bundledReviewDir)).length > 0
|
||||
|
||||
let commitSha = remoteSha
|
||||
|
||||
if (cachedSha === remoteSha && hasCachedFiles) {
|
||||
console.log('✓ All resources up to date, skipping download')
|
||||
} else {
|
||||
if (cachedSha) {
|
||||
console.log(`Cached ${cachedSha.slice(0, 7)} → remote ${remoteSha.slice(0, 7)}, updating`)
|
||||
}
|
||||
const cloneDir = path.join(bundledReviewDir, '.clone')
|
||||
try {
|
||||
console.log(` git clone --depth 1 ${CLONE_URL}`)
|
||||
await fs.rm(cloneDir, { recursive: true, force: true })
|
||||
const cloneResult = git('clone', '--depth', '1', '--branch', BRANCH, CLONE_URL, cloneDir)
|
||||
if (!cloneResult.ok) {
|
||||
throw new Error(`git clone failed: ${cloneResult.stderr}`)
|
||||
}
|
||||
const index = await readIndexJson(cloneDir)
|
||||
await cloneAndCopy(cloneDir, index)
|
||||
console.log(`\n✓ All resources updated (commit ${remoteSha.slice(0, 7)})`)
|
||||
} catch (err) {
|
||||
console.error(` ✗ Download failed: ${err}`)
|
||||
if (!hasCachedFiles) {
|
||||
throw new Error(`Download failed and no cache available`)
|
||||
}
|
||||
console.warn(` ⚠ Using cached resources`)
|
||||
commitSha = cachedSha ?? remoteSha
|
||||
} finally {
|
||||
await fs.rm(cloneDir, { recursive: true, force: true }).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✓ Bundled review directory: ${bundledReviewDir}`)
|
||||
|
||||
await generateBuiltinSkills(commitSha)
|
||||
|
||||
await generateBuiltinAgents(commitSha)
|
||||
|
||||
console.log('\n💡 Run `bun run build` to compile the extension\n')
|
||||
}
|
||||
|
||||
generateBuiltinReview().catch(console.error)
|
||||
|
|
@ -1,244 +0,0 @@
|
|||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Downloads builtin skills from their source repositories and generates
|
||||
* src/costrict/skill/builtin.ts with all skill files embedded as string constants.
|
||||
*
|
||||
* Uses git SSH transport (git ls-remote + git clone).
|
||||
* Compares remote commit SHA with cached version and skips download if unchanged.
|
||||
*
|
||||
* Usage: bun run scripts/generate-skills.ts
|
||||
*/
|
||||
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { spawnSync } from 'child_process'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
const bundledSkillsDir = path.resolve(__dirname, '../.tmp/skills')
|
||||
const builtinTsFile = path.resolve(__dirname, '../src/costrict/skills/builtin.ts')
|
||||
|
||||
type SkillConfig = {
|
||||
repo: string
|
||||
branch: string
|
||||
subdir: string
|
||||
}
|
||||
|
||||
const BUILTIN_SKILLS: Record<string, SkillConfig> = {
|
||||
'security-review': {
|
||||
repo: 'zgsm-ai/security-review-skill',
|
||||
branch: 'main',
|
||||
subdir: 'security-review',
|
||||
},
|
||||
}
|
||||
|
||||
function git(...args: string[]): { ok: boolean; stdout: string; stderr: string } {
|
||||
const result = spawnSync('git', args, { encoding: 'utf-8' })
|
||||
return {
|
||||
ok: result.status === 0,
|
||||
stdout: result.stdout?.trim() ?? '',
|
||||
stderr: result.stderr?.trim() ?? '',
|
||||
}
|
||||
}
|
||||
|
||||
function getCloneUrl(repo: string): string {
|
||||
return `git@github.com:${repo}.git`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest commit SHA for a branch via `git ls-remote`.
|
||||
* No clone needed — lightweight remote query over SSH.
|
||||
*/
|
||||
function lsRemoteSha(repo: string, branch: string): string | null {
|
||||
const cloneUrl = getCloneUrl(repo)
|
||||
const ref = `refs/heads/${branch}`
|
||||
const result = git('ls-remote', '--heads', cloneUrl, ref)
|
||||
if (!result.ok || !result.stdout) {
|
||||
return null
|
||||
}
|
||||
// Output format: "<sha>\t<ref>"
|
||||
const sha = result.stdout.split('\t')[0] ?? ''
|
||||
return sha.length >= 40 ? sha : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the cached commit SHA from the generated builtin.ts file.
|
||||
*/
|
||||
async function readCachedSha(skillName: string): Promise<string | null> {
|
||||
try {
|
||||
const content = await fs.readFile(builtinTsFile, 'utf-8')
|
||||
const regex = new RegExp(
|
||||
`^\\s*${JSON.stringify(skillName)}:\\s*"([a-f0-9]{40})"`,
|
||||
'm',
|
||||
)
|
||||
const match = content.match(regex)
|
||||
return match ? match[1] : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadSkill(
|
||||
name: string,
|
||||
config: SkillConfig,
|
||||
): Promise<{ name: string; commitSha: string | null } | null> {
|
||||
const { repo, branch, subdir } = config
|
||||
const cloneUrl = getCloneUrl(repo)
|
||||
|
||||
console.log(`\n📦 Skill: ${name}`)
|
||||
console.log(` From: ${cloneUrl}`)
|
||||
console.log(` Branch: ${branch}`)
|
||||
|
||||
// Step 1: Get remote commit SHA via git ls-remote (no clone)
|
||||
const remoteSha = lsRemoteSha(repo, branch)
|
||||
if (!remoteSha) {
|
||||
throw new Error(`git ls-remote failed for ${cloneUrl} (branch: ${branch})`)
|
||||
}
|
||||
console.log(` Remote commit: ${remoteSha.slice(0, 7)}`)
|
||||
|
||||
// Step 2: Compare with cached SHA — skip only if SHA matches AND cached files exist
|
||||
const cachedSha = await readCachedSha(name)
|
||||
const skillOutputDir = path.join(bundledSkillsDir, name)
|
||||
const hasCachedFiles = (await walk(skillOutputDir)).length > 0
|
||||
if (cachedSha && cachedSha === remoteSha && hasCachedFiles) {
|
||||
console.log(` ✓ Cached version matches remote, skipping download`)
|
||||
return { name, commitSha: remoteSha }
|
||||
}
|
||||
if (cachedSha) {
|
||||
console.log(` Cached: ${cachedSha.slice(0, 7)} → Remote: ${remoteSha.slice(0, 7)}, updating...`)
|
||||
}
|
||||
|
||||
// Step 3: Clone and extract files
|
||||
const cloneDir = path.join(bundledSkillsDir, `.clone-${name}`)
|
||||
|
||||
console.log(` git clone --depth 1 ${cloneUrl}`)
|
||||
|
||||
await fs.rm(cloneDir, { recursive: true, force: true })
|
||||
|
||||
const cloneResult = git('clone', '--depth', '1', '--branch', branch, cloneUrl, cloneDir)
|
||||
if (!cloneResult.ok) {
|
||||
throw new Error(`git clone failed: ${cloneResult.stderr}`)
|
||||
}
|
||||
|
||||
const srcDir = subdir ? path.join(cloneDir, subdir) : cloneDir
|
||||
await fs.rm(skillOutputDir, { recursive: true, force: true })
|
||||
await fs.cp(srcDir, skillOutputDir, { recursive: true })
|
||||
|
||||
await fs.rm(cloneDir, { recursive: true, force: true })
|
||||
|
||||
const skillMdPath = path.join(skillOutputDir, 'SKILL.md')
|
||||
try {
|
||||
await fs.access(skillMdPath)
|
||||
} catch {
|
||||
throw new Error(`Skill "${name}" missing SKILL.md`)
|
||||
}
|
||||
|
||||
const fileCount = (await walk(skillOutputDir)).length
|
||||
console.log(` ✓ ${fileCount} files copied`)
|
||||
return { name, commitSha: remoteSha }
|
||||
}
|
||||
|
||||
async function walk(dir: string, base = ''): Promise<string[]> {
|
||||
try {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true })
|
||||
const files: string[] = []
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name)
|
||||
const relativePath = base ? path.join(base, entry.name) : entry.name
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...(await walk(fullPath, relativePath)))
|
||||
} else {
|
||||
files.push(relativePath)
|
||||
}
|
||||
}
|
||||
return files
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async function generateBuiltinTs(
|
||||
skills: Array<{ name: string; commitSha: string | null }>,
|
||||
): Promise<void> {
|
||||
const outLines: string[] = [
|
||||
'// This file is auto-generated by scripts/generate-skills.ts',
|
||||
'// Do not edit manually',
|
||||
'',
|
||||
]
|
||||
|
||||
const skillEntries: string[] = []
|
||||
let fileIdx = 0
|
||||
|
||||
for (const { name, commitSha } of skills) {
|
||||
const skillDir = path.join(bundledSkillsDir, name)
|
||||
const files = await walk(skillDir)
|
||||
const fileEntries: string[] = []
|
||||
|
||||
for (const file of files) {
|
||||
const varName = `SKILL_FILE_${fileIdx++}`
|
||||
const content = await fs.readFile(path.join(skillDir, file), 'utf-8')
|
||||
const normalizedPath = file.replaceAll('\\', '/')
|
||||
outLines.push(`const ${varName} = ${JSON.stringify(content)}`)
|
||||
fileEntries.push(` ${JSON.stringify(normalizedPath)}: ${varName}`)
|
||||
}
|
||||
|
||||
skillEntries.push(` ${JSON.stringify(name)}: {\n${fileEntries.join(',\n')}\n }`)
|
||||
}
|
||||
|
||||
outLines.push('')
|
||||
outLines.push('export const BUNDLED_SKILLS: Record<string, Record<string, string>> = {')
|
||||
outLines.push(skillEntries.join(',\n'))
|
||||
outLines.push('}')
|
||||
outLines.push('')
|
||||
|
||||
const versions = skills
|
||||
.filter(s => s.commitSha)
|
||||
.map(s => ` ${JSON.stringify(s.name)}: ${JSON.stringify(s.commitSha)}`)
|
||||
|
||||
outLines.push('export const SKILL_VERSIONS: Record<string, string> = {')
|
||||
outLines.push(versions.join(',\n'))
|
||||
outLines.push('}')
|
||||
outLines.push('')
|
||||
|
||||
await fs.writeFile(builtinTsFile, outLines.join('\n') + '\n')
|
||||
console.log(`\n✓ Generated ${builtinTsFile}`)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('\nCSC — Downloading Builtin Skills\n')
|
||||
|
||||
await fs.mkdir(bundledSkillsDir, { recursive: true })
|
||||
|
||||
const results: Array<{ name: string; commitSha: string | null }> = []
|
||||
|
||||
for (const [name, config] of Object.entries(BUILTIN_SKILLS)) {
|
||||
try {
|
||||
const result = await downloadSkill(name, config)
|
||||
if (result) results.push(result)
|
||||
} catch (err) {
|
||||
const skillDir = path.join(bundledSkillsDir, name)
|
||||
const cached = await walk(skillDir)
|
||||
if (cached.length > 0) {
|
||||
console.warn(` ⚠ Download failed, using cached files for "${name}": ${err}`)
|
||||
results.push({ name, commitSha: null })
|
||||
} else {
|
||||
console.error(` ✗ Download failed and no cache for "${name}": ${err}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (results.length > 0) {
|
||||
await generateBuiltinTs(results)
|
||||
} else {
|
||||
console.warn('\n⚠ No skills downloaded. builtin.ts not updated.')
|
||||
}
|
||||
|
||||
console.log('\n💡 Run `bun run build` to rebuild the CLI.\n')
|
||||
}
|
||||
|
||||
main().catch(e => {
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
})
|
||||
|
|
@ -1,50 +1,27 @@
|
|||
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.js'
|
||||
import type { Command } from '../commands.js'
|
||||
import type { ToolUseContext } from '../Tool.js'
|
||||
import { isUltrareviewEnabled } from './review/ultrareviewEnabled.js'
|
||||
import { CommandLocale } from 'src/costrict/command/locales/index.js'
|
||||
import { PRIMARY_REVIEW_AGENT } from 'src/costrict/review/agent/builtin.js'
|
||||
|
||||
// Legal wants the explicit surface name plus a docs link visible before the
|
||||
// user triggers, so the description carries "Claude Code on the web" + URL.
|
||||
const CCR_TERMS_URL = 'https://costrict.ai/docs/en/claude-code-on-the-web'
|
||||
|
||||
const LOCAL_REVIEW_PROMPT = (args: string) => `
|
||||
You are an expert code reviewer. Follow these steps:
|
||||
|
||||
1. If no PR number is provided in the args, run \`gh pr list\` to show open PRs
|
||||
2. If a PR number is provided, run \`gh pr view <number>\` to get PR details
|
||||
3. Run \`gh pr diff <number>\` to get the diff
|
||||
4. Analyze the changes and provide a thorough code review that includes:
|
||||
- Overview of what the PR does
|
||||
- Analysis of code quality and style
|
||||
- Specific suggestions for improvements
|
||||
- Any potential issues or risks
|
||||
|
||||
Keep your review concise but thorough. Focus on:
|
||||
- Code correctness
|
||||
- Following project conventions
|
||||
- Performance implications
|
||||
- Test coverage
|
||||
- Security considerations
|
||||
|
||||
Format your review with clear sections and bullet points.
|
||||
|
||||
PR number: ${args}
|
||||
`
|
||||
|
||||
const review: Command = {
|
||||
type: 'prompt',
|
||||
name: 'review',
|
||||
description: 'Review a pull request',
|
||||
progressMessage: 'reviewing pull request',
|
||||
description: 'Review code for defects, security vulnerabilities, memory issues, and logic errors',
|
||||
progressMessage: 'reviewing code',
|
||||
contentLength: 0,
|
||||
source: 'builtin',
|
||||
async getPromptForCommand(args): Promise<ContentBlockParam[]> {
|
||||
return [{ type: 'text', text: LOCAL_REVIEW_PROMPT(args) }]
|
||||
agent: PRIMARY_REVIEW_AGENT || 'CoStrictReviewer',
|
||||
async getPromptForCommand(args, _context): Promise<ContentBlockParam[]> {
|
||||
return [{ type: 'text', text: CommandLocale.get('review').replace('$ARGUMENTS', args) }]
|
||||
},
|
||||
}
|
||||
|
||||
// /ultrareview is the ONLY entry point to the remote bughunter path —
|
||||
// /review stays purely local. local-jsx type renders the overage permission
|
||||
// dialog when free reviews are exhausted.
|
||||
const ultrareview: Command = {
|
||||
type: 'local-jsx',
|
||||
name: 'ultrareview',
|
||||
|
|
|
|||
|
|
@ -1,243 +1,18 @@
|
|||
import { parseFrontmatter } from '../utils/frontmatterParser.js'
|
||||
import { parseSlashCommandToolsFromFrontmatter } from '../utils/markdownConfigLoader.js'
|
||||
import { executeShellCommandsInPrompt } from '../utils/promptShellExecution.js'
|
||||
import { createMovedToPluginCommand } from './createMovedToPluginCommand.js'
|
||||
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.js'
|
||||
import type { Command } from '../commands.js'
|
||||
import type { ToolUseContext } from '../Tool.js'
|
||||
import { CommandLocale } from 'src/costrict/command/locales/index.js'
|
||||
|
||||
const SECURITY_REVIEW_MARKDOWN = `---
|
||||
allowed-tools: Bash(git diff:*), Bash(git status:*), Bash(git log:*), Bash(git show:*), Bash(git remote show:*), Read, Glob, Grep, LS, Task
|
||||
description: Complete a security review of the pending changes on the current branch
|
||||
---
|
||||
|
||||
You are a senior security engineer conducting a focused security review of the changes on this branch.
|
||||
|
||||
GIT STATUS:
|
||||
|
||||
\`\`\`
|
||||
!\`git status\`
|
||||
\`\`\`
|
||||
|
||||
FILES MODIFIED:
|
||||
|
||||
\`\`\`
|
||||
!\`git diff --name-only origin/HEAD...\`
|
||||
\`\`\`
|
||||
|
||||
COMMITS:
|
||||
|
||||
\`\`\`
|
||||
!\`git log --no-decorate origin/HEAD...\`
|
||||
\`\`\`
|
||||
|
||||
DIFF CONTENT:
|
||||
|
||||
\`\`\`
|
||||
!\`git diff origin/HEAD...\`
|
||||
\`\`\`
|
||||
|
||||
Review the complete diff above. This contains all code changes in the PR.
|
||||
|
||||
|
||||
OBJECTIVE:
|
||||
Perform a security-focused code review to identify HIGH-CONFIDENCE security vulnerabilities that could have real exploitation potential. This is not a general code review - focus ONLY on security implications newly added by this PR. Do not comment on existing security concerns.
|
||||
|
||||
CRITICAL INSTRUCTIONS:
|
||||
1. MINIMIZE FALSE POSITIVES: Only flag issues where you're >80% confident of actual exploitability
|
||||
2. AVOID NOISE: Skip theoretical issues, style concerns, or low-impact findings
|
||||
3. FOCUS ON IMPACT: Prioritize vulnerabilities that could lead to unauthorized access, data breaches, or system compromise
|
||||
4. EXCLUSIONS: Do NOT report the following issue types:
|
||||
- Denial of Service (DOS) vulnerabilities, even if they allow service disruption
|
||||
- Secrets or sensitive data stored on disk (these are handled by other processes)
|
||||
- Rate limiting or resource exhaustion issues
|
||||
|
||||
SECURITY CATEGORIES TO EXAMINE:
|
||||
|
||||
**Input Validation Vulnerabilities:**
|
||||
- SQL injection via unsanitized user input
|
||||
- Command injection in system calls or subprocesses
|
||||
- XXE injection in XML parsing
|
||||
- Template injection in templating engines
|
||||
- NoSQL injection in database queries
|
||||
- Path traversal in file operations
|
||||
|
||||
**Authentication & Authorization Issues:**
|
||||
- Authentication bypass logic
|
||||
- Privilege escalation paths
|
||||
- Session management flaws
|
||||
- JWT token vulnerabilities
|
||||
- Authorization logic bypasses
|
||||
|
||||
**Crypto & Secrets Management:**
|
||||
- Hardcoded API keys, passwords, or tokens
|
||||
- Weak cryptographic algorithms or implementations
|
||||
- Improper key storage or management
|
||||
- Cryptographic randomness issues
|
||||
- Certificate validation bypasses
|
||||
|
||||
**Injection & Code Execution:**
|
||||
- Remote code execution via deseralization
|
||||
- Pickle injection in Python
|
||||
- YAML deserialization vulnerabilities
|
||||
- Eval injection in dynamic code execution
|
||||
- XSS vulnerabilities in web applications (reflected, stored, DOM-based)
|
||||
|
||||
**Data Exposure:**
|
||||
- Sensitive data logging or storage
|
||||
- PII handling violations
|
||||
- API endpoint data leakage
|
||||
- Debug information exposure
|
||||
|
||||
Additional notes:
|
||||
- Even if something is only exploitable from the local network, it can still be a HIGH severity issue
|
||||
|
||||
ANALYSIS METHODOLOGY:
|
||||
|
||||
Phase 1 - Repository Context Research (Use file search tools):
|
||||
- Identify existing security frameworks and libraries in use
|
||||
- Look for established secure coding patterns in the codebase
|
||||
- Examine existing sanitization and validation patterns
|
||||
- Understand the project's security model and threat model
|
||||
|
||||
Phase 2 - Comparative Analysis:
|
||||
- Compare new code changes against existing security patterns
|
||||
- Identify deviations from established secure practices
|
||||
- Look for inconsistent security implementations
|
||||
- Flag code that introduces new attack surfaces
|
||||
|
||||
Phase 3 - Vulnerability Assessment:
|
||||
- Examine each modified file for security implications
|
||||
- Trace data flow from user inputs to sensitive operations
|
||||
- Look for privilege boundaries being crossed unsafely
|
||||
- Identify injection points and unsafe deserialization
|
||||
|
||||
REQUIRED OUTPUT FORMAT:
|
||||
|
||||
You MUST output your findings in markdown. The markdown output should contain the file, line number, severity, category (e.g. \`sql_injection\` or \`xss\`), description, exploit scenario, and fix recommendation.
|
||||
|
||||
For example:
|
||||
|
||||
# Vuln 1: XSS: \`foo.py:42\`
|
||||
|
||||
* Severity: High
|
||||
* Description: User input from \`username\` parameter is directly interpolated into HTML without escaping, allowing reflected XSS attacks
|
||||
* Exploit Scenario: Attacker crafts URL like /bar?q=<script>alert(document.cookie)</script> to execute JavaScript in victim's browser, enabling session hijacking or data theft
|
||||
* Recommendation: Use Flask's escape() function or Jinja2 templates with auto-escaping enabled for all user inputs rendered in HTML
|
||||
|
||||
SEVERITY GUIDELINES:
|
||||
- **HIGH**: Directly exploitable vulnerabilities leading to RCE, data breach, or authentication bypass
|
||||
- **MEDIUM**: Vulnerabilities requiring specific conditions but with significant impact
|
||||
- **LOW**: Defense-in-depth issues or lower-impact vulnerabilities
|
||||
|
||||
CONFIDENCE SCORING:
|
||||
- 0.9-1.0: Certain exploit path identified, tested if possible
|
||||
- 0.8-0.9: Clear vulnerability pattern with known exploitation methods
|
||||
- 0.7-0.8: Suspicious pattern requiring specific conditions to exploit
|
||||
- Below 0.7: Don't report (too speculative)
|
||||
|
||||
FINAL REMINDER:
|
||||
Focus on HIGH and MEDIUM findings only. Better to miss some theoretical issues than flood the report with false positives. Each finding should be something a security engineer would confidently raise in a PR review.
|
||||
|
||||
FALSE POSITIVE FILTERING:
|
||||
|
||||
> You do not need to run commands to reproduce the vulnerability, just read the code to determine if it is a real vulnerability. Do not use the bash tool or write to any files.
|
||||
>
|
||||
> HARD EXCLUSIONS - Automatically exclude findings matching these patterns:
|
||||
> 1. Denial of Service (DOS) vulnerabilities or resource exhaustion attacks.
|
||||
> 2. Secrets or credentials stored on disk if they are otherwise secured.
|
||||
> 3. Rate limiting concerns or service overload scenarios.
|
||||
> 4. Memory consumption or CPU exhaustion issues.
|
||||
> 5. Lack of input validation on non-security-critical fields without proven security impact.
|
||||
> 6. Input sanitization concerns for GitHub Action workflows unless they are clearly triggerable via untrusted input.
|
||||
> 7. A lack of hardening measures. Code is not expected to implement all security best practices, only flag concrete vulnerabilities.
|
||||
> 8. Race conditions or timing attacks that are theoretical rather than practical issues. Only report a race condition if it is concretely problematic.
|
||||
> 9. Vulnerabilities related to outdated third-party libraries. These are managed separately and should not be reported here.
|
||||
> 10. Memory safety issues such as buffer overflows or use-after-free-vulnerabilities are impossible in rust. Do not report memory safety issues in rust or any other memory safe languages.
|
||||
> 11. Files that are only unit tests or only used as part of running tests.
|
||||
> 12. Log spoofing concerns. Outputting un-sanitized user input to logs is not a vulnerability.
|
||||
> 13. SSRF vulnerabilities that only control the path. SSRF is only a concern if it can control the host or protocol.
|
||||
> 14. Including user-controlled content in AI system prompts is not a vulnerability.
|
||||
> 15. Regex injection. Injecting untrusted content into a regex is not a vulnerability.
|
||||
> 16. Regex DOS concerns.
|
||||
> 16. Insecure documentation. Do not report any findings in documentation files such as markdown files.
|
||||
> 17. A lack of audit logs is not a vulnerability.
|
||||
>
|
||||
> PRECEDENTS -
|
||||
> 1. Logging high value secrets in plaintext is a vulnerability. Logging URLs is assumed to be safe.
|
||||
> 2. UUIDs can be assumed to be unguessable and do not need to be validated.
|
||||
> 3. Environment variables and CLI flags are trusted values. Attackers are generally not able to modify them in a secure environment. Any attack that relies on controlling an environment variable is invalid.
|
||||
> 4. Resource management issues such as memory or file descriptor leaks are not valid.
|
||||
> 5. Subtle or low impact web vulnerabilities such as tabnabbing, XS-Leaks, prototype pollution, and open redirects should not be reported unless they are extremely high confidence.
|
||||
> 6. React and Angular are generally secure against XSS. These frameworks do not need to sanitize or escape user input unless it is using dangerouslySetInnerHTML, bypassSecurityTrustHtml, or similar methods. Do not report XSS vulnerabilities in React or Angular components or tsx files unless they are using unsafe methods.
|
||||
> 7. Most vulnerabilities in github action workflows are not exploitable in practice. Before validating a github action workflow vulnerability ensure it is concrete and has a very specific attack path.
|
||||
> 8. A lack of permission checking or authentication in client-side JS/TS code is not a vulnerability. Client-side code is not trusted and does not need to implement these checks, they are handled on the server-side. The same applies to all flows that send untrusted data to the backend, the backend is responsible for validating and sanitizing all inputs.
|
||||
> 9. Only include MEDIUM findings if they are obvious and concrete issues.
|
||||
> 10. Most vulnerabilities in ipython notebooks (*.ipynb files) are not exploitable in practice. Before validating a notebook vulnerability ensure it is concrete and has a very specific attack path where untrusted input can trigger the vulnerability.
|
||||
> 11. Logging non-PII data is not a vulnerability even if the data may be sensitive. Only report logging vulnerabilities if they expose sensitive information such as secrets, passwords, or personally identifiable information (PII).
|
||||
> 12. Command injection vulnerabilities in shell scripts are generally not exploitable in practice since shell scripts generally do not run with untrusted user input. Only report command injection vulnerabilities in shell scripts if they are concrete and have a very specific attack path for untrusted input.
|
||||
>
|
||||
> SIGNAL QUALITY CRITERIA - For remaining findings, assess:
|
||||
> 1. Is there a concrete, exploitable vulnerability with a clear attack path?
|
||||
> 2. Does this represent a real security risk vs theoretical best practice?
|
||||
> 3. Are there specific code locations and reproduction steps?
|
||||
> 4. Would this finding be actionable for a security team?
|
||||
>
|
||||
> For each finding, assign a confidence score from 1-10:
|
||||
> - 1-3: Low confidence, likely false positive or noise
|
||||
> - 4-6: Medium confidence, needs investigation
|
||||
> - 7-10: High confidence, likely true vulnerability
|
||||
|
||||
START ANALYSIS:
|
||||
|
||||
Begin your analysis now. Do this in 3 steps:
|
||||
|
||||
1. Use a sub-task to identify vulnerabilities. Use the repository exploration tools to understand the codebase context, then analyze the PR changes for security implications. In the prompt for this sub-task, include all of the above.
|
||||
2. Then for each vulnerability identified by the above sub-task, create a new sub-task to filter out false-positives. Launch these sub-tasks as parallel sub-tasks. In the prompt for these sub-tasks, include everything in the "FALSE POSITIVE FILTERING" instructions.
|
||||
3. Filter out any vulnerabilities where the sub-task reported a confidence less than 8.
|
||||
|
||||
Your final reply must contain the markdown report and nothing else.`
|
||||
|
||||
export default createMovedToPluginCommand({
|
||||
const securityReview: Command = {
|
||||
type: 'prompt',
|
||||
name: 'security-review',
|
||||
description:
|
||||
'Complete a security review of the pending changes on the current branch',
|
||||
description: 'Complete a security review of the pending changes on the current branch',
|
||||
progressMessage: 'analyzing code changes for security risks',
|
||||
pluginName: 'security-review',
|
||||
pluginCommand: 'security-review',
|
||||
async getPromptWhileMarketplaceIsPrivate(_args, context) {
|
||||
// Parse frontmatter from the markdown
|
||||
const parsed = parseFrontmatter(SECURITY_REVIEW_MARKDOWN)
|
||||
|
||||
// Parse allowed tools from frontmatter
|
||||
const allowedTools = parseSlashCommandToolsFromFrontmatter(
|
||||
parsed.frontmatter['allowed-tools'],
|
||||
)
|
||||
|
||||
// Execute bash commands in the prompt
|
||||
const processedContent = await executeShellCommandsInPrompt(
|
||||
parsed.content,
|
||||
{
|
||||
...context,
|
||||
getAppState() {
|
||||
const appState = context.getAppState()
|
||||
return {
|
||||
...appState,
|
||||
toolPermissionContext: {
|
||||
...appState.toolPermissionContext,
|
||||
alwaysAllowRules: {
|
||||
...appState.toolPermissionContext.alwaysAllowRules,
|
||||
command: allowedTools,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
'security-review',
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'text',
|
||||
text: processedContent,
|
||||
},
|
||||
]
|
||||
contentLength: 0,
|
||||
source: 'builtin',
|
||||
async getPromptForCommand(args, _context): Promise<ContentBlockParam[]> {
|
||||
return [{ type: 'text', text: CommandLocale.get('security-review').replace('$ARGUMENTS', args) }]
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default securityReview
|
||||
|
|
|
|||
5
src/costrict/command/locales/en/review.txt
Normal file
5
src/costrict/command/locales/en/review.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Code Review
|
||||
|
||||
Please perform a code review on: $ARGUMENTS
|
||||
|
||||
Please respond and write all files in English throughout the entire process.
|
||||
5
src/costrict/command/locales/en/security-review.txt
Normal file
5
src/costrict/command/locales/en/security-review.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Code Security Review
|
||||
|
||||
Please use the Skill tool to load the `security-review` skill to perform a security review on: $ARGUMENTS
|
||||
|
||||
Please respond and write all files in English throughout the entire process.
|
||||
20
src/costrict/command/locales/index.ts
Normal file
20
src/costrict/command/locales/index.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { getResolvedLanguage } from 'src/utils/language.js'
|
||||
import ZH_CN_REVIEW from './zh-CN/review.txt'
|
||||
import EN_REVIEW from './en/review.txt'
|
||||
import ZH_CN_SECURITY_REVIEW from './zh-CN/security-review.txt'
|
||||
import EN_SECURITY_REVIEW from './en/security-review.txt'
|
||||
|
||||
const LOCALE_MAP: Record<string, string> = { zh: 'zh-CN', en: 'en' }
|
||||
|
||||
const TEMPLATES: Record<string, Record<string, string>> = {
|
||||
review: { 'zh-CN': ZH_CN_REVIEW, en: EN_REVIEW },
|
||||
'security-review': { 'zh-CN': ZH_CN_SECURITY_REVIEW, en: EN_SECURITY_REVIEW },
|
||||
}
|
||||
|
||||
export namespace CommandLocale {
|
||||
export function get(name: string): string {
|
||||
const lang = getResolvedLanguage()
|
||||
const locale = LOCALE_MAP[lang] ?? 'zh-CN'
|
||||
return TEMPLATES[name]?.[locale] ?? TEMPLATES[name]?.en ?? ''
|
||||
}
|
||||
}
|
||||
5
src/costrict/command/locales/zh-CN/review.txt
Normal file
5
src/costrict/command/locales/zh-CN/review.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# 代码审查
|
||||
|
||||
请对以下内容执行代码审查:$ARGUMENTS
|
||||
|
||||
全程请使用中文进行回答与文件写入。
|
||||
5
src/costrict/command/locales/zh-CN/security-review.txt
Normal file
5
src/costrict/command/locales/zh-CN/security-review.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# 安全代码审查
|
||||
|
||||
请使用 Skill 工具加载 `security-review` 技能来对以下内容执行安全代码审查:$ARGUMENTS
|
||||
|
||||
全程请使用中文进行回答与文件写入。
|
||||
87
src/costrict/review/extension.ts
Normal file
87
src/costrict/review/extension.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
/**
|
||||
* CoStrict Skill Extension
|
||||
*
|
||||
* Initializes builtin review skills by extracting them from embedded
|
||||
* SKILL_FILES to a cache directory on disk.
|
||||
*
|
||||
* Version tracking uses commit SHA + locale in a .version file.
|
||||
* Skills are re-extracted when version or locale changes.
|
||||
*/
|
||||
|
||||
import path from 'path'
|
||||
import { writeFile, readFile, rm, mkdir, stat } from 'fs/promises'
|
||||
import { getResolvedLanguage } from 'src/utils/language.js'
|
||||
import * as Builtin from './skill/builtin.js'
|
||||
|
||||
const LOCALE_MAP: Record<string, string> = { zh: 'zh-CN', en: 'en' }
|
||||
|
||||
function getSkillCacheDir(): string {
|
||||
const home = process.env.HOME ?? process.env.USERPROFILE ?? ''
|
||||
return path.join(home, '.claude', 'skills')
|
||||
}
|
||||
|
||||
function getVersionFilePath(skillDir: string): string {
|
||||
return path.join(skillDir, '.version')
|
||||
}
|
||||
|
||||
async function getInstalledVersion(skillDir: string): Promise<string | null> {
|
||||
try {
|
||||
const content = await readFile(getVersionFilePath(skillDir), 'utf-8')
|
||||
return content.trim()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function needsUpdate(skillDir: string, skillName: string, locale: string): Promise<boolean> {
|
||||
const builtinVersion = Builtin.getBuiltinSkillVersion(skillName)
|
||||
if (!builtinVersion) return true
|
||||
|
||||
const installedVersion = await getInstalledVersion(skillDir)
|
||||
const expectedVersion = `${builtinVersion}:${locale}`
|
||||
return installedVersion !== expectedVersion
|
||||
}
|
||||
|
||||
async function writeVersionFile(skillDir: string, skillName: string, locale: string): Promise<void> {
|
||||
const builtinVersion = Builtin.getBuiltinSkillVersion(skillName)
|
||||
if (!builtinVersion) return
|
||||
|
||||
await mkdir(skillDir, { recursive: true })
|
||||
await writeFile(getVersionFilePath(skillDir), `${builtinVersion}:${locale}`, 'utf-8')
|
||||
}
|
||||
|
||||
export async function initializeBuiltinSkills(): Promise<void> {
|
||||
const lang = getResolvedLanguage()
|
||||
const locale = LOCALE_MAP[lang] ?? 'zh-CN'
|
||||
|
||||
const cacheDir = getSkillCacheDir()
|
||||
const skillNames = Builtin.listBuiltinSkills()
|
||||
|
||||
for (const name of skillNames) {
|
||||
const skillDir = path.join(cacheDir, name)
|
||||
|
||||
const dirExists = await stat(skillDir).then(s => s.isDirectory()).catch(() => false)
|
||||
|
||||
if (dirExists) {
|
||||
const updateNeeded = await needsUpdate(skillDir, name, locale)
|
||||
if (!updateNeeded) continue
|
||||
|
||||
try {
|
||||
await rm(skillDir, { recursive: true, force: true })
|
||||
} catch {
|
||||
// Continue with copy over existing files
|
||||
}
|
||||
}
|
||||
|
||||
await Builtin.extractBundledSkill(name, skillDir, locale)
|
||||
await writeVersionFile(skillDir, name, locale)
|
||||
|
||||
const skillFiles = Builtin.listSkillFiles(name, locale)
|
||||
const builtinVersion = Builtin.getBuiltinSkillVersion(name)
|
||||
console.log(` [review] initialized skill "${name}" (${locale}, ${skillFiles.length} files, v${builtinVersion?.slice(0, 7)})`)
|
||||
}
|
||||
}
|
||||
|
||||
export function getBuiltinSkillsDir(): string {
|
||||
return getSkillCacheDir()
|
||||
}
|
||||
15
src/costrict/review/index.ts
Normal file
15
src/costrict/review/index.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* CoStrict Review Module
|
||||
*
|
||||
* Provides builtin review skills and agents that are embedded
|
||||
* in the binary and extracted to cache on first run.
|
||||
*/
|
||||
|
||||
export * as Extension from './extension.js'
|
||||
export * as SkillBuiltin from './skill/builtin.js'
|
||||
export {
|
||||
REVIEW_AGENTS,
|
||||
AGENT_VERSIONS,
|
||||
PRIMARY_REVIEW_AGENT,
|
||||
SUB_REVIEW_AGENT,
|
||||
} from './agent/builtin.js'
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,20 +0,0 @@
|
|||
import { registerBundledSkill } from 'src/skills/bundledSkills.js'
|
||||
import { BUNDLED_SKILLS } from './builtin.js'
|
||||
|
||||
export function registerCodeReviewSecuritySkill(): void {
|
||||
const files = BUNDLED_SKILLS['security-review'] ?? {}
|
||||
const skillMd = files['SKILL.md'] ?? ''
|
||||
|
||||
registerBundledSkill({
|
||||
name: 'security-review',
|
||||
description: 'CoStrict Security — identifies code vulnerabilities including SQL injection, XSS, command injection, SSRF, path traversal, deserialization, and business logic flaws',
|
||||
whenToUse:
|
||||
'Use when the user requests a code audit, security audit, vulnerability scan, or wants to review vulnerabilities before deployment. Also triggers on /security-review.',
|
||||
userInvocable: true,
|
||||
disableModelInvocation: true,
|
||||
files,
|
||||
async getPromptForCommand() {
|
||||
return [{ type: 'text', text: skillMd }]
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { feature } from 'bun:bundle'
|
||||
import { shouldAutoEnableClaudeInChrome } from 'src/utils/claudeInChrome/setup.js'
|
||||
import { registerCodeReviewSecuritySkill } from 'src/costrict/skills/codeReviewSecurity.js'
|
||||
import { initializeBuiltinSkills } from 'src/costrict/review/extension.js'
|
||||
import { registerBatchSkill } from './batch.js'
|
||||
import { registerClaudeInChromeSkill } from './claudeInChrome.js'
|
||||
import { registerDebugSkill } from './debug.js'
|
||||
|
|
@ -30,7 +30,9 @@ import { registerTddSkill } from 'src/costrict/skills/tdd.js'
|
|||
* 3. Import and call that function here
|
||||
*/
|
||||
export function initBundledSkills(): void {
|
||||
registerCodeReviewSecuritySkill()
|
||||
// Initialize builtin review skills (extract to cache if needed)
|
||||
initializeBuiltinSkills().catch(() => {})
|
||||
|
||||
registerUpdateConfigSkill()
|
||||
registerProjectWikiSkill()
|
||||
registerTddSkill()
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user