package runtimecodex import ( "context" "encoding/json" "fmt" "os" "path" "path/filepath" "strings" "inbox/internal/app/runtimeconfig" "inbox/internal/domain/role" ) const ( WorkspaceDir = ".inbox/runtime-codex" ContainerDir = "/workspace/.inbox/runtime-codex" ContainerHome = "/root" ContainerCodex = "/root/.codex" configFilename = "config.toml" authFilename = "auth.json" ) type RoleCatalog interface { ListRoles(ctx context.Context) ([]role.Definition, error) } type RoleResolver interface { ResolveRole(ctx context.Context, workspaceID, roleName string) (runtimeconfig.ResolvedRole, error) } type Materializer struct { roles RoleCatalog resolver RoleResolver } func NewMaterializer(roles RoleCatalog, resolver RoleResolver) *Materializer { return &Materializer{ roles: roles, resolver: resolver, } } func (m *Materializer) Sync(ctx context.Context, workspaceID, workspaceRoot string) error { if m == nil { return nil } if strings.TrimSpace(workspaceRoot) == "" { return fmt.Errorf("workspace root is required") } items, err := m.roles.ListRoles(ctx) if err != nil { return fmt.Errorf("list roles for runtime codex config: %w", err) } baseDir := filepath.Join(workspaceRoot, ".inbox") liveDir := filepath.Join(workspaceRoot, filepath.FromSlash(WorkspaceDir)) if err := os.MkdirAll(baseDir, 0755); err != nil { return fmt.Errorf("ensure runtime codex base dir: %w", err) } tmpDir, err := os.MkdirTemp(baseDir, "runtime-codex-") if err != nil { return fmt.Errorf("create runtime codex temp dir: %w", err) } cleanup := func() { _ = os.RemoveAll(tmpDir) } for _, item := range items { item = role.NormalizeDefinition(item) if !item.IsEnabled || item.ExecutorKind != role.ExecutorKindCodex { continue } resolved, err := m.resolver.ResolveRole(ctx, workspaceID, item.Name) if err != nil { cleanup() return fmt.Errorf("resolve role %q for runtime codex config: %w", item.Name, err) } if _, err := WriteResolvedRoleHome(tmpDir, resolved); err != nil { cleanup() return fmt.Errorf("write runtime codex home for %q: %w", item.Name, err) } if err := writeResolvedRoleSkills(tmpDir, resolved); err != nil { cleanup() return fmt.Errorf("write runtime codex skills for %q: %w", item.Name, err) } } _ = os.RemoveAll(liveDir) if err := os.Rename(tmpDir, liveDir); err != nil { cleanup() return fmt.Errorf("publish runtime codex dir: %w", err) } return nil } func ContainerHomeDir(roleName string) string { return path.Join(ContainerDir, sanitizeRolePath(roleName)) } func ContainerUserHomeDir() string { return ContainerHome } func ContainerCodexDir() string { return ContainerCodex } func WorkspaceHomeDir(workspaceRoot, roleName string) string { return filepath.Join(workspaceRoot, filepath.FromSlash(WorkspaceDir), sanitizeRolePath(roleName)) } func WorkspaceCodexDir(workspaceRoot, roleName string) string { return filepath.Join(WorkspaceHomeDir(workspaceRoot, roleName), ".codex") } func WriteResolvedRoleHome(root string, resolved runtimeconfig.ResolvedRole) (string, error) { roleHome := filepath.Join(root, sanitizeRolePath(resolved.Role.Name)) codexDir := filepath.Join(roleHome, ".codex") if err := os.MkdirAll(codexDir, 0755); err != nil { return "", fmt.Errorf("create runtime codex dir for %q: %w", resolved.Role.Name, err) } configText := normalizeConfigTOML(resolved.Config.ConfigTOML) if err := os.WriteFile(filepath.Join(codexDir, configFilename), []byte(configText), 0644); err != nil { return "", fmt.Errorf("write config.toml for %q: %w", resolved.Role.Name, err) } authBytes, err := normalizeAuthJSON(resolved.Config.AuthJSON) if err != nil { return "", fmt.Errorf("normalize auth.json for %q: %w", resolved.Role.Name, err) } if err := os.WriteFile(filepath.Join(codexDir, authFilename), authBytes, 0600); err != nil { return "", fmt.Errorf("write auth.json for %q: %w", resolved.Role.Name, err) } return roleHome, nil } func HostContainerRuntimeRoot(projectRoot, workspaceID string) string { return filepath.Join(projectRoot, ".runtime", "container-codex", sanitizeRolePath(workspaceID)) } func HostContainerCodexDir(projectRoot, workspaceID, roleName string) string { return WorkspaceCodexDir(HostContainerRuntimeRoot(projectRoot, workspaceID), roleName) } func HostLeaderRuntimeRoot(projectRoot, workspaceID string) string { return filepath.Join(projectRoot, ".runtime", "leader-codex", sanitizeRolePath(workspaceID)) } func HostLeaderHomeDir(projectRoot, workspaceID, roleName string) string { return filepath.Join(HostLeaderRuntimeRoot(projectRoot, workspaceID), sanitizeRolePath(roleName)) } func HostLeaderCodexDir(projectRoot, workspaceID, roleName string) string { return filepath.Join(HostLeaderHomeDir(projectRoot, workspaceID, roleName), ".codex") } func sanitizeRolePath(value string) string { value = strings.TrimSpace(value) if value == "" { return "role" } replacer := strings.NewReplacer( "/", "-", "\\", "-", ":", "-", "*", "-", "?", "-", "\"", "-", "<", "-", ">", "-", "|", "-", " ", "-", ) value = replacer.Replace(value) value = strings.Trim(value, ".-") if value == "" { return "role" } return value } func normalizeConfigTOML(value string) string { return strings.TrimSpace(value) } func normalizeAuthJSON(value string) ([]byte, error) { value = strings.TrimSpace(value) if value == "" { value = "{}" } if !json.Valid([]byte(value)) { return nil, fmt.Errorf("invalid json") } var decoded any if err := json.Unmarshal([]byte(value), &decoded); err != nil { return nil, err } return json.MarshalIndent(decoded, "", " ") } func writeResolvedRoleSkills(root string, resolved runtimeconfig.ResolvedRole) error { if len(resolved.Skills) == 0 { return nil } codexDir := filepath.Join(root, sanitizeRolePath(resolved.Role.Name), ".codex") skillsRoot := filepath.Join(codexDir, "skills") for _, item := range resolved.Skills { skillKey := strings.TrimSpace(item.Skill.SkillKey) if skillKey == "" { skillKey = sanitizeRolePath(item.Skill.ID) } if skillKey == "" { return fmt.Errorf("resolved skill key is required") } targetDir := filepath.Join(skillsRoot, sanitizeRolePath(skillKey)) if err := os.MkdirAll(targetDir, 0755); err != nil { return fmt.Errorf("create skill dir %s: %w", targetDir, err) } skillBody := strings.TrimSpace(item.Skill.ContentMarkdown) if skillBody == "" { return fmt.Errorf("skill %q content_markdown is required", skillKey) } if err := os.WriteFile(filepath.Join(targetDir, "SKILL.md"), []byte(skillBody), 0644); err != nil { return fmt.Errorf("write SKILL.md for %q: %w", skillKey, err) } } return nil }