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
111 lines
3.3 KiB
Go
111 lines
3.3 KiB
Go
package runtimeprep
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// exposeUserCodexAgentsFile seeds the run-scoped instruction file with the
|
|
// user's global Codex instructions. The session-owned file must be a copy:
|
|
// WriteManagedBlock adds Tutti's managed policy to it and must never mutate the
|
|
// user's real ~/.codex/AGENTS.md.
|
|
func exposeUserCodexAgentsFile(codexHome string, userCodexHome string) error {
|
|
source := filepath.Join(userCodexHome, "AGENTS.md")
|
|
sourceContent, sourceExists, err := readRegularCodexAgentsFile(source)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
target := filepath.Join(codexHome, "AGENTS.md")
|
|
targetInfo, err := os.Lstat(target)
|
|
if os.IsNotExist(err) {
|
|
if !sourceExists {
|
|
return nil
|
|
}
|
|
if err := writeRegularCodexAgentsFile(target, sourceContent); err != nil {
|
|
return fmt.Errorf("copy user codex AGENTS.md: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("inspect session codex AGENTS.md: %w", err)
|
|
}
|
|
|
|
// Always replace a pre-existing link with a private regular file. This
|
|
// prevents WriteManagedBlock from following a symlink or mutating a hardlink
|
|
// to the user's real ~/.codex/AGENTS.md on the next line of Prepare.
|
|
if targetInfo.Mode()&os.ModeSymlink != 0 {
|
|
if sourceExists {
|
|
if err := writeRegularCodexAgentsFile(target, sourceContent); err != nil {
|
|
return fmt.Errorf("replace session codex AGENTS.md link: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
if err := os.Remove(target); err != nil {
|
|
return fmt.Errorf("remove session codex AGENTS.md link: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
if !targetInfo.Mode().IsRegular() {
|
|
return fmt.Errorf("session codex AGENTS.md is not a regular file")
|
|
}
|
|
|
|
// Re-materialize an existing regular file too. A hardlink has regular-file
|
|
// mode, so an atomic replacement is what guarantees a new inode without
|
|
// discarding any session-local content already present in the file.
|
|
existing, err := os.ReadFile(target)
|
|
if err != nil {
|
|
return fmt.Errorf("read session codex AGENTS.md: %w", err)
|
|
}
|
|
if err := writeRegularCodexAgentsFile(target, existing); err != nil {
|
|
return fmt.Errorf("isolate session codex AGENTS.md: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readRegularCodexAgentsFile(path string) ([]byte, bool, error) {
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, false, nil
|
|
}
|
|
return nil, false, fmt.Errorf("inspect user codex AGENTS.md: %w", err)
|
|
}
|
|
if !info.Mode().IsRegular() {
|
|
// AGENTS.md is optional. Ignore directories, FIFOs, and other special
|
|
// files instead of blocking runtime preparation while reading them.
|
|
return nil, false, nil
|
|
}
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("read user codex AGENTS.md: %w", err)
|
|
}
|
|
return content, true, nil
|
|
}
|
|
|
|
func writeRegularCodexAgentsFile(path string, content []byte) error {
|
|
temporary, err := os.CreateTemp(filepath.Dir(path), ".codex-agents-*.tmp")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
temporaryPath := temporary.Name()
|
|
defer func() {
|
|
_ = temporary.Close()
|
|
_ = os.Remove(temporaryPath)
|
|
}()
|
|
if err := temporary.Chmod(0o600); err != nil {
|
|
return err
|
|
}
|
|
if _, err := temporary.Write(content); err != nil {
|
|
return err
|
|
}
|
|
if err := temporary.Sync(); err != nil {
|
|
return err
|
|
}
|
|
if err := temporary.Close(); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(temporaryPath, path)
|
|
}
|