Skip to content
Closed
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
96 changes: 95 additions & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,100 @@ helm upgrade mosaic "$MOSAIC_CHART" \
--timeout 12m
```

### Feishu / Lark

Feishu and Lark messaging is optional and uses OpenClaw's native `feishu` channel plugin, pinned to the chart's OpenClaw release. The gateway opens an outbound persistent connection to the Feishu Open Platform, so no ingress or public endpoint is required. The `openclaw` pod needs HTTPS egress to `open.feishu.cn` and to the persistent connection endpoint that it returns, currently `msg-frontier.feishu.cn` (`open.larksuite.com` and `msg-frontier.larksuite.com` for Lark). Before the gateway starts, an init container installs `@openclaw/feishu` at `openclaw.feishu.pluginVersion` from `registry.npmjs.org` and stops the pod unless the integrity that npm verified matches `openclaw.feishu.pluginIntegrity`, so the pod also needs egress to `registry.npmjs.org`.

In the [Feishu Open Platform](https://open.feishu.cn/app) or Lark Developer console, create a custom app and:

1. Enable the **Bot** capability.
2. Grant the messaging scopes `im:message`, `im:message.p2p_msg:readonly`, `im:message.group_at_msg:readonly`, `im:message:send_as_bot`, and `im:resource`, plus `cardkit:card:write` for streaming replies and `contact:contact.base:readonly` so the agent sees sender names instead of `open_id` values. Name lookups also require the senders to be inside the app's contact data range, configured under the app's data permissions. If the contact scope cannot be granted, set `openclaw.feishu.resolveSenderNames: false`; otherwise every message makes a failed name lookup. The chart disables the plugin's document, wiki, drive, Bitable, chat, and permission tools, so their scopes are not needed.
3. Under event subscriptions, choose the persistent connection mode and add `im.message.receive_v1`. The console verifies the connection, so save this step after the gateway is running with the app credentials.
4. Publish a version and set the app's availability to the intended users.

Create a Secret in the release namespace from the app's App ID and App Secret. Keep the secret values outside Helm values and source control.

```bash
read -rp 'Feishu App ID (cli_...): ' FEISHU_APP_ID
read -rsp 'Feishu App Secret: ' FEISHU_APP_SECRET; echo

