1 Commits

Author SHA1 Message Date
user
73f31c75be docs: add webhook vs poller notification delivery approaches
All checks were successful
check / check (push) Successful in 11s
Add a new 'Notification Delivery: Webhooks vs Polling' section to
OPENCLAW_TRICKS.md explaining the two approaches for receiving Gitea
notifications:

1. Direct webhooks for VPS/public server deployments (realtime, push-based)
2. Notification poller for local machines behind NAT (simple, no firewall config)

Includes a comparison table to help users choose.

Update SETUP_CHECKLIST.md Phase 10 to present both options (A and B) with
separate sub-checklists, replacing the previous poller-only instructions.
2026-02-28 03:24:32 -08:00
2 changed files with 527 additions and 952 deletions

View File

@@ -173,91 +173,46 @@ The landing checklist (triggered automatically after every flight) updates
location, timezone, nearest airport, and lodging in the daily context file. It location, timezone, nearest airport, and lodging in the daily context file. It
also checks if any cron jobs have hardcoded timezones that need updating. also checks if any cron jobs have hardcoded timezones that need updating.
### Gitea Notification Delivery ### The Gitea Notification Poller
There are two approaches for getting Gitea notifications to your agent, OpenClaw has heartbeats, but those are periodic (every ~30min). For Gitea issues
depending on your network setup. and PRs, we wanted near-realtime response. The solution: a tiny Python script
that polls the Gitea notifications API every 2 seconds and wakes the agent via
#### Option A: Direct Webhooks (VPS / Public Server) OpenClaw's `/hooks/wake` endpoint when new notifications arrive.
If your OpenClaw instance runs on a VPS or other publicly reachable server, the
simplest approach is direct webhooks. Run Traefik (or any reverse proxy with
automatic TLS) on the same server and configure Gitea webhooks to POST directly
to OpenClaw's webhook endpoint. This is push-based and realtime — notifications
arrive instantly.
Setup: add a webhook on each Gitea repo (or use an organization-level webhook)
pointing to `https://your-openclaw-host/hooks/gitea`. OpenClaw handles the rest.
#### Option B: Notification Poller + Dispatcher (Local Machine Behind NAT)
If your OpenClaw runs on a dedicated local machine behind NAT (like a home Mac
or Linux workstation), Gitea can't reach it directly. This is our setup —
OpenClaw runs on a Mac Studio on a home LAN.
The solution: a Python script that polls the Gitea notifications API, triages
each notification, and spawns an isolated agent session per actionable item.
Response time is ~15-30 seconds.
**Evolution note:** We originally used a flag-file approach (poller writes
flag → agent checks during heartbeat → ~30 min latency). This was replaced by
the dispatcher pattern below, which is near-realtime.
Key design decisions: Key design decisions:
- **The poller IS the dispatcher.** It fetches notification details, checks - **The poller never marks notifications as read.** That's the agent's job after
whether the agent is mentioned or assigned, and spawns agents directly. it processes them. This prevents the poller and agent from racing.
No middleman session needed. - **It tracks notification IDs, not counts.** This way it only fires on
- **One agent per actionable notification.** Each spawns via genuinely new notifications, not re-reads of existing ones.
`openclaw cron add --session isolated` with full context (API token, issue - **The wake message tells the agent to route output to Gitea/Mattermost, not to
URL, instructions) baked into the message. Parallel notifications get parallel DM.** This prevents chatty notification processing from disturbing the human.
agents. - **Zero dependencies.** Just Python stdlib (`urllib`, `json`, `time`). Runs
- **Marks notifications as read immediately.** Prevents re-processing. The anywhere.
agent's job is to respond, not to manage notification state.
- **Tracks notification IDs, not counts.** Only fires on genuinely new Here's the full source:
notifications, not re-reads of existing ones.
- **Triage before dispatch.** Not every notification is actionable. The poller
checks: is the agent @-mentioned (in issue body or latest comment)? Is the
issue/PR assigned to the agent? Is the agent's comment already the latest
(no response needed)?
- **Assignment scan as backup.** A secondary loop periodically scans watched
repos for open issues assigned to the agent that were recently updated but
have no agent response. This catches cases where notifications aren't
generated (API-created issues, self-assignment).
- **Strict scope enforcement.** Each spawned agent's prompt includes a SCOPE
constraint: "You are responsible for ONLY this issue. Do NOT touch any other
issues or PRs." This prevents rogue agents from creating unauthorized work.
- **Priority rule.** Agent prompts explicitly state that the user's instructions
in the issue override all boilerplate rules (e.g., if the user asks for a DM
response, the agent should DM).
- **Zero dependencies.** Just Python stdlib. Runs anywhere.
```python ```python
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Gitea notification poller + dispatcher. Gitea notification poller.
Polls for unread notifications and wakes OpenClaw when the count
Two polling loops: changes. The AGENT marks notifications as read after processing —
1. Notification-based: detects new notifications (mentions, assignments) the poller never marks anything as read.
and dispatches agents for actionable ones.
2. Assignment-based: periodically checks for open issues/PRs assigned to
the agent that have no recent response. Catches cases where
notifications aren't generated.
Required env vars: Required env vars:
GITEA_URL - Gitea instance URL GITEA_URL - Gitea instance URL
GITEA_TOKEN - Gitea API token GITEA_TOKEN - Gitea API token
HOOK_TOKEN - OpenClaw hooks auth token
Optional env vars: Optional env vars:
POLL_DELAY - Delay between polls in seconds (default: 15) GATEWAY_URL - OpenClaw gateway URL (default: http://127.0.0.1:18789)
COOLDOWN - Minimum seconds between dispatches (default: 30) POLL_DELAY - Delay between polls in seconds (default: 2)
ASSIGNMENT_INTERVAL - Seconds between assignment scans (default: 120)
OPENCLAW_BIN - Path to openclaw binary
""" """
import json import json
import os import os
import subprocess
import sys import sys
import time import time
import urllib.request import urllib.request
@@ -265,286 +220,117 @@ import urllib.error
GITEA_URL = os.environ.get("GITEA_URL", "").rstrip("/") GITEA_URL = os.environ.get("GITEA_URL", "").rstrip("/")
GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "") GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "")
POLL_DELAY = int(os.environ.get("POLL_DELAY", "15")) GATEWAY_URL = os.environ.get("GATEWAY_URL", "http://127.0.0.1:18789").rstrip(
COOLDOWN = int(os.environ.get("COOLDOWN", "30")) "/"
ASSIGNMENT_INTERVAL = int(os.environ.get("ASSIGNMENT_INTERVAL", "120")) )
OPENCLAW_BIN = os.environ.get("OPENCLAW_BIN", "/opt/homebrew/bin/openclaw") HOOK_TOKEN = os.environ.get("HOOK_TOKEN", "")
POLL_DELAY = int(os.environ.get("POLL_DELAY", "2"))
# Mattermost channel for status updates (customize to your setup)
GIT_CHANNEL = "channel:YOUR_GIT_CHANNEL_ID"
# Repos to scan for assigned issues
WATCHED_REPOS = [
"your-org/repo1",
"your-org/repo2",
]
# Track dispatched issues to prevent duplicates
dispatched_issues = set()
BOT_USERNAME = "your-bot-username" # e.g. "clawbot"
def check_config(): def check_config():
if not GITEA_URL or not GITEA_TOKEN: missing = []
print("ERROR: GITEA_URL and GITEA_TOKEN required", file=sys.stderr) if not GITEA_URL:
missing.append("GITEA_URL")
if not GITEA_TOKEN:
missing.append("GITEA_TOKEN")
if not HOOK_TOKEN:
missing.append("HOOK_TOKEN")
if missing:
print(
f"ERROR: Missing required env vars: {', '.join(missing)}",
file=sys.stderr,
)
sys.exit(1) sys.exit(1)
def gitea_api(method, path, data=None): def gitea_unread_ids():
"""Call Gitea API, return parsed JSON or None on error.""" """Return set of unread notification IDs."""
url = f"{GITEA_URL}/api/v1{path}" req = urllib.request.Request(
body = json.dumps(data).encode() if data else None f"{GITEA_URL}/api/v1/notifications?status-types=unread",
headers = {"Authorization": f"token {GITEA_TOKEN}"} headers={"Authorization": f"token {GITEA_TOKEN}"},
if body: )
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, headers=headers, method=method, data=body)
try: try:
with urllib.request.urlopen(req, timeout=15) as resp: with urllib.request.urlopen(req, timeout=10) as resp:
raw = resp.read() notifs = json.loads(resp.read())
return json.loads(raw) if raw else None return {n["id"] for n in notifs}
except Exception as e: except Exception as e:
print(f"WARN: Gitea API {method} {path}: {e}", print(
file=sys.stderr, flush=True) f"WARN: Gitea API failed: {e}", file=sys.stderr, flush=True
return None
def get_unread_notifications():
result = gitea_api("GET", "/notifications?status-types=unread")
return result if isinstance(result, list) else []
def mark_notification_read(notif_id):
gitea_api("PATCH", f"/notifications/threads/{notif_id}")
def needs_bot_response(repo_full, issue_number):
"""True if bot is NOT the author of the most recent comment."""
comments = gitea_api(
"GET", f"/repos/{repo_full}/issues/{issue_number}/comments"
)
if comments and len(comments) > 0:
if comments[-1].get("user", {}).get("login") == BOT_USERNAME:
return False
return True
def is_actionable_notification(notif):
"""Check if a notification needs agent action.
Returns (actionable, reason, issue_number)."""
subject = notif.get("subject", {})
repo = notif.get("repository", {})
repo_full = repo.get("full_name", "")
url = subject.get("url", "")
number = url.rstrip("/").split("/")[-1] if url else ""
if not number or not number.isdigit():
return False, "no issue number", None
issue = gitea_api("GET", f"/repos/{repo_full}/issues/{number}")
if not issue:
return False, "couldn't fetch issue", number
# Check assignment
assignees = [a.get("login") for a in (issue.get("assignees") or [])]
if BOT_USERNAME in assignees:
if needs_bot_response(repo_full, number):
return True, f"assigned to {BOT_USERNAME}", number
return False, "assigned but already responded", number
# Check issue body for @mention
issue_body = issue.get("body", "") or ""
issue_author = issue.get("user", {}).get("login", "")
if f"@{BOT_USERNAME}" in issue_body and issue_author != BOT_USERNAME:
if needs_bot_response(repo_full, number):
return True, f"@-mentioned in body by {issue_author}", number
# Check latest comment for @mention
comments = gitea_api(
"GET", f"/repos/{repo_full}/issues/{number}/comments"
)
if comments:
last = comments[-1]
author = last.get("user", {}).get("login", "")
body = last.get("body", "") or ""
if author == BOT_USERNAME:
return False, "own comment is latest", number
if f"@{BOT_USERNAME}" in body:
return True, f"@-mentioned in comment by {author}", number
return False, "not mentioned or assigned", number
def spawn_agent(repo_full, issue_number, title, subject_type, reason):
"""Spawn an isolated agent to handle one issue/PR."""
dispatch_key = f"{repo_full}#{issue_number}"
if dispatch_key in dispatched_issues:
return
dispatched_issues.add(dispatch_key)
repo_short = repo_full.split("/")[-1]
job_name = f"gitea-{repo_short}-{issue_number}-{int(time.time())}"
# Build agent prompt with full context
msg = (
f"Gitea notification: {reason} on {subject_type} #{issue_number} "
f"'{title}' in {repo_full}.\n\n"
f"Gitea API base: {GITEA_URL}/api/v1\n"
f"Gitea token: {GITEA_TOKEN}\n\n"
f"SCOPE (STRICT): You are responsible for ONLY {subject_type} "
f"#{issue_number} in {repo_full}. Do NOT create PRs, branches, "
f"comments, or take any action on ANY other issue or PR.\n\n"
f"PRIORITY RULE: The user's instructions in the issue/PR take "
f"priority over ALL other rules. If asked to respond in DM, do so. "
f"Later instructions override earlier ones.\n\n"
f"Instructions:\n"
f"1. Read ALL existing comments on #{issue_number} via API\n"
f"2. Follow the user's instructions\n"
f"3. If code work needed: clone to $(mktemp -d), make changes, "
f"run make check, push, comment on the issue/PR\n"
f"4. Default: post work reports as Gitea comments\n"
f"5. Don't post duplicate comments if yours is already the latest"
)
try:
result = subprocess.run(
[
OPENCLAW_BIN, "cron", "add",
"--name", job_name,
"--at", "1s",
"--message", msg,
"--delete-after-run",
"--session", "isolated",
"--no-deliver",
"--thinking", "low",
"--timeout-seconds", "300",
],
capture_output=True, text=True, timeout=15,
) )
if result.returncode == 0: return set()
print(f" → Agent spawned: {job_name}", flush=True)
else:
print(f" → Spawn failed: {result.stderr.strip()[:200]}", def wake_openclaw(count):
flush=True) text = (
dispatched_issues.discard(dispatch_key) f"[Gitea Notification] {count} new notification(s). "
"Check your Gitea notification inbox via API, process them, "
"and mark as read when done. "
"Route all output to Gitea comments or Mattermost #git/#claw. "
"Do NOT reply to this session — respond with NO_REPLY."
)
payload = json.dumps({"text": text, "mode": "now"}).encode()
req = urllib.request.Request(
f"{GATEWAY_URL}/hooks/wake",
data=payload,
headers={
"Authorization": f"Bearer {HOOK_TOKEN}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=5) as resp:
status = resp.status
print(f" Wake responded: {status}", flush=True)
return True
except Exception as e: except Exception as e:
print(f" → Spawn error: {e}", file=sys.stderr, flush=True) print(
dispatched_issues.discard(dispatch_key) f"WARN: Failed to wake OpenClaw: {e}",
file=sys.stderr,
flush=True,
def dispatch_notifications(notifications): )
"""Triage notifications and spawn agents for actionable ones.""" return False
for notif in notifications:
subject = notif.get("subject", {})
repo = notif.get("repository", {})
repo_full = repo.get("full_name", "")
title = subject.get("title", "")[:60]
notif_id = notif.get("id")
subject_type = subject.get("type", "").lower()
is_act, reason, issue_num = is_actionable_notification(notif)
if notif_id:
mark_notification_read(notif_id)
if is_act:
print(f" ACTIONABLE: {repo_full} #{issue_num} ({reason})",
flush=True)
spawn_agent(repo_full, issue_num, title, subject_type, reason)
else:
print(f" skip: {repo_full} #{issue_num} ({reason})", flush=True)
def scan_assigned_issues():
"""Backup scan: find assigned issues needing response."""
for repo_full in WATCHED_REPOS:
for issue_type in ["issues", "pulls"]:
items = gitea_api(
"GET",
f"/repos/{repo_full}/issues?state=open&type={issue_type}"
f"&assignee={BOT_USERNAME}&sort=updated&limit=10"
)
if not items:
continue
for item in items:
number = str(item["number"])
dispatch_key = f"{repo_full}#{number}"
if dispatch_key in dispatched_issues:
continue
if not needs_bot_response(repo_full, number):
continue
kind = "PR" if issue_type == "pulls" else "issue"
print(f" [assign-scan] {repo_full} {kind} #{number}",
flush=True)
spawn_agent(
repo_full, number, item.get("title", "")[:60],
"pull" if issue_type == "pulls" else "issue",
f"assigned to {BOT_USERNAME}"
)
def main(): def main():
check_config() check_config()
print(f"Gitea poller+dispatcher (poll={POLL_DELAY}s, " print(
f"cooldown={COOLDOWN}s, assign_scan={ASSIGNMENT_INTERVAL}s)", f"Gitea notification poller started (delay={POLL_DELAY}s)",
flush=True) flush=True,
)
seen_ids = set(n["id"] for n in get_unread_notifications()) last_seen_ids = gitea_unread_ids()
last_dispatch_time = 0 print(
last_assign_scan = 0 f"Initial unread: {len(last_seen_ids)} notification(s)", flush=True
print(f"Initial unread: {len(seen_ids)} (draining)", flush=True) )
while True: while True:
time.sleep(POLL_DELAY) time.sleep(POLL_DELAY)
now = time.time()
# Notification polling current_ids = gitea_unread_ids()
notifications = get_unread_notifications() new_ids = current_ids - last_seen_ids
current_ids = {n["id"] for n in notifications}
new_ids = current_ids - seen_ids
if new_ids: if not new_ids:
ts = time.strftime("%H:%M:%S") last_seen_ids = current_ids
new_notifs = [n for n in notifications if n["id"] in new_ids] continue
print(f"[{ts}] {len(new_ids)} new notification(s)", flush=True)
if now - last_dispatch_time >= COOLDOWN:
dispatch_notifications(new_notifs)
last_dispatch_time = now
else:
remaining = int(COOLDOWN - (now - last_dispatch_time))
print(f" → Cooldown ({remaining}s remaining)", flush=True)
seen_ids = current_ids ts = time.strftime("%H:%M:%S")
print(
f"[{ts}] {len(new_ids)} new notification(s) "
f"({len(current_ids)} total unread), waking agent",
flush=True,
)
# Assignment scan (less frequent) wake_openclaw(len(new_ids))
if now - last_assign_scan >= ASSIGNMENT_INTERVAL: last_seen_ids = current_ids
scan_assigned_issues()
last_assign_scan = now
if __name__ == "__main__": if __name__ == "__main__":
main() main()
``` ```
Run it as a background service (launchd on macOS, systemd on Linux) with Run it as a background service (launchd on macOS, systemd on Linux) with the env
`GITEA_URL` and `GITEA_TOKEN` set. Customize `WATCHED_REPOS`, `BOT_USERNAME`, vars set. It's intentionally simple — no frameworks, no async, no dependencies.
and `GIT_CHANNEL` for your setup. It's intentionally simple — no frameworks,
no async, no dependencies.
**Lessons learned during development:**
- `openclaw cron add --at` uses formats like `1s`, `20m` — not `+5s` or `+0s`.
- `--no-deliver` is incompatible with `--session main`. Use
`--session isolated` with `--no-deliver`.
- `--system-event` targets the main DM session. If your agent is active in a
channel session, it won't see system events. Use `--session isolated` with
`--message` instead.
- Isolated agent sessions don't have access to workspace files (TOOLS.md, etc).
Bake all credentials and instructions directly into the agent prompt.
- Agents WILL go out of scope unless the SCOPE constraint is extremely explicit
and uses strong language ("violating scope is a critical failure").
- When the user's explicit instructions in an issue conflict with boilerplate
rules in the agent prompt, the agent will follow the boilerplate unless the
prompt explicitly says "user instructions take priority."
### The Daily Diary ### The Daily Diary
@@ -582,15 +368,13 @@ This applies to everything: project rules ("no mocks in tests"), workflow
preferences ("fewer PRs, don't over-split"), corrections, new policies. preferences ("fewer PRs, don't over-split"), corrections, new policies.
Immediate write to the daily file, and to MEMORY.md if it's a standing rule. Immediate write to the daily file, and to MEMORY.md if it's a standing rule.
### Sensitive Output Routing ### PII-Aware Output Routing
A lesson learned the hard way: **the audience determines what you can say, not A lesson learned the hard way: **the audience determines what you can say, not
who asked.** If the human asks for a medication status report in a group who asked.** If the human asks for a medication status report in a group
channel, the agent can't just dump it there — other people can read it. The channel, the agent can't just dump it there — other people can read it. The
rule: if the output would contain sensitive information (PII, secrets, rule: if the output would contain PII and the channel isn't private, redirect to
credentials, API keys, operational details like flight numbers, locations, DM and reply in-channel with "sent privately."
travel plans, medical info, etc.) and the channel isn't private, redirect to DM
and reply in-channel with "sent privately."
This is enforced at multiple levels: This is enforced at multiple levels:
@@ -621,7 +405,7 @@ The heartbeat handles:
- Periodic memory maintenance - Periodic memory maintenance
State tracking in `memory/heartbeat-state.json` prevents redundant checks (e.g., State tracking in `memory/heartbeat-state.json` prevents redundant checks (e.g.,
don't re-check notifications if you checked 10 minutes ago). don't re-check email if you checked 10 minutes ago).
The key output rule: heartbeats should either be `HEARTBEAT_OK` (nothing to do) The key output rule: heartbeats should either be `HEARTBEAT_OK` (nothing to do)
or a direct alert. Work narration goes to a designated status channel, never to or a direct alert. Work narration goes to a designated status channel, never to
@@ -1633,8 +1417,7 @@ stay quiet.
## Inbox Check (PRIORITY) ## Inbox Check (PRIORITY)
(check whatever notification sources apply to your setup — e.g. Gitea (check notifications, issues, emails — whatever applies)
notifications, emails, issue trackers)
## Flight Prep Blocks (daily) ## Flight Prep Blocks (daily)
@@ -1668,9 +1451,10 @@ Never send internal thinking or status narration to user's DM. Output should be:
```json ```json
{ {
"lastChecks": { "lastChecks": {
"gitea": 1703280000, "email": 1703275200,
"calendar": 1703260800, "calendar": 1703260800,
"weather": null "weather": null,
"gitea": 1703280000
}, },
"lastWeeklyDocsReview": "2026-02-24" "lastWeeklyDocsReview": "2026-02-24"
} }
@@ -1698,9 +1482,51 @@ Never send internal thinking or status narration to user's DM. Output should be:
## Gitea Integration & Notification Polling ## Gitea Integration & Notification Polling
For self-hosted Gitea instances, you can set up a notification poller that For self-hosted Gitea instances, you need a way to deliver notifications (issue
injects Gitea events (issue assignments, PR reviews, @-mentions) into the assignments, PR reviews, @-mentions) to your agent. There are two approaches,
agent's session. depending on your network setup.
### Notification Delivery: Webhooks vs Polling
#### 1. Direct webhooks (VPS / public server)
If your OpenClaw instance runs on a VPS or other publicly reachable server, you
can run Traefik (or any reverse proxy) on the same server and configure Gitea
webhooks to POST directly to OpenClaw's webhook endpoint. This is push-based and
realtime — notifications arrive instantly.
Set up a Gitea webhook (per-repo or org-wide) pointing at your OpenClaw
instance's `/hooks/wake` endpoint. Gitea sends a POST on every event, and the
agent wakes immediately to process it.
#### 2. Notification poller (local machine behind NAT)
If your OpenClaw instance runs on a dedicated local machine behind NAT (like a
home Mac or Linux box), Gitea can't reach it directly. In this case, use a
lightweight polling script that checks the Gitea notifications API every few
seconds and signals the agent when new notifications arrive.
This is the approach we use — OpenClaw runs on a dedicated Mac Studio on a home
LAN, so we poll Gitea's notification API and wake the agent via the local
`/hooks/wake` endpoint when new notifications appear. The poller script is
included below in the [Notification poller](#notification-poller) section.
The poller approach trades ~30 seconds of latency (polling interval) for
simplicity and no NAT/firewall configuration. For most workflows this is
perfectly fine — code review and issue triage don't need sub-second response
times. If no new notifications arrive between heartbeats, the effective latency
is bounded by the heartbeat interval (~30 minutes), but in practice the poller
catches most events within seconds.
#### Which should you choose?
| Factor | Webhooks | Poller |
| ------------------- | ------------------- | ------------------------- |
| Network requirement | Public IP / domain | None (outbound-only) |
| Latency | Instant | ~2-30s (polling interval) |
| Setup complexity | Reverse proxy + TLS | Single background script |
| Dependencies | Traefik/nginx/Caddy | Python stdlib only |
| Best for | VPS / cloud deploys | Home LAN / NAT setups |
### Workflow rules (HEARTBEAT.md / AGENTS.md): ### Workflow rules (HEARTBEAT.md / AGENTS.md):
@@ -1839,24 +1665,21 @@ Never lose a rule or preference your human states:
--- ---
## Sensitive Output Routing — Audience-Aware Responses ## PII Output Routing — Audience-Aware Responses
A critical security pattern: **the audience determines what you can say, not who A critical security pattern: **the audience determines what you can say, not who
asked.** If your human asks for a sitrep (or any sensitive info) in a group asked.** If your human asks for a sitrep (or any PII-containing info) in a group
channel, you can't just dump it there — other people can read it. channel, you can't just dump it there — other people can read it.
### AGENTS.md / checklist prompt: ### AGENTS.md / checklist prompt:
```markdown ```markdown
## Sensitive Output Routing (CRITICAL) ## PII Output Routing (CRITICAL)
- NEVER output sensitive information in any non-private channel, even if your - NEVER output PII in any non-private channel, even if your human asks for it
human asks for it - If a request would produce PII (medication status, travel details, financial
- This includes: PII, secrets, credentials, API keys, and sensitive operational info, etc.) in a shared channel: send the response via DM instead, and reply
information (flight numbers/times/dates, locations, travel plans, medical in-channel with "sent privately"
info, financial details, etc.)
- If a request would produce any of the above in a shared channel: send the
response via DM instead, and reply in-channel with "sent privately"
- The rule is: the audience determines what you can say, not who asked - The rule is: the audience determines what you can say, not who asked
- This applies to: group chats, public issue trackers, shared Mattermost - This applies to: group chats, public issue trackers, shared Mattermost
channels, Discord servers — anywhere that isn't a 1:1 DM channels, Discord servers — anywhere that isn't a 1:1 DM
@@ -1865,10 +1688,10 @@ channel, you can't just dump it there — other people can read it.
### Why this matters: ### Why this matters:
This is a real failure mode. If someone asks "sitrep" in a group channel and you This is a real failure mode. If someone asks "sitrep" in a group channel and you
respond with medication names, partner details, travel dates, hotel names, or respond with medication names, partner details, travel dates, and hotel names
API credentials — you just leaked all of that to everyone in the channel. The you just leaked all of that to everyone in the channel. The human asking is
human asking is authorized to see it; the channel audience is not. Always check authorized to see it; the channel audience is not. Always check WHERE you're
WHERE you're responding, not just WHO asked. responding, not just WHO asked.
--- ---

File diff suppressed because it is too large Load Diff