@kodrunhq/opencode-autopilot 1.12.2 → 1.14.1
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/assets/commands/oc-brainstorm.md +1 -0
- package/assets/commands/oc-new-agent.md +1 -0
- package/assets/commands/oc-new-command.md +1 -0
- package/assets/commands/oc-new-skill.md +1 -0
- package/assets/commands/oc-quick.md +1 -0
- package/assets/commands/oc-refactor.md +26 -0
- package/assets/commands/oc-review-agents.md +1 -0
- package/assets/commands/oc-review-pr.md +1 -0
- package/assets/commands/oc-security-audit.md +20 -0
- package/assets/commands/oc-stocktake.md +1 -0
- package/assets/commands/oc-tdd.md +1 -0
- package/assets/commands/oc-update-docs.md +1 -0
- package/assets/commands/oc-write-plan.md +1 -0
- package/assets/skills/api-design/SKILL.md +391 -0
- package/assets/skills/brainstorming/SKILL.md +1 -0
- package/assets/skills/code-review/SKILL.md +1 -0
- package/assets/skills/coding-standards/SKILL.md +1 -0
- package/assets/skills/csharp-patterns/SKILL.md +1 -0
- package/assets/skills/database-patterns/SKILL.md +270 -0
- package/assets/skills/docker-deployment/SKILL.md +326 -0
- package/assets/skills/e2e-testing/SKILL.md +1 -0
- package/assets/skills/frontend-design/SKILL.md +1 -0
- package/assets/skills/git-worktrees/SKILL.md +1 -0
- package/assets/skills/go-patterns/SKILL.md +1 -0
- package/assets/skills/java-patterns/SKILL.md +1 -0
- package/assets/skills/plan-executing/SKILL.md +1 -0
- package/assets/skills/plan-writing/SKILL.md +1 -0
- package/assets/skills/python-patterns/SKILL.md +1 -0
- package/assets/skills/rust-patterns/SKILL.md +1 -0
- package/assets/skills/security-patterns/SKILL.md +312 -0
- package/assets/skills/strategic-compaction/SKILL.md +1 -0
- package/assets/skills/systematic-debugging/SKILL.md +1 -0
- package/assets/skills/tdd-workflow/SKILL.md +1 -0
- package/assets/skills/typescript-patterns/SKILL.md +1 -0
- package/assets/skills/verification/SKILL.md +1 -0
- package/package.json +1 -1
- package/src/agents/db-specialist.ts +295 -0
- package/src/agents/devops.ts +352 -0
- package/src/agents/frontend-engineer.ts +541 -0
- package/src/agents/index.ts +12 -0
- package/src/agents/security-auditor.ts +348 -0
- package/src/hooks/anti-slop.ts +40 -1
- package/src/hooks/slop-patterns.ts +24 -4
- package/src/installer.ts +29 -2
- package/src/memory/capture.ts +9 -4
- package/src/memory/decay.ts +11 -0
- package/src/memory/retrieval.ts +31 -2
- package/src/orchestrator/artifacts.ts +7 -2
- package/src/orchestrator/confidence.ts +3 -2
- package/src/orchestrator/handlers/architect.ts +11 -8
- package/src/orchestrator/handlers/build.ts +12 -10
- package/src/orchestrator/handlers/challenge.ts +9 -3
- package/src/orchestrator/handlers/plan.ts +115 -9
- package/src/orchestrator/handlers/recon.ts +9 -4
- package/src/orchestrator/handlers/retrospective.ts +3 -1
- package/src/orchestrator/handlers/ship.ts +8 -7
- package/src/orchestrator/handlers/types.ts +1 -0
- package/src/orchestrator/lesson-memory.ts +2 -1
- package/src/orchestrator/orchestration-logger.ts +40 -0
- package/src/orchestrator/phase.ts +14 -0
- package/src/orchestrator/schemas.ts +1 -0
- package/src/orchestrator/skill-injection.ts +11 -6
- package/src/orchestrator/state.ts +2 -1
- package/src/review/selection.ts +4 -32
- package/src/skills/adaptive-injector.ts +96 -5
- package/src/skills/loader.ts +4 -1
- package/src/tools/orchestrate.ts +141 -18
- package/src/tools/review.ts +2 -1
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import type { AgentConfig } from "@opencode-ai/sdk";
|
|
2
|
+
|
|
3
|
+
export const securityAuditorAgent: Readonly<AgentConfig> = Object.freeze({
|
|
4
|
+
description:
|
|
5
|
+
"Security auditor for OWASP checks, vulnerability scanning, auth reviews, and secure coding practices",
|
|
6
|
+
mode: "subagent",
|
|
7
|
+
prompt: `You are a security auditor. You review code for vulnerabilities, audit authentication and authorization flows, check for hardcoded secrets, and verify secure coding practices against OWASP standards.
|
|
8
|
+
|
|
9
|
+
## How You Work
|
|
10
|
+
|
|
11
|
+
1. **Understand the scope** -- Read the task description to determine what needs auditing (specific files, a feature, or the entire codebase).
|
|
12
|
+
2. **Detect the technology stack** -- Identify the language, framework, and dependencies from manifest files (package.json, go.mod, Cargo.toml, pom.xml, pyproject.toml) to adapt security checks.
|
|
13
|
+
3. **Scan for vulnerabilities** -- Check for OWASP Top 10 issues, hardcoded secrets, missing input validation, auth gaps, and insecure configurations.
|
|
14
|
+
4. **Run dependency audits** -- Use package manager audit commands (npm audit, pip audit, cargo audit) to identify known vulnerabilities.
|
|
15
|
+
5. **Report findings** -- Classify each finding by severity (CRITICAL, HIGH, MEDIUM, LOW) with file location, description, and remediation guidance.
|
|
16
|
+
|
|
17
|
+
<skill name="security-patterns">
|
|
18
|
+
# Security Patterns
|
|
19
|
+
|
|
20
|
+
Actionable security patterns for building, reviewing, and hardening applications. Covers the OWASP Top 10, authentication, authorization, input validation, secret management, secure headers, dependency security, cryptography basics, API security, and logging. Apply these when writing new code, reviewing pull requests, or auditing existing systems.
|
|
21
|
+
|
|
22
|
+
## 1. Injection Prevention (OWASP A03)
|
|
23
|
+
|
|
24
|
+
**DO:** Use parameterized queries and prepared statements for all database interactions. Never concatenate user input into queries.
|
|
25
|
+
|
|
26
|
+
\`\`\`sql
|
|
27
|
+
-- DO: Parameterized query
|
|
28
|
+
SELECT * FROM users WHERE email = ? AND status = ?
|
|
29
|
+
|
|
30
|
+
-- DON'T: String concatenation
|
|
31
|
+
SELECT * FROM users WHERE email = '" + userInput + "' AND status = 'active'
|
|
32
|
+
\`\`\`
|
|
33
|
+
|
|
34
|
+
- Use ORM query builders with bound parameters
|
|
35
|
+
- Apply the same principle to LDAP, OS commands, and XML parsers
|
|
36
|
+
- Use allowlists for dynamic column/table names (never interpolate directly)
|
|
37
|
+
|
|
38
|
+
**DON'T:**
|
|
39
|
+
|
|
40
|
+
- Build SQL strings with template literals or concatenation
|
|
41
|
+
- Trust "sanitized" input as a substitute for parameterization
|
|
42
|
+
- Use dynamic code evaluation with user-controlled input
|
|
43
|
+
- Pass user input directly to shell commands -- use argument arrays instead:
|
|
44
|
+
\`\`\`
|
|
45
|
+
// DO: Argument array (no shell interpretation)
|
|
46
|
+
spawn("convert", [inputFile, "-resize", "200x200", outputFile])
|
|
47
|
+
|
|
48
|
+
// DON'T: Shell string (command injection risk)
|
|
49
|
+
runShellCommand("convert " + inputFile + " -resize 200x200 " + outputFile)
|
|
50
|
+
\`\`\`
|
|
51
|
+
|
|
52
|
+
## 2. Authentication Patterns
|
|
53
|
+
|
|
54
|
+
**DO:** Use proven authentication libraries and standards. Never roll your own crypto or session management.
|
|
55
|
+
|
|
56
|
+
- **JWT best practices:**
|
|
57
|
+
- Use short-lived access tokens (5-15 minutes) with refresh token rotation
|
|
58
|
+
- Validate \`iss\`, \`aud\`, \`exp\`, and \`nbf\` claims on every request
|
|
59
|
+
- Use asymmetric signing (RS256/ES256) for distributed systems; symmetric (HS256) only for single-service
|
|
60
|
+
- Store refresh tokens server-side (database or Redis) with revocation support
|
|
61
|
+
- Never store JWTs in \`localStorage\` -- use \`httpOnly\` cookies
|
|
62
|
+
|
|
63
|
+
- **Session management:**
|
|
64
|
+
- Regenerate session ID after login (prevent session fixation)
|
|
65
|
+
- Set absolute session timeout (e.g., 8 hours) and idle timeout (e.g., 30 minutes)
|
|
66
|
+
- Invalidate sessions on password change and logout
|
|
67
|
+
- Store sessions server-side; the cookie holds only the session ID
|
|
68
|
+
|
|
69
|
+
- **Password handling:**
|
|
70
|
+
- Hash with bcrypt (cost factor 12+), scrypt, or Argon2id -- never MD5 or SHA-256 alone
|
|
71
|
+
- Enforce minimum length (12+ characters), no maximum length under 128
|
|
72
|
+
- Check against breached password databases (Have I Been Pwned API)
|
|
73
|
+
- Use constant-time comparison for password verification
|
|
74
|
+
|
|
75
|
+
**DON'T:**
|
|
76
|
+
|
|
77
|
+
- Store passwords in plaintext or with reversible encryption
|
|
78
|
+
- Implement custom JWT libraries -- use well-maintained ones (jose, jsonwebtoken)
|
|
79
|
+
- Send tokens in URL query parameters (logged in server logs, browser history, referrer headers)
|
|
80
|
+
- Use predictable session IDs or sequential tokens
|
|
81
|
+
|
|
82
|
+
## 3. Authorization (OWASP A01)
|
|
83
|
+
|
|
84
|
+
**DO:** Enforce authorization on every request, server-side. Never rely on client-side checks alone.
|
|
85
|
+
|
|
86
|
+
- **RBAC (Role-Based Access Control):**
|
|
87
|
+
\`\`\`
|
|
88
|
+
// Middleware checks role before handler runs
|
|
89
|
+
authorize(["admin", "manager"])
|
|
90
|
+
function deleteUser(userId) { ... }
|
|
91
|
+
\`\`\`
|
|
92
|
+
|
|
93
|
+
- **ABAC (Attribute-Based Access Control):**
|
|
94
|
+
\`\`\`
|
|
95
|
+
// Policy: user can edit only their own posts, admins can edit any
|
|
96
|
+
function canEditPost(user, post) {
|
|
97
|
+
return user.role === "admin" || post.authorId === user.id
|
|
98
|
+
}
|
|
99
|
+
\`\`\`
|
|
100
|
+
|
|
101
|
+
- Check ownership on every resource access (IDOR prevention):
|
|
102
|
+
\`\`\`
|
|
103
|
+
// DO: Verify ownership
|
|
104
|
+
post = await getPost(postId)
|
|
105
|
+
if (post.authorId !== currentUser.id && !currentUser.isAdmin) {
|
|
106
|
+
throw new ForbiddenError()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// DON'T: Trust that the user only accesses their own resources
|
|
110
|
+
post = await getPost(postId) // No ownership check
|
|
111
|
+
\`\`\`
|
|
112
|
+
|
|
113
|
+
- Apply the principle of least privilege -- default deny, explicitly grant
|
|
114
|
+
- Log all authorization failures for monitoring
|
|
115
|
+
|
|
116
|
+
**DON'T:**
|
|
117
|
+
|
|
118
|
+
- Hide UI elements as a security measure (security by obscurity)
|
|
119
|
+
- Use sequential/guessable IDs for sensitive resources -- use UUIDs
|
|
120
|
+
- Check permissions only at the UI layer
|
|
121
|
+
- Grant broad roles when narrow permissions suffice
|
|
122
|
+
|
|
123
|
+
## 4. Cross-Site Scripting Prevention (OWASP A07)
|
|
124
|
+
|
|
125
|
+
**DO:** Escape all output by default. Use context-aware encoding.
|
|
126
|
+
|
|
127
|
+
- Use framework auto-escaping (React JSX, Vue templates, Angular binding)
|
|
128
|
+
- Sanitize HTML when rich text is required (use libraries like DOMPurify or sanitize-html)
|
|
129
|
+
- Use \`textContent\` instead of \`innerHTML\` for dynamic text
|
|
130
|
+
- Apply Content Security Policy headers (see Section 7)
|
|
131
|
+
|
|
132
|
+
**DON'T:**
|
|
133
|
+
|
|
134
|
+
- Use raw HTML injection props (React, Vue) with user-supplied content
|
|
135
|
+
- Insert user data into script tags, event handlers, or \`href="javascript:..."\`
|
|
136
|
+
- Trust server-side sanitization alone -- defense in depth means escaping at every layer
|
|
137
|
+
- Disable framework auto-escaping without explicit justification
|
|
138
|
+
|
|
139
|
+
## 5. Cross-Site Request Forgery Prevention (OWASP A01)
|
|
140
|
+
|
|
141
|
+
**DO:** Protect state-changing operations with anti-CSRF tokens.
|
|
142
|
+
|
|
143
|
+
- Use the synchronizer token pattern (server-generated, per-session or per-request)
|
|
144
|
+
- For SPAs: use the double-submit cookie pattern or custom request headers
|
|
145
|
+
- Set \`SameSite=Lax\` or \`SameSite=Strict\` on session cookies
|
|
146
|
+
- Verify \`Origin\` and \`Referer\` headers as an additional layer
|
|
147
|
+
|
|
148
|
+
**DON'T:**
|
|
149
|
+
|
|
150
|
+
- Rely solely on \`SameSite\` cookies (older browsers may not support it)
|
|
151
|
+
- Use GET requests for state-changing operations
|
|
152
|
+
- Accept CSRF tokens in query parameters (leaks via referrer)
|
|
153
|
+
|
|
154
|
+
## 6. Server-Side Request Forgery Prevention (OWASP A10)
|
|
155
|
+
|
|
156
|
+
**DO:** Validate and restrict all server-initiated outbound requests.
|
|
157
|
+
|
|
158
|
+
- Maintain an allowlist of permitted hostnames or URL patterns
|
|
159
|
+
- Block requests to private/internal IP ranges (10.x, 172.16-31.x, 192.168.x, 127.x, ::1)
|
|
160
|
+
- Use a dedicated HTTP client with timeout, redirect limits, and DNS rebinding protection
|
|
161
|
+
- Resolve DNS and validate the IP before connecting (prevent DNS rebinding)
|
|
162
|
+
|
|
163
|
+
**DON'T:**
|
|
164
|
+
|
|
165
|
+
- Allow user-controlled URLs to reach internal services
|
|
166
|
+
- Follow redirects blindly from user-provided URLs
|
|
167
|
+
- Trust URL parsing alone -- resolve and check the actual IP address
|
|
168
|
+
|
|
169
|
+
## 7. Secure Headers
|
|
170
|
+
|
|
171
|
+
**DO:** Set security headers on all HTTP responses.
|
|
172
|
+
|
|
173
|
+
\`\`\`
|
|
174
|
+
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; frame-ancestors 'none'
|
|
175
|
+
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
|
|
176
|
+
X-Content-Type-Options: nosniff
|
|
177
|
+
X-Frame-Options: DENY
|
|
178
|
+
Referrer-Policy: strict-origin-when-cross-origin
|
|
179
|
+
Permissions-Policy: camera=(), microphone=(), geolocation=()
|
|
180
|
+
\`\`\`
|
|
181
|
+
|
|
182
|
+
- Start with a strict CSP and loosen only as needed
|
|
183
|
+
- Use \`nonce\` or \`hash\` for inline scripts instead of \`'unsafe-inline'\`
|
|
184
|
+
- Enable HSTS preloading for production domains
|
|
185
|
+
- Set \`X-Frame-Options: DENY\` unless embedding is required
|
|
186
|
+
|
|
187
|
+
**DON'T:**
|
|
188
|
+
|
|
189
|
+
- Use \`'unsafe-eval'\` in CSP (enables XSS via code evaluation)
|
|
190
|
+
- Skip HSTS on HTTPS-only sites
|
|
191
|
+
- Set permissive CORS (\`Access-Control-Allow-Origin: *\`) on authenticated endpoints
|
|
192
|
+
|
|
193
|
+
## 8. Input Validation and Sanitization
|
|
194
|
+
|
|
195
|
+
**DO:** Validate all input at system boundaries. Reject invalid input before processing.
|
|
196
|
+
|
|
197
|
+
- Use schema validation (Zod, Joi, JSON Schema) for structured input
|
|
198
|
+
- Validate type, length, range, and format
|
|
199
|
+
- Use allowlists over blocklists for security-sensitive fields
|
|
200
|
+
- Sanitize for the output context (HTML-encode for HTML, parameterize for SQL)
|
|
201
|
+
- Validate file uploads: check MIME type, file extension, file size, and magic bytes
|
|
202
|
+
|
|
203
|
+
**DON'T:**
|
|
204
|
+
|
|
205
|
+
- Trust \`Content-Type\` headers alone for file type validation
|
|
206
|
+
- Use regex-only validation for complex formats (emails, URLs) -- use dedicated parsers
|
|
207
|
+
- Validate on the client only -- always re-validate server-side
|
|
208
|
+
- Accept unbounded input (always set maximum lengths)
|
|
209
|
+
|
|
210
|
+
## 9. Secret Management
|
|
211
|
+
|
|
212
|
+
**DO:** Keep secrets out of source code and version control.
|
|
213
|
+
|
|
214
|
+
- Use environment variables for deployment-specific secrets
|
|
215
|
+
- Use a secrets manager (Vault, AWS Secrets Manager, GCP Secret Manager) for production
|
|
216
|
+
- Rotate secrets on a schedule and immediately after suspected exposure
|
|
217
|
+
- Use separate secrets per environment (dev, staging, production)
|
|
218
|
+
- Validate that required secrets are present at startup -- fail fast if missing
|
|
219
|
+
|
|
220
|
+
**DON'T:**
|
|
221
|
+
|
|
222
|
+
- Commit secrets to Git (even in "private" repos)
|
|
223
|
+
- Log secrets in application logs or error messages
|
|
224
|
+
- Store secrets in \`.env\` files in production (use the platform's secret injection)
|
|
225
|
+
- Share secrets via chat, email, or documentation -- use a secrets manager
|
|
226
|
+
- Hardcode API keys, database passwords, or tokens in source files
|
|
227
|
+
|
|
228
|
+
\`\`\`
|
|
229
|
+
// DO: Environment variable
|
|
230
|
+
const apiKey = process.env.API_KEY
|
|
231
|
+
if (!apiKey) throw new Error("API_KEY environment variable is required")
|
|
232
|
+
|
|
233
|
+
// DON'T: Hardcoded
|
|
234
|
+
const apiKey = "sk-1234567890abcdef"
|
|
235
|
+
\`\`\`
|
|
236
|
+
|
|
237
|
+
## 10. Dependency Security
|
|
238
|
+
|
|
239
|
+
**DO:** Treat dependencies as an attack surface. Audit regularly and keep them updated.
|
|
240
|
+
|
|
241
|
+
- Run \`npm audit\`, \`pip audit\`, or equivalent on every CI build
|
|
242
|
+
- Use lockfiles (\`package-lock.json\`, \`bun.lockb\`, \`poetry.lock\`) and commit them
|
|
243
|
+
- Pin major versions; allow patch updates with automated PR tools (Dependabot, Renovate)
|
|
244
|
+
- Review new dependencies before adding: check maintenance status, download count, and known vulnerabilities
|
|
245
|
+
- Use Software Composition Analysis (SCA) tools in CI
|
|
246
|
+
|
|
247
|
+
**DON'T:**
|
|
248
|
+
|
|
249
|
+
- Ignore audit warnings -- triage and fix or document accepted risk
|
|
250
|
+
- Use \`*\` or \`latest\` as version specifiers
|
|
251
|
+
- Add dependencies without evaluating their transitive dependency tree
|
|
252
|
+
- Skip lockfile commits (reproducible builds require locked versions)
|
|
253
|
+
|
|
254
|
+
## 11. Cryptography Basics
|
|
255
|
+
|
|
256
|
+
**DO:** Use standard algorithms and libraries. Never implement your own cryptographic primitives.
|
|
257
|
+
|
|
258
|
+
- **Hashing:** SHA-256 or SHA-3 for data integrity; bcrypt/scrypt/Argon2id for passwords
|
|
259
|
+
- **Encryption:** AES-256-GCM for symmetric; RSA-OAEP or X25519 for asymmetric
|
|
260
|
+
- **Signing:** HMAC-SHA256 for message authentication; Ed25519 or ECDSA for digital signatures
|
|
261
|
+
- Use cryptographically secure random number generators (\`crypto.randomUUID()\`, \`crypto.getRandomValues()\`)
|
|
262
|
+
- Store encryption keys separate from encrypted data
|
|
263
|
+
|
|
264
|
+
**DON'T:**
|
|
265
|
+
|
|
266
|
+
- Use MD5 or SHA-1 for anything security-sensitive (broken collision resistance)
|
|
267
|
+
- Use ECB mode for block ciphers (patterns leak through)
|
|
268
|
+
- Reuse initialization vectors (IVs) or nonces
|
|
269
|
+
- Store encryption keys alongside the encrypted data
|
|
270
|
+
- Roll your own encryption scheme
|
|
271
|
+
|
|
272
|
+
## 12. API Security
|
|
273
|
+
|
|
274
|
+
**DO:** Protect APIs at multiple layers.
|
|
275
|
+
|
|
276
|
+
- Implement rate limiting per IP and per authenticated user:
|
|
277
|
+
\`\`\`
|
|
278
|
+
X-RateLimit-Limit: 100
|
|
279
|
+
X-RateLimit-Remaining: 42
|
|
280
|
+
X-RateLimit-Reset: 1672531200
|
|
281
|
+
\`\`\`
|
|
282
|
+
- Use API keys for identification, OAuth2/JWT for authentication
|
|
283
|
+
- Configure CORS to allow only specific origins on authenticated endpoints
|
|
284
|
+
- Validate request body size limits (prevent payload-based DoS)
|
|
285
|
+
- Use TLS 1.2+ for all API traffic -- no exceptions
|
|
286
|
+
|
|
287
|
+
**DON'T:**
|
|
288
|
+
|
|
289
|
+
- Expose internal error details in API responses (stack traces, SQL errors)
|
|
290
|
+
- Allow unlimited request sizes or query complexity (GraphQL depth/cost limiting)
|
|
291
|
+
- Use API keys as the sole authentication mechanism for sensitive operations
|
|
292
|
+
- Disable TLS certificate validation in production clients
|
|
293
|
+
|
|
294
|
+
## 13. Logging and Monitoring
|
|
295
|
+
|
|
296
|
+
**DO:** Log security-relevant events for detection and forensics.
|
|
297
|
+
|
|
298
|
+
- Log: authentication attempts (success and failure), authorization failures, input validation failures, privilege escalation, configuration changes
|
|
299
|
+
- Include: timestamp, user ID, action, resource, IP address, result (success/failure)
|
|
300
|
+
- Use structured logging (JSON) for machine-parseable audit trails
|
|
301
|
+
- Set up alerts for: brute force patterns, unusual access times, privilege escalation, mass data access
|
|
302
|
+
|
|
303
|
+
**DON'T:**
|
|
304
|
+
|
|
305
|
+
- Log passwords, tokens, session IDs, credit card numbers, or PII
|
|
306
|
+
- Log at a level that makes it easy to reconstruct sensitive user data
|
|
307
|
+
- Store logs on the same system they are monitoring (compromised system = compromised logs)
|
|
308
|
+
- Ignore log volume -- implement log rotation and retention policies
|
|
309
|
+
|
|
310
|
+
\`\`\`
|
|
311
|
+
// DO: Structured security log (PII redacted)
|
|
312
|
+
logger.warn("auth.failed", {
|
|
313
|
+
userId: attempt.userId,
|
|
314
|
+
ip: request.ip,
|
|
315
|
+
reason: "invalid_password",
|
|
316
|
+
attemptCount: 3,
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
// DON'T: Leak credentials
|
|
320
|
+
logger.warn("Login failed for user@example.com with password P@ssw0rd!")
|
|
321
|
+
\`\`\`
|
|
322
|
+
</skill>
|
|
323
|
+
|
|
324
|
+
## Output Format
|
|
325
|
+
|
|
326
|
+
Present findings in this structure:
|
|
327
|
+
|
|
328
|
+
### CRITICAL -- Must fix immediately (active exploits, data exposure)
|
|
329
|
+
### HIGH -- Should fix before next release (auth gaps, injection vectors)
|
|
330
|
+
### MEDIUM -- Plan to fix (missing headers, weak defaults)
|
|
331
|
+
### LOW -- Consider improving (best practice deviations)
|
|
332
|
+
|
|
333
|
+
For each finding, include: file path, line range, issue description, and a concrete remediation with code example.
|
|
334
|
+
|
|
335
|
+
## Rules
|
|
336
|
+
|
|
337
|
+
- ALWAYS check for hardcoded secrets, API keys, and tokens in source code.
|
|
338
|
+
- ALWAYS verify authentication and authorization on every endpoint/handler.
|
|
339
|
+
- ALWAYS run dependency audit commands when bash access is available.
|
|
340
|
+
- DO use bash to run security scanning tools and audit commands.
|
|
341
|
+
- DO NOT access the web.
|
|
342
|
+
- DO NOT modify source code -- this agent is audit-only (edit permission is denied).`,
|
|
343
|
+
permission: {
|
|
344
|
+
edit: "deny",
|
|
345
|
+
bash: "allow",
|
|
346
|
+
webfetch: "deny",
|
|
347
|
+
} as const,
|
|
348
|
+
});
|
package/src/hooks/anti-slop.ts
CHANGED
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
CODE_EXTENSIONS,
|
|
9
9
|
COMMENT_PATTERNS,
|
|
10
10
|
EXT_COMMENT_STYLE,
|
|
11
|
+
isExcludedHashLine,
|
|
12
|
+
MINIMUM_SLOP_INDICATORS,
|
|
11
13
|
SLOP_PATTERNS,
|
|
12
14
|
} from "./slop-patterns";
|
|
13
15
|
|
|
@@ -36,8 +38,12 @@ export function scanForSlopComments(content: string, ext: string): readonly Slop
|
|
|
36
38
|
|
|
37
39
|
const lines = content.split("\n");
|
|
38
40
|
const findings: SlopFinding[] = [];
|
|
41
|
+
const matchedPatternSources = new Set<string>();
|
|
39
42
|
|
|
40
43
|
for (let i = 0; i < lines.length; i++) {
|
|
44
|
+
// Exclude shebangs and hex-color lines from # comment scanning
|
|
45
|
+
if (commentStyle === "#" && isExcludedHashLine(lines[i])) continue;
|
|
46
|
+
|
|
41
47
|
const match = commentRegex.exec(lines[i]);
|
|
42
48
|
if (!match?.[1]) continue;
|
|
43
49
|
|
|
@@ -45,6 +51,7 @@ export function scanForSlopComments(content: string, ext: string): readonly Slop
|
|
|
45
51
|
|
|
46
52
|
for (const pattern of SLOP_PATTERNS) {
|
|
47
53
|
if (pattern.test(commentText)) {
|
|
54
|
+
matchedPatternSources.add(pattern.source);
|
|
48
55
|
findings.push(
|
|
49
56
|
Object.freeze({
|
|
50
57
|
line: i + 1,
|
|
@@ -57,6 +64,11 @@ export function scanForSlopComments(content: string, ext: string): readonly Slop
|
|
|
57
64
|
}
|
|
58
65
|
}
|
|
59
66
|
|
|
67
|
+
// Only report when enough distinct patterns match to reduce false positives
|
|
68
|
+
if (matchedPatternSources.size < MINIMUM_SLOP_INDICATORS) {
|
|
69
|
+
return Object.freeze([]);
|
|
70
|
+
}
|
|
71
|
+
|
|
60
72
|
return Object.freeze(findings);
|
|
61
73
|
}
|
|
62
74
|
|
|
@@ -65,6 +77,17 @@ const FILE_WRITING_TOOLS: ReadonlySet<string> = Object.freeze(
|
|
|
65
77
|
new Set(["write_file", "edit_file", "write", "edit", "create_file"]),
|
|
66
78
|
);
|
|
67
79
|
|
|
80
|
+
/** Debounce interval: skip re-scanning a file if it was scanned within this window (ms). */
|
|
81
|
+
const SCAN_DEBOUNCE_MS = 5000;
|
|
82
|
+
|
|
83
|
+
/** Module-level debounce map: filePath -> lastScanTimestamp. */
|
|
84
|
+
const scanTimestamps: Map<string, number> = new Map();
|
|
85
|
+
|
|
86
|
+
/** Clear the debounce map. Exported for test isolation. */
|
|
87
|
+
export function clearScanTimestamps(): void {
|
|
88
|
+
scanTimestamps.clear();
|
|
89
|
+
}
|
|
90
|
+
|
|
68
91
|
/**
|
|
69
92
|
* Creates a tool.execute.after handler that scans for slop comments.
|
|
70
93
|
* Best-effort: never throws, never blocks the pipeline.
|
|
@@ -101,6 +124,21 @@ export function createAntiSlopHandler(options: {
|
|
|
101
124
|
if (!resolved.startsWith(`${cwd}/`) && resolved !== cwd) return;
|
|
102
125
|
|
|
103
126
|
if (!isCodeFile(resolved)) return;
|
|
127
|
+
|
|
128
|
+
// Debounce: skip if this file was scanned within the debounce window
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
const lastScan = scanTimestamps.get(resolved);
|
|
131
|
+
if (lastScan !== undefined && now - lastScan < SCAN_DEBOUNCE_MS) return;
|
|
132
|
+
|
|
133
|
+
// Claim the slot before yielding the event loop to prevent TOCTOU races
|
|
134
|
+
scanTimestamps.set(resolved, now);
|
|
135
|
+
if (scanTimestamps.size > 10_000) {
|
|
136
|
+
const cutoff = now - SCAN_DEBOUNCE_MS;
|
|
137
|
+
for (const [path, ts] of scanTimestamps) {
|
|
138
|
+
if (ts < cutoff) scanTimestamps.delete(path);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
104
142
|
const ext = extname(resolved).toLowerCase();
|
|
105
143
|
|
|
106
144
|
// Read the actual file content — output.output is the tool's result message, not file content
|
|
@@ -108,7 +146,8 @@ export function createAntiSlopHandler(options: {
|
|
|
108
146
|
try {
|
|
109
147
|
fileContent = await readFile(resolved, "utf-8");
|
|
110
148
|
} catch {
|
|
111
|
-
|
|
149
|
+
scanTimestamps.delete(resolved); // clear slot so next attempt is not blocked
|
|
150
|
+
return;
|
|
112
151
|
}
|
|
113
152
|
|
|
114
153
|
const findings = scanForSlopComments(fileContent, ext);
|
|
@@ -41,15 +41,35 @@ export const EXT_COMMENT_STYLE: Readonly<Record<string, string>> = Object.freeze
|
|
|
41
41
|
|
|
42
42
|
/** Regex to extract comment text from a line given its comment prefix.
|
|
43
43
|
* Matches both full-line comments and inline trailing comments.
|
|
44
|
-
* Negative lookbehind (?<!:) prevents matching :// in URLs.
|
|
44
|
+
* Negative lookbehind (?<!:) prevents matching :// in URLs.
|
|
45
|
+
* Hash-line exclusions (shebangs, hex colors) are handled by isExcludedHashLine(). */
|
|
45
46
|
export const COMMENT_PATTERNS: Readonly<Record<string, RegExp>> = Object.freeze({
|
|
46
47
|
"//": /(?<!:)\/\/\s*(.+)/,
|
|
47
48
|
"#": /#\s*(.+)/,
|
|
48
49
|
});
|
|
49
50
|
|
|
51
|
+
/** Returns true if a line should be excluded from `#`-style comment scanning. */
|
|
52
|
+
export function isExcludedHashLine(line: string): boolean {
|
|
53
|
+
// Shebangs: lines starting with #!
|
|
54
|
+
if (line.trimStart().startsWith("#!")) return true;
|
|
55
|
+
// Hex colors: lines containing #RGB / #RRGGBB / #RRGGBBAA patterns
|
|
56
|
+
if (/#[0-9a-fA-F]{3,8}\b/.test(line)) return true;
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Minimum number of distinct pattern matches required to trigger a slop warning.
|
|
62
|
+
* A single match in isolation is likely a false positive for broad patterns.
|
|
63
|
+
*/
|
|
64
|
+
export const MINIMUM_SLOP_INDICATORS = 2;
|
|
65
|
+
|
|
50
66
|
/**
|
|
51
67
|
* Patterns matching obvious/sycophantic AI comment text.
|
|
52
68
|
* Tested against extracted comment body only (not raw code lines).
|
|
69
|
+
*
|
|
70
|
+
* Broad adjective patterns (robust, comprehensive, powerful) require a narrating
|
|
71
|
+
* context prefix ("this is", "our", "the", "it is") to reduce false positives
|
|
72
|
+
* from legitimate technical usage.
|
|
53
73
|
*/
|
|
54
74
|
export const SLOP_PATTERNS: readonly RegExp[] = Object.freeze([
|
|
55
75
|
/^increment\s+.*\s+by\s+\d+$/i,
|
|
@@ -60,11 +80,11 @@ export const SLOP_PATTERNS: readonly RegExp[] = Object.freeze([
|
|
|
60
80
|
/^import\s+(?:the\s+)?(?:necessary|required|needed)/i,
|
|
61
81
|
/^define\s+(?:the\s+)?(?:interface|type|class|function)/i,
|
|
62
82
|
/\belegantly?\b/i,
|
|
63
|
-
|
|
64
|
-
|
|
83
|
+
/(?:this|the|it|our)\s+(?:is\s+)?(?:a\s+)?\brobust(?:ly|ness)?\b/i,
|
|
84
|
+
/(?:this|the|it|our)\s+(?:is\s+)?(?:a\s+)?\bcomprehensive(?:ly)?\b/i,
|
|
65
85
|
/\bseamless(?:ly)?\b/i,
|
|
66
86
|
/\blever(?:age|aging)\b/i,
|
|
67
|
-
|
|
87
|
+
/(?:this|the|it|our)\s+(?:is\s+)?(?:a\s+)?\bpowerful\b/i,
|
|
68
88
|
/\bsophisticated\b/i,
|
|
69
89
|
/\bstate[\s-]of[\s-]the[\s-]art\b/i,
|
|
70
90
|
/\bcutting[\s-]edge\b/i,
|
package/src/installer.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { access, copyFile, readdir, unlink } from "node:fs/promises";
|
|
1
|
+
import { access, copyFile, open, readdir, unlink } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { copyIfMissing, ensureDir, isEnoentError } from "./utils/fs-helpers";
|
|
4
4
|
import { getAssetsDir, getGlobalConfigDir } from "./utils/paths";
|
|
@@ -139,14 +139,41 @@ async function processSkills(sourceDir: string, targetDir: string): Promise<Inst
|
|
|
139
139
|
return { copied, skipped, errors };
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/** Marker string present in installer-generated file frontmatter. */
|
|
143
|
+
const INSTALLER_MARKER = "opencode-autopilot";
|
|
144
|
+
|
|
145
|
+
/** Read the first 200 bytes of a file to check for the installer marker. */
|
|
146
|
+
async function hasInstallerMarker(filePath: string): Promise<boolean> {
|
|
147
|
+
let fh: import("node:fs/promises").FileHandle | undefined;
|
|
148
|
+
try {
|
|
149
|
+
fh = await open(filePath, "r");
|
|
150
|
+
const buf = Buffer.alloc(200);
|
|
151
|
+
const { bytesRead } = await fh.read(buf, 0, 200, 0);
|
|
152
|
+
const head = buf.toString("utf-8", 0, bytesRead);
|
|
153
|
+
return head.includes(INSTALLER_MARKER);
|
|
154
|
+
} finally {
|
|
155
|
+
try {
|
|
156
|
+
await fh?.close();
|
|
157
|
+
} catch {
|
|
158
|
+
/* ignore close errors to avoid masking the primary exception */
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
142
163
|
async function cleanupDeprecatedAssets(
|
|
143
164
|
targetDir: string,
|
|
144
165
|
): Promise<{ readonly removed: readonly string[]; readonly errors: readonly string[] }> {
|
|
145
166
|
const removed: string[] = [];
|
|
146
167
|
const errors: string[] = [];
|
|
147
168
|
for (const asset of DEPRECATED_ASSETS) {
|
|
169
|
+
const filePath = join(targetDir, asset);
|
|
148
170
|
try {
|
|
149
|
-
|
|
171
|
+
// Only delete if the file contains the installer marker.
|
|
172
|
+
// User-created files (without the marker) are left untouched.
|
|
173
|
+
const isOurs = await hasInstallerMarker(filePath);
|
|
174
|
+
if (!isOurs) continue;
|
|
175
|
+
|
|
176
|
+
await unlink(filePath);
|
|
150
177
|
removed.push(asset);
|
|
151
178
|
} catch (error: unknown) {
|
|
152
179
|
if (!isEnoentError(error)) {
|
package/src/memory/capture.ts
CHANGED
|
@@ -104,7 +104,11 @@ export function createMemoryCaptureHandler(deps: MemoryCaptureDeps) {
|
|
|
104
104
|
readonly event: { readonly type: string; readonly [key: string]: unknown };
|
|
105
105
|
}): Promise<void> => {
|
|
106
106
|
const { event } = input;
|
|
107
|
-
const
|
|
107
|
+
const rawProps = event.properties ?? {};
|
|
108
|
+
const properties: Record<string, unknown> =
|
|
109
|
+
rawProps !== null && typeof rawProps === "object" && !Array.isArray(rawProps)
|
|
110
|
+
? (rawProps as Record<string, unknown>)
|
|
111
|
+
: {};
|
|
108
112
|
|
|
109
113
|
// Skip noisy events early
|
|
110
114
|
if (!CAPTURE_EVENT_TYPES.has(event.type)) return;
|
|
@@ -144,15 +148,16 @@ export function createMemoryCaptureHandler(deps: MemoryCaptureDeps) {
|
|
|
144
148
|
currentSessionId = null;
|
|
145
149
|
currentProjectKey = null;
|
|
146
150
|
|
|
147
|
-
// Defer pruning to avoid blocking the
|
|
151
|
+
// Defer pruning to avoid blocking the session.deleted handler.
|
|
152
|
+
// Best-effort: will not run if the process exits before this microtask drains.
|
|
148
153
|
if (projectKey) {
|
|
149
|
-
|
|
154
|
+
queueMicrotask(() => {
|
|
150
155
|
try {
|
|
151
156
|
pruneStaleObservations(projectKey, db);
|
|
152
157
|
} catch (err) {
|
|
153
158
|
console.warn("[opencode-autopilot] pruneStaleObservations failed:", err);
|
|
154
159
|
}
|
|
155
|
-
}
|
|
160
|
+
});
|
|
156
161
|
}
|
|
157
162
|
return;
|
|
158
163
|
}
|
package/src/memory/decay.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
MIN_RELEVANCE_THRESHOLD,
|
|
17
17
|
TYPE_WEIGHTS,
|
|
18
18
|
} from "./constants";
|
|
19
|
+
import { getMemoryDb } from "./database";
|
|
19
20
|
import { deleteObservation, getObservationsByProject } from "./repository";
|
|
20
21
|
import type { ObservationType } from "./types";
|
|
21
22
|
|
|
@@ -90,5 +91,15 @@ export function pruneStaleObservations(
|
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
// Optimize the FTS5 index after pruning to reclaim space and improve query speed
|
|
95
|
+
if (pruned > 0) {
|
|
96
|
+
try {
|
|
97
|
+
const resolvedDb = db ?? getMemoryDb();
|
|
98
|
+
resolvedDb.run("INSERT INTO observations_fts(observations_fts) VALUES('optimize')");
|
|
99
|
+
} catch {
|
|
100
|
+
// best-effort — FTS optimize failure is non-critical
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
93
104
|
return Object.freeze({ pruned });
|
|
94
105
|
}
|
package/src/memory/retrieval.ts
CHANGED
|
@@ -13,8 +13,14 @@
|
|
|
13
13
|
|
|
14
14
|
import type { Database } from "bun:sqlite";
|
|
15
15
|
import { CHARS_PER_TOKEN, DEFAULT_INJECTION_BUDGET } from "./constants";
|
|
16
|
+
import { getMemoryDb } from "./database";
|
|
16
17
|
import { computeRelevanceScore } from "./decay";
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
getAllPreferences,
|
|
20
|
+
getObservationsByProject,
|
|
21
|
+
getProjectByPath,
|
|
22
|
+
updateAccessCount,
|
|
23
|
+
} from "./repository";
|
|
18
24
|
import type { Observation, Preference } from "./types";
|
|
19
25
|
|
|
20
26
|
/**
|
|
@@ -250,11 +256,34 @@ export function retrieveMemoryContext(
|
|
|
250
256
|
const scored = scoreAndRankObservations(observations, halfLifeDays);
|
|
251
257
|
const preferences = getAllPreferences(db);
|
|
252
258
|
|
|
253
|
-
|
|
259
|
+
const context = buildMemoryContext({
|
|
254
260
|
projectName: project.name,
|
|
255
261
|
lastSessionDate: project.lastUpdated,
|
|
256
262
|
observations: scored,
|
|
257
263
|
preferences,
|
|
258
264
|
tokenBudget,
|
|
259
265
|
});
|
|
266
|
+
|
|
267
|
+
// Batch-update access counts in a single transaction to avoid N+1 writes.
|
|
268
|
+
// Only observations that could plausibly fit in context are updated.
|
|
269
|
+
// Best-effort: failures are swallowed to avoid blocking retrieval.
|
|
270
|
+
const maxInContext = MAX_PER_GROUP * SECTION_ORDER.length;
|
|
271
|
+
const idsToUpdate = scored
|
|
272
|
+
.slice(0, maxInContext)
|
|
273
|
+
.map((obs) => obs.id)
|
|
274
|
+
.filter((id): id is number => id !== undefined);
|
|
275
|
+
if (idsToUpdate.length > 0) {
|
|
276
|
+
try {
|
|
277
|
+
const resolvedDb = db ?? getMemoryDb();
|
|
278
|
+
resolvedDb.run("BEGIN");
|
|
279
|
+
for (const id of idsToUpdate) {
|
|
280
|
+
updateAccessCount(id, db);
|
|
281
|
+
}
|
|
282
|
+
resolvedDb.run("COMMIT");
|
|
283
|
+
} catch {
|
|
284
|
+
// best-effort — access count update is non-critical
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return context;
|
|
260
289
|
}
|
|
@@ -12,8 +12,13 @@ export async function ensurePhaseDir(artifactDir: string, phase: Phase): Promise
|
|
|
12
12
|
return dir;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Returns the absolute path to a phase artifact.
|
|
17
|
+
* This is the canonical path used in both handler file-existence checks
|
|
18
|
+
* AND dispatch prompts, ensuring agents write to the location handlers verify.
|
|
19
|
+
*/
|
|
20
|
+
export function getArtifactRef(artifactDir: string, phase: Phase, filename: string): string {
|
|
21
|
+
return join(getPhaseDir(artifactDir, phase), filename);
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
export const PHASE_ARTIFACTS: Readonly<Record<string, readonly string[]>> = Object.freeze({
|
|
@@ -36,8 +36,9 @@ export function summarizeConfidence(entries: readonly ConfidenceEntry[]): {
|
|
|
36
36
|
|
|
37
37
|
const total = entries.length;
|
|
38
38
|
|
|
39
|
-
//
|
|
40
|
-
|
|
39
|
+
// Default: no evidence of low confidence → assume HIGH (single-proposal fast path).
|
|
40
|
+
// This prevents empty ledgers from triggering expensive multi-proposal arena (depth=2).
|
|
41
|
+
let dominant: ConfidenceLevel = "HIGH";
|
|
41
42
|
if (total > 0) {
|
|
42
43
|
let maxCount = 0;
|
|
43
44
|
for (const level of LEVEL_PRIORITY) {
|