Skip to content

Commit 613071e

Browse files
Merge branch 'main' into fix/native-hook-relay-stale-prune
2 parents ae75bad + 3844e03 commit 613071e

76 files changed

Lines changed: 2198 additions & 314 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
---
2+
name: release-openclaw-announcement
3+
description: "Draft or post OpenClaw beta/stable Discord release announcements from changelog, GitHub release, registry, and validation evidence. Use when announcing a beta, stable release, release candidate, or asking what users should test after an OpenClaw release."
4+
---
5+
6+
# OpenClaw Release Announcement
7+
8+
Use with `release-openclaw-maintainer` after a beta or stable release is live.
9+
Use with `openclaw-discord` when actually posting to Discord.
10+
11+
## Evidence First
12+
13+
Before drafting focus areas, read real release evidence:
14+
15+
1. Current GitHub release body for the tag.
16+
2. `CHANGELOG.md` section for the released base version.
17+
3. Commits since the previous shipped version or the operator-specified base.
18+
4. Registry/package metadata for the exact version and current dist-tag.
19+
5. Validation status that is relevant to user confidence.
20+
21+
Do not claim a full changelog audit unless you did it. If you only read the
22+
generated release notes or top changelog section, say that and either audit
23+
properly or draft with that limitation.
24+
25+
For beta focus areas, prioritize user-observable changes over internal test or
26+
CI mechanics:
27+
28+
- install/update paths
29+
- OS/platform-specific behavior
30+
- Gateway startup/restart, config, and runtime behavior
31+
- provider/model/runtime routing
32+
- plugin loading and local plugin development
33+
- channels and media paths
34+
- security/data-loss/user-impact fixes
35+
36+
Do not let late release-branch fixes automatically dominate the announcement.
37+
If the version includes a large delta from the previous shipped version, rank
38+
focus areas by the whole release delta and expected user impact; mention late
39+
fixes in their natural category.
40+
41+
## Required Copy
42+
43+
Every beta announcement must make beta status explicit and include:
44+
45+
- exact version, e.g. `OpenClaw 2026.5.25-beta.1`
46+
- one-sentence risk framing: beta, useful for testing, not stable promotion
47+
- focused test areas derived from evidence, not guesswork
48+
- update command promoted near the top:
49+
```sh
50+
openclaw update --channel beta --yes
51+
openclaw --version
52+
```
53+
- fresh install path:
54+
`Install from https://openclaw.ai`
55+
- GitHub release link
56+
- concise validation note, without making CI the headline
57+
58+
Do not suggest npm install commands in beta announcements unless the operator
59+
explicitly asks for npm-specific copy or troubleshooting text. It is fine to use
60+
registry metadata as evidence; do not turn that into public install guidance.
61+
62+
For stable announcements, use the stable channel wording:
63+
64+
```sh
65+
openclaw update --channel stable --yes
66+
openclaw --version
67+
```
68+
69+
Fresh installs still point to `https://openclaw.ai`.
70+
71+
## Style
72+
73+
- Discord Markdown, no tables.
74+
- Keep it skimmable: short intro, bullets, commands, links.
75+
- Lead with what users can feel or test, not proof plumbing.
76+
- Mention validation only after install/update instructions.
77+
- Be specific about where feedback is useful.
78+
- Do not mention private local proof paths in public announcements.
79+
- Do not overstate unverified platforms, channels, or provider behavior.
80+
81+
## Posting
82+
83+
When asked to post, use the configured Discord workflow from
84+
`openclaw-discord` or the approved OpenClaw relay. Never print tokens.
85+
For public channels, inspect the final body before sending.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
interface:
2+
display_name: "OpenClaw Release Announcement"
3+
short_description: "Draft Discord beta/stable release announcements from evidence."
4+
default_prompt: "Use this skill to draft an OpenClaw beta or stable Discord announcement from changelog, release notes, npm/GitHub release proof, and validation evidence."

.github/workflows/mantis-telegram-desktop-proof.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ jobs:
225225
- name: Checkout harness ref
226226
uses: actions/checkout@v6
227227
with:
228+
ref: main
228229
persist-credentials: false
229230
fetch-depth: 0
230231

