From a198448d6d7f140ada526cb27c4e29d796288fcd Mon Sep 17 00:00:00 2001 From: wangyuyan-agent Date: Thu, 21 May 2026 11:47:44 +0800 Subject: [PATCH 1/3] feat: add openab-feishu chart (colocated OAB + gateway, WebSocket default) Single-pod Helm chart for Feishu/Lark deployments: - OAB agent and gateway as colocated containers - WebSocket mode (default): outbound-only, no public endpoint needed - Optional webhook mode with cloudflared sidecar - Supports both Feishu (feishu.cn) and Lark (larksuite.com) - Only 2 required --set flags: feishu.appId, feishu.appSecret - existingSecret support for production credential management - Security contexts: non-root, read-only rootfs, drop all caps --- charts/openab-feishu/Chart.yaml | 6 + charts/openab-feishu/README.md | 220 ++++++++++++++++++ charts/openab-feishu/templates/NOTES.txt | 73 ++++++ charts/openab-feishu/templates/_helpers.tpl | 41 ++++ charts/openab-feishu/templates/configmap.yaml | 36 +++ .../openab-feishu/templates/deployment.yaml | 180 ++++++++++++++ charts/openab-feishu/templates/pvc.yaml | 19 ++ charts/openab-feishu/templates/secret.yaml | 27 +++ charts/openab-feishu/values.yaml | 116 +++++++++ 9 files changed, 718 insertions(+) create mode 100644 charts/openab-feishu/Chart.yaml create mode 100644 charts/openab-feishu/README.md create mode 100644 charts/openab-feishu/templates/NOTES.txt create mode 100644 charts/openab-feishu/templates/_helpers.tpl create mode 100644 charts/openab-feishu/templates/configmap.yaml create mode 100644 charts/openab-feishu/templates/deployment.yaml create mode 100644 charts/openab-feishu/templates/pvc.yaml create mode 100644 charts/openab-feishu/templates/secret.yaml create mode 100644 charts/openab-feishu/values.yaml diff --git a/charts/openab-feishu/Chart.yaml b/charts/openab-feishu/Chart.yaml new file mode 100644 index 000000000..7904d83aa --- /dev/null +++ b/charts/openab-feishu/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: openab-feishu +description: OpenAB + Feishu/Lark — single-pod deployment with gateway (WebSocket by default, optional Cloudflare Tunnel for webhook mode). +type: application +version: 0.1.0 +appVersion: "0.8.3" diff --git a/charts/openab-feishu/README.md b/charts/openab-feishu/README.md new file mode 100644 index 000000000..69711e5ef --- /dev/null +++ b/charts/openab-feishu/README.md @@ -0,0 +1,220 @@ +# openab-feishu + +OpenAB + Feishu/Lark — single-pod Helm chart for deploying an AI agent on Feishu (飛書) or Lark. + +## Architecture + +``` +┌──────────── Pod: openab-feishu ────────────┐ +│ │ +│ ┌──────────┐ localhost ┌─────────────┐ │ +│ │ openab │ ◄──────────► │ gateway │ │ +│ │ (agent) │ │ (feishu) │ │ +│ └──────────┘ └──────┬──────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ PVC /home/agent Outbound WebSocket │ +│ to open.feishu.cn │ +└─────────────────────────────────────────────┘ +``` + +**Default mode: WebSocket** — gateway connects outbound to Feishu, no public endpoint needed. + +Optional webhook mode adds a cloudflared sidecar (same pattern as `openab-telegram`). + +## Quick Start + +```bash +helm install my-bot ./charts/openab-feishu \ + --set feishu.appId="cli_xxx" \ + --set feishu.appSecret="xxx" \ + --namespace openab --create-namespace +``` + +Only **2 required parameters**. Everything else has sane defaults. + +## Prerequisites: Feishu Open Platform Setup + +Before deploying, create a Feishu app. This is a one-time setup. + +> **Hermes Agent** and **OpenClaw** both support Feishu via their own gateway implementations (env-var based config, `pip install` / `npm install`). OAB's approach is K8s-native: a single `helm install` deploys everything, with credentials managed as K8s Secrets. + +### 1. Create App + +1. Go to [Feishu Open Platform](https://open.feishu.cn) (or [Lark Developer](https://open.larksuite.com) for overseas) +2. Click **Create Custom App** +3. Note down the **App ID** (`cli_xxx`) and **App Secret** + +### 2. Enable Bot Capability + +1. In the left sidebar, go to **App Features** → **Bot** +2. Toggle **Enable Bot** to ON + +> ⚠️ This step is easy to miss. Without it, the app cannot receive messages. + +### 3. Configure Event Subscription + +1. Navigate to **Event Subscriptions** in the left sidebar +2. **Connection mode**: Select **WebSocket** (recommended) + - WebSocket requires no public URL — the gateway connects outbound + - If you must use webhook mode, see [Webhook Mode](#webhook-mode) below +3. **Add event**: `im.message.receive_v1` (Receive messages) + +### 4. Add Permissions + +Under **Permissions & Scopes**, add these scopes: + +| Scope | Purpose | +|-------|---------| +| `im:message` | Send and receive messages | +| `im:message.group_at_msg` | Receive @mention messages in groups | +| `im:message.group_at_msg:readonly` | Read group @mention messages | +| `im:message.p2p_msg:readonly` | Read DM messages | +| `im:resource` | Download images/files from messages | +| `contact:user.base:readonly` | Resolve user display names | + +### 5. Publish + +Click **Create Version** → **Apply for publish**. For development, you can use the app in test mode without full approval. + +### 6. Get IDs for Access Control (Optional) + +To restrict which users/groups can interact with the bot: + +- **Group ID** (`oc_xxx`): Open the group → click top-right menu → Settings → Group ID +- **User Open ID** (`ou_xxx`): Check gateway logs after the user sends a message, or use the Feishu Contact API + +## Credential Management + +Three options from simplest to most secure: + +| # | Method | Security | Notes | +|---|--------|----------|-------| +| 1 | `--set feishu.appId=X --set feishu.appSecret=Y` | ⚠️ Stored in Helm release | Good for dev/testing | +| 2 | `kubectl create secret` + `--set existingSecret=name` | ✅ Out of Helm values | Good for production | +| 3 | `kubectl create secret --from-env-file=<(vault/aws sm)` + `--set existingSecret=name` | ✅✅ Never touches disk | Best for security | + +### Option 2 example: + +```bash +kubectl create secret generic feishu-creds -n openab \ + --from-literal=feishu-app-id="cli_xxx" \ + --from-literal=feishu-app-secret="xxx" + +helm install my-bot ./charts/openab-feishu \ + --set existingSecret=feishu-creds \ + --namespace openab --create-namespace +``` + +### Option 3 example (AWS Secrets Manager): + +```bash +kubectl create secret generic feishu-creds -n openab \ + --from-env-file=<(aws secretsmanager get-secret-value \ + --secret-id oab-feishu --query SecretString --output text | \ + jq -r '{"feishu-app-id": .appId, "feishu-app-secret": .appSecret} | to_entries[] | "\(.key)=\(.value)"') + +helm install my-bot ./charts/openab-feishu \ + --set existingSecret=feishu-creds \ + --namespace openab --create-namespace +``` + +## Release Channel + +| `channel` | Core image tag | Gateway image tag | +|-----------|---------------|-------------------| +| `stable` (default) | `ghcr.io/openabdev/openab:stable` | `v0.5.1` (pinned) | +| `beta` | `ghcr.io/openabdev/openab:beta` | `v0.5.1` (pinned) | + +## Webhook Mode + +If WebSocket is not available (e.g., network policy blocks outbound WebSocket), switch to webhook mode: + +```bash +helm install my-bot ./charts/openab-feishu \ + --set feishu.appId="cli_xxx" \ + --set feishu.appSecret="xxx" \ + --set feishu.connectionMode="webhook" \ + --set feishu.verificationToken="xxx" \ + --set feishu.encryptKey="xxx" \ + --set tunnel.token="eyJ..." \ + --set webhookDomain="bot.example.com" \ + --namespace openab --create-namespace +``` + +This adds a cloudflared sidecar (3-container pod, same as `openab-telegram`). + +After deployment: +1. Configure Cloudflare Tunnel ingress to point your domain at `localhost:8080` +2. In Feishu Open Platform → Event Subscriptions → set Request URL to `https://bot.example.com/webhook/feishu` + - ⚠️ The gateway must be running when you set the URL — Feishu sends a challenge request immediately + +## Lark (Overseas) + +For Lark (larksuite.com) instead of Feishu (feishu.cn): + +```bash +helm install my-bot ./charts/openab-feishu \ + --set feishu.appId="cli_xxx" \ + --set feishu.appSecret="xxx" \ + --set feishu.domain="lark" \ + --namespace openab --create-namespace +``` + +## Comparison with Other Platforms + +| Feature | openab-feishu | openab-telegram | OpenClaw | Hermes Agent | +|---------|--------------|-----------------|----------|--------------| +| Default containers | 2 (agent + gateway) | 3 (+ cloudflared) | N/A (no Helm) | N/A (no Helm) | +| Public endpoint needed | ❌ (WebSocket) | ✅ (webhook) | Varies | Varies | +| Feishu/Lark support | ✅ Native | ❌ | ❌ | ❌ | +| K8s-native deployment | ✅ Helm chart | ✅ Helm chart | ❌ docker-compose | ❌ pip install | +| Credential params | 2 (appId + appSecret) | 2 (botToken + tunnelToken) | N/A | N/A | + +## Values Reference + +| Key | Default | Description | +|-----|---------|-------------| +| `feishu.appId` | `""` | **(required)** Feishu App ID | +| `feishu.appSecret` | `""` | **(required)** Feishu App Secret | +| `feishu.domain` | `"feishu"` | `"feishu"` or `"lark"` | +| `feishu.connectionMode` | `"websocket"` | `"websocket"` or `"webhook"` | +| `feishu.verificationToken` | `""` | Webhook verification token | +| `feishu.encryptKey` | `""` | Webhook encrypt key | +| `existingSecret` | `""` | Use pre-existing K8s Secret | +| `tunnel.enabled` | `false` | Enable cloudflared sidecar | +| `tunnel.token` | `""` | Cloudflare Tunnel token | +| `webhookDomain` | `""` | Domain for webhook URL | +| `channel` | `"stable"` | `"stable"` or `"beta"` | +| `platform.requireMention` | `true` | Require @mention in groups | +| `platform.allowedGroups` | `[]` | Allowed group chat IDs | +| `platform.allowedUsers` | `[]` | Allowed user open_ids | +| `persistence.enabled` | `true` | Enable PVC for agent state | +| `persistence.size` | `"1Gi"` | PVC size | + +## Troubleshooting + +| Problem | Fix | +|---------|-----| +| Bot doesn't respond to DMs | Ensure Bot capability is enabled in App Features → Bot | +| Bot doesn't respond in groups | Ensure you @mention the bot (default: `requireMention: true`) | +| Bot doesn't receive any messages | Check event subscription: must have `im.message.receive_v1` and WebSocket mode selected | +| Gateway logs show "token refresh error" | Verify `appId` and `appSecret` are correct | +| Gateway logs show "feishu ws endpoint error" | WebSocket mode requires the app to be published (at least test version) | +| Permission denied on image/file download | Grant `im:resource` scope and re-publish the app | +| User names show as `ou_xxx` | Grant `contact:user.base:readonly` scope | +| Pod CrashLoopBackOff | Check `kubectl logs -c gateway` — usually a credential issue | + +## Comparison: OAB vs OpenClaw vs Hermes Agent (Feishu) + +| Aspect | OAB (this chart) | OpenClaw | Hermes Agent | +|--------|-----------------|----------|--------------| +| Deployment | `helm install` (K8s-native) | `npx @larksuite/openclaw-lark install` | `hermes gateway setup` | +| Runtime | Rust binary (gateway) + any agent | Node.js | Python | +| Connection mode | WebSocket (default) / Webhook | WebSocket (default) / Webhook | WebSocket (default) / Webhook | +| Config style | Helm values + K8s Secrets | JSON config file | `.env` + `config.yaml` | +| Credential management | 3-tier (--set → K8s Secret → external SM) | Plain config file | `.env` file | +| Security hardening | Non-root, read-only rootfs, drop all caps | N/A (runs as user process) | N/A (runs as user process) | +| Public endpoint needed | ❌ (WebSocket mode) | ❌ (WebSocket mode) | ❌ (WebSocket mode) | +| Feishu-specific features | @mention gating, user allowlist, group allowlist, bot-to-bot, media proxy | Streaming cards, multi-account, ACP sessions, pairing | Interactive cards, document comments, per-group ACL, burst batching | + diff --git a/charts/openab-feishu/templates/NOTES.txt b/charts/openab-feishu/templates/NOTES.txt new file mode 100644 index 000000000..f25b8373f --- /dev/null +++ b/charts/openab-feishu/templates/NOTES.txt @@ -0,0 +1,73 @@ +🎉 OpenAB Feishu bot deployed! + +{{- if include "openab-feishu.tunnelEnabled" . }} +Pod: {{ include "openab-feishu.fullname" . }} (3 containers: openab, gateway, cloudflared) +Mode: webhook (with Cloudflare Tunnel) +{{- else }} +Pod: {{ include "openab-feishu.fullname" . }} (2 containers: openab, gateway) +Mode: websocket (outbound-only, no public endpoint needed) +{{- end }} + +## Post-Install Steps + +{{- if eq .Values.feishu.connectionMode "websocket" }} + +### Step 1: Configure Feishu Open Platform (one-time) + +1. Go to https://open.feishu.cn → Create or select your app +2. Under "Event Subscriptions" → Connection mode: select "WebSocket" +3. Subscribe to event: im.message.receive_v1 +4. Under "Permissions", add: + - im:message (Send and receive messages) + - im:message.group_at_msg (Receive group @messages) + - im:resource (Download message resources) + - contact:user.base:readonly (Read user basic info — for display names) +5. Publish the app version + +No public URL or webhook configuration needed — the gateway connects outbound. + +{{- else }} + +### Step 1: Configure Cloudflare Tunnel ingress + +Option A — Via Cloudflare Dashboard: + https://one.dash.cloudflare.com/ → Networks → Tunnels → your tunnel → Public Hostname → Add: + Hostname: {{ .Values.webhookDomain | default "bot.example.com" }} + Type: HTTP + URL: localhost:8080 + +### Step 2: Configure Feishu Open Platform + +1. Go to https://open.feishu.cn → Create or select your app +2. Under "Event Subscriptions" → Connection mode: select "HTTP" +3. Set Request URL: https://{{ .Values.webhookDomain | default "YOUR_DOMAIN" }}/webhook/feishu + (The gateway must be running — Feishu sends a challenge request immediately) +4. Subscribe to event: im.message.receive_v1 +5. Under "Permissions", add: + - im:message + - im:message.group_at_msg + - im:resource + - contact:user.base:readonly +6. Publish the app version + +{{- end }} + +### {{ if eq .Values.feishu.connectionMode "websocket" }}Step 2{{ else }}Step 3{{ end }}: Authenticate the agent + +The agent needs a one-time OAuth login: + + kubectl exec -it deployment/{{ include "openab-feishu.fullname" . }} -n {{ .Release.Namespace }} -c openab -- {{ .Values.agent.command }} login --use-device-flow + +Then restart to pick up credentials: + + kubectl rollout restart deployment/{{ include "openab-feishu.fullname" . }} -n {{ .Release.Namespace }} + +## Verify + +Send a message to your bot on Feishu (DM or @mention in a group). Check logs if no response: + + kubectl logs -n {{ .Release.Namespace }} deployment/{{ include "openab-feishu.fullname" . }} -c openab --tail=20 + kubectl logs -n {{ .Release.Namespace }} deployment/{{ include "openab-feishu.fullname" . }} -c gateway --tail=20 +{{- if include "openab-feishu.tunnelEnabled" . }} + kubectl logs -n {{ .Release.Namespace }} deployment/{{ include "openab-feishu.fullname" . }} -c cloudflared --tail=20 +{{- end }} diff --git a/charts/openab-feishu/templates/_helpers.tpl b/charts/openab-feishu/templates/_helpers.tpl new file mode 100644 index 000000000..f0c4a85d6 --- /dev/null +++ b/charts/openab-feishu/templates/_helpers.tpl @@ -0,0 +1,41 @@ +{{- define "openab-feishu.fullname" -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "openab-feishu.labels" -}} +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 }} +app.kubernetes.io/name: {{ .Chart.Name }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{- define "openab-feishu.selectorLabels" -}} +app.kubernetes.io/name: {{ .Chart.Name }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{- define "openab-feishu.agentImage" -}} +{{- $tag := .Values.image.tag -}} +{{- if not $tag -}} + {{- $tag = .Values.channel | default "stable" -}} +{{- end -}} +{{- printf "%s:%s" .Values.image.repository $tag -}} +{{- end }} + +{{- define "openab-feishu.gatewayImage" -}} +{{- printf "%s:%s" .Values.gateway.image .Values.gateway.tag -}} +{{- end }} + +{{- define "openab-feishu.secretName" -}} +{{- .Values.existingSecret | default (include "openab-feishu.fullname" .) -}} +{{- end }} + +{{- define "openab-feishu.tunnelEnabled" -}} +{{- if .Values.tunnel.enabled -}} +true +{{- else if and (eq .Values.feishu.connectionMode "webhook") .Values.tunnel.token -}} +true +{{- else -}} +{{- end -}} +{{- end }} diff --git a/charts/openab-feishu/templates/configmap.yaml b/charts/openab-feishu/templates/configmap.yaml new file mode 100644 index 000000000..500225c2f --- /dev/null +++ b/charts/openab-feishu/templates/configmap.yaml @@ -0,0 +1,36 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "openab-feishu.fullname" . }} + labels: + {{- include "openab-feishu.labels" . | nindent 4 }} +data: + config.toml: | + [agent] + command = {{ .Values.agent.command | toJson }} + args = {{ .Values.agent.args | default list | toJson }} + working_dir = {{ .Values.agent.workingDir | default "/home/agent" | toJson }} + {{- if .Values.agent.env }} + env = { {{ $first := true }}{{ range $k, $v := .Values.agent.env }}{{ if not $first }}, {{ end }}{{ $k }} = {{ $v | toJson }}{{ $first = false }}{{ end }} } + {{- end }} + {{- $secretEnvKeys := list }} + {{- range .Values.agent.secretEnv }}{{ $secretEnvKeys = append $secretEnvKeys .name }}{{ end }} + {{- if $secretEnvKeys }} + inherit_env = {{ $secretEnvKeys | toJson }} + {{- end }} + + [pool] + max_sessions = {{ .Values.agent.pool.maxSessions | default 10 }} + session_ttl_hours = {{ .Values.agent.pool.sessionTtlHours | default 24 }} + + [reactions] + enabled = {{ .Values.agent.reactions.enabled | default true }} + remove_after_reply = {{ .Values.agent.reactions.removeAfterReply | default false }} + + [gateway] + url = "ws://localhost:8080/ws" + platform = "feishu" + allow_all_channels = {{ if .Values.platform.allowedGroups }}false{{ else }}true{{ end }} + allowed_channels = {{ .Values.platform.allowedGroups | default list | toJson }} + allow_all_users = {{ if .Values.platform.allowedUsers }}false{{ else }}true{{ end }} + allowed_users = {{ .Values.platform.allowedUsers | default list | toJson }} diff --git a/charts/openab-feishu/templates/deployment.yaml b/charts/openab-feishu/templates/deployment.yaml new file mode 100644 index 000000000..03b6ab13e --- /dev/null +++ b/charts/openab-feishu/templates/deployment.yaml @@ -0,0 +1,180 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "openab-feishu.fullname" . }} + labels: + {{- include "openab-feishu.labels" . | nindent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + {{- include "openab-feishu.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/config: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum }} + checksum/secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }} + labels: + {{- include "openab-feishu.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + # --- OAB agent (main) --- + - name: openab + image: {{ include "openab-feishu.agentImage" . | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- with .Values.containerSecurityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + env: + - name: HOME + value: {{ .Values.agent.workingDir | default "/home/agent" }} + {{- range $k, $v := .Values.agent.env }} + - name: {{ $k }} + value: {{ $v | quote }} + {{- end }} + {{- range .Values.agent.secretEnv }} + - name: {{ .name }} + valueFrom: + secretKeyRef: + name: {{ .secretName }} + key: {{ .secretKey }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: config + mountPath: /etc/openab + readOnly: true + {{- if .Values.persistence.enabled }} + - name: data + mountPath: {{ .Values.agent.workingDir | default "/home/agent" }} + {{- end }} + - name: tmp + mountPath: /tmp + + # --- Gateway (sidecar) --- + - name: gateway + image: {{ include "openab-feishu.gatewayImage" . | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- with .Values.containerSecurityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: 8080 + protocol: TCP + env: + - name: HOME + value: {{ .Values.agent.workingDir | default "/home/agent" }} + - name: FEISHU_APP_ID + valueFrom: + secretKeyRef: + name: {{ include "openab-feishu.secretName" . }} + key: feishu-app-id + - name: FEISHU_APP_SECRET + valueFrom: + secretKeyRef: + name: {{ include "openab-feishu.secretName" . }} + key: feishu-app-secret + - name: FEISHU_DOMAIN + value: {{ .Values.feishu.domain | quote }} + - name: FEISHU_CONNECTION_MODE + value: {{ .Values.feishu.connectionMode | quote }} + {{- if .Values.feishu.verificationToken }} + - name: FEISHU_VERIFICATION_TOKEN + valueFrom: + secretKeyRef: + name: {{ include "openab-feishu.secretName" . }} + key: feishu-verification-token + {{- end }} + {{- if .Values.feishu.encryptKey }} + - name: FEISHU_ENCRYPT_KEY + valueFrom: + secretKeyRef: + name: {{ include "openab-feishu.secretName" . }} + key: feishu-encrypt-key + {{- end }} + {{- if .Values.platform.requireMention }} + - name: FEISHU_REQUIRE_MENTION + value: "true" + {{- else }} + - name: FEISHU_REQUIRE_MENTION + value: "false" + {{- end }} + {{- if .Values.platform.allowedGroups }} + - name: FEISHU_ALLOWED_GROUPS + value: {{ join "," .Values.platform.allowedGroups | quote }} + {{- end }} + {{- if .Values.platform.allowedUsers }} + - name: FEISHU_ALLOWED_USERS + value: {{ join "," .Values.platform.allowedUsers | quote }} + {{- end }} + volumeMounts: + {{- if .Values.persistence.enabled }} + - name: data + mountPath: {{ .Values.agent.workingDir | default "/home/agent" }} + {{- end }} + - name: tmp + mountPath: /tmp + + {{- if include "openab-feishu.tunnelEnabled" . }} + # --- Cloudflared tunnel (sidecar, webhook mode only) --- + - name: cloudflared + image: {{ printf "%s:%s" .Values.tunnel.image .Values.tunnel.tag }} + imagePullPolicy: IfNotPresent + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + args: + - tunnel + - --no-autoupdate + - run + - --token + - $(TUNNEL_TOKEN) + env: + - name: TUNNEL_TOKEN + valueFrom: + secretKeyRef: + name: {{ include "openab-feishu.secretName" . }} + key: cloudflare-tunnel-token + volumeMounts: + - name: tmp + mountPath: /tmp + {{- end }} + + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + - name: config + configMap: + name: {{ include "openab-feishu.fullname" . }} + {{- if .Values.persistence.enabled }} + - name: data + persistentVolumeClaim: + claimName: {{ .Values.persistence.existingClaim | default (include "openab-feishu.fullname" .) }} + {{- end }} + - name: tmp + emptyDir: {} diff --git a/charts/openab-feishu/templates/pvc.yaml b/charts/openab-feishu/templates/pvc.yaml new file mode 100644 index 000000000..4f609b877 --- /dev/null +++ b/charts/openab-feishu/templates/pvc.yaml @@ -0,0 +1,19 @@ +{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "openab-feishu.fullname" . }} + annotations: + "helm.sh/resource-policy": keep + labels: + {{- include "openab-feishu.labels" . | nindent 4 }} +spec: + accessModes: + - ReadWriteOnce + {{- if .Values.persistence.storageClass }} + storageClassName: {{ .Values.persistence.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.size | default "1Gi" }} +{{- end }} diff --git a/charts/openab-feishu/templates/secret.yaml b/charts/openab-feishu/templates/secret.yaml new file mode 100644 index 000000000..39f4ae930 --- /dev/null +++ b/charts/openab-feishu/templates/secret.yaml @@ -0,0 +1,27 @@ +{{- if not .Values.existingSecret }} +{{- if not .Values.feishu.appId }} +{{- fail "feishu.appId is required when existingSecret is not set (--set feishu.appId=YOUR_APP_ID)" }} +{{- end }} +{{- if not .Values.feishu.appSecret }} +{{- fail "feishu.appSecret is required when existingSecret is not set (--set feishu.appSecret=YOUR_APP_SECRET)" }} +{{- end }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "openab-feishu.fullname" . }} + labels: + {{- include "openab-feishu.labels" . | nindent 4 }} +type: Opaque +stringData: + feishu-app-id: {{ .Values.feishu.appId | quote }} + feishu-app-secret: {{ .Values.feishu.appSecret | quote }} + {{- if .Values.feishu.verificationToken }} + feishu-verification-token: {{ .Values.feishu.verificationToken | quote }} + {{- end }} + {{- if .Values.feishu.encryptKey }} + feishu-encrypt-key: {{ .Values.feishu.encryptKey | quote }} + {{- end }} + {{- if .Values.tunnel.token }} + cloudflare-tunnel-token: {{ .Values.tunnel.token | quote }} + {{- end }} +{{- end }} diff --git a/charts/openab-feishu/values.yaml b/charts/openab-feishu/values.yaml new file mode 100644 index 000000000..29b48746b --- /dev/null +++ b/charts/openab-feishu/values.yaml @@ -0,0 +1,116 @@ +# openab-feishu values +# +# Install (WebSocket mode — default, no tunnel needed): +# helm install my-bot ./charts/openab-feishu \ +# --set feishu.appId="cli_xxx" \ +# --set feishu.appSecret="xxx" \ +# --namespace openab --create-namespace +# +# Required: +# feishu.appId -- Feishu/Lark App ID (from open.feishu.cn) +# feishu.appSecret -- Feishu/Lark App Secret +# +# Optional: +# feishu.domain -- "feishu" (default, China) or "lark" (overseas) +# feishu.connectionMode -- "websocket" (default) or "webhook" +# tunnel.enabled -- Enable cloudflared sidecar (auto-enabled in webhook mode) +# tunnel.token -- Cloudflare Tunnel token (required if tunnel.enabled) + +# -- Feishu/Lark application credentials +feishu: + # -- (required unless existingSecret is set) App ID from Feishu Open Platform + appId: "" + # -- (required unless existingSecret is set) App Secret + appSecret: "" + # -- "feishu" for feishu.cn (China) or "lark" for larksuite.com (overseas) + domain: "feishu" + # -- "websocket" (default, outbound-only, no public endpoint needed) or "webhook" + connectionMode: "websocket" + # -- (webhook mode only) Verification token from Feishu Open Platform + verificationToken: "" + # -- (webhook mode only) Encrypt key for event signature verification + encryptKey: "" + +# -- Use a pre-existing K8s Secret instead of creating one from --set values. +# The Secret must contain keys: feishu-app-id, feishu-app-secret +# Optional keys: feishu-verification-token, feishu-encrypt-key, cloudflare-tunnel-token +existingSecret: "" + +# -- Cloudflare Tunnel sidecar (only needed for webhook mode) +tunnel: + # -- Enable tunnel sidecar. Auto-enabled when connectionMode=webhook and token is set. + enabled: false + # -- Cloudflare Tunnel token + token: "" + image: cloudflare/cloudflared + tag: "2026.5.0" + +# -- Webhook domain (shown in post-install notes) +webhookDomain: "" + +# -- Release channel: "stable" or "beta" +channel: stable + +# -- OAB agent image +image: + repository: ghcr.io/openabdev/openab + tag: "" # defaults to channel + pullPolicy: IfNotPresent + +# -- Gateway image +gateway: + image: ghcr.io/openabdev/openab-gateway + tag: "v0.5.1" + +# -- Agent configuration +agent: + command: kiro-cli + args: + - acp + - --trust-all-tools + workingDir: /home/agent + env: {} + secretEnv: [] + pool: + maxSessions: 10 + sessionTtlHours: 24 + reactions: + enabled: true + removeAfterReply: false + +# -- Gateway platform settings +platform: + # -- Require @mention in groups (recommended for shared groups) + requireMention: true + # -- Feishu group chat IDs allowed (empty = all groups) + allowedGroups: [] + # -- Feishu user open_ids allowed (empty = all users) + allowedUsers: [] + +# -- Persistence for agent working directory +persistence: + enabled: true + existingClaim: "" + storageClass: "" + size: 1Gi + +# -- Pod-level settings +resources: {} +nodeSelector: {} +tolerations: [] +affinity: {} + +podSecurityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + +containerSecurityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL From de5bc866a986ac45edd9dccc977bef1d83078dfb Mon Sep 17 00:00:00 2001 From: wangyuyan-agent Date: Sat, 23 May 2026 09:10:16 +0800 Subject: [PATCH 2/3] fix(openab-feishu): address chart review findings from chaodu-agent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six issues fixed across deployment.yaml, configmap.yaml, and pvc.yaml: F1 (🔴) existingSecret + webhook mode silently dropped env vars: FEISHU_VERIFICATION_TOKEN and FEISHU_ENCRYPT_KEY secretKeyRefs are now rendered whenever connectionMode=webhook (not only when values are non-empty). Added optional: true so pods start even if those keys are absent from the secret (both are optional security hardening). F2 (🟡) Boolean default trap in reactions config: Removed `| default true/false` pipes from configmap.yaml. Defaults are declared in values.yaml; the pipes caused `false` to be treated as empty and substituted with `true`, making reactions un-disableable. BUG1 (🔴) tunnel.enabled=true without token caused silent CrashLoop: Added a `fail` guard that aborts helm template/install with a clear error message when the tunnel is enabled but no token is provided and no existingSecret is set. BUG2 (🟡) storageClass: "-" rendered as literal "-" storageClassName: Applied the standard Helm convention: "-" is mapped to storageClassName: "" (static PV / empty class), any other non-empty value is passed through as-is. BUG3 (🟡) checksum/secret annotation had wrong semantics in existingSecret mode: When existingSecret is set, secret.yaml renders empty and the checksum was a constant — external secret rotations would not trigger a rolling restart. Annotation is now skipped when existingSecret is set. BUG4 (🟡) TOML env map rendered in non-deterministic order: Replaced manual $first-flag iteration with `keys | sortAlpha` + index lookup. Env keys now render alphabetically, eliminating spurious checksum/config diffs in GitOps pipelines. --- charts/openab-feishu/templates/configmap.yaml | 6 +++--- charts/openab-feishu/templates/deployment.yaml | 11 +++++++++-- charts/openab-feishu/templates/pvc.yaml | 4 ++++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/charts/openab-feishu/templates/configmap.yaml b/charts/openab-feishu/templates/configmap.yaml index 500225c2f..baf3fe999 100644 --- a/charts/openab-feishu/templates/configmap.yaml +++ b/charts/openab-feishu/templates/configmap.yaml @@ -11,7 +11,7 @@ data: args = {{ .Values.agent.args | default list | toJson }} working_dir = {{ .Values.agent.workingDir | default "/home/agent" | toJson }} {{- if .Values.agent.env }} - env = { {{ $first := true }}{{ range $k, $v := .Values.agent.env }}{{ if not $first }}, {{ end }}{{ $k }} = {{ $v | toJson }}{{ $first = false }}{{ end }} } + env = { {{ range $i, $k := (.Values.agent.env | keys | sortAlpha) }}{{ if gt $i 0 }}, {{ end }}{{ $k }} = {{ index $.Values.agent.env $k | toJson }}{{ end }} } {{- end }} {{- $secretEnvKeys := list }} {{- range .Values.agent.secretEnv }}{{ $secretEnvKeys = append $secretEnvKeys .name }}{{ end }} @@ -24,8 +24,8 @@ data: session_ttl_hours = {{ .Values.agent.pool.sessionTtlHours | default 24 }} [reactions] - enabled = {{ .Values.agent.reactions.enabled | default true }} - remove_after_reply = {{ .Values.agent.reactions.removeAfterReply | default false }} + enabled = {{ .Values.agent.reactions.enabled }} + remove_after_reply = {{ .Values.agent.reactions.removeAfterReply }} [gateway] url = "ws://localhost:8080/ws" diff --git a/charts/openab-feishu/templates/deployment.yaml b/charts/openab-feishu/templates/deployment.yaml index 03b6ab13e..dbadc7962 100644 --- a/charts/openab-feishu/templates/deployment.yaml +++ b/charts/openab-feishu/templates/deployment.yaml @@ -15,7 +15,9 @@ spec: metadata: annotations: checksum/config: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- if not .Values.existingSecret }} checksum/secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }} + {{- end }} labels: {{- include "openab-feishu.selectorLabels" . | nindent 8 }} spec: @@ -90,19 +92,21 @@ spec: value: {{ .Values.feishu.domain | quote }} - name: FEISHU_CONNECTION_MODE value: {{ .Values.feishu.connectionMode | quote }} - {{- if .Values.feishu.verificationToken }} + {{- if or .Values.feishu.verificationToken (eq .Values.feishu.connectionMode "webhook") }} - name: FEISHU_VERIFICATION_TOKEN valueFrom: secretKeyRef: name: {{ include "openab-feishu.secretName" . }} key: feishu-verification-token + optional: true {{- end }} - {{- if .Values.feishu.encryptKey }} + {{- if or .Values.feishu.encryptKey (eq .Values.feishu.connectionMode "webhook") }} - name: FEISHU_ENCRYPT_KEY valueFrom: secretKeyRef: name: {{ include "openab-feishu.secretName" . }} key: feishu-encrypt-key + optional: true {{- end }} {{- if .Values.platform.requireMention }} - name: FEISHU_REQUIRE_MENTION @@ -128,6 +132,9 @@ spec: mountPath: /tmp {{- if include "openab-feishu.tunnelEnabled" . }} + {{- if and (not .Values.tunnel.token) (not .Values.existingSecret) }} + {{- fail "tunnel.token is required when the cloudflared tunnel is enabled (set tunnel.token, or supply credentials via existingSecret with a cloudflare-tunnel-token key)" }} + {{- end }} # --- Cloudflared tunnel (sidecar, webhook mode only) --- - name: cloudflared image: {{ printf "%s:%s" .Values.tunnel.image .Values.tunnel.tag }} diff --git a/charts/openab-feishu/templates/pvc.yaml b/charts/openab-feishu/templates/pvc.yaml index 4f609b877..0cca83059 100644 --- a/charts/openab-feishu/templates/pvc.yaml +++ b/charts/openab-feishu/templates/pvc.yaml @@ -11,8 +11,12 @@ spec: accessModes: - ReadWriteOnce {{- if .Values.persistence.storageClass }} + {{- if eq "-" .Values.persistence.storageClass }} + storageClassName: "" + {{- else }} storageClassName: {{ .Values.persistence.storageClass | quote }} {{- end }} + {{- end }} resources: requests: storage: {{ .Values.persistence.size | default "1Gi" }} From 94e1bc8ce2a2d00ea4868e49fddf06d183827ad4 Mon Sep 17 00:00:00 2001 From: wangyuyan-agent Date: Sat, 23 May 2026 09:21:40 +0800 Subject: [PATCH 3/3] docs(openab-feishu): document existingSecret rotation limitation Helm cannot track changes to externally-managed Secrets, so rotating credentials does not automatically trigger a Pod rollout when existingSecret is set. Added a comment in values.yaml explaining this limitation and pointing to Reloader as the recommended solution. --- charts/openab-feishu/values.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/charts/openab-feishu/values.yaml b/charts/openab-feishu/values.yaml index 29b48746b..dd9f11c04 100644 --- a/charts/openab-feishu/values.yaml +++ b/charts/openab-feishu/values.yaml @@ -34,6 +34,12 @@ feishu: # -- Use a pre-existing K8s Secret instead of creating one from --set values. # The Secret must contain keys: feishu-app-id, feishu-app-secret # Optional keys: feishu-verification-token, feishu-encrypt-key, cloudflare-tunnel-token +# +# NOTE: When existingSecret is set, the chart cannot track changes to the external +# Secret's contents — rotating credentials will NOT automatically trigger a Pod +# rollout. To enable automatic rollout on secret rotation, use a tool such as +# Reloader (https://github.com/stakater/Reloader) and annotate the Deployment: +# kubectl annotate deployment reloader.stakater.com/auto="true" existingSecret: "" # -- Cloudflare Tunnel sidecar (only needed for webhook mode)