mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-22 23:13:42 +08:00
Compare commits
33 Commits
fix/plugin
...
agent-memo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c86d8d501 | ||
|
|
f15a70be21 | ||
|
|
a31204ac6c | ||
|
|
4838915c95 | ||
|
|
e14b2d9ba8 | ||
|
|
a43ed080f3 | ||
|
|
bb44b27c2a | ||
|
|
223b643112 | ||
|
|
bda5ccf1c8 | ||
|
|
8b90900b8d | ||
|
|
29a01b86c2 | ||
|
|
6efd70ea20 | ||
|
|
b85ba100b7 | ||
|
|
d8b88c35c2 | ||
|
|
1aa9837321 | ||
|
|
a064e11269 | ||
|
|
01cdf9ca63 | ||
|
|
71168a2ae5 | ||
|
|
55f2ab04f0 | ||
|
|
80d2f54d31 | ||
|
|
021ae312a7 | ||
|
|
6c639c739c | ||
|
|
516263eefd | ||
|
|
86b2d0a569 | ||
|
|
8b14f45bae | ||
|
|
b4bc84caa9 | ||
|
|
bf8c975cea | ||
|
|
44725f80c7 | ||
|
|
d38c702221 | ||
|
|
647869d425 | ||
|
|
1690c3f0dd | ||
|
|
7fe54772a9 | ||
|
|
740237831f |
@@ -1,34 +1,44 @@
|
||||
---
|
||||
name: channel-message-flows
|
||||
description: "Use when running QA Lab channel message flow evidence."
|
||||
description: "Use when previewing local channel message flow fixtures."
|
||||
---
|
||||
|
||||
# Channel Message Flows
|
||||
|
||||
Use this from the OpenClaw repo root to run the QA Lab evidence for Telegram
|
||||
draft/final delivery sequencing. This skill no longer launches a standalone
|
||||
script; the behavior is owned by the QA scenario and its Vitest-backed e2e test.
|
||||
Use this from the OpenClaw repo root to send canned channel preview flows while iterating on message UX. These are real sends/edits/deletes against the configured channel target.
|
||||
|
||||
## QA Scenario
|
||||
## Telegram
|
||||
|
||||
Run the scenario through QA Lab:
|
||||
Native Telegram `sendMessageDraft` tool progress, then a final answer:
|
||||
|
||||
```bash
|
||||
pnpm openclaw qa suite --scenario channel-message-flows
|
||||
node --import tsx scripts/dev/channel-message-flows.ts \
|
||||
--channel telegram \
|
||||
--target <telegram-chat-id> \
|
||||
--flow working-final \
|
||||
--duration-ms 20000
|
||||
```
|
||||
|
||||
Run the focused e2e test directly in a Codex worktree:
|
||||
Thinking preview, then a final answer:
|
||||
|
||||
```bash
|
||||
node scripts/run-vitest.mjs extensions/telegram/src/channel-message-flows.qa.e2e.test.ts
|
||||
node --import tsx scripts/dev/channel-message-flows.ts \
|
||||
--channel telegram \
|
||||
--target <telegram-chat-id> \
|
||||
--flow thinking-final
|
||||
```
|
||||
|
||||
## References
|
||||
## Options
|
||||
|
||||
- `qa/scenarios/channels/channel-message-flows.yaml`
|
||||
- `extensions/telegram/src/channel-message-flows.qa.e2e.test.ts`
|
||||
- `extensions/telegram/src/test-support/channel-message-flows.ts`
|
||||
- `--account <accountId>`: Telegram account id when not using the default.
|
||||
- `--thread-id <id>`: Telegram forum topic/message thread id.
|
||||
- `--delay-ms <ms>`: Override preview update cadence.
|
||||
- `--duration-ms <ms>`: Simulated working duration for `working-final`.
|
||||
- `--final-text <text>`: Override the durable final message.
|
||||
|
||||
The scenario covers `channels.streaming` as primary evidence and records
|
||||
secondary coverage for thread preservation, delivery ordering, and reasoning
|
||||
preview visibility.
|
||||
## Notes
|
||||
|
||||
- `--target` is the numeric Telegram chat id.
|
||||
- `working-final` exercises native Telegram `sendMessageDraft` with static `Working` status and sample tool progress.
|
||||
- `thinking-final` exercises formatted `Thinking` reasoning preview clearing before the final answer.
|
||||
- Only `--channel telegram` is implemented for now.
|
||||
|
||||
@@ -16,8 +16,11 @@ This skill owns the operational workflow for:
|
||||
|
||||
- `taxonomy.yaml`
|
||||
- `docs/maturity-scores.yaml`
|
||||
- `docs/concepts/qa-e2e-automation.md`
|
||||
- `qa/scenarios/index.yaml`
|
||||
- `docs/maturity-scorecard.md`
|
||||
- `docs/taxonomy.md`
|
||||
- `docs/taxonomy-outline.md`
|
||||
- `scripts/render-maturity-docs.mjs`
|
||||
- `.github/workflows/maturity-scorecard.yml`
|
||||
|
||||
Keep person-specific, maintainer-private, Discord archive, and discrawl facts
|
||||
out of this repo. If a score needs private evidence, use the redacted
|
||||
@@ -28,21 +31,12 @@ out of this repo. If a score needs private evidence, use the redacted
|
||||
- `taxonomy.yaml` is the hand-edited source of truth for surfaces, levels,
|
||||
QA profiles, categories, feature coverage IDs, docs refs, LTS overrides, and
|
||||
completeness-instruction paths.
|
||||
- Feature `coverageIds` are ANDed proof targets, not aliases. A feature may
|
||||
list multiple IDs when each ID proves part of one capability.
|
||||
- Coverage IDs use dotted `namespace.behavior` form, with lowercase
|
||||
alphanumeric/dash segments. Profile, surface, and category IDs may remain
|
||||
dashed or dotted.
|
||||
- Keep categories and feature names unique, product-shaped, and broader than raw
|
||||
coverage IDs. Do not promote generic IDs into standalone feature names.
|
||||
- Avoid duplicate coverage-ID bundles under different feature names in one
|
||||
category.
|
||||
- `docs/maturity-scores.yaml` is the aggregate score source committed in this
|
||||
repo. It is the only committed score data; do not add generated inventory
|
||||
directories.
|
||||
- There is no committed maturity-doc renderer or `pnpm maturity:*` script in
|
||||
this repo. Do not invent generated scorecard files; update the source YAML
|
||||
and current docs directly.
|
||||
- `docs/maturity-scorecard.md`, `docs/taxonomy.md`, and
|
||||
`docs/taxonomy-outline.md` are deterministic docs generated from the root
|
||||
taxonomy and aggregate score source.
|
||||
- `qa-evidence.json` artifacts provide per-run QA scorecard evidence. They can
|
||||
enrich generated artifact docs, but they are not committed as inventory.
|
||||
|
||||
@@ -50,28 +44,22 @@ out of this repo. If a score needs private evidence, use the redacted
|
||||
|
||||
Run from the openclaw repo root.
|
||||
|
||||
Validate YAML structure after source edits:
|
||||
Render committed docs:
|
||||
|
||||
```bash
|
||||
node <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const YAML = require("yaml");
|
||||
for (const file of ["taxonomy.yaml", "docs/maturity-scores.yaml", "qa/scenarios/index.yaml"]) {
|
||||
YAML.parse(fs.readFileSync(file, "utf8"));
|
||||
}
|
||||
NODE
|
||||
pnpm maturity:render
|
||||
```
|
||||
|
||||
Check docs when touching docs prose:
|
||||
Check generated docs are current:
|
||||
|
||||
```bash
|
||||
pnpm check:docs
|
||||
pnpm maturity:check
|
||||
```
|
||||
|
||||
Run focused QA/profile checks when changing coverage IDs or profile membership:
|
||||
Render an evidence-enriched docs artifact from downloaded QA artifacts:
|
||||
|
||||
```bash
|
||||
pnpm openclaw qa coverage --json
|
||||
pnpm maturity:render -- --evidence-dir .artifacts/maturity-evidence --output-dir .artifacts/maturity-docs
|
||||
```
|
||||
|
||||
## Scoring Workflow
|
||||
@@ -87,13 +75,13 @@ When asked to score or refresh a surface:
|
||||
discrawl or unredacted private archives.
|
||||
5. Update `docs/maturity-scores.yaml` only when the score change is backed by
|
||||
public or redacted artifact evidence.
|
||||
6. Run the YAML validation command from this skill.
|
||||
7. Run `pnpm check:docs` if docs prose changed, and focused QA coverage checks
|
||||
if coverage IDs or profile membership changed.
|
||||
6. Run `pnpm maturity:render`.
|
||||
7. Run `pnpm maturity:check`.
|
||||
|
||||
For subjective score changes, make the smallest defensible edit and leave the
|
||||
evidence path in the PR or task summary. Keep manual prose in current docs and
|
||||
keep score data in `docs/maturity-scores.yaml`.
|
||||
evidence path in the PR or task summary. The deterministic renderer owns
|
||||
Markdown structure; manual prose tweaks belong in taxonomy, score source, or
|
||||
the renderer rather than in generated docs.
|
||||
|
||||
## Default Completeness Process
|
||||
|
||||
@@ -170,9 +158,13 @@ Bands:
|
||||
- `Alpha`: 50-70
|
||||
- `Experimental`: 0-50
|
||||
|
||||
## Artifacts
|
||||
## GitHub Action
|
||||
|
||||
The `Maturity scorecard` workflow verifies committed generated docs on PRs and
|
||||
pushes. Manual dispatch can also download QA artifacts from another workflow run
|
||||
with `source_run_id` and `artifact_pattern`, render evidence-enriched docs into
|
||||
`.artifacts/maturity-docs`, and upload them as a GitHub artifact.
|
||||
|
||||
Do not add the maintainer repo's `docs/kevinslin/maturity-scorecard/inventory/`
|
||||
tree to openclaw. Evidence-enriched scorecard outputs belong in short-lived
|
||||
artifacts, not committed generated docs, unless this repo adds an explicit
|
||||
renderer/check workflow first.
|
||||
tree to openclaw. Those generated reports are intentionally replaced here by
|
||||
short-lived artifact docs and the committed aggregate scorecard pages.
|
||||
|
||||
@@ -4,7 +4,6 @@ import { execFileSync } from "node:child_process";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
|
||||
const repo = "openclaw/openclaw";
|
||||
const commitAssociationQueryBatchSize = 20;
|
||||
const excludedHandles = new Set(["openclaw", "clawsweeper", "claude", "codex", "steipete"]);
|
||||
const nonEditorialTypes = new Set([
|
||||
"build",
|
||||
@@ -619,25 +618,13 @@ function graphql(query) {
|
||||
let lastError;
|
||||
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||
try {
|
||||
const response = githubApi(["graphql", "-f", `query=${query}`]);
|
||||
if (response?.data && typeof response.data === "object") {
|
||||
return response.data;
|
||||
}
|
||||
const errors = Array.isArray(response?.errors)
|
||||
? response.errors.map((error) => error?.message).filter(Boolean)
|
||||
: [];
|
||||
const detail = [...errors, response?.message].filter(Boolean).join("\n");
|
||||
throw new Error(
|
||||
detail
|
||||
? `GitHub GraphQL response did not include data:\n${detail}`
|
||||
: "GitHub GraphQL response did not include data.",
|
||||
);
|
||||
return githubApi(["graphql", "-f", `query=${query}`]).data;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
const message = [error?.message, error?.stdout, error?.stderr].filter(Boolean).join("\n");
|
||||
// Historical ranges batch hundreds of objects; only retry transient transport failures.
|
||||
if (
|
||||
!/(?:operation timed out|ECONNRESET|ETIMEDOUT|EAI_AGAIN|TLS handshake timeout|stream error: .*CANCEL|unexpected end of JSON input|upstream connect error|connection termination|connection reset by peer|error connecting to api\.github\.com|Unexpected token '<'|something went wrong|temporarily unavailable|internal server error|rate limit)/i.test(
|
||||
!/(?:operation timed out|ECONNRESET|ETIMEDOUT|EAI_AGAIN|TLS handshake timeout|stream error: .*CANCEL|unexpected end of JSON input|upstream connect error|connection termination|error connecting to api\.github\.com|Unexpected token '<')/i.test(
|
||||
message,
|
||||
)
|
||||
) {
|
||||
@@ -670,8 +657,8 @@ function resolveAssociatedPullRequests(commitHashes, targetTimestamp) {
|
||||
pending.push({ commitHash, cursor: connection.pageInfo.endCursor });
|
||||
}
|
||||
}
|
||||
for (let index = 0; index < commitHashes.length; index += commitAssociationQueryBatchSize) {
|
||||
const chunk = commitHashes.slice(index, index + commitAssociationQueryBatchSize);
|
||||
for (let index = 0; index < commitHashes.length; index += 40) {
|
||||
const chunk = commitHashes.slice(index, index + 40);
|
||||
const fields = chunk
|
||||
.map(
|
||||
(hash, offset) =>
|
||||
|
||||
@@ -107,9 +107,16 @@ Reject:
|
||||
|
||||
## PR Body Proof
|
||||
|
||||
Use the repo PR template. Include authored `## What Problem This Solves` and
|
||||
`## Evidence` sections. Keep the body focused on intent and the most useful
|
||||
validation evidence; inspect the code, tests, and CI before judging correctness.
|
||||
Use the repo PR template. Include these exact labels:
|
||||
|
||||
```text
|
||||
Behavior addressed:
|
||||
Real environment tested:
|
||||
Exact steps or command run after this patch:
|
||||
Evidence after fix:
|
||||
Observed result after fix:
|
||||
What was not tested:
|
||||
```
|
||||
|
||||
## Existing PR Rules
|
||||
|
||||
|
||||
@@ -4,14 +4,6 @@ set -euo pipefail
|
||||
repo="openclaw/openclaw"
|
||||
months="12"
|
||||
include_global="0"
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_root="$(git -C "$script_dir/../../../.." rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
if [ -z "$repo_root" ]; then
|
||||
repo_root="$(cd "$script_dir/../../../.." && pwd)"
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source "$repo_root/scripts/lib/plain-gh.sh"
|
||||
|
||||
usage() {
|
||||
printf 'Usage: %s [--repo owner/repo] [--months N] [--global] <github-login> [login...]\n' "$0"
|
||||
@@ -26,10 +18,6 @@ need() {
|
||||
command -v "$1" >/dev/null 2>&1 || die "missing required command: $1"
|
||||
}
|
||||
|
||||
gh() {
|
||||
gh_plain "$@"
|
||||
}
|
||||
|
||||
date_utc_relative_months() {
|
||||
local count="$1"
|
||||
if date -u -v-"${count}"m +%Y-%m-%dT00:00:00Z >/dev/null 2>&1; then
|
||||
@@ -143,8 +131,7 @@ done
|
||||
exit 2
|
||||
}
|
||||
|
||||
OPENCLAW_GH_BIN="$(resolve_plain_gh_bin)" || die "missing required command: gh"
|
||||
export OPENCLAW_GH_BIN
|
||||
need gh
|
||||
need jq
|
||||
|
||||
since_ts=$(date_utc_relative_months "$months")
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
* Usage: node secret-scanning.mjs <command> [options]
|
||||
*/
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { spawnPlainGh } from "../../../../scripts/lib/plain-gh.mjs";
|
||||
|
||||
const REPO = "openclaw/openclaw";
|
||||
const REPO_URL = `https://github.com/${REPO}`;
|
||||
@@ -29,7 +29,7 @@ function tmpFile(purpose) {
|
||||
}
|
||||
|
||||
function gh(args, { json = true, allowFailure = false } = {}) {
|
||||
const proc = spawnPlainGh(args, { encoding: "utf8", maxBuffer: 10 * 1024 * 1024 });
|
||||
const proc = spawnSync("gh", args, { encoding: "utf8", maxBuffer: 10 * 1024 * 1024 });
|
||||
if (proc.status !== 0 && !allowFailure) {
|
||||
fail(`gh ${args.slice(0, 3).join(" ")} failed:\n${(proc.stderr || proc.stdout || "").trim()}`);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
*/
|
||||
import { execFileSync } from "node:child_process";
|
||||
import process from "node:process";
|
||||
import { plainGhEnv, resolvePlainGhBin } from "../../../../scripts/lib/plain-gh.mjs";
|
||||
|
||||
const runId = process.argv[2];
|
||||
const repo = process.env.OPENCLAW_RELEASE_REPO || "openclaw/openclaw";
|
||||
@@ -16,9 +15,8 @@ if (!runId) {
|
||||
}
|
||||
|
||||
function gh(args) {
|
||||
return execFileSync(resolvePlainGhBin(), args, {
|
||||
return execFileSync("gh", args, {
|
||||
encoding: "utf8",
|
||||
env: plainGhEnv(),
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
}
|
||||
@@ -34,15 +32,14 @@ function githubRestJson(pathSuffix) {
|
||||
"-lc",
|
||||
[
|
||||
"set -euo pipefail",
|
||||
'token="$("$OPENCLAW_PLAIN_GH_BIN" auth token)"',
|
||||
'token="$(gh auth token)"',
|
||||
'curl -fsS -H "Authorization: Bearer ${token}" -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" "${OPENCLAW_GITHUB_REST_URL}"',
|
||||
].join("\n"),
|
||||
],
|
||||
{
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...plainGhEnv(),
|
||||
OPENCLAW_PLAIN_GH_BIN: resolvePlainGhBin(),
|
||||
...process.env,
|
||||
OPENCLAW_GITHUB_REST_URL: `https://api.github.com/repos/${repo}/${pathSuffix}`,
|
||||
},
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
|
||||
@@ -22,7 +22,7 @@ paths:
|
||||
- src/plugins/memory-*.ts
|
||||
- src/gateway/server-startup-memory.ts
|
||||
- src/commands/doctor-memory-search.ts
|
||||
- src/commands/doctor/cron/dreaming-payload-migration.ts
|
||||
- src/commands/doctor-cron-dreaming-payload-migration.ts
|
||||
|
||||
paths-ignore:
|
||||
- "**/node_modules"
|
||||
|
||||
@@ -19,6 +19,7 @@ paths:
|
||||
- src/plugins/bundled-compat.ts
|
||||
- src/plugins/bundled-dir.ts
|
||||
- src/plugins/bundled-plugin-metadata.ts
|
||||
- src/plugins/bundled-public-surface-runtime-root.ts
|
||||
- src/plugins/plugin-sdk-dist-alias.ts
|
||||
- src/plugins/captured-registration.ts
|
||||
- src/plugins/config-activation-shared.ts
|
||||
@@ -45,6 +46,7 @@ paths:
|
||||
- src/plugins/runtime-state.ts
|
||||
- src/plugins/runtime.ts
|
||||
- src/plugins/sdk-alias.ts
|
||||
- src/plugins/source-loader.ts
|
||||
- src/plugins/types.ts
|
||||
- src/plugins/validation-diagnostics.ts
|
||||
- src/plugins/web-provider-public-artifacts*.ts
|
||||
|
||||
@@ -51,6 +51,7 @@ paths:
|
||||
- src/plugins/runtime
|
||||
- src/plugins/runtime-state.ts
|
||||
- src/plugins/runtime.ts
|
||||
- src/plugins/source-loader.ts
|
||||
- src/plugins/update.ts
|
||||
- src/plugins/validation-diagnostics.ts
|
||||
- src/plugin-sdk/*entry*.ts
|
||||
|
||||
21
.github/labeler.yml
vendored
21
.github/labeler.yml
vendored
@@ -41,6 +41,12 @@
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/google-meet/**"
|
||||
- "docs/plugins/google-meet.md"
|
||||
"plugin: meeting-notes":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/meeting-notes/**"
|
||||
- "docs/plugins/meeting-notes.md"
|
||||
- "src/meeting-notes/**"
|
||||
"plugin: workboard":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
@@ -103,11 +109,6 @@
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/qqbot/**"
|
||||
- "docs/channels/qqbot.md"
|
||||
"channel: raft":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/raft/**"
|
||||
- "docs/channels/raft.md"
|
||||
"channel: qa-channel":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
@@ -170,10 +171,6 @@
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/zalo/**"
|
||||
- "docs/channels/zalo.md"
|
||||
"channel: zaloclawbot":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "docs/channels/zaloclawbot.md"
|
||||
"channel: zalouser":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
@@ -251,12 +248,12 @@
|
||||
- "src/agents/sandbox*.ts"
|
||||
- "src/commands/sandbox*.ts"
|
||||
- "src/cli/sandbox-cli.ts"
|
||||
- "src/docker-setup.e2e.test.ts"
|
||||
- "src/docker-setup.test.ts"
|
||||
- "src/config/**/*sandbox*"
|
||||
- "docs/cli/sandbox.md"
|
||||
- "docs/gateway/sandbox*.md"
|
||||
- "docs/install/docker.md"
|
||||
- "docs/tools/multi-agent-sandbox-tools.md"
|
||||
- "docs/multi-agent-sandbox-tools.md"
|
||||
|
||||
"agents":
|
||||
- changed-files:
|
||||
@@ -269,7 +266,7 @@
|
||||
- ".github/workflows/opengrep-*.yml"
|
||||
- ".semgrepignore"
|
||||
- "docs/cli/security.md"
|
||||
- "docs/gateway/security/**"
|
||||
- "docs/gateway/security.md"
|
||||
- "security/**"
|
||||
|
||||
"extensions: admin-http-rpc":
|
||||
|
||||
151
.github/pull_request_template.md
vendored
151
.github/pull_request_template.md
vendored
@@ -1,57 +1,118 @@
|
||||
<!--
|
||||
Optional linked context:
|
||||
Add a visible `Closes #<issue-number>` or `Related: #<issue-number>` line
|
||||
below this comment.
|
||||
## Summary
|
||||
|
||||
Required PR title:
|
||||
type: user-facing description
|
||||
Use a parenthesized scope only when it adds clarity:
|
||||
fix(auth): login redirect loops when session cookie is expired
|
||||
What problem does this PR solve?
|
||||
|
||||
Types: feat, fix, improve, refactor, docs, chore.
|
||||
For fixes, describe the user-visible symptom and trigger:
|
||||
fix: task list fails to load when user has no environments
|
||||
Avoid implementation details such as:
|
||||
fix: add null check to task query
|
||||
-->
|
||||
Why does this matter now?
|
||||
|
||||
## What Problem This Solves
|
||||
What is the intended outcome?
|
||||
|
||||
<!--
|
||||
Describe the concrete user, product, or operational problem.
|
||||
For fixes, begin with:
|
||||
"Fixes an issue where users <do X> would <experience Y> when <condition>."
|
||||
or:
|
||||
"Resolves a problem where..."
|
||||
What is intentionally out of scope?
|
||||
|
||||
Name the affected UI surface or workflow. Do not describe the code-level cause here.
|
||||
-->
|
||||
What does success look like?
|
||||
|
||||
## Why This Change Was Made
|
||||
What should reviewers focus on?
|
||||
|
||||
<!--
|
||||
In one or two sentences, explain the complete shipped solution, key design
|
||||
decisions, and relevant boundaries or non-goals. Include implementation detail
|
||||
only when it helps reviewers understand user-visible behavior or risk.
|
||||
Avoid file-by-file narration.
|
||||
-->
|
||||
<details>
|
||||
<summary>Summary guidance</summary>
|
||||
|
||||
## User Impact
|
||||
This PR description is the contributor's durable explanation of the change. Write it for human maintainers first; ClawSweeper and Barnacle use the same text to understand intent, proof, risk, and current review state.
|
||||
|
||||
<!--
|
||||
State what users, operators, or developers can now do or expect. Lead with the
|
||||
concrete benefit and use user-facing language. If there is no user-visible
|
||||
impact, say so plainly.
|
||||
-->
|
||||
Describe the intent and outcome in 2-5 bullets. Avoid restating the diff; reviewers and bots can read the changed files.
|
||||
|
||||
## Evidence
|
||||
If this PR fixes a plugin beta-release blocker, title it `fix(<plugin-id>): beta blocker - <summary>` and link the matching `Beta blocker: <plugin-name> - <summary>` issue labeled `beta-blocker`. Contributors cannot label PRs, so the title is the PR-side signal for maintainers and automation.
|
||||
|
||||
<!--
|
||||
Show the most useful proof that this change works. Screenshots, screencasts,
|
||||
terminal output, focused tests, CI results, live observations, redacted logs,
|
||||
and artifact links are all useful. Include before/after evidence for visual
|
||||
changes when it clarifies the result.
|
||||
</details>
|
||||
|
||||
Reviewers will inspect the code, tests, and CI. Use this section to make the
|
||||
validation easy to understand, not to restate the diff.
|
||||
-->
|
||||
## Linked context
|
||||
|
||||
Which issue does this close?
|
||||
|
||||
Closes #
|
||||
|
||||
Which issues, PRs, or discussions are related?
|
||||
|
||||
Related #
|
||||
|
||||
Was this requested by a maintainer or owner?
|
||||
|
||||
<details>
|
||||
<summary>Linked context guidance</summary>
|
||||
|
||||
Link the issue, PR, discussion, maintainer request, or owner request that explains why this PR should exist. Maintainer context helps reviewers and automation distinguish intended work from drive-by churn.
|
||||
|
||||
</details>
|
||||
|
||||
## Real behavior proof (required for external PRs)
|
||||
|
||||
- Behavior or issue addressed:
|
||||
- Real environment tested:
|
||||
- Exact steps or command run after this patch:
|
||||
- Evidence after fix (screenshot, recording, terminal capture, console output, redacted runtime log, linked artifact, or copied live output):
|
||||
- Observed result after fix:
|
||||
- What was not tested:
|
||||
- Proof limitations or environment constraints:
|
||||
- Before evidence (optional but encouraged):
|
||||
|
||||
<details>
|
||||
<summary>Real behavior proof guidance</summary>
|
||||
|
||||
External contributors must show after-fix evidence from a real OpenClaw setup. Unit tests, mocks, lint, typechecks, snapshots, and CI are supplemental only.
|
||||
|
||||
Screenshots are encouraged even for CLI, console, text, or log changes. Terminal screenshots, copied live output, redacted runtime logs, recordings, and linked artifacts count.
|
||||
|
||||
If your environment cannot produce the ideal proof, explain that under `Proof limitations or environment constraints` so reviewers and ClawSweeper can direct the next step properly.
|
||||
|
||||
Be mindful of private information like IP addresses, API keys, phone numbers, non-public endpoints, or other private details when providing evidence.
|
||||
|
||||
</details>
|
||||
|
||||
## Tests and validation
|
||||
|
||||
Which commands did you run?
|
||||
|
||||
What regression coverage was added or updated?
|
||||
|
||||
What failed before this fix, if known?
|
||||
|
||||
If no test was added, why not?
|
||||
|
||||
<details>
|
||||
<summary>Testing guidance</summary>
|
||||
|
||||
List focused commands, not every incidental check. CI is useful support, but external PRs still need real behavior proof above when behavior changes.
|
||||
|
||||
</details>
|
||||
|
||||
## Risk checklist
|
||||
|
||||
Did user-visible behavior change? (`Yes/No`)
|
||||
|
||||
Did config, environment, or migration behavior change? (`Yes/No`)
|
||||
|
||||
Did security, auth, secrets, network, or tool execution behavior change? (`Yes/No`)
|
||||
|
||||
What is the highest-risk area?
|
||||
|
||||
How is that risk mitigated?
|
||||
|
||||
<details>
|
||||
<summary>Risk guidance</summary>
|
||||
|
||||
Use this for author judgment that is not obvious from the diff. ClawSweeper can see touched files, but it cannot know which behavior you think is risky, why the risk is acceptable, or what mitigation reviewers should verify.
|
||||
|
||||
</details>
|
||||
|
||||
## Current review state
|
||||
|
||||
What is the next action?
|
||||
|
||||
What is still waiting on author, maintainer, CI, or external proof?
|
||||
|
||||
Which bot or reviewer comments were addressed?
|
||||
|
||||
<details>
|
||||
<summary>Review state guidance</summary>
|
||||
|
||||
Keep this as the durable state for review progress. If useful information appears in comments, fold the current next action or blocker back here so maintainers and ClawSweeper do not need to reconstruct state from comment history.
|
||||
|
||||
</details>
|
||||
|
||||
29
.github/workflows/ci-build-artifacts-testbox.yml
vendored
29
.github/workflows/ci-build-artifacts-testbox.yml
vendored
@@ -14,10 +14,6 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.event_name == 'pull_request' && format('{0}-pr-v1-{1}', github.workflow, github.event.pull_request.number) || format('{0}-manual-v1-{1}', github.workflow, github.run_id) }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
@@ -214,49 +210,24 @@ jobs:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
|
||||
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
|
||||
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
|
||||
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
|
||||
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
|
||||
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
|
||||
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
|
||||
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
|
||||
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
|
||||
MODELSTUDIO_API_KEY: ${{ secrets.MODELSTUDIO_API_KEY }}
|
||||
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
|
||||
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
OPENCODE_ZEN_API_KEY: ${{ secrets.OPENCODE_ZEN_API_KEY }}
|
||||
OPENCLAW_LIVE_BROWSER_CDP_URL: ${{ secrets.OPENCLAW_LIVE_BROWSER_CDP_URL }}
|
||||
OPENCLAW_LIVE_SETUP_TOKEN: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN }}
|
||||
OPENCLAW_LIVE_SETUP_TOKEN_MODEL: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_MODEL }}
|
||||
OPENCLAW_LIVE_SETUP_TOKEN_PROFILE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_PROFILE }}
|
||||
OPENCLAW_LIVE_SETUP_TOKEN_VALUE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_VALUE }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
|
||||
FAL_KEY: ${{ secrets.FAL_KEY }}
|
||||
RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }}
|
||||
DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }}
|
||||
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
|
||||
VYDRA_API_KEY: ${{ secrets.VYDRA_API_KEY }}
|
||||
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
|
||||
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
|
||||
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
|
||||
BYTEPLUS_ACCESS_KEY_ID: ${{ secrets.BYTEPLUS_ACCESS_KEY_ID }}
|
||||
BYTEPLUS_SECRET_ACCESS_KEY: ${{ secrets.BYTEPLUS_SECRET_ACCESS_KEY }}
|
||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
OPENCLAW_CODEX_AUTH_JSON: ${{ secrets.OPENCLAW_CODEX_AUTH_JSON }}
|
||||
OPENCLAW_CODEX_CONFIG_TOML: ${{ secrets.OPENCLAW_CODEX_CONFIG_TOML }}
|
||||
OPENCLAW_CLAUDE_JSON: ${{ secrets.OPENCLAW_CLAUDE_JSON }}
|
||||
OPENCLAW_CLAUDE_CREDENTIALS_JSON: ${{ secrets.OPENCLAW_CLAUDE_CREDENTIALS_JSON }}
|
||||
OPENCLAW_CLAUDE_SETTINGS_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_JSON }}
|
||||
OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON }}
|
||||
OPENCLAW_GEMINI_SETTINGS_JSON: ${{ secrets.OPENCLAW_GEMINI_SETTINGS_JSON }}
|
||||
run: bash scripts/ci-hydrate-testbox-env.sh
|
||||
|
||||
- name: Run Testbox
|
||||
|
||||
28
.github/workflows/ci-check-arm-testbox.yml
vendored
28
.github/workflows/ci-check-arm-testbox.yml
vendored
@@ -13,10 +13,6 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.event_name == 'pull_request' && format('{0}-pr-v1-{1}', github.workflow, github.event.pull_request.number) || format('{0}-manual-v1-{1}', github.workflow, github.run_id) }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
PNPM_CONFIG_STORE_DIR: "/tmp/openclaw-pnpm-store"
|
||||
@@ -132,10 +128,8 @@ jobs:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
|
||||
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
|
||||
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
|
||||
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
|
||||
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
|
||||
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
|
||||
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
|
||||
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
@@ -143,38 +137,16 @@ jobs:
|
||||
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
|
||||
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
|
||||
MODELSTUDIO_API_KEY: ${{ secrets.MODELSTUDIO_API_KEY }}
|
||||
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
|
||||
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
OPENCODE_ZEN_API_KEY: ${{ secrets.OPENCODE_ZEN_API_KEY }}
|
||||
OPENCLAW_LIVE_BROWSER_CDP_URL: ${{ secrets.OPENCLAW_LIVE_BROWSER_CDP_URL }}
|
||||
OPENCLAW_LIVE_SETUP_TOKEN: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN }}
|
||||
OPENCLAW_LIVE_SETUP_TOKEN_MODEL: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_MODEL }}
|
||||
OPENCLAW_LIVE_SETUP_TOKEN_PROFILE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_PROFILE }}
|
||||
OPENCLAW_LIVE_SETUP_TOKEN_VALUE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_VALUE }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
|
||||
FAL_KEY: ${{ secrets.FAL_KEY }}
|
||||
RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }}
|
||||
DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }}
|
||||
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
|
||||
VYDRA_API_KEY: ${{ secrets.VYDRA_API_KEY }}
|
||||
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
|
||||
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
|
||||
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
|
||||
BYTEPLUS_ACCESS_KEY_ID: ${{ secrets.BYTEPLUS_ACCESS_KEY_ID }}
|
||||
BYTEPLUS_SECRET_ACCESS_KEY: ${{ secrets.BYTEPLUS_SECRET_ACCESS_KEY }}
|
||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
OPENCLAW_CODEX_AUTH_JSON: ${{ secrets.OPENCLAW_CODEX_AUTH_JSON }}
|
||||
OPENCLAW_CODEX_CONFIG_TOML: ${{ secrets.OPENCLAW_CODEX_CONFIG_TOML }}
|
||||
OPENCLAW_CLAUDE_JSON: ${{ secrets.OPENCLAW_CLAUDE_JSON }}
|
||||
OPENCLAW_CLAUDE_CREDENTIALS_JSON: ${{ secrets.OPENCLAW_CLAUDE_CREDENTIALS_JSON }}
|
||||
OPENCLAW_CLAUDE_SETTINGS_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_JSON }}
|
||||
OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON }}
|
||||
OPENCLAW_GEMINI_SETTINGS_JSON: ${{ secrets.OPENCLAW_GEMINI_SETTINGS_JSON }}
|
||||
run: bash scripts/ci-hydrate-testbox-env.sh
|
||||
|
||||
- name: Run Testbox
|
||||
|
||||
28
.github/workflows/ci-check-testbox.yml
vendored
28
.github/workflows/ci-check-testbox.yml
vendored
@@ -17,10 +17,6 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.event_name == 'pull_request' && format('{0}-pr-v1-{1}', github.workflow, github.event.pull_request.number) || format('{0}-manual-v1-{1}', github.workflow, github.run_id) }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
PNPM_CONFIG_STORE_DIR: "/tmp/openclaw-pnpm-store"
|
||||
@@ -121,10 +117,8 @@ jobs:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
|
||||
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
|
||||
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
|
||||
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
|
||||
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
|
||||
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
|
||||
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
|
||||
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
@@ -132,38 +126,16 @@ jobs:
|
||||
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
|
||||
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
|
||||
MODELSTUDIO_API_KEY: ${{ secrets.MODELSTUDIO_API_KEY }}
|
||||
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
|
||||
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
OPENCODE_ZEN_API_KEY: ${{ secrets.OPENCODE_ZEN_API_KEY }}
|
||||
OPENCLAW_LIVE_BROWSER_CDP_URL: ${{ secrets.OPENCLAW_LIVE_BROWSER_CDP_URL }}
|
||||
OPENCLAW_LIVE_SETUP_TOKEN: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN }}
|
||||
OPENCLAW_LIVE_SETUP_TOKEN_MODEL: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_MODEL }}
|
||||
OPENCLAW_LIVE_SETUP_TOKEN_PROFILE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_PROFILE }}
|
||||
OPENCLAW_LIVE_SETUP_TOKEN_VALUE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_VALUE }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
|
||||
FAL_KEY: ${{ secrets.FAL_KEY }}
|
||||
RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }}
|
||||
DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }}
|
||||
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
|
||||
VYDRA_API_KEY: ${{ secrets.VYDRA_API_KEY }}
|
||||
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
|
||||
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
|
||||
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
|
||||
BYTEPLUS_ACCESS_KEY_ID: ${{ secrets.BYTEPLUS_ACCESS_KEY_ID }}
|
||||
BYTEPLUS_SECRET_ACCESS_KEY: ${{ secrets.BYTEPLUS_SECRET_ACCESS_KEY }}
|
||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
OPENCLAW_CODEX_AUTH_JSON: ${{ secrets.OPENCLAW_CODEX_AUTH_JSON }}
|
||||
OPENCLAW_CODEX_CONFIG_TOML: ${{ secrets.OPENCLAW_CODEX_CONFIG_TOML }}
|
||||
OPENCLAW_CLAUDE_JSON: ${{ secrets.OPENCLAW_CLAUDE_JSON }}
|
||||
OPENCLAW_CLAUDE_CREDENTIALS_JSON: ${{ secrets.OPENCLAW_CLAUDE_CREDENTIALS_JSON }}
|
||||
OPENCLAW_CLAUDE_SETTINGS_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_JSON }}
|
||||
OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON }}
|
||||
OPENCLAW_GEMINI_SETTINGS_JSON: ${{ secrets.OPENCLAW_GEMINI_SETTINGS_JSON }}
|
||||
run: bash scripts/ci-hydrate-testbox-env.sh
|
||||
|
||||
- name: Run Testbox
|
||||
|
||||
129
.github/workflows/ci.yml
vendored
129
.github/workflows/ci.yml
vendored
@@ -41,32 +41,11 @@ env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
jobs:
|
||||
# Keep the canonical main queue quiet long enough for a follow-up push to
|
||||
# cancel this run before it registers the Blacksmith matrix.
|
||||
runner-admission:
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 3
|
||||
env:
|
||||
OPENCLAW_MAIN_CI_DEBOUNCE_SECONDS: "90"
|
||||
steps:
|
||||
- name: Debounce canonical main pushes
|
||||
if: github.event_name == 'push' && github.repository == 'openclaw/openclaw' && github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "Waiting ${OPENCLAW_MAIN_CI_DEBOUNCE_SECONDS}s for a superseding main push before Blacksmith admission"
|
||||
sleep "${OPENCLAW_MAIN_CI_DEBOUNCE_SECONDS}"
|
||||
- name: Admit non-main CI runs immediately
|
||||
if: github.event_name != 'push' || github.repository != 'openclaw/openclaw' || github.ref != 'refs/heads/main'
|
||||
run: echo "No canonical main debounce required"
|
||||
|
||||
# Preflight: establish routing truth and job matrices once, then let real
|
||||
# work fan out from a single source of truth.
|
||||
preflight:
|
||||
permissions:
|
||||
contents: read
|
||||
needs: [runner-admission]
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 20
|
||||
@@ -218,7 +197,7 @@ jobs:
|
||||
node --input-type=module <<'EOF'
|
||||
import { appendFileSync } from "node:fs";
|
||||
import {
|
||||
createNodeTestShardBundles,
|
||||
createNodeTestShards,
|
||||
} from "./scripts/lib/ci-node-test-plan.mjs";
|
||||
import {
|
||||
createChannelContractTestShards,
|
||||
@@ -293,22 +272,18 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
const compactPullRequest = isCanonicalRepository && eventName === "pull_request";
|
||||
const nodeTestShards = runNodeFull
|
||||
? createNodeTestShardBundles({
|
||||
? createNodeTestShards({
|
||||
includeReleaseOnlyPluginShards: false,
|
||||
compact: compactPullRequest,
|
||||
}).map((shard) => ({
|
||||
check_name: shard.checkName,
|
||||
runtime: "node",
|
||||
task: "test-shard",
|
||||
shard_name: shard.shardName,
|
||||
groups: shard.groups,
|
||||
configs: shard.configs,
|
||||
includePatterns: shard.includePatterns,
|
||||
requires_dist: shard.requiresDist,
|
||||
runner: shard.runner,
|
||||
timeout_minutes: shard.timeoutMinutes,
|
||||
}))
|
||||
: [];
|
||||
const nodeTestNonDistShards = nodeTestShards.filter((shard) => !shard.requires_dist);
|
||||
@@ -345,14 +320,7 @@ jobs:
|
||||
run_checks_windows: runWindows,
|
||||
checks_windows_matrix: createMatrix(
|
||||
runWindows
|
||||
? [
|
||||
{
|
||||
check_name: "checks-windows-node-test",
|
||||
runtime: "node",
|
||||
task: "test",
|
||||
runner: "blacksmith-8vcpu-windows-2025",
|
||||
},
|
||||
]
|
||||
? [{ check_name: "checks-windows-node-test", runtime: "node", task: "test" }]
|
||||
: [],
|
||||
),
|
||||
run_macos_node: runMacos,
|
||||
@@ -386,7 +354,6 @@ jobs:
|
||||
security-fast:
|
||||
permissions:
|
||||
contents: read
|
||||
needs: [runner-admission]
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 20
|
||||
@@ -591,7 +558,7 @@ jobs:
|
||||
contents: read
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_build_artifacts == 'true'
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-32vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 20
|
||||
outputs:
|
||||
channels-result: ${{ steps.built_artifact_checks.outputs['channels-result'] }}
|
||||
@@ -852,7 +819,6 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 8
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.checks_fast_core_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -942,7 +908,6 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 8
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.plugin_contracts_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -1023,7 +988,6 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 8
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.channel_contracts_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -1172,11 +1136,10 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_checks_node_core_nondist == 'true'
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (matrix.runner || 'blacksmith-4vcpu-ubuntu-2404') || 'ubuntu-24.04') }}
|
||||
timeout-minutes: ${{ matrix.timeout_minutes || 60 }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (matrix.runner || 'blacksmith-8vcpu-ubuntu-2404') || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 12
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.checks_node_core_nondist_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -1235,7 +1198,6 @@ jobs:
|
||||
- name: Run Node test shard
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
OPENCLAW_NODE_TEST_GROUPS_JSON: ${{ toJson(matrix.groups || null) }}
|
||||
OPENCLAW_NODE_TEST_CONFIGS_JSON: ${{ toJson(matrix.configs) }}
|
||||
OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }}
|
||||
OPENCLAW_VITEST_SHARD_NAME: ${{ matrix.shard_name }}
|
||||
@@ -1250,47 +1212,28 @@ jobs:
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const groups = JSON.parse(process.env.OPENCLAW_NODE_TEST_GROUPS_JSON ?? "null");
|
||||
const plans = Array.isArray(groups) && groups.length > 0
|
||||
? groups
|
||||
: [{
|
||||
configs: JSON.parse(process.env.OPENCLAW_NODE_TEST_CONFIGS_JSON ?? "[]"),
|
||||
includePatterns: JSON.parse(
|
||||
process.env.OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON ?? "null",
|
||||
),
|
||||
shard_name: process.env.OPENCLAW_VITEST_SHARD_NAME,
|
||||
}];
|
||||
for (const plan of plans) {
|
||||
const configs = plan.configs;
|
||||
if (!Array.isArray(configs) || configs.length === 0) {
|
||||
console.error("Missing node test shard configs");
|
||||
process.exit(1);
|
||||
}
|
||||
const childEnv = {
|
||||
...process.env,
|
||||
...(plan.shard_name ? { OPENCLAW_VITEST_SHARD_NAME: plan.shard_name } : {}),
|
||||
};
|
||||
if (Array.isArray(plan.includePatterns) && plan.includePatterns.length > 0) {
|
||||
const includeFile = join(
|
||||
process.env.RUNNER_TEMP ?? ".",
|
||||
`node-test-include-${process.env.GITHUB_JOB ?? "local"}-${Date.now()}.json`,
|
||||
);
|
||||
writeFileSync(includeFile, JSON.stringify(plan.includePatterns), "utf8");
|
||||
childEnv.OPENCLAW_VITEST_INCLUDE_FILE = includeFile;
|
||||
} else {
|
||||
delete childEnv.OPENCLAW_VITEST_INCLUDE_FILE;
|
||||
}
|
||||
const result = spawnSync(
|
||||
"pnpm",
|
||||
["exec", "node", "scripts/test-projects.mjs", ...configs],
|
||||
{
|
||||
env: childEnv,
|
||||
stdio: "inherit",
|
||||
},
|
||||
const configs = JSON.parse(process.env.OPENCLAW_NODE_TEST_CONFIGS_JSON ?? "[]");
|
||||
if (!Array.isArray(configs) || configs.length === 0) {
|
||||
console.error("Missing node test shard configs");
|
||||
process.exit(1);
|
||||
}
|
||||
const includePatterns = JSON.parse(process.env.OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON ?? "null");
|
||||
const childEnv = { ...process.env };
|
||||
if (Array.isArray(includePatterns) && includePatterns.length > 0) {
|
||||
const includeFile = join(
|
||||
process.env.RUNNER_TEMP ?? ".",
|
||||
`node-test-include-${process.env.GITHUB_JOB ?? "local"}-${Date.now()}.json`,
|
||||
);
|
||||
if ((result.status ?? 1) !== 0) {
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
writeFileSync(includeFile, JSON.stringify(includePatterns), "utf8");
|
||||
childEnv.OPENCLAW_VITEST_INCLUDE_FILE = includeFile;
|
||||
}
|
||||
|
||||
const result = spawnSync("pnpm", ["exec", "node", "scripts/test-projects.mjs", ...configs], {
|
||||
env: childEnv,
|
||||
stdio: "inherit",
|
||||
});
|
||||
if ((result.status ?? 1) !== 0) {
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
EOF
|
||||
|
||||
@@ -1305,7 +1248,6 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 8
|
||||
matrix:
|
||||
include:
|
||||
- check_name: check-guards
|
||||
@@ -1322,7 +1264,7 @@ jobs:
|
||||
runner: blacksmith-16vcpu-ubuntu-2404
|
||||
- check_name: check-dependencies
|
||||
task: dependencies
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
runner: blacksmith-8vcpu-ubuntu-2404
|
||||
- check_name: check-test-types
|
||||
task: test-types
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
@@ -1443,39 +1385,30 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check_additional == 'true' }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (matrix.runner || 'blacksmith-4vcpu-ubuntu-2404') || 'ubuntu-24.04') }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 8
|
||||
matrix:
|
||||
include:
|
||||
- check_name: check-additional-boundaries-a
|
||||
group: boundaries
|
||||
boundary_shard: 1/4
|
||||
runner: blacksmith-8vcpu-ubuntu-2404
|
||||
- check_name: check-additional-boundaries-bcd
|
||||
group: boundaries
|
||||
boundary_shard: 2/4,3/4,4/4
|
||||
runner: blacksmith-8vcpu-ubuntu-2404
|
||||
- check_name: check-session-accessor-boundary
|
||||
group: session-accessor-boundary
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- check_name: check-session-transcript-reader-boundary
|
||||
group: session-transcript-reader-boundary
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- check_name: check-additional-extension-channels
|
||||
group: extension-channels
|
||||
runner: blacksmith-8vcpu-ubuntu-2404
|
||||
- check_name: check-additional-extension-bundled
|
||||
group: extension-bundled
|
||||
runner: blacksmith-8vcpu-ubuntu-2404
|
||||
- check_name: check-additional-extension-package-boundary
|
||||
group: extension-package-boundary
|
||||
runner: blacksmith-8vcpu-ubuntu-2404
|
||||
- check_name: check-additional-runtime-topology-architecture
|
||||
group: runtime-topology-architecture
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout
|
||||
shell: bash
|
||||
@@ -1818,7 +1751,7 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_checks_windows == 'true'
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'windows-2025' || (github.repository == 'openclaw/openclaw' && (matrix.runner || 'blacksmith-8vcpu-windows-2025') || 'windows-2025') }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'windows-2025' || (github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-windows-2025' || 'windows-2025') }}
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
@@ -1830,7 +1763,6 @@ jobs:
|
||||
shell: bash
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 2
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.checks_windows_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -2160,7 +2092,6 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 2
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.android_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
81
.github/workflows/clawsweeper-dispatch.yml
vendored
81
.github/workflows/clawsweeper-dispatch.yml
vendored
@@ -18,16 +18,15 @@ permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.event_name == 'push' && format('clawsweeper-dispatch-{0}-{1}', github.repository, github.ref) || format('clawsweeper-dispatch-{0}-{1}', github.repository, github.event.issue.number || github.event.pull_request.number || github.run_id) }}
|
||||
cancel-in-progress: ${{ github.event_name == 'push' || github.event.action == 'edited' || github.event.action == 'synchronize' || github.event.action == 'ready_for_review' }}
|
||||
group: clawsweeper-dispatch-${{ github.repository }}-${{ github.event.issue.number || github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: ${{ github.event.action == 'edited' || github.event.action == 'synchronize' || github.event.action == 'ready_for_review' }}
|
||||
|
||||
jobs:
|
||||
dispatch:
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
${{
|
||||
(github.event_name != 'issue_comment' ||
|
||||
(github.actor != 'clawsweeper[bot]' && github.actor != 'openclaw-clawsweeper[bot]')) &&
|
||||
github.event_name == 'issue_comment' ||
|
||||
!(
|
||||
endsWith(github.actor, '[bot]') &&
|
||||
(github.event.action == 'labeled' || github.event.action == 'unlabeled')
|
||||
@@ -42,34 +41,6 @@ jobs:
|
||||
if: ${{ github.event.action == 'labeled' || github.event.action == 'unlabeled' }}
|
||||
run: sleep 20
|
||||
|
||||
- name: Debounce main push dispatch
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
run: sleep 45
|
||||
|
||||
- name: Install GitHub API backoff helper
|
||||
run: |
|
||||
cat > "$RUNNER_TEMP/github-api-backoff.sh" <<'BASH'
|
||||
gh_api_with_retry() {
|
||||
local attempt output status lower_output
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if output="$(gh api "$@" 2>&1)"; then
|
||||
printf '%s\n' "$output"
|
||||
return 0
|
||||
fi
|
||||
status=$?
|
||||
lower_output="${output,,}"
|
||||
if [[ "$lower_output" != *"rate limit"* && "$output" != *"HTTP 429"* ]]; then
|
||||
printf '%s\n' "$output" >&2
|
||||
return "$status"
|
||||
fi
|
||||
echo "::warning::GitHub API throttled ClawSweeper dispatch on attempt ${attempt}; retrying after backoff." >&2
|
||||
sleep $((attempt * attempt * 5))
|
||||
done
|
||||
printf '%s\n' "$output" >&2
|
||||
return "$status"
|
||||
}
|
||||
BASH
|
||||
|
||||
- name: Create ClawSweeper dispatch token
|
||||
id: token
|
||||
if: ${{ env.HAS_CLAWSWEEPER_APP_PRIVATE_KEY == 'true' }}
|
||||
@@ -81,27 +52,9 @@ jobs:
|
||||
repositories: clawsweeper
|
||||
permission-contents: write
|
||||
|
||||
- name: Pre-filter ClawSweeper comment
|
||||
id: comment_filter
|
||||
if: ${{ github.event_name == 'issue_comment' }}
|
||||
env:
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if grep -Eiq '(^|[[:space:]])@(clawsweeper|openclaw-clawsweeper)\b(\[bot\])?|(^|[[:space:]])/(clawsweeper|review|autoclose|auto([[:space:]]+|-)?merge)\b' <<< "$COMMENT_BODY"; then
|
||||
echo "is_command=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "is_command=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Create target comment token
|
||||
id: target_token
|
||||
if: >-
|
||||
${{
|
||||
github.event_name == 'issue_comment' &&
|
||||
steps.comment_filter.outputs.is_command == 'true' &&
|
||||
env.HAS_CLAWSWEEPER_APP_PRIVATE_KEY == 'true'
|
||||
}}
|
||||
if: ${{ github.event_name == 'issue_comment' && env.HAS_CLAWSWEEPER_APP_PRIVATE_KEY == 'true' }}
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }}
|
||||
@@ -124,7 +77,6 @@ jobs:
|
||||
echo "::notice::Skipping GitHub activity dispatch because no ClawSweeper app token is configured."
|
||||
exit 0
|
||||
fi
|
||||
. "$RUNNER_TEMP/github-api-backoff.sh"
|
||||
activity="$(jq -c \
|
||||
--arg target_repo "$TARGET_REPO" \
|
||||
--arg event_name "$SOURCE_EVENT" \
|
||||
@@ -191,7 +143,7 @@ jobs:
|
||||
' "$GITHUB_EVENT_PATH")"
|
||||
payload="$(jq -nc --argjson activity "$activity" \
|
||||
'{event_type:"github_activity",client_payload:{activity:$activity}}')"
|
||||
if gh_api_with_retry repos/openclaw/clawsweeper/dispatches \
|
||||
if gh api repos/openclaw/clawsweeper/dispatches \
|
||||
--method POST \
|
||||
--input - <<< "$payload"; then
|
||||
echo "Dispatched GitHub activity to ClawSweeper."
|
||||
@@ -213,7 +165,6 @@ jobs:
|
||||
echo "::notice::Skipping ClawSweeper dispatch because no ClawSweeper app token is configured. Not falling back to a maintainer token."
|
||||
exit 0
|
||||
fi
|
||||
. "$RUNNER_TEMP/github-api-backoff.sh"
|
||||
payload="$(jq -nc \
|
||||
--arg target_repo "$TARGET_REPO" \
|
||||
--argjson item_number "$ITEM_NUMBER" \
|
||||
@@ -222,7 +173,7 @@ jobs:
|
||||
--arg source_action "$SOURCE_ACTION" \
|
||||
--argjson supersedes_in_progress "$SUPERSEDES_IN_PROGRESS" \
|
||||
'{event_type:"clawsweeper_item",client_payload:{target_repo:$target_repo,item_number:$item_number,item_kind:$item_kind,source_event:$source_event,source_action:$source_action,supersedes_in_progress:$supersedes_in_progress}}')"
|
||||
if gh_api_with_retry repos/openclaw/clawsweeper/dispatches \
|
||||
if gh api repos/openclaw/clawsweeper/dispatches \
|
||||
--method POST \
|
||||
--input - <<< "$payload"; then
|
||||
echo "Dispatched ClawSweeper review."
|
||||
@@ -231,11 +182,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Acknowledge and dispatch ClawSweeper comment
|
||||
if: >-
|
||||
${{
|
||||
github.event_name == 'issue_comment' &&
|
||||
steps.comment_filter.outputs.is_command == 'true'
|
||||
}}
|
||||
if: ${{ github.event_name == 'issue_comment' }}
|
||||
env:
|
||||
DISPATCH_TOKEN: ${{ steps.token.outputs.token }}
|
||||
TARGET_TOKEN: ${{ steps.target_token.outputs.token }}
|
||||
@@ -251,12 +198,15 @@ jobs:
|
||||
echo "::notice::Skipping ClawSweeper comment dispatch because no ClawSweeper app token is configured."
|
||||
exit 0
|
||||
fi
|
||||
. "$RUNNER_TEMP/github-api-backoff.sh"
|
||||
body_file="$RUNNER_TEMP/clawsweeper-comment-body.txt"
|
||||
printf '%s\n' "$COMMENT_BODY" > "$body_file"
|
||||
if ! grep -Eiq '(^|[[:space:]])@(clawsweeper|openclaw-clawsweeper)\b(\[bot\])?|(^|[[:space:]])/(clawsweeper|review|automerge|autoclose)\b' "$body_file"; then
|
||||
echo "No ClawSweeper command found in comment."
|
||||
exit 0
|
||||
fi
|
||||
if [ -n "$TARGET_TOKEN" ]; then
|
||||
err="$(mktemp)"
|
||||
if GH_TOKEN="$TARGET_TOKEN" gh_api_with_retry -X POST \
|
||||
if GH_TOKEN="$TARGET_TOKEN" gh api -X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"repos/$TARGET_REPO/issues/comments/$COMMENT_ID/reactions" \
|
||||
-f content="eyes" 2>"$err" >/dev/null; then
|
||||
@@ -283,7 +233,7 @@ jobs:
|
||||
"Command router queued. I will update this comment with the next step.")"
|
||||
status_payload="$(jq -nc --arg body "$status_body" '{body:$body}')"
|
||||
status_err="$(mktemp)"
|
||||
if status_response="$(GH_TOKEN="$TARGET_TOKEN" gh_api_with_retry \
|
||||
if status_response="$(GH_TOKEN="$TARGET_TOKEN" gh api \
|
||||
"repos/$TARGET_REPO/issues/$ITEM_NUMBER/comments" \
|
||||
--method POST \
|
||||
--input - <<< "$status_payload" 2>"$status_err")"; then
|
||||
@@ -304,7 +254,7 @@ jobs:
|
||||
--arg source_event "issue_comment" \
|
||||
--arg source_action "$SOURCE_ACTION" \
|
||||
'{event_type:"clawsweeper_comment",client_payload:({target_repo:$target_repo,item_number:$item_number,comment_id:$comment_id,source_event:$source_event,source_action:$source_action,max_comments:"1"} + (if $status_comment_id != "" then {status_comment_id:($status_comment_id|tonumber)} else {} end))}')"
|
||||
if GH_TOKEN="$DISPATCH_TOKEN" gh_api_with_retry repos/openclaw/clawsweeper/dispatches \
|
||||
if GH_TOKEN="$DISPATCH_TOKEN" gh api repos/openclaw/clawsweeper/dispatches \
|
||||
--method POST \
|
||||
--input - <<< "$payload"; then
|
||||
echo "Dispatched ClawSweeper comment router."
|
||||
@@ -326,7 +276,6 @@ jobs:
|
||||
echo "::notice::Skipping ClawSweeper commit dispatch because no ClawSweeper app token is configured. Not falling back to a maintainer token."
|
||||
exit 0
|
||||
fi
|
||||
. "$RUNNER_TEMP/github-api-backoff.sh"
|
||||
case "$CREATE_CHECKS" in
|
||||
true|TRUE|1|yes|YES|on|ON) create_checks=true ;;
|
||||
*) create_checks=false ;;
|
||||
@@ -338,7 +287,7 @@ jobs:
|
||||
--arg ref "$SOURCE_REF" \
|
||||
--argjson create_checks "$create_checks" \
|
||||
'{event_type:"clawsweeper_commit_review",client_payload:{target_repo:$target_repo,before_sha:$before_sha,after_sha:$after_sha,ref:$ref,enabled:true,create_checks:$create_checks}}')"
|
||||
if gh_api_with_retry repos/openclaw/clawsweeper/dispatches \
|
||||
if gh api repos/openclaw/clawsweeper/dispatches \
|
||||
--method POST \
|
||||
--input - <<< "$payload"; then
|
||||
echo "Dispatched ClawSweeper commit review."
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
- cron: "0 7 * * *"
|
||||
|
||||
concurrency:
|
||||
group: codeql-android-critical-security-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && format('manual-{0}', github.run_id) || format('ref-{0}', github.ref) }}
|
||||
group: codeql-android-critical-security-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.sha }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
|
||||
@@ -96,7 +96,7 @@ on:
|
||||
- "src/auto-reply/reply/post-compaction-context.ts"
|
||||
- "src/auto-reply/reply/queue/**"
|
||||
- "src/auto-reply/reply/startup-context.ts"
|
||||
- "src/commands/doctor/cron/dreaming-payload-migration.ts"
|
||||
- "src/commands/doctor-cron-dreaming-payload-migration.ts"
|
||||
- "src/commands/doctor-memory-search.ts"
|
||||
- "src/commands/doctor-session-*.ts"
|
||||
- "src/commands/session-store-targets.ts"
|
||||
@@ -136,7 +136,7 @@ on:
|
||||
- cron: "30 6 * * *"
|
||||
|
||||
concurrency:
|
||||
group: codeql-critical-quality-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && format('manual-{0}', github.run_id) || github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || format('ref-{0}', github.ref) }}
|
||||
group: codeql-critical-quality-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.event_name == 'pull_request' && github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
@@ -257,7 +257,7 @@ jobs:
|
||||
packages/gateway-protocol/src/*|packages/gateway-protocol/src/**/*|src/gateway/method-scopes.ts|src/gateway/server-methods/*|src/gateway/server-methods.ts|src/gateway/server-methods-list.ts)
|
||||
gateway=true
|
||||
;;
|
||||
packages/memory-host-sdk/*|src/commands/doctor/cron/dreaming-payload-migration.ts|src/commands/doctor-memory-search.ts|src/gateway/server-startup-memory.ts|src/memory/*|src/memory-host-sdk/*)
|
||||
packages/memory-host-sdk/*|src/commands/doctor-cron-dreaming-payload-migration.ts|src/commands/doctor-memory-search.ts|src/gateway/server-startup-memory.ts|src/memory/*|src/memory-host-sdk/*)
|
||||
memory=true
|
||||
;;
|
||||
src/infra/outbound/base-session-key.ts|src/infra/outbound/delivery-queue*.ts|src/infra/outbound/outbound-session.ts|src/infra/outbound/session-binding*.ts|src/infra/outbound/session-context.ts|src/infra/outbound/targets-session.ts)
|
||||
@@ -295,7 +295,7 @@ jobs:
|
||||
src/model-catalog/*|src/plugins/*provider*.ts|src/plugins/capability-provider-runtime.ts|src/plugins/compaction-provider.ts|src/plugins/memory-embedding-provider*.ts|src/plugins/memory-embedding-providers*.ts|src/plugins/migration-provider-runtime.ts|src/plugins/synthetic-auth.runtime.ts|src/plugins/web-fetch-providers*.ts|src/plugins/web-search-providers*.ts)
|
||||
provider=true
|
||||
;;
|
||||
src/plugins/activation-planner.ts|src/plugins/api-builder.ts|src/plugins/bundled-*.ts|src/plugins/captured-registration.ts|src/plugins/config-*.ts|src/plugins/discovery.ts|src/plugins/effective-plugin-ids.ts|src/plugins/externalized-bundled-plugins.ts|src/plugins/installed-plugin-index*.ts|src/plugins/loader*.ts|src/plugins/manifest*.ts|src/plugins/module-export.ts|src/plugins/package-entrypoints.ts|src/plugins/plugin-registry*.ts|src/plugins/public-surface*.ts|src/plugins/registry.ts|src/plugins/registry-types.ts|src/plugins/runtime|src/plugins/runtime/*|src/plugins/runtime-state.ts|src/plugins/runtime.ts|src/plugins/sdk-alias.ts|src/plugins/types.ts|src/plugins/validation-diagnostics.ts)
|
||||
src/plugins/activation-planner.ts|src/plugins/api-builder.ts|src/plugins/bundled-*.ts|src/plugins/captured-registration.ts|src/plugins/config-*.ts|src/plugins/discovery.ts|src/plugins/effective-plugin-ids.ts|src/plugins/externalized-bundled-plugins.ts|src/plugins/installed-plugin-index*.ts|src/plugins/loader*.ts|src/plugins/manifest*.ts|src/plugins/module-export.ts|src/plugins/package-entrypoints.ts|src/plugins/plugin-registry*.ts|src/plugins/public-surface*.ts|src/plugins/registry.ts|src/plugins/registry-types.ts|src/plugins/runtime|src/plugins/runtime/*|src/plugins/runtime-state.ts|src/plugins/runtime.ts|src/plugins/sdk-alias.ts|src/plugins/source-loader.ts|src/plugins/types.ts|src/plugins/validation-diagnostics.ts)
|
||||
plugin=true
|
||||
;;
|
||||
packages/plugin-package-contract/*|packages/plugin-sdk/*)
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
- cron: "0 8 * * 1"
|
||||
|
||||
concurrency:
|
||||
group: codeql-macos-critical-security-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && format('manual-{0}', github.run_id) || format('ref-{0}', github.ref) }}
|
||||
group: codeql-macos-critical-security-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.sha }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -32,8 +32,8 @@ on:
|
||||
- cron: "0 6 * * *"
|
||||
|
||||
concurrency:
|
||||
group: codeql-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && format('manual-{0}', github.run_id) || github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || format('ref-{0}', github.ref) }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
|
||||
group: codeql-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.event_name == 'pull_request' && github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
@@ -23,8 +23,8 @@ permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: control-ui-locale-refresh-${{ github.event_name == 'push' && github.ref || github.event_name == 'workflow_dispatch' && format('manual-{0}', github.run_id) || github.event_name == 'release' && format('release-{0}', github.event.release.tag_name) || format('{0}-{1}', github.event_name, github.run_id) }}
|
||||
cancel-in-progress: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
group: control-ui-locale-refresh
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
plan:
|
||||
|
||||
24
.github/workflows/crabbox-hydrate.yml
vendored
24
.github/workflows/crabbox-hydrate.yml
vendored
@@ -663,10 +663,8 @@ jobs:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
|
||||
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
|
||||
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
|
||||
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
|
||||
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
|
||||
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
|
||||
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
|
||||
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
@@ -674,38 +672,16 @@ jobs:
|
||||
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
|
||||
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
|
||||
MODELSTUDIO_API_KEY: ${{ secrets.MODELSTUDIO_API_KEY }}
|
||||
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
|
||||
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
OPENCODE_ZEN_API_KEY: ${{ secrets.OPENCODE_ZEN_API_KEY }}
|
||||
OPENCLAW_LIVE_BROWSER_CDP_URL: ${{ secrets.OPENCLAW_LIVE_BROWSER_CDP_URL }}
|
||||
OPENCLAW_LIVE_SETUP_TOKEN: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN }}
|
||||
OPENCLAW_LIVE_SETUP_TOKEN_MODEL: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_MODEL }}
|
||||
OPENCLAW_LIVE_SETUP_TOKEN_PROFILE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_PROFILE }}
|
||||
OPENCLAW_LIVE_SETUP_TOKEN_VALUE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_VALUE }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
|
||||
FAL_KEY: ${{ secrets.FAL_KEY }}
|
||||
RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }}
|
||||
DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }}
|
||||
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
|
||||
VYDRA_API_KEY: ${{ secrets.VYDRA_API_KEY }}
|
||||
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
|
||||
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
|
||||
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
|
||||
BYTEPLUS_ACCESS_KEY_ID: ${{ secrets.BYTEPLUS_ACCESS_KEY_ID }}
|
||||
BYTEPLUS_SECRET_ACCESS_KEY: ${{ secrets.BYTEPLUS_SECRET_ACCESS_KEY }}
|
||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
OPENCLAW_CODEX_AUTH_JSON: ${{ secrets.OPENCLAW_CODEX_AUTH_JSON }}
|
||||
OPENCLAW_CODEX_CONFIG_TOML: ${{ secrets.OPENCLAW_CODEX_CONFIG_TOML }}
|
||||
OPENCLAW_CLAUDE_JSON: ${{ secrets.OPENCLAW_CLAUDE_JSON }}
|
||||
OPENCLAW_CLAUDE_CREDENTIALS_JSON: ${{ secrets.OPENCLAW_CLAUDE_CREDENTIALS_JSON }}
|
||||
OPENCLAW_CLAUDE_SETTINGS_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_JSON }}
|
||||
OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON }}
|
||||
OPENCLAW_GEMINI_SETTINGS_JSON: ${{ secrets.OPENCLAW_GEMINI_SETTINGS_JSON }}
|
||||
run: bash scripts/ci-hydrate-testbox-env.sh
|
||||
|
||||
- name: Mark Crabbox ready
|
||||
|
||||
4
.github/workflows/docs-sync-publish.yml
vendored
4
.github/workflows/docs-sync-publish.yml
vendored
@@ -13,10 +13,6 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: docs-sync-publish-${{ github.event_name == 'workflow_dispatch' && format('manual-{0}', github.run_id) || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
sync-publish-repo:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
111
.github/workflows/full-release-validation.yml
vendored
111
.github/workflows/full-release-validation.yml
vendored
@@ -70,7 +70,7 @@ on:
|
||||
default: ""
|
||||
type: string
|
||||
npm_telegram_package_spec:
|
||||
description: Optional published package spec for the focused package Telegram E2E rerun
|
||||
description: Optional published package spec for the package Telegram E2E lane
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
@@ -95,7 +95,7 @@ on:
|
||||
default: ""
|
||||
type: string
|
||||
npm_telegram_provider_mode:
|
||||
description: Provider mode for the focused package Telegram E2E rerun
|
||||
description: Provider mode for the package Telegram E2E lane
|
||||
required: false
|
||||
default: mock-openai
|
||||
type: choice
|
||||
@@ -103,7 +103,7 @@ on:
|
||||
- mock-openai
|
||||
- live-frontier
|
||||
npm_telegram_scenario:
|
||||
description: Optional comma-separated Telegram scenario ids for the focused package Telegram E2E rerun
|
||||
description: Optional comma-separated Telegram scenario ids for the package Telegram lane
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
@@ -200,16 +200,14 @@ jobs:
|
||||
if [[ -n "${RELEASE_PACKAGE_SPEC// }" ]]; then
|
||||
echo "- Published release package: \`${RELEASE_PACKAGE_SPEC}\`"
|
||||
fi
|
||||
if [[ "$RERUN_GROUP" == "npm-telegram" && -n "${NPM_TELEGRAM_PACKAGE_SPEC// }" ]]; then
|
||||
if [[ -n "${NPM_TELEGRAM_PACKAGE_SPEC// }" ]]; then
|
||||
echo "- Published-package Telegram E2E: \`${NPM_TELEGRAM_PACKAGE_SPEC}\`"
|
||||
elif [[ "$RERUN_GROUP" == "npm-telegram" && -n "${RELEASE_PACKAGE_SPEC// }" ]]; then
|
||||
elif [[ -n "${RELEASE_PACKAGE_SPEC// }" ]]; then
|
||||
echo "- Published-package Telegram E2E: \`${RELEASE_PACKAGE_SPEC}\`"
|
||||
elif [[ "$RERUN_GROUP" == "npm-telegram" ]]; then
|
||||
echo "- Package Telegram E2E: focused rerun requires \`release_package_spec\` or \`npm_telegram_package_spec\`"
|
||||
elif [[ "$RERUN_GROUP" == "all" || "$RERUN_GROUP" == "release-checks" || "$RERUN_GROUP" == "package" ]]; then
|
||||
echo "- Package Telegram E2E: OpenClaw Release Checks Package Acceptance"
|
||||
elif [[ "$RERUN_GROUP" == "all" && "$RELEASE_PROFILE" == "full" ]]; then
|
||||
echo "- Package Telegram E2E: parent \`release-package-under-test\` artifact"
|
||||
else
|
||||
echo "- Package Telegram E2E: skipped by rerun group"
|
||||
echo "- Package Telegram E2E: skipped unless \`release_profile=full\`, \`release_package_spec\`, or \`npm_telegram_package_spec\` is provided"
|
||||
fi
|
||||
if [[ -n "${EVIDENCE_PACKAGE_SPEC// }" ]]; then
|
||||
echo "- Private evidence package proof: \`${EVIDENCE_PACKAGE_SPEC}\`"
|
||||
@@ -766,13 +764,83 @@ jobs:
|
||||
|
||||
dispatch_and_wait openclaw-release-checks.yml "${args[@]}"
|
||||
|
||||
prepare_release_package:
|
||||
name: Prepare release package artifact
|
||||
needs: [resolve_target, docker_runtime_assets_preflight]
|
||||
if: ${{ always() && needs.resolve_target.result == 'success' && inputs.npm_telegram_package_spec == '' && inputs.release_package_spec == '' && inputs.rerun_group == 'all' && inputs.release_profile == 'full' && needs.docker_runtime_assets_preflight.result == 'success' }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
outputs:
|
||||
artifact_name: ${{ steps.artifact.outputs.name }}
|
||||
package_sha256: ${{ steps.package.outputs.sha256 }}
|
||||
package_version: ${{ steps.package.outputs.package_version }}
|
||||
source_sha: ${{ steps.package.outputs.source_sha }}
|
||||
steps:
|
||||
- name: Checkout trusted workflow ref
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
with:
|
||||
persist-credentials: true
|
||||
ref: ${{ github.ref_name }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set artifact metadata
|
||||
id: artifact
|
||||
run: echo "name=release-package-under-test" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
install-bun: "true"
|
||||
install-deps: "false"
|
||||
|
||||
- name: Resolve release package artifact
|
||||
id: package
|
||||
shell: bash
|
||||
env:
|
||||
PACKAGE_REF: ${{ needs.resolve_target.outputs.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node scripts/resolve-openclaw-package-candidate.mjs \
|
||||
--source ref \
|
||||
--package-ref "$PACKAGE_REF" \
|
||||
--output-dir .artifacts/docker-e2e-package \
|
||||
--output-name openclaw-current.tgz \
|
||||
--metadata .artifacts/docker-e2e-package/package-candidate.json \
|
||||
--github-output "$GITHUB_OUTPUT"
|
||||
digest="$(node -p "JSON.parse(require('fs').readFileSync('.artifacts/docker-e2e-package/package-candidate.json', 'utf8')).sha256")"
|
||||
version="$(node -p "JSON.parse(require('fs').readFileSync('.artifacts/docker-e2e-package/package-candidate.json', 'utf8')).version")"
|
||||
source_sha="$(node -p "JSON.parse(require('fs').readFileSync('.artifacts/docker-e2e-package/package-candidate.json', 'utf8')).packageSourceSha")"
|
||||
echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "## Release package artifact"
|
||||
echo
|
||||
echo "- Artifact: \`release-package-under-test\`"
|
||||
echo "- Package ref: \`$PACKAGE_REF\`"
|
||||
echo "- SHA-256: \`$digest\`"
|
||||
echo "- Version: \`$version\`"
|
||||
echo "- Source SHA: \`$source_sha\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload release package artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: release-package-under-test
|
||||
path: |
|
||||
.artifacts/docker-e2e-package/openclaw-current.tgz
|
||||
.artifacts/docker-e2e-package/package-candidate.json
|
||||
if-no-files-found: error
|
||||
|
||||
npm_telegram:
|
||||
name: Run package Telegram E2E
|
||||
needs: [resolve_target]
|
||||
if: ${{ always() && needs.resolve_target.result == 'success' && inputs.rerun_group == 'npm-telegram' && (inputs.npm_telegram_package_spec != '' || inputs.release_package_spec != '') }}
|
||||
needs: [resolve_target, prepare_release_package]
|
||||
if: ${{ always() && contains(fromJSON('["all","npm-telegram"]'), inputs.rerun_group) && (inputs.npm_telegram_package_spec != '' || inputs.release_package_spec != '' || (inputs.rerun_group == 'all' && inputs.release_profile == 'full')) }}
|
||||
continue-on-error: ${{ startsWith(github.ref, 'refs/heads/tideclaw/alpha/') }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: ${{ inputs.release_profile == 'full' && 360 || 60 }}
|
||||
timeout-minutes: ${{ inputs.release_profile == 'full' && 120 || 60 }}
|
||||
outputs:
|
||||
run_id: ${{ steps.dispatch.outputs.run_id }}
|
||||
url: ${{ steps.dispatch.outputs.url }}
|
||||
@@ -785,6 +853,8 @@ jobs:
|
||||
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
|
||||
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
|
||||
PACKAGE_SPEC: ${{ inputs.npm_telegram_package_spec || inputs.release_package_spec }}
|
||||
PACKAGE_ARTIFACT_NAME: ${{ needs.prepare_release_package.outputs.artifact_name }}
|
||||
PREPARE_PACKAGE_RESULT: ${{ needs.prepare_release_package.result }}
|
||||
PROVIDER_MODE: ${{ inputs.npm_telegram_provider_mode }}
|
||||
SCENARIO: ${{ inputs.npm_telegram_scenario }}
|
||||
run: |
|
||||
@@ -813,7 +883,18 @@ jobs:
|
||||
return "$status"
|
||||
}
|
||||
|
||||
args=(-f package_spec="$PACKAGE_SPEC" -f harness_ref="$TARGET_SHA" -f provider_mode="$PROVIDER_MODE")
|
||||
args=(-f package_spec="${PACKAGE_SPEC:-openclaw@beta}" -f harness_ref="$TARGET_SHA" -f provider_mode="$PROVIDER_MODE")
|
||||
if [[ -z "${PACKAGE_SPEC// }" ]]; then
|
||||
if [[ "$PREPARE_PACKAGE_RESULT" != "success" || -z "${PACKAGE_ARTIFACT_NAME// }" ]]; then
|
||||
echo "Full release Telegram requires either npm_telegram_package_spec or a prepared release-package-under-test artifact." >&2
|
||||
exit 1
|
||||
fi
|
||||
args+=(
|
||||
-f package_artifact_name="$PACKAGE_ARTIFACT_NAME"
|
||||
-f package_artifact_run_id="${GITHUB_RUN_ID}"
|
||||
-f package_label="full-release-${TARGET_SHA:0:12}"
|
||||
)
|
||||
fi
|
||||
if [[ -n "${SCENARIO// }" ]]; then
|
||||
args+=(-f scenario="$SCENARIO")
|
||||
fi
|
||||
@@ -890,7 +971,7 @@ jobs:
|
||||
needs: [resolve_target, docker_runtime_assets_preflight]
|
||||
if: ${{ always() && needs.resolve_target.result == 'success' && contains(fromJSON('["all","performance"]'), inputs.rerun_group) && (inputs.rerun_group != 'all' || needs.docker_runtime_assets_preflight.result == 'success') }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: ${{ inputs.release_profile == 'full' && 360 || 120 }}
|
||||
timeout-minutes: 120
|
||||
outputs:
|
||||
run_id: ${{ steps.dispatch.outputs.run_id }}
|
||||
url: ${{ steps.dispatch.outputs.url }}
|
||||
|
||||
@@ -1686,8 +1686,7 @@ jobs:
|
||||
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
|
||||
OPENCLAW_LIVE_PROVIDERS: ${{ matrix.providers }}
|
||||
OPENCLAW_LIVE_IMAGE: ${{ needs.prepare_live_test_image.outputs.live_image }}
|
||||
OPENCLAW_LIVE_MODELS: ${{ matrix.models || 'modern' }}
|
||||
OPENCLAW_LIVE_MAX_MODELS: ${{ matrix.max_models || '6' }}
|
||||
OPENCLAW_LIVE_MAX_MODELS: "6"
|
||||
OPENCLAW_LIVE_MODEL_TIMEOUT_MS: "45000"
|
||||
OPENCLAW_SKIP_DOCKER_BUILD: "1"
|
||||
OPENCLAW_VITEST_MAX_WORKERS: "2"
|
||||
@@ -2001,7 +2000,7 @@ jobs:
|
||||
profiles: stable full
|
||||
- suite_id: native-live-src-gateway-profiles-minimax
|
||||
label: Native live gateway profiles MiniMax
|
||||
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M2.7,minimax-portal/MiniMax-M2.7 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
|
||||
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M3,minimax-portal/MiniMax-M3 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
|
||||
timeout_minutes: 60
|
||||
profile_env_only: false
|
||||
profiles: stable full
|
||||
@@ -2304,7 +2303,7 @@ jobs:
|
||||
profiles: stable full
|
||||
- suite_id: live-gateway-minimax-docker
|
||||
label: Docker live gateway MiniMax
|
||||
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M2.7,minimax-portal/MiniMax-M2.7 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
|
||||
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M3,minimax-portal/MiniMax-M3 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
|
||||
timeout_minutes: 40
|
||||
profile_env_only: false
|
||||
profiles: stable full
|
||||
|
||||
7
.github/workflows/openclaw-performance.yml
vendored
7
.github/workflows/openclaw-performance.yml
vendored
@@ -45,7 +45,7 @@ on:
|
||||
kova_ref:
|
||||
description: openclaw/Kova Git ref to install
|
||||
required: false
|
||||
default: 4f146016583018bad9e24f8e64a6af5f963bb7ee
|
||||
default: b63b6f9e20efb23641df00487e982230d81a90ac
|
||||
type: string
|
||||
dispatch_id:
|
||||
description: Optional parent workflow dispatch identifier
|
||||
@@ -66,7 +66,6 @@ env:
|
||||
OCM_LINUX_X64_SHA256: b849b8de5d77e97e0df9319703254ae95e29d7f26a7552ea79bf173ff110ea0a
|
||||
KOVA_REPOSITORY: openclaw/Kova
|
||||
PERFORMANCE_MODEL_ID: gpt-5.5
|
||||
KOVA_SCENARIO_TIMEOUT_MS: "300000"
|
||||
|
||||
jobs:
|
||||
kova:
|
||||
@@ -99,7 +98,7 @@ jobs:
|
||||
live: "true"
|
||||
include_filters: "scenario:agent-cold-warm-message"
|
||||
env:
|
||||
KOVA_REF: ${{ inputs.kova_ref || '4f146016583018bad9e24f8e64a6af5f963bb7ee' }}
|
||||
KOVA_REF: ${{ inputs.kova_ref || 'b63b6f9e20efb23641df00487e982230d81a90ac' }}
|
||||
KOVA_HOME: ${{ github.workspace }}/.artifacts/kova/home/${{ matrix.lane }}
|
||||
PERFORMANCE_HELPER_DIR: ${{ github.workspace }}/.artifacts/performance-workflow
|
||||
REPORT_DIR: ${{ github.workspace }}/.artifacts/kova/reports/${{ matrix.lane }}
|
||||
@@ -292,7 +291,6 @@ jobs:
|
||||
--auth "$AUTH_MODE"
|
||||
--parallel 1
|
||||
--repeat "$repeat"
|
||||
--timeout-ms "$KOVA_SCENARIO_TIMEOUT_MS"
|
||||
--report-dir "$REPORT_DIR"
|
||||
--execute
|
||||
--json
|
||||
@@ -363,7 +361,6 @@ jobs:
|
||||
- Kova repository: ${KOVA_REPOSITORY}
|
||||
- Kova ref: ${KOVA_REF}
|
||||
- Kova profile: ${PROFILE}
|
||||
- Kova scenario timeout: ${KOVA_SCENARIO_TIMEOUT_MS}ms
|
||||
- Lane auth: ${AUTH_MODE}
|
||||
- Lane model: ${PERFORMANCE_MODEL_ID}
|
||||
- Lane repeat: ${repeat}
|
||||
|
||||
@@ -717,6 +717,7 @@ jobs:
|
||||
published_upgrade_survivor_baselines: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'last-stable-4 2026.4.23 2026.5.2 2026.4.15' || '' }}
|
||||
published_upgrade_survivor_scenarios: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'reported-issues' || '' }}
|
||||
telegram_mode: mock-openai
|
||||
telegram_scenarios: telegram-help-command,telegram-commands-command,telegram-tools-compact-command,telegram-whoami-command,telegram-status-command,telegram-other-bot-command-gating,telegram-context-command,telegram-mentioned-message-reply,telegram-long-final-reuses-preview,telegram-mention-gating
|
||||
secrets:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
|
||||
|
||||
24
.github/workflows/openclaw-release-publish.yml
vendored
24
.github/workflows/openclaw-release-publish.yml
vendored
@@ -519,7 +519,12 @@ jobs:
|
||||
local workflow="$1"
|
||||
shift
|
||||
|
||||
local dispatch_output run_id
|
||||
local before_json dispatch_output run_id
|
||||
before_json="$(gh api -X GET "repos/${GITHUB_REPOSITORY}/actions/workflows/${workflow}/runs" \
|
||||
-F event=workflow_dispatch \
|
||||
-F per_page=100 \
|
||||
--jq '[.workflow_runs[].id]')"
|
||||
|
||||
dispatch_output="$(gh workflow run --repo "$GITHUB_REPOSITORY" "$workflow" --ref "$workflow_ref" "$@" 2>&1)"
|
||||
printf '%s\n' "$dispatch_output" >&2
|
||||
run_id="$(
|
||||
@@ -529,7 +534,22 @@ jobs:
|
||||
)"
|
||||
|
||||
if [[ -z "$run_id" ]]; then
|
||||
echo "gh workflow run ${workflow} did not return an Actions run URL; refusing to guess from recent workflow_dispatch runs." >&2
|
||||
for _ in $(seq 1 60); do
|
||||
run_id="$(
|
||||
BEFORE_IDS="$before_json" gh api -X GET "repos/${GITHUB_REPOSITORY}/actions/workflows/${workflow}/runs" \
|
||||
-F event=workflow_dispatch \
|
||||
-F per_page=50 \
|
||||
--jq '.workflow_runs | map({databaseId:.id, createdAt:.created_at}) | map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
|
||||
)"
|
||||
if [[ -n "$run_id" ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -z "${run_id:-}" ]]; then
|
||||
echo "Could not find dispatched run for ${workflow}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
114
.github/workflows/openclaw-stable-main-closeout.yml
vendored
114
.github/workflows/openclaw-stable-main-closeout.yml
vendored
@@ -23,8 +23,8 @@ permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: openclaw-stable-main-closeout-${{ github.event_name == 'workflow_dispatch' && (inputs.tag || github.run_id) || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
group: openclaw-stable-main-closeout
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
resolve:
|
||||
@@ -43,30 +43,6 @@ jobs:
|
||||
should_closeout: ${{ steps.inputs.outputs.should_closeout }}
|
||||
tag: ${{ steps.inputs.outputs.tag }}
|
||||
steps:
|
||||
- name: Install GitHub API backoff helper
|
||||
run: |
|
||||
cat > "$RUNNER_TEMP/github-api-backoff.sh" <<'BASH'
|
||||
gh_with_retry() {
|
||||
local attempt output status lower_output
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if output="$(gh "$@" 2>&1)"; then
|
||||
printf '%s\n' "$output"
|
||||
return 0
|
||||
fi
|
||||
status=$?
|
||||
lower_output="${output,,}"
|
||||
if [[ "$lower_output" != *"rate limit"* && "$output" != *"HTTP 429"* ]]; then
|
||||
printf '%s\n' "$output" >&2
|
||||
return "$status"
|
||||
fi
|
||||
echo "::warning::GitHub API throttled stable closeout on attempt ${attempt}; retrying after backoff." >&2
|
||||
sleep $((attempt * attempt * 5))
|
||||
done
|
||||
printf '%s\n' "$output" >&2
|
||||
return "$status"
|
||||
}
|
||||
BASH
|
||||
|
||||
- name: Checkout pushed main
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
@@ -86,13 +62,9 @@ jobs:
|
||||
TRIGGER_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "$EVENT_NAME" == "push" ]]; then
|
||||
sleep 45
|
||||
fi
|
||||
. "$RUNNER_TEMP/github-api-backoff.sh"
|
||||
if [[ "$EVENT_NAME" == "push" ]]; then
|
||||
main_ref="$TRIGGER_SHA"
|
||||
tag="$(gh_with_retry release list --repo "$GITHUB_REPOSITORY" --exclude-drafts --limit 100 \
|
||||
tag="$(gh release list --repo "$GITHUB_REPOSITORY" --exclude-drafts --limit 100 \
|
||||
--json tagName,isPrerelease,publishedAt \
|
||||
--jq '[.[] | select(.isPrerelease | not) | select(.tagName | test("^v[0-9]{4}\\.[0-9]+\\.[0-9]+(-[0-9]+)?$"))] | sort_by(.publishedAt) | last | .tagName // empty')"
|
||||
if [[ -z "$tag" ]]; then
|
||||
@@ -116,27 +88,8 @@ jobs:
|
||||
if [[ "$release_package_version" =~ ^(.+)-[0-9]+$ ]]; then
|
||||
fallback_package_version="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
tag_package_content="$RUNNER_TEMP/tag-package-content.b64"
|
||||
tag_package_read=false
|
||||
for attempt in 1 2 3; do
|
||||
if gh_with_retry api "repos/$GITHUB_REPOSITORY/contents/package.json?ref=$tag" \
|
||||
--jq '.content' > "$tag_package_content"; then
|
||||
tag_package_read=true
|
||||
break
|
||||
fi
|
||||
if [[ "$attempt" != "3" ]]; then
|
||||
sleep $((attempt * 5))
|
||||
fi
|
||||
done
|
||||
if [[ "$tag_package_read" != "true" ]]; then
|
||||
echo "Stable closeout could not read package.json for $tag from GitHub API." >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! tag_package_json="$(tr -d '\n' < "$tag_package_content" | base64 --decode)"; then
|
||||
echo "Stable closeout package.json content for $tag was not valid base64." >&2
|
||||
exit 1
|
||||
fi
|
||||
tag_package_version="$(jq -r '.version // empty' <<<"$tag_package_json")"
|
||||
tag_package_version="$(gh api "repos/$GITHUB_REPOSITORY/contents/package.json?ref=$tag" \
|
||||
--jq '.content' | tr -d '\n' | base64 --decode | jq -r '.version // empty')"
|
||||
fallback_correction=false
|
||||
evidence_source_tag="$tag"
|
||||
if [[ "$release_package_version" != "$fallback_package_version" &&
|
||||
@@ -154,7 +107,7 @@ jobs:
|
||||
closeout_checksum_asset="${closeout_asset}.sha256"
|
||||
closeout_dir="$RUNNER_TEMP/release-closeout-evidence"
|
||||
mkdir -p "$closeout_dir"
|
||||
gh_with_retry release download "$tag" --repo "$GITHUB_REPOSITORY" \
|
||||
gh release download "$tag" --repo "$GITHUB_REPOSITORY" \
|
||||
--pattern "$closeout_asset" --pattern "$closeout_checksum_asset" --dir "$closeout_dir" || true
|
||||
closeout_json_path="$closeout_dir/$closeout_asset"
|
||||
closeout_checksum_path="$closeout_dir/$closeout_checksum_asset"
|
||||
@@ -210,11 +163,8 @@ jobs:
|
||||
fi
|
||||
evidence_dir="$RUNNER_TEMP/release-postpublish-evidence"
|
||||
mkdir -p "$evidence_dir"
|
||||
gh_with_retry release download "$evidence_source_tag" --repo "$GITHUB_REPOSITORY" \
|
||||
--pattern "$evidence_asset" --pattern "$evidence_checksum_asset" --dir "$evidence_dir" || true
|
||||
evidence_path="$evidence_dir/$evidence_asset"
|
||||
evidence_checksum_path="$evidence_dir/$evidence_checksum_asset"
|
||||
if [[ ! -f "$evidence_path" || ! -f "$evidence_checksum_path" ]]; then
|
||||
if ! gh release download "$evidence_source_tag" --repo "$GITHUB_REPOSITORY" \
|
||||
--pattern "$evidence_asset" --pattern "$evidence_checksum_asset" --dir "$evidence_dir"; then
|
||||
if [[ "$EVENT_NAME" == "push" ]]; then
|
||||
echo "Stable closeout skipped: $evidence_source_tag predates immutable postpublish evidence." >&2
|
||||
echo "should_closeout=false" >> "$GITHUB_OUTPUT"
|
||||
@@ -223,6 +173,7 @@ jobs:
|
||||
echo "Stable closeout is required for $tag, but immutable postpublish evidence from $evidence_source_tag is missing." >&2
|
||||
exit 1
|
||||
fi
|
||||
evidence_path="$evidence_dir/$evidence_asset"
|
||||
if ! (
|
||||
cd "$evidence_dir"
|
||||
sha256sum --strict --status -c "$evidence_checksum_asset"
|
||||
@@ -244,11 +195,6 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$ROLLBACK_DRILL_ID" || -z "$ROLLBACK_DRILL_DATE" ]]; then
|
||||
if [[ "$EVENT_NAME" == "push" ]]; then
|
||||
echo "::warning::Stable closeout skipped: rollback drill repository variables are missing; manual dispatch remains required to complete closeout."
|
||||
echo "should_closeout=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
echo "Stable closeout requires repository variables RELEASE_ROLLBACK_DRILL_ID and RELEASE_ROLLBACK_DRILL_DATE, or explicit manual overrides." >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -307,30 +253,6 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install GitHub API backoff helper
|
||||
run: |
|
||||
cat > "$RUNNER_TEMP/github-api-backoff.sh" <<'BASH'
|
||||
gh_with_retry() {
|
||||
local attempt output status lower_output
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if output="$(gh "$@" 2>&1)"; then
|
||||
printf '%s\n' "$output"
|
||||
return 0
|
||||
fi
|
||||
status=$?
|
||||
lower_output="${output,,}"
|
||||
if [[ "$lower_output" != *"rate limit"* && "$output" != *"HTTP 429"* ]]; then
|
||||
printf '%s\n' "$output" >&2
|
||||
return "$status"
|
||||
fi
|
||||
echo "::warning::GitHub API throttled stable closeout on attempt ${attempt}; retrying after backoff." >&2
|
||||
sleep $((attempt * attempt * 5))
|
||||
done
|
||||
printf '%s\n' "$output" >&2
|
||||
return "$status"
|
||||
}
|
||||
BASH
|
||||
|
||||
- name: Verify release workflow evidence
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
@@ -338,8 +260,7 @@ jobs:
|
||||
RELEASE_PUBLISH_RUN_ID: ${{ needs.resolve.outputs.release_publish_run_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
. "$RUNNER_TEMP/github-api-backoff.sh"
|
||||
gh_with_retry run view "$FULL_RELEASE_VALIDATION_RUN_ID" --repo "$GITHUB_REPOSITORY" \
|
||||
gh run view "$FULL_RELEASE_VALIDATION_RUN_ID" --repo "$GITHUB_REPOSITORY" \
|
||||
--json workflowName,event,status,conclusion \
|
||||
> "$RUNNER_TEMP/full-release-validation-run.json"
|
||||
node --input-type=module - "$RUNNER_TEMP/full-release-validation-run.json" <<'NODE'
|
||||
@@ -356,7 +277,7 @@ jobs:
|
||||
}
|
||||
}
|
||||
NODE
|
||||
gh_with_retry run view "$RELEASE_PUBLISH_RUN_ID" --repo "$GITHUB_REPOSITORY" \
|
||||
gh run view "$RELEASE_PUBLISH_RUN_ID" --repo "$GITHUB_REPOSITORY" \
|
||||
--json workflowName,event,status,conclusion \
|
||||
> "$RUNNER_TEMP/release-publish-run.json"
|
||||
node --input-type=module - "$RUNNER_TEMP/release-publish-run.json" <<'NODE'
|
||||
@@ -377,7 +298,7 @@ jobs:
|
||||
manifest_dir="$RUNNER_TEMP/full-release-validation-manifest"
|
||||
rm -rf "$manifest_dir"
|
||||
mkdir -p "$manifest_dir"
|
||||
gh_with_retry run download "$FULL_RELEASE_VALIDATION_RUN_ID" --repo "$GITHUB_REPOSITORY" \
|
||||
gh run download "$FULL_RELEASE_VALIDATION_RUN_ID" --repo "$GITHUB_REPOSITORY" \
|
||||
--name "full-release-validation-${FULL_RELEASE_VALIDATION_RUN_ID}" \
|
||||
--dir "$manifest_dir"
|
||||
tag_sha="$(git -C "$GITHUB_WORKSPACE/release-tag" rev-parse HEAD)"
|
||||
@@ -406,8 +327,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$CLOSEOUT_DIR"
|
||||
. "$RUNNER_TEMP/github-api-backoff.sh"
|
||||
gh_with_retry release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \
|
||||
gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \
|
||||
--json tagName,isDraft,isPrerelease,assets \
|
||||
> "$CLOSEOUT_DIR/github-release.json"
|
||||
node scripts/verify-stable-main-closeout.mjs \
|
||||
@@ -433,23 +353,21 @@ jobs:
|
||||
CLOSEOUT_DIR: ${{ runner.temp }}/openclaw-stable-main-closeout
|
||||
run: |
|
||||
set -euo pipefail
|
||||
. "$RUNNER_TEMP/github-api-backoff.sh"
|
||||
release_version="${RELEASE_TAG#v}"
|
||||
attach_or_verify() {
|
||||
local source_path="$1"
|
||||
local asset_name="$2"
|
||||
local existing_dir="$CLOSEOUT_DIR/existing-${asset_name}"
|
||||
mkdir -p "$existing_dir"
|
||||
gh_with_retry release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \
|
||||
--pattern "$asset_name" --dir "$existing_dir" || true
|
||||
if [[ -f "$existing_dir/$asset_name" ]]; then
|
||||
if gh release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \
|
||||
--pattern "$asset_name" --dir "$existing_dir"; then
|
||||
cmp --silent "$source_path" "$existing_dir/$asset_name" || {
|
||||
echo "Existing release asset $asset_name differs from closeout evidence." >&2
|
||||
exit 1
|
||||
}
|
||||
return
|
||||
fi
|
||||
gh_with_retry release upload "$RELEASE_TAG" "$source_path#$asset_name" --repo "$GITHUB_REPOSITORY"
|
||||
gh release upload "$RELEASE_TAG" "$source_path#$asset_name" --repo "$GITHUB_REPOSITORY"
|
||||
}
|
||||
attach_or_verify \
|
||||
"$CLOSEOUT_DIR/stable-main-closeout.json" \
|
||||
|
||||
4
.github/workflows/plugin-npm-release.yml
vendored
4
.github/workflows/plugin-npm-release.yml
vendored
@@ -38,8 +38,8 @@ on:
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: plugin-npm-release-${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
group: plugin-npm-release-${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
@@ -532,7 +532,6 @@ jobs:
|
||||
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
|
||||
OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "1800000"
|
||||
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
|
||||
OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS: "180000"
|
||||
INPUT_SCENARIO: ${{ github.event_name == 'workflow_dispatch' && inputs.scenario || '' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -625,7 +624,6 @@ jobs:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
|
||||
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
|
||||
OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "1800000"
|
||||
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
|
||||
OPENCLAW_QA_DISCORD_CAPTURE_CONTENT: "1"
|
||||
INPUT_SCENARIO: ${{ github.event_name == 'workflow_dispatch' && inputs.discord_scenario || '' }}
|
||||
@@ -723,7 +721,6 @@ jobs:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
|
||||
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
|
||||
OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "1800000"
|
||||
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
|
||||
OPENCLAW_QA_WHATSAPP_CAPTURE_CONTENT: "1"
|
||||
INPUT_SCENARIO: ${{ github.event_name == 'workflow_dispatch' && inputs.whatsapp_scenario || '' }}
|
||||
@@ -818,7 +815,6 @@ jobs:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
|
||||
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
|
||||
OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "1800000"
|
||||
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
|
||||
OPENCLAW_QA_SLACK_CAPTURE_CONTENT: "1"
|
||||
OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS: "180000"
|
||||
|
||||
2
.github/workflows/sandbox-common-smoke.yml
vendored
2
.github/workflows/sandbox-common-smoke.yml
vendored
@@ -19,7 +19,7 @@ permissions:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
69
.github/workflows/windows-blacksmith-testbox.yml
vendored
69
.github/workflows/windows-blacksmith-testbox.yml
vendored
@@ -57,10 +57,6 @@ jobs:
|
||||
echo "could not read required Blacksmith metadata" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! jq -e 'type == "number"' <<<"$installation_model_id" >/dev/null; then
|
||||
echo "invalid Blacksmith installation model id: ${installation_model_id}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "${BLACKSMITH_HOSTNAME:-}" ]; then
|
||||
runner_host="$BLACKSMITH_HOSTNAME"
|
||||
@@ -69,32 +65,21 @@ jobs:
|
||||
fi
|
||||
runner_ssh_port="${BLACKSMITH_SSH_PORT:-22}"
|
||||
|
||||
hydrating_body="$RUNNER_TEMP/testbox-hydrating.json"
|
||||
hydrating_response="$RUNNER_TEMP/testbox-hydrating.response"
|
||||
jq -n \
|
||||
--arg testbox_id "$TESTBOX_ID" \
|
||||
--argjson installation_model_id "$installation_model_id" \
|
||||
--arg status "hydrating" \
|
||||
--arg ip_address "$runner_host" \
|
||||
--arg ssh_port "$runner_ssh_port" \
|
||||
--arg working_directory "$GITHUB_WORKSPACE" \
|
||||
--arg adopted_run_id "$GITHUB_RUN_ID" \
|
||||
'{
|
||||
testbox_id: $testbox_id,
|
||||
installation_model_id: $installation_model_id,
|
||||
status: $status,
|
||||
ip_address: $ip_address,
|
||||
ssh_port: $ssh_port,
|
||||
working_directory: $working_directory,
|
||||
adopted_run_id: $adopted_run_id,
|
||||
metadata: {}
|
||||
}' > "$hydrating_body"
|
||||
|
||||
hydrating_http_code="$(curl -sS -L --post302 --post303 -o "$hydrating_response" -w '%{http_code}' \
|
||||
-X POST "${api_url}/api/testbox/phone-home" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer ${auth_token}" \
|
||||
--data-binary @"$hydrating_body" || true)"
|
||||
-d "{
|
||||
\"testbox_id\": \"${TESTBOX_ID}\",
|
||||
\"installation_model_id\": ${installation_model_id},
|
||||
\"status\": \"hydrating\",
|
||||
\"ip_address\": \"${runner_host}\",
|
||||
\"ssh_port\": \"${runner_ssh_port}\",
|
||||
\"working_directory\": \"${GITHUB_WORKSPACE}\",
|
||||
\"adopted_run_id\": \"${GITHUB_RUN_ID}\",
|
||||
\"metadata\": {}
|
||||
}" || true)"
|
||||
|
||||
echo "phone_home_hydrating_http=${hydrating_http_code}"
|
||||
if [[ ! "$hydrating_http_code" =~ ^2 ]]; then
|
||||
@@ -167,30 +152,20 @@ jobs:
|
||||
runner_ssh_port="$(cat "$state/runner_ssh_port")"
|
||||
working_directory="$(cat "$state/working_directory")"
|
||||
adopted_run_id="$(cat "$state/adopted_run_id")"
|
||||
if ! jq -e 'type == "number"' <<<"$installation_model_id" >/dev/null; then
|
||||
echo "invalid Blacksmith installation model id: ${installation_model_id}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ready_body="$RUNNER_TEMP/testbox-ready.json"
|
||||
jq -n \
|
||||
--arg testbox_id "$testbox_id" \
|
||||
--argjson installation_model_id "$installation_model_id" \
|
||||
--arg status "ready" \
|
||||
--arg ip_address "$runner_host" \
|
||||
--arg ssh_port "$runner_ssh_port" \
|
||||
--arg working_directory "$working_directory" \
|
||||
--arg adopted_run_id "$adopted_run_id" \
|
||||
'{
|
||||
testbox_id: $testbox_id,
|
||||
installation_model_id: $installation_model_id,
|
||||
status: $status,
|
||||
ip_address: $ip_address,
|
||||
ssh_port: $ssh_port,
|
||||
working_directory: $working_directory,
|
||||
adopted_run_id: $adopted_run_id,
|
||||
metadata: {}
|
||||
}' > "$ready_body"
|
||||
cat > "$ready_body" <<JSON
|
||||
{
|
||||
"testbox_id": "${testbox_id}",
|
||||
"installation_model_id": ${installation_model_id},
|
||||
"status": "ready",
|
||||
"ip_address": "${runner_host}",
|
||||
"ssh_port": "${runner_ssh_port}",
|
||||
"working_directory": "${working_directory}",
|
||||
"adopted_run_id": "${adopted_run_id}",
|
||||
"metadata": {}
|
||||
}
|
||||
JSON
|
||||
|
||||
http_code="$(curl -sS -L --post302 --post303 -o "$RUNNER_TEMP/testbox-ready.response" -w '%{http_code}' \
|
||||
-X POST "${api_url}/api/testbox/phone-home" \
|
||||
|
||||
15
.github/workflows/windows-testbox-probe.yml
vendored
15
.github/workflows/windows-testbox-probe.yml
vendored
@@ -85,22 +85,12 @@ jobs:
|
||||
env:
|
||||
ENABLE_WSL2_FEATURES: ${{ inputs.enable_wsl2_features }}
|
||||
IMPORT_UBUNTU_WSL2: ${{ inputs.import_ubuntu_wsl2 }}
|
||||
UBUNTU_WSL_ROOTFS_URL: https://cloud-images.ubuntu.com/wsl/releases/24.04/current/ubuntu-noble-wsl-amd64-wsl.rootfs.tar.gz
|
||||
run: |
|
||||
$ErrorActionPreference = "Continue"
|
||||
$ok = $false
|
||||
$restartRequired = $false
|
||||
|
||||
function Resolve-UbuntuWslRootfsUrl {
|
||||
$osArch = ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture).ToString().ToLowerInvariant()
|
||||
switch ($osArch) {
|
||||
"x64" { $wslArch = "amd64" }
|
||||
"arm64" { $wslArch = "arm64" }
|
||||
default { throw "Unsupported Windows architecture for Ubuntu WSL rootfs: $osArch" }
|
||||
}
|
||||
Write-Host "ubuntu_wsl_rootfs_arch=$wslArch"
|
||||
"https://cloud-images.ubuntu.com/wsl/releases/24.04/current/ubuntu-noble-wsl-$wslArch-wsl.rootfs.tar.gz"
|
||||
}
|
||||
|
||||
function Invoke-WslText {
|
||||
param([string[]] $Arguments)
|
||||
$output = & wsl.exe @Arguments 2>&1
|
||||
@@ -153,9 +143,8 @@ jobs:
|
||||
Write-Host "import_ubuntu_wsl2=true"
|
||||
$wslRoot = "C:\wsl\UbuntuProbe"
|
||||
$rootfs = "C:\wsl\ubuntu-noble-wsl.rootfs.tar.gz"
|
||||
$rootfsUrl = Resolve-UbuntuWslRootfsUrl
|
||||
New-Item -ItemType Directory -Force -Path @((Split-Path -Parent $rootfs), $wslRoot) | Out-Null
|
||||
Invoke-WebRequest -Uri $rootfsUrl -OutFile $rootfs -UseBasicParsing
|
||||
Invoke-WebRequest -Uri $env:UBUNTU_WSL_ROOTFS_URL -OutFile $rootfs -UseBasicParsing
|
||||
$import = Invoke-WslText -Arguments @("--import", "UbuntuProbe", $wslRoot, $rootfs, "--version", "2")
|
||||
Write-Host $import.Text
|
||||
Write-Host "wsl_import_exit=$($import.Code)"
|
||||
|
||||
25
.github/workflows/workflow-sanity.yml
vendored
25
.github/workflows/workflow-sanity.yml
vendored
@@ -129,28 +129,11 @@ jobs:
|
||||
trusted_config="$RUNNER_TEMP/pre-commit-base.yaml"
|
||||
trusted_zizmor_config="$RUNNER_TEMP/zizmor-base.yml"
|
||||
|
||||
fetch_base_ref() {
|
||||
local ref="$1"
|
||||
local target="$2"
|
||||
local fetch_status
|
||||
for attempt in 1 2 3; do
|
||||
timeout --signal=TERM --kill-after=10s 30s git fetch --no-tags --depth=1 origin \
|
||||
"+${ref}:${target}" && return 0
|
||||
fetch_status="$?"
|
||||
if [ "$fetch_status" != "124" ] && [ "$fetch_status" != "137" ]; then
|
||||
return "$fetch_status"
|
||||
fi
|
||||
if [ "$attempt" = "3" ]; then
|
||||
return "$fetch_status"
|
||||
fi
|
||||
echo "::warning::trusted base fetch for '$ref' timed out on attempt $attempt; retrying"
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
|
||||
if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
|
||||
fetch_base_ref "$BASE_SHA" "refs/remotes/origin/security-base" ||
|
||||
fetch_base_ref "refs/heads/${BASE_REF}" "refs/remotes/origin/${BASE_REF}"
|
||||
timeout --signal=TERM --kill-after=10s 30s git fetch --no-tags --depth=1 origin \
|
||||
"+${BASE_SHA}:refs/remotes/origin/security-base" ||
|
||||
timeout --signal=TERM --kill-after=10s 30s git fetch --no-tags --depth=1 origin \
|
||||
"+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}"
|
||||
fi
|
||||
|
||||
if git cat-file -e "${BASE_SHA}:.pre-commit-config.yaml" 2>/dev/null; then
|
||||
|
||||
@@ -35,7 +35,7 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- One-sided fixes need sibling-surface proof, an explanation for why siblings are unaffected, or explicit follow-up work.
|
||||
- Changelog findings: see Docs / Changelog.
|
||||
- Public ClawSweeper comments prefer `https://docs.openclaw.ai/...` when a public docs page exists; structured evidence still cites repo files, lines, SHAs.
|
||||
- Findings need current source, shipped/current behavior, tests/CI evidence, and dependency contract proof when dependency-backed behavior is involved. Validation is judged against touched and sibling surfaces plus this file's commands; clear evidence matters for user-visible changes, with Telegram/Desktop proof for Telegram-visible behavior when feasible.
|
||||
- Findings need current source, shipped/current behavior, tests/CI evidence, and dependency contract proof when dependency-backed behavior is involved. Validation is judged against touched and sibling surfaces plus this file's commands; real behavior proof matters for user-visible changes, with Telegram/Desktop proof for Telegram-visible behavior when feasible.
|
||||
- Prefer findings for concrete behavior regressions, missing changed-surface proof, owner-boundary violations, security/API contract issues, or docs/config mismatches.
|
||||
- Do not file findings for repo policy preference when changed code follows the relevant scoped guide and no user-visible, runtime, security, or maintainer-risk impact is shown.
|
||||
|
||||
@@ -165,12 +165,13 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- Representing user: if user already has a comment/thread for the point, update/reply there when possible; avoid duplicate PR/issue comments.
|
||||
- No surprise GH writes: chat must mention every posted/updated public comment with URL.
|
||||
- GH comments with backticks, `$`, or shell snippets: use heredoc/body file, not inline double-quoted `--body`.
|
||||
- PR create: real body required. Use the current template: `What Problem This Solves`, `Why This Change Was Made`, `User Impact`, and `Evidence`; include visible refs, behavior, and validation.
|
||||
- PR create: real body required. Include Summary + Verification; mention refs, behavior, and proof.
|
||||
- PR create/refresh: keep PR branches takeover-ready. Use a branch maintainers can push to, or for fork PRs ensure `maintainer_can_modify` / GitHub's `Allow edits by maintainers` is enabled unless explicitly told otherwise or GitHub's Actions/secrets warning makes that unsafe.
|
||||
- GitHub issue/PR create: read `$agent-transcript`; ask about sanitized transcript logs when available.
|
||||
- Contributor PRs: parsed context requires authored `What Problem This Solves` and `Evidence` sections. Do not require field-level proof forms; reviewers inspect code, tests, and CI for correctness.
|
||||
- Contributor PRs: parsed `Real behavior proof` uses exact `field: value` labels: `Behavior addressed`, `Real environment tested`, `Exact steps or command run after this patch`, `Evidence after fix`, `Observed result after fix`, `What was not tested`.
|
||||
- PR artifacts/screenshots: attach to PR/comment/external artifact store. Never push screenshots, videos, proof images, or proof assets to OpenClaw or any product repo branch, including temp artifact branches. Use Crabbox artifact publishing plus the manifest URL. Do not commit `.github/pr-assets`.
|
||||
- CI polling: exact SHA, relevant checks only, minimal fields. Skip routine noise (`Auto response`, `Labeler`, docs agents, performance/stale). Logs only after failure/completion or concrete need.
|
||||
- OpenClaw write-access maintainers may skip `Real behavior proof` when local tests or Crabbox verified behavior; record proof in PR verification.
|
||||
- Agent PR landing to `main`: use only the repo-native `scripts/pr` wrapper: run `scripts/pr review-init <PR>`, follow its emitted checkout/guard guidance, initialize and complete review artifacts with `scripts/pr review-artifacts-init <PR>`, validate them with `scripts/pr review-validate-artifacts <PR>`, then run `scripts/pr prepare-run <PR>` and `scripts/pr merge-run <PR>`; do not idle on `auto-response` or `check-docs`.
|
||||
|
||||
## Code
|
||||
|
||||
468
CHANGELOG.md
468
CHANGELOG.md
@@ -2,465 +2,6 @@
|
||||
|
||||
Docs: https://docs.openclaw.ai
|
||||
|
||||
## 2026.6.9
|
||||
|
||||
### Highlights
|
||||
|
||||
- **Richer Telegram delivery:** Telegram now sends rich HTML, preserves rich markdown and sticker paths, renders progress drafts and command output more faithfully, normalizes HTML tables safely, and keeps mentions and spooled handlers on the right delivery path. (#93286, #93164, #93124, #93364, #93130, #93002, #93088, #93281, #94891, #94856) Thanks @obviyus, @vincentkoc, @goutamadwant, @kesslerio, @NianJiuZst, @SweetSophia, @Marvinthebored, @aaajiao, @zhangguiping-xydt, @zhangqueping, and @jairrab.
|
||||
- **More dependable agent recovery:** retries, terminal outcomes, usage after compaction, session history repair, and reply reconciliation now keep more interrupted or partial turns moving toward a visible final result. (#92191, #93073, #93228, #93084, #93469, #93291, #90943) Thanks @ai-hpc, @lml2468, @fuller-stack-dev, @Hollychou924, @leno23, @de1tydev, @425072024, @wuwahe3, @drvoss, @yetval, @sandieman2, and @vincentkoc.
|
||||
- **A stronger Codex integration:** Codex gains automatic plugin approvals, GPT-5.3 Spark OAuth routing, remote-node `exec` as a dynamic tool, and more reliable app-server teardown and terminal outcomes. (#92625, #89133, #93654, #91767, #93287) Thanks @kevinslin, @VACInc, @vincentkoc, @JPKay-AI, and @aliahnaf2013-max.
|
||||
- **Standalone official provider plugins:** external provider packages are now first-class npm releases, externally installed channel plugins load at Gateway startup, and StepFun is available from npm and ClawHub. (#93470) Thanks @sunlit-deng, @cxdnicole, and @vincentkoc.
|
||||
- **More capable web and native clients:** the Control UI adds a session workspace rail and extension health, iOS adds Watch controls, and Android shows chat context. (#92856, #91952, #93387, #92837) Thanks @Solvely-Colin, @jalehman, @joshavant, and @Tosko4.
|
||||
- **More useful search and skills:** Codex Hosted Search is available, key-free search providers remain deliberate opt-ins, and ClawHub skill installs retain verified source provenance. (#93446, #93616, #93283, #93506) Thanks @fuller-stack-dev, @davemorin, @momothemage, @nmccready-tars, and @vincentkoc.
|
||||
|
||||
### Changes
|
||||
|
||||
- Providers and auth: add Codex Hosted Search, improve Gemini CLI OAuth behind proxies, and keep external provider onboarding on current choices and package metadata. (#93446, #92815) Thanks @fuller-stack-dev, @yetval, @EvetteYoung, and @vincentkoc.
|
||||
- Plugins and installs: externalized official providers publish as independent npm packages, Gateway discovers installed channel plugins at startup, and StepFun installs from npm or ClawHub. (#93470) Thanks @sunlit-deng, @cxdnicole, and @vincentkoc.
|
||||
- Dashboard and mobile: add a session workspace rail, plugin health in status, compact cron lists, and iOS Watch controls. (#92856, #91952, #93395, #93387) Thanks @Solvely-Colin, @jalehman, @yu-xin-c, @centralpc, @joshavant, and @vincentkoc.
|
||||
- Codex, observability, and skills: add automatic plugin approvals and SecretRefs, preserve ClawHub skill provenance, add OpenTelemetry log export, and expose remote-node execution to Codex when a node is connected. (#92625, #94324, #93283, #94561, #93654) Thanks @kevinslin, @kevinlin-openai, @momothemage, @nmccready-tars, @jesse-merhi, @vincentkoc, and @JPKay-AI.
|
||||
- QA and release engineering: QA scenarios now use YAML, with broader profile evidence and release coverage for the plugin and channel matrix. Thanks @vincentkoc.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Security and privacy: redact secrets from debug/config output, block internal HTTP session overrides, audit open-DM tool exposure, and retain plugin write ownership checks. (#93333, #88496, #93443, #92883, #93353) Thanks @Alix-007, @jason-allen-oneal, @coygeek, @RichardCao, @yu-xin-c, @cjg20ss, @eleqtrizit, and @vincentkoc.
|
||||
- Agent and session runtime: retry thinking-only and empty post-tool turns, prevent duplicate hook execution, preserve pending subagent delivery, preserve fresh usage through compaction, and repair partial JSON/history artifacts. (#92191, #93073, #93009, #93084, #93469, #94349, #92383, #94257) Thanks @ai-hpc, @lml2468, @fuller-stack-dev, @zenglingbiao, @dertbv, @Hollychou924, @leno23, @de1tydev, @425072024, @wuwahe3, @drvoss, @vincentkoc, @sallyom, @oiGaDio, @Hidetsugu55, and @Nas01010101.
|
||||
- Channels and replies: fix Telegram rich delivery, table rendering, action-error handling, progress draft cleanup before visible tool output, and ingress recovery; preserve command progress detail across channel adapters; retain WhatsApp opening text after a media failure; keep Mattermost thread replies intact; and harden Discord action handling. (#93286, #93364, #93281, #93002, #93076, #93334, #93424, #93488, #94868, #94891, #94856, #94810, #93823) Thanks @obviyus, @NianJiuZst, @mcaxtr, @zhangguiping-xydt, @rushindrasinha, @amknight, @lzyyzznl, @darealgege, @vincentkoc, @zhangqueping, @jairrab, @ZOOWH, @parveshsaini, and @yetval.
|
||||
- Storage and migrations: avoid SQLite WAL on network filesystems, clean reindex artifacts, keep setup state out of workspace dot-directories, and import default-agent auth profiles into SQLite. (#93454, #92891, #93182, #93295, #93520, #93156) Thanks @vincentkoc, @ZengWen-DT, @Zeng-wen, @potterdigital, @Alix-007, @Pick-cat, @sallyom, @1qh, and @Tazio7.
|
||||
- Provider and model behavior: fix Gemini CLI proxy OAuth, restore Codex Spark OAuth routing, correct Bedrock embedding model IDs, and preserve configured defaults in embedded runs. (#92815, #89133, #93452, #93428) Thanks @yetval, @EvetteYoung, @VACInc, @LiuwqGit, @aleck31, @zenglingbiao, @danielgerlag, and @vincentkoc.
|
||||
- CLI, TUI, and apps: accept global flags after subcommands, keep terminal output and activity indicators visible, preserve CJK IME composition, and refresh stale UI state. (#93455, #93460, #93006, #93427, #93498, #93606) Thanks @ooiuuii, @Alix-007, @ZengWen-DT, @Zeng-wen, @AlethiaQuizForge, @Zhaoqj2016, @liuhao1024, @BrianClaw1955, @vincentkoc, and @NicoBoom13.
|
||||
- Operations and updates: harden official plugin recovery, restart managed Gateways after failed update handoff, keep safe cron delivery defaults, avoid Node-specific npm prefixes, and keep package validation paths reliable. (#93325, #92111, #93650, #94453, #91685) Thanks @vincentkoc, @yetval, @ofan, @yaanfpv, @jincheng-xydt, @sallyom, @davectr, and @nxmxbbd.
|
||||
|
||||
### Complete contribution record
|
||||
|
||||
This audited record covers the complete v2026.6.8..HEAD history: 423 merged PRs. The generation manifest also supplies direct commits as editorial input; the grouped notes above prioritize user impact.
|
||||
|
||||
#### Pull requests
|
||||
|
||||
- **PR #90463** refactor: add session accessor seam with gateway consumer. Thanks @jalehman.
|
||||
- **PR #88656** Drop reasoning-only length turns from replay. Thanks @abel-zer0.
|
||||
- **PR #92856** feat(webui): add session workspace rail. Thanks @Solvely-Colin.
|
||||
- **PR #92845** docs(browser-control): document OPENCLAW_EAGER_BROWSER_CONTROL_SERVER requirement. Related #92841. Thanks @liuhao1024 and @jeugregg.
|
||||
- **PR #82366** fix: use passive periodic sqlite wal checkpoints. Related #81715. Thanks @honor2030 and @KrasimirKralev.
|
||||
- **PR #92815** fix(google): route Gemini CLI OAuth through the env proxy (#46184). Thanks @yetval and @EvetteYoung.
|
||||
- **PR #91331** fix(mattermost): merge progress preview lines by identity. Related #89761. Thanks @iloveleon19 and @leonthe8th and @vincentkoc.
|
||||
- **PR #92909** fix(tui): keep spinner active when toggling tools. Related #49763. Thanks @ZengWen-DT and @Zeng-wen and @vincentkoc and @CrimsonDump.
|
||||
- **PR #92904** fix(elevenlabs): use current TTS model ids. Thanks @vortexopenclaw and @vincentkoc.
|
||||
- **PR #92642** fix #86872: Subagent run reports success but fails to write output file. Thanks @zhangguiping-xydt and @vincentkoc and @zapper35.
|
||||
- **PR #89122** refactor: route command session reads through seam. Thanks @jalehman.
|
||||
- **PR #90943** fix(reply): deliver final reply when queued follow-up claims session; scope dedupe to routed thread. Thanks @sandieman2 and @vincentkoc.
|
||||
- **PR #92894** fix(skills): keep managed prompt paths readable. Related #92875. Thanks @kesslerio and @sallyom.
|
||||
- **PR #39617** fix: reload config in slash command routing so dmScope is respected. Related #39605. Thanks @Ciward.
|
||||
- **PR #92191** fix(agents): retry thinking-only errored turns. Related #91953. Thanks @ai-hpc and @lml2468.
|
||||
- **PR #92891** fix(memory): clean stale reindex temp files. Related #92874. Thanks @ZengWen-DT and @Zeng-wen and @vincentkoc and @potterdigital.
|
||||
- **PR #93005** Add OpenRouter Fusion guidance and prompt context. Related #92984. Thanks @sallyom.
|
||||
- **PR #88792** fix(state): harden sqlite path caching. Thanks @vincentkoc.
|
||||
- **PR #93022** fix(gateway): repair usage cost aggregation across agents. Thanks @luke-skywalker-open-claw and @stablegenius49.
|
||||
- **PR #93020** fix(telegram): cool down transient sendChatAction failures. Related #56096. Thanks @Boulea7 and @sumaiazaman and @Pick-cat and @cal-rufus.
|
||||
- **PR #93002** fix(telegram): clear progress drafts before visible tool output. Thanks @zhangguiping-xydt.
|
||||
- **PR #89160** fix(agents): detect truncated API responses to prevent silent session hang. Related #89051. Thanks @joelnishanth and @ArthurusDent.
|
||||
- **PR #93009** fix(agents): make wrapToolWithBeforeToolCallHook idempotent to prevent double hook execution (fixes #92973). Thanks @zenglingbiao and @dertbv.
|
||||
- **PR #92991** fix(agents): tolerate missing attribution baseUrl. Related #92974. Thanks @samrusani and @Haderach-Ram.
|
||||
- **PR #92913** fix(opencode-go): register model catalog to fix context window detection. Related #92912. Thanks @kumaxs.
|
||||
- **PR #89129** refactor: route bundled plugin session callers through seam. Thanks @jalehman.
|
||||
- **PR #93084** fix(agents): preserve fresh usage after compaction. Related #50795. Thanks @Hollychou924 and @leno23 and @de1tydev and @425072024 and @vincentkoc and @wuwahe3.
|
||||
- **PR #92869** fix #90333: [Bug]: Discord image build aborts at step 66 — openclaw-build-messaging-plugins.py exits 1. Thanks @zhangguiping-xydt and @vincentkoc and @chriskosys.
|
||||
- **PR #93011** fix(gateway): accept file-only input on /v1/responses (parity with image-only). Thanks @yetval and @vincentkoc.
|
||||
- **PR #92915** Convert QA scenarios to YAML files. Thanks @RomneyDa.
|
||||
- **PR #91767** Fix one-shot Codex app-server teardown. Thanks @aliahnaf2013-max.
|
||||
- **PR #92625** feat(codex): add auto plugin approvals. Thanks @kevinslin.
|
||||
- **PR #91587** test(qa): add qa run --qa-profile and unified output summary/evidence. Thanks @RomneyDa.
|
||||
- **PR #93104** test(reply): seed channel fixtures for dedupe tests. Thanks @RomneyDa.
|
||||
- **PR #93107** test(reply): preserve telegram dedupe fallback. Thanks @RomneyDa.
|
||||
- **PR #92954** fix(memory): accept local default model path migration. Thanks @mushuiyu886 and @vincentkoc.
|
||||
- **PR #90936** fix(agents): do not misclassify client-disconnect abort as run timeout. Related #90764. Thanks @openperf and @reginaldomarcilon.
|
||||
- **PR #90812** fix(voice-call): preserve live Twilio streams in stale reaper. Related #79121. Thanks @Takhoffman and @sahibzada-allahyar and @donkeykong91.
|
||||
- **PR #93094** fix(whatsapp): bound socket operations. Thanks @mcaxtr.
|
||||
- **PR #91629** fix(scripts): add database-first legacy store guard. Related #91628. Thanks @galiniliev.
|
||||
- **PR #93124** fix(telegram): render progress drafts as rich previews. Thanks @Marvinthebored.
|
||||
- **PR #93109** test(qa): embed profile scorecard evidence. Thanks @RomneyDa.
|
||||
- **PR #87298** test: add temp directory helper guidance. Thanks @hxy91819.
|
||||
- **PR #92318** fix(cron): require explicit message target proof. Thanks @hxy91819.
|
||||
- **PR #93137** fix(imessage): honor disabled reply actions. Related #92142. Thanks @omarshahine and @dprev.
|
||||
- **PR #93134** fix(feishu): pass card_msg_content_type to get full card content (fixes #78289). Thanks @liuhao1024 and @vincentkoc and @longdoubled7.
|
||||
- **PR #93138** fix(agents): preserve literal current session resolution. Thanks @liuhao1024 and @vincentkoc.
|
||||
- **PR #91225** fix #83830: [Bug]: Dreaming diary repeats "first day" narrative every sweep — same early memories dominate snippets. Thanks @mushuiyu886 and @YinLiuLiu66.
|
||||
- **PR #93153** simplify QA evidence profile and mappings/coverage shape. Thanks @RomneyDa.
|
||||
- **PR #93164** fix(telegram): preserve rich markdown line breaks. Thanks @vincentkoc.
|
||||
- **PR #93119** fix: accept mixed source/dist bundled roots. Related #87730. Thanks @arkyu2077 and @vincentkoc and @jasonftl.
|
||||
- **PR #93130** fix(telegram): preserve sticker media paths. Related #83748. Thanks @goutamadwant and @vincentkoc and @aaajiao.
|
||||
- **PR #93073** fix(agents): retry empty post-tool final turns. Thanks @fuller-stack-dev.
|
||||
- **PR #91784** fix(voice-call): require realtime websocket path boundary. Thanks @jason-allen-oneal.
|
||||
- **PR #89133** Restore GPT-5.3 Codex Spark OAuth routing. Thanks @VACInc.
|
||||
- **PR #91996** refactor: prune unused iOS code. Thanks @zats.
|
||||
- **PR #90231** fix #69443: [Bug] Subagent RPC callback to WeChat session key routed to main session instead. Thanks @zhangguiping-xydt and @sliverp and @chen11221.
|
||||
- **PR #89920** fix(matrix): replace recovered command progress lines. Thanks @bdjben and @jesse-merhi.
|
||||
- **PR #93159** fix(tui): keep parent stdin paused after exit. Thanks @fuller-stack-dev.
|
||||
- **PR #93201** fix(auto-reply): clear pending-final state before honoring post-send abort (#89115). Thanks @amknight and @danashburn.
|
||||
- **PR #93228** fix(agents): replace prose terminal classifiers. Thanks @fuller-stack-dev.
|
||||
- **PR #93231** fix(status): correct pinned model clear hint. Thanks @hxy91819.
|
||||
- **PR #92428** fix(qqbot): keep markdown table chunks valid. Thanks @sliverp.
|
||||
- **PR #93220** fix(status): avoid stale session context windows. Thanks @hxy91819.
|
||||
- **PR #91957** perf(sessions): share one enumeration across archive retention sweeps. Thanks @amknight.
|
||||
- **PR #93281** fix(telegram): recover pid-reused ingress claims. Thanks @obviyus.
|
||||
- **PR #93287** fix(codex): preserve terminal outcome ordering.
|
||||
- **PR #93182** fix(memory): clean rollback-journal reindex temp sidecar on NFS stores. Thanks @Alix-007.
|
||||
- **PR #93283** Persist ClawHub skill install provenance. Related #92077. Thanks @momothemage and @nmccready-tars.
|
||||
- **PR #88872** fix: attribute spawned task runs to child agent. Related #66670. Thanks @Alix-007 and @Neomail2.
|
||||
- **PR #92837** fix(android): show live chat context usage. Thanks @Tosko4.
|
||||
- **PR #93325** fix(cli): harden official plugin recovery. Thanks @vincentkoc.
|
||||
- **PR #93286** feat(telegram): send rich messages as rich html. Thanks @obviyus.
|
||||
- **PR #92910** fix(memory-core): safely refresh qmd index during collection repair.
|
||||
- **PR #93329** fix(cli): allow zero Discord timeout duration. Related #93327. Thanks @rohitjavvadi.
|
||||
- **PR #91625** fix(cron): add cron edit --clear-model to clear a job's model override. Thanks @ly-wang19.
|
||||
- **PR #91691** [AI] fix(memory): prevent empty-string expectedModel in resolveMemory…. Thanks @xydt-tanshanshan.
|
||||
- **PR #93006** fix(tui): keep stderr visible when local shell stdout fills the output cap. Thanks @Alix-007.
|
||||
- **PR #93001** fix(daemon): prefer stderr over stale stdout in gateway restart diagnostics. Thanks @Alix-007.
|
||||
- **PR #91117** refactor: remove dead code and improve string concatenation. Thanks @Pommelle.
|
||||
- **PR #90893** fix(models): mask paste-token input in CLI auth prompt. Thanks @anurag-bg-neu.
|
||||
- **PR #90571** fix(configure): mask gateway password input in CLI wizard prompt. Thanks @anurag-bg-neu.
|
||||
- **PR #91768** fix(ios): respect chat header safe area. Thanks @zats.
|
||||
- **PR #93245** fix(cron): resolve lastRunStatus in cron list/show human output. Thanks @ly-wang19.
|
||||
- **PR #78765** fix(tui): avoid inserting spaces into long CJK text. Thanks @hpt.
|
||||
- **PR #91776** fix(ios): refresh permission rows after grants. Thanks @zats.
|
||||
- **PR #92817** fix(cron): trust agent output when channel is unresolved without explicit delivery. Related #90664. Thanks @fsdwen and @dertbv.
|
||||
- **PR #93297** fix(control-ui): respect agents.defaults.timeFormat for timestamps. Related #58147. Thanks @ZengWen-DT and @Zeng-wen and @TommoT2.
|
||||
- **PR #93364** Fix Telegram rich progress command output. Thanks @obviyus.
|
||||
- **PR #91952** feat(status): surface plugin health. Thanks @jalehman.
|
||||
- **PR #75025** fix(heartbeat): refresh stale Current time line on every helper call (#44993). Thanks @MoerAI and @mclee1975.
|
||||
- **PR #90992** docs(windows): fix WSL gateway-autostart recipe for WSL ≥ 2.6.1.0 idle-termination. Thanks @spencer2211.
|
||||
- **PR #86544** fix(cli): show Gemini CLI runtime auth status. Related #79585. Thanks @giodl73-repo and @fabricefoy.
|
||||
- **PR #88945** fix(plugins): serialize binding approval saves. Related #64065. Thanks @Alix-007 and @lihaokun.
|
||||
- **PR #90115** fix(gateway): pass managed inbound PDFs through chat.send. Related #90097. Thanks @harjothkhara and @joeykrug.
|
||||
- **PR #74613** docs(cli): add agent selector to CLI backend quick start. Related #68940. Thanks @vyctorbrzezowski and @drmarcopapa.
|
||||
- **PR #89121** refactor: add transcript reader seam. Thanks @jalehman.
|
||||
- **PR #84434** fix(cli): disable ScheduleWakeup/CronCreate in --print claude runs. Thanks @SkyWolfDreamer.
|
||||
- **PR #66985** fix(agents): resolve requestedNode to canonical ID before boundNode comparison. Related #87213. Thanks @mujiannan.
|
||||
- **PR #91488** fix(reply): project preflight compaction gate by next-input size on fresh tokens. Thanks @yetval.
|
||||
- **PR #93353** fix(plugins): require owner for plugin writes. Thanks @eleqtrizit.
|
||||
- **PR #91499** fix(cron): preserve scheduled turn tool policy [AI]. Thanks @mmaps.
|
||||
- **PR #90412** fix(sessions): cache warm transcript reads to avoid per-turn re-parse. Related #83943. Thanks @Alix-007 and @yyds-xxxx.
|
||||
- **PR #93118** fix(gateway): guard fast-path startup migrations. Related #93032. Thanks @openperf and @Haderach-Ram.
|
||||
- **PR #93355** fix(ci): verify performance workflow downloads. Thanks @eleqtrizit.
|
||||
- **PR #93358** fix(outbound): guard cross-context message mutations. Thanks @eleqtrizit.
|
||||
- **PR #93362** fix(flock): bind allow-always to wrapped command. Thanks @eleqtrizit.
|
||||
- **PR #92578** refactor(whatsapp): add inbound admission foundation. Thanks @mcaxtr.
|
||||
- **PR #89547** Control Telegram group history context. Thanks @mmaps.
|
||||
- **PR #89201** refactor: add transcript runtime identity contract. Thanks @jalehman.
|
||||
- **PR #93357** fix(plugins): enforce install policy in wrappers. Thanks @eleqtrizit.
|
||||
- **PR #93156** fix(doctor): import default-agent auth profiles into sqlite. Related #93145. Thanks @Pick-cat and @sallyom and @Tazio7.
|
||||
- **PR #93179** Add slim evidence mode for QA profile evidence. Thanks @RomneyDa.
|
||||
- **PR #93349** fix(control-ui): keep workboard card titles visible in overflowing columns (fixes #91717). Thanks @Pick-cat and @NicoBoom13.
|
||||
- **PR #93324** fix(cli): accept --no-color after subcommands. Thanks @ooiuuii.
|
||||
- **PR #89621** Return Google Chat thread metadata from message sends. Thanks @franco-viotti.
|
||||
- **PR #82458** fix(infra): drop duplicated "restart" word in restart-sentinel summary. Thanks @jameswniu.
|
||||
- **PR #85471** Suppress cron announce control replies. Related #85421. Thanks @TurboTheTurtle and @leatherneck-33.
|
||||
- **PR #85316** fix(auth): keep alias-compatible auth-profile overrides instead of clearing them. Thanks @SkyWolfDreamer.
|
||||
- **PR #89260** fix(doctor): separate platform-incompatible skills from missing requirements. Related #89232. Thanks @Alix-007 and @CameronWeller.
|
||||
- **PR #90846** fix(media): stop pruning media on write; let the configured timer do it. Thanks @lundog.
|
||||
- **PR #88062** fix(logging): avoid stalled warnings for active model calls. Thanks @litang9.
|
||||
- **PR #93308** fix(discord): reject malformed realtime consult calls. Thanks @khoek.
|
||||
- **PR #93334** fix(whatsapp): notify user when trailing media send fails instead of silent drop. Thanks @rushindrasinha.
|
||||
- **PR #92575** fix(sessions): preserve user behavior overrides across daily/idle rollover (#92562) [AI-assisted]. Thanks @harjothkhara and @civiltox.
|
||||
- **PR #89124** refactor: route auto-reply sessions through session seam. Thanks @jalehman.
|
||||
- **PR #93431** fix: stabilize transcript cache and CLI env isolation. Thanks @shakkernerd.
|
||||
- **PR #93412** fix(discord): suppress tool progress for message-tool replies. Thanks @mgunnin and @vincentkoc.
|
||||
- **PR #93409** fix(whatsapp): stop markdownToWhatsApp dropping code spans followed by a digit. Thanks @rushindrasinha.
|
||||
- **PR #93295** fix(memory): swap rollback-journal sidecar during atomic reindex. Thanks @Alix-007.
|
||||
- **PR #93076** fix(whatsapp): preserve auth on terminal disconnects. Thanks @mcaxtr.
|
||||
- **PR #93435** fix(agents): bound autoreview scope. Thanks @vincentkoc.
|
||||
- **PR #93279** fix(telegram): restore readable default text sends. Related #93263. Thanks @NianJiuZst and @SweetSophia.
|
||||
- **PR #93429** fix(line): cap carousel column text at 60 chars when a title or image is set. Thanks @harjothkhara and @vincentkoc.
|
||||
- **PR #93428** fix(agents): resolve configured default model in runEmbeddedAgent (fixes #93419). Thanks @zenglingbiao and @vincentkoc and @danielgerlag.
|
||||
- **PR #93427** fix(tui): show activity indicator for system-injected runs. Related #51825. Thanks @ZengWen-DT and @vincentkoc and @Zeng-wen and @AlethiaQuizForge.
|
||||
- **PR #90003** feat(policy): cover exec approvals artifact. Thanks @giodl73-repo.
|
||||
- **PR #93448** fix(guards): allow auth profile sqlite reader. Thanks @amknight.
|
||||
- **PR #93424** fix(mattermost): keep message tool replies in threads. Thanks @amknight and @vincentkoc.
|
||||
- **PR #93418** fix(telegram): forward Bot API 10.1 rich_message content to agent. Related #93410. Thanks @xzh-icenter and @vincentkoc and @0pen7ech.
|
||||
- **PR #93175** test(qa): taxonomy profiles: includeAllCategories for release profile, update some coverage. Thanks @RomneyDa.
|
||||
- **PR #93456** fix(agents): handle string assistant message content. Thanks @vincentkoc.
|
||||
- **PR #93441** fix(outbound): ignore schema-padded poll metadata on send. Related #43015. Thanks @weichengdeng and @charzhou.
|
||||
- **PR #93443** fix(gateway): block internal HTTP session overrides. Thanks @RichardCao.
|
||||
- **PR #93454** fix(sqlite): disable WAL on network filesystems. Thanks @vincentkoc.
|
||||
- **PR #90275** test: make install-safe-path symlink tests compatible with Windows. Thanks @aniruddhaadak80.
|
||||
- **PR #93464** fix(qa): suppress empty WhatsApp debug artifacts. Thanks @vincentkoc.
|
||||
- **PR #90861** fix(cli): preserve sessions_yield over MCP. Related #77426. Thanks @zhangguiping-xydt and @jarvisagimuspicard-hub.
|
||||
- **PR #90946** fix(infra): preserve inherited gateway PID across reparent during cleanup. Thanks @amittell.
|
||||
- **PR #92220** fix(media): extract large managed inbound PDFs via media-understanding. Related #90096, #90097. Thanks @amknight and @joeykrug.
|
||||
- **PR #91208** fix #91047: Plugin session-extension registry not pinned; sessions.pluginPatch fails after agent/subagent plugin-load churn. Thanks @mushuiyu886 and @teamadams.
|
||||
- **PR #92111** fix(update): restart managed gateway when update handoff fails after stop. Related #92088. Thanks @yetval and @ofan.
|
||||
- **PR #93238** fix(agents): honor disabled envelope timestamps at model boundary. Thanks @osolmaz.
|
||||
- **PR #93343** fix(codex): de-duplicate commentary notes across the raw response lane. Related #93296. Thanks @Marvinthebored and @Peetiegonzalez.
|
||||
- **PR #93361** fix(openshell): pin mirror remote mutations. Thanks @eleqtrizit.
|
||||
- **PR #93354** fix(discord): block cross-provider guild admin actions. Thanks @eleqtrizit.
|
||||
- **PR #92178** fix(gateway): normalize malformed paired access lists. Related #90654. Thanks @wangmiao0668000666 and @EmilioNicolas.
|
||||
- **PR #85254** perf(plugins): thread prepared manifestPlugins through runtime model-id normalize chain. Thanks @zeroaltitude.
|
||||
- **PR #93489** Add ClawHub content rights docs to sidebar. Thanks @Patrick-Erichsen.
|
||||
- **PR #93466** [AI] fix(feishu): guard against missing inbound in channelRuntime fallback. Thanks @xydt-tanshanshan.
|
||||
- **PR #93460** fix(cli): honor --log-level in route-first commands. Related #93457. Thanks @ooiuuii.
|
||||
- **PR #93495** fix(cron): clear delivery routing fields from cron edit. Thanks @ly-wang19 and @vincentkoc.
|
||||
- **PR #93494** docs: point PR landing at maintainer workflow. Thanks @fuller-stack-dev and @vincentkoc.
|
||||
- **PR #93487** fix(ui): add agent selector to skills page. Related #78553. Thanks @goutamadwant and @vincentkoc and @xiaobu1112.
|
||||
- **PR #93488** fix(discord): apply tool status emojis immediately to avoid override by thinking reactions. Related #92715. Thanks @lzyyzznl and @vincentkoc and @darealgege.
|
||||
- **PR #93055** fix(ui): restore provider usage pill in desktop chat composer [AI]. Thanks @harjothkhara.
|
||||
- **PR #83156** fix(matrix): accept bracketed display-name mentions. Related #83142. Thanks @wdx-agent-io and @wdongxv.
|
||||
- **PR #93333** fix(auto-reply): redact secrets in /debug show and /debug set output. Thanks @Alix-007.
|
||||
- **PR #88496** fix(auto-reply): redact secrets in config show output. Related #65623. Thanks @jason-allen-oneal and @coygeek.
|
||||
- **PR #93105** fix(doctor): repair null agents.list[].workspace values. Related #77718. Thanks @xydigit-sj and @slideshow-dingo.
|
||||
- **PR #73923** fix(ui): preserve gateway token during safe websocket url edits. Related #41545. Thanks @wsyjh8.
|
||||
- **PR #88970** fix #85871: [Bug]: Heartbeat scheduler silently fails to fire on 5.20 and all 5.x versions (regression from 4.23). Thanks @zhangguiping-xydt and @vincentkoc and @carlbjson.
|
||||
- **PR #93511** fix(imessage): normalize leading NUL echo-cache prefixes. Thanks @vincentkoc and @drvoss.
|
||||
- **PR #92594** [Bug]: ollama-cloud runtime fails DNS lookup for ai.ollama.com, while ollama/<model>:cloud works. Related #92391. Thanks @zhangguiping-xydt and @vincentkoc and @kvzsolt.
|
||||
- **PR #93512** build(docs): finish PowerShell-safe docs formatting. Related #44293. Thanks @vincentkoc and @yil337 and @aniruddhaadak80.
|
||||
- **PR #93513** fix(skills): refresh persisted snapshots after restart. Thanks @vincentkoc and @fif911 and @skadauke.
|
||||
- **PR #93517** fix(skills): quote skill-creator template description. Thanks @vincentkoc and @parubets.
|
||||
- **PR #73976** fix(memory): use per-keyword FTS search in hybrid mode #39484. Thanks @joshuakeithpa-sudo.
|
||||
- **PR #93520** fix(workspace): store setup state outside workspace dot-dir. Thanks @vincentkoc and @1qh.
|
||||
- **PR #93521** fix(onboard): skip Homebrew prompt on unsupported platforms. Related #68893. Thanks @vincentkoc and @yurivict.
|
||||
- **PR #93522** fix(feishu): send post mentions as native at elements. Thanks @vincentkoc and @gavin-ali and @YizukiAme and @Panniantong.
|
||||
- **PR #93496** fix(gateway): rotate already-stale generated transcript filename on /reset. Thanks @harjothkhara and @vincentkoc.
|
||||
- **PR #93471** fix(cron): preserve aborted isolated-run failure. Thanks @BhargavSatya and @vincentkoc.
|
||||
- **PR #93473** fix(memory): report skipped QMD embedding probe. Related #77645. Thanks @TurboTheTurtle and @vincentkoc and @aderius.
|
||||
- **PR #93498** fix(ui): preserve CJK IME composition. Related #86035. Thanks @Zhaoqj2016 and @vincentkoc.
|
||||
- **PR #93088** fix(telegram): bind bot mentions to assistant identity. Thanks @kesslerio and @vincentkoc.
|
||||
- **PR #93499** fix(nodes): return screen snapshots as media. Related #90126. Thanks @zenglingbiao and @vincentkoc and @JeffSteinbok.
|
||||
- **PR #93506** fix(skills): trust verified ClawHub source provenance. Thanks @vincentkoc.
|
||||
- **PR #93525** agents: notify chat exec empty-success completions. Thanks @vincentkoc and @wenkang-xie.
|
||||
- **PR #93446** feat: add Codex hosted web search. Thanks @fuller-stack-dev.
|
||||
- **PR #92883** fix(security): audit open dm tool exposure. Related #55612. Thanks @yu-xin-c and @vincentkoc and @cjg20ss.
|
||||
- **PR #93476** fix(mattermost): preserve Codex progress preview. Related #88766. Thanks @goutamadwant and @vincentkoc and @KelTech-Services.
|
||||
- **PR #93395** feat(cron): add compact list responses. Related #93366. Thanks @yu-xin-c and @vincentkoc and @centralpc.
|
||||
- **PR #93527** fix(cron): preserve model overrides for text payloads. Thanks @vincentkoc and @liaoandi.
|
||||
- **PR #90487** fix: harden ChatGPT Responses missing content-type streams. Thanks @anyech and @vincentkoc.
|
||||
- **PR #93528** fix(gateway): tolerate transient pre-hello clean closes. Thanks @vincentkoc and @ruanrrn.
|
||||
- **PR #93529** fix(auto-reply): allow message tool for group attachments. Related #43146. Thanks @vincentkoc and @Robcis.
|
||||
- **PR #93291** fix(reply): preserve pending thread evidence when reconciling partial send results. Thanks @yetval and @vincentkoc.
|
||||
- **PR #90572** fix(feishu): drop self-authored receive echoes. Thanks @baskduf.
|
||||
- **PR #93455** fix(cli): accept --log-level after subcommands. Thanks @ooiuuii and @vincentkoc.
|
||||
- **PR #93452** fix(bedrock): strip inference profile prefix from model ID in embedding adapter. Related #79212. Thanks @LiuwqGit and @vincentkoc and @aleck31.
|
||||
- **PR #89799** fix(cli): skip compile cache on early Node 24.x to avoid startup deadlock. Related #86550. Thanks @zhangguiping-xydt and @vincentkoc and @renyuliang000.
|
||||
- **PR #93469** fix(agents): drop partialJson streaming artifacts from session history repair. Thanks @drvoss and @vincentkoc.
|
||||
- **PR #93463** fix(codex): log app-server compaction completion. Related #83932. Thanks @goutamadwant and @vincentkoc and @aounakram.
|
||||
- **PR #93562** fix(tui): refresh after external session reset. Related #38966. Thanks @vincentkoc and @wsyjh8 and @yizhanzjz.
|
||||
- **PR #93470** fix(plugins): load externally-installed channel plugins at gateway startup. Related #93219. Thanks @sunlit-deng and @vincentkoc and @cxdnicole.
|
||||
- **PR #88796** fix(discord): resolve guildId from session channel for search actions. Related #88790. Thanks @SebTardif and @vincentkoc and @mugabuga.
|
||||
- **PR #93194** fix(agents): preserve prompt-released session metadata. Related #93193. Thanks @snowzlm.
|
||||
- **PR #89483** fix(gateway): project failed agent turns in chat history. Related #89197. Thanks @IWhatsskill and @vincentkoc and @yangiit.
|
||||
- **PR #93434** fix: avoid parent group allowlist false positive. Related #92684. Thanks @kingrubic and @vincentkoc and @motteman.
|
||||
- **PR #93449** fix(feishu): dedupe redelivered text by stable retry identity. Related #46778. Thanks @ZengWen-DT and @vincentkoc and @kingcuty.
|
||||
- **PR #93407** AGT-80 AGT-81 Fix Discord ingress ack ordering. Thanks @mgunnin and @vincentkoc.
|
||||
- **PR #93439** fix(agents): honor embedded run default model. Related #93419. Thanks @harjothkhara and @vincentkoc and @danielgerlag.
|
||||
- **PR #93565** fix(cli): summarize cleanup dry-run by label. Related #76826. Thanks @AgentArcLab and @vincentkoc and @renatomaluhy.
|
||||
- **PR #93509** fix(skills): clear orphaned idempotency pointer on corrupt-metadata re-begin. Thanks @Alix-007 and @vincentkoc.
|
||||
- **PR #93274** Clarify plugin channel config additional-property errors. Thanks @zhangguiping-xydt and @vincentkoc.
|
||||
- **PR #93555** fix(read): route text decoding through shared Windows codepage fallba…. Thanks @zhanxingxin1998 and @vincentkoc.
|
||||
- **PR #93314** fix(skills): preserve ClawHub origin provenance on readback. Thanks @Alix-007 and @vincentkoc.
|
||||
- **PR #93573** fix(acp): keep bridge sessions out of stale ACP classification [AI-assisted]. Related #38907. Thanks @eldar702 and @vincentkoc and @ninaopenclaw.
|
||||
- **PR #93398** fix(cron): emit isolated model usage diagnostics. Related #92338. Thanks @849261680 and @vincentkoc and @niks999.
|
||||
- **PR #93367** Fix SSH sandbox remote directory args. Related #93344. Thanks @dmorn and @vincentkoc.
|
||||
- **PR #93574** fix(feishu): suppress log noise for bot_p2p_chat_entered_v1 event [AI-assisted]. Related #42351. Thanks @eldar702 and @vincentkoc and @sunking0223.
|
||||
- **PR #93269** Fix tokenjuice bash results without details. Thanks @moeedahmed and @vincentkoc.
|
||||
- **PR #93575** fix(telegram): hydrate group reply-chain media into model context [AI-assisted]. Thanks @eldar702 and @vincentkoc.
|
||||
- **PR #93261** fix(plugins): resolve provider policy surface for plugin-owned CLI backends. Related #93259. Thanks @BitmapAsset and @vincentkoc.
|
||||
- **PR #93303** fix(whatsapp): bound stalled read-receipt socket operations. Thanks @Alix-007 and @vincentkoc.
|
||||
- **PR #93242** fix(mattermost): keep bare @mention with empty body instead of dropping it. Related #93205. Thanks @iloveleon19 and @vincentkoc.
|
||||
- **PR #93606** fix(ui): clear stale Talk error when session transitions to non-error state (fixes #88176). Thanks @liuhao1024 and @vincentkoc and @BrianClaw1955.
|
||||
- **PR #93607** perf(tasks): memoize reconcileInspectableTasks for same-tick calls (fixes #73531). Thanks @liuhao1024 and @vincentkoc and @slideshow-dingo.
|
||||
- **PR #93612** fix(gateway): compute sessions.usage aggregate totals from all sessions, not just the limited page (fixes #76496). Thanks @liuhao1024 and @vincentkoc and @bobsahur-robot.
|
||||
- **PR #93615** fix(telegram): recover lone active spooled handler on timeout (#84158). Thanks @0xghost42 and @vincentkoc and @crash2kx.
|
||||
- **PR #93616** Keep key-free web search providers opt-in. Thanks @davemorin and @vincentkoc.
|
||||
- **PR #93298** fix #93044: control-ui webchat double-renders agent replies when dmScope=main. Thanks @zhangguiping-xydt and @vincentkoc and @cfmilam.
|
||||
- **PR #93618** fix(feishu): filter temporary card-action-c-\* IDs from reply target to prevent Invalid open_message_id errors (fixes #56818). Thanks @liuhao1024 and @vincentkoc and @SwordImmortal.
|
||||
- **PR #93387** feat(ios): add watch action surface. Thanks @Solvely-Colin and @joshavant.
|
||||
- **PR #93648** fix(doctor): archive superseded plugin install index conflicts. Related #90418. Thanks @vincentkoc and @ramitrkar-hash.
|
||||
- **PR #93649** fix(qwen): place DashScope image prompts in user content. Related #92688. Thanks @vincentkoc and @Yachiyo404.
|
||||
- **PR #93650** fix(update): avoid per-Node npm prefixes during self-update. Related #80387. Thanks @vincentkoc and @yaanfpv.
|
||||
- **PR #93653** fix(skill-workshop): skip helper sessions during auto-capture. Thanks @vincentkoc and @zhangguiping-xydt.
|
||||
- **PR #93654** fix(codex): expose remote node exec as a Codex dynamic tool. Related #92141. Thanks @vincentkoc and @JPKay-AI.
|
||||
- **PR #93662** fix(discord): protect mention aliases in code fences. Thanks @vincentkoc and @rohitjavvadi.
|
||||
- **PR #93663** fix(clawdock): open dashboard on published port without starting deps. Related #77344. Thanks @vincentkoc and @dhoman.
|
||||
- **PR #93670** fix(browser): recover stale managed Chrome CDP listener. Related #41750. Thanks @vincentkoc and @rohitjavvadi and @kissman911.
|
||||
- **PR #93672** fix(commands): preserve multiline slash skill args. Related #79155. Thanks @vincentkoc and @web3blind.
|
||||
- **PR #93674** fix(browser): accept top-level act fields with nested requests. Related #38762. Thanks @vincentkoc and @angelusbr and @Lumos-789.
|
||||
- **PR #93678** fix(plugins): allow Dreaming sidecar through restrictive memory allowlists. Related #92536. Thanks @vincentkoc and @pradeep7127 and @resYuto.
|
||||
- **PR #93306** fix(status): ignore stale context after model switch. Thanks @hxy91819.
|
||||
- **PR #93666** fix(control-ui): copy code blocks over plain HTTP via clipboard fallback. Related #93628. Thanks @Pick-cat and @pjq2926.
|
||||
- **PR #93629** fix(reply): preserve unsent text-only finals after block pipeline streamed partial content (fixes #81078). Thanks @liuhao1024 and @Jackten.
|
||||
- **PR #93690** fix(telegram): dispatch MEDIA directives as attachments. Related #77702. Thanks @vincentkoc and @butttersbot.
|
||||
- **PR #93693** fix(gateway): ignore stale sudo scope for root user services. Related #81410. Thanks @vincentkoc and @Ericksza.
|
||||
- **PR #93646** fix(agents): return string assistant content in getLastAssistantText. Thanks @Alix-007 and @vincentkoc.
|
||||
- **PR #93687** fix(i18n): retain Codex error tails in logs. Thanks @hxy91819.
|
||||
- **PR #93630** fix(heartbeat): bootstrap plugin session targets. Thanks @ZengWen-DT and @vincentkoc.
|
||||
- **PR #93658** fix(wizard): preserve existing default model during setup auth choice [AI-assisted]. Related #64129. Thanks @ml12580 and @vegapunk9527.
|
||||
- **PR #93671** fix(respawn): rewrite pnpm versioned entry paths to stable wrapper (fixes #52313). Thanks @liuhao1024 and @vincentkoc and @RichardCao.
|
||||
- **PR #93698** Fix Telegram rich progress detail updates. Thanks @obviyus.
|
||||
- **PR #93656** fix(gateway): send approval route notices with write scope. Related #93563. Thanks @mushuiyu886 and @vincentkoc and @clawbot247-commits.
|
||||
- **PR #93665** fix(gateway): surface codex app-server returned failures. Thanks @litang9 and @vincentkoc.
|
||||
- **PR #93727** fix(context-engine): avoid turn-maintenance lane livelock. Related #77340. Thanks @vincentkoc and @baghvn and @Veda-openclaw.
|
||||
- **PR #93681** fix(llm): handle string assistant content on the OpenAI-compatible completion path. Thanks @Alix-007.
|
||||
- **PR #93722** chore(release): update appcast for 2026.6.8. Thanks @vincentkoc.
|
||||
- **PR #93677** fix(google-meet): declare realtime provider secret inputs. Related #81891. Thanks @goutamadwant and @vincentkoc and @chachi-max.
|
||||
- **PR #92947** fix(qqbot): deliver cron auto-TTS voice by trusting OpenClaw temp root. Related #92816. Thanks @ZengWen-DT and @Zeng-wen and @lewiswu1209.
|
||||
- **PR #93679** fix(whatsapp): extract GIF metadata and distinguish gifPlayback in media placeholders (fixes #49099). Thanks @liuhao1024 and @vincentkoc and @bugkill3r.
|
||||
- **PR #93688** fix(minimax): check base_resp envelope errors in TTS provider. Related #76904. Thanks @dwc1997 and @najef1979-code.
|
||||
- **PR #93714** fix: isolate async model resolution mock from sync mock in flaky test. Related #92117. Thanks @lsr911 and @wangwllu.
|
||||
- **PR #93705** test(macos): cover root command dispatch. Related #83879. Thanks @markoub and @vincentkoc and @davinci282828.
|
||||
- **PR #93711** Keep command text in progress drafts. Thanks @keshavbotagent and @vincentkoc.
|
||||
- **PR #93712** fix: scope assistant avatar override to agent ID. Related #90890. Thanks @lsr911 and @vincentkoc and @najef1979-code.
|
||||
- **PR #93725** fix(usage): prune stale usage cache temp files. Related #78939. Thanks @markoub and @Tramsrepus.
|
||||
- **PR #93726** fix(typing): start typing on reasoning deltas in thinking mode before visible text. Related #79681. Thanks @xialonglee and @novaflash82.
|
||||
- **PR #93716** fix(discord): propagate timeout through channel capabilities diagnostics. Related #77040. Thanks @xialonglee and @vincentkoc and @unicebondoc.
|
||||
- **PR #93729** fix(ollama): preserve configured API during discovery. Related #93710. Thanks @zhangguiping-xydt and @vincentkoc and @obnoxious2011-cmd.
|
||||
- **PR #93719** fix: pin plugin workspace dir for sessions.list to avoid O(rows) memo busting. Related #90814. Thanks @lsr911 and @vincentkoc and @k-l-lambda.
|
||||
- **PR #93732** fix(agents): preserve re-sent user prompt during compaction transcript rotation. Thanks @yetval.
|
||||
- **PR #93738** fix: break plugin registry type import cycle. Thanks @giodl73-repo.
|
||||
- **PR #93740** fix(sessions): release retained locks after takeover. Thanks @TurboTheTurtle.
|
||||
- **PR #93745** fix(usage): reject invalid explicit dates in usage RPC date parsing. Thanks @harjothkhara and @vincentkoc.
|
||||
- **PR #93746** fix(ui): populate realtime talk provider and transport options from talk.catalog. Thanks @shushushv and @vincentkoc.
|
||||
- **PR #93751** fix(ios): fix quick setup sheet layout design. Thanks @zats.
|
||||
- **PR #93749** fix(compaction): ignore stale persisted totalTokens in preflight gate. Thanks @yetval.
|
||||
- **PR #93753** fix: correct tautological uppercase check in tool description summarizer. Thanks @GautamKumarOffical.
|
||||
- **PR #89123** refactor: route transcript writers through session seam. Thanks @jalehman.
|
||||
- **PR #93758** feat(memory): apply outputDimensionality truncation to local GGUF embeddings (fixes #58765). Thanks @liuhao1024 and @vincentkoc and @losz5000.
|
||||
- **PR #93754** feat(inbound-meta): expose per-turn source modality. Related #50482. Thanks @liuhao1024 and @vincentkoc and @JTOrca.
|
||||
- **PR #93767** fix(reasoning-tags): strip MiniMax `mm:` namespaced reasoning tags. Thanks @DrHack1 and @vincentkoc.
|
||||
- **PR #93772** fix(feishu): recover CJK filenames from JSON file_name field (fixes #81103). Thanks @liuhao1024 and @vincentkoc and @pjuneye.
|
||||
- **PR #93773** fix(ui): scope Skill Workshop proposals to selected agent. Related #93760. Thanks @TurboTheTurtle and @vincentkoc and @hannesrudolph.
|
||||
- **PR #88750** feat(context-engine): pass runtime settings into lifecycle. Thanks @ragesaq and @jalehman.
|
||||
- **PR #93763** fix(agents): use neutral billing copy for subscription auth. Related #80877. Thanks @eldar702 and @vincentkoc and @22kyasue.
|
||||
- **PR #93818** List all ClawHub docs in sidebar. Thanks @Patrick-Erichsen.
|
||||
- **PR #93779** fix(webchat): skip textarea resize during IME composition to eliminate typing lag. Related #90800. Thanks @joelnishanth and @vincentkoc and @w10497-create.
|
||||
- **PR #93786** fix(plugins): treat refreshable catalogs as requiring runtime discovery (fixes #93775). Thanks @liuhao1024 and @St0rmz1.
|
||||
- **PR #93791** fix(memory): await search-sync before returning results to prevent stale index (fixes #52115). Thanks @liuhao1024 and @vincentkoc and @FicheallADa.
|
||||
- **PR #93780** fix(google): keep parallel Gemini tool responses in the turn after the model. Thanks @yetval and @vincentkoc.
|
||||
- **PR #93789** fix(agents): make lane suspension consistent across cooldown-precheck and embedded-runner paths. Related #93036. Thanks @joelnishanth and @vincentkoc and @kumaxs.
|
||||
- **PR #93798** fix(status): show 0 (not ?) for fresh-session context tokens. Related #93771. Thanks @Alix-007 and @vincentkoc and @anarchia-99.
|
||||
- **PR #93810** fix(cron): preserve startup overflow catch-up deferrals in start() maintenance pass. Thanks @yetval.
|
||||
- **PR #93811** Strip UTF-8 BOM when reading SKILL.md in quick_validate. Thanks @HrachShah.
|
||||
- **PR #93803** fix(ui): preserve WebChat visible messages across session switches. Related #80855. Thanks @LiuwqGit and @vincentkoc and @viagarsuker.
|
||||
- **PR #93792** fix(android): wait for node capability approval before onboarding. Thanks @Solvely-Colin and @vincentkoc.
|
||||
- **PR #93796** fix(feishu): paginate wiki node and space listing (#37626). Thanks @ZengWen-DT and @vincentkoc and @ritou11.
|
||||
- **PR #93797** fix(browser): use openTab return value to prevent wsUrl race in ensureTabAvailable (fixes #63343). Thanks @liuhao1024 and @vincentkoc and @OpenCodeEngineer.
|
||||
- **PR #93806** fix(reasoning-tags): strip MiniMax mm: tags on silent-reply and streaming paths missed by #93767. Thanks @Alix-007 and @vincentkoc.
|
||||
- **PR #93691** refactor: add gateway sessions.create lifecycle seam. Thanks @jalehman.
|
||||
- **PR #88748** fix(gemini): bridge OAuth profiles into CLI runtime. Related #88742. Thanks @jason-allen-oneal.
|
||||
- **PR #93857** fix(deps): remediate Dependabot alerts. Thanks @vincentkoc.
|
||||
- **PR #93874** fix(slack): recognize MiniMax mm: namespaced reasoning tags in monitor preview. Thanks @Alix-007.
|
||||
- **PR #93832** feat(providers): add ClawRouter managed proxy. Thanks @vincentkoc.
|
||||
- **PR #93880** fix(macos): preserve approvals migration data. Thanks @vincentkoc.
|
||||
- **PR #93903** fix(cron): reject invalid absolute timestamps. Thanks @Alix-007 and @vincentkoc.
|
||||
- **PR #93879** fix(update): use configured npm registry for update metadata. Related #79140. Thanks @vincentkoc and @sixerLiu.
|
||||
- **PR #93924** revert(providers): remove ClawRouter provider. Thanks @vincentkoc.
|
||||
- **PR #93955** fix(telegram): surface rich-message disabled state. Thanks @obviyus.
|
||||
- **PR #93881** fix(agents): route BTW through canonical Codex runtime. Related #88902. Thanks @vincentkoc and @TurboTheTurtle and @khalil-omer.
|
||||
- **PR #90192** fix(feishu): fetch quoted content before empty-message guard. Related #90177. Thanks @bladin and @sliverp and @lkxlaz.
|
||||
- **PR #93237** Fix Mattermost open DM validation. Thanks @amknight.
|
||||
- **PR #93945** feat(diagnostics): add SIEM security events. Thanks @vincentkoc.
|
||||
- **PR #87487** fix(cli): clarify mcp list registry scope. Related #65209. Thanks @Alix-007 and @slideshow-dingo.
|
||||
- **PR #24661** feat(cohere): add provider plugin. Thanks @vincentkoc.
|
||||
- **PR #93532** Expose verified ClawHub source in skill verify output. Thanks @momothemage.
|
||||
- **PR #93538** feat(codex): support app-server network proxy profiles. Thanks @vincentkoc.
|
||||
- **PR #93938** fix(telegram): guard UTF-16 surrogate pairs in outbound chunkers. Related #93921. Thanks @Nas01010101 and @vincentkoc.
|
||||
- **PR #94104** feat(agents): trace compaction summarization model calls. Thanks @amknight.
|
||||
- **PR #94108** Fix package Telegram temp root. Thanks @obviyus.
|
||||
- **PR #94113** Fix Telegram package output mount. Thanks @obviyus.
|
||||
- **PR #89062** feat(docker): support offline setup reruns. Related #70443. Thanks @Alix-007 and @safrano9999.
|
||||
- **PR #93929** fix(secrets): explicitly pass BWS_SERVER_URL to resolver for self-hosted instances. Related #93851. Thanks @Pandah97 and @vincentkoc and @AdoShan.
|
||||
- **PR #90057** Polish Workboard operations view. Thanks @fuller-stack-dev.
|
||||
- **PR #89396** fix(doctor): drop inert legacy cron notify when cron.webhook is unset. Related #44460. Thanks @Alix-007.
|
||||
- **PR #94138** fix(session): prevent stale finalizer from recreating deleted session rows. Related #40840. Thanks @xialonglee and @vincentkoc and @AL-knows.
|
||||
- **PR #93739** refactor: add session patch projection seam. Thanks @jalehman.
|
||||
- **PR #94178** fix(workspace): skip optional bootstrap files when workspace setup is already completed. Related #83593. Thanks @dwc1997 and @jsompis.
|
||||
- **PR #93363** fix(feishu): enforce account tool family gates. Thanks @eleqtrizit.
|
||||
- **PR #93813** fix(codex): keep message registered for internal turns. Related #93750. Thanks @jalehman and @hannesrudolph.
|
||||
- **PR #93659** refactor: add session reset delete lifecycle seam. Thanks @jalehman.
|
||||
- **PR #93852** ci(release): harden release controls. Thanks @vincentkoc.
|
||||
- **PR #94203** feat(codex): support remote app-server plugins. Thanks @kevinslin.
|
||||
- **PR #94263** chore: migrate claw-score skill. Thanks @RomneyDa and @kevinslin.
|
||||
- **PR #93695** refactor: add compact trim lifecycle seam. Thanks @jalehman.
|
||||
- **PR #93114** test: fold lifecycle and package proof into QA Lab. Thanks @RomneyDa.
|
||||
- **PR #93181** test: fold otel smoke into qa e2e. Thanks @RomneyDa.
|
||||
- **PR #93178** test: fold gateway smoke into qa e2e. Thanks @RomneyDa.
|
||||
- **PR #94276** qa-lab: support script-backed evidence scenarios. Thanks @Solvely-Colin and @RomneyDa.
|
||||
- **PR #94282** Support owner-qualified ClawHub skill installs. Thanks @Patrick-Erichsen.
|
||||
- **PR #93704** refactor: add session cleanup lifecycle seam. Thanks @jalehman.
|
||||
- **PR #94296** fix: require all taxonomy coverage ids for a feature - AND not OR. Thanks @RomneyDa.
|
||||
- **PR #92016** fix(plugins): compose live hook registry view for tool-call hooks. Related #91918. Thanks @amknight and @vokaplok.
|
||||
- **PR #89596** fix(policy): recognize declared tool allowlists. Thanks @giodl73-repo.
|
||||
- **PR #93713** fix: route deleted-agent session purge through lifecycle seam. Thanks @jalehman.
|
||||
- **PR #84172** fix(exec): rebuild command authorization on the Tree-sitter command planner. Thanks @jesse-merhi.
|
||||
- **PR #94332** docs: add ClawHub namespace claims to sidebar. Thanks @Patrick-Erichsen.
|
||||
- **PR #86360** fix(codex): honor bound agent exec host policy. Thanks @jesse-merhi.
|
||||
- **PR #73162** fix(slack): remove socket reconnect attempt cap so gateway stays connected indefinitely. Related #72808. Thanks @suboss87 and @tleyden.
|
||||
- **PR #94156** fix: expose OpenAI image quality and moderation CLI options. Thanks @lastguru-net and @fuller-stack-dev.
|
||||
- **PR #94350** feat: externalize GMI provider plugin. Thanks @Patrick-Erichsen and @vincentkoc.
|
||||
- **PR #94543** fix(gateway): bound config.get middleware results. Related #94265. Thanks @vincentkoc and @v-s-gusev.
|
||||
- **PR #91409** fix(update): run plugin convergence after RPC git updates. Thanks @masatohoshino.
|
||||
- **PR #94556** chore(extensions): bump tokenjuice to 0.8.1. Thanks @vincentkoc.
|
||||
- **PR #94580** fix(ci): stabilize update run gates.
|
||||
- **PR #94394** fix(infra): probe 127.0.0.1 in ensurePortAvailable to detect IPv4-only occupants. Related #94379. Thanks @Pandah97 and @wangwllu.
|
||||
- **PR #94421** fix(agents): preserve active compaction retries. Related #94391. Thanks @dexiosmb.
|
||||
- **PR #94428** fix(feishu): preserve replies before error finals. Related #94360. Thanks @xunx33.
|
||||
- **PR #93735** refactor: add restart recovery lifecycle seam. Thanks @jalehman.
|
||||
- **PR #94591** docs(release): backfill complete contribution records. Thanks @vincentkoc.
|
||||
- **PR #94588** fix(cron): retry isolated setup timeouts. Thanks @aaroneden.
|
||||
- **PR #94082** fix(cron): prevent lane timeout during long tool execution. Related #94033. Thanks @ajwan8998 and @JingWang-Star996.
|
||||
- **PR #94551** feat(firecrawl): add keyless scrape support. Thanks @vincentkoc and @developersdigest.
|
||||
- **PR #94619** test(ci): stabilize timeout-sensitive shards. Thanks @vincentkoc.
|
||||
- **PR #94048** fix(telegram): set richMessages default to false explicitly in schema. Related #93770, #93794. Thanks @Monkey-wusky and @obviyus and @Nardoa375 and @laurenceputra.
|
||||
- **PR #94118** [codex] Fix Telegram rich local Markdown link hrefs. Related #94117. Thanks @dankarization and @obviyus.
|
||||
- **PR #94646** refactor(sqlite): land database-first memory and proxy alignment. Thanks @vincentkoc.
|
||||
- **PR #94658** test(sqlite): use shared temp directory helper. Thanks @vincentkoc.
|
||||
- **PR #92135** fix(openai-embedding): preserve openai/ prefix for non-native base URLs. Related #92124. Thanks @xialonglee and @Kambrian.
|
||||
- **PR #93737** refactor: add session maintenance transaction seam. Thanks @jalehman.
|
||||
- **PR #93685** refactor(auto-reply): add lifecycle storage seams. Thanks @jalehman.
|
||||
- **PR #94349** fix(agents): preserve pending subagent completion announces. Related #93323. Thanks @sallyom and @oiGaDio.
|
||||
- **PR #93174** test: fold channel message flows into qa e2e. Thanks @RomneyDa.
|
||||
- **PR #94093** Prevent Codex thread rotation from losing next-step context. Thanks @VACInc.
|
||||
- **PR #53920** fix(scripts): avoid mutating tracked auth-monitor template during setup. Thanks @JackWuGlobal.
|
||||
- **PR #94702** Standardize QA coverage IDs on dotted names. Thanks @RomneyDa.
|
||||
- **PR #81825** fix(skills/1password): stop forcing tmux for desktop app auth (#52540). Thanks @koshaji and @tylerbittner.
|
||||
- **PR #94725** fix(doctor): warn on volatile SQLite state. Thanks @vincentkoc.
|
||||
- **PR #88551** fix(agents): skip auth gate for CLI-owned transport. Thanks @yu-xin-c.
|
||||
- **PR #88581** feat(commands): add /name to rename the current session from chat. Thanks @BSG2000.
|
||||
- **PR #94324** feat(codex): support app-server SecretRefs. Thanks @kevinlin-openai and @kevinslin.
|
||||
- **PR #90882** fix: add self-knowledge docs rule to system prompt. Related #90713. Thanks @SutraHsing.
|
||||
- **PR #94684** fix: #80507 show dry-run output for message send/poll. Thanks @lzyyzznl and @YB0y.
|
||||
- **PR #93823** fix(whatsapp): keep opening text chunk when first media fails on multi-chunk reply. Thanks @yetval.
|
||||
- **PR #89203** refactor: route SDK session compatibility through seam. Thanks @jalehman.
|
||||
- **PR #94453** fix: default cron runMode to "due" instead of "force" (#94270). Thanks @jincheng-xydt and @sallyom and @davectr.
|
||||
- **PR #94746** fix(note): prevent clack from re-breaking copy-sensitive tokens. Related #94730. Thanks @xzh-icenter and @berkgungor.
|
||||
- **PR #89904** refactor: route sdk session compatibility through accessor. Thanks @jalehman.
|
||||
- **PR #86719** fix(skills): retarget stale plugin skill symlinks. Related #85925. Thanks @stevenepalmer and @shakkernerd.
|
||||
- **PR #94337** fix(tui): show 0 not ? for fresh-session context tokens in footer. Thanks @mushuiyu886.
|
||||
- **PR #94539** fix(android): group settings by intent. Thanks @Tosko4.
|
||||
- **PR #92383** fix(gateway): never return an empty chat.history transcript. Thanks @Hidetsugu55.
|
||||
- **PR #92574** test(browser): cover action-input CLI request bodies. Related #83877. Thanks @yu-xin-c and @davinci282828.
|
||||
- **PR #92873** test(diffs): add viewerState, toolbar toggle, shadow root, and hydrateProps tests (fixes #83915). Thanks @liuhao1024 and @davinci282828.
|
||||
- **PR #94257** fix(sessions): preserve Media\* index alignment when reading user-turn fields. Thanks @Nas01010101.
|
||||
- **PR #94756** fix(codex): bound turn/start text when context budget is non-positive. Related #94748. Thanks @Nas01010101.
|
||||
- **PR #94729** fix(skills/trello): add curl to requires.bins to match body examples (fixes #94727). Thanks @liuhao1024 and @berkgungor.
|
||||
- **PR #94790** feat(slack): log INFO receipt for inbound app_mention events. Related #94691. Thanks @ZengWen-DT and @BryceMurray.
|
||||
- **PR #81696** fix: guard tool event callbacks (AI-assisted). Thanks @enjoylife1243.
|
||||
- **PR #94809** chore: forward-port alpha release fixes.
|
||||
- **PR #94612** fix(macos): open NSOpenPanel for embedded Control UI file inputs (#94468). Thanks @bbblending and @DINGDANGMAOUP.
|
||||
- **PR #89806** fix(feishu): avoid axios interceptor internals. Related #83913. Thanks @sweetcornna and @davinci282828.
|
||||
- **PR #91923** fix(ios): clean up notification settings state. Thanks @zats.
|
||||
- **PR #91345** fix: suggest close CLI commands. Related #83999. Thanks @glenn-agent and @HannesOberreiter.
|
||||
- **PR #94561** Add stdout diagnostics OTEL log exporter. Thanks @jesse-merhi.
|
||||
- **PR #91013** fix(gateway): ignore stale abort markers for fresh chat events. Related #91012. Thanks @nxmxbbd.
|
||||
- **PR #89279** fix(tasks): deliver ACP completions to bound Discord threads. Related #84022. Thanks @anyech and @h-mascot.
|
||||
- **PR #91656** test(cron): expand parseAbsoluteTimeMs test coverage to 39 cases. Related #91654. Thanks @SpecialLeon.
|
||||
- **PR #94810** fix(telegram): classify sendChatAction 401 by structured error_code, not bare substring match. Related #94787. Thanks @ZOOWH and @parveshsaini.
|
||||
- **PR #94737** fix(reply): clarify provider internal error copy. Thanks @snowzlmbot.
|
||||
- **PR #94868** fix(channels): preserve command progress detail. Thanks @vincentkoc.
|
||||
- **PR #94891** fix(telegram): send progress previews as html text. Thanks @obviyus.
|
||||
- **PR #94683** fix(outbound): keep direct-only targets out of group sessions. Related #92384. Thanks @scotthuang and @haiwei01.
|
||||
- **PR #92477** fix: migrate watch app to single-target app (Xcode 27+ compat). Thanks @zats and @joshavant.
|
||||
- **PR #94812** test(perf): compare saved CLI startup benchmarks. Thanks @FelixIsaac.
|
||||
- **PR #94856** fix(telegram): normalize all HTML tables before entity-escaping in rich messages. Related #94317. Thanks @zhangqueping and @jairrab.
|
||||
- **PR #91685** fix(cron): refuse keyless implicit isolated cron delivery inherited from shared agent-main bucket. Thanks @nxmxbbd.
|
||||
|
||||
## 2026.6.8
|
||||
|
||||
### Highlights
|
||||
@@ -693,7 +234,6 @@ This audited record covers the complete v2026.6.6..v2026.6.8 history: 192 merged
|
||||
- **PR #93159** fix(tui): keep parent stdin paused after exit. Thanks @fuller-stack-dev.
|
||||
- **PR #93616** Keep key-free web search providers opt-in. Thanks @davemorin and @vincentkoc.
|
||||
- **PR #93164** fix(telegram): preserve rich markdown line breaks. Thanks @vincentkoc.
|
||||
|
||||
## 2026.6.7
|
||||
|
||||
### Highlights
|
||||
@@ -780,7 +320,6 @@ This audited record covers the complete v2026.6.6..v2026.6.7-beta.1 history: 59
|
||||
- **PR #92605** fix(docs): pin Windows Hub download links to v2026.6.5. Related #92470. Thanks @lzyyzznl and @arjkul.
|
||||
- **PR #92593** #92589: fix(internal-runtime-context): wrap prompt-preface runtime context body in delimiters. Thanks @zhangqueping and @jovi2014-cyber.
|
||||
- **PR #92606** Run Vitest and Playwright scenarios from qa suite. Thanks @RomneyDa.
|
||||
|
||||
## 2026.6.6
|
||||
|
||||
### Highlights
|
||||
@@ -1018,7 +557,6 @@ This audited record covers the complete v2026.6.5..v2026.6.6 history: 198 merged
|
||||
- **PR #92150** fix(release): gate beta publish on plugin verification. Thanks @vincentkoc.
|
||||
- **PR #92158** fix(cli): validate gateway RPC timeout inputs. Thanks @ruanrrn and @comeran.
|
||||
- **PR #91911** fix(agents): retry same model across short rate-limit windows. Thanks @lanzhi-lee.
|
||||
|
||||
## 2026.6.5
|
||||
|
||||
### Highlights
|
||||
@@ -1203,7 +741,6 @@ This audited record covers the complete v2026.6.2-beta.1..v2026.6.5 history: 142
|
||||
- **PR #89659** fix(feishu): retry on send rate-limit errors (230020/230006). Related #70879. Thanks @ladygege and @marshallm-create and @sliverp and @AxelHu.
|
||||
- **PR #91547** Fix Docker store seed target packages. Related #91035. Thanks @sallyom and @laurenceputra.
|
||||
- **PR #91423** feat(qqbot): add /bot-group-allways command to toggle mention requirement. Thanks @cxyhhhhh.
|
||||
|
||||
## 2026.6.2
|
||||
|
||||
### Highlights
|
||||
@@ -1296,7 +833,6 @@ This audited record covers the complete v2026.6.1..v2026.6.2-beta.1 history: 57
|
||||
- **PR #89176** fix(browser): honor tab timeout for Chrome MCP. Related #88213. Thanks @MonkeyLeeT and @lamkan0210.
|
||||
- **PR #90043** fix: restore Skill Workshop current chat toggle. Thanks @shakkernerd.
|
||||
- **PR #81422** fix(update): surface plugin channel fallbacks. Thanks @BKF-Gitty.
|
||||
|
||||
## 2026.6.1
|
||||
|
||||
### Highlights
|
||||
@@ -1511,7 +1047,6 @@ This audited record covers the complete v2026.5.31-beta.4..v2026.6.1 history: 11
|
||||
- **PR #88288** fix(config): skip state-dir dotenv values that are unresolved shell references. Related #88274. Thanks @Alix-007 and @mathias15010.
|
||||
- **PR #88305** fix(browser): isolate Chrome MCP pending attach aborts. Related #88304. Thanks @rohitjavvadi.
|
||||
- **PR #74089** fix(openai/tts): handle [[tts:speed]] directive in OpenAI speech provider (#12163). Thanks @stainlu and @useramuser.
|
||||
|
||||
## 2026.5.31
|
||||
|
||||
### Highlights
|
||||
@@ -1642,7 +1177,7 @@ This audited record covers the complete v2026.5.28..v2026.5.31-beta.4 history: 4
|
||||
- **PR #88346** refactor: extract web content core package.
|
||||
- **PR #71280** test(gateway): avoid brittle shutdown timer assertion. Thanks @hansolo949.
|
||||
- **PR #80686** fix(agents): extend session-write-lock payload-less orphan grace from 5s to 30s. Thanks @wAngByg.
|
||||
- **PR #88067** fix(responses): drop orphaned assistant msg\_\* id when reasoning is dropped (#88019). Thanks @BSG2000.
|
||||
- **PR #88067** fix(responses): drop orphaned assistant msg_* id when reasoning is dropped (#88019). Thanks @BSG2000.
|
||||
- **PR #88417** [codex] Route denied exec approval followups to sessions. Related #88167. Thanks @brokemac79 and @jhartman00.
|
||||
- **PR #85996** fix #85782: surface terminal TUI lifecycle errors. Thanks @zhangguiping-xydt and @vincentkoc and @shakkernerd.
|
||||
- **PR #88445** refactor: source model catalog types from core.
|
||||
@@ -1941,7 +1476,6 @@ This audited record covers the complete v2026.5.28..v2026.5.31-beta.4 history: 4
|
||||
- **PR #88978** perf(ui): skip closed slash menu rerenders. Thanks @vincentkoc.
|
||||
- **PR #88982** fix(test): wait for telegram timer flushes. Thanks @vincentkoc.
|
||||
- **PR #88989** perf(ui): guard chat transcript rerenders. Thanks @vincentkoc.
|
||||
|
||||
## 2026.5.28
|
||||
|
||||
### Highlights
|
||||
|
||||
@@ -106,8 +106,7 @@ For coordinated change sets that genuinely need more than 20 PRs, join the **#cl
|
||||
## Before You PR
|
||||
|
||||
- Test locally with your OpenClaw instance
|
||||
- External PRs must describe the user, product, or operational problem in **What Problem This Solves** and include useful validation in **Evidence**. Focused tests, CI results, screenshots, recordings, terminal output, live observations, redacted logs, and artifact links all count. Reviewers will inspect the code, tests, and CI; use the PR body to explain intent and make validation easy to understand.
|
||||
- When ClawSweeper, Codex, Barnacle, or a maintainer asks for more context or evidence, edit the PR description instead of only replying in a new comment. Keep **What Problem This Solves**, **Why This Change Was Made**, **User Impact**, and **Evidence** current; a short comment can point reviewers to the update, but the PR body should remain the durable explanation for maintainers and bots.
|
||||
- External PRs must include a filled **Real behavior proof** section in the PR body. Show the real setup you tested, the exact command or steps you ran after the patch, after-fix evidence, the observed result, and anything you did not test. Screenshots, recordings, terminal screenshots, console output, copied live output, linked artifacts, and redacted runtime logs all count. Unit tests, mocks, snapshots, lint, typechecks, and CI are useful but do not satisfy this requirement by themselves. Maintainers may apply `proof: override` only when the proof gate should not apply.
|
||||
- Keep PRs takeover-ready: open them from a branch maintainers can push to. For fork PRs, leave GitHub's **Allow edits by maintainers** option enabled so maintainers can finish urgent fixes, changelog entries, or merge prep when needed. If GitHub shows **Allow edits and access to secrets by maintainers**, enable it only when that workflow/secrets access is acceptable and say so in the PR.
|
||||
- Do not edit `CHANGELOG.md` in contributor PRs. Maintainers or ClawSweeper add the changelog entry when landing user-facing changes.
|
||||
- Run tests: `pnpm build && pnpm check && pnpm test`
|
||||
@@ -170,7 +169,7 @@ Built with Codex, Claude, or other AI tools? **Awesome - just mark it!**
|
||||
Please include in your PR:
|
||||
|
||||
- [ ] Mark as AI-assisted in the PR title or description
|
||||
- [ ] Include a concise **Evidence** section with the most useful validation. Reviewers will inspect the code, tests, and CI rather than relying on the PR body alone.
|
||||
- [ ] Include human-run real behavior proof from your own setup. AI-generated tests, mocks, lint, typechecks, and CI output are supplemental only; they do not prove the fix works for users.
|
||||
- [ ] Include prompts or session logs if possible (super helpful!)
|
||||
- [ ] Confirm you understand what the code does
|
||||
- [ ] If you have access to Codex, run `codex review --base origin/main` locally and address the findings before asking for review
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# Source of truth: apps/android/version.json
|
||||
# Generated by scripts/android-sync-versioning.ts.
|
||||
|
||||
OPENCLAW_ANDROID_VERSION_NAME=2026.6.9
|
||||
OPENCLAW_ANDROID_VERSION_CODE=2026060901
|
||||
OPENCLAW_ANDROID_VERSION_NAME=2026.6.2
|
||||
OPENCLAW_ANDROID_VERSION_CODE=2026060201
|
||||
|
||||
@@ -898,38 +898,32 @@ private fun SettingsShellScreen(
|
||||
ProfilePanel(displayName = displayName.ifBlank { "OpenClaw" }, onClick = { onRouteChange(SettingsRoute.Profile) })
|
||||
}
|
||||
|
||||
val settingsRows =
|
||||
listOf(
|
||||
SettingsRow("Gateway", gatewaySummary(statusText, isConnected), Icons.Default.Cloud, status = isConnected, route = SettingsRoute.Gateway),
|
||||
SettingsRow("Nodes & Devices", nodesDevicesSummaryText(nodesDevicesSummary), Icons.Default.Cloud, status = nodesDevicesStatus(nodesDevicesSummary), route = SettingsRoute.NodesDevices),
|
||||
SettingsRow("Channels", channelsSummaryText(channelsSummary), Icons.Default.Notifications, status = channelsStatus(channelsSummary), route = SettingsRoute.Channels),
|
||||
SettingsRow("Agents", if (agents.isEmpty()) "Load from gateway" else "${agents.size} available", Icons.Default.Person, status = agents.isNotEmpty(), route = SettingsRoute.Agents),
|
||||
SettingsRow("Approvals", approvalsSummary(pendingToolCalls.size), Icons.Default.Lock, status = approvalsStatus(pendingToolCalls.size), route = SettingsRoute.Approvals),
|
||||
SettingsRow("Cron Jobs", cronJobsSummary(cronStatus.jobs), Icons.Outlined.AccessTime, status = if (cronStatus.jobs > 0) cronStatus.enabled else null, route = SettingsRoute.CronJobs),
|
||||
SettingsRow("Usage", usageSummaryText(usageSummary.providers.size), Icons.Default.Storage, status = if (usageSummary.providers.isNotEmpty()) true else null, route = SettingsRoute.Usage),
|
||||
SettingsRow("Skills", skillsSummaryText(skillsSummary.skills), Icons.Default.Settings, status = skillsStatus(skillsSummary.skills), route = SettingsRoute.Skills),
|
||||
SettingsRow("Dreaming", dreamingSummaryText(dreamingSummary), Icons.Default.Storage, status = dreamingStatus(dreamingSummary), route = SettingsRoute.Dreaming),
|
||||
SettingsRow("Voice", if (speakerEnabled) "Speaker on" else "Speaker muted", Icons.Default.Mic, route = SettingsRoute.Voice),
|
||||
SettingsRow("Canvas", "Screen surface", Icons.AutoMirrored.Filled.ScreenShare, status = isConnected, route = SettingsRoute.Canvas),
|
||||
SettingsRow("Notifications", if (notificationForwardingEnabled) "Smart delivery" else "Off", Icons.Default.Notifications, route = SettingsRoute.Notifications),
|
||||
SettingsRow("Phone Capabilities", if (cameraEnabled) "Camera enabled" else "Locked", Icons.Default.Lock, status = !cameraEnabled, route = SettingsRoute.PhoneCapabilities),
|
||||
SettingsRow("Appearance", appearanceThemeSummary(appearanceThemeMode), Icons.Default.Palette, route = SettingsRoute.Appearance),
|
||||
SettingsRow("About", "Version and update", Icons.Default.Storage, route = SettingsRoute.About),
|
||||
SettingsRow("Health", "Diagnostics", Icons.Default.Settings, status = isConnected, route = SettingsRoute.Health),
|
||||
)
|
||||
|
||||
settingsSections(settingsRows).forEach { section ->
|
||||
item {
|
||||
SettingsSectionTitle(section.title)
|
||||
}
|
||||
item {
|
||||
SettingsGroup(rows = section.rows, onOpen = onRouteChange)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
SettingsSectionTitle("Account")
|
||||
SettingsGroup(
|
||||
rows =
|
||||
listOf(
|
||||
SettingsRow("Profile", displayName.ifBlank { "Local device" }, Icons.Default.Person, route = SettingsRoute.Profile),
|
||||
SettingsRow("Voice", if (speakerEnabled) "Speaker on" else "Speaker muted", Icons.Default.Mic, route = SettingsRoute.Voice),
|
||||
SettingsRow("Agents", if (agents.isEmpty()) "Load from gateway" else "${agents.size} available", Icons.Default.Person, status = agents.isNotEmpty(), route = SettingsRoute.Agents),
|
||||
SettingsRow("Approvals", approvalsSummary(pendingToolCalls.size), Icons.Default.Lock, status = approvalsStatus(pendingToolCalls.size), route = SettingsRoute.Approvals),
|
||||
SettingsRow("Cron Jobs", cronJobsSummary(cronStatus.jobs), Icons.Outlined.AccessTime, status = if (cronStatus.jobs > 0) cronStatus.enabled else null, route = SettingsRoute.CronJobs),
|
||||
SettingsRow("Usage", usageSummaryText(usageSummary.providers.size), Icons.Default.Storage, status = if (usageSummary.providers.isNotEmpty()) true else null, route = SettingsRoute.Usage),
|
||||
SettingsRow("Skills", skillsSummaryText(skillsSummary.skills), Icons.Default.Settings, status = skillsStatus(skillsSummary.skills), route = SettingsRoute.Skills),
|
||||
SettingsRow("Nodes & Devices", nodesDevicesSummaryText(nodesDevicesSummary), Icons.Default.Cloud, status = nodesDevicesStatus(nodesDevicesSummary), route = SettingsRoute.NodesDevices),
|
||||
SettingsRow("Channels", channelsSummaryText(channelsSummary), Icons.Default.Notifications, status = channelsStatus(channelsSummary), route = SettingsRoute.Channels),
|
||||
SettingsRow("Dreaming", dreamingSummaryText(dreamingSummary), Icons.Default.Storage, status = dreamingStatus(dreamingSummary), route = SettingsRoute.Dreaming),
|
||||
SettingsRow("Canvas", "Screen surface", Icons.AutoMirrored.Filled.ScreenShare, status = isConnected, route = SettingsRoute.Canvas),
|
||||
SettingsRow("Notifications", if (notificationForwardingEnabled) "Smart delivery" else "Off", Icons.Default.Notifications, route = SettingsRoute.Notifications),
|
||||
SettingsRow("Phone Capabilities", if (cameraEnabled) "Camera enabled" else "Locked", Icons.Default.Lock, status = !cameraEnabled, route = SettingsRoute.PhoneCapabilities),
|
||||
SettingsRow("Gateway", gatewaySummary(statusText, isConnected), Icons.Default.Cloud, status = isConnected, route = SettingsRoute.Gateway),
|
||||
SettingsRow("Appearance", appearanceThemeSummary(appearanceThemeMode), Icons.Default.Palette, route = SettingsRoute.Appearance),
|
||||
SettingsRow("Health", "Diagnostics", Icons.Default.Settings, status = isConnected, route = SettingsRoute.Health),
|
||||
SettingsRow("About", "Version and update", Icons.Default.Storage, route = SettingsRoute.About),
|
||||
),
|
||||
onOpen = onRouteChange,
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SettingsGroup(
|
||||
rows = listOf(SettingsRow("Sign Out", "Disconnect", Icons.AutoMirrored.Filled.ExitToApp)),
|
||||
@@ -1063,7 +1057,7 @@ private fun dreamingStatus(summary: GatewayDreamingSummary): Boolean? =
|
||||
else -> null
|
||||
}
|
||||
|
||||
internal data class SettingsRow(
|
||||
private data class SettingsRow(
|
||||
val title: String,
|
||||
val value: String,
|
||||
val icon: ImageVector,
|
||||
@@ -1071,65 +1065,6 @@ internal data class SettingsRow(
|
||||
val route: SettingsRoute? = null,
|
||||
)
|
||||
|
||||
internal data class SettingsSection(
|
||||
val title: String,
|
||||
val rows: List<SettingsRow>,
|
||||
)
|
||||
|
||||
internal fun settingsSections(rows: List<SettingsRow>): List<SettingsSection> =
|
||||
settingsSectionOrder.mapNotNull { title ->
|
||||
val sectionRows = rows.filter { row -> row.route?.let(::settingsSectionTitleForRoute) == title }
|
||||
if (sectionRows.isEmpty()) null else SettingsSection(title = title, rows = sectionRows)
|
||||
}
|
||||
|
||||
private val settingsSectionOrder =
|
||||
listOf(
|
||||
"Connection",
|
||||
"Agents & automation",
|
||||
"Phone context & privacy",
|
||||
"Profile & device",
|
||||
"Diagnostics",
|
||||
)
|
||||
|
||||
internal fun settingsSectionTitleForRoute(route: SettingsRoute): String =
|
||||
when (route) {
|
||||
SettingsRoute.Gateway,
|
||||
SettingsRoute.NodesDevices,
|
||||
SettingsRoute.Channels,
|
||||
-> "Connection"
|
||||
|
||||
SettingsRoute.Agents,
|
||||
SettingsRoute.Approvals,
|
||||
SettingsRoute.CronJobs,
|
||||
SettingsRoute.Usage,
|
||||
SettingsRoute.Skills,
|
||||
SettingsRoute.Dreaming,
|
||||
-> "Agents & automation"
|
||||
|
||||
SettingsRoute.Voice,
|
||||
SettingsRoute.Canvas,
|
||||
SettingsRoute.Notifications,
|
||||
SettingsRoute.PhoneCapabilities,
|
||||
-> "Phone context & privacy"
|
||||
|
||||
SettingsRoute.Profile,
|
||||
SettingsRoute.Appearance,
|
||||
SettingsRoute.About,
|
||||
-> "Profile & device"
|
||||
|
||||
SettingsRoute.Health -> "Diagnostics"
|
||||
SettingsRoute.Home -> "Diagnostics"
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsSectionTitle(title: String) {
|
||||
Text(
|
||||
text = title.uppercase(),
|
||||
style = ClawTheme.type.caption.copy(fontSize = 12.sp, lineHeight = 16.sp),
|
||||
color = ClawTheme.colors.textMuted,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfilePanel(
|
||||
displayName: String,
|
||||
|
||||
@@ -7,8 +7,6 @@ import ai.openclaw.app.GatewayNodeApprovalState
|
||||
import ai.openclaw.app.GatewayNodeSummary
|
||||
import ai.openclaw.app.GatewayNodesDevicesSummary
|
||||
import ai.openclaw.app.GatewayPendingDeviceSummary
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
@@ -157,46 +155,7 @@ class ShellScreenLogicTest {
|
||||
assertEquals("Node approval pending", rows.single().subtitle)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun settingsSectionTitlesGroupPowerSettingsByMeaning() {
|
||||
assertEquals("Connection", settingsSectionTitleForRoute(SettingsRoute.Gateway))
|
||||
assertEquals("Connection", settingsSectionTitleForRoute(SettingsRoute.NodesDevices))
|
||||
assertEquals("Agents & automation", settingsSectionTitleForRoute(SettingsRoute.Approvals))
|
||||
assertEquals("Agents & automation", settingsSectionTitleForRoute(SettingsRoute.CronJobs))
|
||||
assertEquals("Phone context & privacy", settingsSectionTitleForRoute(SettingsRoute.PhoneCapabilities))
|
||||
assertEquals("Phone context & privacy", settingsSectionTitleForRoute(SettingsRoute.Notifications))
|
||||
assertEquals("Profile & device", settingsSectionTitleForRoute(SettingsRoute.Appearance))
|
||||
assertEquals("Diagnostics", settingsSectionTitleForRoute(SettingsRoute.Health))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun settingsSectionsPreserveMeaningfulOrder() {
|
||||
val sections =
|
||||
settingsSections(
|
||||
listOf(
|
||||
settingsRow(SettingsRoute.Voice),
|
||||
settingsRow(SettingsRoute.Agents),
|
||||
settingsRow(SettingsRoute.Gateway),
|
||||
settingsRow(SettingsRoute.Appearance),
|
||||
settingsRow(SettingsRoute.Health),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
listOf(
|
||||
"Connection",
|
||||
"Agents & automation",
|
||||
"Phone context & privacy",
|
||||
"Profile & device",
|
||||
"Diagnostics",
|
||||
),
|
||||
sections.map { it.title },
|
||||
)
|
||||
}
|
||||
|
||||
private fun emptyChannels(): GatewayChannelsSummary = GatewayChannelsSummary(channels = emptyList())
|
||||
|
||||
private fun emptyNodesDevices(): GatewayNodesDevicesSummary = GatewayNodesDevicesSummary(nodes = emptyList(), pendingDevices = emptyList(), pairedDevices = emptyList())
|
||||
|
||||
private fun settingsRow(route: SettingsRoute): SettingsRow = SettingsRow(route.name, "Value", Icons.Default.Settings, route = route)
|
||||
}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
Maintenance update for the current OpenClaw Android release.
|
||||
OpenClaw is now available on Android.
|
||||
|
||||
Connect to your OpenClaw Gateway to chat with your assistant, use realtime Talk mode, review approvals, and bring Android device capabilities like camera, location, screen, and notifications into your private automation workflows.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"version": "2026.6.9",
|
||||
"versionCode": 2026060901
|
||||
"version": "2026.6.2",
|
||||
"versionCode": 2026060201
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ report_include:
|
||||
- Sources/**
|
||||
- ShareExtension/**
|
||||
- ActivityWidget/**
|
||||
- WatchApp/Sources/**
|
||||
- WatchExtension/Sources/**
|
||||
build_arguments:
|
||||
- -destination
|
||||
- generic/platform=iOS Simulator
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
# OpenClaw iOS Changelog
|
||||
|
||||
## 2026.6.9 - 2026-06-20
|
||||
|
||||
Maintenance update for the current OpenClaw release.
|
||||
|
||||
- Added Apple Watch controls for common agent actions.
|
||||
- Improved Gateway setup, notification settings, and share-extension identity handling.
|
||||
- Updated the Watch app integration for current Xcode compatibility.
|
||||
|
||||
## 2026.6.2 - 2026-06-02
|
||||
|
||||
OpenClaw is now available on iPhone.
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"signingRepo": "git@github.com:openclaw/apps-signing.git",
|
||||
"signingBranch": "main",
|
||||
"profileType": "appstore",
|
||||
"appGroupId": "group.ai.openclawfoundation.app.shared",
|
||||
"targets": [
|
||||
{
|
||||
"target": "OpenClaw",
|
||||
@@ -12,8 +11,7 @@
|
||||
"platform": "IOS",
|
||||
"profileKey": "OPENCLAW_APP_PROFILE",
|
||||
"profileName": "OpenClaw App Store ai.openclawfoundation.app",
|
||||
"capabilities": ["PUSH_NOTIFICATIONS", "APP_GROUPS"],
|
||||
"appGroups": ["group.ai.openclawfoundation.app.shared"]
|
||||
"capabilities": ["PUSH_NOTIFICATIONS"]
|
||||
},
|
||||
{
|
||||
"target": "OpenClawShareExtension",
|
||||
@@ -22,8 +20,7 @@
|
||||
"platform": "IOS",
|
||||
"profileKey": "OPENCLAW_SHARE_PROFILE",
|
||||
"profileName": "OpenClaw App Store ai.openclawfoundation.app.share",
|
||||
"capabilities": ["APP_GROUPS"],
|
||||
"appGroups": ["group.ai.openclawfoundation.app.shared"]
|
||||
"capabilities": []
|
||||
},
|
||||
{
|
||||
"target": "OpenClawActivityWidget",
|
||||
@@ -42,6 +39,15 @@
|
||||
"profileKey": "OPENCLAW_WATCH_APP_PROFILE",
|
||||
"profileName": "OpenClaw App Store ai.openclawfoundation.app.watchkitapp",
|
||||
"capabilities": []
|
||||
},
|
||||
{
|
||||
"target": "OpenClawWatchExtension",
|
||||
"displayName": "OpenClaw Watch Extension",
|
||||
"bundleId": "ai.openclawfoundation.app.watchkitapp.extension",
|
||||
"platform": "IOS",
|
||||
"profileKey": "OPENCLAW_WATCH_EXTENSION_PROFILE",
|
||||
"profileName": "OpenClaw App Store ai.openclawfoundation.app.watchkitapp.extension",
|
||||
"capabilities": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,11 +7,12 @@ OPENCLAW_DEVELOPMENT_TEAM = $(OPENCLAW_IOS_SELECTED_TEAM)
|
||||
OPENCLAW_CODE_SIGN_STYLE = Automatic
|
||||
OPENCLAW_CODE_SIGN_IDENTITY = Apple Development
|
||||
OPENCLAW_APP_BUNDLE_ID = ai.openclawfoundation.app
|
||||
OPENCLAW_APP_GROUP_ID = group.ai.openclawfoundation.app.shared
|
||||
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclawfoundation.app.watchkitapp
|
||||
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclawfoundation.app.watchkitapp.extension
|
||||
OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ai.openclawfoundation.app.activitywidget
|
||||
OPENCLAW_ACTIVITY_WIDGET_PROFILE =
|
||||
OPENCLAW_WATCH_APP_PROFILE =
|
||||
OPENCLAW_WATCH_EXTENSION_PROFILE =
|
||||
|
||||
// Local contributors can override this by running scripts/ios-configure-signing.sh.
|
||||
// Keep include after defaults: xcconfig is evaluated top-to-bottom.
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Source of truth: apps/ios/version.json
|
||||
// Generated by scripts/ios-sync-versioning.ts.
|
||||
|
||||
OPENCLAW_IOS_VERSION = 2026.6.9
|
||||
OPENCLAW_MARKETING_VERSION = 2026.6.9
|
||||
OPENCLAW_IOS_VERSION = 2026.6.2
|
||||
OPENCLAW_MARKETING_VERSION = 2026.6.2
|
||||
OPENCLAW_BUILD_VERSION = 1
|
||||
|
||||
#include? "../build/Version.xcconfig"
|
||||
|
||||
@@ -7,12 +7,13 @@ OPENCLAW_DEVELOPMENT_TEAM = YOUR_TEAM_ID
|
||||
|
||||
OPENCLAW_APP_BUNDLE_ID = ai.openclawfoundation.app
|
||||
OPENCLAW_SHARE_BUNDLE_ID = ai.openclawfoundation.app.share
|
||||
OPENCLAW_APP_GROUP_ID = group.ai.openclawfoundation.app.shared
|
||||
OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ai.openclawfoundation.app.activitywidget
|
||||
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclawfoundation.app.watchkitapp
|
||||
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclawfoundation.app.watchkitapp.extension
|
||||
|
||||
// Leave empty with automatic signing.
|
||||
OPENCLAW_APP_PROFILE =
|
||||
OPENCLAW_SHARE_PROFILE =
|
||||
OPENCLAW_ACTIVITY_WIDGET_PROFILE =
|
||||
OPENCLAW_WATCH_APP_PROFILE =
|
||||
OPENCLAW_WATCH_EXTENSION_PROFILE =
|
||||
|
||||
@@ -101,7 +101,6 @@ Release-owner secrets:
|
||||
|
||||
- App Store Connect API auth uses Keychain for private key material plus non-secret `apps/ios/fastlane/.env` variables.
|
||||
- The encrypted signing repo password lives outside this repo in the release-owner vault and is exposed locally as `MATCH_PASSWORD`.
|
||||
- The share sheet requires the Apple Developer App Group in `apps/ios/Config/AppStoreSigning.json` to be associated with both the app and share-extension bundle IDs before App Store profiles are regenerated.
|
||||
- Apple Distribution private keys, certificates, provisioning profiles, and decrypted signing sync output stay under `apps/ios/build/` or Keychain and are gitignored.
|
||||
- Rotating release signing means refreshing Fastlane `match` assets and pushing a fresh encrypted sync state.
|
||||
|
||||
@@ -156,8 +155,7 @@ This should create `apps/ios/fastlane/.env` with non-secret App Store Connect va
|
||||
- `ai.openclawfoundation.app.share`
|
||||
- `ai.openclawfoundation.app.activitywidget`
|
||||
- `ai.openclawfoundation.app.watchkitapp`
|
||||
|
||||
The main app and share extension must both be associated with the App Group pinned in `apps/ios/Config/AppStoreSigning.json`.
|
||||
- `ai.openclawfoundation.app.watchkitapp.extension`
|
||||
|
||||
Use `pnpm ios:release:signing:setup` for the initial portal setup, then `MATCH_PASSWORD=... pnpm ios:release:signing:sync:push` to publish encrypted Fastlane match assets to the shared private repo.
|
||||
|
||||
|
||||
@@ -41,7 +41,5 @@
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
|
||||
</dict>
|
||||
<key>OpenClawAppGroupIdentifier</key>
|
||||
<string>$(OPENCLAW_APP_GROUP_ID)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>$(OPENCLAW_APP_GROUP_ID)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -184,8 +184,7 @@ final class ShareViewController: UIViewController {
|
||||
clientId: clientId,
|
||||
clientMode: "node",
|
||||
clientDisplayName: "OpenClaw Share",
|
||||
deviceIdentityProfile: .shareExtension,
|
||||
includeDeviceIdentity: true)
|
||||
includeDeviceIdentity: false)
|
||||
}
|
||||
|
||||
do {
|
||||
|
||||
@@ -10,8 +10,8 @@ OPENCLAW_DEVELOPMENT_TEAM = FWJYW4S8P8
|
||||
|
||||
OPENCLAW_APP_BUNDLE_ID = ai.openclawfoundation.app
|
||||
OPENCLAW_SHARE_BUNDLE_ID = ai.openclawfoundation.app.share
|
||||
OPENCLAW_APP_GROUP_ID = group.ai.openclawfoundation.app.shared
|
||||
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclawfoundation.app.watchkitapp
|
||||
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclawfoundation.app.watchkitapp.extension
|
||||
OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ai.openclawfoundation.app.activitywidget
|
||||
OPENCLAW_APNS_ENTITLEMENT_ENVIRONMENT = development
|
||||
|
||||
@@ -19,6 +19,7 @@ OPENCLAW_APP_PROFILE = ai.openclawfoundation.app Development
|
||||
OPENCLAW_SHARE_PROFILE = ai.openclawfoundation.app.share Development
|
||||
OPENCLAW_ACTIVITY_WIDGET_PROFILE =
|
||||
OPENCLAW_WATCH_APP_PROFILE =
|
||||
OPENCLAW_WATCH_EXTENSION_PROFILE =
|
||||
|
||||
// Keep local includes after defaults: xcconfig is evaluated top-to-bottom,
|
||||
// so later assignments in local files override the defaults above.
|
||||
|
||||
@@ -53,9 +53,8 @@ struct SettingsProTab: View {
|
||||
@State var suppressCredentialPersist = false
|
||||
@State var locationStatusText: String?
|
||||
@State var previousLocationModeRaw: String = OpenClawLocationMode.off.rawValue
|
||||
@State var notificationStatus: SettingsNotificationStatus = .checking
|
||||
@State var isRequestingNotificationAuthorization = false
|
||||
@State var showNotificationRelayDisclosure = false
|
||||
@State var notificationStatusText = "Checking"
|
||||
@State var notificationActionText = "Request Access"
|
||||
@State var diagnosticsLastRunText = "Not run"
|
||||
@State var diagnosticsIssueCount: Int?
|
||||
@State var showTalkIssueDetails = false
|
||||
@@ -63,18 +62,15 @@ struct SettingsProTab: View {
|
||||
let initialRoute: SettingsRoute?
|
||||
let directRoute: SettingsRoute?
|
||||
let headerLeadingAction: OpenClawSidebarHeaderAction?
|
||||
let onRouteChange: ((SettingsRoute?) -> Void)?
|
||||
|
||||
init(
|
||||
initialRoute: SettingsRoute? = nil,
|
||||
directRoute: SettingsRoute? = nil,
|
||||
headerLeadingAction: OpenClawSidebarHeaderAction? = nil,
|
||||
onRouteChange: ((SettingsRoute?) -> Void)? = nil)
|
||||
headerLeadingAction: OpenClawSidebarHeaderAction? = nil)
|
||||
{
|
||||
self.initialRoute = initialRoute
|
||||
self.directRoute = directRoute
|
||||
self.headerLeadingAction = headerLeadingAction
|
||||
self.onRouteChange = onRouteChange
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -122,7 +118,6 @@ struct SettingsProTab: View {
|
||||
self.refreshNotificationSettings()
|
||||
self.applyPendingGatewaySetupLinkIfNeeded()
|
||||
self.applyInitialRouteIfNeeded()
|
||||
self.notifyRouteChange()
|
||||
}
|
||||
.onChange(of: self.scenePhase) { _, phase in
|
||||
if phase == .active {
|
||||
@@ -159,9 +154,6 @@ struct SettingsProTab: View {
|
||||
.onChange(of: self.appModel.gatewaySetupRequestID) { _, _ in
|
||||
self.applyPendingGatewaySetupLinkIfNeeded()
|
||||
}
|
||||
.onChange(of: self.navigationPath) { _, _ in
|
||||
self.notifyRouteChange()
|
||||
}
|
||||
}
|
||||
|
||||
private func settingsModalPresentation(_ content: some View) -> some View {
|
||||
@@ -226,19 +218,6 @@ struct SettingsProTab: View {
|
||||
} message: {
|
||||
Text(self.scannerError ?? "")
|
||||
}
|
||||
.alert("Enable OpenClaw Hosted Push Relay?", isPresented: self.$showNotificationRelayDisclosure) {
|
||||
Button("Continue") {
|
||||
self.requestNotificationAuthorizationFromSettings()
|
||||
}
|
||||
Button("Not Now", role: .cancel) {}
|
||||
} message: {
|
||||
Text(self.notificationRelayDisclosureMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func openNotificationsRouteFromApprovals() {
|
||||
guard self.directRoute == nil else { return }
|
||||
self.navigationPath = [.notifications]
|
||||
}
|
||||
|
||||
private func applyInitialRouteIfNeeded() {
|
||||
@@ -247,12 +226,4 @@ struct SettingsProTab: View {
|
||||
guard self.navigationPath != [initialRoute] else { return }
|
||||
self.navigationPath = [initialRoute]
|
||||
}
|
||||
|
||||
private func notifyRouteChange() {
|
||||
if let directRoute {
|
||||
self.onRouteChange?(directRoute)
|
||||
return
|
||||
}
|
||||
self.onRouteChange?(self.navigationPath.last)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ extension SettingsProTab {
|
||||
title: "Notifications",
|
||||
detail: "Approval and event alert channel",
|
||||
value: self.notificationStatusText,
|
||||
color: self.notificationStatus.color)
|
||||
color: self.notificationStatusText == "Allowed" ? OpenClawBrand.ok : .secondary)
|
||||
Divider().padding(.leading, 60)
|
||||
self.diagnosticCheckRow(
|
||||
icon: "rectangle.on.rectangle",
|
||||
@@ -157,7 +157,7 @@ extension SettingsProTab {
|
||||
gatewayConnected: self.gatewayDiagnosticConnected,
|
||||
discoveredGatewayCount: self.gatewayController.gateways.count,
|
||||
talkConfigLoaded: self.gatewayDiagnosticTalkConfigLoaded,
|
||||
notificationsAllowed: self.notificationStatus == .allowed)
|
||||
notificationStatusText: self.notificationStatusText)
|
||||
self.diagnosticsIssueCount = issueCount
|
||||
self.diagnosticsLastRunText = SettingsDiagnostics.timestamp(Date())
|
||||
}
|
||||
@@ -422,41 +422,40 @@ extension SettingsProTab {
|
||||
}
|
||||
|
||||
func handleNotificationAction() {
|
||||
if self.notificationStatus.shouldOpenNotificationSettings {
|
||||
self.openNotificationSettings()
|
||||
if self.notificationStatusText == "Allowed" || self.notificationStatusText == "Not Allowed" {
|
||||
self.openSystemSettings()
|
||||
return
|
||||
}
|
||||
guard self.notificationStatus == .notSet else { return }
|
||||
|
||||
if PushBuildConfig.current.usesOpenClawHostedRelay {
|
||||
self.showNotificationRelayDisclosure = true
|
||||
return
|
||||
}
|
||||
self.requestNotificationAuthorizationFromSettings()
|
||||
}
|
||||
|
||||
func requestNotificationAuthorizationFromSettings() {
|
||||
guard !self.isRequestingNotificationAuthorization else { return }
|
||||
self.isRequestingNotificationAuthorization = true
|
||||
Task {
|
||||
let granted = await (try? UNUserNotificationCenter.current().requestAuthorization(options: [
|
||||
.alert,
|
||||
.badge,
|
||||
.sound,
|
||||
])) ?? false
|
||||
let settings = await UNUserNotificationCenter.current().notificationSettings()
|
||||
await MainActor.run {
|
||||
self.isRequestingNotificationAuthorization = false
|
||||
self.notificationStatus = SettingsNotificationStatus(settings.authorizationStatus)
|
||||
guard granted, self.notificationStatus.allowsNotifications else { return }
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
self.notificationStatusText = granted ? "Allowed" : "Not Allowed"
|
||||
self.notificationActionText = granted ? "Open System Settings" : "Open System Settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func applyNotificationStatus(_ status: UNAuthorizationStatus) {
|
||||
self.notificationStatus = SettingsNotificationStatus(status)
|
||||
switch status {
|
||||
case .authorized, .provisional, .ephemeral:
|
||||
self.notificationStatusText = "Allowed"
|
||||
self.notificationActionText = "Open System Settings"
|
||||
case .denied:
|
||||
self.notificationStatusText = "Not Allowed"
|
||||
self.notificationActionText = "Open System Settings"
|
||||
case .notDetermined:
|
||||
self.notificationStatusText = "Not Set"
|
||||
self.notificationActionText = "Request Access"
|
||||
@unknown default:
|
||||
self.notificationStatusText = "Unknown"
|
||||
self.notificationActionText = "Open System Settings"
|
||||
}
|
||||
}
|
||||
|
||||
func persistGatewayToken(_ value: String) {
|
||||
@@ -477,8 +476,8 @@ extension SettingsProTab {
|
||||
instanceId: instanceId)
|
||||
}
|
||||
|
||||
func openNotificationSettings() {
|
||||
guard let url = URL(string: UIApplication.openNotificationSettingsURLString) else { return }
|
||||
func openSystemSettings() {
|
||||
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
|
||||
@@ -676,9 +675,6 @@ extension SettingsProTab {
|
||||
if self.appModel.isAppleReviewDemoModeEnabled {
|
||||
return "Live gateway requests are disabled in demo mode."
|
||||
}
|
||||
if self.notificationsNeedAttention {
|
||||
return "Foreground approvals still appear while OpenClaw is connected."
|
||||
}
|
||||
return self.gatewayConnected ? "Gateway requests will appear here." : "Connect to the gateway."
|
||||
}
|
||||
|
||||
@@ -718,19 +714,7 @@ extension SettingsProTab {
|
||||
}
|
||||
|
||||
var approvalsDetail: String {
|
||||
if self.notificationsNeedAttention {
|
||||
return self.pendingApproval == nil ? "Notifications off" : "1 waiting, notifications off"
|
||||
}
|
||||
return self.pendingApproval == nil ? "No approvals waiting" : "1 request waiting"
|
||||
}
|
||||
|
||||
var notificationsNeedAttention: Bool {
|
||||
switch self.notificationStatus {
|
||||
case .allowed, .checking:
|
||||
false
|
||||
case .notAllowed, .notSet, .unknown:
|
||||
true
|
||||
}
|
||||
self.pendingApproval == nil ? "No approvals waiting" : "1 request waiting"
|
||||
}
|
||||
|
||||
var approvalItems: [SettingsApprovalItem] {
|
||||
@@ -793,41 +777,4 @@ extension SettingsProTab {
|
||||
case .always: "Always"
|
||||
}
|
||||
}
|
||||
|
||||
var notificationStatusText: String {
|
||||
self.notificationStatus.text
|
||||
}
|
||||
|
||||
var notificationActionText: String {
|
||||
self.notificationStatus.actionTitle
|
||||
}
|
||||
|
||||
var notificationStatusDetail: String {
|
||||
switch self.notificationStatus {
|
||||
case .checking:
|
||||
"Checking iOS notification permission."
|
||||
case .allowed:
|
||||
"OpenClaw can show approval prompts and event alerts when the app is not active."
|
||||
case .notAllowed:
|
||||
"Notifications have been denied. Enable them in iOS Settings."
|
||||
case .notSet:
|
||||
"Enable notifications to receive approval prompts and event alerts outside the app."
|
||||
case .unknown:
|
||||
"OpenClaw cannot determine the current notification permission state."
|
||||
}
|
||||
}
|
||||
|
||||
var notificationRelayDetail: String {
|
||||
if PushBuildConfig.current.usesOpenClawHostedRelay {
|
||||
return """
|
||||
This build uses OpenClaw's hosted push relay at ios-push-relay.openclaw.ai for notification \
|
||||
delivery data.
|
||||
"""
|
||||
}
|
||||
return "This build is not configured to use OpenClaw's hosted push relay."
|
||||
}
|
||||
|
||||
var notificationRelayDisclosureMessage: String {
|
||||
"Enabling this sends delivery data through OpenClaw's hosted push relay."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,57 +308,15 @@ extension SettingsProTab {
|
||||
self.detailStatusCard(
|
||||
icon: "checkmark.shield.fill",
|
||||
title: "Approvals",
|
||||
detail: self.notificationsNeedAttention
|
||||
? "Out-of-app approval alerts need notification permission."
|
||||
: (self.pendingApproval == nil ? "No gateway actions are waiting for review." :
|
||||
"Review the pending gateway action."),
|
||||
value: self.notificationsNeedAttention
|
||||
? "Alerts Off"
|
||||
: (self.pendingApproval == nil ? "clear" : "1 waiting"),
|
||||
color: self.notificationsNeedAttention ? OpenClawBrand.warn :
|
||||
(self.pendingApproval == nil ? OpenClawBrand.ok : OpenClawBrand.warn))
|
||||
|
||||
if self.notificationsNeedAttention {
|
||||
self.approvalNotificationsWarningCard
|
||||
}
|
||||
detail: self.pendingApproval == nil ? "No gateway actions are waiting for review." :
|
||||
"Review the pending gateway action.",
|
||||
value: self.pendingApproval == nil ? "clear" : "1 waiting",
|
||||
color: self.pendingApproval == nil ? OpenClawBrand.ok : OpenClawBrand.warn)
|
||||
|
||||
self.approvalsReviewCard
|
||||
}
|
||||
}
|
||||
|
||||
var approvalNotificationsWarningCard: some View {
|
||||
ProCard(radius: SettingsLayout.cardRadius) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
ProIconBadge(systemName: "bell.slash.fill", color: OpenClawBrand.warn)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Notifications are off")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
Text(
|
||||
"""
|
||||
Enable Notifications to receive approval notifications while OpenClaw is not open.
|
||||
""")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
|
||||
if self.directRoute == nil {
|
||||
Button {
|
||||
self.openNotificationsRouteFromApprovals()
|
||||
} label: {
|
||||
Label("Open Notifications", systemImage: "bell.badge")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
||||
}
|
||||
|
||||
var approvalsReviewCard: some View {
|
||||
ProCard(radius: SettingsLayout.cardRadius) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
@@ -532,9 +490,9 @@ extension SettingsProTab {
|
||||
self.detailStatusCard(
|
||||
icon: "bell",
|
||||
title: "Notifications",
|
||||
detail: self.notificationStatusDetail,
|
||||
detail: "Approvals and event alerts from OpenClaw.",
|
||||
value: self.notificationStatusText,
|
||||
color: self.notificationStatus.color)
|
||||
color: self.notificationStatusText == "Allowed" ? OpenClawBrand.ok : .secondary)
|
||||
|
||||
ProCard(radius: SettingsLayout.cardRadius) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
@@ -543,30 +501,15 @@ extension SettingsProTab {
|
||||
} label: {
|
||||
Label(
|
||||
self.notificationActionText,
|
||||
systemImage: self.notificationStatus.actionIcon)
|
||||
systemImage: self.notificationStatusText == "Allowed" ? "gear" : "bell.badge")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.small)
|
||||
.disabled(self.notificationStatus == .checking || self.isRequestingNotificationAuthorization)
|
||||
|
||||
Text(self.notificationStatusDetail)
|
||||
Text("OpenClaw uses notifications for approval prompts and mirrored event alerts.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Divider()
|
||||
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
Image(systemName: "network")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(OpenClawBrand.accent)
|
||||
.frame(width: 22, height: 22)
|
||||
Text(self.notificationRelayDetail)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Darwin
|
||||
import OpenClawKit
|
||||
import SwiftUI
|
||||
import UserNotifications
|
||||
|
||||
enum SettingsRoute: Hashable {
|
||||
case gateway
|
||||
@@ -66,87 +65,6 @@ struct SettingsApprovalRow: View {
|
||||
}
|
||||
}
|
||||
|
||||
enum SettingsNotificationStatus: Equatable {
|
||||
case checking
|
||||
case allowed
|
||||
case notAllowed
|
||||
case notSet
|
||||
case unknown
|
||||
|
||||
init(_ status: UNAuthorizationStatus) {
|
||||
switch status {
|
||||
case .authorized, .provisional, .ephemeral:
|
||||
self = .allowed
|
||||
case .denied:
|
||||
self = .notAllowed
|
||||
case .notDetermined:
|
||||
self = .notSet
|
||||
@unknown default:
|
||||
self = .unknown
|
||||
}
|
||||
}
|
||||
|
||||
var text: String {
|
||||
switch self {
|
||||
case .checking: "Checking"
|
||||
case .allowed: "Enabled"
|
||||
case .notAllowed: "Denied"
|
||||
case .notSet: "Not Enabled"
|
||||
case .unknown: "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
var actionTitle: String {
|
||||
switch self {
|
||||
case .notSet:
|
||||
"Enable Notifications"
|
||||
case .checking:
|
||||
"Checking"
|
||||
case .allowed:
|
||||
"Manage in iOS Settings"
|
||||
case .notAllowed, .unknown:
|
||||
"Open iOS Settings"
|
||||
}
|
||||
}
|
||||
|
||||
var actionIcon: String {
|
||||
switch self {
|
||||
case .allowed:
|
||||
"gear"
|
||||
case .notAllowed, .unknown:
|
||||
"gear.badge"
|
||||
case .checking:
|
||||
"hourglass"
|
||||
case .notSet:
|
||||
"bell.badge"
|
||||
}
|
||||
}
|
||||
|
||||
var color: Color {
|
||||
switch self {
|
||||
case .allowed:
|
||||
OpenClawBrand.ok
|
||||
case .notAllowed, .unknown:
|
||||
OpenClawBrand.warn
|
||||
case .checking, .notSet:
|
||||
.secondary
|
||||
}
|
||||
}
|
||||
|
||||
var shouldOpenNotificationSettings: Bool {
|
||||
switch self {
|
||||
case .allowed, .notAllowed, .unknown:
|
||||
true
|
||||
case .checking, .notSet:
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
var allowsNotifications: Bool {
|
||||
self == .allowed
|
||||
}
|
||||
}
|
||||
|
||||
enum SettingsDiagnosticIssue: String, Equatable, CaseIterable {
|
||||
case gatewayOffline
|
||||
case discoveryUnavailable
|
||||
@@ -159,13 +77,13 @@ enum SettingsDiagnostics {
|
||||
gatewayConnected: Bool,
|
||||
discoveredGatewayCount: Int,
|
||||
talkConfigLoaded: Bool,
|
||||
notificationsAllowed: Bool) -> [SettingsDiagnosticIssue]
|
||||
notificationStatusText: String) -> [SettingsDiagnosticIssue]
|
||||
{
|
||||
var issues: [SettingsDiagnosticIssue] = []
|
||||
if !gatewayConnected { issues.append(.gatewayOffline) }
|
||||
if discoveredGatewayCount == 0 { issues.append(.discoveryUnavailable) }
|
||||
if gatewayConnected, !talkConfigLoaded { issues.append(.talkConfigMissing) }
|
||||
if !notificationsAllowed { issues.append(.notificationsUnavailable) }
|
||||
if notificationStatusText != "Allowed" { issues.append(.notificationsUnavailable) }
|
||||
return issues
|
||||
}
|
||||
|
||||
@@ -173,13 +91,13 @@ enum SettingsDiagnostics {
|
||||
gatewayConnected: Bool,
|
||||
discoveredGatewayCount: Int,
|
||||
talkConfigLoaded: Bool,
|
||||
notificationsAllowed: Bool) -> Int
|
||||
notificationStatusText: String) -> Int
|
||||
{
|
||||
self.issues(
|
||||
gatewayConnected: gatewayConnected,
|
||||
discoveredGatewayCount: discoveredGatewayCount,
|
||||
talkConfigLoaded: talkConfigLoaded,
|
||||
notificationsAllowed: notificationsAllowed).count
|
||||
notificationStatusText: notificationStatusText).count
|
||||
}
|
||||
|
||||
static func timestamp(_ date: Date) -> String {
|
||||
|
||||
@@ -2,14 +2,11 @@ import SwiftUI
|
||||
|
||||
private struct ExecApprovalPromptDialogModifier: ViewModifier {
|
||||
@Environment(NodeAppModel.self) private var appModel: NodeAppModel
|
||||
let suppressedApprovalID: String?
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.overlay {
|
||||
if let prompt = self.appModel.pendingExecApprovalPrompt,
|
||||
prompt.id != self.suppressedApprovalID
|
||||
{
|
||||
if let prompt = self.appModel.pendingExecApprovalPrompt {
|
||||
ZStack {
|
||||
Color.black.opacity(0.38)
|
||||
.ignoresSafeArea()
|
||||
@@ -61,7 +58,7 @@ private struct ExecApprovalPromptCard: View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Exec approval required")
|
||||
.font(.headline)
|
||||
Text("Review this exec request before continuing. Your decision will be sent back to the gateway.")
|
||||
Text("OpenClaw opened from a notification. Review this exec request before continuing.")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
@@ -191,7 +188,7 @@ private struct ExecApprovalPromptMetadataRow: View {
|
||||
}
|
||||
|
||||
extension View {
|
||||
func execApprovalPromptDialog(suppressedApprovalID: String? = nil) -> some View {
|
||||
self.modifier(ExecApprovalPromptDialogModifier(suppressedApprovalID: suppressedApprovalID))
|
||||
func execApprovalPromptDialog() -> some View {
|
||||
self.modifier(ExecApprovalPromptDialogModifier())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ struct GatewayConnectConfig {
|
||||
lhs.clientId == rhs.clientId &&
|
||||
lhs.clientMode == rhs.clientMode &&
|
||||
lhs.clientDisplayName == rhs.clientDisplayName &&
|
||||
lhs.deviceIdentityProfile == rhs.deviceIdentityProfile &&
|
||||
lhs.includeDeviceIdentity == rhs.includeDeviceIdentity &&
|
||||
lhsScopes == rhsScopes &&
|
||||
lhsCaps == rhsCaps &&
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
private struct NotificationPermissionGuidanceDialogModifier: ViewModifier {
|
||||
@Environment(NodeAppModel.self) private var appModel: NodeAppModel
|
||||
let openNotifications: (String) -> Void
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.overlay {
|
||||
if let prompt = self.appModel.pendingNotificationPermissionGuidancePrompt {
|
||||
ZStack {
|
||||
Color.black.opacity(0.38)
|
||||
.ignoresSafeArea()
|
||||
|
||||
NotificationPermissionGuidanceCard(
|
||||
onOpenNotifications: {
|
||||
let approvalId = prompt.approvalId
|
||||
self.appModel.dismissNotificationPermissionGuidancePrompt(
|
||||
suppressFuture: false)
|
||||
self.openNotifications(approvalId)
|
||||
},
|
||||
onDismiss: {
|
||||
self.appModel.dismissNotificationPermissionGuidancePrompt(
|
||||
suppressFuture: false)
|
||||
},
|
||||
onSuppressFuture: {
|
||||
self.appModel.dismissNotificationPermissionGuidancePrompt(
|
||||
suppressFuture: true)
|
||||
})
|
||||
.padding(.horizontal, 20)
|
||||
.frame(maxWidth: 460)
|
||||
.transition(.scale(scale: 0.98).combined(with: .opacity))
|
||||
}
|
||||
.zIndex(2)
|
||||
.id(prompt.id)
|
||||
}
|
||||
}
|
||||
.animation(
|
||||
.easeInOut(duration: 0.18),
|
||||
value: self.appModel.pendingNotificationPermissionGuidancePrompt?.id)
|
||||
}
|
||||
}
|
||||
|
||||
private struct NotificationPermissionGuidanceCard: View {
|
||||
let onOpenNotifications: () -> Void
|
||||
let onDismiss: () -> Void
|
||||
let onSuppressFuture: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Notifications are off")
|
||||
.font(.headline)
|
||||
Text(
|
||||
"""
|
||||
Exec approvals can only be reviewed while OpenClaw is open and connected.
|
||||
|
||||
Enable Notifications to receive approval notifications while OpenClaw is not open.
|
||||
""")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
VStack(spacing: 10) {
|
||||
Button {
|
||||
self.onOpenNotifications()
|
||||
} label: {
|
||||
Text("Open Notifications Settings")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
|
||||
Button(role: .cancel) {
|
||||
self.onDismiss()
|
||||
} label: {
|
||||
Text("Not Now")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
||||
Button {
|
||||
self.onSuppressFuture()
|
||||
} label: {
|
||||
Text("Don't show again")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
.controlSize(.large)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.padding(18)
|
||||
.proPanelSurface(tint: OpenClawBrand.warn, radius: 20, isProminent: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func notificationPermissionGuidanceDialog(openNotifications: @escaping (String) -> Void) -> some View {
|
||||
self.modifier(NotificationPermissionGuidanceDialogModifier(openNotifications: openNotifications))
|
||||
}
|
||||
}
|
||||
@@ -78,8 +78,6 @@
|
||||
<string>OpenClaw uses on-device speech recognition for talk mode and voice wake.</string>
|
||||
<key>NSSupportsLiveActivities</key>
|
||||
<true/>
|
||||
<key>OpenClawAppGroupIdentifier</key>
|
||||
<string>$(OPENCLAW_APP_GROUP_ID)</string>
|
||||
<key>OpenClawCanonicalVersion</key>
|
||||
<string>$(OPENCLAW_IOS_VERSION)</string>
|
||||
<key>OpenClawPushAPNsEnvironment</key>
|
||||
|
||||
@@ -23,10 +23,6 @@ private struct WatchChatPreview {
|
||||
var statusText: String?
|
||||
}
|
||||
|
||||
private struct ExecApprovalGatewayEventPayload: Decodable {
|
||||
var id: String
|
||||
}
|
||||
|
||||
/// Ensures notification requests return promptly even if the system prompt blocks.
|
||||
private final class NotificationInvokeLatch<T: Sendable>: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
@@ -87,11 +83,6 @@ final class NodeAppModel {
|
||||
}
|
||||
}
|
||||
|
||||
struct NotificationPermissionGuidancePrompt: Identifiable, Equatable {
|
||||
let id = UUID()
|
||||
let approvalId: String
|
||||
}
|
||||
|
||||
private enum ExecApprovalResolutionOutcome {
|
||||
case resolved
|
||||
case stale
|
||||
@@ -105,8 +96,6 @@ final class NodeAppModel {
|
||||
}
|
||||
|
||||
private let deepLinkLogger = Logger(subsystem: "ai.openclawfoundation.app", category: "DeepLink")
|
||||
private nonisolated static let execApprovalNotificationGuidanceSuppressedKey =
|
||||
"notifications.execApprovalGuidance.suppressed"
|
||||
private let pushWakeLogger = Logger(subsystem: "ai.openclawfoundation.app", category: "PushWake")
|
||||
private let pendingActionLogger = Logger(subsystem: "ai.openclawfoundation.app", category: "PendingAction")
|
||||
private let locationWakeLogger = Logger(subsystem: "ai.openclawfoundation.app", category: "LocationWake")
|
||||
@@ -167,7 +156,6 @@ final class NodeAppModel {
|
||||
private(set) var pendingExecApprovalPromptResolving: Bool = false
|
||||
private(set) var pendingExecApprovalPromptErrorText: String?
|
||||
private var pendingExecApprovalPromptRequestGeneration: Int = 0
|
||||
private(set) var pendingNotificationPermissionGuidancePrompt: NotificationPermissionGuidancePrompt?
|
||||
private var queuedAgentDeepLinkPrompt: AgentDeepLinkPrompt?
|
||||
private var lastAgentDeepLinkPromptAt: Date = .distantPast
|
||||
@ObservationIgnored private var queuedAgentDeepLinkPromptTask: Task<Void, Never>?
|
||||
@@ -907,50 +895,26 @@ final class NodeAppModel {
|
||||
for await evt in stream {
|
||||
if Task.isCancelled { return }
|
||||
guard let payload = evt.payload else { continue }
|
||||
await self.handleOperatorGatewayServerEvent(evt)
|
||||
switch evt.event {
|
||||
case "voicewake.changed":
|
||||
struct Payload: Decodable { var triggers: [String] }
|
||||
guard let decoded = try? GatewayPayloadDecoding.decode(payload, as: Payload.self) else { continue }
|
||||
let triggers = VoiceWakePreferences.sanitizeTriggerWords(decoded.triggers)
|
||||
VoiceWakePreferences.saveTriggerWords(triggers)
|
||||
case "talk.mode":
|
||||
struct Payload: Decodable {
|
||||
var enabled: Bool
|
||||
var phase: String?
|
||||
}
|
||||
guard let decoded = try? GatewayPayloadDecoding.decode(payload, as: Payload.self) else { continue }
|
||||
self.applyTalkModeSync(enabled: decoded.enabled, phase: decoded.phase)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleOperatorGatewayServerEvent(_ evt: EventFrame) async {
|
||||
guard let payload = evt.payload else { return }
|
||||
switch evt.event {
|
||||
case "voicewake.changed":
|
||||
struct Payload: Decodable { var triggers: [String] }
|
||||
guard let decoded = try? GatewayPayloadDecoding.decode(payload, as: Payload.self) else { return }
|
||||
let triggers = VoiceWakePreferences.sanitizeTriggerWords(decoded.triggers)
|
||||
VoiceWakePreferences.saveTriggerWords(triggers)
|
||||
case "talk.mode":
|
||||
struct Payload: Decodable {
|
||||
var enabled: Bool
|
||||
var phase: String?
|
||||
}
|
||||
guard let decoded = try? GatewayPayloadDecoding.decode(payload, as: Payload.self) else { return }
|
||||
self.applyTalkModeSync(enabled: decoded.enabled, phase: decoded.phase)
|
||||
case ExecApprovalNotificationBridge.requestedKind:
|
||||
guard let approvalId = Self.execApprovalEventID(from: payload) else { return }
|
||||
await self.presentNotificationPermissionGuidanceForExecApprovalIfNeeded(approvalId: approvalId)
|
||||
await self.presentExecApprovalNotificationPrompt(
|
||||
ExecApprovalNotificationPrompt(approvalId: approvalId))
|
||||
case ExecApprovalNotificationBridge.resolvedKind:
|
||||
guard let approvalId = Self.execApprovalEventID(from: payload) else { return }
|
||||
await self.handleExecApprovalResolvedRemotePush(approvalId: approvalId)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated static func execApprovalEventID(from payload: AnyCodable) -> String? {
|
||||
guard let decoded = try? GatewayPayloadDecoding.decode(
|
||||
payload,
|
||||
as: ExecApprovalGatewayEventPayload.self)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
let approvalId = decoded.id.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return approvalId.isEmpty ? nil : approvalId
|
||||
}
|
||||
|
||||
private func applyTalkModeSync(enabled: Bool, phase: String?) {
|
||||
_ = phase
|
||||
guard self.talkMode.isEnabled != enabled else { return }
|
||||
@@ -1368,8 +1332,8 @@ final class NodeAppModel {
|
||||
error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: empty notification"))
|
||||
}
|
||||
|
||||
let status = await self.notificationAuthorizationStatus()
|
||||
guard Self.isNotificationAuthorizationAllowed(status) else {
|
||||
let finalStatus = await self.requestNotificationAuthorizationIfNeeded()
|
||||
guard finalStatus == .authorized || finalStatus == .provisional || finalStatus == .ephemeral else {
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
@@ -1421,18 +1385,9 @@ final class NodeAppModel {
|
||||
error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: empty chat.push text"))
|
||||
}
|
||||
|
||||
let shouldSpeak = params.speak ?? true
|
||||
let status = await self.notificationAuthorizationStatus()
|
||||
let notificationsAllowed = Self.isNotificationAuthorizationAllowed(status)
|
||||
if !notificationsAllowed, !shouldSpeak {
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: OpenClawNodeError(code: .unavailable, message: "NOT_AUTHORIZED: notifications"))
|
||||
}
|
||||
|
||||
let finalStatus = await self.requestNotificationAuthorizationIfNeeded()
|
||||
let messageId = UUID().uuidString
|
||||
if notificationsAllowed {
|
||||
if finalStatus == .authorized || finalStatus == .provisional || finalStatus == .ephemeral {
|
||||
let addResult = await self.runNotificationCall(timeoutSeconds: 2.0) { [notificationCenter] in
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = "OpenClaw"
|
||||
@@ -1453,7 +1408,7 @@ final class NodeAppModel {
|
||||
}
|
||||
}
|
||||
|
||||
if shouldSpeak {
|
||||
if params.speak ?? true {
|
||||
let toSpeak = text
|
||||
Task { @MainActor in
|
||||
try? await TalkSystemSpeechSynthesizer.shared.speak(text: toSpeak)
|
||||
@@ -1465,6 +1420,26 @@ final class NodeAppModel {
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json)
|
||||
}
|
||||
|
||||
private func requestNotificationAuthorizationIfNeeded() async -> NotificationAuthorizationStatus {
|
||||
let status = await self.notificationAuthorizationStatus()
|
||||
guard status == .notDetermined else { return status }
|
||||
|
||||
// Avoid hanging invoke requests if the permission prompt is never answered.
|
||||
_ = await self.runNotificationCall(timeoutSeconds: 2.0) { [notificationCenter] in
|
||||
_ = try await notificationCenter.requestAuthorization(options: [.alert, .sound, .badge])
|
||||
}
|
||||
|
||||
let updatedStatus = await self.notificationAuthorizationStatus()
|
||||
if Self.isNotificationAuthorizationAllowed(updatedStatus) {
|
||||
// Refresh APNs registration immediately after the first permission grant so the
|
||||
// gateway can receive a push registration without requiring an app relaunch.
|
||||
await MainActor.run {
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
}
|
||||
}
|
||||
return updatedStatus
|
||||
}
|
||||
|
||||
private func notificationAuthorizationStatus() async -> NotificationAuthorizationStatus {
|
||||
let result = await self.runNotificationCall(timeoutSeconds: 1.5) { [notificationCenter] in
|
||||
await notificationCenter.authorizationStatus()
|
||||
@@ -1488,29 +1463,6 @@ final class NodeAppModel {
|
||||
}
|
||||
}
|
||||
|
||||
private func presentNotificationPermissionGuidanceForExecApprovalIfNeeded(approvalId: String) async {
|
||||
guard !self.execApprovalNotificationGuidanceSuppressed else { return }
|
||||
let status = await self.notificationAuthorizationStatus()
|
||||
guard !Self.isNotificationAuthorizationAllowed(status) else { return }
|
||||
self.pendingNotificationPermissionGuidancePrompt =
|
||||
NotificationPermissionGuidancePrompt(approvalId: approvalId)
|
||||
}
|
||||
|
||||
var execApprovalNotificationGuidanceSuppressed: Bool {
|
||||
UserDefaults.standard.bool(forKey: Self.execApprovalNotificationGuidanceSuppressedKey)
|
||||
}
|
||||
|
||||
func dismissNotificationPermissionGuidancePrompt(suppressFuture: Bool) {
|
||||
if suppressFuture {
|
||||
UserDefaults.standard.set(true, forKey: Self.execApprovalNotificationGuidanceSuppressedKey)
|
||||
}
|
||||
self.pendingNotificationPermissionGuidancePrompt = nil
|
||||
}
|
||||
|
||||
func resetExecApprovalNotificationGuidanceSuppression() {
|
||||
UserDefaults.standard.removeObject(forKey: Self.execApprovalNotificationGuidanceSuppressedKey)
|
||||
}
|
||||
|
||||
private func runNotificationCall<T: Sendable>(
|
||||
timeoutSeconds: Double,
|
||||
operation: @escaping @Sendable () async throws -> T) async -> Result<T, NotificationCallError>
|
||||
@@ -2383,6 +2335,10 @@ extension NodeAppModel {
|
||||
nodeOptions: nodeOptions,
|
||||
sessionBox: sessionBox)
|
||||
}
|
||||
|
||||
// QR bootstrap onboarding should surface the system notification permission
|
||||
// prompt immediately so visible APNs alerts work without a second manual step.
|
||||
_ = await self.requestNotificationAuthorizationIfNeeded()
|
||||
}
|
||||
|
||||
private func refreshBackgroundReconnectSuppressionIfNeeded(source: String) {
|
||||
@@ -3960,15 +3916,11 @@ extension NodeAppModel {
|
||||
let hadWatchPrompt = self.watchExecApprovalPromptsByID[normalizedApprovalID] != nil
|
||||
let hadPendingPrompt = self.pendingExecApprovalPrompt?.id == normalizedApprovalID
|
||||
let hadPendingRecoveryID = self.pendingWatchExecApprovalRecoveryIDs.contains(normalizedApprovalID)
|
||||
let hadGuidancePrompt = self.pendingNotificationPermissionGuidancePrompt?.approvalId == normalizedApprovalID
|
||||
let hadApprovalSurface = hadWatchPrompt || hadPendingPrompt || hadPendingRecoveryID
|
||||
guard hadApprovalSurface || hadGuidancePrompt else {
|
||||
guard hadWatchPrompt || hadPendingPrompt || hadPendingRecoveryID else {
|
||||
return
|
||||
}
|
||||
|
||||
if hadApprovalSurface {
|
||||
await self.publishWatchExecApprovalExpired(approvalId: normalizedApprovalID, reason: .resolved)
|
||||
}
|
||||
await self.publishWatchExecApprovalExpired(approvalId: normalizedApprovalID, reason: .resolved)
|
||||
self.clearPendingExecApprovalPromptIfMatches(normalizedApprovalID)
|
||||
}
|
||||
|
||||
@@ -4449,17 +4401,10 @@ extension NodeAppModel {
|
||||
|
||||
private func clearPendingExecApprovalPromptIfMatches(_ approvalId: String) {
|
||||
let normalizedApprovalID = approvalId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
self.clearNotificationPermissionGuidancePromptIfMatches(normalizedApprovalID)
|
||||
guard self.pendingExecApprovalPrompt?.id == normalizedApprovalID else { return }
|
||||
self.dismissPendingExecApprovalPrompt()
|
||||
}
|
||||
|
||||
private func clearNotificationPermissionGuidancePromptIfMatches(_ approvalId: String) {
|
||||
let normalizedApprovalID = approvalId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard self.pendingNotificationPermissionGuidancePrompt?.approvalId == normalizedApprovalID else { return }
|
||||
self.pendingNotificationPermissionGuidancePrompt = nil
|
||||
}
|
||||
|
||||
private nonisolated static func isApprovalNotificationStaleError(_ error: Error) -> Bool {
|
||||
guard let gatewayError = error as? GatewayResponseError else { return false }
|
||||
if gatewayError.code != "INVALID_REQUEST" {
|
||||
@@ -5165,20 +5110,6 @@ extension NodeAppModel {
|
||||
self.pendingExecApprovalPrompt
|
||||
}
|
||||
|
||||
func _test_pendingNotificationPermissionGuidancePrompt() -> NotificationPermissionGuidancePrompt? {
|
||||
self.pendingNotificationPermissionGuidancePrompt
|
||||
}
|
||||
|
||||
func _debug_presentNotificationPermissionGuidancePromptForScreenshot() {
|
||||
self.resetExecApprovalNotificationGuidanceSuppression()
|
||||
self.pendingNotificationPermissionGuidancePrompt =
|
||||
NotificationPermissionGuidancePrompt(approvalId: "screenshot-exec-approval")
|
||||
}
|
||||
|
||||
func _test_resetExecApprovalNotificationGuidanceSuppression() {
|
||||
self.resetExecApprovalNotificationGuidanceSuppression()
|
||||
}
|
||||
|
||||
func _test_recordPendingWatchExecApprovalRecoveryID(_ approvalId: String) {
|
||||
self.appendPendingWatchExecApprovalRecoveryID(approvalId)
|
||||
}
|
||||
@@ -5208,14 +5139,6 @@ extension NodeAppModel {
|
||||
isBackgrounded: isBackgrounded)
|
||||
}
|
||||
|
||||
nonisolated static func _test_execApprovalEventID(from payload: AnyCodable) -> String? {
|
||||
self.execApprovalEventID(from: payload)
|
||||
}
|
||||
|
||||
func _test_handleOperatorGatewayServerEvent(_ event: EventFrame) async {
|
||||
await self.handleOperatorGatewayServerEvent(event)
|
||||
}
|
||||
|
||||
nonisolated static func _test_watchExecApprovalIDsNeedingFetch(
|
||||
candidateIDs: [String],
|
||||
cachedApprovalIDs: [String]) -> [String]
|
||||
@@ -5312,6 +5235,24 @@ extension NodeAppModel {
|
||||
func _test_restartGatewaySessionsAfterForegroundStaleConnection() async {
|
||||
await self.restartGatewaySessionsAfterForegroundStaleConnection()
|
||||
}
|
||||
|
||||
func _test_handleSuccessfulBootstrapGatewayOnboarding() async {
|
||||
await self.handleSuccessfulBootstrapGatewayOnboarding(
|
||||
url: URL(string: "wss://gateway.example")!,
|
||||
stableID: "test-gateway",
|
||||
token: nil,
|
||||
password: nil,
|
||||
nodeOptions: GatewayConnectOptions(
|
||||
role: "node",
|
||||
scopes: [],
|
||||
caps: [],
|
||||
commands: [],
|
||||
permissions: [:],
|
||||
clientId: "openclaw-ios",
|
||||
clientMode: "node",
|
||||
clientDisplayName: nil),
|
||||
sessionBox: nil)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// swiftlint:enable type_body_length file_length
|
||||
|
||||
@@ -18,7 +18,6 @@ enum GatewayOnboardingReset {
|
||||
let deviceId = DeviceIdentityStore.loadOrCreate().deviceId
|
||||
DeviceAuthStore.clearToken(deviceId: deviceId, role: "node")
|
||||
DeviceAuthStore.clearToken(deviceId: deviceId, role: "operator")
|
||||
DeviceAuthStore.clearAll(profile: .shareExtension)
|
||||
|
||||
GatewaySettingsStore.clearLastGatewayConnection(defaults: defaults)
|
||||
GatewaySettingsStore.clearPreferredGatewayStableID(defaults: defaults)
|
||||
|
||||
@@ -4,9 +4,5 @@
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>$(OPENCLAW_APNS_ENTITLEMENT_ENVIRONMENT)</string>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>$(OPENCLAW_APP_GROUP_ID)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -409,7 +409,7 @@ enum WatchPromptNotificationBridge {
|
||||
let title = params.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let body = params.body.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !title.isEmpty || !body.isEmpty else { return }
|
||||
guard await self.isNotificationAuthorizationAllowed() else { return }
|
||||
guard await self.requestNotificationAuthorizationIfNeeded() else { return }
|
||||
|
||||
let normalizedActions = (params.actions ?? []).compactMap { action -> OpenClawWatchAction? in
|
||||
let id = action.id.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
@@ -516,10 +516,29 @@ enum WatchPromptNotificationBridge {
|
||||
}
|
||||
}
|
||||
|
||||
private static func isNotificationAuthorizationAllowed() async -> Bool {
|
||||
private static func requestNotificationAuthorizationIfNeeded() async -> Bool {
|
||||
let center = UNUserNotificationCenter.current()
|
||||
let status = await self.notificationAuthorizationStatus(center: center)
|
||||
return self.isAuthorizationStatusAllowed(status)
|
||||
switch status {
|
||||
case .authorized, .provisional, .ephemeral:
|
||||
return true
|
||||
case .notDetermined:
|
||||
let granted = await (try? center.requestAuthorization(options: [.alert, .sound, .badge])) ?? false
|
||||
if !granted { return false }
|
||||
let updatedStatus = await self.notificationAuthorizationStatus(center: center)
|
||||
if self.isAuthorizationStatusAllowed(updatedStatus) {
|
||||
// Refresh APNs registration immediately after the first permission grant so the
|
||||
// gateway can receive a push registration without requiring an app relaunch.
|
||||
await MainActor.run {
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
}
|
||||
}
|
||||
return self.isAuthorizationStatusAllowed(updatedStatus)
|
||||
case .denied:
|
||||
return false
|
||||
@unknown default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private static func isAuthorizationStatusAllowed(_ status: UNAuthorizationStatus) -> Bool {
|
||||
@@ -616,9 +635,6 @@ struct OpenClawApp: App {
|
||||
UserDefaults.standard.set(true, forKey: "gateway.hasConnectedOnce")
|
||||
UserDefaults.standard.set(true, forKey: "onboarding.quickSetupDismissed")
|
||||
appModel.enterScreenshotFixtureMode()
|
||||
if Self.screenshotNotificationGuidanceEnabled {
|
||||
appModel._debug_presentNotificationPermissionGuidancePromptForScreenshot()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
OpenClawAppModelRegistry.appModel = appModel
|
||||
@@ -670,14 +686,6 @@ struct OpenClawApp: App {
|
||||
#endif
|
||||
}
|
||||
|
||||
private static var screenshotNotificationGuidanceEnabled: Bool {
|
||||
#if DEBUG
|
||||
ProcessInfo.processInfo.arguments.contains("--openclaw-screenshot-notification-guidance")
|
||||
#else
|
||||
false
|
||||
#endif
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func applyAppearancePreference() {
|
||||
let style = self.appearancePreference.userInterfaceStyle
|
||||
|
||||
@@ -22,20 +22,6 @@ struct PushBuildConfig {
|
||||
let apnsEnvironment: PushAPNsEnvironment
|
||||
|
||||
static let current = PushBuildConfig()
|
||||
static let openClawHostedRelayHost = "ios-push-relay.openclaw.ai"
|
||||
|
||||
var usesOpenClawHostedRelay: Bool {
|
||||
guard self.transport == .relay, self.distribution == .official else { return false }
|
||||
guard let relayBaseURL = self.relayBaseURL,
|
||||
let components = URLComponents(url: relayBaseURL, resolvingAgainstBaseURL: false)
|
||||
else {
|
||||
return false
|
||||
}
|
||||
return components.scheme?.lowercased() == "https"
|
||||
&& components.host?.lowercased() == Self.openClawHostedRelayHost
|
||||
&& components.user == nil
|
||||
&& components.password == nil
|
||||
}
|
||||
|
||||
init(bundle: Bundle = .main) {
|
||||
self.transport = Self.readEnum(
|
||||
|
||||
@@ -24,8 +24,6 @@ struct RootTabs: View {
|
||||
AppAppearancePreference.system.rawValue
|
||||
@State private var selectedTab: AppTab = Self.initialTab
|
||||
@State private var selectedSidebarDestination: SidebarDestination = Self.initialSidebarDestination
|
||||
@State private var selectedSettingsRoute: SettingsRoute? = Self.initialSidebarDestination.settingsRoute
|
||||
@State private var selectedSettingsRouteRequestID: Int = 0
|
||||
@State private var isSidebarVisible: Bool = Self.initialSidebarVisibility ?? false
|
||||
@State private var sidebarVisibilityUserOverridden: Bool = Self.initialSidebarVisibility != nil
|
||||
@State private var isSidebarDrawerLayout: Bool = false
|
||||
@@ -41,7 +39,6 @@ struct RootTabs: View {
|
||||
@State private var didApplyInitialAppearance: Bool = false
|
||||
@State private var didApplyInitialChatSession: Bool = false
|
||||
@State private var handledGatewaySetupRequestID: Int = 0
|
||||
@State private var suppressedExecApprovalPromptIDForNotificationSettings: String?
|
||||
|
||||
private static var initialTab: AppTab {
|
||||
let arguments = ProcessInfo.processInfo.arguments
|
||||
@@ -164,10 +161,8 @@ struct RootTabs: View {
|
||||
.tabItem { Label("Agent", systemImage: "person.2.fill") }
|
||||
.tag(AppTab.agent)
|
||||
|
||||
SettingsProTab(
|
||||
initialRoute: self.selectedSettingsRoute,
|
||||
onRouteChange: self.handleSettingsRouteChange)
|
||||
.id(self.settingsTabViewID)
|
||||
SettingsProTab(initialRoute: self.selectedSidebarDestination.settingsRoute)
|
||||
.id(self.selectedSidebarDestination.settingsRoute.map { "\($0)" } ?? "settings")
|
||||
.tabItem { Label("Settings", systemImage: "gearshape.fill") }
|
||||
.tag(AppTab.settings)
|
||||
}
|
||||
@@ -240,7 +235,7 @@ struct RootTabs: View {
|
||||
|
||||
private var sidebarDetailShell: some View {
|
||||
self.sidebarDetail
|
||||
.id(self.sidebarDetailShellID)
|
||||
.id(self.selectedSidebarDestination.id)
|
||||
}
|
||||
|
||||
private var sidebarColumn: some View {
|
||||
@@ -468,21 +463,11 @@ struct RootTabs: View {
|
||||
headerLeadingAction: self.sidebarHeaderLeadingAction,
|
||||
gatewayAction: { self.selectSidebarDestination(.gateway) })
|
||||
case .settings:
|
||||
if let selectedSettingsRoute {
|
||||
SettingsProTab(
|
||||
directRoute: selectedSettingsRoute,
|
||||
headerLeadingAction: self.sidebarHeaderLeadingAction,
|
||||
onRouteChange: self.handleSettingsRouteChange)
|
||||
} else {
|
||||
SettingsProTab(
|
||||
headerLeadingAction: self.sidebarHeaderLeadingAction,
|
||||
onRouteChange: self.handleSettingsRouteChange)
|
||||
}
|
||||
SettingsProTab(headerLeadingAction: self.sidebarHeaderLeadingAction)
|
||||
case .gateway:
|
||||
SettingsProTab(
|
||||
directRoute: self.selectedSettingsRoute ?? self.selectedSidebarDestination.settingsRoute ?? .gateway,
|
||||
headerLeadingAction: self.sidebarHeaderLeadingAction,
|
||||
onRouteChange: self.handleSettingsRouteChange)
|
||||
directRoute: self.selectedSidebarDestination.settingsRoute ?? .gateway,
|
||||
headerLeadingAction: self.sidebarHeaderLeadingAction)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,21 +492,6 @@ struct RootTabs: View {
|
||||
return UIDevice.current.userInterfaceIdiom
|
||||
}
|
||||
|
||||
private var sidebarDetailShellID: String {
|
||||
let routeID = self.selectedSettingsRoute.map { "\($0)" } ?? "root"
|
||||
return "\(self.selectedSidebarDestination.id):\(routeID):\(self.selectedSettingsRouteRequestID)"
|
||||
}
|
||||
|
||||
private var settingsTabViewID: String {
|
||||
let routeID = self.selectedSettingsRoute.map { "\($0)" } ?? "settings"
|
||||
return "\(routeID):\(self.selectedSettingsRouteRequestID)"
|
||||
}
|
||||
|
||||
private var activeExecApprovalPromptSuppressionID: String? {
|
||||
guard self.selectedTab == .settings, self.selectedSettingsRoute == .notifications else { return nil }
|
||||
return self.suppressedExecApprovalPromptIDForNotificationSettings
|
||||
}
|
||||
|
||||
private var shouldCollapseSidebarAfterSelection: Bool {
|
||||
Self.shouldCollapseSidebarAfterSelection(
|
||||
layoutMode: self.isSidebarDrawerLayout ? .drawer : .split)
|
||||
@@ -735,11 +705,6 @@ struct RootTabs: View {
|
||||
.onChange(of: self.appModel.gatewaySetupRequestID) { _, _ in
|
||||
self.maybeOpenSettingsForGatewaySetup()
|
||||
}
|
||||
.onChange(of: self.appModel.pendingExecApprovalPrompt?.id) { _, newValue in
|
||||
if newValue != self.suppressedExecApprovalPromptIDForNotificationSettings {
|
||||
self.suppressedExecApprovalPromptIDForNotificationSettings = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func rootPresentation(_ content: some View) -> some View {
|
||||
@@ -777,12 +742,7 @@ struct RootTabs: View {
|
||||
}
|
||||
.gatewayTrustPromptAlert()
|
||||
.deepLinkAgentPromptAlert()
|
||||
.execApprovalPromptDialog(
|
||||
suppressedApprovalID: self.activeExecApprovalPromptSuppressionID)
|
||||
.notificationPermissionGuidanceDialog(openNotifications: { approvalId in
|
||||
self.suppressedExecApprovalPromptIDForNotificationSettings = approvalId
|
||||
self.selectSettingsRoute(.notifications)
|
||||
})
|
||||
.execApprovalPromptDialog()
|
||||
}
|
||||
|
||||
private var appearancePreference: AppAppearancePreference {
|
||||
@@ -914,15 +874,9 @@ struct RootTabs: View {
|
||||
private func homeCanvasName(for agent: AgentSummary) -> String {
|
||||
self.normalized(agent.name) ?? agent.id
|
||||
}
|
||||
}
|
||||
|
||||
extension RootTabs {
|
||||
private func selectSidebarDestination(_ destination: SidebarDestination) {
|
||||
if destination.settingsRoute != .notifications {
|
||||
self.suppressedExecApprovalPromptIDForNotificationSettings = nil
|
||||
}
|
||||
self.selectedSidebarDestination = destination
|
||||
self.selectedSettingsRoute = destination.settingsRoute
|
||||
self.selectedTab = destination.appTab
|
||||
guard self.usesSidebarTabs, self.shouldCollapseSidebarAfterSelection else { return }
|
||||
withAnimation(.easeInOut(duration: 0.22)) {
|
||||
@@ -930,31 +884,6 @@ extension RootTabs {
|
||||
}
|
||||
}
|
||||
|
||||
private func selectSettingsRoute(_ route: SettingsRoute) {
|
||||
if route != .notifications {
|
||||
self.suppressedExecApprovalPromptIDForNotificationSettings = nil
|
||||
}
|
||||
self.selectedSettingsRoute = route
|
||||
self.selectedSettingsRouteRequestID &+= 1
|
||||
self.selectedSidebarDestination = .settings
|
||||
self.selectedTab = .settings
|
||||
guard self.usesSidebarTabs, self.shouldCollapseSidebarAfterSelection else { return }
|
||||
withAnimation(.easeInOut(duration: 0.22)) {
|
||||
self.setSidebarVisible(false)
|
||||
}
|
||||
}
|
||||
|
||||
private func handleSettingsRouteChange(_ route: SettingsRoute?) {
|
||||
guard route != .notifications else { return }
|
||||
if route == nil {
|
||||
self.selectedSettingsRoute = nil
|
||||
if self.selectedTab == .settings {
|
||||
self.selectedSidebarDestination = .settings
|
||||
}
|
||||
}
|
||||
self.suppressedExecApprovalPromptIDForNotificationSettings = nil
|
||||
}
|
||||
|
||||
private func showSidebar() {
|
||||
self.sidebarVisibilityUserOverridden = true
|
||||
withAnimation(.easeInOut(duration: 0.22)) {
|
||||
|
||||
@@ -16,6 +16,7 @@ enum NotificationAuthorizationStatus {
|
||||
|
||||
protocol NotificationCentering: Sendable {
|
||||
func authorizationStatus() async -> NotificationAuthorizationStatus
|
||||
func requestAuthorization(options: UNAuthorizationOptions) async throws -> Bool
|
||||
func add(_ request: UNNotificationRequest) async throws
|
||||
func removePendingNotificationRequests(withIdentifiers identifiers: [String]) async
|
||||
func removeDeliveredNotifications(withIdentifiers identifiers: [String]) async
|
||||
@@ -47,6 +48,10 @@ struct LiveNotificationCenter: NotificationCentering, @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
func requestAuthorization(options: UNAuthorizationOptions) async throws -> Bool {
|
||||
try await self.center.requestAuthorization(options: options)
|
||||
}
|
||||
|
||||
func add(_ request: UNNotificationRequest) async throws {
|
||||
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
|
||||
self.center.add(request) { error in
|
||||
|
||||
@@ -50,7 +50,6 @@ Sources/Gateway/GatewayDiscoveryModel.swift
|
||||
Sources/Gateway/GatewayHealthMonitor.swift
|
||||
Sources/Gateway/GatewayProblemView.swift
|
||||
Sources/Gateway/GatewayQuickSetupSheet.swift
|
||||
Sources/Gateway/NotificationPermissionGuidanceDialog.swift
|
||||
Sources/Gateway/GatewayServiceResolver.swift
|
||||
Sources/Gateway/GatewaySettingsStore.swift
|
||||
Sources/Gateway/GatewayTrustPromptAlert.swift
|
||||
@@ -110,10 +109,10 @@ Sources/Voice/VoiceWakePreferences.swift
|
||||
ShareExtension/ShareViewController.swift
|
||||
ActivityWidget/OpenClawActivityWidgetBundle.swift
|
||||
ActivityWidget/OpenClawLiveActivity.swift
|
||||
WatchApp/Sources/OpenClawWatchApp.swift
|
||||
WatchApp/Sources/WatchConnectivityReceiver.swift
|
||||
WatchApp/Sources/WatchInboxStore.swift
|
||||
WatchApp/Sources/WatchInboxView.swift
|
||||
WatchExtension/Sources/OpenClawWatchApp.swift
|
||||
WatchExtension/Sources/WatchConnectivityReceiver.swift
|
||||
WatchExtension/Sources/WatchInboxStore.swift
|
||||
WatchExtension/Sources/WatchInboxView.swift
|
||||
../shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift
|
||||
../shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownRenderer.swift
|
||||
../shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownPreprocessor.swift
|
||||
|
||||
@@ -14,6 +14,10 @@ private final class MockNotificationCenter: NotificationCentering, @unchecked Se
|
||||
self.authorization
|
||||
}
|
||||
|
||||
func requestAuthorization(options _: UNAuthorizationOptions) async throws -> Bool {
|
||||
true
|
||||
}
|
||||
|
||||
func add(_ request: UNNotificationRequest) async throws {
|
||||
self.addedRequests.append(request)
|
||||
}
|
||||
|
||||
@@ -200,16 +200,25 @@ private final class MockWatchMessagingService: @preconcurrency WatchMessagingSer
|
||||
|
||||
private final class MockBootstrapNotificationCenter: NotificationCentering, @unchecked Sendable {
|
||||
var status: NotificationAuthorizationStatus = .notDetermined
|
||||
var addCalls = 0
|
||||
var requestAuthorizationResult = false
|
||||
var requestAuthorizationCalls = 0
|
||||
|
||||
func authorizationStatus() async -> NotificationAuthorizationStatus {
|
||||
self.status
|
||||
}
|
||||
|
||||
func add(_: UNNotificationRequest) async throws {
|
||||
self.addCalls += 1
|
||||
func requestAuthorization(options _: UNAuthorizationOptions) async throws -> Bool {
|
||||
self.requestAuthorizationCalls += 1
|
||||
if self.requestAuthorizationResult {
|
||||
self.status = .authorized
|
||||
} else {
|
||||
self.status = .denied
|
||||
}
|
||||
return self.requestAuthorizationResult
|
||||
}
|
||||
|
||||
func add(_: UNNotificationRequest) async throws {}
|
||||
|
||||
func removePendingNotificationRequests(withIdentifiers _: [String]) async {}
|
||||
|
||||
func removeDeliveredNotifications(withIdentifiers _: [String]) async {}
|
||||
@@ -1151,35 +1160,6 @@ private final class MockBootstrapNotificationCenter: NotificationCentering, @unc
|
||||
isBackgrounded: false))
|
||||
}
|
||||
|
||||
@Test func execApprovalEventIDDecodesGatewayPayload() {
|
||||
#expect(NodeAppModel._test_execApprovalEventID(from: AnyCodable(["id": " approval-1 "])) == "approval-1")
|
||||
#expect(NodeAppModel._test_execApprovalEventID(from: AnyCodable(["id": " "])) == nil)
|
||||
#expect(NodeAppModel._test_execApprovalEventID(from: AnyCodable(["other": "approval-1"])) == nil)
|
||||
}
|
||||
|
||||
@Test @MainActor func operatorGatewayResolvedEventClearsPendingApprovalPrompt() async throws {
|
||||
let appModel = NodeAppModel()
|
||||
try appModel._test_presentExecApprovalPrompt(
|
||||
#require(
|
||||
NodeAppModel._test_makeExecApprovalPrompt(
|
||||
id: "approval-event-resolved",
|
||||
commandText: "echo clear",
|
||||
allowedDecisions: ["allow-once", "deny"],
|
||||
host: "gateway",
|
||||
nodeId: nil,
|
||||
agentId: nil,
|
||||
expiresAtMs: Int(Date().timeIntervalSince1970 * 1000) + 60000)))
|
||||
|
||||
await appModel._test_handleOperatorGatewayServerEvent(EventFrame(
|
||||
type: "event",
|
||||
event: ExecApprovalNotificationBridge.resolvedKind,
|
||||
payload: AnyCodable(["id": "approval-event-resolved"]),
|
||||
seq: nil,
|
||||
stateversion: nil))
|
||||
|
||||
#expect(appModel._test_pendingExecApprovalPrompt() == nil)
|
||||
}
|
||||
|
||||
@Test func watchExecApprovalHydrateFetchesOnlyMissingIDs() {
|
||||
let idsToFetch = NodeAppModel._test_watchExecApprovalIDsNeedingFetch(
|
||||
candidateIDs: ["cached", "pending", "cached", "other", "", " pending "],
|
||||
@@ -1221,65 +1201,13 @@ private final class MockBootstrapNotificationCenter: NotificationCentering, @unc
|
||||
hasStoredOperatorToken: false))
|
||||
}
|
||||
|
||||
@Test @MainActor func operatorGatewayRequestedEventShowsNotificationGuidanceWhenNotificationsOff() async throws {
|
||||
@Test @MainActor func successfulBootstrapOnboardingRequestsNotificationAuthorization() async {
|
||||
let center = MockBootstrapNotificationCenter()
|
||||
center.status = .notDetermined
|
||||
let appModel = NodeAppModel(notificationCenter: center)
|
||||
appModel._test_resetExecApprovalNotificationGuidanceSuppression()
|
||||
defer { appModel._test_resetExecApprovalNotificationGuidanceSuppression() }
|
||||
|
||||
await appModel._test_handleOperatorGatewayServerEvent(EventFrame(
|
||||
type: "event",
|
||||
event: ExecApprovalNotificationBridge.requestedKind,
|
||||
payload: AnyCodable(["id": "approval-notifications-off"]),
|
||||
seq: nil,
|
||||
stateversion: nil))
|
||||
await appModel._test_handleSuccessfulBootstrapGatewayOnboarding()
|
||||
|
||||
let prompt = try #require(appModel._test_pendingNotificationPermissionGuidancePrompt())
|
||||
#expect(prompt.approvalId == "approval-notifications-off")
|
||||
}
|
||||
|
||||
@Test @MainActor func suppressedOperatorGatewayRequestedEventDoesNotShowNotificationGuidance() async {
|
||||
let center = MockBootstrapNotificationCenter()
|
||||
center.status = .denied
|
||||
let appModel = NodeAppModel(notificationCenter: center)
|
||||
appModel._test_resetExecApprovalNotificationGuidanceSuppression()
|
||||
defer { appModel._test_resetExecApprovalNotificationGuidanceSuppression() }
|
||||
appModel.dismissNotificationPermissionGuidancePrompt(suppressFuture: true)
|
||||
|
||||
await appModel._test_handleOperatorGatewayServerEvent(EventFrame(
|
||||
type: "event",
|
||||
event: ExecApprovalNotificationBridge.requestedKind,
|
||||
payload: AnyCodable(["id": "approval-suppressed"]),
|
||||
seq: nil,
|
||||
stateversion: nil))
|
||||
|
||||
#expect(appModel._test_pendingNotificationPermissionGuidancePrompt() == nil)
|
||||
}
|
||||
|
||||
@Test @MainActor func operatorGatewayResolvedEventClearsNotificationGuidancePrompt() async throws {
|
||||
let center = MockBootstrapNotificationCenter()
|
||||
center.status = .denied
|
||||
let appModel = NodeAppModel(notificationCenter: center)
|
||||
appModel._test_resetExecApprovalNotificationGuidanceSuppression()
|
||||
defer { appModel._test_resetExecApprovalNotificationGuidanceSuppression() }
|
||||
|
||||
await appModel._test_handleOperatorGatewayServerEvent(EventFrame(
|
||||
type: "event",
|
||||
event: ExecApprovalNotificationBridge.requestedKind,
|
||||
payload: AnyCodable(["id": "approval-guidance-resolved"]),
|
||||
seq: nil,
|
||||
stateversion: nil))
|
||||
_ = try #require(appModel._test_pendingNotificationPermissionGuidancePrompt())
|
||||
|
||||
await appModel._test_handleOperatorGatewayServerEvent(EventFrame(
|
||||
type: "event",
|
||||
event: ExecApprovalNotificationBridge.resolvedKind,
|
||||
payload: AnyCodable(["id": "approval-guidance-resolved"]),
|
||||
seq: nil,
|
||||
stateversion: nil))
|
||||
|
||||
#expect(appModel._test_pendingNotificationPermissionGuidancePrompt() == nil)
|
||||
#expect(center.requestAuthorizationCalls == 1)
|
||||
}
|
||||
|
||||
@Test func clearingBootstrapTokenStripsReconnectConfigEvenWithoutPersistence() throws {
|
||||
@@ -1341,78 +1269,6 @@ private final class MockBootstrapNotificationCenter: NotificationCentering, @unc
|
||||
#expect(res.error?.message.contains("CAMERA_DISABLED") == true)
|
||||
}
|
||||
|
||||
@Test @MainActor func systemNotifyReturnsUnavailableWhenNotificationsOff() async throws {
|
||||
let center = MockBootstrapNotificationCenter()
|
||||
center.status = .notDetermined
|
||||
let appModel = NodeAppModel(notificationCenter: center)
|
||||
let params = OpenClawSystemNotifyParams(title: "Approval", body: "Review request")
|
||||
let paramsData = try JSONEncoder().encode(params)
|
||||
let req = BridgeInvokeRequest(
|
||||
id: "notify-off",
|
||||
command: OpenClawSystemCommand.notify.rawValue,
|
||||
paramsJSON: String(decoding: paramsData, as: UTF8.self))
|
||||
|
||||
let res = await appModel._test_handleInvoke(req)
|
||||
|
||||
#expect(res.ok == false)
|
||||
#expect(res.error?.code == .unavailable)
|
||||
#expect(res.error?.message == "NOT_AUTHORIZED: notifications")
|
||||
#expect(center.addCalls == 0)
|
||||
}
|
||||
|
||||
@Test @MainActor func systemNotifySchedulesWhenNotificationsAreAlreadyAllowed() async throws {
|
||||
let center = MockBootstrapNotificationCenter()
|
||||
center.status = .authorized
|
||||
let appModel = NodeAppModel(notificationCenter: center)
|
||||
let params = OpenClawSystemNotifyParams(title: "Approval", body: "Review request")
|
||||
let paramsData = try JSONEncoder().encode(params)
|
||||
let req = BridgeInvokeRequest(
|
||||
id: "notify-on",
|
||||
command: OpenClawSystemCommand.notify.rawValue,
|
||||
paramsJSON: String(decoding: paramsData, as: UTF8.self))
|
||||
|
||||
let res = await appModel._test_handleInvoke(req)
|
||||
|
||||
#expect(res.ok)
|
||||
#expect(center.addCalls == 1)
|
||||
}
|
||||
|
||||
@Test @MainActor func chatPushWithoutSpeechReturnsUnavailableWhenNotificationsOff() async throws {
|
||||
let center = MockBootstrapNotificationCenter()
|
||||
center.status = .notDetermined
|
||||
let appModel = NodeAppModel(notificationCenter: center)
|
||||
let params = OpenClawChatPushParams(text: "Build finished", speak: false)
|
||||
let paramsData = try JSONEncoder().encode(params)
|
||||
let req = BridgeInvokeRequest(
|
||||
id: "chat-push-off",
|
||||
command: OpenClawChatCommand.push.rawValue,
|
||||
paramsJSON: String(decoding: paramsData, as: UTF8.self))
|
||||
|
||||
let res = await appModel._test_handleInvoke(req)
|
||||
|
||||
#expect(res.ok == false)
|
||||
#expect(res.error?.code == .unavailable)
|
||||
#expect(res.error?.message == "NOT_AUTHORIZED: notifications")
|
||||
#expect(center.addCalls == 0)
|
||||
}
|
||||
|
||||
@Test @MainActor func chatPushSchedulesWhenNotificationsAreAlreadyAllowed() async throws {
|
||||
let center = MockBootstrapNotificationCenter()
|
||||
center.status = .authorized
|
||||
let appModel = NodeAppModel(notificationCenter: center)
|
||||
let params = OpenClawChatPushParams(text: "Build finished", speak: false)
|
||||
let paramsData = try JSONEncoder().encode(params)
|
||||
let req = BridgeInvokeRequest(
|
||||
id: "chat-push-on",
|
||||
command: OpenClawChatCommand.push.rawValue,
|
||||
paramsJSON: String(decoding: paramsData, as: UTF8.self))
|
||||
|
||||
let res = await appModel._test_handleInvoke(req)
|
||||
|
||||
#expect(res.ok)
|
||||
#expect(center.addCalls == 1)
|
||||
}
|
||||
|
||||
@Test @MainActor func handleInvokeRejectsInvalidScreenFormat() async {
|
||||
let appModel = NodeAppModel()
|
||||
let params = OpenClawScreenRecordParams(format: "gif")
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
struct RootTabsSourceGuardTests {
|
||||
@Test func `hidden sidebar reveal uses destination header without reserved rail`() throws {
|
||||
@Suite struct RootTabsSourceGuardTests {
|
||||
@Test func hiddenSidebarRevealUsesDestinationHeaderWithoutReservedRail() throws {
|
||||
let source = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8)
|
||||
let componentSource = try String(contentsOf: Self.proComponentsSourceURL(), encoding: .utf8)
|
||||
|
||||
@@ -38,7 +38,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(!source.contains("shouldShowOverviewHeaderSidebarReveal"))
|
||||
}
|
||||
|
||||
@Test func `i pad split uses sliding sidebar while portrait keeps drawer overlay`() throws {
|
||||
@Test func iPadSplitUsesSlidingSidebarWhilePortraitKeepsDrawerOverlay() throws {
|
||||
let source = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8)
|
||||
let splitContent = try Self.extract(
|
||||
source,
|
||||
@@ -67,7 +67,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(!drawerContent.contains("NavigationSplitView"))
|
||||
}
|
||||
|
||||
@Test func `sidebar keeps navigation model destination only`() throws {
|
||||
@Test func sidebarKeepsNavigationModelDestinationOnly() throws {
|
||||
let source = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8)
|
||||
let navigationSource = try String(contentsOf: Self.rootTabsNavigationSourceURL(), encoding: .utf8)
|
||||
let sidebarColumn = try Self.extract(
|
||||
@@ -114,7 +114,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(navigationSource.contains("SidebarGroup(title: \"REFERENCE\", destinations: [.docs])"))
|
||||
}
|
||||
|
||||
@Test func `sidebar routes use destination headers instead of repeated product branding`() throws {
|
||||
@Test func sidebarRoutesUseDestinationHeadersInsteadOfRepeatedProductBranding() throws {
|
||||
let rootSource = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8)
|
||||
let agentOverviewSource = try String(contentsOf: Self.agentProTabOverviewSourceURL(), encoding: .utf8)
|
||||
let docsSource = try String(contentsOf: Self.docsSourceURL(), encoding: .utf8)
|
||||
@@ -148,7 +148,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(!docsSource.contains("Text(\"OpenClaw Docs\")"))
|
||||
}
|
||||
|
||||
@Test func `agents direct route keeps single sidebar control`() throws {
|
||||
@Test func agentsDirectRouteKeepsSingleSidebarControl() throws {
|
||||
let source = try String(contentsOf: Self.agentProTabSourceURL(), encoding: .utf8)
|
||||
let destinationsSource = try String(contentsOf: Self.agentProTabDestinationsSourceURL(), encoding: .utf8)
|
||||
let nodesSource = try String(contentsOf: Self.agentProNodesDestinationSourceURL(), encoding: .utf8)
|
||||
@@ -165,7 +165,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(dreamingSource.contains("OpenClawSidebarHeaderLeadingSlot(action: headerLeadingAction)"))
|
||||
}
|
||||
|
||||
@Test func `routed headers use shared adaptive layout`() throws {
|
||||
@Test func routedHeadersUseSharedAdaptiveLayout() throws {
|
||||
let componentsSource = try String(contentsOf: Self.proComponentsSourceURL(), encoding: .utf8)
|
||||
let featureChromeSource = try String(contentsOf: Self.iPadSidebarScreenChromeSourceURL(), encoding: .utf8)
|
||||
let docsSource = try String(contentsOf: Self.docsSourceURL(), encoding: .utf8)
|
||||
@@ -187,7 +187,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(settingsSource.contains("OpenClawAdaptiveHeaderRow("))
|
||||
}
|
||||
|
||||
@Test func `phone hub keeps docs as destination only`() throws {
|
||||
@Test func phoneHubKeepsDocsAsDestinationOnly() throws {
|
||||
let source = try String(contentsOf: Self.phoneHubSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains("case .docs:"))
|
||||
@@ -198,7 +198,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(!source.contains("https://docs.openclaw.ai"))
|
||||
}
|
||||
|
||||
@Test func `root shell preview matrix covers phone and I pad states`() throws {
|
||||
@Test func rootShellPreviewMatrixCoversPhoneAndIPadStates() throws {
|
||||
let source = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains("#Preview(\n \"Shell iPhone portrait\""))
|
||||
@@ -211,7 +211,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(source.contains("#Preview(\n \"Shell iPad gateway error\""))
|
||||
}
|
||||
|
||||
@Test func `shared chat preview matrix covers connection states`() throws {
|
||||
@Test func sharedChatPreviewMatrixCoversConnectionStates() throws {
|
||||
let source = try String(contentsOf: Self.sharedChatPreviewSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains("#Preview(\"Chat connected\")"))
|
||||
@@ -226,7 +226,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(source.contains("Gateway not connected. Check Tailscale and retry."))
|
||||
}
|
||||
|
||||
@Test func `phone hub keeps content above floating tab bar`() throws {
|
||||
@Test func phoneHubKeepsContentAboveFloatingTabBar() throws {
|
||||
let source = try String(contentsOf: Self.phoneHubSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains(".safeAreaPadding(.bottom, self.bottomScrollInset)"))
|
||||
@@ -235,7 +235,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(!source.contains("bottomTabBarClearance"))
|
||||
}
|
||||
|
||||
@Test func `phone hub header stays task first`() throws {
|
||||
@Test func phoneHubHeaderStaysTaskFirst() throws {
|
||||
let source = try String(contentsOf: Self.phoneHubSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains("private var gatewayActionRow: some View"))
|
||||
@@ -253,7 +253,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(!source.contains("private func metric(label:"))
|
||||
}
|
||||
|
||||
@Test func `workboard uses real gateway methods`() throws {
|
||||
@Test func workboardUsesRealGatewayMethods() throws {
|
||||
let source = try String(contentsOf: Self.iPadWorkboardScreenSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains("workboard.cards.list"))
|
||||
@@ -268,7 +268,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(!source.contains("Multi-column queue control"))
|
||||
}
|
||||
|
||||
@Test func `workboard create action surfaces unavailable reasons`() throws {
|
||||
@Test func workboardCreateActionSurfacesUnavailableReasons() throws {
|
||||
let source = try String(contentsOf: Self.iPadWorkboardScreenSourceURL(), encoding: .utf8)
|
||||
let createFunction = try Self.extract(
|
||||
source,
|
||||
@@ -295,7 +295,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(createFunction.contains("return true"))
|
||||
}
|
||||
|
||||
@Test func `task scope controls send real gateway params`() throws {
|
||||
@Test func taskScopeControlsSendRealGatewayParams() throws {
|
||||
let source = try Self.iPadTaskFeatureScreensSource()
|
||||
|
||||
#expect(source.contains("private var boardScopeMenu: some View"))
|
||||
@@ -314,7 +314,7 @@ struct RootTabsSourceGuardTests {
|
||||
"params: EmptyParams(),\n timeoutSeconds: 20)\n let response = try JSONDecoder().decode(IPadSkillProposalManifest.self"))
|
||||
}
|
||||
|
||||
@Test func `compact task rows keep phone native actions`() throws {
|
||||
@Test func compactTaskRowsKeepPhoneNativeActions() throws {
|
||||
let source = try Self.iPadTaskFeatureScreensSource()
|
||||
let compactControls = try Self.extract(
|
||||
source,
|
||||
@@ -348,7 +348,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(compactControls.contains("Label(\"Dispatch\""))
|
||||
}
|
||||
|
||||
@Test func `skill workshop uses kanban lanes on wide I pad`() throws {
|
||||
@Test func skillWorkshopUsesKanbanLanesOnWideIPad() throws {
|
||||
let source = try String(contentsOf: Self.iPadSkillWorkshopScreenSourceURL(), encoding: .utf8)
|
||||
let previewSource = try String(contentsOf: Self.iPadSidebarFeaturePreviewsSourceURL(), encoding: .utf8)
|
||||
let content = try Self.extract(
|
||||
@@ -376,7 +376,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(previewSource.contains("status: \"manual_QA\""))
|
||||
}
|
||||
|
||||
@Test func `compact task rows have populated phone previews`() throws {
|
||||
@Test func compactTaskRowsHavePopulatedPhonePreviews() throws {
|
||||
let source = try String(contentsOf: Self.iPadSidebarFeaturePreviewsSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains("#Preview(\"Workboard phone queue rows\")"))
|
||||
@@ -387,7 +387,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(source.contains("IPadSkillWorkshopPreviewFixtures.proposals"))
|
||||
}
|
||||
|
||||
@Test func `task screen preview matrices cover primary states`() throws {
|
||||
@Test func taskScreenPreviewMatricesCoverPrimaryStates() throws {
|
||||
let source = try String(contentsOf: Self.iPadSidebarFeaturePreviewsSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains("#Preview(\"Workboard states\")"))
|
||||
@@ -412,7 +412,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(source.contains("\"manual_QA\""))
|
||||
}
|
||||
|
||||
@Test func `activity preview matrix covers connection states`() throws {
|
||||
@Test func activityPreviewMatrixCoversConnectionStates() throws {
|
||||
let source = try String(contentsOf: Self.iPadSidebarFeaturePreviewsSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains("#Preview(\"Activity states\")"))
|
||||
@@ -426,7 +426,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(source.contains("title: \"Loading sessions\""))
|
||||
}
|
||||
|
||||
@Test func `routed feature screens reuse shared pro components`() throws {
|
||||
@Test func routedFeatureScreensReuseSharedProComponents() throws {
|
||||
let source = try Self.iPadTaskFeatureScreensSource()
|
||||
let componentsSource = try String(contentsOf: Self.proComponentsSourceURL(), encoding: .utf8)
|
||||
let channelsSource = try String(contentsOf: Self.channelsSourceURL(), encoding: .utf8)
|
||||
@@ -445,22 +445,20 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(componentsSource.contains("struct ProStatusRow"))
|
||||
}
|
||||
|
||||
@Test func `activity screen stays split from task feature screens`() throws {
|
||||
@Test func activityScreenStaysSplitFromTaskFeatureScreens() throws {
|
||||
let taskSource = try Self.iPadTaskFeatureScreensSource()
|
||||
let activitySource = try String(contentsOf: Self.iPadActivityScreenSourceURL(), encoding: .utf8)
|
||||
let appModelSource = try String(contentsOf: Self.nodeAppModelSourceURL(), encoding: .utf8)
|
||||
let projectSource = try String(contentsOf: Self.xcodeProjectSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(activitySource.contains("struct IPadActivityScreen: View"))
|
||||
#expect(activitySource.contains("self.appModel.makeChatTransport()"))
|
||||
#expect(appModelSource.contains("return IOSGatewayChatTransport(gateway: self.operatorSession)"))
|
||||
#expect(activitySource.contains("IOSGatewayChatTransport(gateway: self.appModel.operatorSession)"))
|
||||
#expect(activitySource.contains("IPadSidebarScreenChrome("))
|
||||
#expect(!taskSource.contains("struct IPadActivityScreen"))
|
||||
#expect(!taskSource.contains("import OpenClawChatUI"))
|
||||
#expect(projectSource.contains("IPadActivityScreen.swift in Sources"))
|
||||
}
|
||||
|
||||
@Test func `routed feature chrome stays split from task feature screens`() throws {
|
||||
@Test func routedFeatureChromeStaysSplitFromTaskFeatureScreens() throws {
|
||||
let taskSource = try Self.iPadTaskFeatureScreensSource()
|
||||
let chromeSource = try String(contentsOf: Self.iPadSidebarScreenChromeSourceURL(), encoding: .utf8)
|
||||
let projectSource = try String(contentsOf: Self.xcodeProjectSourceURL(), encoding: .utf8)
|
||||
@@ -472,7 +470,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(projectSource.contains("IPadSidebarScreenChrome.swift in Sources"))
|
||||
}
|
||||
|
||||
@Test func `routed feature chrome keeps gateway pill actionable`() throws {
|
||||
@Test func routedFeatureChromeKeepsGatewayPillActionable() throws {
|
||||
let chromeSource = try String(contentsOf: Self.iPadSidebarScreenChromeSourceURL(), encoding: .utf8)
|
||||
let featureSource = try Self.iPadTaskFeatureScreensSource()
|
||||
let rootSource = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8)
|
||||
@@ -488,18 +486,14 @@ struct RootTabsSourceGuardTests {
|
||||
.count == 1)
|
||||
}
|
||||
|
||||
@Test func `routed gateway pills open gateway settings`() throws {
|
||||
@Test func routedGatewayPillsOpenGatewaySettings() throws {
|
||||
let rootSource = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8)
|
||||
let agentSource = try String(contentsOf: Self.agentProTabSourceURL(), encoding: .utf8)
|
||||
let agentOverviewSource = try String(contentsOf: Self.agentProTabOverviewSourceURL(), encoding: .utf8)
|
||||
let overviewSource = try String(contentsOf: Self.commandCenterSourceURL(), encoding: .utf8)
|
||||
let chatSource = try String(contentsOf: Self.chatProTabSourceURL(), encoding: .utf8)
|
||||
let docsSource = try String(contentsOf: Self.docsSourceURL(), encoding: .utf8)
|
||||
let settingsTabSource = try String(contentsOf: Self.settingsProTabSourceURL(), encoding: .utf8)
|
||||
let settingsSource = try String(contentsOf: Self.settingsProTabSectionsSourceURL(), encoding: .utf8)
|
||||
let notificationGuidanceSource = try String(
|
||||
contentsOf: Self.notificationPermissionGuidanceDialogSourceURL(),
|
||||
encoding: .utf8)
|
||||
|
||||
#expect(rootSource.matches(of: /openSettings: \{ self\.selectSidebarDestination\(\.gateway\) \}/).count >= 2)
|
||||
#expect(rootSource.matches(of: /gatewayAction: \{ self\.selectSidebarDestination\(\.gateway\) \}/).count == 1)
|
||||
@@ -518,39 +512,15 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(docsSource.contains("let gatewayAction: (() -> Void)?"))
|
||||
#expect(settingsSource.contains("NavigationLink(value: SettingsRoute.gateway)"))
|
||||
#expect(rootSource.contains("case .settings:"))
|
||||
#expect(rootSource
|
||||
.matches(of: /SettingsProTab\(\s*headerLeadingAction: self\.sidebarHeaderLeadingAction,/)
|
||||
.count >= 1)
|
||||
#expect(rootSource
|
||||
.contains(
|
||||
"directRoute: self.selectedSettingsRoute ?? self.selectedSidebarDestination.settingsRoute ?? .gateway"))
|
||||
#expect(rootSource.matches(of: /SettingsProTab\(\s*initialRoute: self\.selectedSettingsRoute,/).count == 1)
|
||||
#expect(rootSource.contains(".id(self.settingsTabViewID)"))
|
||||
#expect(rootSource.contains("@State private var selectedSettingsRouteRequestID: Int = 0"))
|
||||
#expect(rootSource.contains("self.selectedSettingsRouteRequestID &+= 1"))
|
||||
#expect(rootSource.contains("@State private var suppressedExecApprovalPromptIDForNotificationSettings"))
|
||||
#expect(rootSource.contains("private var activeExecApprovalPromptSuppressionID: String?"))
|
||||
#expect(rootSource.contains("suppressedApprovalID: self.activeExecApprovalPromptSuppressionID"))
|
||||
#expect(rootSource.contains("if destination.settingsRoute != .notifications"))
|
||||
#expect(rootSource.contains("if route != .notifications"))
|
||||
#expect(rootSource.contains("if route == nil"))
|
||||
#expect(rootSource.contains("self.selectedSettingsRoute = nil"))
|
||||
#expect(rootSource.contains("self.selectedSidebarDestination = .settings"))
|
||||
#expect(rootSource.contains("self.suppressedExecApprovalPromptIDForNotificationSettings = approvalId"))
|
||||
#expect(rootSource.contains("onRouteChange: self.handleSettingsRouteChange"))
|
||||
#expect(rootSource.contains("private func handleSettingsRouteChange(_ route: SettingsRoute?)"))
|
||||
#expect(settingsTabSource.contains("let onRouteChange: ((SettingsRoute?) -> Void)?"))
|
||||
#expect(settingsTabSource.contains("self.onRouteChange?(self.navigationPath.last)"))
|
||||
#expect(notificationGuidanceSource.contains("onSuppressFuture"))
|
||||
#expect(notificationGuidanceSource.contains("suppressFuture: true"))
|
||||
#expect(notificationGuidanceSource.contains("Text(\"Don't show again\")"))
|
||||
#expect(rootSource.contains("private func selectSettingsRoute(_ route: SettingsRoute)"))
|
||||
#expect(rootSource.contains("SettingsProTab(headerLeadingAction: self.sidebarHeaderLeadingAction)"))
|
||||
#expect(rootSource.contains("directRoute: self.selectedSidebarDestination.settingsRoute ?? .gateway"))
|
||||
#expect(rootSource.contains("SettingsProTab(initialRoute: self.selectedSidebarDestination.settingsRoute)"))
|
||||
#expect(settingsSource.contains("title: \"Channels / Integrations\""))
|
||||
#expect(settingsSource.contains("route: .channels"))
|
||||
#expect(docsSource.contains(".accessibilityHint(\"Opens Settings / Gateway\")"))
|
||||
}
|
||||
|
||||
@Test func `gateway settings keeps pairing trust diagnostics and tailscale actions`() throws {
|
||||
@Test func gatewaySettingsKeepsPairingTrustDiagnosticsAndTailscaleActions() throws {
|
||||
let settingsSource = try String(contentsOf: Self.settingsProTabSourceURL(), encoding: .utf8)
|
||||
let sectionsSource = try String(contentsOf: Self.settingsProTabSectionsSourceURL(), encoding: .utf8)
|
||||
let actionsSource = try String(contentsOf: Self.settingsProTabActionsSourceURL(), encoding: .utf8)
|
||||
@@ -596,7 +566,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(controllerSource.contains("trustRotatedGatewayCertificate(from problem: GatewayConnectionProblem)"))
|
||||
}
|
||||
|
||||
@Test func `gateway settings preview matrix covers primary states`() throws {
|
||||
@Test func gatewaySettingsPreviewMatrixCoversPrimaryStates() throws {
|
||||
let supportSource = try String(contentsOf: Self.settingsProTabSupportSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(supportSource.contains("#Preview(\"Gateway settings states\")"))
|
||||
@@ -615,15 +585,12 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(supportSource.contains("self.previewButton(\"Diagnose\""))
|
||||
}
|
||||
|
||||
@Test func `native chat uses gateway transport`() throws {
|
||||
@Test func nativeChatUsesGatewayTransport() throws {
|
||||
let chatSource = try String(contentsOf: Self.chatProTabSourceURL(), encoding: .utf8)
|
||||
let channelsSource = try String(contentsOf: Self.channelsSourceURL(), encoding: .utf8)
|
||||
let settingsSectionsSource = try String(contentsOf: Self.settingsProTabSectionsSourceURL(), encoding: .utf8)
|
||||
let appModelSource = try String(contentsOf: Self.nodeAppModelSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(chatSource.matches(of: /self\.appModel\.makeChatTransport\(\)/).count == 2)
|
||||
#expect(appModelSource.contains("return IOSGatewayChatTransport(gateway: self.operatorSession)"))
|
||||
#expect(settingsSectionsSource.contains("Message routing and external channel clients."))
|
||||
#expect(chatSource.contains("IOSGatewayChatTransport(gateway: self.appModel.operatorSession)"))
|
||||
#expect(channelsSource.contains("Message routing and external channel clients."))
|
||||
#expect(channelsSource.contains("\"clickclack\": SettingsChannelFallbackMetadata"))
|
||||
#expect(channelsSource.contains("label: \"ClickClack\""))
|
||||
#expect(channelsSource.contains("Self-hosted chat bot routing."))
|
||||
@@ -636,13 +603,6 @@ struct RootTabsSourceGuardTests {
|
||||
.appendingPathComponent("Sources/RootTabs.swift")
|
||||
}
|
||||
|
||||
private static func nodeAppModelSourceURL() -> URL {
|
||||
URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent("Sources/Model/NodeAppModel.swift")
|
||||
}
|
||||
|
||||
private static func phoneHubSourceURL() -> URL {
|
||||
URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent()
|
||||
@@ -786,13 +746,6 @@ struct RootTabsSourceGuardTests {
|
||||
.appendingPathComponent("Sources/Design/SettingsProTab.swift")
|
||||
}
|
||||
|
||||
private static func notificationPermissionGuidanceDialogSourceURL() -> URL {
|
||||
URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent("Sources/Gateway/NotificationPermissionGuidanceDialog.swift")
|
||||
}
|
||||
|
||||
private static func settingsProTabActionsSourceURL() -> URL {
|
||||
URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent()
|
||||
|
||||
@@ -8,7 +8,7 @@ import Testing
|
||||
gatewayConnected: false,
|
||||
discoveredGatewayCount: 0,
|
||||
talkConfigLoaded: false,
|
||||
notificationsAllowed: false) == [
|
||||
notificationStatusText: "Not Set") == [
|
||||
.gatewayOffline,
|
||||
.discoveryUnavailable,
|
||||
.notificationsUnavailable,
|
||||
@@ -21,12 +21,12 @@ import Testing
|
||||
gatewayConnected: true,
|
||||
discoveredGatewayCount: 1,
|
||||
talkConfigLoaded: false,
|
||||
notificationsAllowed: true) == [.talkConfigMissing])
|
||||
notificationStatusText: "Allowed") == [.talkConfigMissing])
|
||||
#expect(
|
||||
SettingsDiagnostics.issueCount(
|
||||
gatewayConnected: true,
|
||||
discoveredGatewayCount: 1,
|
||||
talkConfigLoaded: true,
|
||||
notificationsAllowed: true) == 0)
|
||||
notificationStatusText: "Allowed") == 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,6 @@ import OpenClawKit
|
||||
import Testing
|
||||
|
||||
@Suite struct ShareToAgentDeepLinkTests {
|
||||
@Test func appGroupIdentifierUsesCanonicalOpenClawGroup() {
|
||||
#expect(OpenClawAppGroup.canonicalIdentifier == "group.ai.openclawfoundation.app.shared")
|
||||
}
|
||||
|
||||
@Test func buildMessageIncludesSharedFields() {
|
||||
let payload = SharedContentPayload(
|
||||
title: "Article",
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
<string>$(OPENCLAW_MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(OPENCLAW_BUILD_VERSION)</string>
|
||||
<key>WKApplication</key>
|
||||
<true/>
|
||||
<key>WKCompanionAppBundleIdentifier</key>
|
||||
<string>$(OPENCLAW_APP_BUNDLE_ID)</string>
|
||||
<key>WKWatchKitApp</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
6
apps/ios/WatchExtension/Assets.xcassets/Contents.json
Normal file
6
apps/ios/WatchExtension/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
32
apps/ios/WatchExtension/Info.plist
Normal file
32
apps/ios/WatchExtension/Info.plist
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>OpenClaw</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(OPENCLAW_MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(OPENCLAW_BUILD_VERSION)</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>WKAppBundleIdentifier</key>
|
||||
<string>$(OPENCLAW_WATCH_APP_BUNDLE_ID)</string>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.watchkit</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1146,7 +1146,7 @@ private enum WatchNativeTextInput {
|
||||
suggestions: [String],
|
||||
onSubmit: @escaping (String) -> Void)
|
||||
{
|
||||
WKApplication.shared().visibleInterfaceController?.presentTextInputController(
|
||||
WKExtension.shared().visibleInterfaceController?.presentTextInputController(
|
||||
withSuggestions: suggestions,
|
||||
allowedInputMode: .allowEmoji)
|
||||
{ results in
|
||||
@@ -293,8 +293,6 @@ def capture_watch_screenshot
|
||||
Dir[File.join(output_dir, "Apple Watch*-*.png")].each { |path| FileUtils.rm_f(path) }
|
||||
FileUtils.rm_rf(derived_data_path)
|
||||
|
||||
# Single-target watch apps only expose generic simulator build destinations in Xcode.
|
||||
# Keep the selected UDID for install/launch/screenshot below.
|
||||
sh(
|
||||
xcodebuild_shell_join([
|
||||
"xcodebuild",
|
||||
@@ -305,7 +303,7 @@ def capture_watch_screenshot
|
||||
"-configuration",
|
||||
"Debug",
|
||||
"-destination",
|
||||
"generic/platform=watchOS Simulator",
|
||||
"platform=watchOS Simulator,id=#{udid}",
|
||||
"-derivedDataPath",
|
||||
derived_data_path,
|
||||
"build",
|
||||
@@ -313,8 +311,10 @@ def capture_watch_screenshot
|
||||
)
|
||||
|
||||
UI.user_error!("Watch screenshot build did not produce #{app_path}.") unless File.exist?(app_path)
|
||||
extension_path = File.join(app_path, "PlugIns", "OpenClawWatchExtension.appex")
|
||||
watch_app_identifier = bundle_identifier_for_product(app_path)
|
||||
screenshot_mode_bundle_identifiers = [watch_app_identifier]
|
||||
watch_extension_identifier = bundle_identifier_for_product(extension_path)
|
||||
screenshot_mode_bundle_identifiers = [watch_app_identifier, watch_extension_identifier]
|
||||
|
||||
sh("#{shell_join(["xcrun", "simctl", "boot", udid])} >/dev/null 2>&1 || true")
|
||||
sh(shell_join(["xcrun", "simctl", "bootstatus", udid, "-b"]))
|
||||
@@ -492,9 +492,6 @@ def produce_services_for_target(target)
|
||||
if target.fetch("capabilities").include?("PUSH_NOTIFICATIONS")
|
||||
services[:push_notification] = "on"
|
||||
end
|
||||
if target.fetch("capabilities").include?("APP_GROUPS")
|
||||
services[:app_group] = "on"
|
||||
end
|
||||
services
|
||||
end
|
||||
|
||||
@@ -570,15 +567,6 @@ def profile_plist_value(profile_path, key_path)
|
||||
end
|
||||
end
|
||||
|
||||
def profile_plist_array_values(profile_path, key_path)
|
||||
raw = profile_plist_value(profile_path, key_path)
|
||||
return [] unless raw
|
||||
|
||||
raw.lines.map(&:strip).reject do |line|
|
||||
line.empty? || line == "Array {" || line == "}"
|
||||
end
|
||||
end
|
||||
|
||||
def validate_match_profile_capabilities!(target)
|
||||
capabilities = target.fetch("capabilities")
|
||||
return if capabilities.empty?
|
||||
@@ -594,17 +582,6 @@ def validate_match_profile_capabilities!(target)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
if capabilities.include?("APP_GROUPS")
|
||||
expected_app_groups = target.fetch("appGroups")
|
||||
actual_app_groups = profile_plist_array_values(profile_path, "Entitlements:com.apple.security.application-groups")
|
||||
missing = expected_app_groups - actual_app_groups
|
||||
unless missing.empty?
|
||||
UI.user_error!(
|
||||
"Provisioning profile #{target.fetch("profileName")} for #{target.fetch("bundleId")} is missing App Groups #{missing.join(", ")}; actual groups: #{actual_app_groups.empty? ? "missing" : actual_app_groups.join(", ")}."
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def sync_app_store_signing!(readonly:)
|
||||
|
||||
@@ -65,7 +65,7 @@ pnpm ios:release:signing:check
|
||||
pnpm ios:release:signing:setup
|
||||
```
|
||||
|
||||
`signing:setup` uses Fastlane `produce` and `modify_services` to create Developer Portal bundle IDs and enable required services before running `match`. The main app and share extension also require the shared App Group from `apps/ios/Config/AppStoreSigning.json`; associate that group with both bundle IDs in the Apple Developer Portal before regenerating profiles. If Fastlane does not already have a valid Apple Developer Portal session, run `fastlane spaceauth` for a release-owner Apple ID and export the resulting `FASTLANE_SESSION`.
|
||||
`signing:setup` uses Fastlane `produce` and `modify_services` to create Developer Portal bundle IDs and enable required services before running `match`. If Fastlane does not already have a valid Apple Developer Portal session, run `fastlane spaceauth` for a release-owner Apple ID and export the resulting `FASTLANE_SESSION`.
|
||||
|
||||
Shared encrypted signing storage:
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
Maintenance update for the current OpenClaw release.
|
||||
OpenClaw is now available on iPhone.
|
||||
|
||||
- Added Apple Watch controls for common agent actions.
|
||||
- Improved Gateway setup, notification settings, and share-extension identity handling.
|
||||
- Updated the Watch app integration for current Xcode compatibility.
|
||||
Connect to your OpenClaw Gateway to chat with your assistant, use realtime Talk mode, review approvals, share content from iOS, and bring device capabilities like camera, location, screen, and notifications into your private automation workflows.
|
||||
|
||||
@@ -65,8 +65,6 @@ targets:
|
||||
embed: true
|
||||
- target: OpenClawActivityWidget
|
||||
embed: true
|
||||
# A companion watch application belongs in the standard Watch bundle location.
|
||||
# PlugIns is for extension products and breaks paired watch installation.
|
||||
- target: OpenClawWatchApp
|
||||
- package: OpenClawKit
|
||||
- package: OpenClawKit
|
||||
@@ -90,7 +88,7 @@ targets:
|
||||
exit 1
|
||||
fi
|
||||
swiftformat --lint --config "$SRCROOT/../../config/swiftformat" \
|
||||
--unexclude "$SRCROOT/Sources,$SRCROOT/ShareExtension,$SRCROOT/ActivityWidget,$SRCROOT/WatchApp,$SRCROOT/../shared/OpenClawKit,$SRCROOT/../swabble" \
|
||||
--unexclude "$SRCROOT/Sources,$SRCROOT/ShareExtension,$SRCROOT/ActivityWidget,$SRCROOT/WatchExtension,$SRCROOT/../shared/OpenClawKit,$SRCROOT/../swabble" \
|
||||
--filelist "$SRCROOT/SwiftSources.input.xcfilelist"
|
||||
- name: SwiftLint
|
||||
basedOnDependencyAnalysis: false
|
||||
@@ -142,7 +140,6 @@ targets:
|
||||
- openclaw
|
||||
CFBundleShortVersionString: "$(OPENCLAW_MARKETING_VERSION)"
|
||||
OpenClawCanonicalVersion: "$(OPENCLAW_IOS_VERSION)"
|
||||
OpenClawAppGroupIdentifier: "$(OPENCLAW_APP_GROUP_ID)"
|
||||
CFBundleVersion: "$(OPENCLAW_BUILD_VERSION)"
|
||||
UILaunchScreen: {}
|
||||
UIApplicationSceneManifest:
|
||||
@@ -195,7 +192,6 @@ targets:
|
||||
settings:
|
||||
base:
|
||||
CODE_SIGN_IDENTITY: "$(OPENCLAW_CODE_SIGN_IDENTITY)"
|
||||
CODE_SIGN_ENTITLEMENTS: ShareExtension/OpenClawShareExtension.entitlements
|
||||
CODE_SIGN_STYLE: "$(OPENCLAW_CODE_SIGN_STYLE)"
|
||||
DEVELOPMENT_TEAM: "$(OPENCLAW_DEVELOPMENT_TEAM)"
|
||||
ENABLE_APPINTENTS_METADATA: NO
|
||||
@@ -210,7 +206,6 @@ targets:
|
||||
properties:
|
||||
CFBundleDisplayName: OpenClaw Share
|
||||
CFBundleShortVersionString: "$(OPENCLAW_MARKETING_VERSION)"
|
||||
OpenClawAppGroupIdentifier: "$(OPENCLAW_APP_GROUP_ID)"
|
||||
CFBundleVersion: "$(OPENCLAW_BUILD_VERSION)"
|
||||
NSExtension:
|
||||
NSExtensionPointIdentifier: com.apple.share-services
|
||||
@@ -256,17 +251,13 @@ targets:
|
||||
NSExtensionPointIdentifier: com.apple.widgetkit-extension
|
||||
|
||||
OpenClawWatchApp:
|
||||
type: application
|
||||
type: application.watchapp2
|
||||
platform: watchOS
|
||||
deploymentTarget: "11.0"
|
||||
sources:
|
||||
- path: WatchApp
|
||||
excludes:
|
||||
- Info.plist
|
||||
dependencies:
|
||||
- sdk: AppIntents.framework
|
||||
- sdk: WatchConnectivity.framework
|
||||
- sdk: UserNotifications.framework
|
||||
- target: OpenClawWatchExtension
|
||||
configFiles:
|
||||
Debug: Config/Signing.xcconfig
|
||||
Release: Config/Signing.xcconfig
|
||||
@@ -283,8 +274,6 @@ targets:
|
||||
ENABLE_APP_INTENTS_METADATA_GENERATION: NO
|
||||
PRODUCT_BUNDLE_IDENTIFIER: "$(OPENCLAW_WATCH_APP_BUNDLE_ID)"
|
||||
PROVISIONING_PROFILE_SPECIFIER: "$(OPENCLAW_WATCH_APP_PROFILE)"
|
||||
SWIFT_STRICT_CONCURRENCY: complete
|
||||
SWIFT_VERSION: "6.0"
|
||||
info:
|
||||
path: WatchApp/Info.plist
|
||||
properties:
|
||||
@@ -292,7 +281,42 @@ targets:
|
||||
CFBundleShortVersionString: "$(OPENCLAW_MARKETING_VERSION)"
|
||||
CFBundleVersion: "$(OPENCLAW_BUILD_VERSION)"
|
||||
WKCompanionAppBundleIdentifier: "$(OPENCLAW_APP_BUNDLE_ID)"
|
||||
WKApplication: true
|
||||
WKWatchKitApp: true
|
||||
|
||||
OpenClawWatchExtension:
|
||||
type: watchkit2-extension
|
||||
platform: watchOS
|
||||
deploymentTarget: "11.0"
|
||||
sources:
|
||||
- path: WatchExtension/Sources
|
||||
- path: WatchExtension/Assets.xcassets
|
||||
dependencies:
|
||||
- sdk: AppIntents.framework
|
||||
- sdk: WatchConnectivity.framework
|
||||
- sdk: UserNotifications.framework
|
||||
configFiles:
|
||||
Debug: Config/Signing.xcconfig
|
||||
Release: Config/Signing.xcconfig
|
||||
attributes:
|
||||
DevelopmentTeam: "$(OPENCLAW_DEVELOPMENT_TEAM)"
|
||||
ProvisioningStyle: "$(OPENCLAW_CODE_SIGN_STYLE)"
|
||||
settings:
|
||||
base:
|
||||
CODE_SIGN_IDENTITY: "$(OPENCLAW_CODE_SIGN_IDENTITY)"
|
||||
CODE_SIGN_STYLE: "$(OPENCLAW_CODE_SIGN_STYLE)"
|
||||
DEVELOPMENT_TEAM: "$(OPENCLAW_DEVELOPMENT_TEAM)"
|
||||
PRODUCT_BUNDLE_IDENTIFIER: "$(OPENCLAW_WATCH_EXTENSION_BUNDLE_ID)"
|
||||
PROVISIONING_PROFILE_SPECIFIER: "$(OPENCLAW_WATCH_EXTENSION_PROFILE)"
|
||||
info:
|
||||
path: WatchExtension/Info.plist
|
||||
properties:
|
||||
CFBundleDisplayName: OpenClaw
|
||||
CFBundleShortVersionString: "$(OPENCLAW_MARKETING_VERSION)"
|
||||
CFBundleVersion: "$(OPENCLAW_BUILD_VERSION)"
|
||||
NSExtension:
|
||||
NSExtensionAttributes:
|
||||
WKAppBundleIdentifier: "$(OPENCLAW_WATCH_APP_BUNDLE_ID)"
|
||||
NSExtensionPointIdentifier: com.apple.watchkit
|
||||
|
||||
OpenClawTests:
|
||||
type: bundle.unit-test
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2026.6.9"
|
||||
"version": "2026.6.2"
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import AppKit
|
||||
import WebKit
|
||||
|
||||
extension CanvasWindowController {
|
||||
// MARK: - WKUIDelegate
|
||||
|
||||
/// Bridges `<input type="file">` clicks in canvas HTML to a native `NSOpenPanel`.
|
||||
/// Without a `WKUIDelegate`, WebKit silently drops the request and file-picker
|
||||
/// buttons in canvas pages do nothing.
|
||||
@MainActor
|
||||
func webView(
|
||||
_ webView: WKWebView,
|
||||
runOpenPanelWith parameters: WKOpenPanelParameters,
|
||||
initiatedByFrame frame: WKFrameInfo,
|
||||
completionHandler: @escaping @MainActor @Sendable ([URL]?) -> Void)
|
||||
{
|
||||
let panel = NSOpenPanel()
|
||||
panel.canChooseFiles = true
|
||||
panel.canChooseDirectories = parameters.allowsDirectories
|
||||
panel.allowsMultipleSelection = parameters.allowsMultipleSelection
|
||||
panel.resolvesAliases = true
|
||||
if let window = self.window {
|
||||
panel.beginSheetModal(for: window) { response in
|
||||
completionHandler(response == .OK ? panel.urls : nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
panel.begin { response in
|
||||
completionHandler(response == .OK ? panel.urls : nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import OpenClawKit
|
||||
import WebKit
|
||||
|
||||
@MainActor
|
||||
final class CanvasWindowController: NSWindowController, WKNavigationDelegate, WKUIDelegate, NSWindowDelegate {
|
||||
final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NSWindowDelegate {
|
||||
let sessionKey: String
|
||||
private let root: URL
|
||||
private let sessionDir: URL
|
||||
@@ -159,7 +159,6 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, WK
|
||||
}
|
||||
|
||||
self.webView.navigationDelegate = self
|
||||
self.webView.uiDelegate = self
|
||||
self.window?.delegate = self
|
||||
self.container.onClose = { [weak self] in
|
||||
self?.hideCanvas()
|
||||
|
||||
@@ -19,7 +19,7 @@ private final class DashboardWindowDragRegionView: NSView {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class DashboardWindowController: NSWindowController, WKNavigationDelegate, WKUIDelegate, NSWindowDelegate {
|
||||
final class DashboardWindowController: NSWindowController, WKNavigationDelegate, NSWindowDelegate {
|
||||
private let webView: WKWebView
|
||||
private var currentURL: URL
|
||||
private var auth: DashboardWindowAuth
|
||||
@@ -44,37 +44,9 @@ final class DashboardWindowController: NSWindowController, WKNavigationDelegate,
|
||||
super.init(window: window)
|
||||
|
||||
self.webView.navigationDelegate = self
|
||||
self.webView.uiDelegate = self
|
||||
self.window?.delegate = self
|
||||
}
|
||||
|
||||
// MARK: - WKUIDelegate
|
||||
|
||||
/// Bridges `<input type="file">` clicks in the embedded Control UI to a native
|
||||
/// `NSOpenPanel`; without a `WKUIDelegate`, WebKit silently drops the request
|
||||
/// and "Choose image" / file-picker buttons do nothing.
|
||||
func webView(
|
||||
_ webView: WKWebView,
|
||||
runOpenPanelWith parameters: WKOpenPanelParameters,
|
||||
initiatedByFrame frame: WKFrameInfo,
|
||||
completionHandler: @escaping @MainActor @Sendable ([URL]?) -> Void)
|
||||
{
|
||||
let panel = NSOpenPanel()
|
||||
panel.canChooseFiles = true
|
||||
panel.canChooseDirectories = parameters.allowsDirectories
|
||||
panel.allowsMultipleSelection = parameters.allowsMultipleSelection
|
||||
panel.resolvesAliases = true
|
||||
if let window = self.window {
|
||||
panel.beginSheetModal(for: window) { response in
|
||||
completionHandler(response == .OK ? panel.urls : nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
panel.begin { response in
|
||||
completionHandler(response == .OK ? panel.urls : nil)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) is not supported")
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.6.9</string>
|
||||
<string>2026.6.2</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2026060900</string>
|
||||
<string>2026060200</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>OpenClaw</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -21,12 +21,10 @@ private struct DeviceAuthStoreFile: Codable {
|
||||
}
|
||||
|
||||
public enum DeviceAuthStore {
|
||||
public static func loadToken(
|
||||
deviceId: String,
|
||||
role: String,
|
||||
profile: GatewayDeviceIdentityProfile = .primary) -> DeviceAuthEntry?
|
||||
{
|
||||
guard let store = readStore(profile: profile), store.deviceId == deviceId else { return nil }
|
||||
private static let fileName = "device-auth.json"
|
||||
|
||||
public static func loadToken(deviceId: String, role: String) -> DeviceAuthEntry? {
|
||||
guard let store = readStore(), store.deviceId == deviceId else { return nil }
|
||||
let role = self.normalizeRole(role)
|
||||
return store.tokens[role]
|
||||
}
|
||||
@@ -35,11 +33,10 @@ public enum DeviceAuthStore {
|
||||
deviceId: String,
|
||||
role: String,
|
||||
token: String,
|
||||
scopes: [String] = [],
|
||||
profile: GatewayDeviceIdentityProfile = .primary) -> DeviceAuthEntry
|
||||
scopes: [String] = []) -> DeviceAuthEntry
|
||||
{
|
||||
let normalizedRole = self.normalizeRole(role)
|
||||
var next = self.readStore(profile: profile)
|
||||
var next = self.readStore()
|
||||
if next?.deviceId != deviceId {
|
||||
next = DeviceAuthStoreFile(version: 1, deviceId: deviceId, tokens: [:])
|
||||
}
|
||||
@@ -53,25 +50,17 @@ public enum DeviceAuthStore {
|
||||
}
|
||||
next?.tokens[normalizedRole] = entry
|
||||
if let store = next {
|
||||
self.writeStore(store, profile: profile)
|
||||
self.writeStore(store)
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
public static func clearToken(
|
||||
deviceId: String,
|
||||
role: String,
|
||||
profile: GatewayDeviceIdentityProfile = .primary)
|
||||
{
|
||||
guard var store = readStore(profile: profile), store.deviceId == deviceId else { return }
|
||||
public static func clearToken(deviceId: String, role: String) {
|
||||
guard var store = readStore(), store.deviceId == deviceId else { return }
|
||||
let normalizedRole = self.normalizeRole(role)
|
||||
guard store.tokens[normalizedRole] != nil else { return }
|
||||
store.tokens.removeValue(forKey: normalizedRole)
|
||||
self.writeStore(store, profile: profile)
|
||||
}
|
||||
|
||||
public static func clearAll(profile: GatewayDeviceIdentityProfile = .primary) {
|
||||
try? FileManager.default.removeItem(at: self.fileURL(profile: profile))
|
||||
self.writeStore(store)
|
||||
}
|
||||
|
||||
private static func normalizeRole(_ role: String) -> String {
|
||||
@@ -85,14 +74,14 @@ public enum DeviceAuthStore {
|
||||
return Array(Set(trimmed)).sorted()
|
||||
}
|
||||
|
||||
private static func fileURL(profile: GatewayDeviceIdentityProfile) -> URL {
|
||||
private static func fileURL() -> URL {
|
||||
DeviceIdentityPaths.stateDirURL()
|
||||
.appendingPathComponent("identity", isDirectory: true)
|
||||
.appendingPathComponent(profile.authFileName, isDirectory: false)
|
||||
.appendingPathComponent(self.fileName, isDirectory: false)
|
||||
}
|
||||
|
||||
private static func readStore(profile: GatewayDeviceIdentityProfile) -> DeviceAuthStoreFile? {
|
||||
let url = self.fileURL(profile: profile)
|
||||
private static func readStore() -> DeviceAuthStoreFile? {
|
||||
let url = self.fileURL()
|
||||
guard let data = try? Data(contentsOf: url) else { return nil }
|
||||
guard let decoded = try? JSONDecoder().decode(DeviceAuthStoreFile.self, from: data) else {
|
||||
return nil
|
||||
@@ -101,8 +90,8 @@ public enum DeviceAuthStore {
|
||||
return decoded
|
||||
}
|
||||
|
||||
private static func writeStore(_ store: DeviceAuthStoreFile, profile: GatewayDeviceIdentityProfile) {
|
||||
let url = self.fileURL(profile: profile)
|
||||
private static func writeStore(_ store: DeviceAuthStoreFile) {
|
||||
let url = self.fileURL()
|
||||
do {
|
||||
try FileManager.default.createDirectory(
|
||||
at: url.deletingLastPathComponent(),
|
||||
|
||||
@@ -1,29 +1,6 @@
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
|
||||
public enum GatewayDeviceIdentityProfile: String, Sendable {
|
||||
case primary
|
||||
case shareExtension
|
||||
|
||||
var identityFileName: String {
|
||||
switch self {
|
||||
case .primary:
|
||||
"device.json"
|
||||
case .shareExtension:
|
||||
"share-device.json"
|
||||
}
|
||||
}
|
||||
|
||||
var authFileName: String {
|
||||
switch self {
|
||||
case .primary:
|
||||
"device-auth.json"
|
||||
case .shareExtension:
|
||||
"share-device-auth.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct DeviceIdentity: Codable, Sendable {
|
||||
public var deviceId: String
|
||||
public var publicKey: String
|
||||
@@ -42,32 +19,6 @@ enum DeviceIdentityPaths {
|
||||
private static let stateDirEnv = ["OPENCLAW_STATE_DIR"]
|
||||
|
||||
static func stateDirURL() -> URL {
|
||||
self.stateDirURL(
|
||||
overrideURL: self.stateDirOverrideURL(),
|
||||
legacyStateDirURL: self.legacyStateDirURL(),
|
||||
appGroupStateDirURL: self.appGroupStateDirURL(),
|
||||
temporaryDirectory: FileManager.default.temporaryDirectory)
|
||||
}
|
||||
|
||||
static func stateDirURL(
|
||||
overrideURL: URL?,
|
||||
legacyStateDirURL: URL?,
|
||||
appGroupStateDirURL: URL?,
|
||||
temporaryDirectory: URL) -> URL
|
||||
{
|
||||
if let overrideURL {
|
||||
return overrideURL
|
||||
}
|
||||
if let appGroupStateDirURL {
|
||||
return appGroupStateDirURL
|
||||
}
|
||||
if let legacyStateDirURL {
|
||||
return legacyStateDirURL
|
||||
}
|
||||
return temporaryDirectory.appendingPathComponent("openclaw", isDirectory: true)
|
||||
}
|
||||
|
||||
private static func stateDirOverrideURL() -> URL? {
|
||||
for key in self.stateDirEnv {
|
||||
if let raw = getenv(key) {
|
||||
let value = String(cString: raw).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
@@ -76,49 +27,34 @@ enum DeviceIdentityPaths {
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func legacyStateDirURL() -> URL? {
|
||||
if let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first {
|
||||
return appSupport.appendingPathComponent("OpenClaw", isDirectory: true)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func appGroupStateDirURL() -> URL? {
|
||||
guard
|
||||
let containerURL = FileManager.default
|
||||
.containerURL(forSecurityApplicationGroupIdentifier: OpenClawAppGroup.identifier)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
return containerURL.appendingPathComponent("OpenClaw", isDirectory: true)
|
||||
return FileManager.default.temporaryDirectory.appendingPathComponent("openclaw", isDirectory: true)
|
||||
}
|
||||
}
|
||||
|
||||
public enum DeviceIdentityStore {
|
||||
private static let fileName = "device.json"
|
||||
private static let ed25519SPKIPrefix = Data([
|
||||
0x30, 0x2A, 0x30, 0x05, 0x06, 0x03, 0x2B, 0x65,
|
||||
0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65,
|
||||
0x70, 0x03, 0x21, 0x00,
|
||||
])
|
||||
private static let ed25519PKCS8PrivatePrefix = Data([
|
||||
0x30, 0x2E, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06,
|
||||
0x03, 0x2B, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20,
|
||||
0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06,
|
||||
0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20,
|
||||
])
|
||||
|
||||
public static func loadOrCreate() -> DeviceIdentity {
|
||||
self.loadOrCreate(profile: .primary)
|
||||
}
|
||||
|
||||
public static func loadOrCreate(profile: GatewayDeviceIdentityProfile) -> DeviceIdentity {
|
||||
self.loadOrCreate(fileURL: self.fileURL(profile: profile))
|
||||
self.loadOrCreate(fileURL: self.fileURL())
|
||||
}
|
||||
|
||||
static func loadOrCreate(fileURL url: URL) -> DeviceIdentity {
|
||||
if let data = try? Data(contentsOf: url) {
|
||||
switch self.decodeStoredIdentity(data) {
|
||||
case let .identity(decoded):
|
||||
case .identity(let decoded):
|
||||
return decoded
|
||||
case .recognizedInvalid:
|
||||
return self.generate()
|
||||
@@ -207,7 +143,7 @@ public enum DeviceIdentityStore {
|
||||
let privateKeyData = Data(base64Encoded: identity.privateKey)
|
||||
else { return nil }
|
||||
|
||||
guard publicKeyData.count == 32, privateKeyData.count == 32,
|
||||
guard publicKeyData.count == 32 && privateKeyData.count == 32,
|
||||
self.keyPairMatches(publicKeyData: publicKeyData, privateKeyData: privateKeyData)
|
||||
else { return nil }
|
||||
return DeviceIdentity(
|
||||
@@ -275,11 +211,11 @@ public enum DeviceIdentityStore {
|
||||
}
|
||||
}
|
||||
|
||||
private static func fileURL(profile: GatewayDeviceIdentityProfile) -> URL {
|
||||
private static func fileURL() -> URL {
|
||||
let base = DeviceIdentityPaths.stateDirURL()
|
||||
return base
|
||||
.appendingPathComponent("identity", isDirectory: true)
|
||||
.appendingPathComponent(profile.identityFileName, isDirectory: false)
|
||||
.appendingPathComponent(self.fileName, isDirectory: false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -107,7 +107,6 @@ public struct GatewayConnectOptions: Sendable {
|
||||
public var clientId: String
|
||||
public var clientMode: String
|
||||
public var clientDisplayName: String?
|
||||
public var deviceIdentityProfile: GatewayDeviceIdentityProfile
|
||||
/// When false, the connection omits the signed device identity payload and cannot use
|
||||
/// device-scoped auth (role/scope upgrades will require pairing). Keep this true for
|
||||
/// role/scoped sessions such as operator UI clients.
|
||||
@@ -123,7 +122,6 @@ public struct GatewayConnectOptions: Sendable {
|
||||
clientId: String,
|
||||
clientMode: String,
|
||||
clientDisplayName: String?,
|
||||
deviceIdentityProfile: GatewayDeviceIdentityProfile = .primary,
|
||||
includeDeviceIdentity: Bool = true)
|
||||
{
|
||||
self.role = role
|
||||
@@ -135,7 +133,6 @@ public struct GatewayConnectOptions: Sendable {
|
||||
self.clientId = clientId
|
||||
self.clientMode = clientMode
|
||||
self.clientDisplayName = clientDisplayName
|
||||
self.deviceIdentityProfile = deviceIdentityProfile
|
||||
self.includeDeviceIdentity = includeDeviceIdentity
|
||||
}
|
||||
}
|
||||
@@ -439,15 +436,13 @@ public actor GatewayChannelActor {
|
||||
let clientId = options.clientId
|
||||
let clientMode = options.clientMode
|
||||
let role = options.role
|
||||
let deviceIdentityProfile = options.deviceIdentityProfile
|
||||
let requestedScopes = options.scopes
|
||||
let scopesAreExplicit = options.scopesAreExplicit
|
||||
let includeDeviceIdentity = options.includeDeviceIdentity
|
||||
let identity = includeDeviceIdentity ? DeviceIdentityStore.loadOrCreate(profile: deviceIdentityProfile) : nil
|
||||
let identity = includeDeviceIdentity ? DeviceIdentityStore.loadOrCreate() : nil
|
||||
let selectedAuth = self.selectConnectAuth(
|
||||
role: role,
|
||||
includeDeviceIdentity: includeDeviceIdentity,
|
||||
deviceIdentityProfile: deviceIdentityProfile,
|
||||
deviceId: identity?.deviceId,
|
||||
requestedScopes: requestedScopes)
|
||||
let scopes = self.resolveConnectScopes(
|
||||
@@ -537,11 +532,7 @@ public actor GatewayChannelActor {
|
||||
try await self.task?.send(.data(data))
|
||||
do {
|
||||
let response = try await self.waitForConnectResponse(reqId: reqId)
|
||||
try await self.handleConnectResponse(
|
||||
response,
|
||||
identity: identity,
|
||||
role: role,
|
||||
deviceIdentityProfile: deviceIdentityProfile)
|
||||
try await self.handleConnectResponse(response, identity: identity, role: role)
|
||||
self.pendingDeviceTokenRetry = false
|
||||
self.deviceTokenRetryBudgetUsed = false
|
||||
} catch {
|
||||
@@ -559,10 +550,7 @@ public actor GatewayChannelActor {
|
||||
self.shouldClearStoredDeviceTokenAfterRetry(error)
|
||||
{
|
||||
// Retry failed with an explicit device-token mismatch; clear stale local token.
|
||||
DeviceAuthStore.clearToken(
|
||||
deviceId: identity.deviceId,
|
||||
role: role,
|
||||
profile: deviceIdentityProfile)
|
||||
DeviceAuthStore.clearToken(deviceId: identity.deviceId, role: role)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
@@ -571,7 +559,6 @@ public actor GatewayChannelActor {
|
||||
private func selectConnectAuth(
|
||||
role: String,
|
||||
includeDeviceIdentity: Bool,
|
||||
deviceIdentityProfile: GatewayDeviceIdentityProfile,
|
||||
deviceId: String?,
|
||||
requestedScopes: [String]) -> SelectedConnectAuth
|
||||
{
|
||||
@@ -581,7 +568,7 @@ public actor GatewayChannelActor {
|
||||
let explicitPassword = self.password?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty
|
||||
let storedEntry =
|
||||
(includeDeviceIdentity && deviceId != nil)
|
||||
? DeviceAuthStore.loadToken(deviceId: deviceId!, role: role, profile: deviceIdentityProfile)
|
||||
? DeviceAuthStore.loadToken(deviceId: deviceId!, role: role)
|
||||
: nil
|
||||
let storedToken = storedEntry?.token
|
||||
let storedScopes = storedEntry?.scopes ?? []
|
||||
@@ -769,8 +756,7 @@ public actor GatewayChannelActor {
|
||||
deviceId: String,
|
||||
role: String,
|
||||
token: String,
|
||||
scopes: [String],
|
||||
deviceIdentityProfile: GatewayDeviceIdentityProfile)
|
||||
scopes: [String])
|
||||
{
|
||||
guard let filteredScopes = self.filteredBootstrapHandoffScopes(role: role, scopes: scopes) else {
|
||||
return
|
||||
@@ -779,8 +765,7 @@ public actor GatewayChannelActor {
|
||||
deviceId: deviceId,
|
||||
role: role,
|
||||
token: token,
|
||||
scopes: filteredScopes,
|
||||
profile: deviceIdentityProfile)
|
||||
scopes: filteredScopes)
|
||||
}
|
||||
|
||||
private func persistIssuedDeviceToken(
|
||||
@@ -788,8 +773,7 @@ public actor GatewayChannelActor {
|
||||
deviceId: String,
|
||||
role: String,
|
||||
token: String,
|
||||
scopes: [String],
|
||||
deviceIdentityProfile: GatewayDeviceIdentityProfile)
|
||||
scopes: [String])
|
||||
{
|
||||
if authSource == .bootstrapToken {
|
||||
guard self.shouldPersistBootstrapHandoffTokens() else {
|
||||
@@ -799,23 +783,20 @@ public actor GatewayChannelActor {
|
||||
deviceId: deviceId,
|
||||
role: role,
|
||||
token: token,
|
||||
scopes: scopes,
|
||||
deviceIdentityProfile: deviceIdentityProfile)
|
||||
scopes: scopes)
|
||||
return
|
||||
}
|
||||
_ = DeviceAuthStore.storeToken(
|
||||
deviceId: deviceId,
|
||||
role: role,
|
||||
token: token,
|
||||
scopes: scopes,
|
||||
profile: deviceIdentityProfile)
|
||||
scopes: scopes)
|
||||
}
|
||||
|
||||
private func handleConnectResponse(
|
||||
_ res: ResponseFrame,
|
||||
identity: DeviceIdentity?,
|
||||
role: String,
|
||||
deviceIdentityProfile: GatewayDeviceIdentityProfile) async throws
|
||||
role: String) async throws
|
||||
{
|
||||
if res.ok == false {
|
||||
let error = res.error
|
||||
@@ -874,8 +855,7 @@ public actor GatewayChannelActor {
|
||||
deviceId: identity.deviceId,
|
||||
role: authRole,
|
||||
token: deviceToken,
|
||||
scopes: scopes,
|
||||
deviceIdentityProfile: deviceIdentityProfile)
|
||||
scopes: scopes)
|
||||
}
|
||||
if self.shouldPersistBootstrapHandoffTokens(),
|
||||
let tokenEntries = auth["deviceTokens"]?.value as? [ProtoAnyCodable]
|
||||
@@ -893,8 +873,7 @@ public actor GatewayChannelActor {
|
||||
deviceId: identity.deviceId,
|
||||
role: authRole,
|
||||
token: deviceToken,
|
||||
scopes: scopes,
|
||||
deviceIdentityProfile: deviceIdentityProfile)
|
||||
scopes: scopes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +162,6 @@ public actor GatewayNodeSession {
|
||||
let clientId = options.clientId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let clientMode = options.clientMode.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let clientDisplayName = (options.clientDisplayName ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let deviceIdentityProfile = options.deviceIdentityProfile.rawValue
|
||||
let includeDeviceIdentity = options.includeDeviceIdentity ? "1" : "0"
|
||||
let permissions = options.permissions
|
||||
.map { key, value in
|
||||
@@ -180,7 +179,6 @@ public actor GatewayNodeSession {
|
||||
clientId,
|
||||
clientMode,
|
||||
clientDisplayName,
|
||||
deviceIdentityProfile,
|
||||
includeDeviceIdentity,
|
||||
permissions,
|
||||
].joined(separator: "|")
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public enum OpenClawAppGroup {
|
||||
public static let canonicalIdentifier = "group.ai.openclawfoundation.app.shared"
|
||||
|
||||
public static var identifier: String {
|
||||
let raw = Bundle.main.object(forInfoDictionaryKey: "OpenClawAppGroupIdentifier") as? String
|
||||
let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
return trimmed.isEmpty ? self.canonicalIdentifier : trimmed
|
||||
}
|
||||
}
|
||||
@@ -69,17 +69,6 @@
|
||||
"fileName"
|
||||
]
|
||||
},
|
||||
"api": {
|
||||
"emoji": "🌐",
|
||||
"title": "API",
|
||||
"detailKeys": [
|
||||
"url",
|
||||
"endpoint",
|
||||
"path",
|
||||
"method",
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"browser": {
|
||||
"emoji": "🌐",
|
||||
"title": "Browser",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user