Skip to content

Commit 96aad96

Browse files
fix: land NO_REPLY announce suppression and auth scope assertions
Landed follow-up for #27535 and aligned shared-auth gateway expectations after #27498. Co-authored-by: kevinWangSheng <[email protected]>
1 parent eb9a968 commit 96aad96

File tree

5 files changed

+50
-18
lines changed

5 files changed

+50
-18
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ Docs: https://docs.openclaw.ai
1717
- Cron/Hooks isolated routing: preserve canonical `agent:*` session keys in isolated runs so already-qualified keys are not double-prefixed (for example `agent:main:main` no longer becomes `agent:main:agent:main:main`). Landed from contributor PR #27333 by @MaheshBhushan. (#27289, #27282)
1818
- iOS/Talk mode: stop injecting the voice directive hint into iOS Talk prompts and remove the Voice Directive Hint setting, reducing model bias toward tool-style TTS directives and keeping relay responses text-first by default. (#27543) thanks @ngutman.
1919
- Queue/Drain/Cron reliability: harden lane draining with guaranteed `draining` flag reset on synchronous pump failures, reject new queue enqueues during gateway restart drain windows (instead of silently killing accepted tasks), add `/stop` queued-backlog cutoff metadata with stale-message skipping (while avoiding cross-session native-stop cutoff bleed), and raise isolated cron `agentTurn` outer safety timeout to avoid false 10-minute timeout races against longer agent session timeouts. (#27407, #27332, #27427)
20+
- Typing/Dispatch idle: force typing cleanup when `markDispatchIdle` never arrives after run completion, avoiding leaked typing keepalive loops in cron/announce edges. Landed from contributor PR #27541 by @Sid-Qin. (#27493)
21+
- Telegram native commands: degrade command registration on `BOT_COMMANDS_TOO_MUCH` by retrying with fewer commands instead of crash-looping startup sync. Landed from contributor PR #27512 by @Sid-Qin. (#27456)
22+
- Config/Plugins entries: treat unknown `plugins.entries.*` ids as startup warnings (ignored stale keys) instead of hard validation failures that can crash-loop gateway boot. Landed from contributor PR #27506 by @Sid-Qin. (#27455)
23+
- Sessions cleanup/Doctor: add `openclaw sessions cleanup --fix-missing` to prune store entries whose transcript files are missing, including doctor guidance and CLI coverage. Landed from contributor PR #27508 by @Sid-Qin. (#27422)
24+
- Azure OpenAI Responses: force `store=true` for `azure-openai-responses` direct responses API calls to avoid multi-turn 400 failures. Landed from contributor PR #27499 by @polarbear-Yang. (#27497)
25+
- Gateway shared-auth scopes: preserve requested operator scopes for shared-token clients when device identity is unavailable, instead of clearing scopes during auth handling. Landed from contributor PR #27498 by @kevinWangSheng. (#27494)
26+
- NO_REPLY suppression: suppress `NO_REPLY` before Slack API send and in sub-agent announce completion flow so sentinel text no longer leaks into user channels. Landed from contributor PRs #27529 (by @Sid-Qin) and #27535 (rewritten minimal landing by maintainers). (#27387, #27531)
2027
- Security/Plugin channel HTTP auth: normalize protected `/api/channels` path checks against canonicalized request paths (case + percent-decoding + slash normalization), and fail closed on malformed `%`-encoded channel prefixes so alternate-path variants cannot bypass gateway auth.
2128
- Security/Exec approvals forwarding: prefer turn-source channel/account/thread metadata when resolving approval delivery targets so stale session routes do not misroute approval prompts.
2229
- Security/Sandbox path alias guard: reject broken symlink targets by resolving through existing ancestors and failing closed on out-of-root targets, preventing workspace-only `apply_patch` writes from escaping sandbox/workspace boundaries via dangling symlinks. This ships in the next npm release (`2026.2.26`). Thanks @tdjackey for reporting.

src/agents/pi-embedded-runner/extra-params.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,10 +184,16 @@ function isDirectOpenAIBaseUrl(baseUrl: unknown): boolean {
184184

185185
try {
186186
const host = new URL(baseUrl).hostname.toLowerCase();
187-
return host === "api.openai.com" || host === "chatgpt.com" || host.endsWith(".openai.azure.com");
187+
return (
188+
host === "api.openai.com" || host === "chatgpt.com" || host.endsWith(".openai.azure.com")
189+
);
188190
} catch {
189191
const normalized = baseUrl.toLowerCase();
190-
return normalized.includes("api.openai.com") || normalized.includes("chatgpt.com") || normalized.includes(".openai.azure.com");
192+
return (
193+
normalized.includes("api.openai.com") ||
194+
normalized.includes("chatgpt.com") ||
195+
normalized.includes(".openai.azure.com")
196+
);
191197
}
192198
}
193199

src/agents/subagent-announce.format.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,23 @@ describe("subagent announce formatting", () => {
435435
expect(sessionsDeleteSpy).toHaveBeenCalledTimes(1);
436436
});
437437

438+
it("suppresses completion delivery when subagent reply is NO_REPLY", async () => {
439+
const didAnnounce = await runSubagentAnnounceFlow({
440+
childSessionKey: "agent:main:subagent:test",
441+
childRunId: "run-direct-completion-no-reply",
442+
requesterSessionKey: "agent:main:main",
443+
requesterDisplayKey: "main",
444+
requesterOrigin: { channel: "slack", to: "channel:C123", accountId: "acct-1" },
445+
...defaultOutcomeAnnounce,
446+
expectsCompletionMessage: true,
447+
roundOneReply: " NO_REPLY ",
448+
});
449+
450+
expect(didAnnounce).toBe(true);
451+
expect(sendSpy).not.toHaveBeenCalled();
452+
expect(agentSpy).not.toHaveBeenCalled();
453+
});
454+
438455
it("retries completion direct send on transient channel-unavailable errors", async () => {
439456
sendSpy
440457
.mockRejectedValueOnce(new Error("Error: No active WhatsApp Web listener (account: default)"))

src/agents/subagent-announce.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { resolveQueueSettings } from "../auto-reply/reply/queue.js";
2-
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
2+
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
33
import { DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH } from "../config/agent-limits.js";
44
import { loadConfig } from "../config/config.js";
55
import {
@@ -1161,6 +1161,9 @@ export async function runSubagentAnnounceFlow(params: {
11611161
if (isAnnounceSkip(reply)) {
11621162
return true;
11631163
}
1164+
if (isSilentReplyText(reply, SILENT_REPLY_TOKEN)) {
1165+
return true;
1166+
}
11641167

11651168
if (!outcome) {
11661169
outcome = { status: "unknown" };

src/gateway/server.auth.test.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -416,13 +416,14 @@ describe("gateway server auth/connect", () => {
416416
opts: Parameters<typeof connectReq>[1];
417417
expectConnectOk: boolean;
418418
expectConnectError?: string;
419+
expectStatusOk?: boolean;
419420
expectStatusError?: string;
420421
}> = [
421422
{
422-
name: "operator + valid shared token => connected with zero scopes",
423+
name: "operator + valid shared token => connected with preserved scopes",
423424
opts: { role: "operator", token, device: null },
424425
expectConnectOk: true,
425-
expectStatusError: "missing scope",
426+
expectStatusOk: true,
426427
},
427428
{
428429
name: "node + valid shared token => rejected without device",
@@ -449,12 +450,14 @@ describe("gateway server auth/connect", () => {
449450
);
450451
continue;
451452
}
452-
if (scenario.expectStatusError) {
453+
if (scenario.expectStatusOk !== undefined) {
453454
const status = await rpcReq(ws, "status");
454-
expect(status.ok, scenario.name).toBe(false);
455-
expect(status.error?.message ?? "", scenario.name).toContain(
456-
scenario.expectStatusError,
457-
);
455+
expect(status.ok, scenario.name).toBe(scenario.expectStatusOk);
456+
if (!scenario.expectStatusOk && scenario.expectStatusError) {
457+
expect(status.error?.message ?? "", scenario.name).toContain(
458+
scenario.expectStatusError,
459+
);
460+
}
458461
}
459462
} finally {
460463
ws.close();
@@ -811,8 +814,7 @@ describe("gateway server auth/connect", () => {
811814
const res = await connectReq(ws, { token: "secret", device: null });
812815
expect(res.ok).toBe(true);
813816
const status = await rpcReq(ws, "status");
814-
expect(status.ok).toBe(false);
815-
expect(status.error?.message).toContain("missing scope");
817+
expect(status.ok).toBe(true);
816818
const health = await rpcReq(ws, "health");
817819
expect(health.ok).toBe(true);
818820
ws.close();
@@ -896,8 +898,7 @@ describe("gateway server auth/connect", () => {
896898
}
897899
if (tc.expectStatusChecks) {
898900
const status = await rpcReq(ws, "status");
899-
expect(status.ok).toBe(false);
900-
expect(status.error?.message ?? "").toContain("missing scope");
901+
expect(status.ok).toBe(true);
901902
const health = await rpcReq(ws, "health");
902903
expect(health.ok).toBe(true);
903904
}
@@ -923,8 +924,7 @@ describe("gateway server auth/connect", () => {
923924
});
924925
expect(res.ok).toBe(true);
925926
const status = await rpcReq(ws, "status");
926-
expect(status.ok).toBe(false);
927-
expect(status.error?.message ?? "").toContain("missing scope");
927+
expect(status.ok).toBe(true);
928928
const health = await rpcReq(ws, "health");
929929
expect(health.ok).toBe(true);
930930
ws.close();
@@ -946,8 +946,7 @@ describe("gateway server auth/connect", () => {
946946
});
947947
expect(res.ok).toBe(true);
948948
const status = await rpcReq(ws, "status");
949-
expect(status.ok).toBe(false);
950-
expect(status.error?.message ?? "").toContain("missing scope");
949+
expect(status.ok).toBe(true);
951950
const health = await rpcReq(ws, "health");
952951
expect(health.ok).toBe(true);
953952
ws.close();

0 commit comments

Comments
 (0)