diff --git a/pkg/workflow/cloud_hypervisor_test.go b/pkg/workflow/cloud_hypervisor_test.go index 73e9b7eecd1..9e867cb94c7 100644 --- a/pkg/workflow/cloud_hypervisor_test.go +++ b/pkg/workflow/cloud_hypervisor_test.go @@ -205,8 +205,11 @@ func TestCloudHypervisorAWFConfigJSON(t *testing.T) { NetworkPermissions: &NetworkPermissions{ Firewall: &FirewallConfig{Enabled: true}, }, - Tools: map[string]any{"github": map[string]any{"mode": "gh-proxy"}}, - SandboxConfig: &SandboxConfig{Agent: &AgentSandboxConfig{ID: "awf", Runtime: AgentRuntimeCloudHypervisor}}, + Tools: map[string]any{"github": map[string]any{"mode": "gh-proxy"}}, + SandboxConfig: applySandboxDefaults( + &SandboxConfig{Agent: &AgentSandboxConfig{ID: "awf", Runtime: AgentRuntimeCloudHypervisor}}, + &EngineConfig{ID: "copilot"}, + ), }, } @@ -218,6 +221,7 @@ func TestCloudHypervisorAWFConfigJSON(t *testing.T) { assert.Contains(t, jsonStr, `"topologyAttach":["awmg-mcpg"]`) assert.NotContains(t, jsonStr, "awmg-cli-proxy") assert.Contains(t, jsonStr, `"agentTimeout":60`) + assert.Contains(t, jsonStr, `"allowWrite":["/tmp/gh-aw/agent","/tmp/gh-aw/sandbox/agent/logs","/workspace","/workspace/.awf-home"]`) } func TestCloudHypervisorValidationArcDindIncompatible(t *testing.T) { diff --git a/pkg/workflow/sandbox.go b/pkg/workflow/sandbox.go index 337281972c2..1b5cc828631 100644 --- a/pkg/workflow/sandbox.go +++ b/pkg/workflow/sandbox.go @@ -16,6 +16,7 @@ package workflow import ( "slices" + "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/sliceutil" ) @@ -30,7 +31,10 @@ const ( SandboxTypeDefault SandboxType = "default" // Alias for AWF (backward compat) ) -const defaultAgentWorkspaceWritePath = "/tmp/gh-aw/agent" +const ( + defaultAgentWorkspaceWritePath = "/tmp/gh-aw/agent" + defaultAgentLogsWritePath = "/tmp/gh-aw/sandbox/agent/logs" +) // SandboxConfig represents the top-level sandbox configuration from front matter // New format: { agent: "awf"|"srt"|{type, config}, mcp: {port, command, ...} } @@ -246,7 +250,7 @@ func applySandboxDefaults(sandboxConfig *SandboxConfig, engineConfig *EngineConf Type: SandboxTypeAWF, }, } - ensureDefaultAgentWritePath(sandboxConfig) + ensureDefaultAgentWritePath(sandboxConfig, engineConfig) return sandboxConfig } @@ -254,7 +258,7 @@ func applySandboxDefaults(sandboxConfig *SandboxConfig, engineConfig *EngineConf // The legacy Type field indicates explicit sandbox configuration if sandboxConfig.Type != "" { sandboxLog.Printf("Sandbox config uses legacy Type field: %s, preserving it", sandboxConfig.Type) - ensureDefaultAgentWritePath(sandboxConfig) + ensureDefaultAgentWritePath(sandboxConfig, engineConfig) return sandboxConfig } @@ -264,7 +268,7 @@ func applySandboxDefaults(sandboxConfig *SandboxConfig, engineConfig *EngineConf sandboxConfig.Agent = &AgentSandboxConfig{ Type: SandboxTypeAWF, } - ensureDefaultAgentWritePath(sandboxConfig) + ensureDefaultAgentWritePath(sandboxConfig, engineConfig) return sandboxConfig } @@ -279,12 +283,12 @@ func applySandboxDefaults(sandboxConfig *SandboxConfig, engineConfig *EngineConf sandboxConfig.Agent.Type = SandboxTypeAWF } - ensureDefaultAgentWritePath(sandboxConfig) + ensureDefaultAgentWritePath(sandboxConfig, engineConfig) return sandboxConfig } -// cloudHypervisorWorkspaceWritePath and cloudHypervisorAwfHomeWritePath are the additional -// filesystem.allowWrite entries seeded for the Cloud Hypervisor runtime. +// Cloud Hypervisor requires explicit filesystem.allowWrite entries for compiler-managed +// output paths as well as the workspace. // // Under Cloud Hypervisor, /workspace and /tmp/gh-aw are separate virtiofs exports, and the // AWF planner narrows each export independently based on the allowWrite entries that fall @@ -306,7 +310,7 @@ const cloudHypervisorAwfHomeWritePath = "/workspace/.awf-home" // container fails to start ("make mountpoint \"/tmp/awf-init\": read-only file system"). // Seeding a default there would therefore break every compose-runtime workflow, so // filesystem.allowWrite stays opt-in for those runtimes. -func ensureDefaultAgentWritePath(sandboxConfig *SandboxConfig) { +func ensureDefaultAgentWritePath(sandboxConfig *SandboxConfig, engineConfig *EngineConfig) { if sandboxConfig == nil || sandboxConfig.Agent == nil { return } @@ -320,6 +324,9 @@ func ensureDefaultAgentWritePath(sandboxConfig *SandboxConfig) { sandboxConfig.Agent.Config.Filesystem = &SRTFilesystemConfig{} } addAllowWritePathIfMissing(sandboxConfig.Agent.Config.Filesystem, defaultAgentWorkspaceWritePath) + if engineConfig != nil && engineConfig.ID == string(constants.CopilotEngine) { + addAllowWritePathIfMissing(sandboxConfig.Agent.Config.Filesystem, defaultAgentLogsWritePath) + } addAllowWritePathIfMissing(sandboxConfig.Agent.Config.Filesystem, cloudHypervisorWorkspaceWritePath) addAllowWritePathIfMissing(sandboxConfig.Agent.Config.Filesystem, cloudHypervisorAwfHomeWritePath) } diff --git a/pkg/workflow/sandbox_test.go b/pkg/workflow/sandbox_test.go index 2fb1c018ddf..db5f1419468 100644 --- a/pkg/workflow/sandbox_test.go +++ b/pkg/workflow/sandbox_test.go @@ -142,6 +142,7 @@ func TestApplySandboxDefaults(t *testing.T) { expected *SandboxConfig expectDefaultWritePath bool expectedAllowWrite []string + unexpectedAllowWrite []string }{ { name: "nil config creates default with AWF", @@ -245,7 +246,7 @@ func TestApplySandboxDefaults(t *testing.T) { // Cloud Hypervisor narrows the /workspace and /tmp/gh-aw exports independently, // so the default write path alone would leave /workspace (and the CH-managed // HOME under it) read-only. See ensureDefaultAgentWritePath. - name: "cloud-hypervisor runtime seeds agent, workspace and awf-home write paths", + name: "cloud-hypervisor runtime seeds agent, logs, workspace and awf-home write paths", config: &SandboxConfig{ Agent: &AgentSandboxConfig{ Type: SandboxTypeAWF, @@ -254,7 +255,25 @@ func TestApplySandboxDefaults(t *testing.T) { }, engine: &EngineConfig{ID: "copilot"}, expectDefaultWritePath: true, + expectedAllowWrite: []string{defaultAgentWorkspaceWritePath, defaultAgentLogsWritePath, cloudHypervisorWorkspaceWritePath, cloudHypervisorAwfHomeWritePath}, + expected: &SandboxConfig{ + Agent: &AgentSandboxConfig{ + Type: SandboxTypeAWF, + }, + }, + }, + { + name: "cloud-hypervisor runtime does not grant Copilot logs path to other engines", + config: &SandboxConfig{ + Agent: &AgentSandboxConfig{ + Type: SandboxTypeAWF, + Runtime: AgentRuntimeCloudHypervisor, + }, + }, + engine: &EngineConfig{ID: "claude"}, + expectDefaultWritePath: true, expectedAllowWrite: []string{defaultAgentWorkspaceWritePath, cloudHypervisorWorkspaceWritePath, cloudHypervisorAwfHomeWritePath}, + unexpectedAllowWrite: []string{defaultAgentLogsWritePath}, expected: &SandboxConfig{ Agent: &AgentSandboxConfig{ Type: SandboxTypeAWF, @@ -285,6 +304,11 @@ func TestApplySandboxDefaults(t *testing.T) { require.NotNil(t, result.Agent.Config.Filesystem) assert.Contains(t, result.Agent.Config.Filesystem.AllowWrite, expectedPath) } + for _, unexpectedPath := range tt.unexpectedAllowWrite { + require.NotNil(t, result.Agent.Config) + require.NotNil(t, result.Agent.Config.Filesystem) + assert.NotContains(t, result.Agent.Config.Filesystem.AllowWrite, unexpectedPath) + } }) } }