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
2 changes: 1 addition & 1 deletion .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
run: |
UV_VERSION=$(git ls-remote --tags --sort=-v:refname https://github.com/astral-sh/uv.git 'refs/tags/[0-9]*' | head -1 | sed 's/.*refs\/tags\///')
OPENCODE_VERSION=1.18.16
MICROSANDBOX_VERSION=0.6.8
MICROSANDBOX_VERSION=0.6.15
echo "uv=${UV_VERSION}" >> $GITHUB_OUTPUT
echo "opencode=${OPENCODE_VERSION}" >> $GITHUB_OUTPUT
echo "microsandbox=${MICROSANDBOX_VERSION}" >> $GITHUB_OUTPUT
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ FROM base AS runner

ARG UV_VERSION=latest
ARG OPENCODE_VERSION=1.18.16
ARG MICROSANDBOX_VERSION=0.6.8
ARG MICROSANDBOX_VERSION=0.6.15
# Bump TOOLS_CACHEBUST (e.g. via --build-arg) to force a fresh uv/opencode
# install without invalidating the rest of the build cache.
ARG TOOLS_CACHEBUST=0
Expand Down
14 changes: 14 additions & 0 deletions backend/src/routes/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import { opencodeServerManager, ConfigReloadError, resolveOpenCodeExecutable } f
import { getOrCreateInternalToken, rotateInternalToken } from '../services/internal-token'
import { sseAggregator } from '../services/sse-aggregator'
import type { OpenCodeSupervisor } from '../services/opencode-supervisor'
import { detectSandboxCapability } from '../services/sandbox/capability'
import { getProcessIdentityAttestationError } from '../services/opencode/process-identity'
import { restartOpenCode, restartOpenCodeAfterCommit, reloadOpenCodeConfig, getOpenCodeRestartCoordinator } from '../services/opencode-restart'
import type { GitAuthService } from '../services/git-auth'
import { DEFAULT_AGENTS_MD } from '../constants'
Expand Down Expand Up @@ -394,6 +396,18 @@ export function createSettingsRoutes(db: Database, gitAuthService: GitAuthServic
}

const currentSettings = settingsService.getSettings(userId)

if (currentSettings.preferences.sandbox?.enabled !== true && validated.preferences.sandbox?.enabled === true) {
const capability = detectSandboxCapability()
if (capability.available === false) {
return c.json({ error: `Cannot enable sandboxing: ${capability.reason}` }, 400)
}
const attestationError = getProcessIdentityAttestationError()
if (attestationError !== null) {
return c.json({ error: `Cannot enable sandboxing: ${attestationError}` }, 400)
}
}

const settings = settingsService.updateSettings(validated.preferences, userId)

const sandboxChanged = sandboxEnforcementChanged(currentSettings.preferences.sandbox, validated.preferences.sandbox)
Expand Down
105 changes: 68 additions & 37 deletions backend/src/services/credential-provider.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { Database } from 'bun:sqlite'
import type { GitCredential, Repo } from '@opencode-manager/shared'
import type { GitCredential, Repo, UserPreferences } from '@opencode-manager/shared'
import { SettingsService } from './settings'
import {
findPatCredentialForHost,
getSSHCredentialsForHost,
createGitEnv,
createGhCliEnv,
findGitHubCredential,
type ResolvedGitCredential,
} from '../utils/git-auth'
Expand All @@ -22,6 +23,12 @@ interface CredentialResolutionOptions {
repoId?: number
}

interface CredentialResolutionContext {
preferences: UserPreferences
credentials: GitCredential[]
repo: Repo | null
}

export class CredentialProvider {
private settingsService: SettingsService
private database: Database
Expand All @@ -32,8 +39,7 @@ export class CredentialProvider {
}

getGitCredentials(): GitCredential[] {
const settings = this.settingsService.getSettings('default')
return (settings.preferences.gitCredentials || []) as GitCredential[]
return this.getCredentials(this.getPreferences())
}

getGitCredentialById(credentialId: string | undefined): GitCredential | null {
Expand All @@ -42,10 +48,10 @@ export class CredentialProvider {
}

getPatCredentialForHost(hostname: string, options: CredentialResolutionOptions = {}): ResolvedGitCredential | null {
const credentials = this.getGitCredentials()
const selectedCredential = this.getSelectedCredential(options, credentials)
const context = this.resolveContext(options)
const selectedCredential = this.getSelectedCredential(context)
const selectedMatch = selectedCredential ? findPatCredentialForHost([selectedCredential], hostname) : null
return selectedMatch ?? findPatCredentialForHost(credentials, hostname)
return selectedMatch ?? findPatCredentialForHost(context.credentials, hostname)
}

getSshCredentialsForHost(host: string): GitCredential[] {
Expand All @@ -57,24 +63,22 @@ export class CredentialProvider {
}

getGitEnv(options: CredentialResolutionOptions = {}): Record<string, string> {
const credentials = this.getGitCredentials()
return createGitEnv(credentials, this.getSelectedCredential(options, credentials))
return this.getGitEnvForContext(this.resolveContext(options))
}

isSandboxGitCredentialsAllowed(options: CredentialResolutionOptions = {}): boolean {
const repo = this.resolveRepo(options)
if (repo) {
const repoOverride = getRepoSandboxGitCredentials(this.database, repo.id)
if (repoOverride !== null) return repoOverride
}

return this.settingsService.getSettings('default').preferences.sandbox?.gitCredentials === true
return this.getSandboxGitCredentialsAllowed(options)
}

getSandboxGitEnv(options: CredentialResolutionOptions = {}): Record<string, string> {
if (!this.isSandboxGitCredentialsAllowed(options)) return {}
const repo = this.resolveRepo(options)
const repoOverride = repo ? getRepoSandboxGitCredentials(this.database, repo.id) : null
if (repoOverride === false) return {}

const context = this.resolveContext(options, repo)
if (repoOverride !== true && context.preferences.sandbox?.gitCredentials !== true) return {}

const gitEnv = this.getGitEnv(options)
const gitEnv = this.getGitEnvForContext(context)
if (gitEnv.GIT_CONFIG_COUNT === '0') return {}

const { env, dropped } = limitForwardedGitConfigs(gitEnv)
Expand All @@ -84,7 +88,43 @@ export class CredentialProvider {
)
}

return { ...env, ...this.getGhCliEnv(options) }
return { ...env, ...this.getGhCliEnvForContext(context) }
}

getGhCliEnv(options: CredentialResolutionOptions = {}): Record<string, string> {
return this.getGhCliEnvForContext(this.resolveContext(options))
}

private resolveContext(options: CredentialResolutionOptions, repo = this.resolveRepo(options)): CredentialResolutionContext {
const preferences = this.getPreferences()
return {
preferences,
credentials: this.getCredentials(preferences),
repo,
}
}

private getPreferences(): UserPreferences {
return this.settingsService.getSettings('default').preferences
}

private getCredentials(preferences: UserPreferences): GitCredential[] {
return (preferences.gitCredentials || []) as GitCredential[]
}

private getGitEnvForContext(context: CredentialResolutionContext): Record<string, string> {
return createGitEnv(context.credentials, this.getSelectedCredential(context))
}

private getGhCliEnvForContext(context: CredentialResolutionContext): Record<string, string> {
const credential = this.getGhCliCredential(context)
return createGhCliEnv(credential ? [credential] : [])
}

private getSandboxGitCredentialsAllowed(options: CredentialResolutionOptions): boolean {
const repo = this.resolveRepo(options)
const repoOverride = repo ? getRepoSandboxGitCredentials(this.database, repo.id) : null
return repoOverride ?? (this.getPreferences().sandbox?.gitCredentials === true)
}

private resolveRepo(options: CredentialResolutionOptions): Repo | null {
Expand All @@ -94,34 +134,25 @@ export class CredentialProvider {
return options.cwd ? getRepoByDirectory(this.database, options.cwd) : null
}

getGhCliEnv(options: CredentialResolutionOptions = {}): Record<string, string> {
const credential = this.getGhCliCredential(options)
if (!credential?.token) return {}
return { GH_TOKEN: credential.token, GITHUB_TOKEN: credential.token }
}

private getGhCliCredential(options: CredentialResolutionOptions): GitCredential | null {
const credentials = this.getGitCredentials()
const selectedCredential = this.getSelectedCredential(options, credentials)
private getGhCliCredential(context: CredentialResolutionContext): GitCredential | null {
const selectedCredential = this.getSelectedCredential(context)
if (this.isGithubPatCredential(selectedCredential)) return selectedCredential

return findGitHubCredential(credentials)
return findGitHubCredential(context.credentials)
}

private getSelectedCredential(options: CredentialResolutionOptions, credentials: GitCredential[]): GitCredential | null {
const repoCredential = this.getRepoCredential(options, credentials)
private getSelectedCredential(context: CredentialResolutionContext): GitCredential | null {
const repoCredential = this.getRepoCredential(context)
if (repoCredential) return repoCredential

const settings = this.settingsService.getSettings('default')
return credentials.find((credential) => credential.id === settings.preferences.defaultGitCredentialId) ?? null
return context.credentials.find((credential) => credential.id === context.preferences.defaultGitCredentialId) ?? null
}

private getRepoCredential(options: CredentialResolutionOptions, credentials: GitCredential[]): GitCredential | null {
const repo = this.resolveRepo(options)
if (!repo) return null
private getRepoCredential(context: CredentialResolutionContext): GitCredential | null {
if (!context.repo) return null

const credentialId = getRepoGitCredentialId(this.database, repo.id)
return credentials.find((credential) => credential.id === credentialId) ?? null
const credentialId = getRepoGitCredentialId(this.database, context.repo.id)
return context.credentials.find((credential) => credential.id === credentialId) ?? null
}

private isGithubPatCredential(credential: GitCredential | null): credential is GitCredential {
Expand Down
6 changes: 6 additions & 0 deletions backend/src/services/opencode/process-identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ export function resolveProcessIdentityProvider(): ProcessIdentityProvider {
return cachedProvider
}

export function getProcessIdentityAttestationError(): string | null {
return resolveProcessIdentityProvider().attested
? null
: 'process identity attestation is unavailable on this platform (Linux /proc is required)'
}

export function resetProcessIdentityProvider(): void {
cachedProvider = null
}
Expand Down
11 changes: 9 additions & 2 deletions backend/src/services/sandbox/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { mkdirSafe } from '../../utils/fs-safe'
import { logger } from '../../utils/logger'
import { SettingsService } from '../settings'
import { CredentialProvider } from '../credential-provider'
import { getProcessIdentityAttestationError } from '../opencode/process-identity'
import { detectSandboxCapability } from './capability'
import {
WORKSPACE_SANDBOX_NAME,
Expand Down Expand Up @@ -624,10 +625,12 @@ export class SandboxRuntimeService {

getStatus(): SandboxStatus {
const capability = detectSandboxCapability()
const attestationError = capability.available ? getProcessIdentityAttestationError() : null
const reason = capability.reason ?? attestationError
return {
available: capability.available,
available: capability.available && attestationError === null,
enabled: this.isEnabled(),
...(capability.reason !== undefined ? { reason: capability.reason } : {}),
...(reason !== null && reason !== undefined ? { reason } : {}),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
...(capability.msbVersion !== undefined ? { msbVersion: capability.msbVersion } : {}),
}
}
Expand All @@ -640,6 +643,10 @@ export class SandboxRuntimeService {
if (!capability.available) {
return { mode: 'blocked', reason: capability.reason ?? 'Sandbox capability is unavailable' }
}
const attestationError = getProcessIdentityAttestationError()
if (attestationError !== null) {
return { mode: 'blocked', reason: attestationError }
}
const workDirectory = await resolveSandboxWorkDirectory(directory)
if (workDirectory === null) {
return {
Expand Down
Loading