Files
agent-mgr/internal/provider/claudecode/pod.go
Steven Hooker e5b07cc1d8 initial agent-mgr: app builder platform MVP
Go API server + Preact UI + Claude Code adapter.
- App-centric model (ideas, not repos)
- AgentProvider interface for multi-agent support
- K8s pod lifecycle for sandboxed agent sessions
- Gitea integration (create repos, push branches)
- WebSocket streaming for live session output
- Woodpecker CI/CD pipelines (kaniko build + kubectl deploy)
2026-02-18 15:56:32 +01:00

123 lines
3.3 KiB
Go

package claudecode
import (
"fmt"
"github.com/agentsphere/agent-mgr/internal/provider"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
Namespace = "agent-mgr"
RunnerImage = "git.asp.now/platform/claude-code-runner:latest"
GitImage = "alpine/git:latest"
)
func buildPod(cfg provider.SessionConfig, opts *Config) *corev1.Pod {
podName := fmt.Sprintf("agent-%s", cfg.SessionID.String()[:8])
branch := cfg.Branch
cloneScript := fmt.Sprintf(`
set -eu
git clone %s /workspace
cd /workspace
git checkout -b %s
git config user.name "agent-mgr-bot"
git config user.email "bot@asp.now"
`, cfg.RepoClone, branch)
claudeArgs := []string{"--output-format", "stream-json", "--permission-mode", "auto-accept-only"}
if opts != nil {
if opts.Model != "" {
claudeArgs = append(claudeArgs, "--model", opts.Model)
}
if opts.MaxTurns > 0 {
claudeArgs = append(claudeArgs, "--max-turns", fmt.Sprintf("%d", opts.MaxTurns))
}
}
return &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: podName,
Namespace: Namespace,
Labels: map[string]string{
"app": "agent-session",
"session-id": cfg.SessionID.String(),
},
},
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyNever,
ServiceAccountName: "agent-runner",
InitContainers: []corev1.Container{
{
Name: "git-clone",
Image: GitImage,
Command: []string{"sh", "-c", cloneScript},
VolumeMounts: []corev1.VolumeMount{
{Name: "workspace", MountPath: "/workspace"},
},
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("50m"),
corev1.ResourceMemory: resource.MustParse("64Mi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("200m"),
corev1.ResourceMemory: resource.MustParse("128Mi"),
},
},
},
},
Containers: []corev1.Container{
{
Name: "claude",
Image: RunnerImage,
Args: claudeArgs,
Stdin: true,
TTY: false,
WorkingDir: "/workspace",
Env: []corev1.EnvVar{
{
Name: "ANTHROPIC_API_KEY",
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: "agent-mgr-secrets"},
Key: "anthropic-api-key",
},
},
},
{Name: "SESSION_ID", Value: cfg.SessionID.String()},
},
VolumeMounts: []corev1.VolumeMount{
{Name: "workspace", MountPath: "/workspace"},
},
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("200m"),
corev1.ResourceMemory: resource.MustParse("256Mi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("500m"),
corev1.ResourceMemory: resource.MustParse("512Mi"),
},
},
},
},
Volumes: []corev1.Volume{
{
Name: "workspace",
VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{
SizeLimit: resourcePtr(resource.MustParse("1Gi")),
},
},
},
},
},
}
}
func resourcePtr(q resource.Quantity) *resource.Quantity { return &q }