Merge branch 'main' into codex/cortex-openclaw-integration
This commit is contained in:
commit
98ac50f798
10
CHANGELOG.md
10
CHANGELOG.md
@ -4,6 +4,8 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 2026.3.12
|
||||
|
||||
### Changes
|
||||
|
||||
- Agents/subagents: add `sessions_yield` so orchestrators can end the current turn immediately, skip queued tool work, and carry a hidden follow-up payload into the next session turn. (#36537) thanks @jriff
|
||||
@ -15,6 +17,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Models/OpenAI Codex Spark: keep `gpt-5.3-codex-spark` working on the `openai-codex/*` path via resolver fallbacks and clearer Codex-only handling, while continuing to suppress the stale direct `openai/*` Spark row that OpenAI rejects live.
|
||||
- Ollama/Kimi Cloud: apply the Moonshot Kimi payload compatibility wrapper to Ollama-hosted Kimi models like `kimi-k2.5:cloud`, so tool routing no longer breaks when thinking is enabled. (#41519) Thanks @vincentkoc.
|
||||
- Models/Kimi Coding: send the built-in `User-Agent: claude-code/0.1.0` header by default for `kimi-coding` while still allowing explicit provider headers to override it, so Kimi Code subscription auth can work without a local header-injection proxy. (#30099) Thanks @Amineelfarssi and @vincentkoc.
|
||||
- Security/device pairing: switch `/pair` and `openclaw qr` setup codes to short-lived bootstrap tokens so the next release no longer embeds shared gateway credentials in chat or QR pairing payloads. Thanks @lintsinghua.
|
||||
@ -38,6 +41,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Windows/native update: make package installs use the npm update path instead of the git path, carry portable Git into native Windows updates, and mirror the installer's Windows npm env so `openclaw update` no longer dies early on missing `git` or `node-llama-cpp` download setup.
|
||||
- Sandbox/write: preserve pinned mutation-helper payload stdin so sandboxed `write` no longer reports success while creating empty files. (#43876) Thanks @glitch418x.
|
||||
- Security/exec approvals: escape invisible Unicode format characters in approval prompts so zero-width command text renders as visible `\u{...}` escapes instead of spoofing the reviewed command. (`GHSA-pcqg-f7rg-xfvv`)(#43687) Thanks @EkiXu and @vincentkoc.
|
||||
- Hooks/loader: fail closed when workspace hook paths cannot be resolved with `realpath`, so unreadable or broken internal hook paths are skipped instead of falling back to unresolved imports. (#44437) Thanks @vincentkoc.
|
||||
- Hooks/agent deliveries: dedupe repeated hook requests by optional idempotency key so webhook retries can reuse the first run instead of launching duplicate agent executions. (#44438) Thanks @vincentkoc.
|
||||
- Security/exec detection: normalize compatibility Unicode and strip invisible formatting code points before obfuscation checks so zero-width and fullwidth command tricks no longer suppress heuristic detection. (`GHSA-9r3v-37xh-2cf6`)(#44091) Thanks @wooluo and @vincentkoc.
|
||||
- Security/exec allowlist: preserve POSIX case sensitivity and keep `?` within a single path segment so exact-looking allowlist patterns no longer overmatch executables across case or directory boundaries. (`GHSA-f8r2-vg7x-gh8m`)(#43798) Thanks @zpbrent and @vincentkoc.
|
||||
@ -57,6 +61,8 @@ Docs: https://docs.openclaw.ai
|
||||
- Security/Feishu reactions: preserve looked-up group chat typing and fail closed on ambiguous reaction context so group authorization and mention gating cannot be bypassed through synthetic `p2p` reactions. (`GHSA-m69h-jm2f-2pv8`)(#44088) Thanks @zpbrent and @vincentkoc.
|
||||
- Security/LINE webhook: require signatures for empty-event POST probes too so unsigned requests no longer confirm webhook reachability with a `200` response. (`GHSA-mhxh-9pjm-w7q5`)(#44090) Thanks @TerminalsandCoffee and @vincentkoc.
|
||||
- Security/Zalo webhook: rate limit invalid secret guesses before auth so weak webhook secrets cannot be brute-forced through unauthenticated churned requests without pre-auth `429` responses. (`GHSA-5m9r-p9g7-679c`)(#44173) Thanks @zpbrent and @vincentkoc.
|
||||
- Security/Zalouser groups: require stable group IDs for allowlist auth by default and gate mutable group-name matching behind `channels.zalouser.dangerouslyAllowNameMatching`. Thanks @zpbrent.
|
||||
- Security/Slack and Teams routing: require stable channel and team IDs for allowlist routing by default, with mutable name matching only via each channel's `dangerouslyAllowNameMatching` break-glass flag.
|
||||
- Security/exec approvals: fail closed for ambiguous inline loader and shell-payload script execution, bind the real script after POSIX shell value-taking flags, and unwrap `pnpm`/`npm exec`/`npx` script runners before approval binding. (`GHSA-57jw-9722-6rf2`)(`GHSA-jvqh-rfmh-jh27`)(`GHSA-x7pp-23xv-mmr4`)(`GHSA-jc5j-vg4r-j5jx`)(#44247) Thanks @tdjackey and @vincentkoc.
|
||||
- Doctor/gateway service audit: canonicalize service entrypoint paths before comparing them so symlink-vs-realpath installs no longer trigger false "entrypoint does not match the current install" repair prompts. (#43882) Thanks @ngutman.
|
||||
- Doctor/gateway service audit: earlier groundwork for this fix landed in the superseded #28338 branch. Thanks @realriphub.
|
||||
@ -65,12 +71,14 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/failover: classify z.ai `network_error` stop reasons as retryable timeouts so provider connectivity failures trigger fallback instead of surfacing raw unhandled-stop-reason errors. (#43884) Thanks @hougangdev.
|
||||
- Memory/session sync: add mode-aware post-compaction session reindexing with `agents.defaults.compaction.postIndexSync` plus `agents.defaults.memorySearch.sync.sessions.postCompactionForce`, so compacted session memory can refresh immediately without forcing every deployment into synchronous reindexing. (#25561) thanks @rodrigouroz.
|
||||
- Telegram/model picker: make inline model button selections persist the chosen session model correctly, clear overrides when selecting the configured default, and include effective fallback models in `/models` button validation. (#40105) Thanks @avirweb.
|
||||
- Telegram/native command sync: suppress expected `BOT_COMMANDS_TOO_MUCH` retry error noise, add a final fallback summary log, and document the difference between command-menu overflow and real Telegram network failures.
|
||||
- Mattermost/reply media delivery: pass agent-scoped `mediaLocalRoots` through shared reply delivery so allowed local files upload correctly from button, slash-command, and model-picker replies. (#44021) Thanks @LyleLiu666.
|
||||
- Plugins/env-scoped roots: fix plugin discovery/load caches and provenance tracking so same-process `HOME`/`OPENCLAW_HOME` changes no longer reuse stale plugin state or misreport `~/...` plugins as untracked. (#44046) thanks @gumadeiras.
|
||||
- Gateway/session discovery: discover disk-only and retired ACP session stores under custom templated `session.store` roots so ACP reconciliation, session-id/session-label targeting, and run-id fallback keep working after restart. (#44176) thanks @gumadeiras.
|
||||
- Models/OpenRouter native ids: canonicalize native OpenRouter model keys across config writes, runtime lookups, fallback management, and `models list --plain`, and migrate legacy duplicated `openrouter/openrouter/...` config entries forward on write.
|
||||
- Gateway/hooks: bucket hook auth failures by forwarded client IP behind trusted proxies and warn when `hooks.allowedAgentIds` leaves hook routing unrestricted.
|
||||
- Agents/compaction: skip the post-compaction `cache-ttl` marker write when a compaction completed in the same attempt, preventing the next turn from immediately triggering a second tiny compaction. (#28548) thanks @MoerAI.
|
||||
- Native chat/macOS: add `/new`, `/reset`, and `/clear` reset triggers, keep shared main-session aliases aligned, and ignore stale model-selection completions so native chat state stays in sync across reset and fast model changes. (#10898) Thanks @Nachx639.
|
||||
|
||||
## 2026.3.11
|
||||
|
||||
@ -106,6 +114,8 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- Windows/install: stop auto-installing `node-llama-cpp` during normal npm CLI installs so `openclaw@latest` no longer fails on Windows while building optional local-embedding dependencies.
|
||||
- Windows/update: mirror the native installer environment during global npm updates, including portable Git fallback and Windows-safe npm shell settings, so `openclaw update` works again on native Windows installs.
|
||||
- Gateway/status: expose `runtimeVersion` in gateway status output so install/update smoke tests can verify the running version before and after updates.
|
||||
- Agents/text sanitization: strip leaked model control tokens (`<|...|>` and full-width `<|...|>` variants) from user-facing assistant text, preventing GLM-5 and DeepSeek internal delimiters from reaching end users. (#42173) Thanks @imwyvern.
|
||||
- iOS/gateway foreground recovery: reconnect immediately on foreground return after stale background sockets are torn down, so the app no longer stays disconnected until a later wake path happens. (#41384) Thanks @mbelinky.
|
||||
- Gateway/Control UI: keep dashboard auth tokens in session-scoped browser storage so same-tab refreshes preserve remote token auth without restoring long-lived localStorage token persistence, while scoping tokens to the selected gateway URL and fragment-only bootstrap flow. (#40892) thanks @velvet-shark.
|
||||
|
||||
@ -63,8 +63,8 @@ android {
|
||||
applicationId = "ai.openclaw.app"
|
||||
minSdk = 31
|
||||
targetSdk = 36
|
||||
versionCode = 202603110
|
||||
versionName = "2026.3.11"
|
||||
versionCode = 202603120
|
||||
versionName = "2026.3.12"
|
||||
ndk {
|
||||
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
|
||||
@ -65,9 +65,9 @@ Release behavior:
|
||||
- Beta release also switches the app to `OpenClawPushTransport=relay`, `OpenClawPushDistribution=official`, and `OpenClawPushAPNsEnvironment=production`.
|
||||
- The beta flow does not modify `apps/ios/.local-signing.xcconfig` or `apps/ios/LocalSigning.xcconfig`.
|
||||
- Root `package.json.version` is the only version source for iOS.
|
||||
- A root version like `2026.3.11-beta.1` becomes:
|
||||
- `CFBundleShortVersionString = 2026.3.11`
|
||||
- `CFBundleVersion = next TestFlight build number for 2026.3.11`
|
||||
- A root version like `2026.3.12-beta.1` becomes:
|
||||
- `CFBundleShortVersionString = 2026.3.12`
|
||||
- `CFBundleVersion = next TestFlight build number for 2026.3.12`
|
||||
|
||||
Required env for beta builds:
|
||||
|
||||
|
||||
@ -39,6 +39,13 @@ struct IOSGatewayChatTransport: OpenClawChatTransport, Sendable {
|
||||
// (chat.subscribe is a node event, not an operator RPC method.)
|
||||
}
|
||||
|
||||
func resetSession(sessionKey: String) async throws {
|
||||
struct Params: Codable { var key: String }
|
||||
let data = try JSONEncoder().encode(Params(key: sessionKey))
|
||||
let json = String(data: data, encoding: .utf8)
|
||||
_ = try await self.gateway.request(method: "sessions.reset", paramsJSON: json, timeoutSeconds: 10)
|
||||
}
|
||||
|
||||
func requestHistory(sessionKey: String) async throws -> OpenClawChatHistoryPayload {
|
||||
struct Params: Codable { var sessionKey: String }
|
||||
let data = try JSONEncoder().encode(Params(sessionKey: sessionKey))
|
||||
|
||||
@ -26,5 +26,10 @@ import Testing
|
||||
_ = try await transport.requestHealth(timeoutMs: 250)
|
||||
Issue.record("Expected requestHealth to throw when gateway not connected")
|
||||
} catch {}
|
||||
|
||||
do {
|
||||
try await transport.resetSession(sessionKey: "node-test")
|
||||
Issue.record("Expected resetSession to throw when gateway not connected")
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,7 +99,7 @@ def normalize_release_version(raw_value)
|
||||
version = raw_value.to_s.strip.sub(/\Av/, "")
|
||||
UI.user_error!("Missing root package.json version.") unless env_present?(version)
|
||||
unless version.match?(/\A\d+\.\d+\.\d+(?:[.-]?beta[.-]\d+)?\z/i)
|
||||
UI.user_error!("Invalid package.json version '#{raw_value}'. Expected 2026.3.11 or 2026.3.11-beta.1.")
|
||||
UI.user_error!("Invalid package.json version '#{raw_value}'. Expected 2026.3.12 or 2026.3.12-beta.1.")
|
||||
end
|
||||
|
||||
version
|
||||
|
||||
@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.3.11</string>
|
||||
<string>2026.3.12</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>202603110</string>
|
||||
<string>202603120</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>OpenClaw</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@ -59,7 +59,23 @@ struct MacGatewayChatTransport: OpenClawChatTransport {
|
||||
method: "sessions.list",
|
||||
params: params,
|
||||
timeoutMs: 15000)
|
||||
return try JSONDecoder().decode(OpenClawChatSessionsListResponse.self, from: data)
|
||||
let decoded = try JSONDecoder().decode(OpenClawChatSessionsListResponse.self, from: data)
|
||||
let mainSessionKey = await GatewayConnection.shared.cachedMainSessionKey()
|
||||
let defaults = decoded.defaults.map {
|
||||
OpenClawChatSessionsDefaults(
|
||||
model: $0.model,
|
||||
contextTokens: $0.contextTokens,
|
||||
mainSessionKey: mainSessionKey)
|
||||
} ?? OpenClawChatSessionsDefaults(
|
||||
model: nil,
|
||||
contextTokens: nil,
|
||||
mainSessionKey: mainSessionKey)
|
||||
return OpenClawChatSessionsListResponse(
|
||||
ts: decoded.ts,
|
||||
path: decoded.path,
|
||||
count: decoded.count,
|
||||
defaults: defaults,
|
||||
sessions: decoded.sessions)
|
||||
}
|
||||
|
||||
func setSessionModel(sessionKey: String, model: String?) async throws {
|
||||
@ -103,6 +119,13 @@ struct MacGatewayChatTransport: OpenClawChatTransport {
|
||||
try await GatewayConnection.shared.healthOK(timeoutMs: timeoutMs)
|
||||
}
|
||||
|
||||
func resetSession(sessionKey: String) async throws {
|
||||
_ = try await GatewayConnection.shared.request(
|
||||
method: "sessions.reset",
|
||||
params: ["key": AnyCodable(sessionKey)],
|
||||
timeoutMs: 10000)
|
||||
}
|
||||
|
||||
func events() -> AsyncStream<OpenClawChatTransportEvent> {
|
||||
AsyncStream { continuation in
|
||||
let task = Task {
|
||||
|
||||
@ -1322,6 +1322,7 @@ public struct SessionsPatchParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let label: AnyCodable?
|
||||
public let thinkinglevel: AnyCodable?
|
||||
public let fastmode: AnyCodable?
|
||||
public let verboselevel: AnyCodable?
|
||||
public let reasoninglevel: AnyCodable?
|
||||
public let responseusage: AnyCodable?
|
||||
@ -1343,6 +1344,7 @@ public struct SessionsPatchParams: Codable, Sendable {
|
||||
key: String,
|
||||
label: AnyCodable?,
|
||||
thinkinglevel: AnyCodable?,
|
||||
fastmode: AnyCodable?,
|
||||
verboselevel: AnyCodable?,
|
||||
reasoninglevel: AnyCodable?,
|
||||
responseusage: AnyCodable?,
|
||||
@ -1363,6 +1365,7 @@ public struct SessionsPatchParams: Codable, Sendable {
|
||||
self.key = key
|
||||
self.label = label
|
||||
self.thinkinglevel = thinkinglevel
|
||||
self.fastmode = fastmode
|
||||
self.verboselevel = verboselevel
|
||||
self.reasoninglevel = reasoninglevel
|
||||
self.responseusage = responseusage
|
||||
@ -1385,6 +1388,7 @@ public struct SessionsPatchParams: Codable, Sendable {
|
||||
case key
|
||||
case label
|
||||
case thinkinglevel = "thinkingLevel"
|
||||
case fastmode = "fastMode"
|
||||
case verboselevel = "verboseLevel"
|
||||
case reasoninglevel = "reasoningLevel"
|
||||
case responseusage = "responseUsage"
|
||||
|
||||
@ -34,6 +34,13 @@ public struct OpenClawChatModelChoice: Identifiable, Codable, Sendable, Hashable
|
||||
public struct OpenClawChatSessionsDefaults: Codable, Sendable {
|
||||
public let model: String?
|
||||
public let contextTokens: Int?
|
||||
public let mainSessionKey: String?
|
||||
|
||||
public init(model: String?, contextTokens: Int?, mainSessionKey: String? = nil) {
|
||||
self.model = model
|
||||
self.contextTokens = contextTokens
|
||||
self.mainSessionKey = mainSessionKey
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawChatSessionEntry: Codable, Identifiable, Sendable, Hashable {
|
||||
@ -69,4 +76,18 @@ public struct OpenClawChatSessionsListResponse: Codable, Sendable {
|
||||
public let count: Int?
|
||||
public let defaults: OpenClawChatSessionsDefaults?
|
||||
public let sessions: [OpenClawChatSessionEntry]
|
||||
|
||||
public init(
|
||||
ts: Double?,
|
||||
path: String?,
|
||||
count: Int?,
|
||||
defaults: OpenClawChatSessionsDefaults?,
|
||||
sessions: [OpenClawChatSessionEntry])
|
||||
{
|
||||
self.ts = ts
|
||||
self.path = path
|
||||
self.count = count
|
||||
self.defaults = defaults
|
||||
self.sessions = sessions
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,11 +27,19 @@ public protocol OpenClawChatTransport: Sendable {
|
||||
func events() -> AsyncStream<OpenClawChatTransportEvent>
|
||||
|
||||
func setActiveSessionKey(_ sessionKey: String) async throws
|
||||
func resetSession(sessionKey: String) async throws
|
||||
}
|
||||
|
||||
extension OpenClawChatTransport {
|
||||
public func setActiveSessionKey(_: String) async throws {}
|
||||
|
||||
public func resetSession(sessionKey _: String) async throws {
|
||||
throw NSError(
|
||||
domain: "OpenClawChatTransport",
|
||||
code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: "sessions.reset not supported by this transport"])
|
||||
}
|
||||
|
||||
public func abortRun(sessionKey _: String, runId _: String) async throws {
|
||||
throw NSError(
|
||||
domain: "OpenClawChatTransport",
|
||||
|
||||
@ -138,21 +138,23 @@ public final class OpenClawChatViewModel {
|
||||
let now = Date().timeIntervalSince1970 * 1000
|
||||
let cutoff = now - (24 * 60 * 60 * 1000)
|
||||
let sorted = self.sessions.sorted { ($0.updatedAt ?? 0) > ($1.updatedAt ?? 0) }
|
||||
let mainSessionKey = self.resolvedMainSessionKey
|
||||
|
||||
var result: [OpenClawChatSessionEntry] = []
|
||||
var included = Set<String>()
|
||||
|
||||
// Always show the main session first, even if it hasn't been updated recently.
|
||||
if let main = sorted.first(where: { $0.key == "main" }) {
|
||||
// Always show the resolved main session first, even if it hasn't been updated recently.
|
||||
if let main = sorted.first(where: { $0.key == mainSessionKey }) {
|
||||
result.append(main)
|
||||
included.insert(main.key)
|
||||
} else {
|
||||
result.append(self.placeholderSession(key: "main"))
|
||||
included.insert("main")
|
||||
result.append(self.placeholderSession(key: mainSessionKey))
|
||||
included.insert(mainSessionKey)
|
||||
}
|
||||
|
||||
for entry in sorted {
|
||||
guard !included.contains(entry.key) else { continue }
|
||||
guard entry.key == self.sessionKey || !Self.isHiddenInternalSession(entry.key) else { continue }
|
||||
guard (entry.updatedAt ?? 0) >= cutoff else { continue }
|
||||
result.append(entry)
|
||||
included.insert(entry.key)
|
||||
@ -169,6 +171,18 @@ public final class OpenClawChatViewModel {
|
||||
return result
|
||||
}
|
||||
|
||||
private var resolvedMainSessionKey: String {
|
||||
let trimmed = self.sessionDefaults?.mainSessionKey?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return (trimmed?.isEmpty == false ? trimmed : nil) ?? "main"
|
||||
}
|
||||
|
||||
private static func isHiddenInternalSession(_ key: String) -> Bool {
|
||||
let trimmed = key.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return false }
|
||||
return trimmed == "onboarding" || trimmed.hasSuffix(":onboarding")
|
||||
}
|
||||
|
||||
public var showsModelPicker: Bool {
|
||||
!self.modelChoices.isEmpty
|
||||
}
|
||||
@ -365,10 +379,19 @@ public final class OpenClawChatViewModel {
|
||||
return "\(message.role)|\(timestamp)|\(text)"
|
||||
}
|
||||
|
||||
private static let resetTriggers: Set<String> = ["/new", "/reset", "/clear"]
|
||||
|
||||
private func performSend() async {
|
||||
guard !self.isSending else { return }
|
||||
let trimmed = self.input.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty || !self.attachments.isEmpty else { return }
|
||||
|
||||
if Self.resetTriggers.contains(trimmed.lowercased()) {
|
||||
self.input = ""
|
||||
await self.performReset()
|
||||
return
|
||||
}
|
||||
|
||||
let sessionKey = self.sessionKey
|
||||
|
||||
guard self.healthOK else {
|
||||
@ -499,6 +522,22 @@ public final class OpenClawChatViewModel {
|
||||
await self.bootstrap()
|
||||
}
|
||||
|
||||
private func performReset() async {
|
||||
self.isLoading = true
|
||||
self.errorText = nil
|
||||
defer { self.isLoading = false }
|
||||
|
||||
do {
|
||||
try await self.transport.resetSession(sessionKey: self.sessionKey)
|
||||
} catch {
|
||||
self.errorText = error.localizedDescription
|
||||
chatUILogger.error("session reset failed \(error.localizedDescription, privacy: .public)")
|
||||
return
|
||||
}
|
||||
|
||||
await self.bootstrap()
|
||||
}
|
||||
|
||||
private func performSelectThinkingLevel(_ level: String) async {
|
||||
let next = Self.normalizedThinkingLevel(level) ?? "off"
|
||||
guard next != self.thinkingLevel else { return }
|
||||
@ -549,7 +588,9 @@ public final class OpenClawChatViewModel {
|
||||
sessionKey: sessionKey,
|
||||
model: nextModelRef)
|
||||
guard requestID == self.latestModelSelectionRequestIDsBySession[sessionKey] else {
|
||||
self.applySuccessfulModelSelection(next, sessionKey: sessionKey, syncSelection: false)
|
||||
// Keep older successful patches as rollback state, but do not replay
|
||||
// stale UI/session state over a newer in-flight or completed selection.
|
||||
self.lastSuccessfulModelSelectionIDsBySession[sessionKey] = next
|
||||
return
|
||||
}
|
||||
self.applySuccessfulModelSelection(next, sessionKey: sessionKey, syncSelection: true)
|
||||
|
||||
@ -1322,6 +1322,7 @@ public struct SessionsPatchParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let label: AnyCodable?
|
||||
public let thinkinglevel: AnyCodable?
|
||||
public let fastmode: AnyCodable?
|
||||
public let verboselevel: AnyCodable?
|
||||
public let reasoninglevel: AnyCodable?
|
||||
public let responseusage: AnyCodable?
|
||||
@ -1343,6 +1344,7 @@ public struct SessionsPatchParams: Codable, Sendable {
|
||||
key: String,
|
||||
label: AnyCodable?,
|
||||
thinkinglevel: AnyCodable?,
|
||||
fastmode: AnyCodable?,
|
||||
verboselevel: AnyCodable?,
|
||||
reasoninglevel: AnyCodable?,
|
||||
responseusage: AnyCodable?,
|
||||
@ -1363,6 +1365,7 @@ public struct SessionsPatchParams: Codable, Sendable {
|
||||
self.key = key
|
||||
self.label = label
|
||||
self.thinkinglevel = thinkinglevel
|
||||
self.fastmode = fastmode
|
||||
self.verboselevel = verboselevel
|
||||
self.reasoninglevel = reasoninglevel
|
||||
self.responseusage = responseusage
|
||||
@ -1385,6 +1388,7 @@ public struct SessionsPatchParams: Codable, Sendable {
|
||||
case key
|
||||
case label
|
||||
case thinkinglevel = "thinkingLevel"
|
||||
case fastmode = "fastMode"
|
||||
case verboselevel = "verboseLevel"
|
||||
case reasoninglevel = "reasoningLevel"
|
||||
case responseusage = "responseUsage"
|
||||
|
||||
@ -83,6 +83,7 @@ private func makeViewModel(
|
||||
historyResponses: [OpenClawChatHistoryPayload],
|
||||
sessionsResponses: [OpenClawChatSessionsListResponse] = [],
|
||||
modelResponses: [[OpenClawChatModelChoice]] = [],
|
||||
resetSessionHook: (@Sendable (String) async throws -> Void)? = nil,
|
||||
setSessionModelHook: (@Sendable (String?) async throws -> Void)? = nil,
|
||||
setSessionThinkingHook: (@Sendable (String) async throws -> Void)? = nil,
|
||||
initialThinkingLevel: String? = nil,
|
||||
@ -93,6 +94,7 @@ private func makeViewModel(
|
||||
historyResponses: historyResponses,
|
||||
sessionsResponses: sessionsResponses,
|
||||
modelResponses: modelResponses,
|
||||
resetSessionHook: resetSessionHook,
|
||||
setSessionModelHook: setSessionModelHook,
|
||||
setSessionThinkingHook: setSessionThinkingHook)
|
||||
let vm = await MainActor.run {
|
||||
@ -199,6 +201,7 @@ private actor TestChatTransportState {
|
||||
var historyCallCount: Int = 0
|
||||
var sessionsCallCount: Int = 0
|
||||
var modelsCallCount: Int = 0
|
||||
var resetSessionKeys: [String] = []
|
||||
var sentRunIds: [String] = []
|
||||
var sentThinkingLevels: [String] = []
|
||||
var abortedRunIds: [String] = []
|
||||
@ -211,6 +214,7 @@ private final class TestChatTransport: @unchecked Sendable, OpenClawChatTranspor
|
||||
private let historyResponses: [OpenClawChatHistoryPayload]
|
||||
private let sessionsResponses: [OpenClawChatSessionsListResponse]
|
||||
private let modelResponses: [[OpenClawChatModelChoice]]
|
||||
private let resetSessionHook: (@Sendable (String) async throws -> Void)?
|
||||
private let setSessionModelHook: (@Sendable (String?) async throws -> Void)?
|
||||
private let setSessionThinkingHook: (@Sendable (String) async throws -> Void)?
|
||||
|
||||
@ -221,12 +225,14 @@ private final class TestChatTransport: @unchecked Sendable, OpenClawChatTranspor
|
||||
historyResponses: [OpenClawChatHistoryPayload],
|
||||
sessionsResponses: [OpenClawChatSessionsListResponse] = [],
|
||||
modelResponses: [[OpenClawChatModelChoice]] = [],
|
||||
resetSessionHook: (@Sendable (String) async throws -> Void)? = nil,
|
||||
setSessionModelHook: (@Sendable (String?) async throws -> Void)? = nil,
|
||||
setSessionThinkingHook: (@Sendable (String) async throws -> Void)? = nil)
|
||||
{
|
||||
self.historyResponses = historyResponses
|
||||
self.sessionsResponses = sessionsResponses
|
||||
self.modelResponses = modelResponses
|
||||
self.resetSessionHook = resetSessionHook
|
||||
self.setSessionModelHook = setSessionModelHook
|
||||
self.setSessionThinkingHook = setSessionThinkingHook
|
||||
var cont: AsyncStream<OpenClawChatTransportEvent>.Continuation!
|
||||
@ -301,6 +307,13 @@ private final class TestChatTransport: @unchecked Sendable, OpenClawChatTranspor
|
||||
}
|
||||
}
|
||||
|
||||
func resetSession(sessionKey: String) async throws {
|
||||
await self.state.resetSessionKeysAppend(sessionKey)
|
||||
if let resetSessionHook = self.resetSessionHook {
|
||||
try await resetSessionHook(sessionKey)
|
||||
}
|
||||
}
|
||||
|
||||
func setSessionThinking(sessionKey _: String, thinkingLevel: String) async throws {
|
||||
await self.state.patchedThinkingLevelsAppend(thinkingLevel)
|
||||
if let setSessionThinkingHook = self.setSessionThinkingHook {
|
||||
@ -336,6 +349,10 @@ private final class TestChatTransport: @unchecked Sendable, OpenClawChatTranspor
|
||||
func patchedThinkingLevels() async -> [String] {
|
||||
await self.state.patchedThinkingLevels
|
||||
}
|
||||
|
||||
func resetSessionKeys() async -> [String] {
|
||||
await self.state.resetSessionKeys
|
||||
}
|
||||
}
|
||||
|
||||
extension TestChatTransportState {
|
||||
@ -370,6 +387,10 @@ extension TestChatTransportState {
|
||||
fileprivate func patchedThinkingLevelsAppend(_ v: String) {
|
||||
self.patchedThinkingLevels.append(v)
|
||||
}
|
||||
|
||||
fileprivate func resetSessionKeysAppend(_ v: String) {
|
||||
self.resetSessionKeys.append(v)
|
||||
}
|
||||
}
|
||||
|
||||
@Suite struct ChatViewModelTests {
|
||||
@ -592,6 +613,151 @@ extension TestChatTransportState {
|
||||
#expect(keys == ["main", "custom"])
|
||||
}
|
||||
|
||||
@Test func sessionChoicesUseResolvedMainSessionKeyInsteadOfLiteralMain() async throws {
|
||||
let now = Date().timeIntervalSince1970 * 1000
|
||||
let recent = now - (30 * 60 * 1000)
|
||||
let recentOlder = now - (90 * 60 * 1000)
|
||||
let history = historyPayload(sessionKey: "Luke’s MacBook Pro", sessionId: "sess-main")
|
||||
let sessions = OpenClawChatSessionsListResponse(
|
||||
ts: now,
|
||||
path: nil,
|
||||
count: 2,
|
||||
defaults: OpenClawChatSessionsDefaults(
|
||||
model: nil,
|
||||
contextTokens: nil,
|
||||
mainSessionKey: "Luke’s MacBook Pro"),
|
||||
sessions: [
|
||||
OpenClawChatSessionEntry(
|
||||
key: "Luke’s MacBook Pro",
|
||||
kind: nil,
|
||||
displayName: "Luke’s MacBook Pro",
|
||||
surface: nil,
|
||||
subject: nil,
|
||||
room: nil,
|
||||
space: nil,
|
||||
updatedAt: recent,
|
||||
sessionId: nil,
|
||||
systemSent: nil,
|
||||
abortedLastRun: nil,
|
||||
thinkingLevel: nil,
|
||||
verboseLevel: nil,
|
||||
inputTokens: nil,
|
||||
outputTokens: nil,
|
||||
totalTokens: nil,
|
||||
modelProvider: nil,
|
||||
model: nil,
|
||||
contextTokens: nil),
|
||||
sessionEntry(key: "recent-1", updatedAt: recentOlder),
|
||||
])
|
||||
|
||||
let (_, vm) = await makeViewModel(
|
||||
sessionKey: "Luke’s MacBook Pro",
|
||||
historyResponses: [history],
|
||||
sessionsResponses: [sessions])
|
||||
await MainActor.run { vm.load() }
|
||||
try await waitUntil("sessions loaded") { await MainActor.run { !vm.sessions.isEmpty } }
|
||||
|
||||
let keys = await MainActor.run { vm.sessionChoices.map(\.key) }
|
||||
#expect(keys == ["Luke’s MacBook Pro", "recent-1"])
|
||||
}
|
||||
|
||||
@Test func sessionChoicesHideInternalOnboardingSession() async throws {
|
||||
let now = Date().timeIntervalSince1970 * 1000
|
||||
let recent = now - (2 * 60 * 1000)
|
||||
let recentOlder = now - (5 * 60 * 1000)
|
||||
let history = historyPayload(sessionKey: "agent:main:main", sessionId: "sess-main")
|
||||
let sessions = OpenClawChatSessionsListResponse(
|
||||
ts: now,
|
||||
path: nil,
|
||||
count: 2,
|
||||
defaults: OpenClawChatSessionsDefaults(
|
||||
model: nil,
|
||||
contextTokens: nil,
|
||||
mainSessionKey: "agent:main:main"),
|
||||
sessions: [
|
||||
OpenClawChatSessionEntry(
|
||||
key: "agent:main:onboarding",
|
||||
kind: nil,
|
||||
displayName: "Luke’s MacBook Pro",
|
||||
surface: nil,
|
||||
subject: nil,
|
||||
room: nil,
|
||||
space: nil,
|
||||
updatedAt: recent,
|
||||
sessionId: nil,
|
||||
systemSent: nil,
|
||||
abortedLastRun: nil,
|
||||
thinkingLevel: nil,
|
||||
verboseLevel: nil,
|
||||
inputTokens: nil,
|
||||
outputTokens: nil,
|
||||
totalTokens: nil,
|
||||
modelProvider: nil,
|
||||
model: nil,
|
||||
contextTokens: nil),
|
||||
OpenClawChatSessionEntry(
|
||||
key: "agent:main:main",
|
||||
kind: nil,
|
||||
displayName: "Luke’s MacBook Pro",
|
||||
surface: nil,
|
||||
subject: nil,
|
||||
room: nil,
|
||||
space: nil,
|
||||
updatedAt: recentOlder,
|
||||
sessionId: nil,
|
||||
systemSent: nil,
|
||||
abortedLastRun: nil,
|
||||
thinkingLevel: nil,
|
||||
verboseLevel: nil,
|
||||
inputTokens: nil,
|
||||
outputTokens: nil,
|
||||
totalTokens: nil,
|
||||
modelProvider: nil,
|
||||
model: nil,
|
||||
contextTokens: nil),
|
||||
])
|
||||
|
||||
let (_, vm) = await makeViewModel(
|
||||
sessionKey: "agent:main:main",
|
||||
historyResponses: [history],
|
||||
sessionsResponses: [sessions])
|
||||
await MainActor.run { vm.load() }
|
||||
try await waitUntil("sessions loaded") { await MainActor.run { !vm.sessions.isEmpty } }
|
||||
|
||||
let keys = await MainActor.run { vm.sessionChoices.map(\.key) }
|
||||
#expect(keys == ["agent:main:main"])
|
||||
}
|
||||
|
||||
@Test func resetTriggerResetsSessionAndReloadsHistory() async throws {
|
||||
let before = historyPayload(
|
||||
messages: [
|
||||
chatTextMessage(role: "assistant", text: "before reset", timestamp: 1),
|
||||
])
|
||||
let after = historyPayload(
|
||||
messages: [
|
||||
chatTextMessage(role: "assistant", text: "after reset", timestamp: 2),
|
||||
])
|
||||
|
||||
let (transport, vm) = await makeViewModel(historyResponses: [before, after])
|
||||
try await loadAndWaitBootstrap(vm: vm)
|
||||
try await waitUntil("initial history loaded") {
|
||||
await MainActor.run { vm.messages.first?.content.first?.text == "before reset" }
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
vm.input = "/new"
|
||||
vm.send()
|
||||
}
|
||||
|
||||
try await waitUntil("reset called") {
|
||||
await transport.resetSessionKeys() == ["main"]
|
||||
}
|
||||
try await waitUntil("history reloaded") {
|
||||
await MainActor.run { vm.messages.first?.content.first?.text == "after reset" }
|
||||
}
|
||||
#expect(await transport.lastSentRunId() == nil)
|
||||
}
|
||||
|
||||
@Test func bootstrapsModelSelectionFromSessionAndDefaults() async throws {
|
||||
let now = Date().timeIntervalSince1970 * 1000
|
||||
let history = historyPayload()
|
||||
@ -758,7 +924,8 @@ extension TestChatTransportState {
|
||||
}
|
||||
|
||||
#expect(await MainActor.run { vm.modelSelectionID } == "openai/gpt-5.4-pro")
|
||||
#expect(await MainActor.run { vm.sessions.first(where: { $0.key == "main" })?.model } == "openai/gpt-5.4-pro")
|
||||
#expect(await MainActor.run { vm.sessions.first(where: { $0.key == "main" })?.model } == "gpt-5.4-pro")
|
||||
#expect(await MainActor.run { vm.sessions.first(where: { $0.key == "main" })?.modelProvider } == "openai")
|
||||
}
|
||||
|
||||
@Test func sendWaitsForInFlightModelPatchToFinish() async throws {
|
||||
@ -852,11 +1019,15 @@ extension TestChatTransportState {
|
||||
}
|
||||
|
||||
try await waitUntil("older model completion wins after latest failure") {
|
||||
await MainActor.run { vm.sessions.first(where: { $0.key == "main" })?.model == "openai/gpt-5.4" }
|
||||
await MainActor.run {
|
||||
vm.sessions.first(where: { $0.key == "main" })?.model == "gpt-5.4" &&
|
||||
vm.sessions.first(where: { $0.key == "main" })?.modelProvider == "openai"
|
||||
}
|
||||
}
|
||||
|
||||
#expect(await MainActor.run { vm.modelSelectionID } == "openai/gpt-5.4")
|
||||
#expect(await MainActor.run { vm.sessions.first(where: { $0.key == "main" })?.model } == "openai/gpt-5.4")
|
||||
#expect(await MainActor.run { vm.sessions.first(where: { $0.key == "main" })?.model } == "gpt-5.4")
|
||||
#expect(await MainActor.run { vm.sessions.first(where: { $0.key == "main" })?.modelProvider } == "openai")
|
||||
#expect(await transport.patchedModels() == ["openai/gpt-5.4", "openai/gpt-5.4-pro"])
|
||||
}
|
||||
|
||||
@ -1012,12 +1183,17 @@ extension TestChatTransportState {
|
||||
}
|
||||
|
||||
try await waitUntil("late model completion updates only the original session") {
|
||||
await MainActor.run { vm.sessions.first(where: { $0.key == "main" })?.model == "openai/gpt-5.4" }
|
||||
await MainActor.run {
|
||||
vm.sessions.first(where: { $0.key == "main" })?.model == "gpt-5.4" &&
|
||||
vm.sessions.first(where: { $0.key == "main" })?.modelProvider == "openai"
|
||||
}
|
||||
}
|
||||
|
||||
#expect(await MainActor.run { vm.modelSelectionID } == "openai/gpt-5.4")
|
||||
#expect(await MainActor.run { vm.sessions.first(where: { $0.key == "main" })?.model } == "openai/gpt-5.4")
|
||||
#expect(await MainActor.run { vm.sessions.first(where: { $0.key == "main" })?.model } == "gpt-5.4")
|
||||
#expect(await MainActor.run { vm.sessions.first(where: { $0.key == "main" })?.modelProvider } == "openai")
|
||||
#expect(await MainActor.run { vm.sessions.first(where: { $0.key == "other" })?.model } == "openai/gpt-5.4-pro")
|
||||
#expect(await MainActor.run { vm.sessions.first(where: { $0.key == "other" })?.modelProvider } == nil)
|
||||
#expect(await transport.patchedModels() == ["openai/gpt-5.4", "openai/gpt-5.4-pro"])
|
||||
}
|
||||
|
||||
|
||||
@ -114,11 +114,11 @@ Example:
|
||||
**Teams + channel allowlist**
|
||||
|
||||
- Scope group/channel replies by listing teams and channels under `channels.msteams.teams`.
|
||||
- Keys can be team IDs or names; channel keys can be conversation IDs or names.
|
||||
- Keys should use stable team IDs and channel conversation IDs.
|
||||
- When `groupPolicy="allowlist"` and a teams allowlist is present, only listed teams/channels are accepted (mention‑gated).
|
||||
- The configure wizard accepts `Team/Channel` entries and stores them for you.
|
||||
- On startup, OpenClaw resolves team/channel and user allowlist names to IDs (when Graph permissions allow)
|
||||
and logs the mapping; unresolved entries are kept as typed.
|
||||
and logs the mapping; unresolved team/channel names are kept as typed but ignored for routing by default unless `channels.msteams.dangerouslyAllowNameMatching: true` is enabled.
|
||||
|
||||
Example:
|
||||
|
||||
@ -457,7 +457,7 @@ Key settings (see `/gateway/configuration` for shared channel patterns):
|
||||
- `channels.msteams.webhook.path` (default `/api/messages`)
|
||||
- `channels.msteams.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing)
|
||||
- `channels.msteams.allowFrom`: DM allowlist (AAD object IDs recommended). The wizard resolves names to IDs during setup when Graph access is available.
|
||||
- `channels.msteams.dangerouslyAllowNameMatching`: break-glass toggle to re-enable mutable UPN/display-name matching.
|
||||
- `channels.msteams.dangerouslyAllowNameMatching`: break-glass toggle to re-enable mutable UPN/display-name matching and direct team/channel name routing.
|
||||
- `channels.msteams.textChunkLimit`: outbound text chunk size.
|
||||
- `channels.msteams.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
|
||||
- `channels.msteams.mediaAllowHosts`: allowlist for inbound attachment hosts (defaults to Microsoft/Teams domains).
|
||||
|
||||
@ -169,15 +169,15 @@ For actions/directory reads, user token can be preferred when configured. For wr
|
||||
- `allowlist`
|
||||
- `disabled`
|
||||
|
||||
Channel allowlist lives under `channels.slack.channels`.
|
||||
Channel allowlist lives under `channels.slack.channels` and should use stable channel IDs.
|
||||
|
||||
Runtime note: if `channels.slack` is completely missing (env-only setup), runtime falls back to `groupPolicy="allowlist"` and logs a warning (even if `channels.defaults.groupPolicy` is set).
|
||||
|
||||
Name/ID resolution:
|
||||
|
||||
- channel allowlist entries and DM allowlist entries are resolved at startup when token access allows
|
||||
- unresolved entries are kept as configured
|
||||
- inbound authorization matching is ID-first by default; direct username/slug matching requires `channels.slack.dangerouslyAllowNameMatching: true`
|
||||
- unresolved channel-name entries are kept as configured but ignored for routing by default
|
||||
- inbound authorization and channel routing are ID-first by default; direct username/slug matching requires `channels.slack.dangerouslyAllowNameMatching: true`
|
||||
|
||||
</Tab>
|
||||
|
||||
@ -190,7 +190,7 @@ For actions/directory reads, user token can be preferred when configured. For wr
|
||||
- mention regex patterns (`agents.list[].groupChat.mentionPatterns`, fallback `messages.groupChat.mentionPatterns`)
|
||||
- implicit reply-to-bot thread behavior
|
||||
|
||||
Per-channel controls (`channels.slack.channels.<id|name>`):
|
||||
Per-channel controls (`channels.slack.channels.<id>`; names only via startup resolution or `dangerouslyAllowNameMatching`):
|
||||
|
||||
- `requireMention`
|
||||
- `users` (allowlist)
|
||||
|
||||
@ -335,9 +335,10 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
|
||||
If native commands are disabled, built-ins are removed. Custom/plugin commands may still register if configured.
|
||||
|
||||
Common setup failure:
|
||||
Common setup failures:
|
||||
|
||||
- `setMyCommands failed` usually means outbound DNS/HTTPS to `api.telegram.org` is blocked.
|
||||
- `setMyCommands failed` with `BOT_COMMANDS_TOO_MUCH` means the Telegram menu still overflowed after trimming; reduce plugin/skill/custom commands or disable `channels.telegram.commands.native`.
|
||||
- `setMyCommands failed` with network/fetch errors usually means outbound DNS/HTTPS to `api.telegram.org` is blocked.
|
||||
|
||||
### Device pairing commands (`device-pair` plugin)
|
||||
|
||||
@ -843,7 +844,8 @@ openclaw message poll --channel telegram --target -1001234567890:topic:42 \
|
||||
|
||||
- authorize your sender identity (pairing and/or numeric `allowFrom`)
|
||||
- command authorization still applies even when group policy is `open`
|
||||
- `setMyCommands failed` usually indicates DNS/HTTPS reachability issues to `api.telegram.org`
|
||||
- `setMyCommands failed` with `BOT_COMMANDS_TOO_MUCH` means the native menu has too many entries; reduce plugin/skill/custom commands or disable native menus
|
||||
- `setMyCommands failed` with network/fetch errors usually indicates DNS/HTTPS reachability issues to `api.telegram.org`
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
@ -44,12 +44,13 @@ Full troubleshooting: [/channels/whatsapp#troubleshooting-quick](/channels/whats
|
||||
|
||||
### Telegram failure signatures
|
||||
|
||||
| Symptom | Fastest check | Fix |
|
||||
| --------------------------------- | ----------------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| `/start` but no usable reply flow | `openclaw pairing list telegram` | Approve pairing or change DM policy. |
|
||||
| Bot online but group stays silent | Verify mention requirement and bot privacy mode | Disable privacy mode for group visibility or mention bot. |
|
||||
| Send failures with network errors | Inspect logs for Telegram API call failures | Fix DNS/IPv6/proxy routing to `api.telegram.org`. |
|
||||
| Upgraded and allowlist blocks you | `openclaw security audit` and config allowlists | Run `openclaw doctor --fix` or replace `@username` with numeric sender IDs. |
|
||||
| Symptom | Fastest check | Fix |
|
||||
| ----------------------------------- | ----------------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| `/start` but no usable reply flow | `openclaw pairing list telegram` | Approve pairing or change DM policy. |
|
||||
| Bot online but group stays silent | Verify mention requirement and bot privacy mode | Disable privacy mode for group visibility or mention bot. |
|
||||
| Send failures with network errors | Inspect logs for Telegram API call failures | Fix DNS/IPv6/proxy routing to `api.telegram.org`. |
|
||||
| `setMyCommands` rejected at startup | Inspect logs for `BOT_COMMANDS_TOO_MUCH` | Reduce plugin/skill/custom Telegram commands or disable native menus. |
|
||||
| Upgraded and allowlist blocks you | `openclaw security audit` and config allowlists | Run `openclaw doctor --fix` or replace `@username` with numeric sender IDs. |
|
||||
|
||||
Full troubleshooting: [/channels/telegram#troubleshooting](/channels/telegram#troubleshooting)
|
||||
|
||||
|
||||
@ -86,11 +86,13 @@ Approve via:
|
||||
- Default: `channels.zalouser.groupPolicy = "open"` (groups allowed). Use `channels.defaults.groupPolicy` to override the default when unset.
|
||||
- Restrict to an allowlist with:
|
||||
- `channels.zalouser.groupPolicy = "allowlist"`
|
||||
- `channels.zalouser.groups` (keys are group IDs or names; controls which groups are allowed)
|
||||
- `channels.zalouser.groups` (keys should be stable group IDs; names are resolved to IDs on startup when possible)
|
||||
- `channels.zalouser.groupAllowFrom` (controls which senders in allowed groups can trigger the bot)
|
||||
- Block all groups: `channels.zalouser.groupPolicy = "disabled"`.
|
||||
- The configure wizard can prompt for group allowlists.
|
||||
- On startup, OpenClaw resolves group/user names in allowlists to IDs and logs the mapping; unresolved entries are kept as typed.
|
||||
- On startup, OpenClaw resolves group/user names in allowlists to IDs and logs the mapping.
|
||||
- Group allowlist matching is ID-only by default. Unresolved names are ignored for auth unless `channels.zalouser.dangerouslyAllowNameMatching: true` is enabled.
|
||||
- `channels.zalouser.dangerouslyAllowNameMatching: true` is a break-glass compatibility mode that re-enables mutable group-name matching.
|
||||
- If `groupAllowFrom` is unset, runtime falls back to `allowFrom` for group sender checks.
|
||||
- Sender checks apply to both normal group messages and control commands (for example `/new`, `/reset`).
|
||||
|
||||
|
||||
@ -48,6 +48,7 @@ OpenClaw ships with the pi‑ai catalog. These providers require **no**
|
||||
- OpenAI Responses WebSocket warm-up defaults to enabled via `params.openaiWsWarmup` (`true`/`false`)
|
||||
- OpenAI priority processing can be enabled via `agents.defaults.models["openai/<model>"].params.serviceTier`
|
||||
- OpenAI fast mode can be enabled per model via `agents.defaults.models["<provider>/<model>"].params.fastMode`
|
||||
- `openai/gpt-5.3-codex-spark` is intentionally suppressed in OpenClaw because the live OpenAI API rejects it; Spark is treated as Codex-only
|
||||
|
||||
```json5
|
||||
{
|
||||
@ -81,6 +82,7 @@ OpenClaw ships with the pi‑ai catalog. These providers require **no**
|
||||
- Default transport is `auto` (WebSocket-first, SSE fallback)
|
||||
- Override per model via `agents.defaults.models["openai-codex/<model>"].params.transport` (`"sse"`, `"websocket"`, or `"auto"`)
|
||||
- Shares the same `/fast` toggle and `params.fastMode` config as direct `openai/*`
|
||||
- `openai-codex/gpt-5.3-codex-spark` remains available when the Codex OAuth catalog exposes it; entitlement-dependent
|
||||
- Policy note: OpenAI Codex OAuth is explicitly supported for external tools/workflows like OpenClaw.
|
||||
|
||||
```json5
|
||||
|
||||
@ -304,6 +304,7 @@ schema:
|
||||
- `channels.googlechat.dangerouslyAllowNameMatching`
|
||||
- `channels.googlechat.accounts.<accountId>.dangerouslyAllowNameMatching`
|
||||
- `channels.msteams.dangerouslyAllowNameMatching`
|
||||
- `channels.zalouser.dangerouslyAllowNameMatching` (extension channel)
|
||||
- `channels.irc.dangerouslyAllowNameMatching` (extension channel)
|
||||
- `channels.irc.accounts.<accountId>.dangerouslyAllowNameMatching` (extension channel)
|
||||
- `channels.mattermost.dangerouslyAllowNameMatching` (extension channel)
|
||||
|
||||
@ -179,7 +179,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
|
||||
- [I closed my terminal on Windows - how do I restart OpenClaw?](#i-closed-my-terminal-on-windows-how-do-i-restart-openclaw)
|
||||
- [The Gateway is up but replies never arrive. What should I check?](#the-gateway-is-up-but-replies-never-arrive-what-should-i-check)
|
||||
- ["Disconnected from gateway: no reason" - what now?](#disconnected-from-gateway-no-reason-what-now)
|
||||
- [Telegram setMyCommands fails with network errors. What should I check?](#telegram-setmycommands-fails-with-network-errors-what-should-i-check)
|
||||
- [Telegram setMyCommands fails. What should I check?](#telegram-setmycommands-fails-what-should-i-check)
|
||||
- [TUI shows no output. What should I check?](#tui-shows-no-output-what-should-i-check)
|
||||
- [How do I completely stop then start the Gateway?](#how-do-i-completely-stop-then-start-the-gateway)
|
||||
- [ELI5: `openclaw gateway restart` vs `openclaw gateway`](#eli5-openclaw-gateway-restart-vs-openclaw-gateway)
|
||||
@ -2710,7 +2710,7 @@ openclaw logs --follow
|
||||
|
||||
Docs: [Dashboard](/web/dashboard), [Remote access](/gateway/remote), [Troubleshooting](/gateway/troubleshooting).
|
||||
|
||||
### Telegram setMyCommands fails with network errors What should I check
|
||||
### Telegram setMyCommands fails What should I check
|
||||
|
||||
Start with logs and channel status:
|
||||
|
||||
@ -2719,7 +2719,11 @@ openclaw channels status
|
||||
openclaw channels logs --channel telegram
|
||||
```
|
||||
|
||||
If you are on a VPS or behind a proxy, confirm outbound HTTPS is allowed and DNS works.
|
||||
Then match the error:
|
||||
|
||||
- `BOT_COMMANDS_TOO_MUCH`: the Telegram menu has too many entries. OpenClaw already trims to the Telegram limit and retries with fewer commands, but some menu entries still need to be dropped. Reduce plugin/skill/custom commands, or disable `channels.telegram.commands.native` if you do not need the menu.
|
||||
- `TypeError: fetch failed`, `Network request for 'setMyCommands' failed!`, or similar network errors: if you are on a VPS or behind a proxy, confirm outbound HTTPS is allowed and DNS works for `api.telegram.org`.
|
||||
|
||||
If the Gateway is remote, make sure you are looking at logs on the Gateway host.
|
||||
|
||||
Docs: [Telegram](/channels/telegram), [Channel troubleshooting](/channels/troubleshooting).
|
||||
|
||||
@ -39,7 +39,7 @@ Notes:
|
||||
# Default is auto-derived from APP_VERSION when omitted.
|
||||
SKIP_NOTARIZE=1 \
|
||||
BUNDLE_ID=ai.openclaw.mac \
|
||||
APP_VERSION=2026.3.11 \
|
||||
APP_VERSION=2026.3.12 \
|
||||
BUILD_CONFIG=release \
|
||||
SIGN_IDENTITY="Developer ID Application: <Developer Name> (<TEAMID>)" \
|
||||
scripts/package-mac-dist.sh
|
||||
@ -47,10 +47,10 @@ scripts/package-mac-dist.sh
|
||||
# `package-mac-dist.sh` already creates the zip + DMG.
|
||||
# If you used `package-mac-app.sh` directly instead, create them manually:
|
||||
# If you want notarization/stapling in this step, use the NOTARIZE command below.
|
||||
ditto -c -k --sequesterRsrc --keepParent dist/OpenClaw.app dist/OpenClaw-2026.3.11.zip
|
||||
ditto -c -k --sequesterRsrc --keepParent dist/OpenClaw.app dist/OpenClaw-2026.3.12.zip
|
||||
|
||||
# Optional: build a styled DMG for humans (drag to /Applications)
|
||||
scripts/create-dmg.sh dist/OpenClaw.app dist/OpenClaw-2026.3.11.dmg
|
||||
scripts/create-dmg.sh dist/OpenClaw.app dist/OpenClaw-2026.3.12.dmg
|
||||
|
||||
# Recommended: build + notarize/staple zip + DMG
|
||||
# First, create a keychain profile once:
|
||||
@ -58,13 +58,13 @@ scripts/create-dmg.sh dist/OpenClaw.app dist/OpenClaw-2026.3.11.dmg
|
||||
# --apple-id "<apple-id>" --team-id "<team-id>" --password "<app-specific-password>"
|
||||
NOTARIZE=1 NOTARYTOOL_PROFILE=openclaw-notary \
|
||||
BUNDLE_ID=ai.openclaw.mac \
|
||||
APP_VERSION=2026.3.11 \
|
||||
APP_VERSION=2026.3.12 \
|
||||
BUILD_CONFIG=release \
|
||||
SIGN_IDENTITY="Developer ID Application: <Developer Name> (<TEAMID>)" \
|
||||
scripts/package-mac-dist.sh
|
||||
|
||||
# Optional: ship dSYM alongside the release
|
||||
ditto -c -k --keepParent apps/macos/.build/release/OpenClaw.app.dSYM dist/OpenClaw-2026.3.11.dSYM.zip
|
||||
ditto -c -k --keepParent apps/macos/.build/release/OpenClaw.app.dSYM dist/OpenClaw-2026.3.12.dSYM.zip
|
||||
```
|
||||
|
||||
## Appcast entry
|
||||
@ -72,7 +72,7 @@ ditto -c -k --keepParent apps/macos/.build/release/OpenClaw.app.dSYM dist/OpenCl
|
||||
Use the release note generator so Sparkle renders formatted HTML notes:
|
||||
|
||||
```bash
|
||||
SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/OpenClaw-2026.3.11.zip https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml
|
||||
SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/OpenClaw-2026.3.12.zip https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml
|
||||
```
|
||||
|
||||
Generates HTML release notes from `CHANGELOG.md` (via [`scripts/changelog-to-html.sh`](https://github.com/openclaw/openclaw/blob/main/scripts/changelog-to-html.sh)) and embeds them in the appcast entry.
|
||||
@ -80,7 +80,7 @@ Commit the updated `appcast.xml` alongside the release assets (zip + dSYM) when
|
||||
|
||||
## Publish & verify
|
||||
|
||||
- Upload `OpenClaw-2026.3.11.zip` (and `OpenClaw-2026.3.11.dSYM.zip`) to the GitHub release for tag `v2026.3.11`.
|
||||
- Upload `OpenClaw-2026.3.12.zip` (and `OpenClaw-2026.3.12.dSYM.zip`) to the GitHub release for tag `v2026.3.12`.
|
||||
- Ensure the raw appcast URL matches the baked feed: `https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml`.
|
||||
- Sanity checks:
|
||||
- `curl -I https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml` returns 200.
|
||||
|
||||
@ -36,6 +36,12 @@ openclaw onboard --openai-api-key "$OPENAI_API_KEY"
|
||||
|
||||
OpenAI's current API model docs list `gpt-5.4` and `gpt-5.4-pro` for direct
|
||||
OpenAI API usage. OpenClaw forwards both through the `openai/*` Responses path.
|
||||
OpenClaw intentionally suppresses the stale `openai/gpt-5.3-codex-spark` row,
|
||||
because direct OpenAI API calls reject it in live traffic.
|
||||
|
||||
OpenClaw does **not** expose `openai/gpt-5.3-codex-spark` on the direct OpenAI
|
||||
API path. `pi-ai` still ships a built-in row for that model, but live OpenAI API
|
||||
requests currently reject it. Spark is treated as Codex-only in OpenClaw.
|
||||
|
||||
## Option B: OpenAI Code (Codex) subscription
|
||||
|
||||
@ -63,6 +69,18 @@ openclaw models auth login --provider openai-codex
|
||||
OpenAI's current Codex docs list `gpt-5.4` as the current Codex model. OpenClaw
|
||||
maps that to `openai-codex/gpt-5.4` for ChatGPT/Codex OAuth usage.
|
||||
|
||||
If your Codex account is entitled to Codex Spark, OpenClaw also supports:
|
||||
|
||||
- `openai-codex/gpt-5.3-codex-spark`
|
||||
|
||||
OpenClaw treats Codex Spark as Codex-only. It does not expose a direct
|
||||
`openai/gpt-5.3-codex-spark` API-key path.
|
||||
|
||||
OpenClaw also preserves `openai-codex/gpt-5.3-codex-spark` when `pi-ai`
|
||||
discovers it. Treat it as entitlement-dependent and experimental: Codex Spark is
|
||||
separate from GPT-5.4 `/fast`, and availability depends on the signed-in Codex /
|
||||
ChatGPT account.
|
||||
|
||||
### Transport default
|
||||
|
||||
OpenClaw uses `pi-ai` for model streaming. For both `openai/*` and
|
||||
|
||||
@ -771,18 +771,22 @@ are not just "OAuth helpers" anymore.
|
||||
|
||||
### Provider plugin lifecycle
|
||||
|
||||
A provider plugin can participate in four distinct phases:
|
||||
A provider plugin can participate in five distinct phases:
|
||||
|
||||
1. **Auth**
|
||||
`auth[].run(ctx)` performs OAuth, API-key capture, device code, or custom
|
||||
setup and returns auth profiles plus optional config patches.
|
||||
2. **Wizard integration**
|
||||
2. **Non-interactive setup**
|
||||
`auth[].runNonInteractive(ctx)` handles `openclaw onboard --non-interactive`
|
||||
without prompts. Use this when the provider needs custom headless setup
|
||||
beyond the built-in simple API-key paths.
|
||||
3. **Wizard integration**
|
||||
`wizard.onboarding` adds an entry to `openclaw onboard`.
|
||||
`wizard.modelPicker` adds a setup entry to the model picker.
|
||||
3. **Implicit discovery**
|
||||
4. **Implicit discovery**
|
||||
`discovery.run(ctx)` can contribute provider config automatically during
|
||||
model resolution/listing.
|
||||
4. **Post-selection follow-up**
|
||||
5. **Post-selection follow-up**
|
||||
`onModelSelected(ctx)` runs after a model is chosen. Use this for provider-
|
||||
specific work such as downloading a local model.
|
||||
|
||||
@ -790,6 +794,7 @@ This is the recommended split because these phases have different lifecycle
|
||||
requirements:
|
||||
|
||||
- auth is interactive and writes credentials/config
|
||||
- non-interactive setup is flag/env-driven and must not prompt
|
||||
- wizard metadata is static and UI-facing
|
||||
- discovery should be safe, quick, and failure-tolerant
|
||||
- post-select hooks are side effects tied to the chosen model
|
||||
@ -814,6 +819,32 @@ Core then:
|
||||
That means a provider plugin owns the provider-specific setup logic, while core
|
||||
owns the generic persistence and config-merge path.
|
||||
|
||||
### Provider non-interactive contract
|
||||
|
||||
`auth[].runNonInteractive(ctx)` is optional. Implement it when the provider
|
||||
needs headless setup that cannot be expressed through the built-in generic
|
||||
API-key flows.
|
||||
|
||||
The non-interactive context includes:
|
||||
|
||||
- the current and base config
|
||||
- parsed onboarding CLI options
|
||||
- runtime logging/error helpers
|
||||
- agent/workspace dirs
|
||||
- `resolveApiKey(...)` to read provider keys from flags, env, or existing auth
|
||||
profiles while honoring `--secret-input-mode`
|
||||
- `toApiKeyCredential(...)` to convert a resolved key into an auth-profile
|
||||
credential with the right plaintext vs secret-ref storage
|
||||
|
||||
Use this surface for providers such as:
|
||||
|
||||
- self-hosted OpenAI-compatible runtimes that need `--custom-base-url` +
|
||||
`--custom-model-id`
|
||||
- provider-specific non-interactive verification or config synthesis
|
||||
|
||||
Do not prompt from `runNonInteractive`. Reject missing inputs with actionable
|
||||
errors instead.
|
||||
|
||||
### Provider wizard metadata
|
||||
|
||||
`wizard.onboarding` controls how the provider appears in grouped onboarding:
|
||||
@ -836,6 +867,13 @@ entry in model selection:
|
||||
When a provider has multiple auth methods, the wizard can either point at one
|
||||
explicit method or let OpenClaw synthesize per-method choices.
|
||||
|
||||
OpenClaw validates provider wizard metadata when the plugin registers:
|
||||
|
||||
- duplicate or blank auth-method ids are rejected
|
||||
- wizard metadata is ignored when the provider has no auth methods
|
||||
- invalid `methodId` bindings are downgraded to warnings and fall back to the
|
||||
provider's remaining auth methods
|
||||
|
||||
### Provider discovery contract
|
||||
|
||||
`discovery.run(ctx)` returns one of:
|
||||
@ -970,6 +1008,9 @@ Notes:
|
||||
|
||||
- `run` receives a `ProviderAuthContext` with `prompter`, `runtime`,
|
||||
`openUrl`, and `oauth.createVpsAwareHandlers` helpers.
|
||||
- `runNonInteractive` receives a `ProviderAuthMethodNonInteractiveContext`
|
||||
with `opts`, `resolveApiKey`, and `toApiKeyCredential` helpers for
|
||||
headless onboarding.
|
||||
- Return `configPatch` when you need to add default models or provider config.
|
||||
- Return `defaultModel` so `--set-default` can update agent defaults.
|
||||
- `wizard.onboarding` adds a provider choice to `openclaw onboard`.
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "@openclaw/acpx",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"description": "OpenClaw ACP runtime backend via acpx",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"acpx": "0.2.0"
|
||||
"acpx": "0.3.0"
|
||||
},
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/bluebubbles",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"description": "OpenClaw BlueBubbles channel plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/copilot-proxy",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"private": true,
|
||||
"description": "OpenClaw Copilot Proxy provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/diagnostics-otel",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"description": "OpenClaw diagnostics OpenTelemetry exporter",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/diffs",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"private": true,
|
||||
"description": "OpenClaw diff viewer plugin",
|
||||
"type": "module",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/discord",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"description": "OpenClaw Discord channel plugin",
|
||||
"type": "module",
|
||||
"openclaw": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/feishu",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"description": "OpenClaw Feishu/Lark channel plugin (community maintained by @m1heng)",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/google-gemini-cli-auth",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"private": true,
|
||||
"description": "OpenClaw Gemini CLI OAuth provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/googlechat",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"private": true,
|
||||
"description": "OpenClaw Google Chat channel plugin",
|
||||
"type": "module",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/imessage",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"private": true,
|
||||
"description": "OpenClaw iMessage channel plugin",
|
||||
"type": "module",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/irc",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"description": "OpenClaw IRC channel plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/line",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"private": true,
|
||||
"description": "OpenClaw LINE channel plugin",
|
||||
"type": "module",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/llm-task",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"private": true,
|
||||
"description": "OpenClaw JSON-only LLM task plugin",
|
||||
"type": "module",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/lobster",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.3.12
|
||||
|
||||
### Changes
|
||||
|
||||
- Version alignment with core OpenClaw release numbers.
|
||||
|
||||
## 2026.3.11
|
||||
|
||||
### Changes
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/matrix",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"description": "OpenClaw Matrix channel plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
@ -8,7 +8,7 @@
|
||||
"@matrix-org/matrix-sdk-crypto-nodejs": "^0.4.0",
|
||||
"@vector-im/matrix-bot-sdk": "0.8.0-element.3",
|
||||
"markdown-it": "14.1.1",
|
||||
"music-metadata": "^11.12.1",
|
||||
"music-metadata": "^11.12.3",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"openclaw": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/mattermost",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"description": "OpenClaw Mattermost channel plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/memory-core",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"private": true,
|
||||
"description": "OpenClaw core memory search plugin",
|
||||
"type": "module",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/memory-lancedb",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"private": true,
|
||||
"description": "OpenClaw LanceDB-backed long-term memory plugin with auto-recall/capture",
|
||||
"type": "module",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/minimax-portal-auth",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"private": true,
|
||||
"description": "OpenClaw MiniMax Portal OAuth provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.3.12
|
||||
|
||||
### Changes
|
||||
|
||||
- Version alignment with core OpenClaw release numbers.
|
||||
|
||||
## 2026.3.11
|
||||
|
||||
### Changes
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/msteams",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"description": "OpenClaw Microsoft Teams channel plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@ -175,6 +175,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
teamName,
|
||||
conversationId,
|
||||
channelName,
|
||||
allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg),
|
||||
});
|
||||
const senderGroupPolicy = resolveSenderScopedGroupPolicy({
|
||||
groupPolicy,
|
||||
|
||||
@ -50,7 +50,7 @@ describe("msteams policy", () => {
|
||||
expect(res.allowed).toBe(false);
|
||||
});
|
||||
|
||||
it("matches team and channel by name", () => {
|
||||
it("blocks team and channel name matches by default", () => {
|
||||
const cfg: MSTeamsConfig = {
|
||||
teams: {
|
||||
"My Team": {
|
||||
@ -69,6 +69,31 @@ describe("msteams policy", () => {
|
||||
conversationId: "ignored",
|
||||
});
|
||||
|
||||
expect(res.teamConfig).toBeUndefined();
|
||||
expect(res.channelConfig).toBeUndefined();
|
||||
expect(res.allowed).toBe(false);
|
||||
});
|
||||
|
||||
it("matches team and channel by name when dangerous name matching is enabled", () => {
|
||||
const cfg: MSTeamsConfig = {
|
||||
teams: {
|
||||
"My Team": {
|
||||
requireMention: true,
|
||||
channels: {
|
||||
"General Chat": { requireMention: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const res = resolveMSTeamsRouteConfig({
|
||||
cfg,
|
||||
teamName: "My Team",
|
||||
channelName: "General Chat",
|
||||
conversationId: "ignored",
|
||||
allowNameMatching: true,
|
||||
});
|
||||
|
||||
expect(res.teamConfig?.requireMention).toBe(true);
|
||||
expect(res.channelConfig?.requireMention).toBe(false);
|
||||
expect(res.allowed).toBe(true);
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
resolveToolsBySender,
|
||||
resolveChannelEntryMatchWithFallback,
|
||||
resolveNestedAllowlistDecision,
|
||||
isDangerousNameMatchingEnabled,
|
||||
} from "openclaw/plugin-sdk/msteams";
|
||||
|
||||
export type MSTeamsResolvedRouteConfig = {
|
||||
@ -35,6 +36,7 @@ export function resolveMSTeamsRouteConfig(params: {
|
||||
teamName?: string | null | undefined;
|
||||
conversationId?: string | null | undefined;
|
||||
channelName?: string | null | undefined;
|
||||
allowNameMatching?: boolean;
|
||||
}): MSTeamsResolvedRouteConfig {
|
||||
const teamId = params.teamId?.trim();
|
||||
const teamName = params.teamName?.trim();
|
||||
@ -44,8 +46,8 @@ export function resolveMSTeamsRouteConfig(params: {
|
||||
const allowlistConfigured = Object.keys(teams).length > 0;
|
||||
const teamCandidates = buildChannelKeyCandidates(
|
||||
teamId,
|
||||
teamName,
|
||||
teamName ? normalizeChannelSlug(teamName) : undefined,
|
||||
params.allowNameMatching ? teamName : undefined,
|
||||
params.allowNameMatching && teamName ? normalizeChannelSlug(teamName) : undefined,
|
||||
);
|
||||
const teamMatch = resolveChannelEntryMatchWithFallback({
|
||||
entries: teams,
|
||||
@ -58,8 +60,8 @@ export function resolveMSTeamsRouteConfig(params: {
|
||||
const channelAllowlistConfigured = Object.keys(channels).length > 0;
|
||||
const channelCandidates = buildChannelKeyCandidates(
|
||||
conversationId,
|
||||
channelName,
|
||||
channelName ? normalizeChannelSlug(channelName) : undefined,
|
||||
params.allowNameMatching ? channelName : undefined,
|
||||
params.allowNameMatching && channelName ? normalizeChannelSlug(channelName) : undefined,
|
||||
);
|
||||
const channelMatch = resolveChannelEntryMatchWithFallback({
|
||||
entries: channels,
|
||||
@ -101,6 +103,7 @@ export function resolveMSTeamsGroupToolPolicy(
|
||||
const groupId = params.groupId?.trim();
|
||||
const groupChannel = params.groupChannel?.trim();
|
||||
const groupSpace = params.groupSpace?.trim();
|
||||
const allowNameMatching = isDangerousNameMatchingEnabled(cfg);
|
||||
|
||||
const resolved = resolveMSTeamsRouteConfig({
|
||||
cfg,
|
||||
@ -108,6 +111,7 @@ export function resolveMSTeamsGroupToolPolicy(
|
||||
teamName: groupSpace,
|
||||
conversationId: groupId,
|
||||
channelName: groupChannel,
|
||||
allowNameMatching,
|
||||
});
|
||||
|
||||
if (resolved.channelConfig) {
|
||||
@ -158,8 +162,8 @@ export function resolveMSTeamsGroupToolPolicy(
|
||||
|
||||
const channelCandidates = buildChannelKeyCandidates(
|
||||
groupId,
|
||||
groupChannel,
|
||||
groupChannel ? normalizeChannelSlug(groupChannel) : undefined,
|
||||
allowNameMatching ? groupChannel : undefined,
|
||||
allowNameMatching && groupChannel ? normalizeChannelSlug(groupChannel) : undefined,
|
||||
);
|
||||
for (const teamConfig of Object.values(cfg.teams ?? {})) {
|
||||
const match = resolveChannelEntryMatchWithFallback({
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/nextcloud-talk",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"description": "OpenClaw Nextcloud Talk channel plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.3.12
|
||||
|
||||
### Changes
|
||||
|
||||
- Version alignment with core OpenClaw release numbers.
|
||||
|
||||
## 2026.3.11
|
||||
|
||||
### Changes
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/nostr",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"description": "OpenClaw Nostr channel plugin for NIP-04 encrypted DMs",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@ -4,8 +4,10 @@ import {
|
||||
ensureOllamaModelPulled,
|
||||
OLLAMA_DEFAULT_BASE_URL,
|
||||
promptAndConfigureOllama,
|
||||
configureOllamaNonInteractive,
|
||||
type OpenClawPluginApi,
|
||||
type ProviderAuthContext,
|
||||
type ProviderAuthMethodNonInteractiveContext,
|
||||
type ProviderAuthResult,
|
||||
type ProviderDiscoveryContext,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
@ -50,6 +52,12 @@ const ollamaPlugin = {
|
||||
defaultModel: `ollama/${result.defaultModelId}`,
|
||||
};
|
||||
},
|
||||
runNonInteractive: async (ctx: ProviderAuthMethodNonInteractiveContext) =>
|
||||
configureOllamaNonInteractive({
|
||||
nextConfig: ctx.config,
|
||||
opts: ctx.opts,
|
||||
runtime: ctx.runtime,
|
||||
}),
|
||||
},
|
||||
],
|
||||
discovery: {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/open-prose",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"private": true,
|
||||
"description": "OpenProse VM skill pack plugin (slash command + telemetry).",
|
||||
"type": "module",
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import {
|
||||
buildSglangProvider,
|
||||
configureOpenAICompatibleSelfHostedProviderNonInteractive,
|
||||
emptyPluginConfigSchema,
|
||||
promptAndConfigureOpenAICompatibleSelfHostedProvider,
|
||||
type OpenClawPluginApi,
|
||||
type ProviderAuthContext,
|
||||
type ProviderAuthMethodNonInteractiveContext,
|
||||
type ProviderAuthResult,
|
||||
type ProviderDiscoveryContext,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
@ -49,6 +51,15 @@ const sglangPlugin = {
|
||||
defaultModel: result.modelRef,
|
||||
};
|
||||
},
|
||||
runNonInteractive: async (ctx: ProviderAuthMethodNonInteractiveContext) =>
|
||||
configureOpenAICompatibleSelfHostedProviderNonInteractive({
|
||||
ctx,
|
||||
providerId: PROVIDER_ID,
|
||||
providerLabel: "SGLang",
|
||||
defaultBaseUrl: DEFAULT_BASE_URL,
|
||||
defaultApiKeyEnvVar: "SGLANG_API_KEY",
|
||||
modelPlaceholder: "Qwen/Qwen3-8B",
|
||||
}),
|
||||
},
|
||||
],
|
||||
discovery: {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/signal",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"private": true,
|
||||
"description": "OpenClaw Signal channel plugin",
|
||||
"type": "module",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/slack",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"private": true,
|
||||
"description": "OpenClaw Slack channel plugin",
|
||||
"type": "module",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/synology-chat",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"description": "Synology Chat channel plugin for OpenClaw",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/telegram",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"private": true,
|
||||
"description": "OpenClaw Telegram channel plugin",
|
||||
"type": "module",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/tlon",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"description": "OpenClaw Tlon/Urbit channel plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.3.12
|
||||
|
||||
### Changes
|
||||
|
||||
- Version alignment with core OpenClaw release numbers.
|
||||
|
||||
## 2026.3.11
|
||||
|
||||
### Changes
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/twitch",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"description": "OpenClaw Twitch channel plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import {
|
||||
buildVllmProvider,
|
||||
configureOpenAICompatibleSelfHostedProviderNonInteractive,
|
||||
emptyPluginConfigSchema,
|
||||
promptAndConfigureOpenAICompatibleSelfHostedProvider,
|
||||
type OpenClawPluginApi,
|
||||
type ProviderAuthContext,
|
||||
type ProviderAuthMethodNonInteractiveContext,
|
||||
type ProviderAuthResult,
|
||||
type ProviderDiscoveryContext,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
@ -49,6 +51,15 @@ const vllmPlugin = {
|
||||
defaultModel: result.modelRef,
|
||||
};
|
||||
},
|
||||
runNonInteractive: async (ctx: ProviderAuthMethodNonInteractiveContext) =>
|
||||
configureOpenAICompatibleSelfHostedProviderNonInteractive({
|
||||
ctx,
|
||||
providerId: PROVIDER_ID,
|
||||
providerLabel: "vLLM",
|
||||
defaultBaseUrl: DEFAULT_BASE_URL,
|
||||
defaultApiKeyEnvVar: "VLLM_API_KEY",
|
||||
modelPlaceholder: "meta-llama/Meta-Llama-3-8B-Instruct",
|
||||
}),
|
||||
},
|
||||
],
|
||||
discovery: {
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.3.12
|
||||
|
||||
### Changes
|
||||
|
||||
- Version alignment with core OpenClaw release numbers.
|
||||
|
||||
## 2026.3.11
|
||||
|
||||
### Changes
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/voice-call",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"description": "OpenClaw voice-call plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/whatsapp",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"private": true,
|
||||
"description": "OpenClaw WhatsApp channel plugin",
|
||||
"type": "module",
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.3.12
|
||||
|
||||
### Changes
|
||||
|
||||
- Version alignment with core OpenClaw release numbers.
|
||||
|
||||
## 2026.3.11
|
||||
|
||||
### Changes
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "@openclaw/zalo",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"description": "OpenClaw Zalo channel plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"undici": "7.22.0",
|
||||
"undici": "7.24.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"openclaw": {
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.3.12
|
||||
|
||||
### Changes
|
||||
|
||||
- Version alignment with core OpenClaw release numbers.
|
||||
|
||||
## 2026.3.11
|
||||
|
||||
### Changes
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/zalouser",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"description": "OpenClaw Zalo Personal Account plugin via native zca-js integration",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@ -22,6 +22,7 @@ import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
deleteAccountFromConfigSection,
|
||||
formatAllowFromLowercase,
|
||||
isDangerousNameMatchingEnabled,
|
||||
isNumericTargetId,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
normalizeAccountId,
|
||||
@ -216,6 +217,7 @@ function resolveZalouserGroupPolicyEntry(params: ChannelGroupContext) {
|
||||
groupId: params.groupId,
|
||||
groupChannel: params.groupChannel,
|
||||
includeWildcard: true,
|
||||
allowNameMatching: isDangerousNameMatchingEnabled(account.config),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ const zalouserAccountSchema = z.object({
|
||||
enabled: z.boolean().optional(),
|
||||
markdown: MarkdownConfigSchema,
|
||||
profile: z.string().optional(),
|
||||
dangerouslyAllowNameMatching: z.boolean().optional(),
|
||||
dmPolicy: DmPolicySchema.optional(),
|
||||
allowFrom: AllowFromListSchema,
|
||||
historyLimit: z.number().int().min(0).optional(),
|
||||
|
||||
@ -23,6 +23,18 @@ describe("zalouser group policy helpers", () => {
|
||||
).toEqual(["123", "group:123", "chan-1", "Team Alpha", "team-alpha", "*"]);
|
||||
});
|
||||
|
||||
it("builds id-only candidates when name matching is disabled", () => {
|
||||
expect(
|
||||
buildZalouserGroupCandidates({
|
||||
groupId: "123",
|
||||
groupChannel: "chan-1",
|
||||
groupName: "Team Alpha",
|
||||
includeGroupIdAlias: true,
|
||||
allowNameMatching: false,
|
||||
}),
|
||||
).toEqual(["123", "group:123", "*"]);
|
||||
});
|
||||
|
||||
it("finds the first matching group entry", () => {
|
||||
const groups = {
|
||||
"group:123": { allow: true },
|
||||
|
||||
@ -23,6 +23,7 @@ export function buildZalouserGroupCandidates(params: {
|
||||
groupName?: string | null;
|
||||
includeGroupIdAlias?: boolean;
|
||||
includeWildcard?: boolean;
|
||||
allowNameMatching?: boolean;
|
||||
}): string[] {
|
||||
const seen = new Set<string>();
|
||||
const out: string[] = [];
|
||||
@ -43,10 +44,12 @@ export function buildZalouserGroupCandidates(params: {
|
||||
if (params.includeGroupIdAlias === true && groupId) {
|
||||
push(`group:${groupId}`);
|
||||
}
|
||||
push(groupChannel);
|
||||
push(groupName);
|
||||
if (groupName) {
|
||||
push(normalizeZalouserGroupSlug(groupName));
|
||||
if (params.allowNameMatching !== false) {
|
||||
push(groupChannel);
|
||||
push(groupName);
|
||||
if (groupName) {
|
||||
push(normalizeZalouserGroupSlug(groupName));
|
||||
}
|
||||
}
|
||||
if (params.includeWildcard !== false) {
|
||||
push("*");
|
||||
|
||||
@ -424,6 +424,73 @@ describe("zalouser monitor group mention gating", () => {
|
||||
expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not accept a different group id by matching only the mutable group name by default", async () => {
|
||||
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
||||
commandAuthorized: false,
|
||||
});
|
||||
await __testing.processMessage({
|
||||
message: createGroupMessage({
|
||||
threadId: "g-attacker-001",
|
||||
groupName: "Trusted Team",
|
||||
senderId: "666",
|
||||
hasAnyMention: true,
|
||||
wasExplicitlyMentioned: true,
|
||||
content: "ping @bot",
|
||||
}),
|
||||
account: {
|
||||
...createAccount(),
|
||||
config: {
|
||||
...createAccount().config,
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["*"],
|
||||
groups: {
|
||||
"group:g-trusted-001": { allow: true },
|
||||
"Trusted Team": { allow: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
config: createConfig(),
|
||||
runtime: createRuntimeEnv(),
|
||||
});
|
||||
|
||||
expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("accepts mutable group-name matches only when dangerouslyAllowNameMatching is enabled", async () => {
|
||||
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
||||
commandAuthorized: false,
|
||||
});
|
||||
await __testing.processMessage({
|
||||
message: createGroupMessage({
|
||||
threadId: "g-attacker-001",
|
||||
groupName: "Trusted Team",
|
||||
senderId: "666",
|
||||
hasAnyMention: true,
|
||||
wasExplicitlyMentioned: true,
|
||||
content: "ping @bot",
|
||||
}),
|
||||
account: {
|
||||
...createAccount(),
|
||||
config: {
|
||||
...createAccount().config,
|
||||
dangerouslyAllowNameMatching: true,
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["*"],
|
||||
groups: {
|
||||
"group:g-trusted-001": { allow: true },
|
||||
"Trusted Team": { allow: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
config: createConfig(),
|
||||
runtime: createRuntimeEnv(),
|
||||
});
|
||||
|
||||
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
|
||||
const callArg = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
|
||||
expect(callArg?.ctx?.To).toBe("zalouser:group:g-attacker-001");
|
||||
});
|
||||
|
||||
it("allows group control commands when sender is in groupAllowFrom", async () => {
|
||||
const { dispatchReplyWithBufferedBlockDispatcher, resolveCommandAuthorizedFromAuthorizers } =
|
||||
installRuntime({
|
||||
|
||||
@ -19,6 +19,7 @@ import {
|
||||
createScopedPairingAccess,
|
||||
createReplyPrefixOptions,
|
||||
evaluateGroupRouteAccessForPolicy,
|
||||
isDangerousNameMatchingEnabled,
|
||||
issuePairingChallenge,
|
||||
resolveOutboundMediaUrls,
|
||||
mergeAllowlist,
|
||||
@ -212,6 +213,7 @@ function resolveGroupRequireMention(params: {
|
||||
groupId: string;
|
||||
groupName?: string | null;
|
||||
groups: Record<string, { allow?: boolean; enabled?: boolean; requireMention?: boolean }>;
|
||||
allowNameMatching?: boolean;
|
||||
}): boolean {
|
||||
const entry = findZalouserGroupEntry(
|
||||
params.groups ?? {},
|
||||
@ -220,6 +222,7 @@ function resolveGroupRequireMention(params: {
|
||||
groupName: params.groupName,
|
||||
includeGroupIdAlias: true,
|
||||
includeWildcard: true,
|
||||
allowNameMatching: params.allowNameMatching,
|
||||
}),
|
||||
);
|
||||
if (typeof entry?.requireMention === "boolean") {
|
||||
@ -316,6 +319,7 @@ async function processMessage(
|
||||
});
|
||||
|
||||
const groups = account.config.groups ?? {};
|
||||
const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
|
||||
if (isGroup) {
|
||||
const groupEntry = findZalouserGroupEntry(
|
||||
groups,
|
||||
@ -324,6 +328,7 @@ async function processMessage(
|
||||
groupName,
|
||||
includeGroupIdAlias: true,
|
||||
includeWildcard: true,
|
||||
allowNameMatching,
|
||||
}),
|
||||
);
|
||||
const routeAccess = evaluateGroupRouteAccessForPolicy({
|
||||
@ -466,6 +471,7 @@ async function processMessage(
|
||||
groupId: chatId,
|
||||
groupName,
|
||||
groups,
|
||||
allowNameMatching,
|
||||
})
|
||||
: false;
|
||||
const mentionRegexes = core.channel.mentions.buildMentionRegexes(config, route.agentId);
|
||||
|
||||
@ -97,6 +97,7 @@ type ZalouserSharedConfig = {
|
||||
enabled?: boolean;
|
||||
name?: string;
|
||||
profile?: string;
|
||||
dangerouslyAllowNameMatching?: boolean;
|
||||
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
||||
allowFrom?: Array<string | number>;
|
||||
historyLimit?: number;
|
||||
|
||||
18
package.json
18
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openclaw",
|
||||
"version": "2026.3.11",
|
||||
"version": "2026.3.12",
|
||||
"description": "Multi-channel AI gateway with extensible messaging integrations",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/openclaw/openclaw#readme",
|
||||
@ -339,7 +339,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.16.1",
|
||||
"@aws-sdk/client-bedrock": "^3.1007.0",
|
||||
"@aws-sdk/client-bedrock": "^3.1008.0",
|
||||
"@buape/carbon": "0.0.0-beta-20260216184201",
|
||||
"@clack/prompts": "^1.1.0",
|
||||
"@discordjs/voice": "^0.19.1",
|
||||
@ -388,7 +388,7 @@
|
||||
"sqlite-vec": "0.1.7-alpha.2",
|
||||
"tar": "7.5.11",
|
||||
"tslog": "^4.10.2",
|
||||
"undici": "^7.22.0",
|
||||
"undici": "^7.24.0",
|
||||
"ws": "^8.19.0",
|
||||
"yaml": "^2.8.2",
|
||||
"zod": "^4.3.6"
|
||||
@ -399,21 +399,21 @@
|
||||
"@lit/context": "^1.1.6",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/node": "^25.4.0",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/qrcode-terminal": "^0.12.2",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260311.1",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260312.1",
|
||||
"@vitest/coverage-v8": "^4.1.0",
|
||||
"jscpd": "4.0.8",
|
||||
"lit": "^3.3.2",
|
||||
"oxfmt": "0.38.0",
|
||||
"oxlint": "^1.53.0",
|
||||
"oxfmt": "0.40.0",
|
||||
"oxlint": "^1.55.0",
|
||||
"oxlint-tsgolint": "^0.16.0",
|
||||
"signal-utils": "0.21.1",
|
||||
"tsdown": "0.21.2",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.18"
|
||||
"vitest": "^4.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@napi-rs/canvas": "^0.1.89",
|
||||
|
||||
2048
pnpm-lock.yaml
generated
2048
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -73,7 +73,7 @@ fi
|
||||
if [[ "${PACKAGE_VERSION}" =~ ^([0-9]{4}\.[0-9]{1,2}\.[0-9]{1,2})([.-]?beta[.-][0-9]+)?$ ]]; then
|
||||
MARKETING_VERSION="${BASH_REMATCH[1]}"
|
||||
else
|
||||
echo "Unsupported package.json.version '${PACKAGE_VERSION}'. Expected 2026.3.11 or 2026.3.11-beta.1." >&2
|
||||
echo "Unsupported package.json.version '${PACKAGE_VERSION}'. Expected 2026.3.12 or 2026.3.12-beta.1." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
1
src/agents/auth-profiles.runtime.ts
Normal file
1
src/agents/auth-profiles.runtime.ts
Normal file
@ -0,0 +1 @@
|
||||
export { ensureAuthProfileStore } from "./auth-profiles.js";
|
||||
@ -157,7 +157,8 @@ function ensureContextWindowCacheLoaded(): Promise<void> {
|
||||
}
|
||||
|
||||
try {
|
||||
const { discoverAuthStorage, discoverModels } = await import("./pi-model-discovery.js");
|
||||
const { discoverAuthStorage, discoverModels } =
|
||||
await import("./pi-model-discovery-runtime.js");
|
||||
const agentDir = resolveOpenClawAgentDir();
|
||||
const authStorage = discoverAuthStorage(agentDir);
|
||||
const modelRegistry = discoverModels(authStorage, agentDir) as unknown as ModelRegistryLike;
|
||||
|
||||
@ -114,6 +114,55 @@ describe("loadModelCatalog", () => {
|
||||
expect(spark?.reasoning).toBe(true);
|
||||
});
|
||||
|
||||
it("filters stale openai gpt-5.3-codex-spark built-ins from the catalog", async () => {
|
||||
mockPiDiscoveryModels([
|
||||
{
|
||||
id: "gpt-5.3-codex-spark",
|
||||
provider: "openai",
|
||||
name: "GPT-5.3 Codex Spark",
|
||||
reasoning: true,
|
||||
contextWindow: 128000,
|
||||
input: ["text", "image"],
|
||||
},
|
||||
{
|
||||
id: "gpt-5.3-codex-spark",
|
||||
provider: "azure-openai-responses",
|
||||
name: "GPT-5.3 Codex Spark",
|
||||
reasoning: true,
|
||||
contextWindow: 128000,
|
||||
input: ["text", "image"],
|
||||
},
|
||||
{
|
||||
id: "gpt-5.3-codex-spark",
|
||||
provider: "openai-codex",
|
||||
name: "GPT-5.3 Codex Spark",
|
||||
reasoning: true,
|
||||
contextWindow: 128000,
|
||||
input: ["text"],
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await loadModelCatalog({ config: {} as OpenClawConfig });
|
||||
expect(result).not.toContainEqual(
|
||||
expect.objectContaining({
|
||||
provider: "openai",
|
||||
id: "gpt-5.3-codex-spark",
|
||||
}),
|
||||
);
|
||||
expect(result).not.toContainEqual(
|
||||
expect.objectContaining({
|
||||
provider: "azure-openai-responses",
|
||||
id: "gpt-5.3-codex-spark",
|
||||
}),
|
||||
);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
provider: "openai-codex",
|
||||
id: "gpt-5.3-codex-spark",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("adds gpt-5.4 forward-compat catalog entries when template models exist", async () => {
|
||||
mockPiDiscoveryModels([
|
||||
{
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { type OpenClawConfig, loadConfig } from "../config/config.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { resolveOpenClawAgentDir } from "./agent-paths.js";
|
||||
import { shouldSuppressBuiltInModel } from "./model-suppression.js";
|
||||
import { ensureOpenClawModelsJson } from "./models-config.js";
|
||||
|
||||
const log = createSubsystemLogger("model-catalog");
|
||||
@ -29,7 +30,7 @@ type PiSdkModule = typeof import("./pi-model-discovery.js");
|
||||
|
||||
let modelCatalogPromise: Promise<ModelCatalogEntry[]> | null = null;
|
||||
let hasLoggedModelCatalogError = false;
|
||||
const defaultImportPiSdk = () => import("./pi-model-discovery.js");
|
||||
const defaultImportPiSdk = () => import("./pi-model-discovery-runtime.js");
|
||||
let importPiSdk = defaultImportPiSdk;
|
||||
|
||||
const CODEX_PROVIDER = "openai-codex";
|
||||
@ -242,6 +243,9 @@ export async function loadModelCatalog(params?: {
|
||||
if (!provider) {
|
||||
continue;
|
||||
}
|
||||
if (shouldSuppressBuiltInModel({ provider, id })) {
|
||||
continue;
|
||||
}
|
||||
const name = String(entry?.name ?? id).trim() || id;
|
||||
const contextWindow =
|
||||
typeof entry?.contextWindow === "number" && entry.contextWindow > 0
|
||||
|
||||
@ -16,6 +16,9 @@ const OPENAI_CODEX_GPT_54_CONTEXT_TOKENS = 1_050_000;
|
||||
const OPENAI_CODEX_GPT_54_MAX_TOKENS = 128_000;
|
||||
const OPENAI_CODEX_GPT_54_TEMPLATE_MODEL_IDS = ["gpt-5.3-codex", "gpt-5.2-codex"] as const;
|
||||
const OPENAI_CODEX_GPT_53_MODEL_ID = "gpt-5.3-codex";
|
||||
const OPENAI_CODEX_GPT_53_SPARK_MODEL_ID = "gpt-5.3-codex-spark";
|
||||
const OPENAI_CODEX_GPT_53_SPARK_CONTEXT_TOKENS = 128_000;
|
||||
const OPENAI_CODEX_GPT_53_SPARK_MAX_TOKENS = 128_000;
|
||||
const OPENAI_CODEX_TEMPLATE_MODEL_IDS = ["gpt-5.2-codex"] as const;
|
||||
|
||||
const ANTHROPIC_OPUS_46_MODEL_ID = "claude-opus-4-6";
|
||||
@ -133,6 +136,19 @@ function resolveOpenAICodexForwardCompatModel(
|
||||
contextWindow: OPENAI_CODEX_GPT_54_CONTEXT_TOKENS,
|
||||
maxTokens: OPENAI_CODEX_GPT_54_MAX_TOKENS,
|
||||
};
|
||||
} else if (lower === OPENAI_CODEX_GPT_53_SPARK_MODEL_ID) {
|
||||
templateIds = [OPENAI_CODEX_GPT_53_MODEL_ID, ...OPENAI_CODEX_TEMPLATE_MODEL_IDS];
|
||||
eligibleProviders = CODEX_GPT54_ELIGIBLE_PROVIDERS;
|
||||
patch = {
|
||||
api: "openai-codex-responses",
|
||||
provider: normalizedProvider,
|
||||
baseUrl: "https://chatgpt.com/backend-api",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: OPENAI_CODEX_GPT_53_SPARK_CONTEXT_TOKENS,
|
||||
maxTokens: OPENAI_CODEX_GPT_53_SPARK_MAX_TOKENS,
|
||||
};
|
||||
} else if (lower === OPENAI_CODEX_GPT_53_MODEL_ID) {
|
||||
templateIds = OPENAI_CODEX_TEMPLATE_MODEL_IDS;
|
||||
eligibleProviders = CODEX_GPT53_ELIGIBLE_PROVIDERS;
|
||||
|
||||
27
src/agents/model-suppression.ts
Normal file
27
src/agents/model-suppression.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { normalizeProviderId } from "./model-selection.js";
|
||||
|
||||
const OPENAI_DIRECT_SPARK_MODEL_ID = "gpt-5.3-codex-spark";
|
||||
const SUPPRESSED_SPARK_PROVIDERS = new Set(["openai", "azure-openai-responses"]);
|
||||
|
||||
export function shouldSuppressBuiltInModel(params: {
|
||||
provider?: string | null;
|
||||
id?: string | null;
|
||||
}) {
|
||||
const provider = normalizeProviderId(params.provider?.trim().toLowerCase() ?? "");
|
||||
const id = params.id?.trim().toLowerCase() ?? "";
|
||||
|
||||
// pi-ai still ships non-Codex Spark rows, but OpenClaw treats Spark as
|
||||
// Codex-only until upstream availability is proven on direct API paths.
|
||||
return SUPPRESSED_SPARK_PROVIDERS.has(provider) && id === OPENAI_DIRECT_SPARK_MODEL_ID;
|
||||
}
|
||||
|
||||
export function buildSuppressedBuiltInModelError(params: {
|
||||
provider?: string | null;
|
||||
id?: string | null;
|
||||
}): string | undefined {
|
||||
if (!shouldSuppressBuiltInModel(params)) {
|
||||
return undefined;
|
||||
}
|
||||
const provider = normalizeProviderId(params.provider?.trim().toLowerCase() ?? "") || "openai";
|
||||
return `Unknown model: ${provider}/${OPENAI_DIRECT_SPARK_MODEL_ID}. ${OPENAI_DIRECT_SPARK_MODEL_ID} is only supported via openai-codex OAuth. Use openai-codex/${OPENAI_DIRECT_SPARK_MODEL_ID}.`;
|
||||
}
|
||||
@ -11,6 +11,7 @@ import {
|
||||
} from "./live-auth-keys.js";
|
||||
import { isModernModelRef } from "./live-model-filter.js";
|
||||
import { getApiKeyForModel, requireApiKey } from "./model-auth.js";
|
||||
import { shouldSuppressBuiltInModel } from "./model-suppression.js";
|
||||
import { ensureOpenClawModelsJson } from "./models-config.js";
|
||||
import { isRateLimitErrorMessage } from "./pi-embedded-helpers/errors.js";
|
||||
import { discoverAuthStorage, discoverModels } from "./pi-model-discovery.js";
|
||||
@ -202,6 +203,31 @@ function resolveTestReasoning(
|
||||
return "low";
|
||||
}
|
||||
|
||||
function resolveLiveSystemPrompt(model: Model<Api>): string | undefined {
|
||||
if (model.provider === "openai-codex") {
|
||||
return "You are a concise assistant. Follow the user's instruction exactly.";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
describe("resolveLiveSystemPrompt", () => {
|
||||
it("adds instructions for openai-codex probes", () => {
|
||||
expect(
|
||||
resolveLiveSystemPrompt({
|
||||
provider: "openai-codex",
|
||||
} as Model<Api>),
|
||||
).toContain("Follow the user's instruction exactly.");
|
||||
});
|
||||
|
||||
it("keeps other providers unchanged", () => {
|
||||
expect(
|
||||
resolveLiveSystemPrompt({
|
||||
provider: "openai",
|
||||
} as Model<Api>),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
async function completeSimpleWithTimeout<TApi extends Api>(
|
||||
model: Model<TApi>,
|
||||
context: Parameters<typeof completeSimple<TApi>>[1],
|
||||
@ -246,6 +272,7 @@ async function completeOkWithRetry(params: {
|
||||
const res = await completeSimpleWithTimeout(
|
||||
params.model,
|
||||
{
|
||||
systemPrompt: resolveLiveSystemPrompt(params.model),
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
@ -317,6 +344,9 @@ describeLive("live models (profile keys)", () => {
|
||||
}> = [];
|
||||
|
||||
for (const model of models) {
|
||||
if (shouldSuppressBuiltInModel({ provider: model.provider, id: model.id })) {
|
||||
continue;
|
||||
}
|
||||
if (providers && !providers.has(model.provider)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -58,6 +58,16 @@ describe("pi embedded model e2e smoke", () => {
|
||||
expect(result.model).toMatchObject(buildOpenAICodexForwardCompatExpectation("gpt-5.4"));
|
||||
});
|
||||
|
||||
it("builds an openai-codex forward-compat fallback for gpt-5.3-codex-spark", () => {
|
||||
mockOpenAICodexTemplateModel();
|
||||
|
||||
const result = resolveModel("openai-codex", "gpt-5.3-codex-spark", "/tmp/agent");
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.model).toMatchObject(
|
||||
buildOpenAICodexForwardCompatExpectation("gpt-5.3-codex-spark"),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps unknown-model errors for non-forward-compat IDs", () => {
|
||||
const result = resolveModel("openai-codex", "gpt-4.1-mini", "/tmp/agent");
|
||||
expect(result.model).toBeUndefined();
|
||||
|
||||
@ -35,15 +35,25 @@ export function mockOpenAICodexTemplateModel(): void {
|
||||
|
||||
export function buildOpenAICodexForwardCompatExpectation(
|
||||
id: string = "gpt-5.3-codex",
|
||||
): Partial<typeof OPENAI_CODEX_TEMPLATE_MODEL> & { provider: string; id: string } {
|
||||
): Partial<ModelDefinitionConfig> & {
|
||||
provider: string;
|
||||
id: string;
|
||||
api: string;
|
||||
baseUrl: string;
|
||||
} {
|
||||
const isGpt54 = id === "gpt-5.4";
|
||||
const isSpark = id === "gpt-5.3-codex-spark";
|
||||
return {
|
||||
provider: "openai-codex",
|
||||
id,
|
||||
api: "openai-codex-responses",
|
||||
baseUrl: "https://chatgpt.com/backend-api",
|
||||
reasoning: true,
|
||||
contextWindow: isGpt54 ? 1_050_000 : 272000,
|
||||
input: isSpark ? ["text"] : ["text", "image"],
|
||||
cost: isSpark
|
||||
? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
|
||||
: OPENAI_CODEX_TEMPLATE_MODEL.cost,
|
||||
contextWindow: isGpt54 ? 1_050_000 : isSpark ? 128_000 : 272000,
|
||||
maxTokens: 128000,
|
||||
};
|
||||
}
|
||||
|
||||
@ -546,6 +546,60 @@ describe("resolveModel", () => {
|
||||
expect(result.model).toMatchObject(buildOpenAICodexForwardCompatExpectation("gpt-5.4"));
|
||||
});
|
||||
|
||||
it("builds an openai-codex fallback for gpt-5.3-codex-spark", () => {
|
||||
mockOpenAICodexTemplateModel();
|
||||
|
||||
const result = resolveModel("openai-codex", "gpt-5.3-codex-spark", "/tmp/agent");
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.model).toMatchObject(
|
||||
buildOpenAICodexForwardCompatExpectation("gpt-5.3-codex-spark"),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps openai-codex gpt-5.3-codex-spark when discovery provides it", () => {
|
||||
mockDiscoveredModel({
|
||||
provider: "openai-codex",
|
||||
modelId: "gpt-5.3-codex-spark",
|
||||
templateModel: {
|
||||
...buildOpenAICodexForwardCompatExpectation("gpt-5.3-codex-spark"),
|
||||
name: "GPT-5.3 Codex Spark",
|
||||
input: ["text"],
|
||||
},
|
||||
});
|
||||
|
||||
const result = resolveModel("openai-codex", "gpt-5.3-codex-spark", "/tmp/agent");
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.model).toMatchObject({
|
||||
provider: "openai-codex",
|
||||
id: "gpt-5.3-codex-spark",
|
||||
api: "openai-codex-responses",
|
||||
baseUrl: "https://chatgpt.com/backend-api",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects stale direct openai gpt-5.3-codex-spark discovery rows", () => {
|
||||
mockDiscoveredModel({
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.3-codex-spark",
|
||||
templateModel: buildForwardCompatTemplate({
|
||||
id: "gpt-5.3-codex-spark",
|
||||
name: "GPT-5.3 Codex Spark",
|
||||
provider: "openai",
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
}),
|
||||
});
|
||||
|
||||
const result = resolveModel("openai", "gpt-5.3-codex-spark", "/tmp/agent");
|
||||
|
||||
expect(result.model).toBeUndefined();
|
||||
expect(result.error).toBe(
|
||||
"Unknown model: openai/gpt-5.3-codex-spark. gpt-5.3-codex-spark is only supported via openai-codex OAuth. Use openai-codex/gpt-5.3-codex-spark.",
|
||||
);
|
||||
});
|
||||
|
||||
it("applies provider overrides to openai gpt-5.4 forward-compat models", () => {
|
||||
mockDiscoveredModel({
|
||||
provider: "openai",
|
||||
@ -725,6 +779,24 @@ describe("resolveModel", () => {
|
||||
expectUnknownModelError("openai-codex", "gpt-4.1-mini");
|
||||
});
|
||||
|
||||
it("rejects direct openai gpt-5.3-codex-spark with a codex-only hint", () => {
|
||||
const result = resolveModel("openai", "gpt-5.3-codex-spark", "/tmp/agent");
|
||||
|
||||
expect(result.model).toBeUndefined();
|
||||
expect(result.error).toBe(
|
||||
"Unknown model: openai/gpt-5.3-codex-spark. gpt-5.3-codex-spark is only supported via openai-codex OAuth. Use openai-codex/gpt-5.3-codex-spark.",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects azure openai gpt-5.3-codex-spark with a codex-only hint", () => {
|
||||
const result = resolveModel("azure-openai-responses", "gpt-5.3-codex-spark", "/tmp/agent");
|
||||
|
||||
expect(result.model).toBeUndefined();
|
||||
expect(result.error).toBe(
|
||||
"Unknown model: azure-openai-responses/gpt-5.3-codex-spark. gpt-5.3-codex-spark is only supported via openai-codex OAuth. Use openai-codex/gpt-5.3-codex-spark.",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses codex fallback even when openai-codex provider is configured", () => {
|
||||
// This test verifies the ordering: codex fallback must fire BEFORE the generic providerCfg fallback.
|
||||
// If ordering is wrong, the generic fallback would use api: "openai-responses" (the default)
|
||||
|
||||
@ -8,6 +8,10 @@ import { buildModelAliasLines } from "../model-alias-lines.js";
|
||||
import { isSecretRefHeaderValueMarker } from "../model-auth-markers.js";
|
||||
import { resolveForwardCompatModel } from "../model-forward-compat.js";
|
||||
import { findNormalizedProviderValue, normalizeProviderId } from "../model-selection.js";
|
||||
import {
|
||||
buildSuppressedBuiltInModelError,
|
||||
shouldSuppressBuiltInModel,
|
||||
} from "../model-suppression.js";
|
||||
import { discoverAuthStorage, discoverModels } from "../pi-model-discovery.js";
|
||||
import { normalizeResolvedProviderModel } from "./model.provider-normalization.js";
|
||||
|
||||
@ -159,6 +163,9 @@ export function resolveModelWithRegistry(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
}): Model<Api> | undefined {
|
||||
const { provider, modelId, modelRegistry, cfg } = params;
|
||||
if (shouldSuppressBuiltInModel({ provider, id: modelId })) {
|
||||
return undefined;
|
||||
}
|
||||
const providerConfig = resolveConfiguredProviderConfig(cfg, provider);
|
||||
const model = modelRegistry.find(provider, modelId) as Model<Api> | null;
|
||||
|
||||
@ -303,6 +310,10 @@ const LOCAL_PROVIDER_HINTS: Record<string, string> = {
|
||||
};
|
||||
|
||||
function buildUnknownModelError(provider: string, modelId: string): string {
|
||||
const suppressed = buildSuppressedBuiltInModelError({ provider, id: modelId });
|
||||
if (suppressed) {
|
||||
return suppressed;
|
||||
}
|
||||
const base = `Unknown model: ${provider}/${modelId}`;
|
||||
const hint = LOCAL_PROVIDER_HINTS[provider.toLowerCase()];
|
||||
return hint ? `${base}. ${hint}` : base;
|
||||
|
||||
@ -1 +1,6 @@
|
||||
export { discoverAuthStorage, discoverModels } from "./pi-model-discovery.js";
|
||||
export {
|
||||
AuthStorage,
|
||||
discoverAuthStorage,
|
||||
discoverModels,
|
||||
ModelRegistry,
|
||||
} from "./pi-model-discovery.js";
|
||||
|
||||
@ -365,7 +365,7 @@ export async function createModelSelectionState(params: {
|
||||
}
|
||||
|
||||
if (sessionEntry && sessionStore && sessionKey && sessionEntry.authProfileOverride) {
|
||||
const { ensureAuthProfileStore } = await import("../../agents/auth-profiles.js");
|
||||
const { ensureAuthProfileStore } = await import("../../agents/auth-profiles.runtime.js");
|
||||
const store = ensureAuthProfileStore(undefined, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
|
||||
@ -512,13 +512,15 @@ describe("update-cli", () => {
|
||||
call[0][1] === "i" &&
|
||||
call[0][2] === "-g",
|
||||
);
|
||||
const mergedPath = updateCall?.[1]?.env?.Path ?? updateCall?.[1]?.env?.PATH ?? "";
|
||||
const updateOptions =
|
||||
typeof updateCall?.[1] === "object" && updateCall[1] !== null ? updateCall[1] : undefined;
|
||||
const mergedPath = updateOptions?.env?.Path ?? updateOptions?.env?.PATH ?? "";
|
||||
expect(mergedPath.split(path.delimiter).slice(0, 2)).toEqual([
|
||||
portableGitMingw,
|
||||
portableGitUsr,
|
||||
]);
|
||||
expect(updateCall?.[1]?.env?.NPM_CONFIG_SCRIPT_SHELL).toBe("cmd.exe");
|
||||
expect(updateCall?.[1]?.env?.NODE_LLAMA_CPP_SKIP_DOWNLOAD).toBe("1");
|
||||
expect(updateOptions?.env?.NPM_CONFIG_SCRIPT_SHELL).toBe("cmd.exe");
|
||||
expect(updateOptions?.env?.NODE_LLAMA_CPP_SKIP_DOWNLOAD).toBe("1");
|
||||
});
|
||||
|
||||
it("uses OPENCLAW_UPDATE_PACKAGE_SPEC for package updates", async () => {
|
||||
|
||||
@ -107,6 +107,40 @@ describe("doctor config flow", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("warns on mutable Zalouser group entries when dangerous name matching is disabled", async () => {
|
||||
const doctorWarnings = await collectDoctorWarnings({
|
||||
channels: {
|
||||
zalouser: {
|
||||
groups: {
|
||||
"Ops Room": { allow: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
doctorWarnings.some(
|
||||
(line) =>
|
||||
line.includes("mutable allowlist") && line.includes("channels.zalouser.groups: Ops Room"),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not warn on mutable Zalouser group entries when dangerous name matching is enabled", async () => {
|
||||
const doctorWarnings = await collectDoctorWarnings({
|
||||
channels: {
|
||||
zalouser: {
|
||||
dangerouslyAllowNameMatching: true,
|
||||
groups: {
|
||||
"Ops Room": { allow: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(doctorWarnings.some((line) => line.includes("channels.zalouser.groups"))).toBe(false);
|
||||
});
|
||||
|
||||
it("warns when imessage group allowlist is empty even if allowFrom is set", async () => {
|
||||
const doctorWarnings = await collectDoctorWarnings({
|
||||
channels: {
|
||||
|
||||
@ -44,6 +44,7 @@ import {
|
||||
isMSTeamsMutableAllowEntry,
|
||||
isMattermostMutableAllowEntry,
|
||||
isSlackMutableAllowEntry,
|
||||
isZalouserMutableGroupEntry,
|
||||
} from "../security/mutable-allowlist-detectors.js";
|
||||
import { inspectTelegramAccount } from "../telegram/account-inspect.js";
|
||||
import { listTelegramAccountIds, resolveTelegramAccount } from "../telegram/accounts.js";
|
||||
@ -885,6 +886,27 @@ function scanMutableAllowlistEntries(cfg: OpenClawConfig): MutableAllowlistHit[]
|
||||
}
|
||||
}
|
||||
|
||||
for (const scope of collectProviderDangerousNameMatchingScopes(cfg, "zalouser")) {
|
||||
if (scope.dangerousNameMatchingEnabled) {
|
||||
continue;
|
||||
}
|
||||
const groups = asObjectRecord(scope.account.groups);
|
||||
if (!groups) {
|
||||
continue;
|
||||
}
|
||||
for (const entry of Object.keys(groups)) {
|
||||
if (!isZalouserMutableGroupEntry(entry)) {
|
||||
continue;
|
||||
}
|
||||
hits.push({
|
||||
channel: "zalouser",
|
||||
path: `${scope.prefix}.groups`,
|
||||
entry,
|
||||
dangerousFlagPath: scope.dangerousFlagPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return hits;
|
||||
}
|
||||
|
||||
|
||||
@ -163,6 +163,30 @@ describe("models list/status", () => {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
contextWindow: 128000,
|
||||
};
|
||||
const OPENAI_SPARK_MODEL = {
|
||||
provider: "openai",
|
||||
id: "gpt-5.3-codex-spark",
|
||||
name: "GPT-5.3 Codex Spark",
|
||||
input: ["text", "image"],
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
contextWindow: 128000,
|
||||
};
|
||||
const OPENAI_CODEX_SPARK_MODEL = {
|
||||
provider: "openai-codex",
|
||||
id: "gpt-5.3-codex-spark",
|
||||
name: "GPT-5.3 Codex Spark",
|
||||
input: ["text"],
|
||||
baseUrl: "https://chatgpt.com/backend-api",
|
||||
contextWindow: 128000,
|
||||
};
|
||||
const AZURE_OPENAI_SPARK_MODEL = {
|
||||
provider: "azure-openai-responses",
|
||||
id: "gpt-5.3-codex-spark",
|
||||
name: "GPT-5.3 Codex Spark",
|
||||
input: ["text", "image"],
|
||||
baseUrl: "https://example.openai.azure.com/openai/v1",
|
||||
contextWindow: 128000,
|
||||
};
|
||||
const GOOGLE_ANTIGRAVITY_TEMPLATE_BASE = {
|
||||
provider: "google-antigravity",
|
||||
api: "google-gemini-cli",
|
||||
@ -363,6 +387,34 @@ describe("models list/status", () => {
|
||||
expect(ensureOpenClawModelsJson).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("filters stale direct OpenAI spark rows from models list and registry views", async () => {
|
||||
setDefaultModel("openai-codex/gpt-5.3-codex-spark");
|
||||
modelRegistryState.models = [
|
||||
OPENAI_SPARK_MODEL,
|
||||
AZURE_OPENAI_SPARK_MODEL,
|
||||
OPENAI_CODEX_SPARK_MODEL,
|
||||
];
|
||||
modelRegistryState.available = [
|
||||
OPENAI_SPARK_MODEL,
|
||||
AZURE_OPENAI_SPARK_MODEL,
|
||||
OPENAI_CODEX_SPARK_MODEL,
|
||||
];
|
||||
const runtime = makeRuntime();
|
||||
|
||||
await modelsListCommand({ all: true, json: true }, runtime);
|
||||
|
||||
const payload = parseJsonLog(runtime);
|
||||
expect(payload.models.map((model: { key: string }) => model.key)).toEqual([
|
||||
"openai-codex/gpt-5.3-codex-spark",
|
||||
]);
|
||||
|
||||
const loaded = await loadModelRegistry({} as never);
|
||||
expect(loaded.models.map((model) => `${model.provider}/${model.id}`)).toEqual([
|
||||
"openai-codex/gpt-5.3-codex-spark",
|
||||
]);
|
||||
expect(Array.from(loaded.availableKeys ?? [])).toEqual(["openai-codex/gpt-5.3-codex-spark"]);
|
||||
});
|
||||
|
||||
it("modelsListCommand persists using the write snapshot config when provided", async () => {
|
||||
modelRegistryState.models = [OPENAI_MODEL];
|
||||
modelRegistryState.available = [OPENAI_MODEL];
|
||||
|
||||
@ -347,5 +347,55 @@ describe("modelsListCommand forward-compat", () => {
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("suppresses direct openai gpt-5.3-codex-spark rows in --all output", async () => {
|
||||
mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] });
|
||||
mocks.loadModelRegistry.mockResolvedValueOnce({
|
||||
models: [
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-5.3-codex-spark",
|
||||
name: "GPT-5.3 Codex Spark",
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
input: ["text", "image"],
|
||||
contextWindow: 128000,
|
||||
maxTokens: 32000,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
},
|
||||
{
|
||||
provider: "azure-openai-responses",
|
||||
id: "gpt-5.3-codex-spark",
|
||||
name: "GPT-5.3 Codex Spark",
|
||||
api: "azure-openai-responses",
|
||||
baseUrl: "https://example.openai.azure.com/openai/v1",
|
||||
input: ["text", "image"],
|
||||
contextWindow: 128000,
|
||||
maxTokens: 32000,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
},
|
||||
{ ...OPENAI_CODEX_53_MODEL },
|
||||
],
|
||||
availableKeys: new Set([
|
||||
"openai/gpt-5.3-codex-spark",
|
||||
"azure-openai-responses/gpt-5.3-codex-spark",
|
||||
"openai-codex/gpt-5.3-codex",
|
||||
]),
|
||||
registry: {
|
||||
getAll: () => [{ ...OPENAI_CODEX_53_MODEL }],
|
||||
},
|
||||
});
|
||||
mocks.loadModelCatalog.mockResolvedValueOnce([]);
|
||||
const runtime = createRuntime();
|
||||
|
||||
await modelsListCommand({ all: true, json: true }, runtime as never);
|
||||
|
||||
expect(mocks.printModelTable).toHaveBeenCalled();
|
||||
expect(lastPrintedRows<{ key: string }>()).toEqual([
|
||||
expect.objectContaining({
|
||||
key: "openai-codex/gpt-5.3-codex",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -25,7 +25,7 @@ export async function modelsListCommand(
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
ensureFlagCompatibility(opts);
|
||||
const { ensureAuthProfileStore } = await import("../../agents/auth-profiles.js");
|
||||
const { ensureAuthProfileStore } = await import("../../agents/auth-profiles.runtime.js");
|
||||
const { ensureOpenClawModelsJson } = await import("../../agents/models-config.js");
|
||||
const { sourceConfig, resolvedConfig: cfg } = await loadModelsConfigWithSource({
|
||||
commandName: "models list",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user