Skip to content

Commit ffe970e

Browse files
committed
Merge remote-tracking branch 'origin/main' into test-opencode-zen
# Conflicts: # web/src/app/api/v1/chat/completions/__tests__/completions.test.ts
2 parents 018a162 + 873c191 commit ffe970e

27 files changed

Lines changed: 1598 additions & 370 deletions

File tree

agents/file-explorer/code-searcher.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ const codeSearcher: SecretAgentDefinition = {
8585
yield {
8686
toolName: 'set_output',
8787
input: {
88+
message: '',
8889
results: toolResults,
8990
},
9091
includeToolCall: false,

cli/release/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codebuff",
3-
"version": "1.0.673",
3+
"version": "1.0.674",
44
"description": "AI coding agent",
55
"license": "MIT",
66
"bin": {
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { describe, expect, test } from 'bun:test'
2+
import React from 'react'
3+
import { renderToStaticMarkup } from 'react-dom/server'
4+
5+
import { initializeThemeStore } from '../../../hooks/use-theme'
6+
import { CodeSearchComponent } from '../code-search'
7+
8+
import type { ChatTheme } from '../../../types/theme-system'
9+
import type { ToolBlock } from '../types'
10+
11+
initializeThemeStore()
12+
13+
const createToolBlock = (
14+
output?: string,
15+
): ToolBlock & { toolName: 'code_search' } => ({
16+
type: 'tool',
17+
toolName: 'code_search',
18+
toolCallId: 'code-search-test',
19+
input: {
20+
pattern: 'getAgentBaseName',
21+
cwd: 'cli/src/utils',
22+
},
23+
output,
24+
})
25+
26+
describe('CodeSearchComponent', () => {
27+
test('uses formatted match count from current code search output', () => {
28+
const result = CodeSearchComponent.render(
29+
createToolBlock(`Found 2 matches
30+
./message-block-helpers.ts:
31+
Line 13: export const getAgentBaseName = (type: string): string => {
32+
Line 196: getAgentBaseName(options.agentType ?? '') === 'code-searcher'`),
33+
{} as ChatTheme,
34+
{
35+
availableWidth: 80,
36+
indentationOffset: 0,
37+
labelWidth: 10,
38+
},
39+
)
40+
41+
const markup = renderToStaticMarkup(<>{result.content}</>)
42+
43+
expect(markup).toContain('getAgentBaseName in cli/src/utils (2 results)')
44+
})
45+
})

cli/src/components/tools/code-search.tsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,22 @@ export const CodeSearchComponent = defineToolComponent({
2323

2424
if (toolBlock.output && typeof toolBlock.output === 'string') {
2525
const lines = toolBlock.output.split('\n')
26+
const matchCountLine = lines.find((line) =>
27+
/^Found \d+ matches?$/.test(line.trim()),
28+
)
29+
const parsedTotalResults = matchCountLine
30+
?.trim()
31+
.match(/^Found (\d+) matches?$/)?.[1]
2632

27-
for (const line of lines) {
28-
const trimmed = line.trim()
33+
if (parsedTotalResults !== undefined) {
34+
totalResults = Number(parsedTotalResults)
35+
} else {
36+
for (const line of lines) {
37+
const trimmed = line.trim()
2938

30-
// Result lines start with a number followed by a colon
31-
if (/^\d+:/.test(trimmed)) {
32-
totalResults++
39+
if (/^(?:Line\s+)?\d+:/.test(trimmed)) {
40+
totalResults++
41+
}
3342
}
3443
}
3544
}
@@ -52,12 +61,7 @@ export const CodeSearchComponent = defineToolComponent({
5261

5362
// Return as content using SimpleToolCallItem
5463
return {
55-
content: (
56-
<SimpleToolCallItem
57-
name="Search"
58-
description={summary}
59-
/>
60-
),
64+
content: <SimpleToolCallItem name="Search" description={summary} />,
6165
}
6266
},
6367
})

cli/src/utils/__tests__/message-block-helpers.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,23 @@ describe('extractSpawnAgentResultContent', () => {
376376
hasError: false,
377377
})
378378
})
379+
380+
test('uses an empty structuredOutput message as no display content', () => {
381+
const result = extractSpawnAgentResultContent({
382+
type: 'structuredOutput',
383+
value: {
384+
message: '',
385+
results: [
386+
{
387+
stdout: 'Found 1 match\n./file.ts:\nLine 1: needle',
388+
message: 'Exit code: 0',
389+
},
390+
],
391+
},
392+
})
393+
394+
expect(result).toEqual({ content: '', hasError: false })
395+
})
379396
})
380397

381398
describe('appendInterruptionNotice', () => {

docs/authentication.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@ sequenceDiagram
1313
participant DB as Database
1414
1515
CLI->>Web: POST /api/auth/cli/code {fingerprintId}
16-
Web->>Web: Generate auth code (1h expiry)
17-
Web->>CLI: Return login URL
16+
Web->>Web: Generate signed auth payload (1h expiry)
17+
Web->>DB: Store payload behind opaque browser token
18+
Web->>CLI: Return login URL with opaque token
1819
CLI->>CLI: Open browser
1920
Note over Web: User completes OAuth
21+
Web->>DB: Resolve opaque token to signed payload
22+
Web->>DB: Mark opaque token consumed
2023
Web->>DB: Check fingerprint ownership
2124
Web->>DB: Create/update session
2225
loop Every 5s
@@ -64,11 +67,14 @@ sequenceDiagram
6467
### 4. Failure: Invalid/Expired Code
6568

6669
- Auth code validation fails or expired (1h limit)
70+
- Opaque browser tokens resolve expired signed payloads before returning the expired-code error
6771
- Returns authentication error
6872

6973
## Security Features
7074

71-
- Auth codes expire after 1 hour
75+
- Signed auth payloads expire after 1 hour
76+
- Browser login URLs use opaque 43-character tokens instead of exposing the signed auth payload
77+
- Opaque browser tokens are stored in `verificationToken` under `cli-login:<token>` and atomically moved to `cli-login-consumed:<token-hash>` when onboarding resolves them; consumed markers scrub the signed auth payload from the `token` column
7278
- Fingerprint uniqueness: hardware info + 8 random bytes
7379
- Ownership conflicts blocked and logged
7480
- Sessions linked to fingerprint_id in database

docs/testing.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,37 @@ CLI hook testing note: React 19 + Bun + RTL `renderHook()` is unreliable; prefer
99
## CLI tmux Testing
1010

1111
For testing CLI behavior via tmux, use the helper scripts in `scripts/tmux/`. These handle bracketed paste mode and session logging automatically. Session data is saved to `debug/tmux-sessions/` in YAML format and can be viewed with `bun scripts/tmux/tmux-viewer/index.tsx`. See `scripts/tmux/README.md` for details.
12+
13+
Useful workflow for agents:
14+
15+
```bash
16+
# Start the dev CLI in a detached tmux session.
17+
SESSION=$(./scripts/tmux/tmux-cli.sh start --name cli-check -w 160 -h 40 --wait 6)
18+
19+
# Capture the initial screen. Captures are written to debug/tmux-sessions/$SESSION/.
20+
./scripts/tmux/tmux-cli.sh capture "$SESSION" --label initial
21+
22+
# Send a prompt. The helper uses bracketed paste so text is not dropped.
23+
./scripts/tmux/tmux-cli.sh send "$SESSION" "Search for getAgentBaseName and report what you find" --wait-idle 4
24+
25+
# Capture after the run, then inspect the saved capture text.
26+
./scripts/tmux/tmux-cli.sh capture "$SESSION" --label after-search --wait 2
27+
28+
# Clean up when finished.
29+
./scripts/tmux/tmux-cli.sh stop "$SESSION"
30+
```
31+
32+
If a change can be verified with a small local harness instead of a live model-backed CLI run, run that harness inside tmux too. This still checks terminal rendering and produces a capture:
33+
34+
```bash
35+
SESSION=$(./scripts/tmux/tmux-cli.sh start \
36+
--name render-check \
37+
-w 160 -h 20 \
38+
--wait 1 \
39+
--command "bun .context/my-render-check.tsx")
40+
41+
./scripts/tmux/tmux-cli.sh capture "$SESSION" --label rendered
42+
./scripts/tmux/tmux-cli.sh stop "$SESSION"
43+
```
44+
45+
When verifying UI output, prefer checking the saved capture file for concrete strings that should and should not appear. For example, after expanding a code-searcher agent, check that the capture shows the search summary but not raw structured payload keys like `results:` or `stdout:`.

freebuff/cli/release/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "freebuff",
3-
"version": "0.0.84",
3+
"version": "0.0.85",
44
"description": "The world's strongest free coding agent",
55
"license": "MIT",
66
"bin": {

freebuff/web/src/app/api/auth/cli/code/route.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { randomBytes } from 'node:crypto'
2+
13
import { genAuthCode } from '@codebuff/common/util/credentials'
24
import db from '@codebuff/internal/db'
35
import * as schema from '@codebuff/internal/db/schema'
@@ -6,6 +8,11 @@ import { and, eq, gt } from 'drizzle-orm'
68
import { NextResponse } from 'next/server'
79
import { z } from 'zod/v4'
810

11+
import {
12+
buildCliAuthCode,
13+
getCliAuthCodeHashPrefix,
14+
getCliAuthCodeTokenIdentifier,
15+
} from '@/app/onboard/_helpers'
916
import { logger } from '@/util/logger'
1017

1118
import { getLoginUrlOrigin } from './_origin'
@@ -55,6 +62,19 @@ export async function POST(req: Request) {
5562
)
5663
}
5764

65+
const authCode = buildCliAuthCode(
66+
fingerprintId,
67+
expiresAt.toString(),
68+
fingerprintHash,
69+
)
70+
const loginToken = randomBytes(32).toString('base64url')
71+
72+
await db.insert(schema.verificationToken).values({
73+
identifier: getCliAuthCodeTokenIdentifier(loginToken),
74+
token: authCode,
75+
expires: new Date(expiresAt),
76+
})
77+
5878
const loginUrl = new URL(
5979
'/login',
6080
getLoginUrlOrigin(
@@ -64,9 +84,25 @@ export async function POST(req: Request) {
6484
env.NEXT_PUBLIC_CB_ENVIRONMENT !== 'prod',
6585
),
6686
)
67-
loginUrl.searchParams.set(
68-
'auth_code',
69-
`${fingerprintId}.${expiresAt}.${fingerprintHash}`,
87+
loginUrl.searchParams.set('auth_code', loginToken)
88+
89+
logger.info(
90+
{
91+
authCodeTokenHashPrefix: getCliAuthCodeHashPrefix(loginToken),
92+
authCodeTokenLength: loginToken.length,
93+
fingerprintIdPrefix: fingerprintId.slice(0, 24),
94+
fingerprintIdLength: fingerprintId.length,
95+
expiresAt,
96+
loginUrlOrigin: loginUrl.origin,
97+
requestOrigin: new URL(req.url).origin,
98+
requestHost: req.headers.get('host'),
99+
forwardedHost: req.headers.get('x-forwarded-host'),
100+
forwardedProto: req.headers.get('x-forwarded-proto'),
101+
originHeader: req.headers.get('origin'),
102+
configuredAppUrl: env.NEXT_PUBLIC_CODEBUFF_APP_URL,
103+
environment: env.NEXT_PUBLIC_CB_ENVIRONMENT,
104+
},
105+
'Issued Freebuff CLI auth code token',
70106
)
71107

72108
return NextResponse.json({

0 commit comments

Comments
 (0)