Skip to content

Latest commit

 

History

History
157 lines (122 loc) · 44.6 KB

File metadata and controls

157 lines (122 loc) · 44.6 KB

CodeGraph — Progress Tracker

Last updated: 2026-07-12 Companion to: IMPROVEMENT_PLAN.md (read that first for task definitions/acceptance criteria)

Update this file every time a task's status changes. Keep the plan doc unedited — this is the only file that moves.

Status legend

✅ Done · 🔄 In progress · ⬜ Not started · 🚫 Blocked · ⏭️ Skipped (with reason)


Scorecard

Metric Baseline (2026-07-10, morning) Current Target (Phase 4)
Security 3/10 9.0/10 8.5/10 (exceeded)
Overall project 7/10 9.1/10 9.2/10

Phases 0–3 of the plan are complete and verified end-to-end (typecheck, full test suite, production Docker build under Render's exact 512MB/0.5CPU constraints, branch protection live against confirmed CI check names). Security jumped from 3→8.5 in the initial lockdown, then 8.5→9.0 in Phase 7's two remediation passes (17 of 27 audit findings closed; the residual gap to 10 is the deliberately-deferred product/infra tradeoffs — F001/F014/F018/F020/F052/F074 — plus test-coverage debt on already-correct code, not live control gaps). Not a full independent re-audit — this is a self-assessment against the same 2026-07-12 audit's own severity weighting.


Phase 0 — Security Lockdown

Status: ✅ Done — verified in the real production Docker image under Render-equivalent adversarial conditions

# Task Status Date Notes
0.1 Gate/disable local-folder indexing + /api/browse in production ✅ Done 2026-07-10 app/src/lib/localAccess.ts — allowed iff CG_ALLOW_LOCAL_ACCESS=true, or non-production by default. Frontend (page.tsx) checks /api/health's new localAccessAllowed field and disables the "Local folder" tab with an explanatory tooltip instead of letting users hit a raw 403. Verified: curl -X POST /api/index -d '{"localPath":"/app"}' → 403 in the built production image.
0.2 Harden git URL validation against SSRF ✅ Done 2026-07-10 app/src/lib/urlSafety.ts — rejects loopback/private/link-local IPv4 & IPv6 literals and localhost-style hostnames before git clone runs. Documented residual risk: doesn't defend DNS rebinding (would need to resolve+pin the connection ourselves). Verified against 169.254.169.254 (cloud metadata) and localhost in the production image.
0.3 npm audit fix + resolve remaining advisories ✅ Done (1 accepted risk) 2026-07-10 Added overrides: { dompurify: "^3.4.11" } to app/package.jsonmonaco-editor@0.55.1 (latest) pins a vulnerable dompurify@3.2.7 upstream; the override patches it without touching monaco itself. Verified Monaco still loads/renders correctly (browser test, zero console/CSP errors). Remaining: postcss bundled inside next's own node_modulesnpm audit fix --force wants to downgrade Next.js to 9.3.3 (an ancient major version), which would be far more damaging than the vulnerability itself (build-time-only tool processing our own trusted CSS, not user input). Accepted risk, documented here rather than silently ignored.
0.4 Add an access gate to the public deployment ✅ Done (opt-in, not yet activated) 2026-07-10 app/src/proxy.ts (Next.js 16's current convention, not the deprecated middleware.ts) — optional HTTP Basic Auth, OFF by default so nothing breaks unless CG_BASIC_AUTH_PASSWORD is explicitly set. /api/health always stays open for uptime monitors. Credential-check logic extracted to app/src/lib/basicAuth.ts (pure, unit-tested). Action needed: set CG_BASIC_AUTH_PASSWORD in the Render dashboard to actually turn this on for the live deployment — it's shipped but inert until configured.
0.5 Add security headers (CSP etc.) ✅ Done 2026-07-10 app/next.config.ts headers(). Verified via headless browser against the real Editor tab (which loads Monaco from cdn.jsdelivr.net at runtime): zero console/CSP violations, .monaco-editor DOM mounts, all CDN scripts/workers/CSS load. frame-ancestors 'none' + X-Frame-Options: DENY block clickjacking; object-src 'none' blocks plugin embeds.
0.6 Fix cross-tenant repo data leak (discovered live 2026-07-12, out of original plan sequence) ✅ Done 2026-07-12 Once GitHub sign-in (0.4) went live, the repos table had no owner column at alllistRepos() and every /api/repos/[id]/* route (fs read/write/download/delete, git, search, fix, agents, intel, trash, /api/fleet) served ANY repo to ANY requester, signed in or not. A signed-in user's imported private repo was readable/writable/deletable by a completely different account or an anonymous visitor. Fixed: repos.owner_id column (app/src/lib/db.ts), createIndexJob stamps the indexing session's userId (null if signed out), listRepos(viewerId) scopes to the viewer's own rows + a shared public bucket (owner_id IS NULL, for the no-login "paste a URL" flow), and a new app/src/lib/authz.ts#repoAccessDenied gates every single-repo route — a non-owner gets 404 (indistinguishable from not-found, so private-repo existence isn't leaked). 14 new regression tests (app/tests/tenant-isolation.test.ts); full suite 96/96 passing; npm run build (typecheck + build) clean.

Phase 1 — Reliability Guardrails

Status: ✅ Done

# Task Status Date Notes
1.1 GitHub Actions CI (test + build on push/PR) ✅ Done 2026-07-10 .github/workflows/ci.yml — two jobs: test-and-build (typecheck, unit tests, next build) and docker-smoke-test (builds the real image, runs it under --memory=512m --memory-swap=512m --cpus=0.5 --tmpfs /app/data:uid=0,gid=0 — i.e. Render's exact constraints — indexes octocat/Hello-World and expressjs/express end-to-end, confirms the server survives). Ran this exact sequence locally against the final built image before pushing; passed identically.
1.2 Branch protection on main ✅ Done 2026-07-10 Pushed, CI ran green on the first real run (both jobs), which registered the check names with GitHub; applied via gh api PUT .../branches/main/protection and verified live: required_checks: ["Test & Build (app/)", "Docker build + adversarial smoke test"], strict: true, enforce_admins: false, pr_required: null. Scoped conservatively — PR merges now require both checks green; direct pushes (the hotfix workflow used throughout today's incident response) are untouched.
1.3 Post-deploy smoke-test script ✅ Done 2026-07-10 app/scripts/smoke.sh — health check → real index job → poll to completion → confirm server still healthy after. Explicitly designed around the lesson from all three incidents: a green /api/health alone would not have caught any of them. Verified working (pass case) and verified it fails correctly (non-zero exit) against an unreachable target.
1.4 Postmortems for today's incidents ✅ Done 2026-07-10 docs/postmortems/ — 4 write-ups, not 3 (the plan under-counted; the crash-loop from the disk-permission fix's own setpriv step was a distinct incident): 2026-07-10-disk-permission-crash.md, 2026-07-10-render-crash-loop.md, 2026-07-10-tree-sitter-init-hang.md, 2026-07-10-tree-sitter-oom.md. Each follows Summary/Impact/Root Cause/Detection/Resolution/Action Items.

Phase 2 — Test Coverage Expansion

Status: ✅ Done for the security surface added today

# Task Status Date Notes
2.1 Route tests for /api/index, /api/repos/:id/fs, /api/repos/:id/trash ⏭️ Superseded by 2.2's scope Rather than route-level tests requiring NextRequest mocking, the security-critical logic was extracted into pure, directly-testable functions (localAccessAllowed, isPublicHttpUrl, checkBasicAuth) — see 2.2. Broader route/fs/trash test coverage beyond the security surface remains open for a future pass.
2.2 Regression tests locking Phase 0.1/0.2/0.4 ✅ Done 2026-07-10 app/tests/security.test.ts — 54 tests, written by a delegated Tester subagent, reviewed and one real bug fixed before acceptance (see below). Covers localAccessAllowed (all env-var × NODE_ENV combinations), isPublicHttpUrl (43 cases: public URLs incl. IPv4 boundary values 172.15/172.32, private/loopback/link-local IPv4 & IPv6, localhost variants, non-http(s) schemes, malformed input), checkBasicAuth (null/malformed/wrong-credential cases, colon-in-password edge case with a negative truncation check). Full suite: 82/82 passing, run twice back-to-back with identical results (no env leakage). Caught during review: the delegated tests initially assigned directly to process.env.NODE_ENV, which is typed readonly (via Next.js's global type augmentation) — passed at runtime (vitest doesn't typecheck) but failed tsc --noEmit, which is exactly what the new CI's test-and-build job runs. Fixed with an index-signature cast before this was committed; would have broken CI on the very first run otherwise.
2.3 Docker-based integration test in CI ✅ Done 2026-07-10 Folded into 1.1's docker-smoke-test CI job rather than a separate task — same constraint, same repos, one workflow.

Phase 3 — Documentation Cleanup

Status: ✅ Done

# Task Status Date Notes
3.1 Archive legacy Python/Postgres design docs ✅ Done 2026-07-10 git mv'd (history-preserving) 00_blueprint.md08_api_and_schema.md, DECISIONS.md, PROJECT_REPORT.md, and the entire codegraph/ Python package into docs/archive/legacy-design/, with a new README.md there explaining what it is, why it's kept, and explicitly "don't treat this as current."
3.2 Write accurate ARCHITECTURE.md ✅ Done 2026-07-10 New root-level ARCHITECTURE.md — real stack, request flow, security model, and a "Known constraints (learned the hard way)" section pointing at the postmortems, so the next person doesn't have to rediscover the WASM-memory/root-container/disk-mount lessons the hard way again.
3.3 Update root README.md ✅ Done 2026-07-10 Rewritten to point at app/ as the real product first, ARCHITECTURE.md for detail, and the legacy docs clearly labeled as historical. Kept the original pitch/vision language (still accurate as direction) but separated "what's built" from "what's aspirational."

Phase 4 — Close the Agent Loop

Status: ⬜ Not started (next up)

No change from the plan — not attempted in this pass. Phases 0–3 were prioritized first per the plan's own sequencing rationale (security → reliability → tests → docs → product feature), and doing Phase 4's PR-opening feature properly (branch/commit/push/PR via GitHub API, a confirmation gate, and a visible audit trail) is a multi-day-scale effort that deserves its own focused pass rather than being rushed alongside a security-critical lockdown.

Phase 5 — Scale & Domains (Stretch)

Status: ⬜ Not started (intentionally last, unchanged)


Phase 6 — Agent & Analysis Accuracy

Status: ✅ Done — all 16 tasks implemented and verified (typecheck + full test suite + next build, each task individually)

Added from a direct code-reading assessment of the actual swarm implementation (orchestrator.ts, specialists.ts, codeintel/graph.ts, codeintel/query.ts, codeintel/ast-extractor.ts) — not the UI. Executed in full on 2026-07-13, in the recommended order (6.1–6.4 root-cause first). See IMPROVEMENT_PLAN.md for the full task breakdown and rationale.

# Task Status Date Notes
6.1 Position-aware collectRefs + line-range caller attribution ✅ Done 2026-07-13 extractors.ts's ExtractResult.references changed from Map<string, number> to RawReference[] ({name, line}), both the regex and (now TS-compiler-based, see 6.5) extractors updated. graph.ts pass 2 rewritten: findEnclosingCaller() picks the smallest symbol range containing the ref's line, replacing the old "file's first function" fallback; unattributed calls are now honestly dropped rather than wrongly attributed. Verified: tests/resolution.test.ts — a 3-function fixture where only the innermost function calls a target resolves the caller edge to that function, not the file's first-defined one.
6.2 Per-file import-binding resolution before global name fallback ✅ Done 2026-07-13 Added RawImport capture (regex + AST extractors) and resolveModulePath() (extension/index probing against the known-files set) in graph.ts. Resolution order: same-file def → import binding (resolved to the specific imported file/symbol) → global name search (now truly last-resort, and prefers exported defs — fixed a dead id !== undefined no-op in the old fallback). Verified: tests/resolution.test.ts — two files each define a same-named parse(); a third file importing only one of them resolves to the imported one, not whichever was registered first.
6.3 Iterative Tarjan SCC in cycles() ✅ Done 2026-07-13 query.ts's cycles() converted from recursive strongconnect to an explicit-stack iterative version (same algorithm, no call-stack growth on deep chains). All existing codeintel/specialists cycle tests still pass.
6.4 Bounded top-N per rule per file (was 1-hit cap) ✅ Done 2026-07-13 indexer.ts's analyzeFiles now reports up to 5 hits per rule per file (was break after the first). tests/indexer.test.ts unaffected (its fixtures don't hit the cap).
6.5 Real AST extraction in production via TS Compiler API ✅ Done 2026-07-13 Replaced ast-extractor.ts's WASM tree-sitter path (disabled by default in production — see the 2026-07-12 finding) entirely with ts.createSourceFile + ts.forEachChild AST walking. No RSS budget/WASM-memory concern at all (ts.SourceFile/ts.Program objects are ordinary GC'able JS objects). initTreeSitter() kept as a no-op for callsite compatibility.
6.6 Type-aware call resolution (checker.getSymbolAtLocation) ✅ Done 2026-07-13 graph.ts now builds an in-memory ts.Program (custom CompilerHost serving the already-in-memory scanned file contents, no disk I/O) when TS/JS files are present, and passes it into the extractor via a new ExtractContext. ast-extractor.ts calls checker.getSymbolAtLocation on each call expression's callee when a Program is available, stamping a resolvedTargetId onto the reference that graph.ts uses to bypass the heuristic resolver entirely — correct even through re-exports/aliases. Falls back to the 6.1/6.2 heuristic when no Program (e.g. non-TS files).
6.7 Evidence-derived confidence (replace flat per-agent constants) ✅ Done 2026-07-13 Added confidence? to the Rule/Issue types; each of the 11 regex RULES now carries a hand-assessed confidence (0.7–1.0, scaled to how anchored/ambiguous the pattern is — e.g. debugger/TODO markers are 1.0, SQL-concat regex is 0.7). specialists.ts reads i.confidence where available instead of a flat per-agent constant; Performance/Dead-code/Architecture specialists derive confidence from real graph evidence (fan-in magnitude, exported-vs-local, cycle length) instead of a hardcoded number.
6.8 Locus-only critic dedup (was exact title match) ✅ Done 2026-07-13 orchestrator.ts's critique() rewritten: groups by file, sorts by line, merges findings within 2 lines of each other regardless of title wording (was exact file:line:title match). Verified: two agents flagging adjacent-but-not-identical lines with different titles now corroborate; findings >2 lines apart, or in different files, correctly do not merge.
6.9 Test agent: real fan-in ∩ untested-files intersection ✅ Done 2026-07-13 Test specialist now has a second pass beyond the repo-level ratio issues: walks qe.hubs(30), excludes test files, and flags any hub with zero callers from a test file — literally what its own suggestedFix text already promised ("highest-fan-in untested functions first") but never computed. Verified against a fixture with both a covered and an uncovered high-fan-in function.
6.10 Cyclomatic/branching complexity metric for Refactor agent ✅ Done 2026-07-13 ast-extractor.ts now computes real cyclomatic complexity per function (counts if/for/while/do/catch/case/?:/&&/||) during the same AST walk added in 6.5, stored on CodeSymbol.complexity. Refactor specialist adds a second pass flagging functions with complexity >15, independent of raw LOC — a flat 600-line file and a deeply-nested 600-line file now score differently.
6.11 Git churn signal into judge scoring ✅ Done 2026-07-13 New computeChurn() in indexer.ts (git log --since="6 months ago" --name-only) populates IndexResult.churnByFile (persisted via a new churn_by_file DB column). Finding.churn threaded through every specialist; judgeScore() in orchestrator.ts multiplies by 1 + log10(1+churn). Verified: tests/churn.test.ts — two files with identical fan-in but different churn produce different (correctly ordered) scores.
6.12 Expand fixers.ts beyond debugFixer ✅ Done (3, not 4+ — see note) 2026-07-13 Added todoFixer (removes stale, comment-only TODO/FIXME/HACK/XXX markers) and emptyCatchFixer (documents, never silently deletes, an empty catch {} — same-line content replacement, not a deletion). Scope correction from the original plan: dependency-pinning, a lockfile-generation fixer, and hardcoded-URL removal all turned out to be unsafe or infeasible as pure line-fixers on closer inspection (no npm install step exists in the executor's sandbox lifecycle so there's no real installed-version to pin to; blindly touching a hardcoded URL or auto-generating a lockfile risks changing program/build behavior, violating the fixer safety bar fixers.ts itself states). Also fixed a real latent bug found while wiring this up: fixers were chained (each fed the previous fixer's mutated lines), which silently shifts a later fixer's reported line numbers once an earlier one deletes a line — now every fixer runs independently against the pristine original lines and edits are merged by original line index. Extended buildDiff in executor.ts to correctly render both deletions and same-line replacements (previously deletion-only). Verified end-to-end: tests/executor.test.ts's new executeFixes integration test proves all 3 fixer classes fire in one pass on a real sandboxed repo, with a diff that correctly shows --only for deletions and -/+ pairs for replacements.
6.13 orchestrator.test.ts / specialists.test.ts unit coverage ✅ Done 2026-07-13 13 tests in orchestrator.test.ts (critic merge/no-merge across line-distance and file boundaries, hand-computed judge-score → priority-threshold boundaries for all 4 buckets, the security-severity≥4 P0 override, projected-score capping at 100 and no-op when there's nothing to fix) + 12 tests in specialists.test.ts (one meaningful behavior per specialist, driven through real buildSymbolGraph fixtures, not mocks).
6.14 Surface truncated (MAX_SYMBOLS) flag to the user ✅ Done 2026-07-13 Added RemediationPlan.truncated, prepended to planSummary's text ("Partial analysis..."). Added a visible amber banner on the repo detail page when repo.symbolGraph.truncated is true. Verified: tests/orchestrator.test.ts for the summary/flag propagation; next build for the UI/JSX change.
6.15 ESLint security-plugin detector layer ✅ Done 2026-07-13 New eslintSecurity.ts: uses ESLint's low-level Linter API directly (@typescript-eslint/parser, no .eslintrc/project config, no filesystem I/O) with a curated 10-rule subset of eslint-plugin-security (excludes detect-object-injection — notoriously high false-positive rate — and the Express-specific CSRF-ordering rule, which can't be evaluated from one file in isolation). Wired into indexer.ts's analyzeFiles as an additional pass alongside the existing 11 line-regexes; degrades to [] (never throws) on unparseable content. Verified: catches a ReDoS-vulnerable regex literal (/^(a+)+$/) that none of the 11 existing regexes structurally can, both in isolation and end-to-end through the real indexRepo pipeline (tests/eslintSecurity.test.ts, 6 tests). Added eslint-plugin-security@4.0.1 as a real dependency + a hand-written ambient .d.ts (the DefinitelyTyped package targets an older major and the plugin ships no first-party types).
6.16 Shallow taint reachability for Security agent ✅ Done 2026-07-13 Added QueryEngine.reachableCallees() (forward BFS via callees(), the mirror of the existing impact()) and QueryEngine.symbolAt(file, line) (resolves an issue's location to its enclosing symbol). Security specialist's new taintFindings(): sources = callables whose signature matches an HTTP-handler-shaped first parameter (req/request); sinks = the enclosing symbol of every existing security-dimension issue (eval, exec, SQL, HTML injection); flags any source that reaches a sink within 3 hops, with the hop count in the finding detail — genuinely stronger evidence than "eval() appears somewhere in this file". Verified against the plan's exact acceptance scenario: tests/specialists.test.ts — a 2-hop handler → processInput → runDynamic → eval() chain is flagged; a chain that never reaches a sink is not; a same-function (0-hop) case is correctly left to the existing base finding, not double-reported.

Phase 7 — Critical Security Remediation (from docs/AUDIT_2026-07-12.md)

Status: ✅ Done — 17 of 27 Security findings fixed across two passes (7.1–7.8 below, plus 7.9–7.19 in the follow-up pass on 2026-07-17); the remaining 10 are testing-debt items (F077/F079/F086/F087 — "add tests for existing, already-correct code", tracked but not control gaps) or explicitly deferred product/infra tradeoffs (F001 — anonymous bring-your-own-PAT is an intentional feature; F014 — CSRF token needs a coordinated frontend change; F018 — token-in-argv needs an askpass-helper rewrite with real-PAT verification unavailable here; F020/F052 — CSP nonce migration needs its own telemetry-gated rollout, not a blind tightening; F074 — read-only-rootfs/network-egress hardening is a platform-level change needing Render-specific verification this pass can't perform). Every fix below preserves the deliberate "anonymous bring-your-own GitHub-PAT" push/fix flow (GitPanel.tsx, AgentSwarm.tsx's RemediationExecutor both accept a pasted PAT with no sign-in required) — that is an intentional product feature, not a bug, so the audit's literal "bind to session, 401 without one" suggestion for F001 was deliberately NOT applied; it would have removed that feature. Verified via npx tsc --noEmit, full vitest run, npm run lint, and next build after every change in this phase.

# Task Status Date Notes
7.1 F006 — gate token attachment on github.com host in the git-push route ✅ Done 2026-07-13 The actual severe primitive behind F001/F006: POST /api/repos/:id/git {op:"push", githubToken} called withToken(repo.url, githubToken) with no host check, so a repo indexed against any public https host (isPublicHttpUrl allows any, not just GitHub) would get the caller's real GitHub token embedded in a URL and pushed to that host — live token exfiltration to an attacker-controlled remote. New isGithubHost() in gitops.ts (shared, so this and the clone path can't drift apart again per the audit's own recommendation); store.ts's clone-time check now uses it too (dedup, same behavior). executor.ts's PR-push flow already effectively host-locked (hardcoded literal github.com in the constructed remote URL) — hardened with an explicit isGithubHost(repo.url) guard instead of relying solely on a loose regex. Reasoned residual risk of F001 (no session binding) explicitly NOT closed — see status note above.
7.2 F003 — symlink-aware resolveSafe (editor path-traversal boundary) ✅ Done 2026-07-13 workspace.ts's resolveSafe was purely lexical despite its own doc-comment claiming symlink defense. Now walks up from the resolved target to the nearest existing ancestor, realpathSyncs it, and verifies containment against the workspace root's own realpath — closes a symlink escape (a git-tracked symlink pointing outside the workspace, or an attacker-crafted local folder) reachable through every editor fs op (listDir/read/write/create/rename/duplicate/search) and trash.ts. Ordinary paths and not-yet-existing create targets verified unaffected.
7.3 F002 — symlink-aware sandbox walk in the remediation executor ✅ Done 2026-07-13 executor.ts's walkCode used statSync (follows symlinks) with no containment check, contradicting its own comment that "the user's original source is never mutated" — a tracked symlink in a cloned repo could read/edit through it. Switched to lstatSync and skip any symlink entry outright.
7.4 F004 — strip credentials from clone-failure error text before it's stored ✅ Done 2026-07-13 Node's execFile rejection embeds the full argv — including a withToken()-embedded PAT — in .message/.cmd on failure; store.ts's runJob stores that verbatim into jobs.error, served by /api/jobs/:id. New redactCredentials() in indexer.ts, applied to .message/.cmd/.stderr in cloneRepo's catch — covers every caller (index flow and any future one), not just this one call site.
7.5 F005 — ownership check on GET /api/jobs/:id ✅ Done 2026-07-13 Had no repoAccessDenied call at all — any jobId (including ones carrying the token-leak text from 7.4, pre-fix) was readable by anyone. Now resolves the job's repoId and applies the same 404-not-403 tenant-isolation check as every other repo-scoped route.
7.6 F010 — validate OAuth returnTo as a same-origin relative path ✅ Done 2026-07-13 /api/auth/github?returnTo= was stored and redirected to with zero validation — an absolute (https://evil.example) or protocol-relative (//evil.example) value would send a freshly authenticated session cookie's owner straight to a phishing origin post-login. New isSafeReturnPath() in urlSafety.ts; falls back to / on anything unsafe, ordinary same-origin paths unaffected.
7.7 Missing eslint.config.mjsnpm run lint was completely non-functional ✅ Done 2026-07-13 Discovered while verifying the fixes above: ESLint 9 requires a flat config file; none existed, so npm run lint (and any future CI lint gate) failed immediately with "couldn't find an eslint.config.(js|mjs|cjs) file" rather than linting anything. Added, using eslint-config-next's own ready-made flat-config array. Running it for the first time surfaced 17 pre-existing react-hooks/set-state-in-effect / react-hooks/refs errors in NodeGraph.tsx, FileExplorer.tsx, GitPanel.tsx, TrashPanel.tsx — real, but out of this phase's scope (behavioral component changes, not security) and deliberately left unfixed rather than risking a rushed change to render-timing-sensitive code; not wired into CI yet for the same reason (would immediately red-flag every PR on unrelated pre-existing findings). Follow-up task, not done here.
7.8 Regression tests for 7.1–7.6 ✅ Done 2026-07-13 New tests/security-hardening.test.ts — 19 tests: isGithubHost (accept/reject/malformed), resolveSafe (symlink-file escape, symlink-dir escape, plus 3 no-regression cases proving ordinary/new-file/..-traversal behavior is unchanged), redactCredentials (unit + a real cloneRepo failure against an instant-refusing loopback address, proving the actual wiring, not just the pure function), isSafeReturnPath (accept/reject), the executor's symlink skip (end-to-end through executeFixes, proving the legitimate in-tree file is still fixed while the symlinked target's content never surfaces), and 5 cases for the jobs-ownership check (owner/public-bucket/non-owner/anonymous-on-private/nonexistent). Full suite 154/154.

| 7.9 | F012 — enforce session expiry server-side, not just via the cookie's client-enforced Max-Age | ✅ Done | 2026-07-17 | session.ts's decryptSession now rejects (returns null) once Date.now() - payload.issuedAt exceeds SESSION_MAX_AGE_S, on top of the existing GCM-tag tamper check. A leaked/exfiltrated cookie now has a real server-enforced expiry instead of validating forever. | | 7.10 | F013 — derive the Secure cookie flag from the actual request scheme, not NODE_ENV | ✅ Done | 2026-07-17 | New requestIsSecure() in session.ts: trusts x-forwarded-proto (reverse-proxy topologies like Render), falls back to the request's own scheme, with an explicit CG_FORCE_SECURE_COOKIES override for edge cases. setSessionCookie/oauthTransitCookieOptions now take an optional req and both call sites (/api/auth/github, /api/auth/github/callback) pass it through. | | 7.11 | F011 — enforce a max content size on editor writes/uploads | ✅ Done | 2026-07-17 | No cap existed on writes at all (MAX_EDITABLE_BYTES only governed read/search truncation). New MAX_WRITE_BYTES (8MB) in workspace.ts, enforced in writeWorkspaceFile/createEntry's write path and separately in the fs route's raw-bytes upload op (checked against the base64 length first, then the decoded byte count, to reject oversized payloads before the allocating decode step). | | 7.12 | F015 — rate limit the OAuth callback, /api/index, and /api/browse | ✅ Done | 2026-07-17 | New in-process token-bucket limiter (rateLimit.ts, keyed by best-effort client IP via x-forwarded-for/x-real-ip), returns 429 + Retry-After. Applied: OAuth callback (10/min — protects the app's own OAuth quota), /api/index (10/min — gates real clone/filesystem work), /api/browse (30/min — cheap enumeration once local access is on). Verified live against a running dev server: 13 rapid /api/index POSTs from one IP produced exactly 3 429s after the 10-token bucket emptied. | | 7.13 | F016 — scope /api/browse to a configured base directory | ✅ Done | 2026-07-17 | New optional CG_LOCAL_ACCESS_ROOT env var; when set, withinConfiguredRoot() in browse/route.ts rejects (403) any resolved+realpath'd target outside it, defense-in-depth against a local-access misconfiguration. Unset preserves today's unrestricted behavior exactly (default-off, zero behavior change unless explicitly configured). | | 7.14 | F017 — redact the embedded GitHub token before it can reach a log line on a failed PR/push | ✅ Done | 2026-07-17 | executor.ts's PR-push failure path (execFile rejections embed the full argv, including the token-bearing remoteUrl set via git remote set-url) and the outer executeFixes catch both now redact via the existing redactCredentials() (F004's helper) before logging/returning. | | 7.15 | F019 — stop exposing deployment-configuration detail on the unauthenticated /api/health endpoint | ✅ Done (partial — see note) | 2026-07-17 | Dropped uptime/ts (zero legitimate client use — confirmed lib/api.ts's HealthStatus type and every caller never read them; pure unauthenticated reconnaissance). Kept localAccessAllowed, deviating from the audit's literal recommendation: app/page.tsx genuinely depends on it to pre-emptively disable the "Local folder" indexing button — dropping it would leave the button stuck enabled until a 403 on submit, a real UX regression the finding didn't account for. | | 7.16 | F022 — use the existing timingSafeEqual helper for the OAuth state comparison | ✅ Done | 2026-07-17 | Exported timingSafeEqual from basicAuth.ts (was file-local); the OAuth callback route now uses it instead of !== for the CSRF-state check, consistent with the codebase's own stated security posture for this class of secret comparison. | | 7.17 | F023 — sanitize raw Error.message before it reaches a client response | ✅ Done | 2026-07-17 | OAuth callback, /api/index, and /api/browse's stat/readdir catch blocks now log the real error server-side (console.warn) and return a generic client-safe message instead of forwarding e.message (which could carry a resolved filesystem path or library-internal detail) verbatim. | | 7.18 | F021 — scope the cdn.jsdelivr.net CSP allowance to Monaco's actual path | ✅ Done | 2026-07-17 | next.config.ts's CSP now allows https://cdn.jsdelivr.net/npm/monaco-editor/ (the exact path prefix @monaco-editor/react's default loader fetches from) in script-src/style-src/font-src/connect-src, instead of trusting jsdelivr's entire arbitrary-package catalog. | | 7.19 | F068 — exact-pin the two dependencies that parse untrusted repo content | ✅ Done | 2026-07-17 | tree-sitter-wasms and web-tree-sitter changed from ^ ranges to exact pins in package.json — an unreviewed transitive bump of either now requires an explicit version-bump commit rather than landing silently via npm install, since both sit directly in the untrusted-input (arbitrary git repo content) parsing path. Lockfile regenerated (npm install --package-lock-only); other UI-only deps left on caret ranges per the finding's own recommendation. | | 7.20 | Regression tests for 7.9–7.18 | ✅ Done | 2026-07-17 | New tests/security-hardening-2.test.ts — 24 tests covering the write-size cap (under/at/over the limit), session expiry (fresh/expired/boundary/still-tamper-resistant), requestIsSecure (proxy header, plain http, https URL, both CG_FORCE_SECURE_COOKIES overrides), the rate limiter (capacity exhaustion + 429, time-based refill, per-key isolation), clientIp (XFF/X-Real-IP/fallback), timingSafeEqual (equal/unequal/length-mismatch/empty), and /api/browse's root-scoping (unset = unrestricted, inside-root allowed, outside-root 403). Full suite 207/207; tsc --noEmit, npm run lint (baseline 20/17 unchanged, none in touched files), and next build all clean. |

Phase 7 remaining (deliberately deferred, not attempted): F001 (anonymous bring-your-own-PAT push relay — intentional product feature per the 2026-07-13 product decision, not a bug); F014 (CSRF token on POST /api/index — needs a coordinated frontend double-submit-cookie change, current SameSite=Lax coverage judged adequate in the interim); F018 (token-in-argv via git push — needs an askpass-helper rewrite that can't be verified end-to-end without a real GitHub PAT in this environment); F020/F052 (CSP nonce migration + route-scoped Monaco relaxation — needs a dedicated rollout with report-to telemetry first, not a blind tightening that could silently break the editor); F074 (read-only root filesystem + network-egress allowlist for the Docker runtime — a platform-level change requiring Render-specific verification outside this pass's scope). Also still open: F077/F079/F086/F087 (regression-test coverage for pre-existing, already-correct code paths — tracked as test-coverage debt, not security-control gaps), all 21 Functionality findings, 18 Performance findings, 9 Code Quality findings (beyond 7.7), 8 DevOps findings (beyond this pass), 6 Architecture findings, and 4 UX findings from the same audit. See docs/AUDIT_2026-07-12.md for the full list.


Session log

Date What happened
2026-07-10 Added server-side folder browser for local-path indexing (/api/browse, FolderBrowser.tsx)
2026-07-10 Added restorable trash for editor deletes (/api/repos/:id/trash, TrashPanel.tsx) instead of permanent rm
2026-07-10 Diagnosed + fixed Render disk-permission crash (unable to open database file) — commit 2dd8618
2026-07-10 Diagnosed + fixed Render crash-loop from setpriv under restricted capabilities — commit 1505ed3
2026-07-10 Diagnosed + fixed indefinite hang at "Initializing Tree-sitter parsers…" — commit 25cd6ec
2026-07-10 Diagnosed + fixed OOM crash on real repos (WASM memory growth) — commit 4f78fd5
2026-07-10 Full security + quality audit of live app; identified live arbitrary-file-read exposure, missing CI, thin API test coverage, stale root docs
2026-07-10 IMPROVEMENT_PLAN.md + this tracker created
2026-07-10 Executed Phases 0–3 of the plan: local-access + SSRF + dependency + auth-gate + security-header hardening; GitHub Actions CI with a real Docker smoke test under Render's exact constraints; smoke-test script; 4 incident postmortems; 54 new regression tests (one real pre-commit typecheck bug caught and fixed during review); legacy docs archived; new ARCHITECTURE.md and README.md. All verified end-to-end against the actual production Docker image before pushing.
2026-07-10 Pushed Phases 0–3; CI passed on the first real run (both jobs green); branch protection applied and verified against the real, registered check names.
2026-07-12 User asked whether two signed-in accounts could see each other's indexed repos — audit found a live cross-tenant leak (repos table had no owner column; every repo/workspace route was globally readable/writable regardless of session). Fixed same-session: owner_id column + app/src/lib/authz.ts, enforced on every /api/repos/[id]/* route and /api/fleet; product decision (user's call): anonymous-indexed repos stay in a shared public bucket, GitHub-account-indexed repos are private to that account. 14 new tests, full suite 96/96, build clean. See Phase 0.6.
2026-07-12 Direct code-reading assessment of the agent swarm (not the UI) — read orchestrator.ts, specialists.ts, codeintel/graph.ts, codeintel/query.ts, codeintel/ast-extractor.ts, indexer.ts's RULES, and fixers.ts end to end. Root finding: the orchestration logic (critic/judge) is sound; most agent inaccuracy traces to the call graph itself — graph.ts attributes every call in a file to "the file's first function" (no per-line reference data exists to do better), resolution is global name-matching with zero import awareness, and tree-sitter AST extraction is disabled by default in production (MAX_RSS_BYTES defaults to 0, so the RSS gate trips on the first check) — meaning the deployed system runs on the regex fallback extractor, not the AST path the code implies is primary. Also confirmed: zero unit tests for orchestrator.ts/specialists.ts, zero git-churn signal used anywhere, only 1 of 7 issue classes has an automated fixer. Wrote up as IMPROVEMENT_PLAN.md Phase 6 (16 tasks) — not executed yet, tracked here for a future pass.
2026-07-13 Executed all 16 tasks of Phase 6 (Agent & Analysis Accuracy) end to end. Root-cause fixes: position-aware caller attribution (findEnclosingCaller) + per-file import-aware resolution replace the old "attribute every call to the file's first function" heuristic and global-name-only lookup; iterative Tarjan; bounded per-rule findings. Replaced the RSS-gated (disabled-by-default) tree-sitter path entirely with the TypeScript Compiler API, including a real in-memory ts.Program for checker.getSymbolAtLocation-based type-aware call resolution. Evidence-derived confidence, locus-based critic dedup, a real fan-in∩untested-files Test agent, AST-computed cyclomatic complexity, and a genuine git-churn hotspot signal threaded into judge scoring. Expanded the Fixer roster from 1 to 3 (found and fixed a real latent line-number-drift bug in fixer chaining along the way; extended the diff builder to handle replacements, not just deletions). Added 37 new tests across 5 new test files (resolution, churn, orchestrator, specialists, eslintSecurity) plus extended executor.test.ts — suite grew from 96 to 133 passing, typecheck clean, next build clean, each task verified individually before being marked done. Surfaced the MAX_SYMBOLS truncation flag in both the agent summary and a new UI banner. Added a second, AST-based security detector layer (eslint-plugin-security, curated 10-rule subset) and shallow graph-verified taint reachability (source-parameter heuristic → sink issue, BFS via a new reachableCallees()) to the Security agent. See Phase 6 above for the full per-task breakdown.
2026-07-13 Executed Phase 7 (Critical Security Remediation): closed the 6 audit findings fixable without a product-behavior tradeoff — F006 (github.com host gate before ever attaching a token to a remote, the real severe primitive behind F001/F006), F003 (symlink-aware resolveSafe), F002 (symlink-aware executor sandbox walk), F004 (credential redaction in clone-failure errors), F005 (job-ownership check), F010 (OAuth open-redirect guard). Also discovered and fixed a missing eslint.config.mjs (npm run lint was completely non-functional — no config file existed at all). 19 new regression tests (tests/security-hardening.test.ts); full suite 154/154; tsc --noEmit, npm run lint, and next build all clean. Deliberately did NOT apply F001's literal "bind token to session" suggestion — it would have removed the intentional anonymous bring-your-own-PAT push/fix feature; flagged as a residual, lower-severity risk instead. 21 of 27 Security findings and all other categories from the 07-12 audit remain open — see Phase 7 in this file.
2026-07-17 Executed Phase 7's follow-up pass (tasks 7.9–7.20): closed 11 more of the 21 previously-open Security findings from docs/AUDIT_2026-07-12.md — session expiry enforced server-side (F012), Secure-cookie flag derived from the real request scheme (F013), a write-size cap on editor writes/uploads (F011), rate limiting on the OAuth callback//api/index//api/browse (F015, verified live against a running dev server: 429s kick in exactly as expected), optional /api/browse root-scoping (F016), credential redaction in executor PR-failure logs (F017), dropped uptime/ts from the public health payload while deliberately keeping localAccessAllowed (F019 — the audit's literal recommendation would have broken a real frontend UX dependency), constant-time OAuth state comparison (F022), sanitized client-facing error messages on 3 routes (F023), and CDN CSP scoping (F021), plus exact-pinning the two tree-sitter packages that parse untrusted repo content (F068). Also fixed, unprompted, a live production bug reported mid-session: the Claude Agent SDK's platform-specific native binary was silently dropped from .next/standalone's output by Next.js's file tracer (confirmed via a real next build — this affected every deployment target, not just Render's linux-x64) — isolated onto its own branch/PR (#13) per the user's explicit request, kept separate from the security work. 24 new regression tests (tests/security-hardening-2.test.ts); full suite 207/207; tsc --noEmit, npm run lint (baseline unchanged), and next build all clean. Remaining 10 Security findings are either testing-debt (F077/F079/F086/F087) or explicit product/infra tradeoffs (F001/F014/F018/F020/F052/F074) — see Phase 7's closing note for the reasoning behind each deferral.

| 2026-07-23 | Organizational audit + targeted incremental refactor (scoped down from a generic full-layered-architecture request after grounding against this tracker: security/CI/tests/docs were already done; the real gap was code organization). Audit: no debug leftovers/dead TODOs (the only regex hits were inside the indexer's own rule-definition strings), no genuine circular dependencies (madge flagged 3, all resolve to import type-only edges erased at compile time — verified by reading every import line by hand), and — per the app's own established god-file bar (indexer.ts's own rule flags >600 LOC as a maintainability issue) — exactly one file crossed it: indexer.ts itself (803 lines, 6 mixed concerns). api.ts (473) and settings/page.tsx (582) stayed below the project's own bar and were deliberately left alone rather than split for the sake of a generic template. Refactor: split lib/indexer.ts into lib/indexer/{constants,util,clone,scan,analyze,score,viz,index}.ts (directory-import convention already used by lib/agents/, lib/codeintel/, lib/gitops/ — zero callsite changes, all 4 existing importers resolve unchanged) — also dropped an unused SymbolGraph type import found in the process. Separately found and fixed real duplication: the exact repoAccessDenied + getWorkspaceDir-or-404 sequence was hand-copied across 8 call sites in 4 route files (fs, git, search, timeline) — the same class of copy-paste gap that caused the Phase 0.6 cross-tenant leak. Consolidated into requireWorkspace() in authz.ts; also removed fs/route.ts's workspaceOr404 (a one-line no-op wrapper around getWorkspaceDir). Verified: tsc --noEmit clean, full suite 265/265, next build clean, lint baseline unchanged (22/17, same pre-existing component-file findings as before, none in touched files), and a live next dev smoke test — 404 "Repo not found" on all 4 refactored routes for a nonexistent/unowned repo id, then a real end-to-end index of octocat/Hello-World through the split indexer modules (score computed, fs/git/search all correctly gated through requireWorkspace). |