Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

790 lines
27 KiB
Go

package runtimeprep
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
)
const (
codexProjectRootMarkersDisabledConfig = `project_root_markers = []`
)
type CodexPreparer struct {
AuthProjector AuthFileProjector
PersonalSkillRoot string
}
func (CodexPreparer) Provider() string {
return "codex"
}
func (p CodexPreparer) Prepare(ctx context.Context, input ProviderPrepareInput) (result ProviderPrepareResult, err error) {
codexHome := filepath.Join(input.RuntimeRoot, "codex-home")
providerStateHome, err := resolveCodexProviderStateHome(input.PrepareInput)
if err != nil {
return ProviderPrepareResult{}, err
}
logRuntimePrepareTrace("runtime_prepare.codex.entered", input.PrepareInput, nil)
extraSkillRoots, err := prepareCodexHome(
codexHome,
filepath.Join(input.RuntimeRoot, "codex-session-skills"),
p.PersonalSkillRoot,
providerStateHome,
input.PrepareInput,
)
if err != nil {
return ProviderPrepareResult{}, err
}
cleanup, err := projectCodexAuth(ctx, codexHome, providerStateHome, p.AuthProjector)
if err != nil {
return ProviderPrepareResult{}, err
}
defer func() {
if err != nil && cleanup != nil {
err = errors.Join(err, cleanup(ctx))
}
}()
if input.CodexSaverMode {
rolePath, err := installCodexLunaWorkerRole(codexHome)
if err != nil {
return ProviderPrepareResult{}, err
}
if err := ensureCodexSaverDefaultRole(filepath.Join(codexHome, "config.toml")); err != nil {
return ProviderPrepareResult{}, err
}
if input.Manifest != nil {
input.Manifest.RecordManagedFile(rolePath, "codex-agent-role", true)
}
}
logRuntimePrepareTrace("runtime_prepare.codex.home_prepared", input.PrepareInput, nil)
instructionsPath := filepath.Join(codexHome, "AGENTS.md")
logRuntimePrepareTrace("runtime_prepare.codex.instructions_write_requested", input.PrepareInput, nil)
policy, err := tuttiCLIPolicy(input.PrepareInput)
if err != nil {
return ProviderPrepareResult{}, err
}
if input.CodexSaverMode {
policy = strings.TrimSpace(policy) + "\n\n" + codexSaverModePolicy
}
writeResult, err := input.Store.WriteManagedBlock(instructionsPath, policy)
if err != nil {
return ProviderPrepareResult{}, err
}
if err := ensureRTKInstructionsReferenceFirst(instructionsPath, input.PrepareInput); err != nil {
return ProviderPrepareResult{}, err
}
logRuntimePrepareTrace("runtime_prepare.codex.instructions_write_resolved", input.PrepareInput, map[string]any{
"created": writeResult.Created,
})
if input.Manifest != nil {
input.Manifest.RecordManagedFile(instructionsPath, "provider-instructions", writeResult.Created)
input.Manifest.RecordManagedFile(codexHome, "codex-home", true)
}
logRuntimePrepareTrace("runtime_prepare.codex.resolved", input.PrepareInput, nil)
env := []string{
"CODEX_HOME=" + codexHome,
}
if len(extraSkillRoots) > 0 {
encodedRoots, err := json.Marshal(extraSkillRoots)
if err != nil {
return ProviderPrepareResult{}, fmt.Errorf("encode Codex extra skill roots: %w", err)
}
env = append(env, tuttiAgentExtraSkillRootsEnv+"="+string(encodedRoots))
}
if input.ModelEndpoint.supportsCodex() {
env = append(env, codexModelPlanAPIKeyEnv+"="+input.ModelEndpoint.APIKey)
}
return ProviderPrepareResult{
Cwd: input.Cwd,
Env: env,
Cleanup: cleanup,
}, nil
}
func projectCodexAuth(ctx context.Context, codexHome, providerStateHome string, projector AuthFileProjector) (func(context.Context) error, error) {
if projector == nil || strings.TrimSpace(providerStateHome) == "" {
return nil, nil
}
source := filepath.Join(providerStateHome, "auth.json")
if _, err := os.Stat(source); err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("stat codex auth source: %w", err)
}
target := filepath.Join(codexHome, "auth.json")
if info, err := os.Lstat(target); err == nil && info.Mode()&os.ModeSymlink != 0 {
current, readErr := os.Readlink(target)
if readErr != nil {
return nil, fmt.Errorf("read codex auth projection: %w", readErr)
}
if current == source {
return nil, nil
}
}
cleanup, err := projector.Project(ctx, AuthFileProjection{SourcePath: source, TargetPath: target})
if err != nil {
return nil, fmt.Errorf("project codex auth: %w", err)
}
return cleanup, nil
}
func prepareCodexHome(
codexHome string,
sessionSkillRoot string,
personalSkillRoot string,
providerStateHome string,
input PrepareInput,
) ([]string, error) {
var extraSkillRoots []string
logRuntimePrepareTrace("runtime_prepare.codex.home_dir_requested", input, nil)
if err := os.MkdirAll(codexHome, 0o700); err != nil {
return nil, fmt.Errorf("create codex home: %w", err)
}
logRuntimePrepareTrace("runtime_prepare.codex.home_dir_resolved", input, nil)
logRuntimePrepareTrace("runtime_prepare.codex.user_files_requested", input, nil)
if err := exposeUserCodexFiles(codexHome, providerStateHome); err != nil {
return nil, err
}
logRuntimePrepareTrace("runtime_prepare.codex.user_files_resolved", input, nil)
logRuntimePrepareTrace("runtime_prepare.codex.imported_rollout_requested", input, nil)
if err := exposeCodexImportedRolloutFile(codexHome, providerStateHome, input.ExternalRolloutSourcePath); err != nil {
return nil, err
}
logRuntimePrepareTrace("runtime_prepare.codex.imported_rollout_resolved", input, nil)
logRuntimePrepareTrace("runtime_prepare.codex.session_config_requested", input, nil)
if err := ensureCodexSessionConfig(filepath.Join(codexHome, "config.toml"), input); err != nil {
return nil, err
}
logRuntimePrepareTrace("runtime_prepare.codex.session_config_resolved", input, nil)
if !input.SkipSkills {
logRuntimePrepareTrace("runtime_prepare.codex.user_skills_requested", input, nil)
if strings.TrimSpace(personalSkillRoot) == "" {
if err := exposeUserCodexSkillFolders(filepath.Join(codexHome, "skills"), providerStateHome, input); err != nil {
return nil, err
}
} else {
if err := exposePersonalCodexSkillRoot(
filepath.Join(codexHome, "skills"),
personalSkillRoot,
sessionSkillRoot,
); err != nil {
return nil, err
}
providerSkillRoot, err := distinctCodexProviderSkillRoot(providerStateHome, personalSkillRoot)
if err != nil {
return nil, err
}
if providerSkillRoot != "" {
extraSkillRoots = append(extraSkillRoots, providerSkillRoot)
}
}
logRuntimePrepareTrace("runtime_prepare.codex.user_skills_resolved", input, nil)
logRuntimePrepareTrace("runtime_prepare.codex.native_skills_requested", input, nil)
nativeSkillRoot := filepath.Join(codexHome, "skills")
reservedRoots := []string(nil)
if strings.TrimSpace(personalSkillRoot) != "" {
nativeSkillRoot = sessionSkillRoot
reservedRoots = []string{filepath.Clean(personalSkillRoot)}
}
skillPaths, err := installProviderNativeSkillsSessionScopedWithReservedRoots(
nativeSkillRoot,
reservedRoots,
input,
)
if err != nil {
return nil, err
}
logRuntimePrepareTrace("runtime_prepare.codex.native_skills_resolved", input, map[string]any{
"skill_count": len(skillPaths),
})
if strings.TrimSpace(personalSkillRoot) != "" && len(skillPaths) > 0 {
extraSkillRoots = append(extraSkillRoots, nativeSkillRoot)
}
}
logRuntimePrepareTrace("runtime_prepare.codex.approval_rules_requested", input, nil)
if err := installCodexApprovalRules(codexHome, input); err != nil {
return nil, err
}
logRuntimePrepareTrace("runtime_prepare.codex.approval_rules_resolved", input, nil)
return extraSkillRoots, nil
}
func installCodexApprovalRules(codexHome string, input PrepareInput) error {
rulesDir := filepath.Join(codexHome, "rules")
if err := os.MkdirAll(rulesDir, 0o700); err != nil {
return fmt.Errorf("create codex rules directory: %w", err)
}
content := codexApprovalRules(input)
if err := os.WriteFile(filepath.Join(rulesDir, "default.rules"), []byte(content), 0o644); err != nil {
return fmt.Errorf("write codex approval rules: %w", err)
}
return nil
}
func codexApprovalRules(input PrepareInput) string {
command := normalizeCLICommandName(input.CLICommand)
if input.CommandCapabilityProjection == nil {
return codexApprovalRule([]string{command})
}
if input.commandCapabilities == nil {
return ""
}
var rules strings.Builder
seen := make(map[string]struct{}, len(input.commandCapabilities.commands))
for _, capability := range input.commandCapabilities.commands {
pattern := append([]string{command}, normalizedCommandPath(capability.Path)...)
key := strings.Join(pattern, "\x00")
if _, exists := seen[key]; exists {
continue
}
seen[key] = struct{}{}
rules.WriteString(codexApprovalRule(pattern))
}
return rules.String()
}
func codexApprovalRule(pattern []string) string {
quoted := make([]string, 0, len(pattern))
for _, segment := range pattern {
if segment = strings.TrimSpace(segment); segment != "" {
quoted = append(quoted, strconv.Quote(segment))
}
}
if len(quoted) == 0 {
return ""
}
return "prefix_rule(pattern=[" + strings.Join(quoted, ", ") +
"], decision=\"allow\")\n"
}
func exposeUserCodexFiles(codexHome, userCodexHome string) error {
if strings.TrimSpace(userCodexHome) == "" {
return nil
}
for _, name := range []string{"auth.json"} {
source := filepath.Join(userCodexHome, name)
if _, err := os.Stat(source); err != nil {
continue
}
target := filepath.Join(codexHome, name)
if _, err := os.Lstat(target); err == nil {
continue
}
if err := exposeCodexFile(source, target, 0o600); err != nil {
if copyErr := copyFile(source, target, 0o600); copyErr != nil {
return fmt.Errorf("expose codex %s: link failed: %v; copy failed: %w", name, err, copyErr)
}
}
}
if err := exposeUserCodexModelsCache(codexHome, userCodexHome); err != nil {
return err
}
if err := exposeUserCodexPluginState(codexHome, userCodexHome); err != nil {
return err
}
if err := exposeUserCodexConfig(codexHome, userCodexHome); err != nil {
return err
}
if err := exposeUserCodexAgentsFile(codexHome, userCodexHome); err != nil {
return err
}
if err := exposeUserCodexModelCatalog(codexHome, userCodexHome); err != nil {
return err
}
return exposeUserCodexInstructionsFile(codexHome, userCodexHome)
}
// exposeCodexImportedRolloutFile symlinks the single Codex CLI rollout
// (conversation transcript) file that an imported session was read from into
// the sandboxed CODEX_HOME, at the same path it has relative to the stable
// provider state home (e.g. `sessions/2026/07/04/rollout-...jsonl` or
// `archived_sessions/...`). Codex CLI resolves rollouts for `thread/resume`
// relative to CODEX_HOME, so mirroring the real relative layout lets it find
// the transcript by thread id without needing this code to know or guess
// Codex's internal sharding/naming scheme, and without exposing any other
// unrelated conversation under the provider sessions directory into a
// sandbox scoped to this one session/run.
//
// sourcePath is empty for every non-imported session, so this is a no-op for
// the overwhelming majority of sessions. When it is set but the file can't be
// resolved or no longer exists (moved, pruned by the user's own Codex CLI
// retention, or a custom CODEX_HOME was in effect on another device at import
// time), this intentionally returns nil rather than an error: resume still
// falls back to the existing documented "recreatable" path (a fresh thread
// with a visible notice) exactly as it did before this file existed.
func exposeCodexImportedRolloutFile(codexHome, userCodexHome, sourcePath string) error {
sourcePath = strings.TrimSpace(sourcePath)
if sourcePath == "" || strings.TrimSpace(userCodexHome) == "" {
return nil
}
rel, err := filepath.Rel(userCodexHome, sourcePath)
if err != nil || rel == ".." || rel == "." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
// Not under the stable provider state tree we know how to mirror - leave it to
// the recreate fallback rather than guessing at a different layout.
return nil
}
if info, err := os.Stat(sourcePath); err != nil || info.IsDir() {
// Original rollout is gone or was never a real file - fall back to
// recreate, same as before this fix.
return nil
}
target := filepath.Join(codexHome, rel)
if _, err := os.Lstat(target); err == nil {
return nil
}
if err := os.MkdirAll(filepath.Dir(target), 0o700); err != nil {
return fmt.Errorf("create codex imported rollout parent dir: %w", err)
}
if err := exposeCodexFile(sourcePath, target, 0o600); err != nil {
return fmt.Errorf("expose codex imported rollout file: %w", err)
}
return nil
}
func exposeUserCodexPluginState(codexHome string, userCodexHome string) error {
for _, rel := range []string{
filepath.Join("plugins", "cache"),
filepath.Join("plugins", "data"),
filepath.Join("plugins", ".plugin-appserver"),
} {
source := filepath.Join(userCodexHome, rel)
if _, err := os.Stat(source); err != nil {
continue
}
target := filepath.Join(codexHome, rel)
if _, err := os.Lstat(target); err == nil {
continue
}
if err := os.MkdirAll(filepath.Dir(target), 0o700); err != nil {
return fmt.Errorf("create codex plugin state parent: %w", err)
}
if err := exposeCodexDirectory(source, target); err != nil {
return fmt.Errorf("expose codex plugin state %s: %w", rel, err)
}
}
return nil
}
func exposeUserCodexConfig(codexHome string, userCodexHome string) error {
target := filepath.Join(codexHome, "config.toml")
if targetInfo, err := os.Lstat(target); err == nil {
if targetInfo.Mode()&os.ModeSymlink != 0 {
if err := os.Remove(target); err != nil {
return fmt.Errorf("replace codex config symlink: %w", err)
}
}
} else if !os.IsNotExist(err) {
return fmt.Errorf("inspect codex config: %w", err)
}
source := filepath.Join(userCodexHome, "config.toml")
if _, err := os.Stat(source); os.IsNotExist(err) {
if removeErr := os.Remove(target); removeErr != nil && !os.IsNotExist(removeErr) {
return fmt.Errorf("remove stale codex config: %w", removeErr)
}
return nil
} else if err != nil {
return fmt.Errorf("inspect user codex config: %w", err)
}
if err := copyFile(source, target, 0o600); err != nil {
return fmt.Errorf("copy codex config: %w", err)
}
return nil
}
func ensureCodexSessionConfig(configPath string, input PrepareInput) error {
contentBytes, err := os.ReadFile(configPath)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("read codex config: %w", err)
}
next, changed := codexConfigWithProjectRootMarkersDisabled(string(contentBytes))
if serviceTierNext, serviceTierChanged := codexConfigWithSupportedServiceTier(next); serviceTierChanged {
next = serviceTierNext
changed = true
}
if tuttiNext, tuttiChanged := codexConfigWithTuttiConversationDetailMode(next, input.ConversationDetailMode); tuttiChanged {
next = tuttiNext
changed = true
}
if detailModeNext, detailModeChanged := codexConfigWithConversationDetailModeInstructions(next, input.ConversationDetailMode); detailModeChanged {
next = detailModeNext
changed = true
}
if planNext, planChanged := codexConfigWithModelPlanEndpoint(next, input.ModelEndpoint); planChanged {
next = planNext
changed = true
}
if mcpNext, mcpChanged := codexConfigWithConnectorMCP(next, input.MCPServers); mcpChanged {
next = mcpNext
changed = true
}
// Tutti launches the Codex app-server from the non-elevated desktop daemon.
// On Windows, the elevated sandbox implementation invokes a separate setup
// helper through ShellExecuteExW, which requires an interactive UAC consent
// flow that is not owned by the app-server protocol. Use the restricted,
// non-elevated implementation for Tutti-owned session homes so a hidden or
// canceled UAC prompt cannot prevent every command from starting. The
// Codex/Tutti permission mode and approval policy remain unchanged.
if windowsSandboxNext, windowsSandboxChanged := codexConfigWithTuttiWindowsSandbox(next); windowsSandboxChanged {
next = windowsSandboxNext
changed = true
}
if !changed {
return nil
}
if err := os.WriteFile(configPath, []byte(next), 0o600); err != nil {
return fmt.Errorf("write codex config: %w", err)
}
return nil
}
// codexConfigWithTuttiWindowsSandbox pins Tutti-owned Codex session homes to
// the unelevated Windows sandbox implementation. This is intentionally applied
// only on Windows and only to the copied per-session config, never to the
// provider's stable global config.toml.
func codexConfigWithTuttiWindowsSandbox(content string) (string, bool) {
if runtime.GOOS != "windows" {
return content, false
}
normalized := strings.ReplaceAll(content, "\r\n", "\n")
lines := strings.Split(normalized, "\n")
for sectionStart, line := range lines {
if strings.TrimSpace(line) != "[windows]" {
continue
}
sectionEnd := len(lines)
for index := sectionStart + 1; index < len(lines); index++ {
trimmed := strings.TrimSpace(lines[index])
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
sectionEnd = index
break
}
}
for index := sectionStart + 1; index < sectionEnd; index++ {
trimmed := strings.TrimSpace(lines[index])
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
if !codexConfigLineHasKey(trimmed, "sandbox") {
continue
}
if strings.TrimSpace(lines[index]) == `sandbox = "unelevated"` {
return content, false
}
next := append([]string{}, lines...)
next[index] = `sandbox = "unelevated"`
return strings.Join(next, "\n"), true
}
next := make([]string, 0, len(lines)+1)
next = append(next, lines[:sectionEnd]...)
next = append(next, `sandbox = "unelevated"`)
next = append(next, lines[sectionEnd:]...)
return strings.Join(next, "\n"), true
}
next := strings.TrimRight(normalized, "\n")
if next != "" {
next += "\n\n"
}
next += "[windows]\nsandbox = \"unelevated\"\n"
return next, true
}
func codexConfigWithTuttiConversationDetailMode(content string, conversationDetailMode string) (string, bool) {
mode := normalizeAgentConversationDetailMode(conversationDetailMode)
line := `conversationDetailMode = ` + strconv.Quote(mode)
lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")
for sectionStart, existingLine := range lines {
if strings.TrimSpace(existingLine) != "[tutti]" {
continue
}
sectionEnd := len(lines)
for index := sectionStart + 1; index < len(lines); index++ {
trimmed := strings.TrimSpace(lines[index])
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
sectionEnd = index
break
}
}
for index := sectionStart + 1; index < sectionEnd; index++ {
trimmed := strings.TrimSpace(lines[index])
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
if !codexConfigLineHasKey(trimmed, "conversationDetailMode") {
continue
}
if strings.TrimSpace(lines[index]) == line {
return content, false
}
nextLines := append([]string{}, lines...)
nextLines[index] = line
return strings.Join(nextLines, "\n"), true
}
nextLines := make([]string, 0, len(lines)+1)
nextLines = append(nextLines, lines[:sectionEnd]...)
nextLines = append(nextLines, line)
nextLines = append(nextLines, lines[sectionEnd:]...)
return strings.Join(nextLines, "\n"), true
}
block := "[tutti]\n" + line + "\n"
if strings.TrimSpace(content) == "" {
return block, true
}
return strings.TrimRight(content, "\r\n") + "\n\n" + block, true
}
func codexConfigWithConversationDetailModeInstructions(content string, conversationDetailMode string) (string, bool) {
instructions := agentConversationDetailModeInstructions(conversationDetailMode)
if strings.TrimSpace(instructions) == "" {
return codexConfigWithoutConversationDetailModeInstructions(content)
}
if strings.Contains(content, instructions) {
return content, false
}
line := `developer_instructions = ` + strconv.Quote(instructions)
lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")
for index, existingLine := range lines {
trimmed := strings.TrimSpace(existingLine)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
if strings.HasPrefix(trimmed, "[") {
break
}
if !codexConfigLineHasKey(trimmed, "developer_instructions") {
continue
}
value, endIndex, ok := codexConfigStringAssignmentValueAt(lines, index, "developer_instructions")
if ok && strings.TrimSpace(value) != "" {
line = `developer_instructions = ` + strconv.Quote(strings.TrimRight(value, "\n")+"\n\n"+instructions)
}
nextLines := make([]string, 0, len(lines)-(endIndex-index))
nextLines = append(nextLines, lines[:index]...)
nextLines = append(nextLines, line)
nextLines = append(nextLines, lines[endIndex+1:]...)
return strings.Join(nextLines, "\n"), true
}
if strings.TrimSpace(content) == "" {
return line + "\n", true
}
return line + "\n\n" + strings.TrimLeft(content, "\r\n"), true
}
func codexConfigWithoutConversationDetailModeInstructions(content string) (string, bool) {
lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")
for index, existingLine := range lines {
trimmed := strings.TrimSpace(existingLine)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
if strings.HasPrefix(trimmed, "[") {
break
}
if !codexConfigLineHasKey(trimmed, "developer_instructions") {
continue
}
value, endIndex, ok := codexConfigStringAssignmentValueAt(lines, index, "developer_instructions")
if !ok {
return content, false
}
nextValue, removed := codexDeveloperInstructionsWithoutConversationDetailMode(value)
if !removed {
return content, false
}
nextLines := make([]string, 0, len(lines)-(endIndex-index))
nextLines = append(nextLines, lines[:index]...)
if strings.TrimSpace(nextValue) != "" {
nextLines = append(nextLines, `developer_instructions = `+strconv.Quote(nextValue))
}
nextLines = append(nextLines, lines[endIndex+1:]...)
return strings.Join(nextLines, "\n"), true
}
return content, false
}
func codexDeveloperInstructionsWithoutConversationDetailMode(value string) (string, bool) {
instructions := nonTechnicalUIConversationDetailModeInstructions
if !strings.Contains(value, instructions) {
return value, false
}
next := strings.ReplaceAll(value, instructions, "")
for strings.Contains(next, "\n\n\n") {
next = strings.ReplaceAll(next, "\n\n\n", "\n\n")
}
return strings.TrimSpace(next), true
}
func codexConfigWithProjectRootMarkersDisabled(content string) (string, bool) {
lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")
for index, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
if strings.HasPrefix(trimmed, "[") {
break
}
if codexConfigLineHasKey(trimmed, "project_root_markers") {
endIndex := codexConfigAssignmentEndLine(lines, index)
if endIndex == index && trimmed == codexProjectRootMarkersDisabledConfig {
return content, false
}
nextLines := make([]string, 0, len(lines)-(endIndex-index))
nextLines = append(nextLines, lines[:index]...)
nextLines = append(nextLines, codexProjectRootMarkersDisabledConfig)
nextLines = append(nextLines, lines[endIndex+1:]...)
return strings.Join(nextLines, "\n"), true
}
}
next := codexProjectRootMarkersDisabledConfig + "\n"
if strings.TrimSpace(content) != "" {
next += "\n" + strings.TrimLeft(content, "\r\n")
}
return next, true
}
func codexConfigWithSupportedServiceTier(content string) (string, bool) {
lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")
for index, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
if strings.HasPrefix(trimmed, "[") {
break
}
if !codexConfigLineHasKey(trimmed, "service_tier") {
continue
}
value, ok := codexConfigStringAssignmentValue(trimmed, "service_tier")
if !ok {
return content, false
}
switch strings.ToLower(strings.TrimSpace(value)) {
case "fast", "flex":
return content, false
case "priority":
nextLines := append([]string{}, lines...)
nextLines[index] = `service_tier = "fast"`
return strings.Join(nextLines, "\n"), true
case "", "default", "standard":
endIndex := codexConfigAssignmentEndLine(lines, index)
nextLines := make([]string, 0, len(lines)-(endIndex-index+1))
nextLines = append(nextLines, lines[:index]...)
nextLines = append(nextLines, lines[endIndex+1:]...)
return strings.Join(nextLines, "\n"), true
default:
return content, false
}
}
return content, false
}
func codexConfigLineHasKey(line string, key string) bool {
if !strings.HasPrefix(line, key) {
return false
}
return strings.HasPrefix(strings.TrimSpace(strings.TrimPrefix(line, key)), "=")
}
func codexConfigStringAssignmentValue(line string, key string) (string, bool) {
if !codexConfigLineHasKey(line, key) {
return "", false
}
_, rawValue, ok := strings.Cut(line, "=")
if !ok {
return "", false
}
rawValue = strings.TrimSpace(rawValue)
if rawValue == "" {
return "", true
}
quote := rawValue[0]
if quote != '"' && quote != '\'' {
return "", false
}
var builder strings.Builder
escaped := false
for index := 1; index < len(rawValue); index++ {
char := rawValue[index]
if quote == '"' && escaped {
switch char {
case 'n':
builder.WriteByte('\n')
case 'r':
builder.WriteByte('\r')
case 't':
builder.WriteByte('\t')
default:
builder.WriteByte(char)
}
escaped = false
continue
}
if quote == '"' && char == '\\' {
escaped = true
continue
}
if char == quote {
return builder.String(), true
}
builder.WriteByte(char)
}
return "", false
}
func codexConfigStringAssignmentValueAt(lines []string, index int, key string) (string, int, bool) {
if index < 0 || index >= len(lines) {
return "", index, false
}
line := strings.TrimSpace(lines[index])
if value, ok := codexConfigStringAssignmentValue(line, key); ok {
return value, index, true
}
if !codexConfigLineHasKey(line, key) {
return "", index, false
}
_, rawValue, ok := strings.Cut(line, "=")
if !ok {
return "", index, false
}
rawValue = strings.TrimSpace(rawValue)
if !strings.HasPrefix(rawValue, `"""`) && !strings.HasPrefix(rawValue, `'''`) {
return "", codexConfigAssignmentEndLine(lines, index), false
}
delimiter := rawValue[:3]
rest := strings.TrimPrefix(rawValue, delimiter)
if endOffset := strings.Index(rest, delimiter); endOffset >= 0 {
return rest[:endOffset], index, true
}
var builder strings.Builder
builder.WriteString(rest)
for lineIndex := index + 1; lineIndex < len(lines); lineIndex++ {
builder.WriteByte('\n')
lineValue := lines[lineIndex]
if endOffset := strings.Index(lineValue, delimiter); endOffset >= 0 {
builder.WriteString(lineValue[:endOffset])
return builder.String(), lineIndex, true
}
builder.WriteString(lineValue)
}
return "", index, false
}
func copyFile(source string, target string, mode os.FileMode) error {
content, err := os.ReadFile(source)
if err != nil {
return err
}
return os.WriteFile(target, content, mode)
}