- mergeAccessPolicy !base fast-path: scripts["policy"] and per-script entry.policy were
shallow-copied, leaving them as references into the cached _fileCache object.
autoExpandBareDir mutations would propagate back into the cache, violating the invariant
established by the policy-copy fix. Now deep-copied via Object.fromEntries map.
- exec-sandbox-seatbelt: replace hardcoded "file-write*" with SEATBELT_WRITE_OPS constant
in the /tmp write allowance branch, consistent with all other allowance lines in the file.
- Tests added for nested scripts deep-copy invariant.
- bwrap: '---' rules on SYSTEM_RO_BIND_PATHS (/etc /usr /bin /lib /sbin /opt) now emit
--tmpfs in restrictive mode — previously the deny branch was gated to permissive mode
only, leaving syscalls inside the sandbox able to read /etc/passwd etc. despite policy
- seatbelt: bracket globs [abc] now detected as wildcards (/[*?[]/ and strip regex updated);
previously emitted as SBPL literals matching only a file literally named '[abc]'
- access-policy-file: mergeAccessPolicy fast-path (!base) returns shallow copy instead of
reference — autoExpandBareDir was mutating the cached agents['*'].policy in-place,
corrupting all subsequent resolveAccessPolicyForAgent calls in the same process
- access-policy: sha256 comparison normalizes to lowercase (.toLowerCase()) — validation
regex accepts uppercase (/i) but crypto.digest always returns lowercase, causing uppercase
sha256 in config to silently deny exec at runtime with no useful error
- Tests added for all four findings
- permAllowsWrite (bwrap), permToOps/deniedOps (seatbelt): guard all positional perm accesses with VALID_PERM_RE
- catchAllPerm/tmpPerm (seatbelt): validate rawPerm before positional access; fail closed to '---'
- hasScriptOverride (exec-runtime): check entry shape (non-null object, not array) before setting bypass flag
- scripts["policy"] merged into overrideRules in applyScriptPolicyOverride (was silently dropped)
- mergeAccessPolicy: reject non-object script entries before propagating
- validateAccessPolicyFileStructure: recurse into per-script entries to catch removed deny/default fields
- validateAccessPolicyConfig: reject non-object entries, validate sha256 format, emit mid-path wildcard
diagnostics for scripts["policy"] AND per-script policy blocks (previously only config.policy)
- env-prefix regex: handle escaped quotes in double-quoted values ((?:[^"\\]|\\.)*)
- _resetBwrapAvailableCacheForTest: export added for test isolation
- Tests added for all of the above
* fix(feishu): fetch thread context so AI can see bot replies in topic threads
When a user replies in a Feishu topic thread, the AI previously could only
see the quoted parent message but not the bot's own prior replies in the
thread. This made multi-turn conversations in threads feel broken.
- Add `threadId` (omt_xxx) to `FeishuMessageInfo` and `getMessageFeishu`
- Add `listFeishuThreadMessages()` using `container_id_type=thread` API
to fetch all messages in a thread including bot replies
- In `handleFeishuMessage`, fetch ThreadStarterBody and ThreadHistoryBody
for topic session modes and pass them to the AI context
- Reuse quoted message result when rootId === parentId to avoid redundant
API calls; exclude root message from thread history to prevent duplication
- Fall back to inbound ctx.threadId when rootId is absent or API fails
- Fetch newest messages first (ByCreateTimeDesc + reverse) so long threads
keep the most recent turns instead of the oldest
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): skip redundant thread context injection on subsequent turns
Only inject ThreadHistoryBody on the first turn of a thread session.
On subsequent turns the session already contains prior context, so
re-injecting thread history (and starter) would waste tokens.
The heuristic checks whether the current user has already sent a
non-root message in the thread — if so, the session has prior turns
and thread context injection is skipped entirely.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): handle thread_id-only events in prior-turn detection
When ctx.rootId is undefined (thread_id-only events), the starter
message exclusion check `msg.messageId !== ctx.rootId` was always
true, causing the first follow-up to be misclassified as a prior
turn. Fall back to the first message in the chronologically-sorted
thread history as the starter.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): bootstrap topic thread context via session state
* test(memory): pin remote embedding hostnames in offline suites
* fix(feishu): use plugin-safe session runtime for thread bootstrap
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
Fixes#46142
Stop forcing supportsUsageInStreaming=false on non-native openai-completions
endpoints. Most OpenAI-compatible APIs (DashScope, DeepSeek, Groq, Together,
etc.) handle stream_options: { include_usage: true } correctly. The blanket
disable broke usage/cost tracking for all non-OpenAI providers.
supportsDeveloperRole is still forced off for non-native endpoints since
the developer message role is genuinely OpenAI-specific.
Users on backends that reject stream_options can opt out with
compat.supportsUsageInStreaming: false in their model config.
Fixes#46142
Fixes#43057
* fix(auth): clear stale lockout on re-login
Clear stale `auth_permanent` and `billing` disabled state for all
profiles matching the target provider when `openclaw models auth login`
is invoked, so users locked out by expired or revoked OAuth tokens can
recover by re-authenticating instead of waiting for the cooldown timer.
Uses the agent-scoped store (`loadAuthProfileStoreForRuntime`) for
correct multi-agent profile resolution and wraps the housekeeping in
try/catch so corrupt store files never block re-authentication.
Fixes#43057
* test(auth): remove unnecessary non-null assertions
oxlint no-unnecessary-type-assertion: invocationCallOrder[0]
already returns number, not number | undefined.
Fixes#42931
When gateway.auth.mode is set to "none", authentication succeeds with
method "none" but sharedAuthOk remains false because the auth-context
only recognises token/password/trusted-proxy methods. This causes all
pairing-skip conditions to fail, so Control UI browser connections get
closed with code 1008 "pairing required" despite auth being disabled.
Short-circuit the skipPairing check: if the operator explicitly
disabled authentication, device pairing (which is itself an auth
mechanism) must also be bypassed.
Fixes#42931
Fixes#43322
* fix(feishu): clear stale streamingStartPromise on card creation failure
When FeishuStreamingSession.start() throws (HTTP 400), the catch block
sets streaming = null but leaves streamingStartPromise dangling. The
guard in startStreaming() checks streamingStartPromise first, so all
future deliver() calls silently skip streaming - the session locks
permanently.
Clear streamingStartPromise in the catch block so subsequent messages
can retry streaming instead of dropping all future replies.
Fixes#43322
* test(feishu): wrap push override in try/finally for cleanup safety