chore(repo): reinitialize repository
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package runtimecodex
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"inbox/internal/app/runtimeconfig"
|
||||
"inbox/internal/domain/role"
|
||||
"inbox/internal/domain/skill"
|
||||
)
|
||||
|
||||
type fakeRoleCatalog struct {
|
||||
roles []role.Definition
|
||||
}
|
||||
|
||||
func (f fakeRoleCatalog) ListRoles(_ context.Context) ([]role.Definition, error) {
|
||||
return append([]role.Definition(nil), f.roles...), nil
|
||||
}
|
||||
|
||||
type fakeRoleResolver struct {
|
||||
resolved map[string]runtimeconfig.ResolvedRole
|
||||
}
|
||||
|
||||
func (f fakeRoleResolver) ResolveRole(_ context.Context, _ string, roleName string) (runtimeconfig.ResolvedRole, error) {
|
||||
return f.resolved[roleName], nil
|
||||
}
|
||||
|
||||
func TestMaterializerSyncWritesRoleConfigs(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
root := t.TempDir()
|
||||
|
||||
materializer := NewMaterializer(
|
||||
fakeRoleCatalog{
|
||||
roles: []role.Definition{
|
||||
{Name: "worker", Title: "Worker", IsEnabled: true},
|
||||
{Name: "disabled_role", Title: "Disabled", IsEnabled: false},
|
||||
},
|
||||
},
|
||||
fakeRoleResolver{
|
||||
resolved: map[string]runtimeconfig.ResolvedRole{
|
||||
"worker": {
|
||||
Role: role.Definition{Name: "worker", Title: "Worker", IsEnabled: true},
|
||||
Config: role.Config{
|
||||
RoleName: "worker",
|
||||
ConfigTOML: strings.Join([]string{
|
||||
`model = "gpt-5.2"`,
|
||||
`model_provider = "custom"`,
|
||||
``,
|
||||
`[shell_environment_policy]`,
|
||||
`inherit = "core"`,
|
||||
`exclude = [ ]`,
|
||||
`include_only = [ ]`,
|
||||
`experimental_use_profile = false`,
|
||||
``,
|
||||
`[shell_environment_policy.set]`,
|
||||
`XDG_CACHE_HOME = "/tmp/codex-xdg-cache"`,
|
||||
``,
|
||||
`[model_providers.custom]`,
|
||||
`base_url = "http://example.test/v1"`,
|
||||
`wire_api = "responses"`,
|
||||
``,
|
||||
`[mcp_servers.playwright]`,
|
||||
`enabled = false`,
|
||||
`command = "npx"`,
|
||||
`args = [ "-y", "@playwright/mcp@latest" ]`,
|
||||
`startup_timeout_sec = 90`,
|
||||
``,
|
||||
`[projects."/workspace"]`,
|
||||
`trust_level = "trusted"`,
|
||||
}, "\n"),
|
||||
AuthJSON: `{"OPENAI_API_KEY":"token-1"}`,
|
||||
},
|
||||
Skills: []runtimeconfig.ResolvedSkill{
|
||||
{
|
||||
Skill: skill.Definition{
|
||||
ID: "builtin-skill-inbox",
|
||||
SkillKey: "inbox",
|
||||
ContentMarkdown: "# Inbox\n\nUse inbox.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if err := materializer.Sync(ctx, "ws_1", root); err != nil {
|
||||
t.Fatalf("Sync() error = %v", err)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(root, filepath.FromSlash(WorkspaceDir), "worker", ".codex", configFilename)
|
||||
authPath := filepath.Join(root, filepath.FromSlash(WorkspaceDir), "worker", ".codex", authFilename)
|
||||
configBody, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(config) error = %v", err)
|
||||
}
|
||||
authBody, err := os.ReadFile(authPath)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(auth) error = %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(root, filepath.FromSlash(WorkspaceDir), "disabled_role")); !os.IsNotExist(err) {
|
||||
t.Fatalf("disabled role should not have runtime codex home")
|
||||
}
|
||||
|
||||
configText := string(configBody)
|
||||
for _, needle := range []string{
|
||||
`model = "gpt-5.2"`,
|
||||
`model_provider = "custom"`,
|
||||
`[model_providers.custom]`,
|
||||
`base_url = "http://example.test/v1"`,
|
||||
`[mcp_servers.playwright]`,
|
||||
`[shell_environment_policy.set]`,
|
||||
`[projects."/workspace"]`,
|
||||
} {
|
||||
if !strings.Contains(configText, needle) {
|
||||
t.Fatalf("expected config to contain %q, got:\n%s", needle, configText)
|
||||
}
|
||||
}
|
||||
|
||||
var auth map[string]string
|
||||
if err := json.Unmarshal(authBody, &auth); err != nil {
|
||||
t.Fatalf("json.Unmarshal(auth) error = %v", err)
|
||||
}
|
||||
if auth["OPENAI_API_KEY"] != "token-1" {
|
||||
t.Fatalf("expected auth OPENAI_API_KEY to be written, got %#v", auth)
|
||||
}
|
||||
skillBody, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(WorkspaceDir), "worker", ".codex", "skills", "inbox", "SKILL.md"))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(skill) error = %v", err)
|
||||
}
|
||||
if string(skillBody) != "# Inbox\n\nUse inbox." {
|
||||
t.Fatalf("unexpected skill body %q", string(skillBody))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteResolvedRoleHomeWritesOnlyResolvedConfig(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
roleHome, err := WriteResolvedRoleHome(root, runtimeconfig.ResolvedRole{
|
||||
WorkspaceID: "ws_1",
|
||||
Role: role.Definition{Name: "leader", Title: "Leader", IsEnabled: true},
|
||||
Config: role.Config{
|
||||
RoleName: "leader",
|
||||
ConfigTOML: "model = \"gpt-5.4\"",
|
||||
AuthJSON: `{"OPENAI_API_KEY":"token-2"}`,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("WriteResolvedRoleHome() error = %v", err)
|
||||
}
|
||||
authBody, err := os.ReadFile(filepath.Join(roleHome, ".codex", authFilename))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(auth) error = %v", err)
|
||||
}
|
||||
var auth map[string]string
|
||||
if err := json.Unmarshal(authBody, &auth); err != nil {
|
||||
t.Fatalf("json.Unmarshal(auth) error = %v", err)
|
||||
}
|
||||
if auth["OPENAI_API_KEY"] != "token-2" {
|
||||
t.Fatalf("expected auth OPENAI_API_KEY to be written, got %#v", auth)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user