Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ services:
- WEKNORA_SANDBOX_MODE=${WEKNORA_SANDBOX_MODE:-docker}
- WEKNORA_SANDBOX_TIMEOUT=${WEKNORA_SANDBOX_TIMEOUT:-60}
- WEKNORA_SANDBOX_DOCKER_IMAGE=${WEKNORA_SANDBOX_DOCKER_IMAGE:-wechatopenai/weknora-sandbox:${WEKNORA_VERSION:-latest}}
- WEKNORA_SANDBOX_IMAGE=${WEKNORA_SANDBOX_IMAGE:-}
- WEKNORA_SANDBOX_MAX_CONCURRENT=${WEKNORA_SANDBOX_MAX_CONCURRENT:-}
- WEKNORA_SANDBOX_OPENSANDBOX_API_URL=${WEKNORA_SANDBOX_OPENSANDBOX_API_URL:-}
- WEKNORA_SANDBOX_OPENSANDBOX_API_KEY=${WEKNORA_SANDBOX_OPENSANDBOX_API_KEY:-}
- APK_MIRROR_ARG=${APK_MIRROR_ARG:-}
depends_on:
redis:
Expand Down
29 changes: 29 additions & 0 deletions helm/templates/app.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ spec:
spec:
{{- include "weknora.imagePullSecrets" . | nindent 6 }}
serviceAccountName: {{ include "weknora.serviceAccountName" . }}
{{- if eq .Values.app.sandbox.mode "kubernetes" }}
automountServiceAccountToken: true
{{- end }}
{{- with .Values.app.podSecurityContext | default .Values.global.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
Expand Down Expand Up @@ -141,6 +144,32 @@ spec:
name: {{ include "weknora.secretName" . }}
key: NEO4J_PASSWORD
{{- end }}
# Sandbox configuration
- name: WEKNORA_SANDBOX_MODE
value: {{ .Values.app.sandbox.mode | quote }}
- name: WEKNORA_SANDBOX_TIMEOUT
value: {{ .Values.app.sandbox.timeout | quote }}
- name: WEKNORA_SANDBOX_IMAGE
value: {{ .Values.app.sandbox.image | quote }}
{{- if eq .Values.app.sandbox.mode "kubernetes" }}
- name: WEKNORA_SANDBOX_KUBE_NAMESPACE
value: {{ .Values.app.sandbox.kubernetes.namespace | quote }}
- name: WEKNORA_SANDBOX_MAX_CONCURRENT
value: {{ .Values.app.sandbox.kubernetes.maxConcurrentSandboxes | quote }}
{{- end }}
{{- if eq .Values.app.sandbox.mode "opensandbox" }}
- name: WEKNORA_SANDBOX_OPENSANDBOX_API_URL
value: {{ .Values.app.sandbox.opensandbox.apiURL | quote }}
- name: WEKNORA_SANDBOX_OPENSANDBOX_API_KEY
{{- if .Values.app.sandbox.opensandbox.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.app.sandbox.opensandbox.existingSecret }}
key: apiKey
{{- else }}
value: {{ .Values.app.sandbox.opensandbox.apiKey | quote }}
{{- end }}
{{- end }}
{{- with .Values.app.extraEnv }}
# Additional environment variables
{{- toYaml . | nindent 12 }}
Expand Down
53 changes: 53 additions & 0 deletions helm/templates/sandbox-rbac.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{{/*
Sandbox RBAC resources for kubernetes sandbox mode.
Only created when sandbox mode is "kubernetes".
*/}}
{{- if eq .Values.app.sandbox.mode "kubernetes" }}
---
apiVersion: v1
kind: Namespace
metadata:
name: {{ .Values.app.sandbox.kubernetes.namespace }}
labels:
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: weknora
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: weknora-sandbox-runner
namespace: {{ .Values.app.sandbox.kubernetes.namespace }}
labels:
app.kubernetes.io/managed-by: {{ .Release.Service }}
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: weknora-sandbox-manager
namespace: {{ .Values.app.sandbox.kubernetes.namespace }}
rules:
- apiGroups: ["batch"]
resources: ["jobs"]
verbs: ["create", "delete", "get", "list", "watch"]
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["create", "delete", "get", "list"]
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: weknora-sandbox-manager
namespace: {{ .Values.app.sandbox.kubernetes.namespace }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: weknora-sandbox-manager
subjects:
- kind: ServiceAccount
name: {{ include "weknora.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
{{- end }}
30 changes: 30 additions & 0 deletions helm/templates/sandbox-resources.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{{/*
Sandbox ResourceQuota and NetworkPolicy for kubernetes sandbox mode.
Only created when sandbox mode is "kubernetes".
*/}}
{{- if eq .Values.app.sandbox.mode "kubernetes" }}
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: weknora-sandbox-quota
namespace: {{ .Values.app.sandbox.kubernetes.namespace }}
spec:
hard:
pods: {{ .Values.app.sandbox.kubernetes.quota.maxPods | quote }}
limits.memory: {{ .Values.app.sandbox.kubernetes.quota.maxMemory | quote }}
limits.cpu: {{ .Values.app.sandbox.kubernetes.quota.maxCPU | quote }}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: weknora-sandbox-deny-all
namespace: {{ .Values.app.sandbox.kubernetes.namespace }}
spec:
podSelector:
matchLabels:
app.kubernetes.io/component: sandbox
policyTypes:
- Ingress
- Egress
{{- end }}
37 changes: 37 additions & 0 deletions helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,43 @@ app:
# - name: OLLAMA_BASE_URL
# value: "http://ollama:11434"

# -- Sandbox configuration for Agent Skills script execution
sandbox:
# -- Sandbox mode: local, docker, kubernetes, opensandbox, disabled
# In Kubernetes, "local" is recommended as the baseline.
# Use "kubernetes" for container-level isolation without Docker daemon.
# Use "opensandbox" to integrate with an external OpenSandbox service.
mode: local
# -- Script execution timeout in seconds
timeout: 60
# -- Sandbox container image (used by docker/kubernetes modes)
image: wechatopenai/weknora-sandbox:latest
# -- Kubernetes sandbox mode configuration
kubernetes:
# -- Namespace for sandbox Pods
namespace: weknora-sandbox
# -- Max concurrent sandbox Jobs per WeKnora instance
maxConcurrentSandboxes: 5
# -- Resource limits per sandbox Pod
podResources:
limits:
cpu: "500m"
memory: "256Mi"
# -- ResourceQuota for sandbox namespace
quota:
maxPods: 10
maxMemory: "2Gi"
maxCPU: "4"
# -- OpenSandbox mode configuration
opensandbox:
# -- OpenSandbox Server URL
apiURL: ""
# -- API Key (plain text, used when existingSecret is not set)
apiKey: ""
# -- Use an existing Secret for the API key (recommended for production)
# The Secret must contain a key named "apiKey"
existingSecret: ""

# -- Service configuration
service:
type: ClusterIP
Expand Down
48 changes: 47 additions & 1 deletion internal/application/service/agent_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,10 @@ func (s *agentService) initializeSkillsManager(
if sandboxMode == "" {
sandboxMode = "disabled"
}
dockerImage := os.Getenv("WEKNORA_SANDBOX_DOCKER_IMAGE")
dockerImage := os.Getenv("WEKNORA_SANDBOX_IMAGE")
if dockerImage == "" {
dockerImage = os.Getenv("WEKNORA_SANDBOX_DOCKER_IMAGE") // backward compat
}
if dockerImage == "" {
dockerImage = sandbox.DefaultDockerImage
}
Expand All @@ -263,6 +266,19 @@ func (s *agentService) initializeSkillsManager(
}
}

// Read additional env vars for new sandbox modes
kubeNamespace := os.Getenv("WEKNORA_SANDBOX_KUBE_NAMESPACE")
kubeServiceAccount := os.Getenv("WEKNORA_SANDBOX_KUBE_SERVICE_ACCOUNT")
maxConcurrentStr := os.Getenv("WEKNORA_SANDBOX_MAX_CONCURRENT")
maxConcurrent := sandbox.DefaultMaxConcurrentSandboxes
if maxConcurrentStr != "" {
if v, err := strconv.Atoi(maxConcurrentStr); err == nil && v > 0 {
maxConcurrent = v
}
}
opensandboxAPIURL := os.Getenv("WEKNORA_SANDBOX_OPENSANDBOX_API_URL")
opensandboxAPIKey := os.Getenv("WEKNORA_SANDBOX_OPENSANDBOX_API_KEY")

switch sandboxMode {
case "docker":
sandboxMgr, err = sandbox.NewManagerFromType("docker", true, dockerImage) // Enable fallback to local
Expand All @@ -276,6 +292,36 @@ func (s *agentService) initializeSkillsManager(
logger.Warnf(ctx, "Failed to initialize local sandbox: %v", err)
sandboxMgr = sandbox.NewDisabledManager()
}
case "kubernetes":
config := sandbox.DefaultConfig()
config.Type = sandbox.SandboxTypeKubernetes
config.FallbackEnabled = true
config.DockerImage = dockerImage
if kubeNamespace != "" {
config.KubeNamespace = kubeNamespace
}
if kubeServiceAccount != "" {
config.KubeServiceAccount = kubeServiceAccount
}
config.MaxConcurrentSandboxes = maxConcurrent
sandboxMgr, err = sandbox.NewManager(config)
if err != nil {
logger.Warnf(ctx, "Failed to initialize kubernetes sandbox, falling back to disabled: %v", err)
sandboxMgr = sandbox.NewDisabledManager()
}
case "opensandbox":
config := sandbox.DefaultConfig()
config.Type = sandbox.SandboxTypeOpenSandbox
config.FallbackEnabled = true
config.DockerImage = dockerImage
config.OpenSandboxAPIURL = opensandboxAPIURL
config.OpenSandboxAPIKey = opensandboxAPIKey
config.MaxConcurrentSandboxes = maxConcurrent
sandboxMgr, err = sandbox.NewManager(config)
if err != nil {
logger.Warnf(ctx, "Failed to initialize opensandbox, falling back to disabled: %v", err)
sandboxMgr = sandbox.NewDisabledManager()
}
default:
sandboxMgr = sandbox.NewDisabledManager()
}
Expand Down
Loading