.github/workflows/qa-live-transports-convex.yml

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ env:
5252
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
5353
NODE_VERSION: "24.x"
5454
OPENCLAW_CI_OPENAI_MODEL: ${{ vars.OPENCLAW_CI_OPENAI_MODEL || 'openai/gpt-5.5' }}
55+
OPENCLAW_CI_OPENAI_FALLBACK_MODEL: ${{ vars.OPENCLAW_CI_OPENAI_FALLBACK_MODEL || 'openai/gpt-5.4' }}
5556
OPENCLAW_BUILD_PRIVATE_QA: "1"
5657
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
5758

@@ -288,7 +289,7 @@ jobs:
288289
--runtime-parity-tier live-only \
289290
--concurrency "${QA_PARITY_CONCURRENCY}" \
290291
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
291-
--alt-model "${OPENCLAW_CI_OPENAI_MODEL}" \
292+
--alt-model "${OPENCLAW_CI_OPENAI_FALLBACK_MODEL}" \
292293
--runtime-pair openclaw,codex \
293294
--fast \
294295
--allow-failures \
@@ -373,7 +374,7 @@ jobs:
373374
--output-dir "${output_dir}" \
374375
--provider-mode live-frontier \
375376
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
376-
--alt-model "${OPENCLAW_CI_OPENAI_MODEL}" \
377+
--alt-model "${OPENCLAW_CI_OPENAI_FALLBACK_MODEL}" \
377378
--profile "${INPUT_MATRIX_PROFILE}" \
378379
--fast
379380
)
@@ -457,7 +458,7 @@ jobs:
457458
--output-dir "${output_dir}" \
458459
--provider-mode live-frontier \
459460
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
460-
--alt-model "${OPENCLAW_CI_OPENAI_MODEL}" \
461+
--alt-model "${OPENCLAW_CI_OPENAI_FALLBACK_MODEL}" \
461462
--profile "${{ matrix.profile }}" \
462463
--fast
463464
)
@@ -555,7 +556,7 @@ jobs:
555556
--output-dir "${output_dir}" \
556557
--provider-mode live-frontier \
557558
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
558-
--alt-model "${OPENCLAW_CI_OPENAI_MODEL}" \
559+
--alt-model "${OPENCLAW_CI_OPENAI_FALLBACK_MODEL}" \
559560
--fast \
560561
--credential-source convex \
561562
--credential-role ci \
@@ -649,7 +650,7 @@ jobs:
649650
--output-dir "${output_dir}" \
650651
--provider-mode live-frontier \
651652
--model openai/gpt-5.5 \
652-
--alt-model openai/gpt-5.5 \
653+
--alt-model "${OPENCLAW_CI_OPENAI_FALLBACK_MODEL}" \
653654
--fast \
654655
--credential-source convex \
655656
--credential-role ci \
@@ -746,7 +747,7 @@ jobs:
746747
--output-dir "${output_dir}" \
747748
--provider-mode live-frontier \
748749
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
749-
--alt-model "${OPENCLAW_CI_OPENAI_MODEL}" \
750+
--alt-model "${OPENCLAW_CI_OPENAI_FALLBACK_MODEL}" \
750751
--fast \
751752
--credential-source convex \
752753
--credential-role ci \
@@ -840,7 +841,7 @@ jobs:
840841
--output-dir "${output_dir}" \
841842
--provider-mode live-frontier \
842843
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
843-
--alt-model "${OPENCLAW_CI_OPENAI_MODEL}" \
844+
--alt-model "${OPENCLAW_CI_OPENAI_FALLBACK_MODEL}" \
844845
--fast \
845846
--credential-source convex \
846847
--credential-role ci \

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ Docs: https://docs.openclaw.ai
2121

2222
### Fixes
2323

