Skip to content
Closed
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
2 changes: 1 addition & 1 deletion src/commands/install-github-app/repoSlug.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ test('keeps owner/repo input as-is', () => {
test('extracts slug from https GitHub URLs', () => {
assert.equal(
extractGitHubRepoSlug('https://github.com/verbeux-ai/code'),
'Gitlawb/verboo',
'verbeux-ai/code',
)
assert.equal(
extractGitHubRepoSlug('https://www.github.com/Gitlawb/verboo.git'),
Expand Down
2 changes: 1 addition & 1 deletion src/schemas/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* Both files now import from this shared location instead of each other.
*/

import { HOOK_EVENTS, type HookEvent } from 'src/entrypoints/agentSdkTypes.js'
import { HOOK_EVENTS, type HookEvent } from 'src/entrypoints/sdk/coreTypes.js'
import { z } from 'zod/v4'
import { lazySchema } from '../utils/lazySchema.js'
import { SHELL_TYPES } from '../utils/shell/shellProvider.js'
Expand Down
23 changes: 6 additions & 17 deletions src/services/api/openaiShim.diagnostics.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
import { afterEach, beforeEach, expect, mock, spyOn, test } from 'bun:test'
import { acquireSharedMutationLock, releaseSharedMutationLock } from '../../test/sharedMutationLock.js'
import * as debugModule from '../../utils/debug.js'

const originalFetch = globalThis.fetch
const originalEnv = {
Expand Down Expand Up @@ -33,10 +34,7 @@ afterEach(() => {
})

test('logs classified transport diagnostics with category and code', async () => {
const debugSpy = mock(() => {})
mock.module('../../utils/debug.js', () => ({
logForDebugging: debugSpy,
}))
const debugSpy = spyOn(debugModule, 'logForDebugging').mockImplementation(() => {})

const nonce = `${Date.now()}-${Math.random()}`
const { createOpenAIShimClient } = await import(`./openaiShim.ts?ts=${nonce}`)
Expand Down Expand Up @@ -80,10 +78,7 @@ test('logs classified transport diagnostics with category and code', async () =>
})

test('redacts credentials in transport diagnostic URL logs', async () => {
const debugSpy = mock(() => {})
mock.module('../../utils/debug.js', () => ({
logForDebugging: debugSpy,
}))
const debugSpy = spyOn(debugModule, 'logForDebugging').mockImplementation(() => {})

const nonce = `${Date.now()}-${Math.random()}`
const { createOpenAIShimClient } = await import(`./openaiShim.ts?ts=${nonce}`)
Expand Down Expand Up @@ -127,10 +122,7 @@ test('redacts credentials in transport diagnostic URL logs', async () => {
expect(logLine).not.toContain('supersecret@')
})
test('logs self-heal localhost fallback with redacted from/to URLs', async () => {
const debugSpy = mock(() => {})
mock.module('../../utils/debug.js', () => ({
logForDebugging: debugSpy,
}))
const debugSpy = spyOn(debugModule, 'logForDebugging').mockImplementation(() => {})

const nonce = `${Date.now()}-${Math.random()}`
const { createOpenAIShimClient } = await import(`./openaiShim.ts?ts=${nonce}`)
Expand Down Expand Up @@ -204,10 +196,7 @@ test('logs self-heal localhost fallback with redacted from/to URLs', async () =>
})

test('logs self-heal toolless retry for local tool-call incompatibility', async () => {
const debugSpy = mock(() => {})
mock.module('../../utils/debug.js', () => ({
logForDebugging: debugSpy,
}))
const debugSpy = spyOn(debugModule, 'logForDebugging').mockImplementation(() => {})

const nonce = `${Date.now()}-${Math.random()}`
const { createOpenAIShimClient } = await import(`./openaiShim.ts?ts=${nonce}`)
Expand Down
11 changes: 6 additions & 5 deletions src/services/api/withRetry.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from 'bun:test'
import * as providersModule from '../../utils/model/providers.js'
import { APIError } from '@anthropic-ai/sdk'
import { acquireSharedMutationLock, releaseSharedMutationLock } from '../../test/sharedMutationLock.js'

Expand Down Expand Up @@ -60,10 +61,10 @@ async function importFreshWithRetryModule(
| 'foundry' = 'firstParty',
) {
mock.restore()
mock.module('src/utils/model/providers.js', () => ({
getAPIProvider: () => provider,
getAPIProviderForStatsig: () => provider,
}))
spyOn(providersModule, 'getAPIProvider').mockImplementation(() => provider)
spyOn(providersModule, 'getAPIProviderForStatsig').mockImplementation(
() => provider as ReturnType<typeof providersModule.getAPIProviderForStatsig>,
)
return import(`./withRetry.js?ts=${Date.now()}-${Math.random()}`)
}

Expand Down
6 changes: 3 additions & 3 deletions src/skills/loadSkillsDir.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from '../test/sharedMutationLock.js'

function writeSkill(rootDir: string, skillPath: string): void {
const skillDir = join(rootDir, '.verboo', 'skills', ...skillPath.split('/'))
const skillDir = join(rootDir, 'skills', ...skillPath.split('/'))
mkdirSync(skillDir, { recursive: true })
writeFileSync(
join(skillDir, 'SKILL.md'),
Expand Down Expand Up @@ -47,15 +47,15 @@ test('loads flat and nested skills with colon namespaces', async () => {

const nestedSkill = promptSkills.find(skill => skill.name === 'git:commit')
assert.ok(nestedSkill)
assert.equal(nestedSkill.skillRoot, join(configDir, '.verboo', 'skills', 'git', 'commit'))
assert.equal(nestedSkill.skillRoot, join(configDir, 'skills', 'git', 'commit'))

const deepSkill = promptSkills.find(
skill => skill.name === 'frontend:react:form',
)
assert.ok(deepSkill)
assert.equal(
deepSkill.skillRoot,
join(configDir, '.verboo', 'skills', 'frontend', 'react', 'form'),
join(configDir, 'skills', 'frontend', 'react', 'form'),
)
} finally {
try {
Expand Down
12 changes: 10 additions & 2 deletions src/tools.lsp.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { beforeEach, expect, mock, test } from 'bun:test'
import { getEmptyToolPermissionContext } from './Tool.js'
import type { ToolPermissionContext } from './Tool.js'

let lspConnected = false

Expand All @@ -8,6 +8,7 @@ mock.module('./services/lsp/manager.js', () => ({
getLspServerManager: () => undefined,
isLspConnected: () => lspConnected,
reinitializeLspServerManager: () => {},
shutdownLspServerManager: async () => {},
waitForInitialization: async () => {},
}))

Expand All @@ -22,7 +23,14 @@ test('LSPTool is part of the base tool pool', () => {
})

test('LSPTool is filtered from usable tools until a server is connected', () => {
const permissionContext = getEmptyToolPermissionContext()
const permissionContext: ToolPermissionContext = {
mode: 'default',
additionalWorkingDirectories: new Map(),
alwaysAllowRules: {},
alwaysDenyRules: {},
alwaysAskRules: {},
isBypassPermissionsModeAvailable: false,
}

expect(getTools(permissionContext).map(tool => tool.name)).not.toContain('LSP')

Expand Down
4 changes: 2 additions & 2 deletions src/types/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import {
HOOK_EVENTS,
type HookInput,
type PermissionUpdate,
} from 'src/entrypoints/agentSdkTypes.js'
} from 'src/entrypoints/sdk/coreTypes.js'
import type {
HookJSONOutput,
AsyncHookJSONOutput,
SyncHookJSONOutput,
} from 'src/entrypoints/agentSdkTypes.js'
} from 'src/entrypoints/sdk/coreTypes.js'
import type { Message } from 'src/types/message.js'
import type { PermissionResult } from 'src/utils/permissions/PermissionResult.js'
import { permissionBehaviorSchema } from 'src/utils/permissions/PermissionRule.js'
Expand Down
10 changes: 6 additions & 4 deletions src/utils/autoUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,10 +369,12 @@ export async function getLatestVersion(
logForDebugging(
`Tag 'stable' indisponível; caindo para 'latest' como fallback`,
)
const fallback = await execFileNoThrowWithCwd(
'npm',
['view', `${MACRO.PACKAGE_URL}@latest`, 'version', '--prefer-online'],
{ abortSignal: AbortSignal.timeout(5000), cwd: homedir() },
const fallback = await withTimeoutSignal(5000, abortSignal =>
execFileNoThrowWithCwd(
'npm',
['view', `${MACRO.PACKAGE_URL}@latest`, 'version', '--prefer-online'],
{ abortSignal, cwd: homedir() },
),
)
if (fallback.code === 0) {
return fallback.stdout.trim()
Expand Down
31 changes: 5 additions & 26 deletions src/utils/execFileNoThrow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,51 +88,30 @@ function validateWorkingDirectory(cwd: string | undefined): string | null {
return null
}

// Track which env keys we've already logged about so we don't spam the
// debug log every time a child is spawned with the same dirty env.
const warnedDirtyEnvKeys = new Set<string>()

function sanitizeEnvironment(
env: NodeJS.ProcessEnv | undefined,
): { value?: NodeJS.ProcessEnv; error?: string } {
if (!env) {
return {}
}

// Keys with control characters are still rejected — there is no safe way
// to forward them and they almost certainly indicate tampering. Values
// with control characters are dropped instead of aborting the whole spawn:
// the most common cause is a multi-line value exported by the user's
// shell rc (PS1, PROMPT_COMMAND, ssh keys), and rejecting the whole call
// turned simple operations like `git clone` into hard failures.
// Keys OR values with control characters are rejected — there is no safe way
// to forward them and they almost certainly indicate tampering.
const sanitized: NodeJS.ProcessEnv = {}
const droppedKeys: string[] = []
for (const [key, value] of Object.entries(env)) {
if (CONTROL_CHAR_PATTERN.test(key)) {
return {
error: 'Unsafe environment: control characters are not allowed in keys',
}
}
if (typeof value === 'string' && CONTROL_CHAR_PATTERN.test(value)) {
droppedKeys.push(key)
continue
return {
error: `Unsafe environment: control characters are not allowed in values`,
}
}
sanitized[key] = value
}

if (droppedKeys.length > 0) {
const newKeys = droppedKeys.filter(k => !warnedDirtyEnvKeys.has(k))
if (newKeys.length > 0) {
for (const k of newKeys) warnedDirtyEnvKeys.add(k)
// Use console.warn so the user sees this without DEBUG=1; it is
// strictly less noisy than the previous behavior of failing the
// command outright.
console.warn(
`[verboo] Dropping env var(s) with control characters before spawning child: ${newKeys.join(', ')}. Fix the source (likely your shell rc) to silence this.`,
)
}
}

return { value: sanitized }
}

Expand Down
18 changes: 8 additions & 10 deletions src/utils/hookChains.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from 'bun:test'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
Expand All @@ -23,17 +23,15 @@ async function importHookChainsModule(options?: {

const allowRemoteSessions = options?.allowRemoteSessions ?? true

mock.module('../services/analytics/index.js', () => ({
logEvent: () => {},
}))
// Spy on real modules instead of mock.module to preserve transitive exports
const analytics = await import('../services/analytics/index.js')
spyOn(analytics, 'logEvent').mockImplementation(() => {})

mock.module('./telemetry/events.js', () => ({
logOTelEvent: async () => {},
}))
const telemetry = await import('./telemetry/events.js')
spyOn(telemetry, 'logOTelEvent').mockImplementation(async () => {})

mock.module('../services/policyLimits/index.js', () => ({
isPolicyAllowed: () => allowRemoteSessions,
}))
const policyLimits = await import('../services/policyLimits/index.js')
spyOn(policyLimits, 'isPolicyAllowed').mockImplementation(() => allowRemoteSessions)

return import(`./hookChains.js?test=${Date.now()}-${Math.random()}`)
}
Expand Down
2 changes: 1 addition & 1 deletion src/utils/hookChains.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createHash } from 'crypto'
import { statSync } from 'fs'
import { join, resolve } from 'path'
import { HOOK_EVENTS } from 'src/entrypoints/agentSdkTypes.js'
import { HOOK_EVENTS } from 'src/entrypoints/sdk/coreTypes.js'
import { getOriginalCwd } from '../bootstrap/state.js'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
Expand Down
2 changes: 1 addition & 1 deletion src/utils/hooks/registerFrontmatterHooks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HOOK_EVENTS, type HookEvent } from 'src/entrypoints/agentSdkTypes.js'
import { HOOK_EVENTS, type HookEvent } from 'src/entrypoints/sdk/coreTypes.js'
import type { AppState } from 'src/state/AppState.js'
import { logForDebugging } from '../debug.js'
import type { HooksSettings } from '../settings/types.js'
Expand Down
2 changes: 1 addition & 1 deletion src/utils/hooks/registerSkillHooks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HOOK_EVENTS } from 'src/entrypoints/agentSdkTypes.js'
import { HOOK_EVENTS } from 'src/entrypoints/sdk/coreTypes.js'
import type { AppState } from 'src/state/AppState.js'
import { logForDebugging } from '../debug.js'
import type { HooksSettings } from '../settings/types.js'
Expand Down
2 changes: 1 addition & 1 deletion src/utils/hooks/sessionHooks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HOOK_EVENTS, type HookEvent } from 'src/entrypoints/agentSdkTypes.js'
import { HOOK_EVENTS, type HookEvent } from 'src/entrypoints/sdk/coreTypes.js'
import type { AppState } from 'src/state/AppState.js'
import type { Message } from 'src/types/message.js'
import { logForDebugging } from '../debug.js'
Expand Down
15 changes: 11 additions & 4 deletions src/utils/knowledgeGraph.stress.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ import { setClaudeConfigHomeDirForTesting } from './envUtils.js'
import { getFsImplementation } from './fsOperations.js'

describe('KnowledgeGraph Phase 1 Stress & Edge Cases', () => {
const originalConfigDir = process.env.CLAUDE_CONFIG_DIR
const originalConfigDir = process.env.VERBOO_CONFIG_DIR
const originalOrama = process.env.OPENCLAUDE_KNOWLEDGE_ORAMA
const originalProjectsDir = process.env.VERBOO_PROJECTS_DIR
const configDir = mkdtempSync(join(tmpdir(), 'openclaude-stress-'))
const cwd = getFsImplementation().cwd()

Expand Down Expand Up @@ -47,7 +48,8 @@ describe('KnowledgeGraph Phase 1 Stress & Edge Cases', () => {

beforeEach(async () => {
await acquireEnvMutex()
process.env.CLAUDE_CONFIG_DIR = configDir
process.env.VERBOO_CONFIG_DIR = configDir
process.env.VERBOO_PROJECTS_DIR = join(configDir, 'projects')
process.env.OPENCLAUDE_KNOWLEDGE_ORAMA = '1'
setClaudeConfigHomeDirForTesting(configDir)
resetGlobalGraph()
Expand All @@ -58,9 +60,14 @@ describe('KnowledgeGraph Phase 1 Stress & Edge Cases', () => {
resetGlobalGraph()
clearMemoryOnly()
if (originalConfigDir === undefined) {
delete process.env.CLAUDE_CONFIG_DIR
delete process.env.VERBOO_CONFIG_DIR
} else {
process.env.CLAUDE_CONFIG_DIR = originalConfigDir
process.env.VERBOO_CONFIG_DIR = originalConfigDir
}
if (originalProjectsDir === undefined) {
delete process.env.VERBOO_PROJECTS_DIR
} else {
process.env.VERBOO_PROJECTS_DIR = originalProjectsDir
}
if (originalOrama === undefined) {
delete process.env.OPENCLAUDE_KNOWLEDGE_ORAMA
Expand Down
2 changes: 1 addition & 1 deletion src/utils/markdownConfigLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const CLAUDE_CONFIG_DIRECTORIES = [

export type ClaudeConfigDirectory = (typeof CLAUDE_CONFIG_DIRECTORIES)[number]

export const PROJECT_CONFIG_DIR_NAMES = ['.verboo', '.claude'] as const
export const PROJECT_CONFIG_DIR_NAMES = ['.claude', '.verboo'] as const

export type MarkdownFile = {
filePath: string
Expand Down
18 changes: 14 additions & 4 deletions src/utils/permissions/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,25 @@ export function getClaudeSkillScope(
const absolutePath = expandPath(filePath)
const absolutePathLower = normalizeCaseForComparison(absolutePath)

const defaultConfigHome = join(homedir(), '.verboo').normalize('NFC')
const isDefaultConfigHome = getClaudeConfigHomeDir() === defaultConfigHome

const bases = [
{
dir: expandPath(join(getOriginalCwd(), '.verboo', 'skills')),
prefix: '/.verboo/skills/',
},
{
dir: expandPath(join(getClaudeConfigHomeDir(), 'skills')),
prefix: '~/.verboo/skills/',
},
// Só emite regra com prefixo ~/.verboo/skills/ quando VERBOO_CONFIG_DIR
// é o diretório padrão. Config dir customizado (ex.: env var apontando
// para outro path) não deve gerar regra fixa — o prefixo não bateria.
...(isDefaultConfigHome
? [
{
dir: expandPath(join(getClaudeConfigHomeDir(), 'skills')),
prefix: '~/.verboo/skills/',
},
]
: []),
{
dir: expandPath(join(homedir(), '.claude', 'skills')),
prefix: '~/.claude/skills/',
Expand Down
Loading