Skip to content
Draft
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
39 changes: 34 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,51 @@ on:
- "src/**"
- "crates/**"
- "operator/**"
- "scripts/teams-ack-drop-proxy.py"
- "scripts/test-teams-ack-drop-proxy.py"
- ".github/workflows/ci.yml"
- "Cargo.toml"
- "Cargo.lock"
- "Dockerfile*"

env:
CARGO_TERM_COLOR: always

permissions:
contents: read

jobs:
changes:
runs-on: ubuntu-latest
outputs:
core: ${{ steps.filter.outputs.core }}
operator: ${{ steps.filter.outputs.operator }}
teams_ack_proxy: ${{ steps.filter.outputs.teams_ack_proxy }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
fetch-depth: 0
persist-credentials: false
- id: filter
env:
BASE: ${{ github.event.pull_request.base.sha }}
HEAD: ${{ github.event.pull_request.head.sha }}
run: |
CHANGED=$(git diff --name-only "$BASE" "$HEAD")
echo "core=$(echo "$CHANGED" | grep -qE '^(src/|crates/|Cargo\.(toml|lock))' && echo true || echo false)" >> "$GITHUB_OUTPUT"
echo "operator=$(echo "$CHANGED" | grep -q '^operator/' && echo true || echo false)" >> "$GITHUB_OUTPUT"
{
echo "core=$(echo "$CHANGED" | grep -qE '^(src/|crates/|Cargo\.(toml|lock)|\.github/workflows/ci\.yml$)' && echo true || echo false)"
echo "operator=$(echo "$CHANGED" | grep -q '^operator/' && echo true || echo false)"
echo "teams_ack_proxy=$(echo "$CHANGED" | grep -qE '^(scripts/(teams-ack-drop-proxy|test-teams-ack-drop-proxy)\.py|\.github/workflows/ci\.yml)$' && echo true || echo false)"
} >> "$GITHUB_OUTPUT"

check:
needs: changes
if: needs.changes.outputs.core == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable (2026-07-13)
with:
components: clippy
Expand Down Expand Up @@ -94,6 +107,20 @@ jobs:
- name: cargo build (unified)
run: cargo build --features unified

teams-ack-proxy:
needs: changes
if: needs.changes.outputs.teams_ack_proxy == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- name: Test bounded Teams ACK-drop proxy
run: |
set -euo pipefail
python3 -m py_compile scripts/teams-ack-drop-proxy.py scripts/test-teams-ack-drop-proxy.py
python3 -W error::ResourceWarning scripts/test-teams-ack-drop-proxy.py

operator:
needs: changes
if: needs.changes.outputs.operator == 'true'
Expand All @@ -102,7 +129,9 @@ jobs:
run:
working-directory: operator
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable (2026-07-13)
with:
components: clippy
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ gateway/target/
config.toml
*.swp
.DS_Store
__pycache__/
*.py[cod]
.env
.kiro/

Expand Down
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 37 additions & 2 deletions charts/openab/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,13 @@ Each agent lives under `agents.<name>`.
| `stt.baseUrl` | STT API base URL. | `"https://api.groq.com/openai/v1"` |
| `gateway.enabled` | Enable the gateway config block for webhook-based platforms. | `false` |
| `gateway.deploy` | Deploy the gateway Deployment and Service. | `true` |
| `cron.usercronEnabled` | Enable user-provided cron configuration. | `false` |
| `cronjobs` | Config-driven scheduled messages for an agent. | `[]` |
| `gateway.teams.reactionsEnabled` | Opt in to Microsoft public-preview Bot Connector reactions. | `false` |
| `gateway.teams.inboundAttachments` | Enable metadata-first Teams image/text ingress on both Core and Gateway. | `false` |
| `gateway.teams.conversationRegistryPath` | Opt in to the Gateway-local persistent Teams conversation registry. Mount the path separately. | `""` |
| `gateway.teams.conversationRegistryMaxEntries` | Persistent Teams registry entry cap. | `1000` |
| `gateway.teams.conversationRegistryTtlSecs` | Active/disabled registry retention window. | `31536000` |
| `configToml` | Raw authoritative `config.toml`, including baseline `[[cron.jobs]]`. Required unless `configUrl` is used. | `""` |
| `configUrl` | External authoritative config URL; mutually exclusive with the rendered ConfigMap path. | `""` |
| `persistence.enabled` | Enable persistent storage for auth and settings. | `true` |
| `persistence.existingClaim` | Reuse an existing PVC instead of creating one. | `""` |
| `agentsMd` | Contents of `AGENTS.md` mounted into the working directory. | `""` |
Expand Down Expand Up @@ -112,6 +117,36 @@ See [`docs/migrate-to-configtoml.md`](../../docs/migrate-to-configtoml.md) for a
[`docs/adr/configurl-over-helm-rendering.md`](../../docs/adr/configurl-over-helm-rendering.md) for when to prefer `configUrl` instead
(platform-agnostic — works identically on Kubernetes, ECS, Zeabur, and AgentCore).

For Teams typed scope, put the policy in that raw TOML rather than under the Gateway transport values:

```toml
[teams]
allowed_teams = []
allowed_channels = [] # both empty = all Team channels; otherwise Team OR channel match
allow_personal = true
allow_group_chats = true
```

Presence of any of these four fields opts into typed L2 policy. In Standalone Gateway mode, the policy still belongs to the OpenAB Core `configToml`; `gateway.teams.*` configures transport credentials and reaction preview on the Gateway container.

`gateway.teams.inboundAttachments=true` is the exception that must stay aligned across processes: the chart emits `TEAMS_INBOUND_ATTACHMENTS=true` into both Core and Gateway. It enables bounded metadata-first image/text materialization only after Core trust admission. When `gateway.deploy=false`, configure the same environment variable on the external Gateway yourself.

`gateway.teams.conversationRegistryPath` is a separate Gateway-only opt-in. The chart does not silently provision or attach a Gateway PVC; use `gateway.extraVolumeMounts` and `gateway.extraVolumes` (prefer an externally managed PVC with `"helm.sh/resource-policy": keep`) so the configured file survives pod replacement. An empty path preserves the previous process-local behavior and emits no registry environment variables.

Teams operator cron belongs only in the raw Core `configToml`; the chart does not create a parallel target selector:

```toml
[[cron.jobs]]
schedule = "0 9 * * 1-5"
platform = "teams"
channel = "<teams-conversation-id>"
teams_tenant_id = "<tenant-id>"
message = "summarize yesterday's merged work"
timezone = "Asia/Taipei"
```

This requires an exact active record in the Gateway registry. `serviceUrl` remains Gateway-local, `thread_id` is invalid for Teams, and agent-writable usercron cannot select the record.

### Discord ID precision warning

Discord IDs must be set with `--set-string`, not `--set`. Otherwise Helm may coerce them into numbers and lose precision.
4 changes: 4 additions & 0 deletions charts/openab/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ spec:
name: {{ include "openab.agentFullname" $d }}
key: gateway-ws-token
{{- end }}
{{- if and ($cfg.gateway).enabled (hasKey (($cfg.gateway).teams) "inboundAttachments") }}
- name: TEAMS_INBOUND_ATTACHMENTS
value: {{ ($cfg.gateway).teams.inboundAttachments | quote }}
{{- end }}
- name: HOME
value: {{ $cfg.workingDir | default "/home/agent" }}
{{- range $k, $v := $cfg.env }}
Expand Down
16 changes: 16 additions & 0 deletions charts/openab/templates/gateway.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,22 @@ spec:
- name: TEAMS_WEBHOOK_PATH
value: {{ ($cfg.gateway).teams.webhookPath | quote }}
{{- end }}
{{- if hasKey (($cfg.gateway).teams) "reactionsEnabled" }}
- name: TEAMS_REACTIONS_ENABLED
value: {{ ($cfg.gateway).teams.reactionsEnabled | quote }}
{{- end }}
{{- if hasKey (($cfg.gateway).teams) "inboundAttachments" }}
- name: TEAMS_INBOUND_ATTACHMENTS
value: {{ ($cfg.gateway).teams.inboundAttachments | quote }}
{{- end }}
{{- if ($cfg.gateway).teams.conversationRegistryPath }}
- name: TEAMS_CONVERSATION_REGISTRY_PATH
value: {{ ($cfg.gateway).teams.conversationRegistryPath | quote }}
- name: TEAMS_CONVERSATION_REGISTRY_MAX_ENTRIES
value: {{ ($cfg.gateway).teams.conversationRegistryMaxEntries | int64 | quote }}
- name: TEAMS_CONVERSATION_REGISTRY_TTL_SECS
value: {{ ($cfg.gateway).teams.conversationRegistryTtlSecs | int64 | quote }}
{{- end }}
{{- end }}
{{- $hasFeishu := and (($cfg.gateway).feishu).appId (($cfg.gateway).feishu).appSecret }}
{{- if $hasFeishu }}
Expand Down
57 changes: 57 additions & 0 deletions charts/openab/tests/teams_registry_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
suite: Teams persistent conversation registry rendering
templates:
- templates/gateway.yaml

set:
agents.kiro.gateway.enabled: true
agents.kiro.gateway.teams.appId: test-app
agents.kiro.gateway.teams.appSecret: test-secret

tests:
- it: keeps registry disabled and adds no volume by default
asserts:
- notContains:
path: spec.template.spec.containers[0].env
content:
name: TEAMS_CONVERSATION_REGISTRY_PATH
- notExists:
path: spec.template.spec.volumes

- it: renders integer defaults after an explicit path
set:
agents.kiro.gateway.teams.conversationRegistryPath: /var/lib/openab/teams/conversations.json
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: TEAMS_CONVERSATION_REGISTRY_MAX_ENTRIES
value: "1000"
- contains:
path: spec.template.spec.containers[0].env
content:
name: TEAMS_CONVERSATION_REGISTRY_TTL_SECS
value: "31536000"

- it: renders bounded registry settings only after an explicit path
set:
agents.kiro.gateway.teams.conversationRegistryPath: /var/lib/openab/teams/conversations.json
agents.kiro.gateway.teams.conversationRegistryMaxEntries: 123
agents.kiro.gateway.teams.conversationRegistryTtlSecs: 456
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: TEAMS_CONVERSATION_REGISTRY_PATH
value: /var/lib/openab/teams/conversations.json
- contains:
path: spec.template.spec.containers[0].env
content:
name: TEAMS_CONVERSATION_REGISTRY_MAX_ENTRIES
value: "123"
- contains:
path: spec.template.spec.containers[0].env
content:
name: TEAMS_CONVERSATION_REGISTRY_TTL_SECS
value: "456"
- notExists:
path: spec.template.spec.volumes
17 changes: 16 additions & 1 deletion charts/openab/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ agents:
# # allowed_channels = ["C01234567"]
# # allow_user_messages = "mentions"
# #
# # [teams]
# # allowed_teams = [] # Team IDs; both lists empty = all Team channels
# # allowed_channels = [] # a Team OR channel match admits
# # allow_personal = true
# # allow_group_chats = true
# #
# # [agent]
# # command = "claude-agent-acp"
# # inherit_env = ["ANTHROPIC_API_KEY"]
Expand Down Expand Up @@ -416,7 +422,7 @@ agents:
gateway:
enabled: false # set to true + provide url to enable the [gateway] config block
deploy: true # set to false to skip Gateway Deployment/Service (config-only mode)
url: "" # e.g. ws://openab-gateway:8080/ws
url: "" # WebSocket URL, e.g. the in-cluster openab-gateway Service
platform: "telegram" # default platform when gateway is enabled
token: "" # optional shared secret (injected via GATEWAY_WS_TOKEN env var)
botUsername: "" # optional, for @mention gating
Expand Down Expand Up @@ -461,6 +467,15 @@ agents:
openidMetadata: "" # Override for sovereign clouds → TEAMS_OPENID_METADATA
allowedTenants: [] # List of tenant IDs → TEAMS_ALLOWED_TENANTS
webhookPath: "" # Gateway default: /webhook/teams → TEAMS_WEBHOOK_PATH
reactionsEnabled: false # Public-preview Bot Connector reactions → TEAMS_REACTIONS_ENABLED
# Default-off metadata-first image/text ingress. Sets the same env on
# Core and Gateway; no Microsoft URL or token crosses into Core.
inboundAttachments: false # → TEAMS_INBOUND_ATTACHMENTS
# Optional PR 11 persistent route registry. The chart does not create a
# Gateway PVC; mount this path with gateway.extraVolumeMounts/extraVolumes.
conversationRegistryPath: "" # disabled → TEAMS_CONVERSATION_REGISTRY_PATH
conversationRegistryMaxEntries: 1000 # → TEAMS_CONVERSATION_REGISTRY_MAX_ENTRIES
conversationRegistryTtlSecs: 31536000 # → TEAMS_CONVERSATION_REGISTRY_TTL_SECS
# Feishu/Lark adapter config (gateway-side env vars)
# See docs/feishu.md for full setup guide
feishu:
Expand Down
34 changes: 31 additions & 3 deletions config.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ allowed_channels = ["1234567890"] # ↑ omitted + non-empty list → auto-
# # send-once (streaming is forced off to avoid
# # posting duplicate, growing messages)
# streaming_placeholder = false # set false for draft-based platforms (e.g. Telegram Rich Messages)
# gateway_ack_timeout_secs = 12 # only enforced for ACKs advertised by a negotiated gateway

# --- Telegram (first-class section; alternative to TELEGRAM_* env vars) ---
# Config-authoritative with ${} expansion; each field falls back to its
Expand Down Expand Up @@ -133,7 +134,23 @@ allowed_channels = ["1234567890"] # ↑ omitted + non-empty list → auto-
# app_secret = "${TEAMS_APP_SECRET}" # env fallback: TEAMS_APP_SECRET
# allowed_tenants = ["<tenant-guid>"] # env fallback: TEAMS_ALLOWED_TENANTS (empty = all)
# webhook_path = "/webhook/teams" # env fallback: TEAMS_WEBHOOK_PATH
# allow_all_users = false # env fallback: TEAMS_ALLOW_ALL_USERS
# dedupe_ttl_secs = 600 # env fallback: TEAMS_DEDUPE_TTL_SECS
# route_ttl_secs = 3600 # env fallback: TEAMS_ROUTE_TTL_SECS
# max_route_entries = 10000 # independent route/dedupe/ownership caps; env: TEAMS_MAX_ROUTE_ENTRIES
# reactions_enabled = false # public-preview reactions; env: TEAMS_REACTIONS_ENABLED
# processing_indicator = "off" # off | message; env: TEAMS_PROCESSING_INDICATOR
# streaming = false # progressive bot-owned edits; env: TEAMS_STREAMING
# inbound_attachments = false # post-trust image/text materialization; env: TEAMS_INBOUND_ATTACHMENTS
# conversation_registry_path = "teams/conversations.json" # opt-in; relative to $HOME/.openab/
# conversation_registry_max_entries = 1000 # env: TEAMS_CONVERSATION_REGISTRY_MAX_ENTRIES
# conversation_registry_ttl_secs = 31536000 # one year; env: TEAMS_CONVERSATION_REGISTRY_TTL_SECS
# allowed_teams = [] # Team IDs; env: TEAMS_ALLOWED_TEAMS (comma-separated)
# allowed_channels = [] # channel IDs; env: TEAMS_ALLOWED_CHANNELS
# # both empty = all Team channels; Team OR channel match
# allow_personal = true # env: TEAMS_ALLOW_PERSONAL
# allow_group_chats = true # env: TEAMS_ALLOW_GROUP_CHATS
# # setting any field above opts into typed scope policy
# allow_all_users = false # independent L3 gate; env: TEAMS_ALLOW_ALL_USERS
# allowed_users = ["29:1abc..."] # Bot Framework activity.from.id values (29:…)
# # env fallback: TEAMS_ALLOWED_USERS (comma-separated)

Expand Down Expand Up @@ -340,10 +357,21 @@ error_hold_ms = 2500
# schedule = "0 9 * * 1-5" # weekdays at 9:00 AM
# channel = "123456789" # target channel/thread ID
# message = "summarize yesterday's merged PRs" # prompt for the agent
# platform = "discord" # "discord" or "slack"
# platform = "discord" # discord/slack/telegram/googlechat/lineworks/teams
# sender_name = "DailyOps" # attribution (default: "openab-cron")
# timezone = "America/New_York" # IANA timezone (default: "UTC")
# thread_id = "" # optional: post to existing thread
# thread_id = "" # optional existing thread; rejected for Teams

# Teams operator baseline: requires an active Gateway conversation-registry record.
# `channel` is the trusted Teams conversation ID; do not configure serviceUrl.
# [[cron.jobs]]
# schedule = "0 9 * * 1-5"
# platform = "teams"
# channel = "<teams-conversation-id>"
# teams_tenant_id = "<tenant-id>"
# message = "summarize yesterday's merged work"
# sender_name = "DailyOps"
# timezone = "Asia/Taipei"

# [[cron.jobs]]
# schedule = "0 0 * * 0"
Expand Down
Loading
Loading