Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .husky/commit-msg
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,14 @@ if ! echo "$msg" | grep -qE '^(feat|fix|docs|chore|refactor|test|perf|ci|build|r
echo "Got: $msg"
exit 1
fi

# Strip AI co-authorship trailers from commit message
# These are injected by AI coding tools and misrepresent authorship.
# See docs/AI_ATTRIBUTION_POLICY.md for the full list.
if [ "$(uname)" = "Darwin" ]; then
sed -i '' '/^Co-authored-by:.*[Cc]laude\|[Cc]opilot\|[Cc]ursor\|[Cc]hat[Gg][Pp][Tt]\|[Oo]pen[Aa][Ii]\|[Aa]nthrop\|[Dd]evin\|[Aa]ider\|[Cc]odeium\|[Tt]abnine\|[Ww]indsurf\|[Cc]ody\|[Cc]ontinue\|[Aa]ugment\|[Ss]upermaven/d' "$1"
sed -i '' '/^Authored-by:.*AI\|^Written-by:.*AI\|^Generated-by:/d' "$1"
else
sed -i '/^Co-authored-by:.*[Cc]laude\|[Cc]opilot\|[Cc]ursor\|[Cc]hat[Gg][Pp][Tt]\|[Oo]pen[Aa][Ii]\|[Aa]nthrop\|[Dd]evin\|[Aa]ider\|[Cc]odeium\|[Tt]abnine\|[Ww]indsurf\|[Cc]ody\|[Cc]ontinue\|[Aa]ugment\|[Ss]upermaven/d' "$1"
sed -i '/^Authored-by:.*AI\|^Written-by:.*AI\|^Generated-by:/d' "$1"
fi
6 changes: 6 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ set -e
bun prettier --write "packages/opencode/src/**/*.ts" "packages/plugin/src/**/*.ts"
git update-index --again

# Strip AI tool attribution from staged files
scripts/strip-ai-attribution.sh

# SAST: check for security anti-patterns in staged files
scripts/sast-check.sh

# Typecheck
bun turbo typecheck 2>&1 | tail -5

Expand Down
33 changes: 33 additions & 0 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/bin/sh
set -e

# Check if bun version matches package.json
# keep in sync with packages/script/src/index.ts semver qualifier
bun -e '
Expand All @@ -17,5 +18,37 @@ if (process.versions.bun !== expectedBunVersion) {
console.warn(`Warning: Bun version ${process.versions.bun} differs from expected ${expectedBunVersion}`);
}
'

# Verify no AI attribution in commits being pushed
# Checks all commits between upstream tracking branch and HEAD
commits=$(git log @{u}..HEAD --format="%H" 2>/dev/null || echo "")
for commit in $commits; do
msg=$(git log -1 --format="%B" "$commit")
if echo "$msg" | grep -qiE "co-authored-by:.*(claude|copilot|cursor|chatgpt|openai|anthropic|devin|aider|codeium|tabnine|windsurf|cody|continue|augment|supermaven)"; then
short=$(git log -1 --format="%h %s" "$commit")
echo ""
echo "ERROR: AI co-authorship attribution found in commit:"
echo " $short"
echo ""
echo "Strip it with: git rebase -i HEAD~N (edit the commit message)"
echo "See docs/AI_ATTRIBUTION_POLICY.md for details."
exit 1
fi
done

# Strip AI attribution from existing PR body (if gh is available and PR exists)
if command -v gh >/dev/null 2>&1; then
branch=$(git rev-parse --abbrev-ref HEAD)
pr_number=$(gh pr view "$branch" --json number --jq '.number' 2>/dev/null || echo "")
if [ -n "$pr_number" ]; then
body=$(gh pr view "$pr_number" --json body --jq '.body' 2>/dev/null || echo "")
cleaned=$(echo "$body" | sed -E '/[Cc]o-authored-by:.*(Claude|Copilot|Cursor|ChatGPT|OpenAI|Anthropic|Devin|Aider|Codeium|Tabnine|Windsurf|Cody|Continue|Augment|Supermaven)/Id; /Created with Cursor/Id; /Generated by (Claude|Copilot|ChatGPT|Codeium|Windsurf|Devin|Aider)/Id; /AI-generated/Id')
if [ "$body" != "$cleaned" ]; then
echo "$cleaned" | gh pr edit "$pr_number" --body-file - 2>/dev/null
echo "Stripped AI attribution from PR #$pr_number body"
fi
fi
fi

