* feat(gateway): add auth rate-limiting & brute-force protection
Add a per-IP sliding-window rate limiter to Gateway authentication
endpoints (HTTP, WebSocket upgrade, and WS message-level auth).
When gateway.auth.rateLimit is configured, failed auth attempts are
tracked per client IP. Once the threshold is exceeded within the
sliding window, further attempts are blocked with HTTP 429 + Retry-After
until the lockout period expires. Loopback addresses are exempt by
default so local CLI sessions are never locked out.
The limiter is only created when explicitly configured (undefined
otherwise), keeping the feature fully opt-in and backward-compatible.
* fix(gateway): isolate auth rate-limit scopes and normalize 429 responses
---------
Co-authored-by: buerbaumer <buerbaumer@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Older OpenClaw versions stored absolute sessionFile paths in sessions.json.
v2026.2.12 added path traversal security that rejected these absolute paths,
breaking all Telegram group handlers with 'Session file path must be within
sessions directory' errors.
Changes:
- resolvePathWithinSessionsDir() now normalizes absolute paths that resolve
within the sessions directory, converting them to relative before validation
- Added 3 tests for absolute path handling (within dir, with topic, outside dir)
Fixes#15283Closes#15214, #15237, #15216, #15152, #15213
Escape regex metacharacters in display names before constructing RegExp
to prevent runtime errors or unintended matches when names contain special
characters like (, ), ., +, ?, [, etc.
Add test coverage for names with regex metacharacters.
Clarify that User.Read.All permission is only needed for searching
users not in the current conversation. Mentions work out of the box
for conversation participants.
- Add mention parsing and validation logic
- Handle mention entities with proper whitespace
- Validate mention IDs to prevent false positives from code snippets
- Use fake placeholders in tests for privacy
* Agents: allow gpt-5.3-codex-spark in fallback and thinking
* Fix: model picker issue for openai-codex/gpt-5.3-codex-spark
Fixed an issue in the model picker.
* feat(slack): populate thread session with existing thread history
When a new session is created for a Slack thread, fetch and inject
the full thread history as context. This preserves conversation
continuity so the bot knows what it previously said in the thread.
- Add resolveSlackThreadHistory() to fetch all thread messages
- Add ThreadHistoryBody to context payload
- Use thread history instead of just thread starter for new sessions
Fixes#4470
* chore: remove redundant comments
* fix: use threadContextNote in queue body
* fix(slack): address Greptile review feedback
- P0: Use thread session key (not base session key) for new-session check
This ensures thread history is injected when the thread session is new,
even if the base channel session already exists.
- P1: Fetch up to 200 messages and take the most recent N
Slack API returns messages in chronological order (oldest first).
Previously we took the first N, now we take the last N for relevant context.
- P1: Batch resolve user names with Promise.all
Avoid N sequential API calls when resolving user names in thread history.
- P2: Include file-only messages in thread history
Messages with attachments but no text are now included with a placeholder
like '[attached: image.png, document.pdf]'.
- P2: Add documentation about intentional 200-message fetch limit
Clarifies that we intentionally don't paginate; 200 covers most threads.
* style: add braces for curly lint rule
* feat(slack): add thread.initialHistoryLimit config option
Allow users to configure the maximum number of thread messages to fetch
when starting a new thread session. Defaults to 20. Set to 0 to disable
thread history fetching entirely.
This addresses the optional configuration request from #2608.
* chore: trigger CI
* fix(slack): ensure isNewSession=true on first thread turn
recordInboundSession() in prepare.ts creates the thread session entry
before session.ts reads the store, causing isNewSession to be false
on the very first user message in a thread. This prevented thread
context (history/starter) from being injected.
Add IsFirstThreadTurn flag to message context, set when
readSessionUpdatedAt() returns undefined for the thread session key.
session.ts uses this flag to force isNewSession=true.
* style: format prepare.ts for oxfmt
* fix: suppress InboundHistory/ThreadStarterBody when ThreadHistoryBody present (#13912)
When ThreadHistoryBody is fetched from the Slack API (conversations.replies),
it already contains pending messages and the thread starter. Passing both
InboundHistory and ThreadStarterBody alongside ThreadHistoryBody caused
duplicate content in the LLM context on new thread sessions.
Suppress InboundHistory and ThreadStarterBody when ThreadHistoryBody is
present, since it is a strict superset of both.
* remove verbose comment
* fix(slack): paginate thread history context fetch
* fix(slack): wire session file path options after main merge
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
replyToMode "first"/"all" only filters replyToId but never generates
it — that required the LLM to emit [[reply_to_current]] tags. Inject
replyToCurrent:true on all payloads so applyReplyTagsToPayload sets
replyToId=currentMessageId, then let the existing mode filter decide
which replies keep threading (first only, all, or off).
Covers both final reply path (reply-payloads.ts) and block streaming
path (agent-runner-execution.ts).
Adds thread_ts and parent_user_id to the Slack message footer for thread
replies, giving agents awareness of thread context. Top-level messages
remain unchanged.
Includes tests verifying:
- Thread replies include thread_ts and parent_user_id in footer
- Top-level messages exclude thread metadata
* fix: replace file-based session store lock with in-process Promise chain mutex
Node.js is single-threaded, so file-based locking (open('wx') + polling +
stale eviction) is unnecessary and causes timeouts under heavy session load.
Replace with a simple per-storePath Promise chain that serializes access
without any filesystem overhead.
In a 1159-session environment over 3 hours:
- Lock timeouts: 25
- Stuck sessions: 157 (max 1031s, avg 388s)
- Slow listeners: 39 (max 265s, avg 70s)
Root cause: during sessions.json file I/O, await yields control and other
lock requests hit the 10s timeout waiting for the .lock file to be released.
* test: add comprehensive tests for Promise chain mutex lock
- Concurrent access serialization (10 parallel writers, counter integrity)
- Error resilience (single & multiple consecutive throws don't poison queue)
- Independent storePath parallelism (different paths run concurrently)
- LOCK_QUEUES cleanup after completion and after errors
- No .lock file created on disk
Also fix: store caught promise in LOCK_QUEUES to avoid unhandled rejection
warnings when queued fn() throws.
* fix: add timeout to Promise chain mutex to prevent infinite hangs on Windows
* fix(session-store): enforce strict queue timeout + cross-process lock
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(security): distinguish webhooks from internal hooks in audit summary
The attack surface summary reported a single 'hooks: disabled/enabled' line
that only checked the external webhook endpoint (hooks.enabled), ignoring
internal hooks (hooks.internal.enabled). Users who enabled internal hooks
(session-memory, command-logger, etc.) saw 'hooks: disabled' and thought
something was broken.
Split into two separate lines:
- hooks.webhooks: disabled/enabled
- hooks.internal: disabled/enabled
Fixes#13466
* test(security): move attack surface tests to focused test file
Move the 3 new hook-distinction tests from the monolithic audit.test.ts
(1,511 lines) into a dedicated audit-extra.sync.test.ts that tests
collectAttackSurfaceSummaryFindings directly. Avoids growing the
already-large test file and keeps tests focused on the changed unit.
* fix: add changelog entry for security audit hook split (#13474) (thanks @mcaxtr)
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
The newly added 'resolved' field contains secrets after ${ENV}
substitution. This commit ensures redactConfigSnapshot also redacts
the resolved field to prevent credential leaks in config.get responses.
The initial fix using snapshot.parsed broke configs with $include directives.
This commit adds a new 'resolved' field to ConfigFileSnapshot that contains
the config after $include and ${ENV} substitution but BEFORE runtime defaults
are applied. This is now used by config set/unset to avoid:
1. Breaking configs with $include directives
2. Leaking runtime defaults into the written config file
Also removes applyModelDefaults from writeConfigFile since runtime defaults
should only be applied when loading, not when writing.
Fixes#6070
The config set/unset commands were using snapshot.config (which contains
runtime-merged defaults) instead of snapshot.parsed (the raw user config).
This caused runtime defaults like agents.defaults to leak into the written
config file when any value was set or unset.
Changed both set and unset commands to use structuredClone(snapshot.parsed)
to preserve only user-specified config values.