kubectl -n mosaic create secret generic mosaic-feishu \
--from-literal=FEISHU_APP_ID="$FEISHU_APP_ID" \
--from-literal=FEISHU_APP_SECRET="$FEISHU_APP_SECRET" \
--dry-run=client -o yaml | kubectl apply -f -
unset FEISHU_APP_SECRET
```

Add these values to the release configuration and apply them through the normal Helm upgrade workflow:

```yaml
openclaw:
feishu:
enabled: true
existingSecret: mosaic-feishu
# Use lark for Lark tenants.
domain: feishu
allowedUserIds:
- "ou_xxxxxxxxxxxxxxxx"
# Optional group chats where allowed users can @mention the bot.
groupChatIds:
- "oc_xxxxxxxxxxxxxxxx"
```

`allowedUserIds` lists Feishu `open_id` values (`ou_...`). An `open_id` is specific to the app. To find one, deploy with a placeholder ID, have the user send the bot a direct message, and read the rejected sender from the gateway log:

```bash
kubectl -n mosaic logs deploy/openclaw -c gateway | grep 'blocked unauthorized sender'
```

`groupChatIds` lists group `chat_id` values (`oc_...`), shown in the group's settings. A group message reaches the agent only when its chat is listed, its sender is in `allowedUserIds`, and it @mentions the bot. Group messages are ignored while `groupChatIds` is empty.

Each Feishu user has a separate direct-message session, and each group chat shares one session among its allowed members. Feishu conversations always run in View, whether or not the Edit module is enabled; use the web UI for Edit or Auto. Slack can remain enabled alongside Feishu.

To rotate the App Secret, update the Secret and run `kubectl -n mosaic rollout restart deployment/openclaw`. Updating only the Secret does not restart the pod. When changing `openclaw.feishu.pluginVersion`, set `openclaw.feishu.pluginIntegrity` to the new package's integrity:

```bash
read -rp 'Feishu plugin version: ' VERSION
curl -fsSL "https://registry.npmjs.org/@openclaw/feishu/-/feishu-${VERSION}.tgz" \
| python3 -c 'import base64, hashlib, sys; print("sha512-" + base64.b64encode(hashlib.sha512(sys.stdin.buffer.read()).digest()).decode())'
```

The Feishu connection does not affect the gateway's readiness, so a Ready `openclaw` pod does not mean that Feishu is connected. Check the plugin's log lines instead:

```bash
kubectl -n mosaic logs deploy/openclaw -c gateway | grep '\[feishu\]'
```

| Log message | Meaning |
|---|---|
| `app_id or app_secret is invalid` | The Secret holds a wrong App ID or App Secret. |
| `bot open_id unknown` | The bot identity lookup failed, usually because of wrong credentials or a missing Bot capability. Group mentions are ignored until a later retry succeeds. |
| `blocked unauthorized sender ou_...` | The direct-message sender is not in `allowedUserIds`. |
| `not in groupAllowFrom` | The group chat is not in `groupChatIds`. |
| `not in group ... sender allowlist` | The group message sender is not in `allowedUserIds`. |
| `did not mention bot` | A group message without an @mention, ignored by design. |
| `permission error resolving sender name` or `ignoring stale permission scope error` | The app lacks `contact:contact.base:readonly`. |
| `failed to resolve sender name` | The name lookup failed for another reason, most often because the sender is outside the app's contact data range. |

To deliver scheduled reports to Feishu, enable the OpenClaw scheduler, which is disabled by default:

```yaml
openclaw:
cron:
enabled: true
```

Then create an isolated job that announces its result to a user (`user:ou_...`) or a group chat (`chat:oc_...`):

```bash
kubectl -n mosaic exec deploy/openclaw -c gateway -- node /app/dist/index.js cron create "0 9 * * *" \
--tz Asia/Shanghai --name "Daily cluster check" --session isolated \
--message "Run a read-only cluster check. Summarize node status, GPU capacity, allocatable and requested counts, failing pods, and recent warning events." \
--announce --channel feishu --to "user:ou_xxxxxxxxxxxxxxxx"
```

Creating, editing, and manually running jobs requires the `operator.admin` scope. After each pod start, the gateway CLI is paired with read access only, so its first job command opens a scope upgrade request. Approve that request from the OpenClaw Control UI, which listens on the `openclaw` Service's gateway port and accepts the token from the `openclaw-secrets` Secret. Alternatively, manage jobs directly in the Control UI. Jobs persist on the OpenClaw volume across pod restarts, and scheduled runs stay in View.

### Access Modes

Enable guarded write operations with the Edit module:
Expand All @@ -394,7 +488,7 @@ modules:

Every interactive conversation starts in **View**. Users can switch that conversation to **Edit** for approval-gated changes or **Auto** for validated changes without approval. Auto requires confirmation through a warning dialog. Other conversations remain in View, and automated alert and cluster-health sessions are always read-only. When the module is disabled, the access control is not shown.

Native Slack users remain read-only unless their verified Slack user ID is listed under `openclaw.slack.editUserIds`. Edit users approve their own mutations with Slack buttons. The broader `openclaw.slack.allowedUserIds` list controls who may use AI Factory Operations Agent through DMs, group DMs, and channel mentions.
Native Slack users remain read-only unless their verified Slack user ID is listed under `openclaw.slack.editUserIds`. Edit users approve their own mutations with Slack buttons. The broader `openclaw.slack.allowedUserIds` list controls who may use AI Factory Operations Agent through DMs, group DMs, and channel mentions. Feishu users are always read-only.

Privileges are attached to separate tools and credentials. Read-only infrastructure tools and sandbox-local execution run automatically in View. OpenShell enforces local execution boundaries; the diagnostic command allowlist applies to remote SSH. Mutating Kubernetes, Base Command Manager, and SSH operations are blocked in View, approval-gated in Edit when `hitl: true`, and automatic in Auto. Kubernetes RBAC, request validation, configured SSH hosts, and audit logging apply in every mode.

Expand Down
2 changes: 2 additions & 0 deletions docs/security_model.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Automated alert and cluster-health sessions are always View-only. They cannot be

For native Slack, `openclaw.slack.allowedUserIds` remains the messaging allowlist for DMs, group DMs, and channel mentions. `openclaw.slack.editUserIds` is narrower: those verified users receive Edit and approve their own mutations through Slack buttons; every other allowed Slack user remains in View.

For native Feishu, `openclaw.feishu.allowedUserIds` is the messaging allowlist for DMs and group mentions, and `openclaw.feishu.groupChatIds` limits which group chats are admitted. Feishu has no identity-verified approval path, so every Feishu run is View even if the conversation's stored mode was changed elsewhere. The chart also disables the Feishu plugin's document, wiki, drive, Bitable, chat, and permission tools.

Setting `modules.edit.hitl=false` also removes approvals from Edit mode. Keep the default `true` unless every user permitted to select Edit is trusted to make changes directly.

## Kubernetes Boundaries
Expand Down
Binary file modified helm/mosaic-stack/files/agent-documentation.tar.gz
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,22 @@ export default definePluginEntry({
});
api.on("agent_end", (_event, context) => clearRunAccess(context.runId));
}
if (pluginConfig.feishuEnabled === true) {
// Feishu has no identity-verified approval path, so its runs stay in View
// even if the conversation's stored access state was changed elsewhere.
api.on("before_prompt_build", (_event, context) => {
if (context.channel !== "feishu" && context.messageProvider !== "feishu") return;
setRunAccess(context.runId, {
mode: "view",
provider: "feishu",
...(context.senderId ? { senderId: context.senderId } : {}),
});
return {
prependContext: "[Runtime Context]\nai_factory_operations_agent_access_mode=view\nThis access mode is authoritative for the current Feishu user.",
};
});
api.on("agent_end", (_event, context) => clearRunAccess(context.runId));
}
api.session.state.registerSessionExtension(sessionAccessExtension);
api.registerTrustedToolPolicy({
id: "session-access",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
"type": "array",
"items": { "type": "string" }
},
"feishuEnabled": {
"type": "boolean"
},
"hitl": {
"type": "boolean"
},
Expand Down
50 changes: 43 additions & 7 deletions helm/mosaic-stack/files/openclaw-seed/nemoclaw.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,12 @@
}
]
},
{{- if .Values.openclaw.slack.enabled }}
{{- if or .Values.openclaw.slack.enabled .Values.openclaw.feishu.enabled }}
"session": {
"dmScope": {{ .Values.openclaw.slack.session.dmScope | quote }}
"dmScope": {{ ternary .Values.openclaw.slack.session.dmScope "per-channel-peer" .Values.openclaw.slack.enabled | quote }}
},
{{- end }}
{{- if .Values.openclaw.slack.enabled }}
"diagnostics": {
"enabled": true,
"stuckSessionWarnMs": {{ .Values.openclaw.slack.diagnostics.stuckSessionWarnMs }},
Expand Down Expand Up @@ -141,8 +143,36 @@
"servers": {{ $managedMcpServers | toJson }}
},
{{- end }}
{{- if .Values.openclaw.slack.enabled }}
{{- if or .Values.openclaw.slack.enabled .Values.openclaw.feishu.enabled }}
"channels": {
{{- if .Values.openclaw.feishu.enabled }}
"feishu": {
"enabled": true,
"domain": {{ .Values.openclaw.feishu.domain | quote }},
"connectionMode": "websocket",
"appId": "${FEISHU_APP_ID}",
"appSecret": "${FEISHU_APP_SECRET}",
"configWrites": false,
"dmPolicy": "allowlist",
"allowFrom": {{ .Values.openclaw.feishu.allowedUserIds | toJson }},
"groupPolicy": {{ ternary "allowlist" "disabled" (gt (len .Values.openclaw.feishu.groupChatIds) 0) | quote }},
"groupAllowFrom": {{ .Values.openclaw.feishu.groupChatIds | toJson }},
"groupSenderAllowFrom": {{ .Values.openclaw.feishu.allowedUserIds | toJson }},
"requireMention": true,
"streaming": {{ .Values.openclaw.feishu.streaming }},
"resolveSenderNames": {{ .Values.openclaw.feishu.resolveSenderNames }},
"tools": {
"doc": false,
"chat": false,
"wiki": false,
"drive": false,
"perm": false,
"scopes": false,
"bitable": false
}
}{{- if .Values.openclaw.slack.enabled }},{{- end }}
{{- end }}
{{- if .Values.openclaw.slack.enabled }}
"slack": {
"enabled": true,
"mode": "socket",
Expand Down Expand Up @@ -177,6 +207,7 @@
"unfurlLinks": false,
"unfurlMedia": false
}
{{- end }}
},
{{- end }}
"models": {
Expand Down Expand Up @@ -213,7 +244,7 @@
}
},
"cron": {
"enabled": false
"enabled": {{ .Values.openclaw.cron.enabled }}
},
"plugins": {
"allow": [
Expand All @@ -227,7 +258,8 @@
"observability",
"remote-ssh",
"slurm",
"llm"{{- if .Values.openclaw.slack.enabled }},
"llm"{{- if .Values.openclaw.feishu.enabled }},
"feishu"{{- end }}{{- if .Values.openclaw.slack.enabled }},
"slack"{{- end }}{{- if .Values.modules.webSearch.enabled }},
{{ .Values.modules.webSearch.provider | quote }}{{- end }}
],
Expand Down Expand Up @@ -258,12 +290,16 @@
{{- end }}
},
"slackEnabled": {{ .Values.openclaw.slack.enabled }},
"slackEditUserIds": {{ .Values.openclaw.slack.editUserIds | toJson }}
"slackEditUserIds": {{ .Values.openclaw.slack.editUserIds | toJson }},
"feishuEnabled": {{ .Values.openclaw.feishu.enabled }}
}
},
"llm": {
"enabled": true
}{{- if .Values.openclaw.slack.enabled }},
}{{- if .Values.openclaw.feishu.enabled }},
"feishu": {
"enabled": true
}{{- end }}{{- if .Values.openclaw.slack.enabled }},
"slack": {
"enabled": true
}{{- end }}{{- if .Values.modules.webSearch.enabled }},
Expand Down
41 changes: 41 additions & 0 deletions helm/mosaic-stack/templates/openclaw.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,35 @@ spec:
- name: remote-ssh-credentials
mountPath: /credentials
{{- end }}
{{- if .Values.openclaw.feishu.enabled }}
- name: init-feishu-plugin
image: {{ include "mosaic-stack.image" .Values.openclaw.image | quote }}
imagePullPolicy: {{ .Values.openclaw.image.pullPolicy }}
command:
- sh
- -c
- |
set -eu
node /app/dist/index.js plugins install {{ printf "@openclaw/feishu@%v" .Values.openclaw.feishu.pluginVersion | quote }} --force
node /app/dist/index.js plugins inspect feishu --json > /tmp/feishu-plugin.json
node -e '
const install = JSON.parse(require("fs").readFileSync("/tmp/feishu-plugin.json", "utf8")).install;
if (install?.integrity !== process.argv[1]) {
console.error(`@openclaw/feishu integrity ${install?.integrity} does not match ${process.argv[1]}`);
process.exit(1);
}
console.log(`@openclaw/feishu integrity verified: ${install.integrity}`);
' {{ .Values.openclaw.feishu.pluginIntegrity | quote }}
env:
- name: HOME
value: /home/node
securityContext:
runAsUser: 1000
runAsGroup: 1000
volumeMounts:
- name: openclaw-home
mountPath: /home/node/.openclaw
{{- end }}
{{- if .Values.openclaw.slack.enabled }}
- name: init-slack-plugin
image: {{ include "mosaic-stack.image" .Values.openclaw.image | quote }}
Expand Down Expand Up @@ -578,6 +607,18 @@ spec:
name: {{ .Values.openclaw.slack.existingSecret | quote }}
key: {{ .Values.openclaw.slack.appTokenKey | quote }}
{{- end }}
{{- if .Values.openclaw.feishu.enabled }}
- name: FEISHU_APP_ID
valueFrom:
secretKeyRef:
name: {{ .Values.openclaw.feishu.existingSecret | quote }}
key: {{ .Values.openclaw.feishu.appIdKey | quote }}
- name: FEISHU_APP_SECRET
valueFrom:
secretKeyRef:
name: {{ .Values.openclaw.feishu.existingSecret | quote }}
key: {{ .Values.openclaw.feishu.appSecretKey | quote }}
{{- end }}
{{- if and .Values.modules.webSearch.enabled (eq .Values.modules.webSearch.provider "brave") }}
- name: WEB_SEARCH_KEY
valueFrom:
Expand Down
20 changes: 20 additions & 0 deletions helm/mosaic-stack/templates/validation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,26 @@
{{- fail (printf "Slack edit user %q must also appear in openclaw.slack.allowedUserIds" .) -}}
{{- end -}}
{{- end -}}
{{- if .Values.openclaw.feishu.enabled -}}
{{- if not (and .Values.modules.execution.enabled .Values.openclaw.enabled) -}}
{{- fail "openclaw.feishu.enabled requires modules.execution.enabled and openclaw.enabled" -}}
{{- end -}}
{{- if not .Values.openclaw.feishu.existingSecret -}}
{{- fail "openclaw.feishu.existingSecret is required when Feishu is enabled" -}}
{{- end -}}
{{- if empty .Values.openclaw.feishu.allowedUserIds -}}
{{- fail "openclaw.feishu.allowedUserIds must contain at least one Feishu open_id or user_id" -}}
{{- end -}}
{{- if has "*" .Values.openclaw.feishu.allowedUserIds -}}
{{- fail "openclaw.feishu.allowedUserIds must list individual users, not \"*\"" -}}
{{- end -}}
{{- if not (has .Values.openclaw.feishu.domain (list "feishu" "lark")) -}}
{{- fail "openclaw.feishu.domain must be one of: feishu, lark" -}}
{{- end -}}
{{- if and .Values.openclaw.slack.enabled (eq .Values.openclaw.slack.session.dmScope "main") -}}
{{- fail "Feishu requires separate DM sessions per sender; set openclaw.slack.session.dmScope to per-channel-peer" -}}
{{- end -}}
{{- end -}}
{{- if .Values.modules.webSearch.enabled -}}
{{- if not (has .Values.modules.webSearch.provider (list "duckduckgo" "brave")) -}}
{{- fail "modules.webSearch.provider must be one of: duckduckgo, brave" -}}
Expand Down
22 changes: 22 additions & 0 deletions helm/mosaic-stack/tests/automation-context.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,28 @@ test("fails closed when a Slack Edit run has no identity approval broker", async
clearRunAccess("run-no-broker");
});

test("keeps Feishu runs in View even when the conversation stores Auto", () => {
const feishu = {
runId: "run-feishu",
sessionKey: "agent:default:feishu:direct:ou_1234567890",
getSessionExtension: () => ({ mode: "auto" }),
};
setRunAccess(feishu.runId, { mode: "view", provider: "feishu", senderId: "ou_1234567890" });
assert.equal(runAccessMode(feishu), "view");
assert.equal(contextAllowsMutation(true, feishu), false);
assert.equal(contextSkipsApproval(feishu), false);
clearRunAccess(feishu.runId);
assert.equal(runAccessMode(feishu), "auto");

const guard = readFileSync(
new URL("../files/openclaw-seed/extensions/automation-guard/index.ts", import.meta.url),
"utf8",
);
assert.match(guard, /pluginConfig\.feishuEnabled === true/);
assert.match(guard, /context\.channel !== "feishu" && context\.messageProvider !== "feishu"/);
assert.match(guard, /mode: "view",\s*provider: "feishu"/);
});

test("projects and registers access state in every approval-owning plugin", () => {
assert.deepEqual(sessionAccessExtension.project({ state: { mode: "auto" } }), { mode: "auto" });
for (const plugin of ["automation-guard", "kubernetes", "bcm", "remote-ssh"]) {
Expand Down
Loading
Loading