bun turbo typecheck
bun turbo test
12 changes: 12 additions & 0 deletions BUGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ All bugs tracked here. Do not create per-package bug files.
| S4 | Server unauthenticated on non-loopback | Med | Server throws if bound to non-loopback without `OPENCODE_SERVER_PASSWORD` |
| S5 | Read tool exposes .env files | Med | Sensitive file deny-list; `always: []` for sensitive files forces permission prompt |

## Open — Code Quality (5)

Found during QA bug hunt (static analysis). Not crashes, but code quality issues.

| # | Issue | Sev | Location | Notes |
| --- | ----- | --- | -------- | ----- |
| Q1 | 95 empty `.catch(() => {})` blocks across 29 files | Low | Various | Most intentional (file ops), ~10 mask real errors in `config.ts`, `lsp/client.ts`, `sdk.tsx` |
| Q2 | 17 TODO/FIXME/HACK comments | Low | 13 files | Track as tech debt; key ones: copilot lost type safety (#374), process.env vs Env.set (#300, #524) |
| Q3 | `console.log` in TUI production code | Low | `cli/cmd/tui/` | **FIXED** in this PR — replaced 18 calls with `Log.create()` |
| Q4 | Copilot SDK lost chunk type safety | Med | `provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts:374` | TODO says "MUST FIX" — type safety lost on Chunk due to error schema |
| Q5 | `process.env` used directly instead of `Env.set` | Low | `provider/provider.ts:300,524` | Env.set only updates shallow copy, not process.env — architectural issue |

## Open — Bugs (0)

_No open bugs._
Expand Down
3 changes: 2 additions & 1 deletion WHAT_WE_DID.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ CAS, edit graph, context editing (6 ops), side threads, objective tracker, class
- **#23:** TUI types fixed, logger types strengthened (unknown → string | Error)
- **#24:** Zod v4 migration (zodToJsonSchema → z.toJSONSchema), 25 Frankencode unit tests, tracking docs cleanup
- **#25:** Upstream catalogue (162 commits + ~195 PRs), security audit (2 CVEs, 5 issues), 6-phase roadmap
- **#26:** Phase 1 security fixes: S1 symlink bypass, S2 exec→spawn, S4 server auth, S5 sensitive deny-list, S3 warning (13 tests)
- **#27:** Phase 2 upstream fixes: prompt parts (#17815), thinkingConfig guard (#18283), chunk timeout (#18264), error messages (#18165), event queue (#18259)
- **#28:** Phase 3+4 merged: OpenTUI 0.1.88 upgrade, agent ordering stability. Most other items already applied or diverged.
- **#29:** Phase 5 tests: filterEdited (8), filterEphemeral (6), ContextEdit validation (10) — 24 new tests
- **#30:** Phase 6 Effect analysis: all 12 upstream Effect PRs reviewed — zero need reimplementation
- **#31:** QA bug hunt: 18 console.log→Log.create() fixes, 5 code quality issues documented (Q1-Q5)
- **#32:** Hardened git hooks: SAST checks, AI attribution stripping (pre-commit/commit-msg/pre-push), PR body cleaning
59 changes: 59 additions & 0 deletions docs/AI_ATTRIBUTION_POLICY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# AI Attribution Stripping Policy

Frankencode automatically strips AI tool attribution from commits and staged files to maintain clean git history and accurate authorship.

## Why

AI coding assistants inject co-authorship trailers, watermark comments, and tool-specific markers into code and commit messages. These:
- Misrepresent authorship (the human developer is the author)
- Leak tool usage metadata unnecessarily
- Create inconsistent attribution across commits

## What Gets Stripped

### Git Trailer Patterns (commit messages)


| Pattern | Tool |
|---------|------|

### Code Comment Watermarks (staged files)


| Pattern | Tool |
|---------|------|

## When Stripping Happens

| Hook | Action |
|------|--------|
| `pre-commit` | Auto-strips watermarks from staged files, re-stages them |
| `commit-msg` | Auto-strips co-authorship trailers from commit message |
| `pre-push` | Verifies no AI attribution in any commit being pushed (blocks if found) |

## SAST Checks (also in pre-commit)

The pre-commit hook also runs lightweight SAST via `scripts/sast-check.sh`:

| Check | CWE | Severity |
|-------|-----|----------|
| No `eval()` | CWE-95 (Code Injection) | Error (blocks) |
| No `new Function()` | CWE-95 | Error (blocks) |
| No `innerHTML` | CWE-79 (XSS) | Warning |
| No hardcoded secrets | CWE-798 | Error (blocks) |
| No `console.log` in src/ | Code quality | Warning |

Add `// sast-ignore` comment to suppress false positives.

## Adding New Patterns

When a new AI tool emerges that injects attribution:
1. Add the pattern to `scripts/strip-ai-attribution.sh`
2. Add the trailer pattern to `.husky/commit-msg`
3. Add the push check pattern to `.husky/pre-push`
4. Update this document

## See Also

- [AGENTS.md](../AGENTS.md) — development guidelines
- [docs/SECURITY_AUDIT.md](SECURITY_AUDIT.md) — security vulnerabilities
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
| [AGENT_CLIENT_PROTOCOL.md](AGENT_CLIENT_PROTOCOL.md) | ACP v1 protocol support for IDE integration |
| [API_PROVIDERS.md](API_PROVIDERS.md) | 21+ LLM providers, models.dev API, transform pipeline |
| [SECURITY_AUDIT.md](SECURITY_AUDIT.md) | CVEs, upstream security issues, Frankencode-specific vulnerabilities |
| [AI_ATTRIBUTION_POLICY.md](AI_ATTRIBUTION_POLICY.md) | AI tool attribution stripping policy, SAST checks |

## Architecture at a Glance

Expand Down
7 changes: 5 additions & 2 deletions packages/opencode/src/cli/cmd/tui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ import { writeHeapSnapshot } from "v8"
import { PromptRefProvider, usePromptRef } from "./context/prompt"
import { TuiConfigProvider } from "./context/tui-config"
import { TuiConfig } from "@/config/tui"
import { Log } from "@/util/log"

const log = Log.create({ service: "tui" })

async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
// can't set raw mode if not a TTY
Expand Down Expand Up @@ -191,7 +194,7 @@ export function tui(input: {
keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }],
onCopySelection: (text) => {
Clipboard.copy(text).catch((error) => {
console.error(`Failed to copy console selection to clipboard: ${error}`)
log.error(`Failed to copy console selection to clipboard: ${error}`)
})
},
},
Expand Down Expand Up @@ -258,7 +261,7 @@ function App() {
const [terminalTitleEnabled, setTerminalTitleEnabled] = createSignal(kv.get("terminal_title_enabled", true))

createEffect(() => {
console.log(JSON.stringify(route.data))
log.debug("route changed", { data: route.data })
})

// Update terminal window title based on current route and session
Expand Down
7 changes: 5 additions & 2 deletions packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { useTheme } from "../context/theme"
import { Keybind } from "@/util/keybind"
import { TextAttributes } from "@opentui/core"
import { useSDK } from "@tui/context/sdk"
import { Log } from "@/util/log"

const log = Log.create({ service: "tui" })

function Status(props: { enabled: boolean; loading: boolean }) {
const { theme } = useTheme()
Expand Down Expand Up @@ -61,10 +64,10 @@ export function DialogMcp() {
if (status.data) {
sync.set("mcp", status.data)
} else {
console.error("Failed to refresh MCP status: no data returned")
log.error("Failed to refresh MCP status: no data returned")
}
} catch (error) {
console.error("Failed to toggle MCP:", error)
log.error("Failed to toggle MCP", { error: error as Error })
} finally {
setLoading(null)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { useToast } from "../ui/toast"
import { useKeybind } from "../context/keybind"
import { DialogSessionList } from "./workspace/dialog-session-list"
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
import { Log } from "@/util/log"

const log = Log.create({ service: "tui" })

async function openWorkspace(input: {
dialog: ReturnType<typeof useDialog>
Expand Down Expand Up @@ -112,10 +115,10 @@ function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) => Promi
setCreating(type)

const result = await sdk.client.experimental.workspace.create({ type, branch: null }).catch((err) => {
console.log(err)
log.error("workspace create failed", { error: err as Error })
return undefined
})
console.log(JSON.stringify(result, null, 2))
log.debug("workspace create result", { data: result ?? "undefined" })
const workspace = result?.data
if (!workspace) {
setCreating(undefined)
Expand Down
5 changes: 4 additions & 1 deletion packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { TuiEvent } from "../../event"
import { iife } from "@/util/iife"
import { Locale } from "@/util/locale"
import { formatDuration } from "@/util/format"
import { Log } from "@/util/log"
import { createColors, createFrames } from "../../ui/spinner.ts"
import { useDialog } from "@tui/ui/dialog"
import { DialogProvider as DialogProviderConnect } from "../dialog-provider"
Expand All @@ -36,6 +37,8 @@ import { useKV } from "../../context/kv"
import { useTextareaKeybindings } from "../textarea-keybindings"
import { DialogSkill } from "../dialog-skill"

const log = Log.create({ service: "tui" })

export type PromptProps = {
sessionID?: string
workspaceID?: string
Expand Down Expand Up @@ -549,7 +552,7 @@ export function Prompt(props: PromptProps) {
})

if (res.error) {
console.log("Creating a session failed:", res.error)
log.error("Creating a session failed", { error: res.error })

toast.show({
message: "Creating a session failed. Open console for more details.",
Expand Down
5 changes: 4 additions & 1 deletion packages/opencode/src/cli/cmd/tui/context/route.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { createStore } from "solid-js/store"
import { createSimpleContext } from "./helper"
import { Log } from "@/util/log"
import type { PromptInfo } from "../component/prompt/history"

const log = Log.create({ service: "tui" })

export type HomeRoute = {
type: "home"
initialPrompt?: PromptInfo
Expand Down Expand Up @@ -32,7 +35,7 @@ export const { use: useRoute, provider: RouteProvider } = createSimpleContext({
return store
},
navigate(route: Route) {
console.log("navigate", route)
log.debug("navigate", { route })
setStore(route)
},
}
Expand Down
4 changes: 3 additions & 1 deletion packages/opencode/src/cli/cmd/tui/context/sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import { Log } from "@/util/log"
import type { Path } from "@opencode-ai/sdk"
import type { Workspace } from "@opencode-ai/sdk/v2"

const log = Log.create({ service: "tui" })

export const { use: useSync, provider: SyncProvider } = createSimpleContext({
name: "Sync",
init: () => {
Expand Down Expand Up @@ -356,7 +358,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const args = useArgs()

async function bootstrap() {
console.log("bootstrapping")
log.debug("bootstrapping")
const start = Date.now() - 30 * 24 * 60 * 60 * 1000
const sessionListPromise = sdk.client.session
.list({ start: start })
Expand Down
7 changes: 5 additions & 2 deletions packages/opencode/src/cli/cmd/tui/context/theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SyntaxStyle, RGBA, type TerminalColors } from "@opentui/core"
import path from "path"
import { createEffect, createMemo, onMount } from "solid-js"
import { createSimpleContext } from "./helper"
import { Log } from "@/util/log"
import { Glob } from "../../../../util/glob"
import aura from "./theme/aura.json" with { type: "json" }
import ayu from "./theme/ayu.json" with { type: "json" }
Expand Down Expand Up @@ -43,6 +44,8 @@ import { Global } from "@/global"
import { Filesystem } from "@/util/filesystem"
import { useTuiConfig } from "./tui-config"

const log = Log.create({ service: "tui" })

type ThemeColors = {
primary: RGBA
secondary: RGBA
Expand Down Expand Up @@ -317,13 +320,13 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
onMount(init)

function resolveSystemTheme() {
console.log("resolveSystemTheme")
log.debug("resolveSystemTheme")
renderer
.getPalette({
size: 16,
})
.then((colors) => {
console.log(colors.palette)
log.debug("system palette", { palette: colors.palette })
if (!colors.palette[0]) {
if (store.active === "system") {
setStore(
Expand Down
5 changes: 4 additions & 1 deletion packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { Dynamic } from "solid-js/web"
import path from "path"
import { useRoute, useRouteData } from "@tui/context/route"
import { Log } from "@/util/log"
import { useSync } from "@tui/context/sync"
import { SplitBorder } from "@tui/component/border"
import { Spinner } from "@tui/component/spinner"
Expand Down Expand Up @@ -83,6 +84,8 @@ import { formatTranscript } from "../../util/transcript"
import { UI } from "@/cli/ui.ts"
import { useTuiConfig } from "../../context/tui-config"

const log = Log.create({ service: "tui" })

addDefaultParsers(parsers.parsers)

class CustomSpeedScroll implements ScrollAcceleration {
Expand Down Expand Up @@ -196,7 +199,7 @@ export function Session() {
if (scroll) scroll.scrollBy(100_000)
})
.catch((e) => {
console.error(e)
log.error("session sync failed", { error: e as Error })
toast.show({
message: `Session not found: ${route.sessionID}`,
variant: "error",
Expand Down
Loading
Loading