24+
- Tighten phone-control mutation authorization [AI]. (#87150) Thanks @pgondhi987.
25+
- Clarify directive persistence authorization policy [AI]. (#86369) Thanks @pgondhi987.
2426
- Agents/Codex: keep spawned agent cwd/workspace state separated, keep hook context prompt-local, release session locks on timeout abort, avoid session event queue self-wait, preserve shared app-server state across startup or helper failures, keep native hook relay alive across restarts, route workspace memory through tools, resolve Codex runtime models first, report quarantined dynamic tools, format `skills` command output, and bound compaction/steering retries. (#87218, #86875, #86123, #87399, #87375, #87383, #87400) Thanks @mbelinky, @Alix-007, @luoyanglang, @yetval, and @sjf.
2527
- Channels: thread canonical session keys into outbound hooks, preserve Matrix room-id case, keep fallback tool warnings mention-inert, retain delivered Slack final replies during late cleanup, continue iMessage polling after denied reactions, suppress duplicate native exec approvals, preserve Telegram SecretRef prompt config, suppress Discord recovered tool warnings, and block untrusted Teams service URLs. (#73706, #75670, #87366, #87451, #87334) Thanks @zeroaltitude, @lukeboyett, @xiaotian, and @eleqtrizit.
2628
- CLI/auth/doctor/providers: reject malformed numeric/timeout/subcommand-version inputs, wait for respawn child shutdown, bound Codex and GitHub Copilot OAuth/token requests, warm provider auth off the main thread, honor Codex response timeouts, bound local service startup, resolve GPT-5.5 without cached catalog, migrate legacy memory auto-provider config, rewrite non-canonical `api_key` auth profiles, and make doctor restart follow-ups actionable. (#87398, #86281, #87361) Thanks @Patrick-Erichsen, @samzong, @giodl73-repo, and @alkor2000.

docs/tools/exec.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,10 @@ Example:
164164
## Authorization model
165165

166166
`/exec` is only honored for **authorized senders** (channel allowlists/pairing plus `commands.useAccessGroups`).
167-
It updates **session state only** and does not write config. To hard-disable exec, deny it via tool
168-
policy (`tools.deny: ["exec"]` or per-agent). Host approvals still apply unless you explicitly set
169-
`security=full` and `ask=off`.
167+
It updates **session state only** and does not write config. Authorized external channel senders may
168+
set these session defaults. Internal gateway/webchat clients need `operator.admin` to persist them.
169+
To hard-disable exec, deny it via tool policy (`tools.deny: ["exec"]` or per-agent). Host approvals
170+
still apply unless you explicitly set `security=full` and `ask=off`.
170171

171172
## Exec approvals (companion app / node host)
172173

extensions/phone-control/index.test.ts

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ describe("phone-control plugin", () => {
129129
it("arms sms.send as part of the writes group", async () => {
130130
await withRegisteredPhoneControl(async ({ command, writeConfigFile, getConfig }) => {
131131
expect(command.name).toBe("phone");
132+
expect(command.requiredScopes).toBeUndefined();
133+
expect(command.exposeSenderIsOwner).toBe(true);
132134

133135
const res = await command.handler({
134136
...createCommandContext("arm writes 30s"),
@@ -163,30 +165,58 @@ describe("phone-control plugin", () => {
163165
});
164166
});
165167

166-
it("allows external channel callers without operator.admin to mutate phone control", async () => {
168+
it("blocks external non-owner callers without operator.admin from mutating phone control", async () => {
167169
await withRegisteredPhoneControl(async ({ command, writeConfigFile }) => {
168170
const res = await command.handler({
169171
...createCommandContext("arm writes 30s"),
170172
channel: "telegram",
173+
senderIsOwner: false,
171174
});
172175

173-
expect(res?.text ?? "").toContain("Phone control: armed");
174-
expect(writeConfigFile).toHaveBeenCalledTimes(1);
176+
expect(res?.text ?? "").toContain("requires operator.admin");
177+
expect(writeConfigFile).not.toHaveBeenCalled();
175178
});
176179
});
177180

178-
it("allows external channel callers without operator.admin to disarm phone control", async () => {
181+
it("blocks external non-owner callers without operator.admin from disarming phone control", async () => {
179182
await withRegisteredPhoneControl(async ({ command, writeConfigFile }) => {
180183
const res = await command.handler({
181184
...createCommandContext("disarm"),
182185
channel: "telegram",
186+
senderIsOwner: false,
187+
});
188+
189+
expect(res?.text ?? "").toContain("requires operator.admin");
190+
expect(writeConfigFile).not.toHaveBeenCalled();
191+
});
192+
});
193+
194+
it("allows external non-owner callers without operator.admin to read phone control status", async () => {
195+
await withRegisteredPhoneControl(async ({ command, writeConfigFile }) => {
196+
const res = await command.handler({
197+
...createCommandContext("status"),
198+
channel: "telegram",
199+
senderIsOwner: false,
183200
});
184201

185202
expect(res?.text ?? "").toContain("Phone control: disarmed.");
186203
expect(writeConfigFile).not.toHaveBeenCalled();
187204
});
188205
});
189206

207+
it("allows external non-owner callers without operator.admin to read phone control help", async () => {
208+
await withRegisteredPhoneControl(async ({ command, writeConfigFile }) => {
209+
const res = await command.handler({
210+
...createCommandContext("help"),
211+
channel: "telegram",
212+
senderIsOwner: false,
213+
});
214+
215+
expect(res?.text ?? "").toContain("/phone status");
216+
expect(writeConfigFile).not.toHaveBeenCalled();
217+
});
218+
});
219+
190220
it("regression: blocks non-webchat gateway callers with operator.write from arm/disarm", async () => {
191221
await withRegisteredPhoneControl(async ({ command, writeConfigFile }) => {
192222
const armRes = await command.handler({
@@ -220,6 +250,19 @@ describe("phone-control plugin", () => {
220250
});
221251
});
222252

253+
it("allows external owner callers without gateway scopes to mutate phone control", async () => {
254+
await withRegisteredPhoneControl(async ({ command, writeConfigFile }) => {
255+
const res = await command.handler({
256+
...createCommandContext("arm writes 30s"),
257+
channel: "telegram",
258+
senderIsOwner: true,
259+
});
260+
261+
expect(res?.text ?? "").toContain("Phone control: armed");
262+
expect(writeConfigFile).toHaveBeenCalledTimes(1);
263+
});
264+
});
265+
223266
it("allows external channel callers with operator.admin to disarm phone control", async () => {
224267
await withRegisteredPhoneControl(async ({ command, writeConfigFile }) => {
225268
await command.handler({

extensions/phone-control/index.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -282,14 +282,15 @@ function parseGroup(raw: string | undefined): ArmGroup | null {
282282
return null;
283283
}
284284

285-
function requiresAdminToMutatePhoneControl(
286-
channel: string,
287-
gatewayClientScopes?: readonly string[],
288-
): boolean {
285+
function lacksAdminToMutatePhoneControl(params: {
286+
senderIsOwner?: boolean;
287+
gatewayClientScopes?: readonly string[];
288+
}): boolean {
289+
const { senderIsOwner, gatewayClientScopes } = params;
289290
if (Array.isArray(gatewayClientScopes)) {
290291
return !gatewayClientScopes.includes(PHONE_ADMIN_SCOPE);
291292
}
292-
return channel === "webchat";
293+
return senderIsOwner !== true;
293294
}
294295

295296
function formatStatus(state: ArmStateFile | null): string {
@@ -363,6 +364,7 @@ export default definePluginEntry({
363364
name: "phone",
364365
description: "Arm/disarm high-risk phone node commands (camera/screen/writes).",
365366
acceptsArgs: true,
367+
exposeSenderIsOwner: true,
366368
handler: async (ctx) => {
367369
const args = ctx.args?.trim() ?? "";
368370
const tokens = args.split(/\s+/).filter(Boolean);
@@ -382,7 +384,12 @@ export default definePluginEntry({
382384
}
383385

384386
if (action === "disarm") {
385-
if (requiresAdminToMutatePhoneControl(ctx.channel, ctx.gatewayClientScopes)) {
387+
if (
388+
lacksAdminToMutatePhoneControl({
389+
senderIsOwner: ctx.senderIsOwner,
390+
gatewayClientScopes: ctx.gatewayClientScopes,
391+
})
392+
) {
386393
return {
387394
text: "⚠️ /phone disarm requires operator.admin.",
388395
};
@@ -404,7 +411,12 @@ export default definePluginEntry({
404411
}
405412

406413
if (action === "arm") {
407-
if (requiresAdminToMutatePhoneControl(ctx.channel, ctx.gatewayClientScopes)) {
414+
if (
415+
lacksAdminToMutatePhoneControl({
416+
senderIsOwner: ctx.senderIsOwner,
417+
gatewayClientScopes: ctx.gatewayClientScopes,
418+
})
419+
) {
408420
return {
409421
text: "⚠️ /phone arm requires operator.admin.",
410422
};

extensions/qa-lab/src/cli.runtime.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -521,21 +521,26 @@ function printQaCredentialListTable(credentials: QaCredentialRecord[]) {
521521
}
522522
const rows = credentials.map((credential) => ({
523523
credentialId: credential.credentialId,
524+
fingerprint: credential.credentialFingerprint ?? "",
524525
kind: credential.kind,
525526
status: credential.status,
526527
leased: formatQaCredentialLeaseState(credential),
527528
note: credential.note ?? "",
528529
}));
529530
const idWidth = Math.max("credentialId".length, ...rows.map((row) => row.credentialId.length));
531+
const fingerprintWidth = Math.max(
532+
"fingerprint".length,
533+
...rows.map((row) => row.fingerprint.length),
534+
);
530535
const kindWidth = Math.max("kind".length, ...rows.map((row) => row.kind.length));
531536
const statusWidth = Math.max("status".length, ...rows.map((row) => row.status.length));
532537
const leaseWidth = Math.max("leased".length, ...rows.map((row) => row.leased.length));
533538
process.stdout.write(
534-
`${"credentialId".padEnd(idWidth)} ${"kind".padEnd(kindWidth)} ${"status".padEnd(statusWidth)} ${"leased".padEnd(leaseWidth)} note\n`,
539+
`${"credentialId".padEnd(idWidth)} ${"fingerprint".padEnd(fingerprintWidth)} ${"kind".padEnd(kindWidth)} ${"status".padEnd(statusWidth)} ${"leased".padEnd(leaseWidth)} note\n`,
535540
);
536541
for (const row of rows) {
537542
process.stdout.write(
538-
`${row.credentialId.padEnd(idWidth)} ${row.kind.padEnd(kindWidth)} ${row.status.padEnd(statusWidth)} ${row.leased.padEnd(leaseWidth)} ${row.note}\n`,
543+
`${row.credentialId.padEnd(idWidth)} ${row.fingerprint.padEnd(fingerprintWidth)} ${row.kind.padEnd(kindWidth)} ${row.status.padEnd(statusWidth)} ${row.leased.padEnd(leaseWidth)} ${row.note}\n`,
539544
);
540545
}
541546
}

extensions/qa-lab/src/live-transports/slack/slack-live.runtime.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,27 @@ describe("Slack live QA runtime helpers", () => {
106106
expect(account?.channels?.C123456789?.users).toEqual(["U999999999"]);
107107
});
108108

109+
it("overrides both owner and channel allowlists for block scenarios", () => {
110+
const cfg = testing.buildSlackQaConfig(
111+
{},
112+
{
113+
channelId: "C123456789",
114+
driverBotUserId: "U999999999",
115+
overrides: {
116+
allowFrom: ["U_NEVER_ALLOWED"],
117+
users: ["U_NEVER_ALLOWED"],
118+
},
119+
sutAccountId: "sut",
120+
sutAppToken: "xapp-sut",
121+
sutBotToken: "xoxb-sut",
122+
},
123+
);
124+
125+
const account = cfg.channels?.slack?.accounts?.sut;
126+
expect(account?.allowFrom).toEqual(["U_NEVER_ALLOWED"]);
127+
expect(account?.channels?.C123456789?.users).toEqual(["U_NEVER_ALLOWED"]);
128+
});
129+
109130
it("extracts Slack native approval button values from blocks", () => {
110131
expect(
111132
testing.collectSlackActionValues([

0 commit comments

Comments
 (0)