mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-28 10:21:45 +08:00
Compare commits
1 Commits
codex/slac
...
aknight/ex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd5cf54f75 |
@@ -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.
|
||||
|
||||
@@ -15,9 +15,12 @@ committed `inventory/` report tree.
|
||||
This skill owns the operational workflow for:
|
||||
|
||||
- `taxonomy.yaml`
|
||||
- `qa/maturity-scores.yaml`
|
||||
- `docs/concepts/qa-e2e-automation.md`
|
||||
- `qa/scenarios/index.yaml`
|
||||
- `docs/maturity-scores.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,57 +31,35 @@ 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.
|
||||
- `qa/maturity-scores.yaml` is the committed aggregate source for Quality,
|
||||
Completeness, and LTS review state.
|
||||
- `extensions/qa-lab/src/scorecard-taxonomy.ts` exports
|
||||
`qaMaturityScoresSchema` and `readValidatedQaMaturityScoreSources`; use those
|
||||
QA Lab utilities to validate score output.
|
||||
- Generated public docs are `docs/maturity/scorecard.md` and
|
||||
`docs/maturity/taxonomy.md`; both come from `pnpm maturity:render`. Do not
|
||||
hand-edit generated Markdown to change score results.
|
||||
- `qa-evidence.json` artifacts provide per-run QA scorecard evidence. Release
|
||||
profile artifacts are the source of truth for Coverage. They can enrich
|
||||
generated artifact docs, but they are not committed as inventory.
|
||||
- `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.
|
||||
- `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.
|
||||
|
||||
## Commands
|
||||
|
||||
Run from the openclaw repo root.
|
||||
|
||||
Validate taxonomy YAML structure and the maturity score schema after source
|
||||
edits:
|
||||
Render committed docs:
|
||||
|
||||
```bash
|
||||
node --import tsx --input-type=module <<'NODE'
|
||||
import fs from "node:fs";
|
||||
import YAML from "yaml";
|
||||
import { readValidatedQaMaturityScoreSources } from "./extensions/qa-lab/src/scorecard-taxonomy.ts";
|
||||
|
||||
for (const file of ["taxonomy.yaml", "qa/scenarios/index.yaml"]) {
|
||||
YAML.parse(fs.readFileSync(file, "utf8"));
|
||||
}
|
||||
readValidatedQaMaturityScoreSources();
|
||||
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
|
||||
@@ -90,17 +71,17 @@ When asked to score or refresh a surface:
|
||||
`.agents/skills/claw-score/references/completeness/`.
|
||||
3. Gather public repo evidence from docs, source, tests, and QA scenario
|
||||
metadata.
|
||||
4. Prefer existing release profile `qa-evidence.json` artifacts for executed
|
||||
proof.
|
||||
5. Update `qa/maturity-scores.yaml` only for Quality, Completeness, and LTS
|
||||
review state backed by public or redacted artifact evidence.
|
||||
6. Run the schema 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.
|
||||
4. Prefer existing `qa-evidence.json` artifacts for executed proof. Do not use
|
||||
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 `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 `qa/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
|
||||
|
||||
@@ -146,7 +127,7 @@ Default guidance:
|
||||
|
||||
Default Completeness bands:
|
||||
|
||||
- `Clawesome` (95-100): complete across expected workflows, variants, and
|
||||
- `Lovable` (95-100): complete across expected workflows, variants, and
|
||||
recovery branches, with only minor polish gaps.
|
||||
- `Stable` (80-95): the expected workflow set is broadly present, with only
|
||||
bounded missing branches.
|
||||
@@ -159,28 +140,31 @@ Default Completeness bands:
|
||||
|
||||
## Score Semantics
|
||||
|
||||
- Coverage: deterministic release validation coverage derived from the release
|
||||
profile `qa-evidence.json.scorecard` feature fulfillment data.
|
||||
- Coverage: public or redacted proof that the feature is exercised by docs,
|
||||
tests, QA scenarios, live lanes, or release evidence.
|
||||
- Quality: reliability, maintainability, operator safety, and regression
|
||||
confidence for the category.
|
||||
- Completeness: how much of the intended operator-visible workflow exists for
|
||||
the category. Use the default completeness process plus any surface-specific
|
||||
variation before changing this score.
|
||||
- LTS: derived from Quality, release-evidence Coverage, and
|
||||
`human_lts_override`; do not hand-edit generated Markdown to change LTS
|
||||
status.
|
||||
- LTS: derived from score thresholds and `human_lts_override`; do not hand-edit
|
||||
generated Markdown to change LTS status.
|
||||
|
||||
Bands:
|
||||
|
||||
- `Clawesome`: 95-100
|
||||
- `Lovable`: 95-100
|
||||
- `Stable`: 80-95
|
||||
- `Beta`: 70-80
|
||||
- `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.
|
||||
|
||||
@@ -12,10 +12,10 @@ content, ordering, grouping, and attribution discipline.
|
||||
|
||||
## Goal
|
||||
|
||||
Rebuild the target `CHANGELOG.md` version section from a complete, generated
|
||||
history manifest, not stale draft notes. Produce grouped user-facing release
|
||||
notes sorted by user interest while preserving every relevant issue/PR ref and
|
||||
every human `Thanks @...` attribution.
|
||||
Rewrite the target `CHANGELOG.md` version section from history, not from stale
|
||||
draft notes. Produce grouped user-facing release notes sorted by user interest
|
||||
while preserving every relevant issue/PR ref and every human `Thanks @...`
|
||||
attribution.
|
||||
|
||||
## Inputs
|
||||
|
||||
@@ -34,37 +34,8 @@ every human `Thanks @...` attribution.
|
||||
- `git log --first-parent --date=iso-strict --pretty=format:'%h%x09%ad%x09%s' <base-tag>..<target-ref>`
|
||||
- `git log --first-parent --grep='(#' --date=short --pretty=format:'%h%x09%ad%x09%s' <base-tag>..<target-ref>`
|
||||
- also inspect `--since='24 hours ago'` when main moved during the release.
|
||||
3. Generate the complete contribution record and editorial manifest before
|
||||
writing grouped prose:
|
||||
|
||||
```bash
|
||||
node .agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs \
|
||||
--base <base-tag> \
|
||||
--target <target-ref> \
|
||||
--version <YYYY.M.PATCH> \
|
||||
--manifest /tmp/openclaw-release-<YYYY.M.PATCH>.json \
|
||||
--write-ledger
|
||||
```
|
||||
|
||||
- the manifest is the required input to the rewrite, not an after-the-fact
|
||||
audit; it contains every referenced PR, eligible contributor credit,
|
||||
inline issue context, every direct commit, and an editorial-eligibility
|
||||
classification for PRs and direct commits
|
||||
- for a historical backfill, add `--seed-ref <pre-backfill-ref>` once so
|
||||
contribution records from the prior changelog are retained even when an
|
||||
older merged commit omitted its PR number; the verifier excludes records
|
||||
for work reverted after the base tag, including beta work reverted before
|
||||
the stable release
|
||||
- source PR discovery combines merged GitHub commit associations with merged
|
||||
PR references explicitly present in active commit subjects/bodies so
|
||||
cherry-picks and squash commits remain accounted for. Resolve every
|
||||
association page and exclude PRs merged after the target release commit
|
||||
- read the manifest before editing `### Highlights`, `### Changes`, or
|
||||
`### Fixes`; do not carry old grouped prose forward without re-auditing it
|
||||
- inspect linked PRs/issues or diffs for ambiguous commits. Direct commits
|
||||
are editorial input, not public ledger rows; infer material user outcomes
|
||||
from subject, body, touched files, tests, and nearby commits
|
||||
|
||||
3. Read linked PRs/issues or diffs for ambiguous commits. Direct commits matter;
|
||||
infer notes from subject, body, touched files, tests, and nearby commits.
|
||||
4. Rewrite one stable-base section only:
|
||||
- use `## YYYY.M.PATCH`
|
||||
- do not create beta-specific headings
|
||||
@@ -73,21 +44,10 @@ every human `Thanks @...` attribution.
|
||||
section instead of deleting them
|
||||
5. Section shape:
|
||||
- `### Highlights`: 5-8 bullets, broad user wins first
|
||||
- include only a clear user-visible capability or workflow unlock, a
|
||||
material reliability/safety fix, a broad cross-surface improvement, or
|
||||
a release-defining integration/compatibility milestone
|
||||
- every highlight must say what changed for a user in one sentence; use
|
||||
one user story per bullet and group its supporting PRs
|
||||
- exclude tests, CI, refactors, docs, catalog churn, and implementation
|
||||
detail unless the outcome is a material install/update, data-safety, or
|
||||
widely visible user improvement
|
||||
- `### Changes`: new capabilities and behavior changes
|
||||
- `### Fixes`: user-facing fixes first, grouped by impact and surface
|
||||
- group related changes/fixes by surface and user impact; avoid one bullet
|
||||
per tiny commit when several commits tell one user-facing story
|
||||
- `### Complete contribution record`: generated PR-first record after the
|
||||
grouped prose; it is the exhaustive accounting surface, not a second
|
||||
release summary
|
||||
6. Preserve attribution:
|
||||
- keep `#issue`, `(#PR)`, `Fixes #...`, and `Thanks @...`
|
||||
- every human-authored merged PR represented by a user-facing entry needs
|
||||
@@ -102,35 +62,17 @@ every human `Thanks @...` attribution.
|
||||
- multiple `Thanks @...` handles in one bullet are expected; do not drop or
|
||||
collapse contributor credit just because the note is grouped
|
||||
- if one grouped bullet covers both direct commits and PRs, keep all PR refs
|
||||
and thanks, plus any issue refs and human credit from the direct work
|
||||
- issues remain normal inline `#NNN` references. Do not add a separate
|
||||
linked-issues inventory. The generated PR record keeps source issues
|
||||
inline as `Related #NNN` on the PR that shipped them
|
||||
- when backfilling an older linked-issues inventory, preserve reporter
|
||||
credit inline for every GitHub-confirmed closing PR relationship. Do not
|
||||
infer a PR relationship from a generic cross-reference event, invent an
|
||||
unrelated PR link for a standalone report, or recreate the retired
|
||||
inventory
|
||||
- the complete contribution record lists every merged source PR exactly once
|
||||
as `**PR #NNN**`; source PRs include GitHub commit associations and merged
|
||||
PR references explicitly present in active commit subjects/bodies. It
|
||||
preserves author/co-author credit and any issue references in the original
|
||||
title
|
||||
- direct commits remain in the manifest with GitHub-resolved author,
|
||||
co-author, issue, and editorial-eligibility data. They inform grouped
|
||||
prose but are never rendered as a public `#### Direct commits` dump. Add
|
||||
direct-commit credit to a grouped bullet only when it shares an explicit
|
||||
closing issue reference or at least two distinctive subject terms
|
||||
- the verifier rejects `docs`, `test`, `refactor`, `ci`, `build`, `chore`,
|
||||
and `style` PRs in Highlights, Changes, or Fixes. Keep those internal
|
||||
contributions in the complete PR record, but do not give them editorial
|
||||
release-note space
|
||||
- classify internal-only work from conventional prefixes and clear title
|
||||
signals such as `QA`, `test`, `docs`, `refactor`, `lint`, or `CI`; an
|
||||
untyped title is not automatically editorial
|
||||
and thanks, plus any issue refs from the direct commits
|
||||
- before finalizing, audit the final release-note body:
|
||||
- extract all `#NNN` refs from the notes
|
||||
- resolve which refs are PRs and collect human PR authors
|
||||
- resolve issue refs used as bug/report refs and collect human reporters
|
||||
- scan represented commits for `Co-authored-by`
|
||||
- compare those handles to the final `Thanks @...` set
|
||||
- fix every missing human credit or explicitly record why it is omitted
|
||||
- do not add GHSA references, advisory IDs, or security advisory slugs to
|
||||
changelog entries or GitHub release-note text unless explicitly requested
|
||||
- never thank bots, `@claude`, `@openclaw`, `@clawsweeper`, or `@steipete`
|
||||
- never thank bots, `@openclaw`, `@clawsweeper`, or `@steipete`
|
||||
- do not use GitHub's release contributor count as the source of truth; the
|
||||
changelog must carry the complete human credit set itself
|
||||
7. Sorting preference:
|
||||
@@ -149,50 +91,36 @@ every human `Thanks @...` attribution.
|
||||
- if any compatibility `removeAfter` is on/before release date, resolve it
|
||||
or explicitly record the blocker before shipping
|
||||
10. Validate and ship:
|
||||
|
||||
- after the manifest-driven rewrite, regenerate and verify the complete
|
||||
contribution record before committing:
|
||||
```bash
|
||||
node .agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs \
|
||||
--base <base-tag> \
|
||||
--target <target-ref> \
|
||||
--version <YYYY.M.PATCH> \
|
||||
--manifest /tmp/openclaw-release-<YYYY.M.PATCH>.json \
|
||||
--write-ledger
|
||||
```
|
||||
- the command fails when any `#NNN` reference in release history or the
|
||||
rendered release section cannot resolve, when reverted work is presented
|
||||
as shipped, when a source PR is absent from the contribution record, when
|
||||
direct commits are rendered as a public record dump, when non-editorial
|
||||
PRs appear in grouped prose, or when an eligible PR author or known
|
||||
co-author is missing from that PR's `Thanks @...` credit
|
||||
- when grouped prose names a PR, that same bullet must retain every
|
||||
contributor and linked-reporter credit from its generated PR record
|
||||
- unqualified `#NNN` references resolve against `openclaw/openclaw`;
|
||||
cross-repository references such as `openclaw/imsg#141` remain literal
|
||||
text and must not be rewritten as local issue links
|
||||
- after the GitHub release or prerelease is published, verify every matching
|
||||
release page against the same source section:
|
||||
```bash
|
||||
node .agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs \
|
||||
--base <base-tag> \
|
||||
--target <target-ref> \
|
||||
--version <YYYY.M.PATCH> \
|
||||
--release-tag v<YYYY.M.PATCH> \
|
||||
--check-github
|
||||
```
|
||||
- add one `--release-tag` for every beta and stable page in the train; a
|
||||
`### Release verification` tail is permitted, but any other body drift
|
||||
fails the check; the GitHub body must begin with the complete
|
||||
`## YYYY.M.PATCH` changelog section, including its heading
|
||||
- GitHub release bodies are limited to 125,000 characters. If the complete
|
||||
source section plus an existing verification tail exceeds that limit, keep
|
||||
the source section intact and omit the tail; never truncate the
|
||||
contribution record
|
||||
- `git diff --check`
|
||||
- for docs/changelog-only changes, no broad tests are required
|
||||
- commit with `scripts/committer "docs(changelog): refresh YYYY.M.PATCH notes" CHANGELOG.md`
|
||||
- push, pull/rebase if needed, then branch/rebase release from latest `main`
|
||||
- generate and verify the complete contribution ledger before committing:
|
||||
```bash
|
||||
node .agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs \
|
||||
--base <base-tag> \
|
||||
--target <target-ref> \
|
||||
--version <YYYY.M.PATCH> \
|
||||
--write-ledger
|
||||
```
|
||||
- the command fails when any `#NNN` reference in release history or the
|
||||
rendered release section is absent from the ledger, when reverted work is
|
||||
presented as shipped, or when an eligible PR author, issue reporter, or
|
||||
known co-author is missing from that entry's `Thanks @...` credit
|
||||
- after the GitHub release or prerelease is published, verify every matching
|
||||
release page against the same source section:
|
||||
```bash
|
||||
node .agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs \
|
||||
--base <base-tag> \
|
||||
--target <target-ref> \
|
||||
--version <YYYY.M.PATCH> \
|
||||
--release-tag v<YYYY.M.PATCH> \
|
||||
--check-github
|
||||
```
|
||||
- add one `--release-tag` for every beta and stable page in the train; a
|
||||
`### Release verification` tail is permitted, but any other body drift
|
||||
fails the check; the GitHub body must begin with the complete
|
||||
`## YYYY.M.PATCH` changelog section, including its heading
|
||||
- `git diff --check`
|
||||
- for docs/changelog-only changes, no broad tests are required
|
||||
- commit with `scripts/committer "docs(changelog): refresh YYYY.M.PATCH notes" CHANGELOG.md`
|
||||
- push, pull/rebase if needed, then branch/rebase release from latest `main`
|
||||
|
||||
## Quota / API Outage Rule
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,197 +0,0 @@
|
||||
---
|
||||
name: openclaw-ci-limits
|
||||
description: Manage OpenClaw GitHub Actions and Blacksmith CI capacity, runner-registration budgets, fanout caps, main-push debounce, shard sizing, hosted-runner offload, queue health, and safe ramp-down/ramp-up changes. Use when tuning `.github/workflows/*`, `docs/ci.md`, CI runner labels, matrix `max-parallel`, ClawSweeper/Blacksmith burst protection, CodeQL runner placement, or investigating slow/queued OpenClaw CI.
|
||||
---
|
||||
|
||||
# OpenClaw CI Limits
|
||||
|
||||
Use this skill for CI capacity changes, not ordinary test failure triage. The
|
||||
goal is to keep OpenClaw fast while staying below GitHub's self-hosted runner
|
||||
registration edge limit.
|
||||
|
||||
## Core Facts
|
||||
|
||||
- The scarce resource is Blacksmith runner registrations, not Blacksmith vCPU
|
||||
capacity.
|
||||
- GitHub runner registrations for `openclaw` are currently capped at 3,000 per
|
||||
5 minutes per repository, organization, or enterprise. The `openclaw`
|
||||
organization shares one bucket.
|
||||
- Core REST quota does not draw down this bucket. Check
|
||||
`actions_runner_registration` separately; core quota can be healthy while
|
||||
runner registration is throttled.
|
||||
- Use 2,000 registrations per 5 minutes as the operating target. Leave the last
|
||||
third for other repos, retries, and burst overlap.
|
||||
- Jobs that route, notify, summarize, choose shards, or run short CodeQL quality
|
||||
scans should stay on GitHub-hosted runners unless measured evidence says
|
||||
Blacksmith is required.
|
||||
|
||||
## First Checks
|
||||
|
||||
Before changing CI, collect current pressure:
|
||||
|
||||
```bash
|
||||
ghx api rate_limit --jq '{core:.resources.core,graphql:.resources.graphql,search:.resources.search,actions_runner_registration:.resources.actions_runner_registration}'
|
||||
ghx run list -R openclaw/openclaw --limit 20 --json databaseId,status,conclusion,workflowName,event,headBranch,createdAt,updatedAt,url
|
||||
ghx run list -R openclaw/clawsweeper --limit 20 --json databaseId,status,conclusion,workflowName,event,headBranch,createdAt,updatedAt,url
|
||||
curl -fsS https://clawsweeper.openclaw.ai/api/status | jq '{generated_at,fleet,diagnostics:{errors:.diagnostics.errors}}'
|
||||
curl -fsS https://clawsweeper.openclaw.ai/api/exact-review-queue | jq '.'
|
||||
node scripts/ci-run-timings.mjs --latest-main
|
||||
node scripts/ci-run-timings.mjs --recent 10
|
||||
```
|
||||
|
||||
Read:
|
||||
|
||||
- `.github/workflows/ci.yml`
|
||||
- `.github/workflows/codeql-critical-quality.yml`
|
||||
- `docs/ci.md`
|
||||
- `test/scripts/ci-workflow-guards.test.ts`
|
||||
- touched planner files under `scripts/lib/*ci*`, `scripts/lib/*test-plan*`, or
|
||||
`scripts/ci-changed-scope.mjs`
|
||||
|
||||
## Diagnose The Bottleneck
|
||||
|
||||
Classify the issue before changing caps:
|
||||
|
||||
- **Runner-registration throttle:** many jobs queued before runner assignment,
|
||||
Blacksmith/GitHub reports 403/429 or spam-style 422 responses from
|
||||
`generate-jitconfig`, and API core quota is still healthy. Treat 422 as this
|
||||
signal only when the request payload is otherwise valid. Fix burstiness and
|
||||
Blacksmith job count.
|
||||
- **Blacksmith capacity:** Blacksmith dashboard shows actual concurrency caps or
|
||||
unavailable capacity. Do not solve this with GitHub workflow fanout alone.
|
||||
- **OpenClaw test runtime:** jobs start quickly but one lane dominates wall time.
|
||||
Use `$openclaw-test-performance` instead of runner tuning.
|
||||
- **Real failing CI:** one job fails after starting. Use `$github:gh-fix-ci` or
|
||||
`$openclaw-testing`, not this skill.
|
||||
- **ClawSweeper backlog:** exact-review queue grows while CI is healthy. Tune
|
||||
ClawSweeper workers in `openclaw/clawsweeper`, not OpenClaw CI.
|
||||
|
||||
## Registration Budget Math
|
||||
|
||||
Estimate worst-case registrations for a change before editing:
|
||||
|
||||
```text
|
||||
new Blacksmith registrations ~= number of Blacksmith jobs that can become queued
|
||||
inside one 5 minute window
|
||||
```
|
||||
|
||||
For matrix jobs, count every row that can start in the 5-minute window.
|
||||
`strategy.max-parallel` only caps simultaneous rows; short rows can turn over
|
||||
and register more runners before the window resets. Use job duration, retries,
|
||||
and queue turnover to justify any lower estimate. Add non-matrix Blacksmith jobs
|
||||
such as `preflight`, `security-fast`, `build-artifacts`, and platform lanes.
|
||||
|
||||
For repeated pushes, multiply by the number of runs expected to reach
|
||||
Blacksmith admission in the same 5-minute window, including runs canceled after
|
||||
admission. The debounce only suppresses pushes that arrive while
|
||||
`runner-admission` is still sleeping; once Blacksmith jobs register, those
|
||||
registrations are spent even if a later push cancels the run. If timing is
|
||||
uncertain, count every sequential push in the window.
|
||||
|
||||
Reject a change unless the org-level worst case stays below 2,000 registrations
|
||||
per 5 minutes with headroom for ClawSweeper, ClawHub, Clownfish, OpenClaw RTT,
|
||||
and Clawbench.
|
||||
|
||||
## Safe Levers
|
||||
|
||||
Prefer these in order:
|
||||
|
||||
1. Add or preserve concurrency groups that cancel superseded PR and canonical
|
||||
`main` runs before Blacksmith work starts.
|
||||
2. Keep the `runner-admission` hosted debounce for canonical `main` pushes.
|
||||
Change `OPENCLAW_MAIN_CI_DEBOUNCE_SECONDS` only with evidence.
|
||||
3. Move high-frequency, short, non-build jobs to `ubuntu-24.04`.
|
||||
4. Reduce matrix rows by bundling related tests inside one runner job when the
|
||||
combined job stays under timeout and keeps useful failure names.
|
||||
5. Lower `strategy.max-parallel` for bursty Blacksmith matrices.
|
||||
6. Right-size runners from timing evidence. Use fewer/larger jobs only when
|
||||
elapsed time improves enough to justify registration count.
|
||||
7. Split truly slow tests with `$openclaw-test-performance`; do not hide a slow
|
||||
test problem by registering more runners.
|
||||
|
||||
Do not:
|
||||
|
||||
- add another Blacksmith installation expecting a higher registration bucket;
|
||||
- move CodeQL Critical Quality back to Blacksmith;
|
||||
- raise all `max-parallel` values at once;
|
||||
- make manual `workflow_dispatch` runs cancel normal push/PR validation;
|
||||
- delete coverage just to reduce runner count;
|
||||
- treat cancelled superseded runs as failures without checking the newest run
|
||||
for the same ref.
|
||||
|
||||
## Current OpenClaw Knobs
|
||||
|
||||
These are intentionally guarded by `test/scripts/ci-workflow-guards.test.ts`:
|
||||
|
||||
- `CI` concurrency key version and `cancel-in-progress` for PRs and canonical
|
||||
`main` pushes.
|
||||
- `runner-admission` on `ubuntu-24.04` with
|
||||
`OPENCLAW_MAIN_CI_DEBOUNCE_SECONDS=90`.
|
||||
- `preflight` and `security-fast` needing `runner-admission`.
|
||||
- CI matrix caps: fast/check lanes at 12, Node test shards at 24, Windows and
|
||||
Android at 2.
|
||||
- `build-artifacts` on `blacksmith-16vcpu-ubuntu-2404`.
|
||||
- lower-weight Node/check shards on `blacksmith-4vcpu-ubuntu-2404`.
|
||||
- heavy retained Linux/Android shards on `blacksmith-8vcpu-ubuntu-2404`.
|
||||
- CodeQL Critical Quality on `ubuntu-24.04` with no `blacksmith-` labels.
|
||||
|
||||
When changing one knob, update `docs/ci.md` and the guard test in the same PR.
|
||||
|
||||
## Validation
|
||||
|
||||
For workflow-only or docs/skill-only changes in a Codex worktree:
|
||||
|
||||
```bash
|
||||
node scripts/run-vitest.mjs test/scripts/ci-workflow-guards.test.ts
|
||||
node scripts/check-workflows.mjs
|
||||
node scripts/docs-list.js
|
||||
./node_modules/.bin/oxfmt --check .github/workflows/ci.yml .github/workflows/codeql-critical-quality.yml docs/ci.md test/scripts/ci-workflow-guards.test.ts .agents/skills/openclaw-ci-limits/SKILL.md .agents/skills/openclaw-ci-limits/agents/openai.yaml
|
||||
git diff --check
|
||||
```
|
||||
|
||||
If `pnpm docs:list` tries to reconcile dependencies in a linked Codex worktree,
|
||||
stop and use `node scripts/docs-list.js`.
|
||||
|
||||
For a PR before requesting maintainer approval:
|
||||
|
||||
```bash
|
||||
.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main
|
||||
ghx pr checks <pr> -R openclaw/openclaw --watch --interval 15
|
||||
```
|
||||
|
||||
Use hosted exact-head gates for CI workflow tuning. Do not burn local
|
||||
`pnpm test` on unrelated full-suite proof.
|
||||
|
||||
Only after the maintainer explicitly asks you to prepare or land the PR, run the
|
||||
repo-native mutating wrapper:
|
||||
|
||||
```bash
|
||||
scripts/pr review-init <pr>
|
||||
scripts/pr review-artifacts-init <pr>
|
||||
scripts/pr review-validate-artifacts <pr>
|
||||
OPENCLAW_TESTBOX=1 scripts/pr prepare-run <pr>
|
||||
```
|
||||
|
||||
`prepare-run` can push a prepared commit to the PR branch. Only run
|
||||
`scripts/pr merge-run <pr>` after the maintainer has explicitly asked you to
|
||||
land the PR. Both commands mutate GitHub state.
|
||||
|
||||
## Post-Land Monitoring
|
||||
|
||||
After merge, watch at least one fresh main cycle and the adjacent repos:
|
||||
|
||||
```bash
|
||||
ghx run list -R openclaw/openclaw --limit 20 --json databaseId,status,conclusion,workflowName,event,headBranch,createdAt,updatedAt,url
|
||||
for repo in openclaw/clawsweeper openclaw/clawhub openclaw/clownfish openclaw/openclaw-rtt openclaw/clawbench; do
|
||||
ghx run list -R "$repo" --limit 12 --json databaseId,status,conclusion,workflowName,event,headBranch,createdAt,updatedAt,url
|
||||
done
|
||||
curl -fsS https://clawsweeper.openclaw.ai/api/exact-review-queue | jq '.'
|
||||
```
|
||||
|
||||
Report:
|
||||
|
||||
- exact PR/commit landed;
|
||||
- expected registration reduction or added headroom;
|
||||
- CI run status and slowest/queued jobs;
|
||||
- ClawSweeper queue pending, dispatching, leased, oldest pending age;
|
||||
- any real failures that remain outside runner registration.
|
||||
@@ -1,4 +0,0 @@
|
||||
interface:
|
||||
display_name: "OpenClaw CI Limits"
|
||||
short_description: "Tune OpenClaw CI fanout and runner budgets"
|
||||
default_prompt: "Use $openclaw-ci-limits to inspect OpenClaw CI pressure, tune runner-registration fanout safely, and document the exact validation before landing."
|
||||
@@ -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,
|
||||
|
||||
@@ -5,7 +5,7 @@ description: "Run or recover OpenClaw macOS release signing, notarization, appca
|
||||
|
||||
# OpenClaw Mac Release
|
||||
|
||||
Use with `$release-openclaw-maintainer`, `$release-openclaw-ci`, `$one-password`, and `$release-private` if it exists when stable macOS assets, release-ops mac preflight, notarization, appcast promotion, or mac release recovery is involved.
|
||||
Use with `$release-openclaw-maintainer`, `$release-openclaw-ci`, `$one-password`, and `$release-private` if it exists when stable macOS assets, private mac preflight, notarization, appcast promotion, or mac release recovery is involved.
|
||||
|
||||
## Credentials
|
||||
|
||||
@@ -23,7 +23,7 @@ Use with `$release-openclaw-maintainer`, `$release-openclaw-ci`, `$one-password`
|
||||
|
||||
## GitHub Secrets
|
||||
|
||||
Target release-ops repo environment: `openclaw/releases`, env `mac-release`.
|
||||
Target private repo environment: `openclaw/releases-private`, env `mac-release`.
|
||||
|
||||
Set only after local notary auth validation:
|
||||
|
||||
@@ -35,24 +35,12 @@ Do not update these from mixed sources. All three ASC fields must come from the
|
||||
|
||||
## Workflow Shape
|
||||
|
||||
- `openclaw/openclaw` is the public product repo. Its GitHub Releases page is
|
||||
where macOS assets are ultimately attached.
|
||||
- `openclaw/openclaw` `macos-release.yml` is public handoff validation only.
|
||||
It never signs, notarizes, or uploads macOS assets, regardless of
|
||||
`preflight_only`.
|
||||
- `openclaw/releases` is the restricted release-ops repo. Its macOS workflows
|
||||
sign, notarize, validate, and promote assets onto the
|
||||
`openclaw/openclaw` GitHub release.
|
||||
- Public release branch may carry mac-only packaging fixes after the stable tag/npm are already live.
|
||||
- Use `source_ref=release/YYYY.M.PATCH` for release-ops mac preflight/validation when building that branch variation.
|
||||
- Use `source_ref=release/YYYY.M.PATCH` for private mac preflight/validation when building that branch variation.
|
||||
- Keep `tag=vYYYY.M.PATCH` pointing at the original stable release commit.
|
||||
- Real mac publish must reuse:
|
||||
- a successful release-ops mac preflight run for the same tag/source SHA
|
||||
- a successful release-ops mac validation run for the same tag/source SHA
|
||||
- Release-ops preflight and real publish enter the protected `mac-release`
|
||||
environment in the `build_sign_and_package` job. Operators may be able to
|
||||
trigger the workflow while Vincent or another environment reviewer approves
|
||||
the paused deployment before signing/notarization/promotion proceeds.
|
||||
- a successful private mac preflight run for the same tag/source SHA
|
||||
- a successful private mac validation run for the same tag/source SHA
|
||||
- If preflight source SHA differs from tag SHA, validation must also use the same `source_ref`; promotion rejects mismatched proof.
|
||||
|
||||
## Notarization
|
||||
@@ -64,25 +52,10 @@ Do not update these from mixed sources. All three ASC fields must come from the
|
||||
|
||||
## Dispatch
|
||||
|
||||
Public handoff validation:
|
||||
Private preflight:
|
||||
|
||||
```bash
|
||||
gh workflow run macos-release.yml --repo openclaw/openclaw \
|
||||
--ref release/YYYY.M.PATCH \
|
||||
-f tag=vYYYY.M.PATCH \
|
||||
-f preflight_only=true \
|
||||
-f public_release_branch=release/YYYY.M.PATCH
|
||||
```
|
||||
|
||||
- Use the public release branch as the workflow ref so the Actions list displays
|
||||
`release/YYYY.M.PATCH`, matching prior stable macOS handoff runs.
|
||||
- Do not use `--ref main` or `--ref vYYYY.M.PATCH` for this public handoff
|
||||
validation. The workflow checks out the tag from the `tag` input internally.
|
||||
|
||||
Release-ops preflight:
|
||||
|
||||
```bash
|
||||
gh workflow run openclaw-macos-publish.yml --repo openclaw/releases --ref main \
|
||||
gh workflow run openclaw-macos-publish.yml --repo openclaw/releases-private --ref main \
|
||||
-f tag=vYYYY.M.PATCH \
|
||||
-f source_ref=release/YYYY.M.PATCH \
|
||||
-f preflight_only=true \
|
||||
@@ -91,24 +64,18 @@ gh workflow run openclaw-macos-publish.yml --repo openclaw/releases --ref main \
|
||||
-f public_release_branch=release/YYYY.M.PATCH
|
||||
```
|
||||
|
||||
Wait for the run to reach the `mac-release` environment approval if GitHub
|
||||
pauses it, then get approval from Vincent or another configured environment
|
||||
reviewer. Record the successful preflight run id.
|
||||
|
||||
Release-ops validation for a branch-variation preflight:
|
||||
Private validation for a branch-variation preflight:
|
||||
|
||||
```bash
|
||||
gh workflow run openclaw-macos-validate.yml --repo openclaw/releases --ref main \
|
||||
gh workflow run openclaw-macos-validate.yml --repo openclaw/releases-private --ref main \
|
||||
-f tag=vYYYY.M.PATCH \
|
||||
-f source_ref=release/YYYY.M.PATCH
|
||||
```
|
||||
|
||||
Record the successful validation run id.
|
||||
|
||||
Real publish:
|
||||
|
||||
```bash
|
||||
gh workflow run openclaw-macos-publish.yml --repo openclaw/releases --ref main \
|
||||
gh workflow run openclaw-macos-publish.yml --repo openclaw/releases-private --ref main \
|
||||
-f tag=vYYYY.M.PATCH \
|
||||
-f preflight_only=false \
|
||||
-f smoke_test_only=false \
|
||||
@@ -118,14 +85,6 @@ gh workflow run openclaw-macos-publish.yml --repo openclaw/releases --ref main \
|
||||
-f public_release_branch=release/YYYY.M.PATCH
|
||||
```
|
||||
|
||||
Wait for the `mac-release` environment approval again if GitHub pauses the real
|
||||
publish run before it promotes assets.
|
||||
|
||||
- Release-ops `openclaw/releases` publish/validate workflows run from their own
|
||||
trusted `main` workflow ref. Real publish has a guard that rejects any other
|
||||
workflow ref. That displayed `main` ref is expected; the public OpenClaw
|
||||
source is selected by `tag` and optional `source_ref`.
|
||||
|
||||
## Verify
|
||||
|
||||
- `gh release view vYYYY.M.PATCH --repo openclaw/openclaw` shows zip, dmg, dSYM zip, not draft, not prerelease.
|
||||
|
||||
@@ -203,9 +203,8 @@ Stable publication is not complete until `main` carries the actual shipped relea
|
||||
validation-only release machinery. If mac packaging needs release-branch-only
|
||||
fixes after the stable npm package or GitHub tag is already published, do not
|
||||
create a `vYYYY.M.PATCH-N` correction tag just to change the workflow source.
|
||||
Dispatch the release-ops mac workflows for the original `tag=vYYYY.M.PATCH`
|
||||
with `source_ref=release/YYYY.M.PATCH` and
|
||||
`public_release_branch=release/YYYY.M.PATCH`;
|
||||
Dispatch the private mac workflows for the original `tag=vYYYY.M.PATCH` with
|
||||
`source_ref=release/YYYY.M.PATCH` and `public_release_branch=release/YYYY.M.PATCH`;
|
||||
provenance checks must prove the source SHA descends from the tag and
|
||||
validation/preflight use the same source. Reserve `vYYYY.M.PATCH-N` correction
|
||||
tags for emergency hotfixes that must publish a new npm package/release
|
||||
@@ -250,20 +249,12 @@ Stable publication is not complete until `main` carries the actual shipped relea
|
||||
section from history, not existing notes. Use the last reachable stable or
|
||||
beta release tag as the base, then inspect every commit through the target
|
||||
release SHA.
|
||||
- Generate `$openclaw-changelog-update`'s full contribution manifest before
|
||||
the editorial rewrite. It is the required source for `### Highlights`,
|
||||
`### Changes`, and `### Fixes`; do not preserve old grouped prose without
|
||||
comparing it to the manifest's PRs, contributors, direct commits, and
|
||||
unlinked commits.
|
||||
- The changelog rewrite is not optional for beta reruns: any `beta.N` after a
|
||||
rebase or backport must refresh the same stable-base `## YYYY.M.PATCH` section
|
||||
before the new version/tag commit.
|
||||
- Include both merged PR commits and direct commits on `main`. Direct commits
|
||||
matter: infer notes from their subject, body, touched files, linked issues,
|
||||
tests, and nearby code when no PR body exists.
|
||||
- Keep direct commits in the generated manifest and use them to shape grouped
|
||||
user outcomes, but never dump them into `CHANGELOG.md` or GitHub release
|
||||
bodies. The public complete record is PR-first and exhaustive for PRs.
|
||||
- Prefer PR bodies, issue links, review proof, and commit bodies over commit
|
||||
subjects alone. If a commit fixed an issue directly, the commit body should
|
||||
name the user-visible behavior, affected surface, issue ref, and credited
|
||||
@@ -279,31 +270,11 @@ Stable publication is not complete until `main` carries the actual shipped relea
|
||||
`#issue`, `(#PR)`, `Fixes #...`, and every human `Thanks @...` handle.
|
||||
Multiple thanks in one bullet are expected when multiple contributor PRs are
|
||||
grouped.
|
||||
- Highlights earn their place only when they are a visible capability/workflow
|
||||
unlock, a material reliability or safety repair, a broad user-facing
|
||||
improvement, or a release-defining integration/compatibility change. Keep
|
||||
five to eight user-outcome bullets; omit tests, CI, refactors, docs, and
|
||||
implementation trivia unless their outcome materially affects users.
|
||||
- Do not give `docs`, `test`, `refactor`, `ci`, `build`, `chore`, or `style`
|
||||
PRs/direct commits their own Highlights, Changes, or Fixes entry. They remain
|
||||
accounted for in the PR record or manifest, but are not product release
|
||||
content. Treat explicit internal title signals such as `QA`, `lint`, or
|
||||
`testing` the same way even when the PR has no conventional prefix.
|
||||
- Use the generated `### Complete contribution record` as PR-first accounting:
|
||||
every merged source PR appears once with author/co-author credit, including
|
||||
PRs identified only by an explicit active-commit `#NNN` reference after a
|
||||
cherry-pick or squash. Keep issues inline as `#NNN` in titles and grouped
|
||||
prose; do not create a linked-issues inventory or a direct-commit listing.
|
||||
When grouped prose names a PR, keep every contributor and linked-reporter
|
||||
credit from that PR's record on the same bullet.
|
||||
- Changelog entries should be user-facing, not internal release-process notes.
|
||||
- GitHub release and prerelease bodies must use the full matching
|
||||
`CHANGELOG.md` version section, not highlights or an excerpt. When creating
|
||||
or editing a release, extract from `## YYYY.M.PATCH` through the line before the
|
||||
next level-2 heading and use that complete block as the release notes.
|
||||
- GitHub limits release bodies to 125,000 characters. If a historical
|
||||
`### Release verification` tail would exceed that cap, omit the tail and keep
|
||||
the complete changelog section; do not truncate the contribution record.
|
||||
- Before publishing or closing a release, run
|
||||
`$openclaw-changelog-update`'s `verify-release-notes.mjs` with every stable
|
||||
and beta release tag in the train. Do not publish or leave a page live when
|
||||
@@ -580,8 +551,8 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
|
||||
- Actual npm install/update phases are capped at 5 minutes. If `npm install -g`, installer package install, or `openclaw update` takes longer than 300s in release e2e, stop treating the run as healthy progress and debug the installer/updater or harness.
|
||||
- Serialize host build/package mutations ahead of VM lanes. Finish `pnpm build`, `pnpm ui:build`, `pnpm release:check`, install smoke, and any Docker/package-prep lanes before starting Parallels `npm pack` lanes; otherwise `dist` can disappear during VM pack prep and produce false failures.
|
||||
- Include mac release readiness in preflight by running the public validation
|
||||
workflow in `openclaw/openclaw` and the release-ops mac preflight in
|
||||
`openclaw/releases` for every release.
|
||||
workflow in `openclaw/openclaw` and the real mac preflight in
|
||||
`openclaw/releases-private` for every release.
|
||||
- Treat the `appcast.xml` update on `main` as part of mac release readiness, not an optional follow-up.
|
||||
- The workflows remain tag-based. The agent is responsible for making sure
|
||||
preflight runs complete successfully before any publish run starts.
|
||||
@@ -609,16 +580,16 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
|
||||
## Use the right auth flow
|
||||
|
||||
- OpenClaw publish uses GitHub trusted publishing.
|
||||
- Stable npm promotion from `beta` to `latest` uses the restricted release-ops
|
||||
`openclaw/releases/.github/workflows/openclaw-npm-dist-tags.yml` workflow
|
||||
because `npm dist-tag` management needs `NPM_TOKEN`, while the public npm
|
||||
release workflow stays OIDC-only.
|
||||
- Prefer fixing the release-ops workflow token path over any local 1Password
|
||||
fallback. The desired setup is a granular npm token stored as the release-ops
|
||||
- Stable npm promotion from `beta` to `latest` uses the private
|
||||
`openclaw/releases-private/.github/workflows/openclaw-npm-dist-tags.yml`
|
||||
workflow because `npm dist-tag` management needs `NPM_TOKEN`, while the
|
||||
public npm release workflow stays OIDC-only.
|
||||
- Prefer fixing the private workflow token path over any local 1Password
|
||||
fallback. The desired setup is a granular npm token stored as the private
|
||||
repo's `NPM_TOKEN` secret, scoped to the `openclaw` package with read/write
|
||||
and 2FA bypass for automation.
|
||||
- If the release-ops dist-tag workflow cannot promote because `NPM_TOKEN` is
|
||||
absent or stale, use the local tmux + 1Password fallback:
|
||||
- If the private dist-tag workflow cannot promote because `NPM_TOKEN` is absent
|
||||
or stale, use the local tmux + 1Password fallback:
|
||||
- Start or reuse a tmux session so interactive `npm login` and OTP prompts
|
||||
are observable and recoverable.
|
||||
- Hard rule: never run `op` directly in the main agent shell during release
|
||||
@@ -636,21 +607,21 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
|
||||
- Verify with a cache-bypassed registry read, for example:
|
||||
`npm view openclaw dist-tags --json --prefer-online --cache /tmp/openclaw-npm-cache-verify-$$`
|
||||
and `npm view openclaw@latest version dist.tarball --json --prefer-online`.
|
||||
- Direct stable publishes can also use that release-ops dist-tag workflow to
|
||||
point `beta` at the already-published `latest` version when the operator wants
|
||||
both tags aligned immediately.
|
||||
- Direct stable publishes can also use that private dist-tag workflow to point
|
||||
`beta` at the already-published `latest` version when the operator wants both
|
||||
tags aligned immediately.
|
||||
- The publish run must be started manually with `workflow_dispatch`.
|
||||
- The npm workflow and the release-ops mac publish workflow accept
|
||||
- The npm workflow and the private mac publish workflow accept
|
||||
`preflight_only=true` to run validation/build/package steps without uploading
|
||||
public release assets.
|
||||
- Real npm publish requires a prior successful npm preflight run id and the
|
||||
successful Full Release Validation run id for the same tag/SHA so the publish
|
||||
job promotes the prepared tarball instead of rebuilding it and attaches the
|
||||
correct release evidence.
|
||||
- Real release-ops mac publish requires a prior successful release-ops mac
|
||||
preflight run id so the publish job promotes the prepared artifacts instead of
|
||||
- Real private mac publish requires a prior successful private mac preflight
|
||||
run id so the publish job promotes the prepared artifacts instead of
|
||||
rebuilding or renotarizing them again.
|
||||
- The release-ops mac workflow also accepts `smoke_test_only=true` for branch-safe
|
||||
- The private mac workflow also accepts `smoke_test_only=true` for branch-safe
|
||||
workflow smoke tests that use ad-hoc signing, skip notarization, skip shared
|
||||
appcast generation, and do not prove release readiness.
|
||||
- `preflight_only=true` on the npm workflow is also the right way to validate an
|
||||
@@ -671,27 +642,27 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
|
||||
use only `main` or `release/YYYY.M.PATCH`.
|
||||
- `.github/workflows/macos-release.yml` in `openclaw/openclaw` is now a
|
||||
public validation-only handoff. It validates the tag/release state and points
|
||||
operators to the release-ops repo. It still rebuilds the JS outputs needed for
|
||||
operators to the private repo. It still rebuilds the JS outputs needed for
|
||||
release validation, but it does not sign, notarize, or publish macOS
|
||||
artifacts.
|
||||
- `openclaw/releases/.github/workflows/openclaw-macos-validate.yml` is the
|
||||
required release-ops mac validation lane for `swift test`; keep it green
|
||||
- `openclaw/releases-private/.github/workflows/openclaw-macos-validate.yml`
|
||||
is the required private mac validation lane for `swift test`; keep it green
|
||||
before any real stable mac publish run starts.
|
||||
- Real mac preflight and real mac publish both use
|
||||
`openclaw/releases/.github/workflows/openclaw-macos-publish.yml`.
|
||||
- The release-ops mac validation lane runs on GitHub's standard macOS runner.
|
||||
- The release-ops mac preflight path runs on GitHub's xlarge macOS runner and uses
|
||||
`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml`.
|
||||
- The private mac validation lane runs on GitHub's standard macOS runner.
|
||||
- The private mac preflight path runs on GitHub's xlarge macOS runner and uses
|
||||
a SwiftPM cache because the build/sign/notarize/package path is CPU-heavy.
|
||||
- Release-ops mac preflight uploads notarized build artifacts as workflow
|
||||
artifacts instead of uploading public GitHub release assets.
|
||||
- Release-ops smoke-test runs upload ad-hoc, non-notarized build artifacts as
|
||||
- Private mac preflight uploads notarized build artifacts as workflow artifacts
|
||||
instead of uploading public GitHub release assets.
|
||||
- Private smoke-test runs upload ad-hoc, non-notarized build artifacts as
|
||||
workflow artifacts and intentionally skip stable `appcast.xml` generation.
|
||||
- For stable releases, npm preflight, Full Release Validation, public mac
|
||||
validation, release-ops mac validation, and release-ops mac preflight must all
|
||||
pass before any real publish run starts. For beta releases, npm preflight and
|
||||
Full Release Validation must pass before npm publish unless the operator
|
||||
explicitly waives the full gate; mac beta validation is still only required
|
||||
when requested.
|
||||
validation, private mac validation, and private mac preflight must all pass
|
||||
before any real publish run starts. For beta releases, npm preflight and Full
|
||||
Release Validation must pass before npm publish unless the operator explicitly
|
||||
waives the full gate; mac beta validation is still only required when
|
||||
requested.
|
||||
- Real publish runs may be dispatched from `main` or from a
|
||||
`release/YYYY.M.PATCH` branch. For release-branch runs, the tag must be contained
|
||||
in that release branch, and the real publish must reuse a successful preflight
|
||||
@@ -700,21 +671,21 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
|
||||
rather than workflow-level SHA pinning.
|
||||
- The `npm-release` environment must be approved by `@openclaw/openclaw-release-managers` before publish continues.
|
||||
- Mac publish uses
|
||||
`openclaw/releases/.github/workflows/openclaw-macos-publish.yml` for
|
||||
release-ops mac preflight artifact preparation and real publish artifact
|
||||
`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml` for
|
||||
private mac preflight artifact preparation and real publish artifact
|
||||
promotion.
|
||||
- Real release-ops mac publish uploads the packaged `.zip`, `.dmg`, and
|
||||
- Real private mac publish uploads the packaged `.zip`, `.dmg`, and
|
||||
`.dSYM.zip` assets to the existing GitHub release in `openclaw/openclaw`
|
||||
automatically when `OPENCLAW_PUBLIC_REPO_RELEASE_TOKEN` is present in the
|
||||
release-ops repo `mac-release` environment.
|
||||
private repo `mac-release` environment.
|
||||
- For stable releases, the agent must also download the signed
|
||||
`macos-appcast-<tag>` artifact from the successful release-ops mac workflow
|
||||
and then update `appcast.xml` on `main`.
|
||||
`macos-appcast-<tag>` artifact from the successful private mac workflow and
|
||||
then update `appcast.xml` on `main`.
|
||||
- For beta mac releases, do not update the shared production `appcast.xml`
|
||||
unless a separate beta Sparkle feed exists.
|
||||
- The release-ops repo targets a dedicated `mac-release` environment. If the
|
||||
GitHub plan does not yet support required reviewers there, do not assume the
|
||||
environment alone is the approval boundary; rely on restricted repo access and
|
||||
- The private repo targets a dedicated `mac-release` environment. If the GitHub
|
||||
plan does not yet support required reviewers there, do not assume the
|
||||
environment alone is the approval boundary; rely on private repo access and
|
||||
CODEOWNERS until those settings can be enabled.
|
||||
- Do not use `NPM_TOKEN` or the plugin OTP flow for the OpenClaw package
|
||||
publish path; package publishing uses trusted publishing.
|
||||
@@ -801,12 +772,12 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
|
||||
18. For stable releases, start `.github/workflows/macos-release.yml` in
|
||||
`openclaw/openclaw` and wait for the public validation-only run to pass.
|
||||
19. For stable releases, start
|
||||
`openclaw/releases/.github/workflows/openclaw-macos-validate.yml` with the
|
||||
same tag and wait for the release-ops mac validation lane to pass.
|
||||
`openclaw/releases-private/.github/workflows/openclaw-macos-validate.yml`
|
||||
with the same tag and wait for the private mac validation lane to pass.
|
||||
20. For stable releases, start
|
||||
`openclaw/releases/.github/workflows/openclaw-macos-publish.yml` with
|
||||
`preflight_only=true` and wait for it to pass. Save that run id because the
|
||||
real publish requires it to reuse the notarized mac artifacts.
|
||||
`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml`
|
||||
with `preflight_only=true` and wait for it to pass. Save that run id because
|
||||
the real publish requires it to reuse the notarized mac artifacts.
|
||||
21. If any preflight or validation run fails, fix the issue on a new commit,
|
||||
delete the tag and any accidental draft/incomplete GitHub release, recreate
|
||||
the tag from the fixed commit, and rerun all relevant preflights from
|
||||
@@ -862,23 +833,22 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
|
||||
promotion roster when the matching beta already carried the full confidence
|
||||
pass: published npm postpublish verify, Docker install/update smoke,
|
||||
macOS-only Parallels install/update smoke, and required QA signal.
|
||||
Then start the restricted release-ops
|
||||
`openclaw/releases/.github/workflows/openclaw-npm-dist-tags.yml` workflow
|
||||
to promote that stable version from `beta` to `latest`, then verify
|
||||
`latest` now points at that version.
|
||||
Then start the private
|
||||
`openclaw/releases-private/.github/workflows/openclaw-npm-dist-tags.yml`
|
||||
workflow to promote that stable version from `beta` to `latest`, then
|
||||
verify `latest` now points at that version.
|
||||
29. If the stable release was published directly to `latest` and `beta` should
|
||||
follow it, start that same release-ops dist-tag workflow to point `beta` at
|
||||
the stable version, then verify both `latest` and `beta` point at that
|
||||
version.
|
||||
follow it, start that same private dist-tag workflow to point `beta` at the
|
||||
stable version, then verify both `latest` and `beta` point at that version.
|
||||
30. For stable releases, start
|
||||
`openclaw/releases/.github/workflows/openclaw-macos-publish.yml` for the
|
||||
real publish with the successful release-ops mac `preflight_run_id` and wait
|
||||
for success.
|
||||
31. Verify the successful real release-ops mac run uploaded the `.zip`, `.dmg`,
|
||||
`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml`
|
||||
for the real publish with the successful private mac `preflight_run_id` and
|
||||
wait for success.
|
||||
31. Verify the successful real private mac run uploaded the `.zip`, `.dmg`,
|
||||
and `.dSYM.zip` artifacts to the existing GitHub release in
|
||||
`openclaw/openclaw`.
|
||||
32. For stable releases, download `macos-appcast-<tag>` from the successful
|
||||
release-ops mac run, update `appcast.xml` on `main`, verify the feed, then
|
||||
private mac run, update `appcast.xml` on `main`, verify the feed, then
|
||||
complete the **Close stable releases on main** gate.
|
||||
33. For beta releases, publish the mac assets only when intentionally requested;
|
||||
expect no shared production
|
||||
|
||||
76
.github/ISSUE_TEMPLATE/docs_bug_report.yml
vendored
76
.github/ISSUE_TEMPLATE/docs_bug_report.yml
vendored
@@ -1,76 +0,0 @@
|
||||
name: Docs bug report
|
||||
description: Report documentation defects (incorrect, missing, outdated, or contradictory docs).
|
||||
title: "[Docs Bug]: "
|
||||
labels:
|
||||
- bug
|
||||
- docs
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Report a documentation defect with concrete evidence from current docs behavior/content.
|
||||
Please only report one documentation defect per submission.
|
||||
- type: textarea
|
||||
id: summary
|
||||
attributes:
|
||||
label: Summary
|
||||
description: One-sentence statement of what is wrong in the docs.
|
||||
placeholder: The WhatsApp config example defines duplicate top-level keys in one JSON5 block.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: doc_paths
|
||||
attributes:
|
||||
label: Affected docs path(s) or URL(s)
|
||||
description: Repo-relative docs file path(s) or published docs URL(s).
|
||||
placeholder: docs/gateway/config-channels.md or https://docs.openclaw.ai/gateway/config-channels
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: repro
|
||||
attributes:
|
||||
label: Steps to reproduce / verify
|
||||
description: Minimal steps to observe the docs defect in the current docs.
|
||||
placeholder: |
|
||||
1. Open docs/gateway/config-channels.md
|
||||
2. Go to the WhatsApp example block
|
||||
3. Observe duplicate top-level key definitions
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected docs behavior/content
|
||||
description: What the docs should say/show instead.
|
||||
placeholder: The example should use a single merged top-level object with no duplicate keys.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: Actual docs behavior/content
|
||||
description: What the docs currently say/show.
|
||||
placeholder: The snippet defines the same top-level key twice in one object.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: impact
|
||||
attributes:
|
||||
label: Impact
|
||||
description: Who is affected and practical consequence.
|
||||
placeholder: Users who copy-paste the snippet can end up with ambiguous config behavior.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: evidence
|
||||
attributes:
|
||||
label: Evidence
|
||||
description: Links/snippets/screenshots proving the docs defect.
|
||||
placeholder: Include exact file links and line ranges.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional_information
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Optional context, related issues/PRs, or constraints.
|
||||
@@ -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
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# OpenClaw Maturity Scorecard Agent
|
||||
|
||||
You are refreshing the OpenClaw maturity score source for a release scorecard.
|
||||
|
||||
Goal: use the `$claw-score` skill to refresh `qa/maturity-scores.yaml` for every active surface in `taxonomy.yaml`, using the current repository and the release evidence artifacts in `.artifacts/maturity-evidence`.
|
||||
|
||||
Allowed tracked paths:
|
||||
|
||||
- `qa/maturity-scores.yaml`
|
||||
|
||||
Hard limits:
|
||||
|
||||
- Do not edit generated docs, taxonomy, workflows, scripts, package metadata, lockfiles, tests, or application code.
|
||||
- Do not render docs. The workflow renders docs after validating the score source.
|
||||
- Keep the score source schema valid for QA Lab maturity score validation.
|
||||
|
||||
Required workflow:
|
||||
|
||||
1. Use the `$claw-score` skill before editing.
|
||||
2. Read `taxonomy.yaml`, any existing maturity score file, and the release evidence artifacts.
|
||||
3. Refresh scores for every active surface in `taxonomy.yaml`.
|
||||
4. Run the QA Lab maturity score validation used by this repository.
|
||||
5. If no defensible score update is possible, leave a valid `qa/maturity-scores.yaml` and explain the uncertainty in the final message.
|
||||
22
.github/labeler.yml
vendored
22
.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:
|
||||
@@ -118,7 +119,6 @@
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/qa-lab/**"
|
||||
- "qa/scenarios/**"
|
||||
- "docs/maturity/**"
|
||||
- "docs/concepts/qa-e2e-automation.md"
|
||||
- "docs/concepts/personal-agent-benchmark-pack.md"
|
||||
- "docs/channels/qa-channel.md"
|
||||
@@ -171,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:
|
||||
@@ -252,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:
|
||||
@@ -270,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>
|
||||
|
||||
48
.github/workflows/ci-build-artifacts-testbox.yml
vendored
48
.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"
|
||||
|
||||
@@ -198,19 +194,10 @@ jobs:
|
||||
"+refs/heads/main:refs/remotes/origin/main"
|
||||
|
||||
node_bin="$(dirname "$(node -p 'process.execPath')")"
|
||||
link_node_tool() {
|
||||
local tool="$1"
|
||||
local source="$node_bin/$tool"
|
||||
local target="/usr/local/bin/$tool"
|
||||
if [ -e "$target" ] && [ "$(readlink -f "$source")" = "$(readlink -f "$target")" ]; then
|
||||
return
|
||||
fi
|
||||
sudo ln -sf "$source" "$target"
|
||||
}
|
||||
link_node_tool node
|
||||
link_node_tool npm
|
||||
link_node_tool npx
|
||||
link_node_tool corepack
|
||||
sudo ln -sf "$node_bin/node" /usr/local/bin/node
|
||||
sudo ln -sf "$node_bin/npm" /usr/local/bin/npm
|
||||
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
|
||||
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
|
||||
sudo tee /usr/local/bin/pnpm >/dev/null <<'PNPM'
|
||||
#!/usr/bin/env bash
|
||||
exec /usr/local/bin/corepack pnpm "$@"
|
||||
@@ -223,53 +210,28 @@ 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
|
||||
uses: useblacksmith/run-testbox@3f60ff9ceb2c10c3feefa87dc0c6490cffae059d
|
||||
if: always()
|
||||
if: success()
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
47
.github/workflows/ci-check-arm-testbox.yml
vendored
47
.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"
|
||||
@@ -116,19 +112,10 @@ jobs:
|
||||
"+refs/heads/main:refs/remotes/origin/main"
|
||||
|
||||
node_bin="$(dirname "$(node -p 'process.execPath')")"
|
||||
link_node_tool() {
|
||||
local tool="$1"
|
||||
local source="$node_bin/$tool"
|
||||
local target="/usr/local/bin/$tool"
|
||||
if [ -e "$target" ] && [ "$(readlink -f "$source")" = "$(readlink -f "$target")" ]; then
|
||||
return
|
||||
fi
|
||||
sudo ln -sf "$source" "$target"
|
||||
}
|
||||
link_node_tool node
|
||||
link_node_tool npm
|
||||
link_node_tool npx
|
||||
link_node_tool corepack
|
||||
sudo ln -sf "$node_bin/node" /usr/local/bin/node
|
||||
sudo ln -sf "$node_bin/npm" /usr/local/bin/npm
|
||||
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
|
||||
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
|
||||
sudo tee /usr/local/bin/pnpm >/dev/null <<'PNPM'
|
||||
#!/usr/bin/env bash
|
||||
exec /usr/local/bin/corepack pnpm "$@"
|
||||
@@ -141,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 }}
|
||||
@@ -152,42 +137,20 @@ 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
|
||||
uses: useblacksmith/run-testbox@5ca05834db1d3813554d1dd109e5f2087a8d7cbc
|
||||
if: always()
|
||||
if: success()
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
49
.github/workflows/ci-check-testbox.yml
vendored
49
.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"
|
||||
@@ -33,7 +29,7 @@ jobs:
|
||||
contents: read
|
||||
name: "check"
|
||||
runs-on: blacksmith-32vcpu-ubuntu-2404
|
||||
timeout-minutes: ${{ fromJSON(inputs.timeout_minutes || '120') }}
|
||||
timeout-minutes: ${{ fromJSON(inputs.timeout_minutes || '30') }}
|
||||
steps:
|
||||
- name: Begin Testbox
|
||||
uses: useblacksmith/begin-testbox@233448af4bfdc6fca509a7f0974411ac6d8a8043
|
||||
@@ -105,19 +101,10 @@ jobs:
|
||||
"+refs/heads/main:refs/remotes/origin/main"
|
||||
|
||||
node_bin="$(dirname "$(node -p 'process.execPath')")"
|
||||
link_node_tool() {
|
||||
local tool="$1"
|
||||
local source="$node_bin/$tool"
|
||||
local target="/usr/local/bin/$tool"
|
||||
if [ -e "$target" ] && [ "$(readlink -f "$source")" = "$(readlink -f "$target")" ]; then
|
||||
return
|
||||
fi
|
||||
sudo ln -sf "$source" "$target"
|
||||
}
|
||||
link_node_tool node
|
||||
link_node_tool npm
|
||||
link_node_tool npx
|
||||
link_node_tool corepack
|
||||
sudo ln -sf "$node_bin/node" /usr/local/bin/node
|
||||
sudo ln -sf "$node_bin/npm" /usr/local/bin/npm
|
||||
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
|
||||
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
|
||||
sudo tee /usr/local/bin/pnpm >/dev/null <<'PNPM'
|
||||
#!/usr/bin/env bash
|
||||
exec /usr/local/bin/corepack pnpm "$@"
|
||||
@@ -130,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 }}
|
||||
@@ -141,42 +126,20 @@ 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
|
||||
uses: useblacksmith/run-testbox@3f60ff9ceb2c10c3feefa87dc0c6490cffae059d
|
||||
if: always()
|
||||
if: success()
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
255
.github/workflows/ci.yml
vendored
255
.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
|
||||
@@ -100,7 +79,6 @@ jobs:
|
||||
run_macos_node: ${{ steps.manifest.outputs.run_macos_node }}
|
||||
macos_node_matrix: ${{ steps.manifest.outputs.macos_node_matrix }}
|
||||
run_macos_swift: ${{ steps.manifest.outputs.run_macos_swift }}
|
||||
run_ios_build: ${{ steps.manifest.outputs.run_ios_build }}
|
||||
run_android_job: ${{ steps.manifest.outputs.run_android_job }}
|
||||
android_matrix: ${{ steps.manifest.outputs.android_matrix }}
|
||||
steps:
|
||||
@@ -205,7 +183,6 @@ jobs:
|
||||
OPENCLAW_CI_DOCS_CHANGED: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.docs_scope.outputs.docs_changed }}
|
||||
OPENCLAW_CI_RUN_NODE: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_node || 'false' }}
|
||||
OPENCLAW_CI_RUN_MACOS: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_macos || 'false' }}
|
||||
OPENCLAW_CI_RUN_IOS_BUILD: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_ios_build || 'false' }}
|
||||
OPENCLAW_CI_RUN_ANDROID: ${{ github.event_name == 'workflow_dispatch' && (inputs.release_gate || inputs.include_android) && 'true' || steps.changed_scope.outputs.run_android || 'false' }}
|
||||
OPENCLAW_CI_RUN_WINDOWS: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_windows || 'false' }}
|
||||
OPENCLAW_CI_RUN_NODE_FAST_ONLY: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_scope.outputs.run_node_fast_only || 'false' }}
|
||||
@@ -220,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,
|
||||
@@ -251,6 +228,7 @@ jobs:
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const createMatrix = (include) => ({ include });
|
||||
const outputPath = process.env.GITHUB_OUTPUT;
|
||||
const isCanonicalRepository = process.env.OPENCLAW_CI_REPOSITORY === "openclaw/openclaw";
|
||||
@@ -268,8 +246,6 @@ jobs:
|
||||
const runPluginContractShards = runNodeFull || runNodeFastPluginContracts;
|
||||
const runMacos =
|
||||
parseBoolean(process.env.OPENCLAW_CI_RUN_MACOS) && !docsOnly && isCanonicalRepository;
|
||||
const runIosBuild =
|
||||
parseBoolean(process.env.OPENCLAW_CI_RUN_IOS_BUILD) && !docsOnly && isCanonicalRepository;
|
||||
const runAndroid =
|
||||
parseBoolean(process.env.OPENCLAW_CI_RUN_ANDROID) && !docsOnly && isCanonicalRepository;
|
||||
const runWindows =
|
||||
@@ -284,7 +260,6 @@ jobs:
|
||||
if (runNodeFull) {
|
||||
checksFastCoreTasks.push(
|
||||
{ check_name: "checks-fast-bundled-protocol", runtime: "node", task: "bundled-protocol" },
|
||||
{ check_name: "QA Smoke CI", runtime: "node", task: "qa-smoke-ci" },
|
||||
{ check_name: "checks-fast-bun-launcher", runtime: "bun", task: "bun-launcher" },
|
||||
);
|
||||
} else {
|
||||
@@ -297,23 +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,
|
||||
env: shard.env,
|
||||
includePatterns: shard.includePatterns,
|
||||
requires_dist: shard.requiresDist,
|
||||
runner: shard.runner,
|
||||
timeout_minutes: shard.timeoutMinutes,
|
||||
}))
|
||||
: [];
|
||||
const nodeTestNonDistShards = nodeTestShards.filter((shard) => !shard.requires_dist);
|
||||
@@ -350,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,
|
||||
@@ -365,7 +328,6 @@ jobs:
|
||||
runMacos ? [{ check_name: "macos-node", runtime: "node", task: "test" }] : [],
|
||||
),
|
||||
run_macos_swift: runMacos,
|
||||
run_ios_build: runIosBuild,
|
||||
run_android_job: runAndroid,
|
||||
android_matrix: createMatrix(
|
||||
runAndroid
|
||||
@@ -392,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
|
||||
@@ -597,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'] }}
|
||||
@@ -858,7 +819,6 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 12
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.checks_fast_core_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -922,26 +882,6 @@ jobs:
|
||||
pnpm test:bundled
|
||||
pnpm protocol:check
|
||||
;;
|
||||
qa-smoke-ci)
|
||||
output_dir=".artifacts/qa-e2e/smoke-ci-profile"
|
||||
export OPENCLAW_BUILD_PRIVATE_QA=1
|
||||
export OPENCLAW_ENABLE_PRIVATE_QA_CLI=1
|
||||
export OPENCLAW_DISABLE_BUNDLED_PLUGINS=0
|
||||
export OPENCLAW_QA_REDACT_PUBLIC_METADATA=1
|
||||
export OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS=180000
|
||||
NODE_OPTIONS=--max-old-space-size=8192 node scripts/build-all.mjs qaRuntime
|
||||
qa_exit_code=0
|
||||
pnpm openclaw qa run \
|
||||
--repo-root . \
|
||||
--qa-profile smoke-ci \
|
||||
--concurrency 8 \
|
||||
--output-dir "$output_dir" || qa_exit_code=$?
|
||||
echo "QA smoke profile evidence: \`${output_dir}\`" >> "$GITHUB_STEP_SUMMARY"
|
||||
if [ "$qa_exit_code" -ne 0 ]; then
|
||||
echo "::error title=QA smoke profile failed::smoke-ci exited ${qa_exit_code}; evidence upload will still run"
|
||||
exit "$qa_exit_code"
|
||||
fi
|
||||
;;
|
||||
contracts-plugins-ci-routing)
|
||||
pnpm test:contracts:plugins
|
||||
pnpm test src/commands/status.scan-result.test.ts src/scripts/ci-changed-scope.test.ts test/scripts/changed-lanes.test.ts test/scripts/ci-workflow-guards.test.ts test/scripts/run-vitest.test.ts test/scripts/test-projects.test.ts
|
||||
@@ -958,15 +898,6 @@ jobs:
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Upload QA smoke profile evidence
|
||||
if: always() && matrix.task == 'qa-smoke-ci'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: qa-smoke-profile-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: .artifacts/qa-e2e/smoke-ci-profile/
|
||||
if-no-files-found: warn
|
||||
retention-days: 7
|
||||
|
||||
checks-fast-plugin-contracts-shard:
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -977,7 +908,6 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 12
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.plugin_contracts_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -1058,7 +988,6 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 12
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.channel_contracts_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -1207,13 +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
|
||||
# The canonical main path waits for the admission debounce above, so
|
||||
# widen this large matrix within the current runner-registration budget.
|
||||
max-parallel: 24
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.checks_node_core_nondist_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -1272,9 +1198,7 @@ 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_ENV_JSON: ${{ toJson(matrix.env) }}
|
||||
OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }}
|
||||
OPENCLAW_VITEST_SHARD_NAME: ${{ matrix.shard_name }}
|
||||
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "300000"
|
||||
@@ -1288,55 +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 ?? "[]"),
|
||||
env: JSON.parse(process.env.OPENCLAW_NODE_TEST_ENV_JSON ?? "null"),
|
||||
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 (plan.env && typeof plan.env === "object" && !Array.isArray(plan.env)) {
|
||||
for (const [key, value] of Object.entries(plan.env)) {
|
||||
if (typeof value === "string") {
|
||||
childEnv[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
@@ -1351,7 +1248,6 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 12
|
||||
matrix:
|
||||
include:
|
||||
- check_name: check-guards
|
||||
@@ -1368,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
|
||||
@@ -1489,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: 12
|
||||
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
|
||||
@@ -1864,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
|
||||
@@ -1876,7 +1763,6 @@ jobs:
|
||||
shell: bash
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 2
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.checks_windows_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -2196,76 +2082,6 @@ jobs:
|
||||
done
|
||||
exit 1
|
||||
|
||||
ios-build:
|
||||
permissions:
|
||||
contents: read
|
||||
name: "ios-build"
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_ios_build == 'true'
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'macos-26' || (github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-26' || 'macos-26') }}
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout
|
||||
env:
|
||||
CHECKOUT_REPO: ${{ github.repository }}
|
||||
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git init "$GITHUB_WORKSPACE"
|
||||
git -C "$GITHUB_WORKSPACE" config gc.auto 0
|
||||
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
|
||||
fetch_timeout_seconds=90
|
||||
fetch_checkout_ref() {
|
||||
git -C "$GITHUB_WORKSPACE" \
|
||||
-c protocol.version=2 \
|
||||
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
||||
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout" &
|
||||
local fetch_pid="$!"
|
||||
local elapsed=0
|
||||
while kill -0 "$fetch_pid" 2>/dev/null; do
|
||||
if [ "$elapsed" -ge "$fetch_timeout_seconds" ]; then
|
||||
kill -TERM "$fetch_pid" 2>/dev/null || true
|
||||
sleep 10
|
||||
kill -KILL "$fetch_pid" 2>/dev/null || true
|
||||
wait "$fetch_pid" || true
|
||||
return 124
|
||||
fi
|
||||
sleep 1
|
||||
elapsed=$((elapsed + 1))
|
||||
done
|
||||
wait "$fetch_pid"
|
||||
}
|
||||
fetch_checkout_ref
|
||||
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
|
||||
|
||||
- name: Select Xcode 26
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for xcode_app in /Applications/Xcode_26.5.app /Applications/Xcode-26.5.0.app; do
|
||||
if [ -d "$xcode_app/Contents/Developer" ]; then
|
||||
sudo xcode-select -s "$xcode_app/Contents/Developer"
|
||||
break
|
||||
fi
|
||||
done
|
||||
xcodebuild -version
|
||||
xcode_version="$(xcodebuild -version | awk 'NR == 1 { print $2 }')"
|
||||
if [[ "$xcode_version" != 26.* ]]; then
|
||||
echo "error: expected Xcode 26.x, got $xcode_version" >&2
|
||||
exit 1
|
||||
fi
|
||||
swift --version
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
|
||||
- name: Install iOS Swift tooling
|
||||
run: brew install xcodegen swiftlint swiftformat
|
||||
|
||||
- name: Build iOS app
|
||||
run: pnpm ios:build
|
||||
|
||||
android:
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -2276,7 +2092,6 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 2
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.android_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -2339,7 +2154,7 @@ jobs:
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: ~/.android-sdk
|
||||
key: ${{ runner.os }}-android-sdk-v1-cmdline-14742923-platform-36-build-tools-36.0.0
|
||||
key: ${{ runner.os }}-android-sdk-v1-cmdline-14742923-platform-37.0-build-tools-36.0.0
|
||||
restore-keys: |
|
||||
${{ runner.os }}-android-sdk-v1-
|
||||
|
||||
@@ -2369,7 +2184,7 @@ jobs:
|
||||
yes | sdkmanager --sdk_root="${ANDROID_SDK_ROOT}" --licenses >/dev/null
|
||||
sdkmanager --sdk_root="${ANDROID_SDK_ROOT}" --install \
|
||||
"platform-tools" \
|
||||
"platforms;android-36" \
|
||||
"platforms;android-37.0" \
|
||||
"build-tools;36.0.0"
|
||||
|
||||
- name: Run Android ${{ matrix.task }}
|
||||
@@ -2417,10 +2232,8 @@ jobs:
|
||||
- checks-windows
|
||||
- macos-node
|
||||
- macos-swift
|
||||
- ios-build
|
||||
- android
|
||||
# Re-enable this job when we want to collect CI timing data for timing optimization.
|
||||
if: ${{ false && !cancelled() && always() && github.event_name != 'push' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
|
||||
if: ${{ !cancelled() && always() && github.event_name != 'push' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
|
||||
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:
|
||||
|
||||
40
.github/workflows/codeql-critical-quality.yml
vendored
40
.github/workflows/codeql-critical-quality.yml
vendored
@@ -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:
|
||||
@@ -152,7 +152,7 @@ jobs:
|
||||
quality-shards:
|
||||
name: Select Critical Quality shards
|
||||
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
agent: ${{ steps.detect.outputs.agent }}
|
||||
@@ -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/*)
|
||||
@@ -333,7 +333,7 @@ jobs:
|
||||
name: Critical Quality (core-auth-secrets)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.core_auth_secrets == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'core-auth-secrets') }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -356,7 +356,7 @@ jobs:
|
||||
name: Critical Quality (config-boundary)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.config == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'config-boundary') }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -379,7 +379,7 @@ jobs:
|
||||
name: Critical Quality (gateway-runtime-boundary)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.gateway == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'gateway-runtime-boundary') }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -402,7 +402,7 @@ jobs:
|
||||
name: Critical Quality (channel-runtime-boundary)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.channel == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'channel-runtime-boundary') }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -425,7 +425,7 @@ jobs:
|
||||
name: Critical Quality (network-runtime-boundary)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.network_runtime == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'network-runtime-boundary') }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -509,7 +509,7 @@ jobs:
|
||||
name: Critical Quality (agent-runtime-boundary)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.agent == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'agent-runtime-boundary') }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -532,7 +532,7 @@ jobs:
|
||||
name: Critical Quality (mcp-process-runtime-boundary)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.mcp_process == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'mcp-process-runtime-boundary') }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -555,7 +555,7 @@ jobs:
|
||||
name: Critical Quality (memory-runtime-boundary)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.memory == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'memory-runtime-boundary') }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -578,7 +578,7 @@ jobs:
|
||||
name: Critical Quality (session-diagnostics-boundary)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.session_diagnostics == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'session-diagnostics-boundary') }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -601,7 +601,7 @@ jobs:
|
||||
name: Critical Quality (plugin-sdk-reply-runtime)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.plugin_sdk_reply == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'plugin-sdk-reply-runtime') }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -624,7 +624,7 @@ jobs:
|
||||
name: Critical Quality (provider-runtime-boundary)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.provider == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'provider-runtime-boundary') }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -646,7 +646,7 @@ jobs:
|
||||
ui-control-plane:
|
||||
name: Critical Quality (ui-control-plane)
|
||||
if: ${{ github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || inputs.profile == 'all') }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -668,7 +668,7 @@ jobs:
|
||||
web-media-runtime-boundary:
|
||||
name: Critical Quality (web-media-runtime-boundary)
|
||||
if: ${{ github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || inputs.profile == 'all') }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -691,7 +691,7 @@ jobs:
|
||||
name: Critical Quality (plugin-boundary)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.plugin == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'plugin-boundary') }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -714,7 +714,7 @@ jobs:
|
||||
name: Critical Quality (plugin-sdk-package-contract)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.plugin_sdk_package == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'plugin-sdk-package-contract') }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -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:
|
||||
|
||||
22
.github/workflows/codeql.yml
vendored
22
.github/workflows/codeql.yml
vendored
@@ -22,12 +22,18 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- ".github/actions/**"
|
||||
- ".github/codeql/**"
|
||||
- ".github/workflows/**"
|
||||
- "packages/**"
|
||||
- "src/**"
|
||||
schedule:
|
||||
- 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"
|
||||
@@ -49,32 +55,32 @@ jobs:
|
||||
include:
|
||||
- language: javascript-typescript
|
||||
category: core-auth-secrets
|
||||
runs_on: ubuntu-24.04
|
||||
runs_on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout_minutes: 25
|
||||
config_file: ./.github/codeql/codeql-core-auth-secrets-critical-security.yml
|
||||
- language: javascript-typescript
|
||||
category: channel-runtime-boundary
|
||||
runs_on: ubuntu-24.04
|
||||
runs_on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout_minutes: 25
|
||||
config_file: ./.github/codeql/codeql-channel-runtime-boundary-critical-security.yml
|
||||
- language: javascript-typescript
|
||||
category: network-ssrf-boundary
|
||||
runs_on: ubuntu-24.04
|
||||
runs_on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout_minutes: 25
|
||||
config_file: ./.github/codeql/codeql-network-ssrf-boundary-critical-security.yml
|
||||
- language: javascript-typescript
|
||||
category: mcp-process-tool-boundary
|
||||
runs_on: ubuntu-24.04
|
||||
runs_on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout_minutes: 25
|
||||
config_file: ./.github/codeql/codeql-mcp-process-tool-boundary-critical-security.yml
|
||||
- language: javascript-typescript
|
||||
category: plugin-trust-boundary
|
||||
runs_on: ubuntu-24.04
|
||||
runs_on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout_minutes: 25
|
||||
config_file: ./.github/codeql/codeql-plugin-trust-boundary-critical-security.yml
|
||||
- language: actions
|
||||
category: actions
|
||||
runs_on: ubuntu-24.04
|
||||
runs_on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout_minutes: 10
|
||||
config_file: ./.github/codeql/codeql-actions-critical-security.yml
|
||||
steps:
|
||||
|
||||
@@ -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:
|
||||
|
||||
62
.github/workflows/crabbox-hydrate.yml
vendored
62
.github/workflows/crabbox-hydrate.yml
vendored
@@ -171,19 +171,10 @@ jobs:
|
||||
set -euo pipefail
|
||||
|
||||
node_bin="$(dirname "$(node -p 'process.execPath')")"
|
||||
link_node_tool() {
|
||||
local tool="$1"
|
||||
local source="$node_bin/$tool"
|
||||
local target="/usr/local/bin/$tool"
|
||||
if [ -e "$target" ] && [ "$(readlink -f "$source")" = "$(readlink -f "$target")" ]; then
|
||||
return
|
||||
fi
|
||||
sudo ln -sf "$source" "$target"
|
||||
}
|
||||
link_node_tool node
|
||||
link_node_tool npm
|
||||
link_node_tool npx
|
||||
link_node_tool corepack
|
||||
sudo ln -sf "$node_bin/node" /usr/local/bin/node
|
||||
sudo ln -sf "$node_bin/npm" /usr/local/bin/npm
|
||||
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
|
||||
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
|
||||
sudo tee /usr/local/bin/pnpm >/dev/null <<'PNPM'
|
||||
#!/usr/bin/env bash
|
||||
exec /usr/local/bin/corepack pnpm "$@"
|
||||
@@ -499,7 +490,7 @@ jobs:
|
||||
if (-not $env:CRABBOX_ID -or $env:CRABBOX_ID -notmatch '^[A-Za-z0-9._-]+$') {
|
||||
Write-Error "Invalid crabbox_id"
|
||||
}
|
||||
$actionsRoot = "C:\ProgramData\crabbox\actions"
|
||||
$actionsRoot = Join-Path $HOME ".crabbox\actions"
|
||||
New-Item -ItemType Directory -Force $actionsRoot | Out-Null
|
||||
$state = Join-Path $actionsRoot "$env:CRABBOX_ID.env"
|
||||
$envFile = Join-Path $actionsRoot "$env:CRABBOX_ID.env.ps1"
|
||||
@@ -555,7 +546,7 @@ jobs:
|
||||
if ($env:CRABBOX_KEEP_ALIVE_MINUTES -match '^[0-9]+$') {
|
||||
$minutes = [int]$env:CRABBOX_KEEP_ALIVE_MINUTES
|
||||
}
|
||||
$stop = Join-Path "C:\ProgramData\crabbox\actions" "$env:CRABBOX_ID.stop"
|
||||
$stop = Join-Path $HOME ".crabbox\actions\$env:CRABBOX_ID.stop"
|
||||
$deadline = (Get-Date).AddMinutes($minutes)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
if (Test-Path $stop) {
|
||||
@@ -593,19 +584,10 @@ jobs:
|
||||
fi
|
||||
|
||||
node_bin="$(dirname "$(node -p 'process.execPath')")"
|
||||
link_node_tool() {
|
||||
local tool="$1"
|
||||
local source="$node_bin/$tool"
|
||||
local target="/usr/local/bin/$tool"
|
||||
if [ -e "$target" ] && [ "$(readlink -f "$source")" = "$(readlink -f "$target")" ]; then
|
||||
return
|
||||
fi
|
||||
sudo ln -sf "$source" "$target"
|
||||
}
|
||||
link_node_tool node
|
||||
link_node_tool npm
|
||||
link_node_tool npx
|
||||
link_node_tool corepack
|
||||
sudo ln -sf "$node_bin/node" /usr/local/bin/node
|
||||
sudo ln -sf "$node_bin/npm" /usr/local/bin/npm
|
||||
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
|
||||
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
|
||||
sudo tee /usr/local/bin/pnpm >/dev/null <<'PNPM'
|
||||
#!/usr/bin/env bash
|
||||
exec /usr/local/bin/corepack pnpm "$@"
|
||||
@@ -681,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 }}
|
||||
@@ -692,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 }}
|
||||
|
||||
63
.github/workflows/ios-periphery-comment.yml
vendored
63
.github/workflows/ios-periphery-comment.yml
vendored
@@ -27,8 +27,10 @@ jobs:
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
with:
|
||||
script: |
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const zlib = require("node:zlib");
|
||||
const childProcess = require("node:child_process");
|
||||
|
||||
const marker = "<!-- openclaw-ios-periphery-dead-code -->";
|
||||
const run = context.payload.workflow_run;
|
||||
@@ -124,7 +126,10 @@ jobs:
|
||||
archive_format: "zip",
|
||||
});
|
||||
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ios-periphery-"));
|
||||
const archivePath = path.join(dir, "artifact.zip");
|
||||
const archiveBuffer = Buffer.from(archive.data);
|
||||
fs.writeFileSync(archivePath, archiveBuffer);
|
||||
|
||||
const allowedArtifactFiles = new Set([
|
||||
"periphery.json",
|
||||
@@ -235,59 +240,19 @@ jobs:
|
||||
return;
|
||||
}
|
||||
|
||||
entries.set(name, {
|
||||
compressedSize,
|
||||
compressionMethod,
|
||||
localHeaderOffset: readUInt32(offset + 42),
|
||||
uncompressedSize,
|
||||
});
|
||||
entries.set(name, { uncompressedSize });
|
||||
offset = nextOffset;
|
||||
}
|
||||
|
||||
const readZipEntry = (name, entry) => {
|
||||
const localHeaderOffset = entry.localHeaderOffset;
|
||||
if (
|
||||
localHeaderOffset + 30 > archiveBuffer.length ||
|
||||
readUInt32(localHeaderOffset) !== 0x04034b50
|
||||
) {
|
||||
throw new Error(`${name} has an invalid local header.`);
|
||||
}
|
||||
|
||||
const localNameLength = readUInt16(localHeaderOffset + 26);
|
||||
const localExtraLength = readUInt16(localHeaderOffset + 28);
|
||||
const dataStart = localHeaderOffset + 30 + localNameLength + localExtraLength;
|
||||
const dataEnd = dataStart + entry.compressedSize;
|
||||
if (dataEnd > archiveBuffer.length) {
|
||||
throw new Error(`${name} exceeds archive bounds.`);
|
||||
}
|
||||
|
||||
const compressed = archiveBuffer.subarray(dataStart, dataEnd);
|
||||
let contents;
|
||||
if (entry.compressionMethod === 0) {
|
||||
contents = compressed;
|
||||
} else {
|
||||
try {
|
||||
contents = zlib.inflateRawSync(compressed, { maxOutputLength: maxEntryBytes });
|
||||
} catch (error) {
|
||||
if (error && error.code === "ERR_BUFFER_TOO_LARGE") {
|
||||
throw new Error(`${name} exceeded the per-file size limit while reading.`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
if (contents.length !== entry.uncompressedSize || contents.length > maxEntryBytes) {
|
||||
throw new Error(`${name} exceeded the per-file size limit while reading.`);
|
||||
}
|
||||
return contents.toString("utf8");
|
||||
};
|
||||
|
||||
const files = new Map();
|
||||
for (const [name, entry] of entries) {
|
||||
let contents;
|
||||
try {
|
||||
contents = readZipEntry(name, entry);
|
||||
} catch (error) {
|
||||
core.warning(`Skipping ${artifactName}; ${error instanceof Error ? error.message : String(error)}`);
|
||||
const contents = childProcess.execFileSync("unzip", ["-p", archivePath, name], {
|
||||
encoding: "utf8",
|
||||
maxBuffer: Math.max(1, entry.uncompressedSize + 1024),
|
||||
timeout: 5000,
|
||||
});
|
||||
if (Buffer.byteLength(contents, "utf8") > maxEntryBytes) {
|
||||
core.warning(`Skipping ${artifactName}; ${name} exceeded the per-file size limit while reading.`);
|
||||
return;
|
||||
}
|
||||
files.set(name, contents);
|
||||
|
||||
2
.github/workflows/ios-periphery.yml
vendored
2
.github/workflows/ios-periphery.yml
vendored
@@ -220,7 +220,7 @@ jobs:
|
||||
with:
|
||||
name: ios-periphery-dead-code-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/ios-periphery
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
retention-days: 14
|
||||
|
||||
- name: Fail on dead code
|
||||
|
||||
2
.github/workflows/mantis-discord-smoke.yml
vendored
2
.github/workflows/mantis-discord-smoke.yml
vendored
@@ -171,4 +171,4 @@ jobs:
|
||||
name: mantis-discord-smoke-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: .artifacts/qa-e2e/mantis/
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
@@ -540,7 +540,7 @@ jobs:
|
||||
name: mantis-discord-status-reactions-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ steps.run_mantis.outputs.output_dir }}
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Create Mantis GitHub App token
|
||||
id: mantis_app_token
|
||||
|
||||
@@ -547,7 +547,7 @@ jobs:
|
||||
with:
|
||||
name: mantis-discord-thread-attachment-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ steps.run_mantis.outputs.output_dir }}
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
retention-days: 14
|
||||
|
||||
- name: Create Mantis GitHub App token
|
||||
|
||||
@@ -458,7 +458,7 @@ jobs:
|
||||
name: mantis-slack-desktop-smoke-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ steps.run_mantis.outputs.output_dir }}
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Create Mantis GitHub App token
|
||||
id: mantis_app_token
|
||||
|
||||
@@ -556,7 +556,7 @@ jobs:
|
||||
name: mantis-telegram-desktop-proof-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ steps.inspect.outputs.output_dir }}
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Create Mantis GitHub App token
|
||||
id: mantis_app_token
|
||||
|
||||
2
.github/workflows/mantis-telegram-live.yml
vendored
2
.github/workflows/mantis-telegram-live.yml
vendored
@@ -506,7 +506,7 @@ jobs:
|
||||
name: mantis-telegram-live-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ steps.run_mantis.outputs.output_dir }}
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Create Mantis GitHub App token
|
||||
id: mantis_app_token
|
||||
|
||||
464
.github/workflows/maturity-scorecard.yml
vendored
464
.github/workflows/maturity-scorecard.yml
vendored
@@ -1,464 +0,0 @@
|
||||
name: Maturity scorecard
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
qa_evidence_run_id:
|
||||
description: Optional workflow run id containing qa-evidence.json
|
||||
required: false
|
||||
type: string
|
||||
ref:
|
||||
description: OpenClaw branch, tag, or SHA containing the maturity score source
|
||||
required: true
|
||||
default: main
|
||||
type: string
|
||||
expected_sha:
|
||||
description: Optional full SHA that ref must resolve to
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
workflow_call:
|
||||
inputs:
|
||||
qa_evidence_run_id:
|
||||
description: Optional workflow run id containing qa-evidence.json
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
ref:
|
||||
description: OpenClaw branch, tag, or SHA containing the maturity score source
|
||||
required: true
|
||||
type: string
|
||||
expected_sha:
|
||||
description: Optional full SHA that ref must resolve to
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
secrets:
|
||||
OPENAI_API_KEY:
|
||||
description: OpenAI API key used by live QA profile scenarios
|
||||
required: true
|
||||
OPENCLAW_MATURITY_SCORECARD_AGENT_OPENAI_API_KEY:
|
||||
description: Optional OpenAI API key used by maturity scorecard agent steps
|
||||
required: false
|
||||
GH_APP_PRIVATE_KEY:
|
||||
description: Optional GitHub App private key for generated docs PR creation
|
||||
required: false
|
||||
GH_APP_PRIVATE_KEY_FALLBACK:
|
||||
description: Optional fallback GitHub App private key for generated docs PR creation
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ format('{0}-{1}-{2}', github.workflow, inputs.ref, inputs.qa_evidence_run_id || github.run_id) }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
NODE_VERSION: "24.x"
|
||||
|
||||
jobs:
|
||||
validate_selected_ref:
|
||||
name: Validate selected ref
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
selected_revision: ${{ steps.validate.outputs.selected_revision }}
|
||||
trusted_reason: ${{ steps.validate.outputs.trusted_reason }}
|
||||
steps:
|
||||
- name: Checkout selected ref
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ inputs.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate selected ref
|
||||
id: validate
|
||||
env:
|
||||
EXPECTED_SHA: ${{ inputs.expected_sha }}
|
||||
INPUT_REF: ${{ inputs.ref }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
selected_revision="$(git rev-parse HEAD)"
|
||||
expected_sha="${EXPECTED_SHA,,}"
|
||||
trusted_reason=""
|
||||
|
||||
if [[ -n "${expected_sha// }" && ! "$expected_sha" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "expected_sha must be a full 40-character SHA; got: ${EXPECTED_SHA}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -n "${expected_sha// }" && "${selected_revision,,}" != "$expected_sha" ]]; then
|
||||
echo "Ref '${INPUT_REF}' resolved to ${selected_revision}, expected ${EXPECTED_SHA}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
|
||||
|
||||
if git merge-base --is-ancestor "$selected_revision" refs/remotes/origin/main; then
|
||||
trusted_reason="main-ancestor"
|
||||
elif git tag --points-at "$selected_revision" | grep -Eq '^v'; then
|
||||
trusted_reason="release-tag"
|
||||
elif [[ "$INPUT_REF" =~ ^release/[0-9]{4}\.[0-9]+\.[0-9]+$ ]]; then
|
||||
git fetch --no-tags origin "+refs/heads/${INPUT_REF}:refs/remotes/origin/${INPUT_REF}"
|
||||
release_branch_sha="$(git rev-parse "refs/remotes/origin/${INPUT_REF}")"
|
||||
if [[ "$selected_revision" == "$release_branch_sha" ]]; then
|
||||
trusted_reason="release-branch-head"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$trusted_reason" ]]; then
|
||||
echo "Ref '${INPUT_REF}' resolved to $selected_revision, which is not trusted for this secret-bearing maturity scorecard run." >&2
|
||||
echo "Allowed refs must be on main, point to a release tag, or match a release branch head." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "selected_revision=$selected_revision" >> "$GITHUB_OUTPUT"
|
||||
echo "trusted_reason=$trusted_reason" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "### Target"
|
||||
echo
|
||||
echo "- Requested ref: \`${INPUT_REF}\`"
|
||||
echo "- Resolved SHA: \`$selected_revision\`"
|
||||
echo "- Trust reason: \`$trusted_reason\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
generate_qa_evidence:
|
||||
name: Generate full taxonomy QA evidence
|
||||
needs: validate_selected_ref
|
||||
if: ${{ inputs.qa_evidence_run_id == '' }}
|
||||
uses: ./.github/workflows/qa-profile-evidence.yml
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
expected_sha: ${{ needs.validate_selected_ref.outputs.selected_revision }}
|
||||
qa_profile: all
|
||||
secrets:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
|
||||
publish:
|
||||
name: Publish maturity docs PR
|
||||
needs:
|
||||
- validate_selected_ref
|
||||
- generate_qa_evidence
|
||||
if: ${{ always() && needs.validate_selected_ref.result == 'success' && (inputs.qa_evidence_run_id != '' || needs.generate_qa_evidence.result == 'success') }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout selected ref
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
with:
|
||||
ref: ${{ needs.validate_selected_ref.outputs.selected_revision }}
|
||||
fetch-depth: 1
|
||||
fetch-tags: false
|
||||
persist-credentials: false
|
||||
submodules: false
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
install-bun: "false"
|
||||
|
||||
- name: Download provided QA evidence artifact
|
||||
if: ${{ inputs.qa_evidence_run_id != '' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
QA_EVIDENCE_RUN_ID: ${{ inputs.qa_evidence_run_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p .artifacts/maturity-evidence
|
||||
gh run download "$QA_EVIDENCE_RUN_ID" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--dir .artifacts/maturity-evidence
|
||||
|
||||
- name: Download generated QA evidence artifact
|
||||
if: ${{ inputs.qa_evidence_run_id == '' }}
|
||||
env:
|
||||
GENERATED_ARTIFACT_NAME: ${{ needs.generate_qa_evidence.outputs.artifact_name }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z "${GENERATED_ARTIFACT_NAME:-}" ]]; then
|
||||
echo "Generated QA evidence workflow did not expose an artifact name." >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p .artifacts/maturity-evidence
|
||||
gh run download "$GITHUB_RUN_ID" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--name "$GENERATED_ARTIFACT_NAME" \
|
||||
--dir .artifacts/maturity-evidence
|
||||
|
||||
- name: Require one QA evidence file
|
||||
id: evidence
|
||||
env:
|
||||
QA_EVIDENCE_RUN_ID: ${{ inputs.qa_evidence_run_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mapfile -t evidence_paths < <(find .artifacts/maturity-evidence -type f -name qa-evidence.json | sort)
|
||||
if [[ "${#evidence_paths[@]}" -eq 0 ]]; then
|
||||
echo "Expected a qa-evidence.json file in the downloaded QA evidence artifact." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${#evidence_paths[@]}" -gt 1 ]]; then
|
||||
echo "Expected exactly one qa-evidence.json file, found ${#evidence_paths[@]}:" >&2
|
||||
printf '%s\n' "${evidence_paths[@]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "qa_evidence_path=${evidence_paths[0]}" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "### QA evidence"
|
||||
echo
|
||||
echo "- Evidence path: \`${evidence_paths[0]}\`"
|
||||
echo "- Evidence source run: \`${QA_EVIDENCE_RUN_ID:-$GITHUB_RUN_ID}\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Validate QA evidence manifest
|
||||
env:
|
||||
QA_EVIDENCE_PATH: ${{ steps.evidence.outputs.qa_evidence_path }}
|
||||
TARGET_SHA: ${{ needs.validate_selected_ref.outputs.selected_revision }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node --input-type=module <<'NODE'
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const evidencePath = process.env.QA_EVIDENCE_PATH;
|
||||
const targetSha = process.env.TARGET_SHA;
|
||||
if (!evidencePath) {
|
||||
throw new Error("QA_EVIDENCE_PATH is required");
|
||||
}
|
||||
if (!targetSha) {
|
||||
throw new Error("TARGET_SHA is required");
|
||||
}
|
||||
|
||||
const evidence = JSON.parse(fs.readFileSync(evidencePath, "utf8"));
|
||||
if (evidence.profile !== "all") {
|
||||
throw new Error(`qa-evidence.json profile must be all, got ${JSON.stringify(evidence.profile)}`);
|
||||
}
|
||||
|
||||
const artifactDir = path.dirname(evidencePath);
|
||||
const manifestNames = fs
|
||||
.readdirSync(artifactDir)
|
||||
.filter((name) => name.endsWith("qa-profile-evidence-manifest.json"))
|
||||
.sort();
|
||||
if (manifestNames.length !== 1) {
|
||||
throw new Error(
|
||||
`Expected exactly one QA profile evidence manifest next to qa-evidence.json, found ${manifestNames.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
const manifestPath = path.join(artifactDir, manifestNames[0]);
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
||||
const manifestProfile = manifest.qaProfile ?? evidence.profile;
|
||||
if (manifestProfile !== "all") {
|
||||
throw new Error(`QA evidence manifest profile must be all, got ${JSON.stringify(manifestProfile)}`);
|
||||
}
|
||||
if (manifest.targetSha !== targetSha) {
|
||||
throw new Error(`QA evidence manifest targetSha ${manifest.targetSha} does not match selected ref ${targetSha}`);
|
||||
}
|
||||
NODE
|
||||
|
||||
- name: Ensure maturity scorecard agent key exists
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENCLAW_MATURITY_SCORECARD_AGENT_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
|
||||
echo "Missing OPENCLAW_MATURITY_SCORECARD_AGENT_OPENAI_API_KEY or OPENAI_API_KEY secret." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Run Codex maturity scorecard agent
|
||||
uses: openai/codex-action@e0fdf01220eb9a88167c4898839d273e3f2609d1
|
||||
env:
|
||||
MATURITY_EVIDENCE_DIR: .artifacts/maturity-evidence
|
||||
MATURITY_SCORES_PATH: qa/maturity-scores.yaml
|
||||
MATURITY_TAXONOMY_PATH: taxonomy.yaml
|
||||
with:
|
||||
openai-api-key: ${{ secrets.OPENCLAW_MATURITY_SCORECARD_AGENT_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
|
||||
prompt-file: .github/codex/prompts/maturity-scorecard-agent.md
|
||||
model: ${{ vars.OPENCLAW_CI_OPENAI_MODEL_BARE }}
|
||||
effort: high
|
||||
sandbox: workspace-write
|
||||
safety-strategy: drop-sudo
|
||||
|
||||
- name: Enforce focused maturity score patch
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git restore --staged :/
|
||||
|
||||
allowed='^qa/maturity-scores\.yaml$'
|
||||
bad_tracked="$(
|
||||
git diff --name-only HEAD -- | while IFS= read -r path; do
|
||||
if [[ ! "$path" =~ $allowed ]]; then
|
||||
printf '%s\n' "$path"
|
||||
fi
|
||||
done
|
||||
)"
|
||||
if [[ -n "$bad_tracked" ]]; then
|
||||
echo "Maturity scorecard agent touched forbidden tracked paths:"
|
||||
printf '%s\n' "$bad_tracked"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bad_untracked="$(
|
||||
git ls-files --others --exclude-standard | while IFS= read -r path; do
|
||||
if [[ "$path" != "qa/maturity-scores.yaml" ]]; then
|
||||
printf '%s\n' "$path"
|
||||
fi
|
||||
done
|
||||
)"
|
||||
if [[ -n "$bad_untracked" ]]; then
|
||||
echo "Maturity scorecard agent created forbidden untracked paths:"
|
||||
printf '%s\n' "$bad_untracked"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f qa/maturity-scores.yaml ]]; then
|
||||
echo "Maturity scorecard agent must produce qa/maturity-scores.yaml." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Validate maturity score sources
|
||||
run: |
|
||||
node --import tsx --input-type=module <<'NODE'
|
||||
import { readValidatedQaMaturityScoreSources } from "./extensions/qa-lab/src/scorecard-taxonomy.ts";
|
||||
|
||||
const { warnings } = readValidatedQaMaturityScoreSources({
|
||||
scoresPath: "qa/maturity-scores.yaml",
|
||||
taxonomyPath: "taxonomy.yaml",
|
||||
});
|
||||
for (const warning of warnings) {
|
||||
console.error(`warning: ${warning}`);
|
||||
}
|
||||
NODE
|
||||
|
||||
- name: Render artifact docs
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pnpm maturity:render -- \
|
||||
--output-dir .artifacts/maturity-docs \
|
||||
--static-assets-dir .artifacts/maturity-docs/assets/maturity \
|
||||
--scores qa/maturity-scores.yaml \
|
||||
--evidence-dir .artifacts/maturity-evidence \
|
||||
--strict-inputs
|
||||
{
|
||||
echo "### Maturity scorecard docs"
|
||||
echo
|
||||
echo "- Source validation: passed"
|
||||
echo "- Artifact docs: \`.artifacts/maturity-docs\`"
|
||||
echo "- Strict inputs: \`true\`"
|
||||
echo "- QA evidence: included"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Render committed docs preview
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pnpm maturity:render -- \
|
||||
--output-dir docs \
|
||||
--scores qa/maturity-scores.yaml \
|
||||
--evidence-dir .artifacts/maturity-evidence \
|
||||
--strict-inputs
|
||||
|
||||
- name: Create generated docs PR app token
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
id: app-token
|
||||
continue-on-error: true
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
|
||||
with:
|
||||
app-id: "2729701"
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
permission-contents: write
|
||||
permission-pull-requests: write
|
||||
|
||||
- name: Create generated docs PR fallback app token
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && steps.app-token.outcome == 'failure' }}
|
||||
id: app-token-fallback
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
|
||||
with:
|
||||
app-id: "2971289"
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
|
||||
permission-contents: write
|
||||
permission-pull-requests: write
|
||||
|
||||
- name: Open generated docs PR
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
|
||||
QA_EVIDENCE_RUN_ID: ${{ inputs.qa_evidence_run_id }}
|
||||
REF_INPUT: ${{ inputs.ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z "${GH_TOKEN:-}" ]]; then
|
||||
echo "Maturity scorecard PR creation requires the OpenClaw GitHub App token secrets." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$(git status --porcelain -- qa/maturity-scores.yaml docs/maturity/scorecard.md docs/maturity/taxonomy.md)" ]]; then
|
||||
{
|
||||
echo
|
||||
echo "- Pull request: skipped; generated scorecard matches selected ref"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
evidence_run_id="${QA_EVIDENCE_RUN_ID:-$GITHUB_RUN_ID}"
|
||||
branch="automation/maturity-scorecard-${evidence_run_id}"
|
||||
base_branch="${REF_INPUT:-main}"
|
||||
if ! git ls-remote --exit-code --heads origin "$base_branch" >/dev/null 2>&1; then
|
||||
base_branch="main"
|
||||
fi
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
gh auth setup-git
|
||||
git fetch --no-tags --depth=1 origin "refs/heads/${branch}:refs/remotes/origin/${branch}" || true
|
||||
git switch -C "$branch"
|
||||
git add qa/maturity-scores.yaml docs/maturity/scorecard.md docs/maturity/taxonomy.md
|
||||
git commit -m "docs: update maturity scorecard"
|
||||
git push --force-with-lease origin "$branch"
|
||||
|
||||
body_file=".artifacts/maturity-scorecard-pr-body.md"
|
||||
mkdir -p "$(dirname "$body_file")"
|
||||
cat > "$body_file" <<BODY
|
||||
## Summary
|
||||
|
||||
- render maturity scorecard docs from \`qa/maturity-scores.yaml\` and full taxonomy QA evidence
|
||||
- maturity source ref: ${REF_INPUT}
|
||||
- QA evidence run: ${evidence_run_id}
|
||||
|
||||
## Verification
|
||||
|
||||
- QA Lab maturity score validation passed
|
||||
- Maturity scorecard workflow rendered docs from all profile qa-evidence.json artifacts with strict inputs
|
||||
BODY
|
||||
|
||||
pr_url="$(gh pr list --head "$branch" --state open --json url --jq '.[0].url // ""')"
|
||||
if [[ -n "$pr_url" ]]; then
|
||||
gh pr edit "$pr_url" \
|
||||
--title "docs: update maturity scorecard" \
|
||||
--body-file "$body_file"
|
||||
else
|
||||
pr_url="$(gh pr create \
|
||||
--base "$base_branch" \
|
||||
--head "$branch" \
|
||||
--title "docs: update maturity scorecard" \
|
||||
--body-file "$body_file")"
|
||||
fi
|
||||
{
|
||||
echo
|
||||
echo "- Pull request: ${pr_url}"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload maturity docs artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: maturity-scorecard-docs-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: .artifacts/maturity-docs/
|
||||
retention-days: 30
|
||||
if-no-files-found: error
|
||||
2
.github/workflows/npm-telegram-beta-e2e.yml
vendored
2
.github/workflows/npm-telegram-beta-e2e.yml
vendored
@@ -273,4 +273,4 @@ jobs:
|
||||
name: npm-telegram-beta-e2e-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: .artifacts/qa-e2e/
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
@@ -609,6 +609,7 @@ jobs:
|
||||
requires_repo_e2e: true
|
||||
requires_live_suites: false
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENCLAW_E2E_WORKERS: "1"
|
||||
OPENCLAW_VITEST_MAX_WORKERS: "1"
|
||||
steps:
|
||||
@@ -642,74 +643,9 @@ jobs:
|
||||
set -euo pipefail
|
||||
case "${{ matrix.suite_id }}" in
|
||||
openshell-e2e)
|
||||
echo "OPENCLAW_E2E_OPENSHELL_CONFIG_HOME=$HOME/.config" >> "$GITHUB_ENV"
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Install OpenShell CLI
|
||||
if: |
|
||||
(inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) &&
|
||||
matrix.suite_id == 'openshell-e2e'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export OPENSHELL_VERSION=v0.0.68
|
||||
curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/d64542f69d06694cbd203b64929d286dd0533bbb/install.sh | sh
|
||||
openshell --version
|
||||
|
||||
- name: Bootstrap OpenShell gateway
|
||||
if: |
|
||||
(inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) &&
|
||||
matrix.suite_id == 'openshell-e2e'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mtls_dir="$HOME/.config/openshell/gateways/openshell/mtls"
|
||||
gateway_tls_dir="$RUNNER_TEMP/openshell-gateway-certs"
|
||||
fallback_pid=""
|
||||
if ! openshell --gateway openshell sandbox list >/dev/null 2>&1; then
|
||||
rm -rf "$gateway_tls_dir"
|
||||
openshell-gateway generate-certs \
|
||||
--output-dir "$gateway_tls_dir" \
|
||||
--server-san 127.0.0.1 \
|
||||
--server-san localhost \
|
||||
--server-san host.openshell.internal
|
||||
rm -rf "$mtls_dir"
|
||||
mkdir -p "$mtls_dir"
|
||||
cp "$gateway_tls_dir/ca.crt" "$mtls_dir/ca.crt"
|
||||
cp "$gateway_tls_dir/client/tls.crt" "$mtls_dir/tls.crt"
|
||||
cp "$gateway_tls_dir/client/tls.key" "$mtls_dir/tls.key"
|
||||
openshell gateway remove openshell >/dev/null 2>&1 || true
|
||||
OPENSHELL_LOCAL_TLS_DIR="$gateway_tls_dir" nohup openshell-gateway \
|
||||
--bind-address 0.0.0.0 \
|
||||
--port 17670 \
|
||||
--drivers docker \
|
||||
--tls-cert "$gateway_tls_dir/server/tls.crt" \
|
||||
--tls-key "$gateway_tls_dir/server/tls.key" \
|
||||
--tls-client-ca "$mtls_dir/ca.crt" \
|
||||
>"$RUNNER_TEMP/openshell-gateway.log" 2>&1 &
|
||||
fallback_pid=$!
|
||||
echo "OPENCLAW_OPENSHELL_FALLBACK_PID=$fallback_pid" >> "$GITHUB_ENV"
|
||||
for _ in $(seq 1 30); do
|
||||
if openshell gateway add --local --name openshell https://127.0.0.1:17670; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
openshell gateway select openshell
|
||||
for _ in $(seq 1 60); do
|
||||
if openshell --gateway openshell sandbox list >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
fi
|
||||
if [[ -z "$fallback_pid" ]]; then
|
||||
echo "OPENCLAW_OPENSHELL_FALLBACK_PID=" >> "$GITHUB_ENV"
|
||||
fi
|
||||
openshell --gateway openshell sandbox list >/dev/null
|
||||
openshell gateway list
|
||||
|
||||
- name: Validate suite credentials
|
||||
if: inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id
|
||||
shell: bash
|
||||
@@ -729,15 +665,6 @@ jobs:
|
||||
(inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id)
|
||||
run: ${{ matrix.command }}
|
||||
|
||||
- name: Stop fallback OpenShell gateway
|
||||
if: always() && matrix.suite_id == 'openshell-e2e'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -n "${OPENCLAW_OPENSHELL_FALLBACK_PID:-}" ]]; then
|
||||
kill "$OPENCLAW_OPENSHELL_FALLBACK_PID" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
validate_docker_e2e:
|
||||
needs: [validate_selected_ref, prepare_docker_e2e_image, plan_release_workflow_matrices]
|
||||
if: inputs.include_release_path_suites && inputs.docker_lanes == '' && needs.plan_release_workflow_matrices.outputs.docker_e2e_count != '0'
|
||||
@@ -1759,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"
|
||||
@@ -2074,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
|
||||
@@ -2377,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
|
||||
|
||||
37
.github/workflows/openclaw-performance.yml
vendored
37
.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 }}
|
||||
@@ -151,39 +150,11 @@ jobs:
|
||||
echo "present=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Resolve OpenClaw target ref
|
||||
id: target
|
||||
if: steps.lane.outputs.run == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TARGET_REF_INPUT: ${{ inputs.target_ref }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
requested="${TARGET_REF_INPUT:-}"
|
||||
if [[ -z "$requested" ]]; then
|
||||
echo "checkout_ref=${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
|
||||
echo "tested_ref=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
encoded_ref="$(node -e 'process.stdout.write(encodeURIComponent(process.argv[1]))' "$requested")"
|
||||
if ! resolved_sha="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${encoded_ref}" --jq '.sha')"; then
|
||||
echo "::error::Unable to resolve OpenClaw target_ref '${requested}'." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! "$resolved_sha" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "::error::OpenClaw target_ref '${requested}' resolved to invalid SHA '${resolved_sha}'." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "checkout_ref=${resolved_sha}" >> "$GITHUB_OUTPUT"
|
||||
echo "tested_ref=${requested}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout OpenClaw
|
||||
if: steps.lane.outputs.run == 'true'
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
with:
|
||||
ref: ${{ steps.target.outputs.checkout_ref }}
|
||||
ref: ${{ inputs.target_ref || github.ref }}
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
@@ -320,7 +291,6 @@ jobs:
|
||||
--auth "$AUTH_MODE"
|
||||
--parallel 1
|
||||
--repeat "$repeat"
|
||||
--timeout-ms "$KOVA_SCENARIO_TIMEOUT_MS"
|
||||
--report-dir "$REPORT_DIR"
|
||||
--execute
|
||||
--json
|
||||
@@ -391,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}
|
||||
|
||||
53
.github/workflows/openclaw-release-checks.yml
vendored
53
.github/workflows/openclaw-release-checks.yml
vendored
@@ -44,11 +44,6 @@ on:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
run_maturity_scorecard:
|
||||
description: Render advisory maturity scorecard release docs; default release checks rely on dedicated package, QA, live, and E2E gates
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
rerun_group:
|
||||
description: Release check group to run
|
||||
required: false
|
||||
@@ -111,7 +106,6 @@ jobs:
|
||||
mode: ${{ steps.inputs.outputs.mode }}
|
||||
release_profile: ${{ steps.inputs.outputs.release_profile }}
|
||||
run_release_soak: ${{ steps.inputs.outputs.run_release_soak }}
|
||||
run_maturity_scorecard: ${{ steps.inputs.outputs.run_maturity_scorecard }}
|
||||
rerun_group: ${{ steps.inputs.outputs.rerun_group }}
|
||||
live_suite_filter: ${{ steps.inputs.outputs.live_suite_filter }}
|
||||
cross_os_suite_filter: ${{ steps.inputs.outputs.cross_os_suite_filter }}
|
||||
@@ -285,7 +279,6 @@ jobs:
|
||||
RELEASE_MODE_INPUT: ${{ inputs.mode }}
|
||||
RELEASE_PROFILE_INPUT: ${{ inputs.release_profile }}
|
||||
RELEASE_RUN_RELEASE_SOAK_INPUT: ${{ inputs.run_release_soak }}
|
||||
RELEASE_RUN_MATURITY_SCORECARD_INPUT: ${{ inputs.run_maturity_scorecard }}
|
||||
RELEASE_RERUN_GROUP_INPUT: ${{ inputs.rerun_group }}
|
||||
RELEASE_LIVE_SUITE_FILTER_INPUT: ${{ inputs.live_suite_filter }}
|
||||
RELEASE_CROSS_OS_SUITE_FILTER_INPUT: ${{ inputs.cross_os_suite_filter }}
|
||||
@@ -326,12 +319,6 @@ jobs:
|
||||
else
|
||||
run_release_soak=true
|
||||
fi
|
||||
run_maturity_scorecard="$(printf '%s' "$RELEASE_RUN_MATURITY_SCORECARD_INPUT" | tr '[:upper:]' '[:lower:]')"
|
||||
if [[ "$run_maturity_scorecard" != "true" && "$run_maturity_scorecard" != "1" && "$run_maturity_scorecard" != "yes" ]]; then
|
||||
run_maturity_scorecard=false
|
||||
else
|
||||
run_maturity_scorecard=true
|
||||
fi
|
||||
release_profile="$RELEASE_PROFILE_INPUT"
|
||||
if [[ "$release_profile" == "minimum" ]]; then
|
||||
release_profile=beta
|
||||
@@ -435,7 +422,6 @@ jobs:
|
||||
printf 'mode=%s\n' "$RELEASE_MODE_INPUT"
|
||||
printf 'release_profile=%s\n' "$release_profile"
|
||||
printf 'run_release_soak=%s\n' "$run_release_soak"
|
||||
printf 'run_maturity_scorecard=%s\n' "$run_maturity_scorecard"
|
||||
printf 'rerun_group=%s\n' "$RELEASE_RERUN_GROUP_INPUT"
|
||||
printf 'live_suite_filter=%s\n' "$RELEASE_LIVE_SUITE_FILTER_INPUT"
|
||||
printf 'cross_os_suite_filter=%s\n' "$RELEASE_CROSS_OS_SUITE_FILTER_INPUT"
|
||||
@@ -458,7 +444,6 @@ jobs:
|
||||
RELEASE_MODE: ${{ inputs.mode }}
|
||||
RELEASE_PROFILE: ${{ steps.inputs.outputs.release_profile }}
|
||||
RUN_RELEASE_SOAK: ${{ steps.inputs.outputs.run_release_soak }}
|
||||
RUN_MATURITY_SCORECARD: ${{ steps.inputs.outputs.run_maturity_scorecard }}
|
||||
RELEASE_RERUN_GROUP: ${{ inputs.rerun_group }}
|
||||
RELEASE_LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }}
|
||||
RELEASE_CROSS_OS_SUITE_FILTER: ${{ inputs.cross_os_suite_filter }}
|
||||
@@ -476,7 +461,6 @@ jobs:
|
||||
echo "- Cross-OS mode: \`${RELEASE_MODE}\`"
|
||||
echo "- Release profile: \`${RELEASE_PROFILE}\`"
|
||||
echo "- Release soak lanes: \`${RUN_RELEASE_SOAK}\`"
|
||||
echo "- Maturity scorecard docs: \`${RUN_MATURITY_SCORECARD}\`"
|
||||
echo "- Rerun group: \`${RELEASE_RERUN_GROUP}\`"
|
||||
if [[ -n "${RELEASE_LIVE_SUITE_FILTER// }" ]]; then
|
||||
echo "- Live suite filter: \`${RELEASE_LIVE_SUITE_FILTER}\`"
|
||||
@@ -733,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 }}
|
||||
@@ -783,20 +768,6 @@ jobs:
|
||||
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
|
||||
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
|
||||
|
||||
maturity_scorecard_release_checks:
|
||||
name: Render maturity scorecard release docs
|
||||
needs: [resolve_target]
|
||||
if: contains(fromJSON('["all","qa"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.run_maturity_scorecard == 'true'
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
uses: ./.github/workflows/maturity-scorecard.yml
|
||||
with:
|
||||
ref: ${{ needs.resolve_target.outputs.ref }}
|
||||
expected_sha: ${{ needs.resolve_target.outputs.revision }}
|
||||
secrets:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
|
||||
qa_lab_parity_lane_release_checks:
|
||||
name: Run QA Lab parity lane (${{ matrix.lane }})
|
||||
needs: [resolve_target]
|
||||
@@ -883,7 +854,7 @@ jobs:
|
||||
name: release-qa-parity-${{ matrix.lane }}-${{ needs.resolve_target.outputs.revision }}
|
||||
path: .artifacts/qa-e2e/
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Record advisory status
|
||||
if: always()
|
||||
@@ -989,7 +960,7 @@ jobs:
|
||||
name: release-qa-parity-${{ needs.resolve_target.outputs.revision }}
|
||||
path: .artifacts/qa-e2e/
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Record advisory status
|
||||
if: always()
|
||||
@@ -1161,7 +1132,7 @@ jobs:
|
||||
name: release-qa-runtime-parity-${{ needs.resolve_target.outputs.revision }}
|
||||
path: .artifacts/qa-e2e/
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Record advisory status
|
||||
if: always()
|
||||
@@ -1271,13 +1242,13 @@ jobs:
|
||||
--output .artifacts/qa-e2e/runtime-parity-standard-report/qa-runtime-tool-coverage-report.md
|
||||
|
||||
- name: Upload runtime tool coverage artifacts
|
||||
if: ${{ always() && steps.verify_runtime_parity_status.outputs.ready == 'true' }}
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: release-qa-runtime-tool-coverage-${{ needs.resolve_target.outputs.revision }}
|
||||
path: .artifacts/qa-e2e/runtime-parity-standard-report/
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
qa_live_matrix_release_checks:
|
||||
name: Run QA Lab live Matrix lane
|
||||
@@ -1357,7 +1328,7 @@ jobs:
|
||||
name: release-qa-live-matrix-${{ needs.resolve_target.outputs.revision }}
|
||||
path: .artifacts/qa-e2e/
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Record advisory status
|
||||
if: always()
|
||||
@@ -1497,7 +1468,7 @@ jobs:
|
||||
name: release-qa-live-telegram-${{ needs.resolve_target.outputs.revision }}
|
||||
path: .artifacts/qa-e2e/
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Record advisory status
|
||||
if: always()
|
||||
@@ -1637,7 +1608,7 @@ jobs:
|
||||
name: release-qa-live-discord-${{ needs.resolve_target.outputs.revision }}
|
||||
path: .artifacts/qa-e2e/
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Record advisory status
|
||||
if: always()
|
||||
@@ -1780,7 +1751,7 @@ jobs:
|
||||
name: release-qa-live-whatsapp-${{ needs.resolve_target.outputs.revision }}
|
||||
path: .artifacts/qa-e2e/
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Record advisory status
|
||||
if: always()
|
||||
@@ -1920,7 +1891,7 @@ jobs:
|
||||
name: release-qa-live-slack-${{ needs.resolve_target.outputs.revision }}
|
||||
path: .artifacts/qa-e2e/
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Record advisory status
|
||||
if: always()
|
||||
@@ -1976,7 +1947,6 @@ jobs:
|
||||
- docker_e2e_release_checks
|
||||
- package_acceptance_release_checks
|
||||
- qa_lab_parity_lane_release_checks
|
||||
- maturity_scorecard_release_checks
|
||||
- qa_lab_parity_report_release_checks
|
||||
- qa_lab_runtime_parity_release_checks
|
||||
- runtime_tool_coverage_release_checks
|
||||
@@ -2062,7 +2032,6 @@ jobs:
|
||||
"docker_e2e_release_checks=${{ needs.docker_e2e_release_checks.result }}" \
|
||||
"package_acceptance_release_checks=${{ needs.package_acceptance_release_checks.result }}" \
|
||||
"qa_lab_parity_lane_release_checks=${{ needs.qa_lab_parity_lane_release_checks.result }}" \
|
||||
"maturity_scorecard_release_checks=${{ needs.maturity_scorecard_release_checks.result }}" \
|
||||
"qa_lab_parity_report_release_checks=${{ needs.qa_lab_parity_report_release_checks.result }}" \
|
||||
"qa_lab_runtime_parity_release_checks=${{ needs.qa_lab_runtime_parity_release_checks.result }}" \
|
||||
"runtime_tool_coverage_release_checks=${{ needs.runtime_tool_coverage_release_checks.result }}" \
|
||||
|
||||
28
.github/workflows/openclaw-release-publish.yml
vendored
28
.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
|
||||
|
||||
@@ -1466,9 +1486,9 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload postpublish evidence
|
||||
if: ${{ always() && inputs.publish_openclaw_npm }}
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: openclaw-release-postpublish-evidence-${{ inputs.tag }}
|
||||
path: ${{ runner.temp }}/openclaw-release-postpublish-evidence
|
||||
if-no-files-found: error
|
||||
if-no-files-found: ignore
|
||||
|
||||
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" \
|
||||
|
||||
2
.github/workflows/opengrep-precise-full.yml
vendored
2
.github/workflows/opengrep-precise-full.yml
vendored
@@ -66,5 +66,5 @@ jobs:
|
||||
with:
|
||||
name: opengrep-full-sarif
|
||||
path: .opengrep-out/precise.sarif
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
retention-days: 30
|
||||
|
||||
2
.github/workflows/opengrep-precise.yml
vendored
2
.github/workflows/opengrep-precise.yml
vendored
@@ -97,5 +97,5 @@ jobs:
|
||||
with:
|
||||
name: opengrep-pr-diff-sarif
|
||||
path: .opengrep-out/precise.sarif
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
retention-days: 30
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
name: Plugin Init Scaffold Validation
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- ".github/workflows/plugin-init-scaffold-validation.yml"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "scripts/validate-plugin-init-provider-scaffold.ts"
|
||||
- "src/cli/plugins-authoring-command.ts"
|
||||
- "src/cli/plugins-authoring-command.test.ts"
|
||||
- "src/cli/plugins-cli.ts"
|
||||
- "src/plugin-sdk/**"
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
paths:
|
||||
- ".github/workflows/plugin-init-scaffold-validation.yml"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "scripts/validate-plugin-init-provider-scaffold.ts"
|
||||
- "src/cli/plugins-authoring-command.ts"
|
||||
- "src/cli/plugins-authoring-command.test.ts"
|
||||
- "src/cli/plugins-cli.ts"
|
||||
- "src/plugin-sdk/**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
validate-provider-scaffold:
|
||||
name: Validate provider scaffold
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
|
||||
- name: Generate and validate provider scaffold
|
||||
run: pnpm test:plugins:init-provider-scaffold
|
||||
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"
|
||||
|
||||
20
.github/workflows/qa-live-transports-convex.yml
vendored
20
.github/workflows/qa-live-transports-convex.yml
vendored
@@ -226,7 +226,7 @@ jobs:
|
||||
name: qa-parity-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: .artifacts/qa-e2e/
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
run_live_runtime_token_efficiency:
|
||||
name: Run live runtime token-efficiency lane
|
||||
@@ -315,7 +315,7 @@ jobs:
|
||||
name: qa-live-runtime-token-efficiency-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ steps.run_lane.outputs.output_dir }}
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
run_live_matrix:
|
||||
name: Run Matrix live QA lane
|
||||
@@ -391,7 +391,7 @@ jobs:
|
||||
name: qa-live-matrix-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ steps.run_lane.outputs.output_dir }}
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
run_live_matrix_sharded:
|
||||
name: Run Matrix live QA lane (${{ matrix.profile }})
|
||||
@@ -475,7 +475,7 @@ jobs:
|
||||
name: qa-live-matrix-${{ matrix.profile }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ steps.run_lane.outputs.output_dir }}
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
run_live_telegram:
|
||||
name: Run Telegram live QA lane with Convex leases
|
||||
@@ -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
|
||||
@@ -570,7 +569,7 @@ jobs:
|
||||
name: qa-live-telegram-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ steps.run_lane.outputs.output_dir }}
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
run_live_discord:
|
||||
name: Run Discord live QA lane with Convex leases
|
||||
@@ -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 || '' }}
|
||||
@@ -665,7 +663,7 @@ jobs:
|
||||
name: qa-live-discord-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ steps.run_lane.outputs.output_dir }}
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
run_live_whatsapp:
|
||||
name: Run WhatsApp live QA lane with Convex leases
|
||||
@@ -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 || '' }}
|
||||
@@ -763,7 +760,7 @@ jobs:
|
||||
name: qa-live-whatsapp-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ steps.run_lane.outputs.output_dir }}
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
run_live_slack:
|
||||
name: Run Slack live QA lane with Convex leases
|
||||
@@ -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"
|
||||
@@ -859,4 +855,4 @@ jobs:
|
||||
name: qa-live-slack-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ steps.run_lane.outputs.output_dir }}
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
|
||||
385
.github/workflows/qa-profile-evidence.yml
vendored
385
.github/workflows/qa-profile-evidence.yml
vendored
@@ -1,385 +0,0 @@
|
||||
name: QA Profile Evidence
|
||||
|
||||
run-name: ${{ format('QA Profile Evidence {0} {1}', inputs.qa_profile, inputs.ref) }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: OpenClaw branch, tag, or SHA to run
|
||||
required: true
|
||||
default: main
|
||||
type: string
|
||||
expected_sha:
|
||||
description: Optional full SHA that ref must resolve to
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
qa_profile:
|
||||
description: Taxonomy QA profile id to run (for example release or all)
|
||||
required: true
|
||||
default: all
|
||||
type: string
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: OpenClaw branch, tag, or SHA to run
|
||||
required: true
|
||||
type: string
|
||||
expected_sha:
|
||||
description: Optional full SHA that ref must resolve to
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
qa_profile:
|
||||
description: Taxonomy QA profile id to run
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
OPENAI_API_KEY:
|
||||
description: OpenAI API key used by live QA profile scenarios
|
||||
required: true
|
||||
outputs:
|
||||
artifact_name:
|
||||
description: Uploaded QA profile evidence artifact name
|
||||
value: ${{ jobs.run_qa_profile.outputs.artifact_name }}
|
||||
qa_profile:
|
||||
description: Taxonomy QA profile id that produced the evidence
|
||||
value: ${{ jobs.run_qa_profile.outputs.qa_profile }}
|
||||
qa_exit_code:
|
||||
description: Exit code from the QA profile run; non-zero evidence is still uploaded
|
||||
value: ${{ jobs.run_qa_profile.outputs.qa_exit_code }}
|
||||
qa_passed:
|
||||
description: Whether the QA profile command exited successfully
|
||||
value: ${{ jobs.run_qa_profile.outputs.qa_passed }}
|
||||
target_sha:
|
||||
description: Resolved OpenClaw SHA that produced the evidence
|
||||
value: ${{ jobs.run_qa_profile.outputs.target_sha }}
|
||||
trusted_reason:
|
||||
description: Trust reason accepted before the secret-bearing QA job
|
||||
value: ${{ jobs.run_qa_profile.outputs.trusted_reason }}
|
||||
qa_evidence_path:
|
||||
description: Path to qa-evidence.json inside the uploaded artifact
|
||||
value: ${{ jobs.run_qa_profile.outputs.qa_evidence_path }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: qa-profile-evidence-${{ inputs.qa_profile }}-${{ inputs.expected_sha || inputs.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
NODE_VERSION: "24.x"
|
||||
OPENCLAW_BUILD_PRIVATE_QA: "1"
|
||||
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
|
||||
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
|
||||
OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS: "180000"
|
||||
|
||||
jobs:
|
||||
authorize_actor:
|
||||
name: Authorize workflow actor
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
outputs:
|
||||
authorized: ${{ steps.permission.outputs.authorized }}
|
||||
steps:
|
||||
- name: Require maintainer-level repository access
|
||||
id: permission
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
with:
|
||||
script: |
|
||||
// Reusable workflow jobs inherit the caller event but run as
|
||||
// github-actions[bot]; selected ref validation still gates secrets.
|
||||
if (context.actor === "github-actions[bot]") {
|
||||
core.info("Skipping manual actor permission check for a reusable workflow call.");
|
||||
core.setOutput("authorized", "true");
|
||||
return;
|
||||
}
|
||||
if (context.eventName !== "workflow_dispatch") {
|
||||
core.info(`Skipping manual actor permission check for ${context.eventName}.`);
|
||||
core.setOutput("authorized", "true");
|
||||
return;
|
||||
}
|
||||
const allowed = new Set(["admin", "maintain", "write"]);
|
||||
const { owner, repo } = context.repo;
|
||||
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner,
|
||||
repo,
|
||||
username: context.actor,
|
||||
});
|
||||
const permission = data.permission;
|
||||
core.info(`Actor ${context.actor} permission: ${permission}`);
|
||||
if (!allowed.has(permission)) {
|
||||
core.notice(
|
||||
`Workflow requires write/maintain/admin access. Actor "${context.actor}" has "${permission}".`,
|
||||
);
|
||||
core.setOutput("authorized", "false");
|
||||
return;
|
||||
}
|
||||
core.setOutput("authorized", "true");
|
||||
|
||||
validate_selected_ref:
|
||||
name: Validate selected ref
|
||||
needs: authorize_actor
|
||||
if: needs.authorize_actor.outputs.authorized == 'true'
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
outputs:
|
||||
selected_revision: ${{ steps.validate.outputs.selected_revision }}
|
||||
trusted_reason: ${{ steps.validate.outputs.trusted_reason }}
|
||||
steps:
|
||||
- name: Checkout selected ref
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ inputs.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate selected ref
|
||||
id: validate
|
||||
env:
|
||||
EXPECTED_SHA: ${{ inputs.expected_sha }}
|
||||
INPUT_REF: ${{ inputs.ref }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
selected_revision="$(git rev-parse HEAD)"
|
||||
expected_sha="${EXPECTED_SHA,,}"
|
||||
trusted_reason=""
|
||||
|
||||
if [[ -n "${expected_sha// }" && ! "$expected_sha" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "expected_sha must be a full 40-character SHA; got: ${EXPECTED_SHA}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -n "${expected_sha// }" && "${selected_revision,,}" != "$expected_sha" ]]; then
|
||||
echo "Ref '${INPUT_REF}' resolved to ${selected_revision}, expected ${EXPECTED_SHA}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
|
||||
|
||||
if git merge-base --is-ancestor "$selected_revision" refs/remotes/origin/main; then
|
||||
trusted_reason="main-ancestor"
|
||||
elif git tag --points-at "$selected_revision" | grep -Eq '^v'; then
|
||||
trusted_reason="release-tag"
|
||||
elif [[ "$INPUT_REF" =~ ^release/[0-9]{4}\.[0-9]+\.[0-9]+$ ]]; then
|
||||
git fetch --no-tags origin "+refs/heads/${INPUT_REF}:refs/remotes/origin/${INPUT_REF}"
|
||||
release_branch_sha="$(git rev-parse "refs/remotes/origin/${INPUT_REF}")"
|
||||
if [[ "$selected_revision" == "$release_branch_sha" ]]; then
|
||||
trusted_reason="release-branch-head"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$trusted_reason" ]]; then
|
||||
echo "Ref '${INPUT_REF}' resolved to $selected_revision, which is not trusted for this secret-bearing QA evidence run." >&2
|
||||
echo "Allowed refs must be on main, point to a release tag, or match a release branch head." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "selected_revision=$selected_revision" >> "$GITHUB_OUTPUT"
|
||||
echo "trusted_reason=$trusted_reason" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "### Target"
|
||||
echo
|
||||
echo "- Requested ref: \`${INPUT_REF}\`"
|
||||
echo "- Resolved SHA: \`$selected_revision\`"
|
||||
echo "- Trust reason: \`$trusted_reason\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
run_qa_profile:
|
||||
name: Generate QA profile evidence
|
||||
needs: validate_selected_ref
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
artifact_name: ${{ steps.evidence.outputs.artifact_name }}
|
||||
qa_profile: ${{ steps.profile.outputs.profile }}
|
||||
qa_exit_code: ${{ steps.evidence.outputs.qa_exit_code }}
|
||||
qa_passed: ${{ steps.evidence.outputs.qa_passed }}
|
||||
target_sha: ${{ steps.evidence.outputs.target_sha }}
|
||||
trusted_reason: ${{ steps.evidence.outputs.trusted_reason }}
|
||||
qa_evidence_path: ${{ steps.evidence.outputs.qa_evidence_path }}
|
||||
environment: qa-live-shared
|
||||
steps:
|
||||
- name: Checkout selected ref
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ needs.validate_selected_ref.outputs.selected_revision }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
install-bun: "true"
|
||||
|
||||
- name: Validate QA profile input
|
||||
id: profile
|
||||
env:
|
||||
QA_PROFILE: ${{ inputs.qa_profile }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node --import tsx --input-type=module <<'NODE'
|
||||
import fs from "node:fs";
|
||||
import { readQaScorecardTaxonomyReport } from "./extensions/qa-lab/src/scorecard-taxonomy.ts";
|
||||
|
||||
const requested = process.env.QA_PROFILE?.trim() ?? "";
|
||||
if (!/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/.test(requested)) {
|
||||
throw new Error(`qa_profile must use a taxonomy profile id, got ${JSON.stringify(process.env.QA_PROFILE)}`);
|
||||
}
|
||||
|
||||
const taxonomy = readQaScorecardTaxonomyReport([]);
|
||||
const profile = taxonomy.profiles.find((entry) => entry.id === requested);
|
||||
if (!profile) {
|
||||
const available = taxonomy.profiles.map((entry) => entry.id).join(", ");
|
||||
throw new Error(`Unknown QA profile ${requested}. Available profiles: ${available}`);
|
||||
}
|
||||
|
||||
fs.appendFileSync(process.env.GITHUB_OUTPUT, `profile=${profile.id}\n`);
|
||||
NODE
|
||||
|
||||
echo "QA profile: \`${QA_PROFILE}\`" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Build private QA runtime
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: node scripts/build-all.mjs qaRuntime
|
||||
|
||||
- name: Ensure Playwright Chromium
|
||||
run: node scripts/ensure-playwright-chromium.mjs
|
||||
|
||||
- name: Run QA profile
|
||||
id: run_profile
|
||||
env:
|
||||
QA_PROFILE: ${{ steps.profile.outputs.profile }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
output_dir=".artifacts/qa-e2e/profile-${QA_PROFILE}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
|
||||
echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
qa_exit_code=0
|
||||
pnpm openclaw qa run \
|
||||
--repo-root . \
|
||||
--qa-profile "${QA_PROFILE}" \
|
||||
--output-dir "${output_dir}" || qa_exit_code=$?
|
||||
|
||||
echo "qa_exit_code=${qa_exit_code}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate QA profile evidence
|
||||
id: evidence
|
||||
if: always()
|
||||
env:
|
||||
ARTIFACT_NAME: qa-profile-evidence-${{ steps.profile.outputs.profile }}-${{ needs.validate_selected_ref.outputs.selected_revision }}
|
||||
OUTPUT_DIR: ${{ steps.run_profile.outputs.output_dir }}
|
||||
QA_EXIT_CODE: ${{ steps.run_profile.outputs.qa_exit_code }}
|
||||
QA_PROFILE: ${{ steps.profile.outputs.profile }}
|
||||
REQUESTED_REF: ${{ inputs.ref }}
|
||||
TARGET_SHA: ${{ needs.validate_selected_ref.outputs.selected_revision }}
|
||||
TRUSTED_REASON: ${{ needs.validate_selected_ref.outputs.trusted_reason }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
node --input-type=module <<'NODE'
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const outputDir = process.env.OUTPUT_DIR;
|
||||
if (!outputDir) {
|
||||
throw new Error("OUTPUT_DIR is required");
|
||||
}
|
||||
if (!process.env.QA_EXIT_CODE) {
|
||||
throw new Error("QA_EXIT_CODE is required");
|
||||
}
|
||||
|
||||
const evidencePath = path.join(outputDir, "qa-evidence.json");
|
||||
const payload = JSON.parse(fs.readFileSync(evidencePath, "utf8"));
|
||||
if (payload.profile !== process.env.QA_PROFILE) {
|
||||
throw new Error(`qa-evidence.json profile must be ${process.env.QA_PROFILE}, got ${JSON.stringify(payload.profile)}`);
|
||||
}
|
||||
if (!payload.scorecard || !Array.isArray(payload.scorecard.categoryReports)) {
|
||||
throw new Error("QA profile qa-evidence.json must include scorecard.categoryReports");
|
||||
}
|
||||
if (payload.scorecard.categoryReports.length === 0) {
|
||||
throw new Error("QA profile qa-evidence.json scorecard has no category reports");
|
||||
}
|
||||
|
||||
const manifest = {
|
||||
artifactName: process.env.ARTIFACT_NAME,
|
||||
generatedAt: new Date().toISOString(),
|
||||
qaProfile: process.env.QA_PROFILE,
|
||||
qaExitCode: Number(process.env.QA_EXIT_CODE),
|
||||
qaPassed: process.env.QA_EXIT_CODE === "0",
|
||||
requestedRef: process.env.REQUESTED_REF,
|
||||
targetSha: process.env.TARGET_SHA,
|
||||
trustedReason: process.env.TRUSTED_REASON,
|
||||
evidenceMode: payload.evidenceMode,
|
||||
qaEvidencePath: "qa-evidence.json",
|
||||
scorecard: {
|
||||
categories: payload.scorecard.categories,
|
||||
features: payload.scorecard.features,
|
||||
categoryReports: payload.scorecard.categoryReports.length,
|
||||
},
|
||||
};
|
||||
fs.writeFileSync(
|
||||
path.join(outputDir, "qa-profile-evidence-manifest.json"),
|
||||
`${JSON.stringify(manifest, null, 2)}\n`,
|
||||
);
|
||||
NODE
|
||||
|
||||
echo "artifact_name=${ARTIFACT_NAME}" >> "$GITHUB_OUTPUT"
|
||||
echo "qa_profile=${QA_PROFILE}" >> "$GITHUB_OUTPUT"
|
||||
echo "qa_exit_code=${QA_EXIT_CODE}" >> "$GITHUB_OUTPUT"
|
||||
if [[ "$QA_EXIT_CODE" == "0" ]]; then
|
||||
echo "qa_passed=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "qa_passed=false" >> "$GITHUB_OUTPUT"
|
||||
echo "::warning::QA profile '${QA_PROFILE}' completed with exit code ${QA_EXIT_CODE}; evidence was still validated and uploaded."
|
||||
fi
|
||||
echo "target_sha=${TARGET_SHA}" >> "$GITHUB_OUTPUT"
|
||||
echo "trusted_reason=${TRUSTED_REASON}" >> "$GITHUB_OUTPUT"
|
||||
echo "qa_evidence_path=qa-evidence.json" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "### QA profile evidence"
|
||||
echo
|
||||
echo "- Artifact: \`${ARTIFACT_NAME}\`"
|
||||
echo "- QA profile: \`${QA_PROFILE}\`"
|
||||
echo "- QA exit code: \`${QA_EXIT_CODE}\`"
|
||||
echo "- Target SHA: \`${TARGET_SHA}\`"
|
||||
echo "- Evidence path: \`${OUTPUT_DIR}/qa-evidence.json\`"
|
||||
echo "- Manifest: \`${OUTPUT_DIR}/qa-profile-evidence-manifest.json\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload QA profile evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: qa-profile-evidence-${{ steps.profile.outputs.profile }}-${{ needs.validate_selected_ref.outputs.selected_revision }}
|
||||
path: ${{ steps.run_profile.outputs.output_dir }}
|
||||
retention-days: 30
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Fail if QA profile failed
|
||||
if: always()
|
||||
env:
|
||||
QA_EXIT_CODE: ${{ steps.run_profile.outputs.qa_exit_code }}
|
||||
QA_PROFILE: ${{ steps.profile.outputs.profile }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z "${QA_EXIT_CODE:-}" ]]; then
|
||||
echo "QA profile did not report an exit code." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$QA_EXIT_CODE" != "0" ]]; then
|
||||
echo "QA profile '${QA_PROFILE}' failed with exit code ${QA_EXIT_CODE}." >&2
|
||||
exit "$QA_EXIT_CODE"
|
||||
fi
|
||||
4
.github/workflows/real-behavior-proof.yml
vendored
4
.github/workflows/real-behavior-proof.yml
vendored
@@ -24,9 +24,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
with:
|
||||
# Old PR events can carry a stale base SHA that predates current
|
||||
# trusted checker scripts. Use the workflow revision instead.
|
||||
ref: ${{ github.workflow_sha }}
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
persist-credentials: false
|
||||
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
|
||||
id: app-token
|
||||
|
||||
7
.github/workflows/sandbox-common-smoke.yml
vendored
7
.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"
|
||||
@@ -57,10 +57,11 @@ jobs:
|
||||
BASE_IMAGE="openclaw-sandbox-smoke-base:bookworm-slim" \
|
||||
TARGET_IMAGE="openclaw-sandbox-common-smoke:bookworm-slim" \
|
||||
PACKAGES="ca-certificates" \
|
||||
INSTALL_PNPM=0 \
|
||||
INSTALL_BUN=0 \
|
||||
INSTALL_BREW=0 \
|
||||
FINAL_USER=sandbox \
|
||||
scripts/sandbox-common-setup.sh
|
||||
|
||||
timeout --kill-after=30s 2m docker run --rm openclaw-sandbox-common-smoke:bookworm-slim sh -lc \
|
||||
'set -e; test "$(id -un)" = sandbox; node --version; pnpm --version'
|
||||
u="$(timeout --kill-after=30s 2m docker run --rm openclaw-sandbox-common-smoke:bookworm-slim sh -lc 'id -un')"
|
||||
test "$u" = "sandbox"
|
||||
|
||||
42
.github/workflows/tui-pty.yml
vendored
Normal file
42
.github/workflows/tui-pty.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: TUI PTY
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "src/tui/**"
|
||||
- "scripts/dev/tui-pty-test-watch.ts"
|
||||
- "scripts/test-projects.test-support.mjs"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "test/scripts/test-projects.test.ts"
|
||||
- "test/vitest/vitest.test-shards.mjs"
|
||||
- "test/vitest/vitest.tui-pty.config.ts"
|
||||
- ".github/workflows/tui-pty.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
jobs:
|
||||
tui-pty:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 8
|
||||
env:
|
||||
OPENCLAW_TUI_PTY_INCLUDE_LOCAL: "1"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
|
||||
- name: Run TUI PTY tests
|
||||
run: timeout --kill-after=30s 240s node scripts/run-vitest.mjs run --config test/vitest/vitest.tui-pty.config.ts
|
||||
70
.github/workflows/windows-blacksmith-testbox.yml
vendored
70
.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
|
||||
@@ -150,7 +135,6 @@ jobs:
|
||||
git --version
|
||||
|
||||
- name: Run Testbox
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -168,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" \
|
||||
|
||||
123
.github/workflows/windows-testbox-probe.yml
vendored
123
.github/workflows/windows-testbox-probe.yml
vendored
@@ -37,11 +37,6 @@ on:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
run_windows_ci:
|
||||
description: "Run the focused Windows-native CI test shard after probing"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -85,21 +80,10 @@ 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)
|
||||
@@ -128,15 +112,9 @@ jobs:
|
||||
Write-Host "wsl.exe=$($wsl.Source)"
|
||||
if ($env:ENABLE_WSL2_FEATURES -eq "true") {
|
||||
Write-Host "enable_wsl2_features=true"
|
||||
foreach ($feature in @("Microsoft-Windows-Subsystem-Linux", "VirtualMachinePlatform", "HypervisorPlatform")) {
|
||||
foreach ($feature in @("Microsoft-Windows-Subsystem-Linux", "VirtualMachinePlatform", "HypervisorPlatform", "Microsoft-Hyper-V-All")) {
|
||||
dism.exe /online /enable-feature /featurename:$feature /all /norestart
|
||||
Write-Host "enable_feature_${feature}_exit=$LASTEXITCODE"
|
||||
if ($LASTEXITCODE -eq 3010) {
|
||||
$restartRequired = $true
|
||||
}
|
||||
}
|
||||
if ($restartRequired) {
|
||||
Write-Warning "wsl2_restart_required=true; Windows optional feature changes require a runner reboot before WSL2 can be imported."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,13 +127,12 @@ jobs:
|
||||
Write-Host "wsl_list_exit=$($list.Code)"
|
||||
|
||||
$distros = @(Get-WslDistros)
|
||||
if ($distros.Count -eq 0 -and $env:IMPORT_UBUNTU_WSL2 -eq "true" -and -not $restartRequired) {
|
||||
if ($distros.Count -eq 0 -and $env:IMPORT_UBUNTU_WSL2 -eq "true") {
|
||||
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)"
|
||||
@@ -163,16 +140,12 @@ jobs:
|
||||
Write-Host $list.Text
|
||||
Write-Host "wsl_list_after_import_exit=$($list.Code)"
|
||||
$distros = @(Get-WslDistros)
|
||||
} elseif ($distros.Count -eq 0 -and $env:IMPORT_UBUNTU_WSL2 -eq "true" -and $restartRequired) {
|
||||
Write-Warning "import_ubuntu_wsl2=skipped_restart_required"
|
||||
}
|
||||
|
||||
if ($distros.Count -gt 0) {
|
||||
$distro = $distros[0]
|
||||
Write-Host "wsl_probe_distro=$distro"
|
||||
$exec = Invoke-WslText -Arguments @("-d", $distro, "--exec", "bash", "-lc", 'set -euo pipefail; uname -a; if [ -f /etc/os-release ]; then sed -n "1,8p" /etc/os-release; fi')
|
||||
} elseif ($restartRequired) {
|
||||
$exec = [pscustomobject]@{ Code = 1; Text = "wsl_exec_skipped=restart_required" }
|
||||
} else {
|
||||
$exec = Invoke-WslText -Arguments @("--exec", "bash", "-lc", 'set -euo pipefail; uname -a; if [ -f /etc/os-release ]; then sed -n "1,8p" /etc/os-release; fi')
|
||||
}
|
||||
@@ -185,99 +158,17 @@ jobs:
|
||||
|
||||
if ($ok) {
|
||||
"wsl2_ok=true" >> $env:GITHUB_OUTPUT
|
||||
"wsl2_restart_required=false" >> $env:GITHUB_OUTPUT
|
||||
"OPENCLAW_WSL2_PROBE_OK=true" >> $env:GITHUB_ENV
|
||||
"OPENCLAW_WSL2_RESTART_REQUIRED=false" >> $env:GITHUB_ENV
|
||||
Write-Host "wsl2_ok=true"
|
||||
} else {
|
||||
"wsl2_ok=false" >> $env:GITHUB_OUTPUT
|
||||
"wsl2_restart_required=$($restartRequired.ToString().ToLowerInvariant())" >> $env:GITHUB_OUTPUT
|
||||
"OPENCLAW_WSL2_PROBE_OK=false" >> $env:GITHUB_ENV
|
||||
"OPENCLAW_WSL2_RESTART_REQUIRED=$($restartRequired.ToString().ToLowerInvariant())" >> $env:GITHUB_ENV
|
||||
Write-Warning "wsl2_ok=false"
|
||||
}
|
||||
|
||||
exit 0
|
||||
|
||||
- name: Try to exclude workspace from Windows Defender (best-effort)
|
||||
if: ${{ inputs.run_windows_ci }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
$cmd = Get-Command Add-MpPreference -ErrorAction SilentlyContinue
|
||||
if (-not $cmd) {
|
||||
Write-Host "Add-MpPreference not available, skipping Defender exclusions."
|
||||
exit 0
|
||||
}
|
||||
|
||||
try {
|
||||
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE" -ErrorAction Stop
|
||||
Add-MpPreference -ExclusionProcess "node.exe" -ErrorAction Stop
|
||||
Write-Host "Defender exclusions applied."
|
||||
} catch {
|
||||
Write-Warning "Failed to apply Defender exclusions, continuing. $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
- name: Setup Node.js
|
||||
if: ${{ inputs.run_windows_ci }}
|
||||
shell: bash
|
||||
env:
|
||||
REQUESTED_NODE_VERSION: "22.x"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source .github/actions/setup-pnpm-store-cache/ensure-node.sh
|
||||
openclaw_ensure_node "$REQUESTED_NODE_VERSION"
|
||||
|
||||
- name: Setup pnpm
|
||||
if: ${{ inputs.run_windows_ci }}
|
||||
uses: ./.github/actions/setup-pnpm-store-cache
|
||||
with:
|
||||
node-version: 22.x
|
||||
|
||||
- name: Runtime versions
|
||||
if: ${{ inputs.run_windows_ci }}
|
||||
shell: bash
|
||||
run: |
|
||||
node -v
|
||||
npm -v
|
||||
pnpm -v
|
||||
|
||||
- name: Capture node path
|
||||
if: ${{ inputs.run_windows_ci }}
|
||||
shell: bash
|
||||
run: |
|
||||
node_bin="$(dirname "$(node -p 'process.execPath')")"
|
||||
if command -v cygpath >/dev/null 2>&1; then
|
||||
node_bin="$(cygpath -u "$node_bin")"
|
||||
fi
|
||||
echo "NODE_BIN=$node_bin" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install dependencies
|
||||
if: ${{ inputs.run_windows_ci }}
|
||||
shell: bash
|
||||
env:
|
||||
CI: true
|
||||
run: |
|
||||
export PATH="$NODE_BIN:$PATH"
|
||||
which node
|
||||
node -v
|
||||
pnpm -v
|
||||
pnpm install --frozen-lockfile --prefer-offline --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true --config.side-effects-cache=true || pnpm install --frozen-lockfile --prefer-offline --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true --config.side-effects-cache=true
|
||||
|
||||
- name: Run Windows CI tests
|
||||
if: ${{ inputs.run_windows_ci }}
|
||||
shell: bash
|
||||
env:
|
||||
CI: true
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
OPENCLAW_TEST_SKIP_FULL_EXTENSIONS_SHARD: 1
|
||||
OPENCLAW_VITEST_MAX_WORKERS: 1
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export PATH="$NODE_BIN:$PATH"
|
||||
pnpm test:windows:ci
|
||||
|
||||
- name: Keep runner alive for SSH inspection
|
||||
if: ${{ always() && !cancelled() }}
|
||||
env:
|
||||
KEEPALIVE_MINUTES: ${{ inputs.keepalive_minutes }}
|
||||
run: |
|
||||
@@ -294,13 +185,9 @@ jobs:
|
||||
}
|
||||
|
||||
- name: Enforce WSL2 requirement
|
||||
if: ${{ always() && !cancelled() && inputs.require_wsl2 }}
|
||||
if: ${{ inputs.require_wsl2 }}
|
||||
run: |
|
||||
if ($env:OPENCLAW_WSL2_PROBE_OK -ne "true") {
|
||||
if ($env:OPENCLAW_WSL2_RESTART_REQUIRED -eq "true") {
|
||||
Write-Error "WSL2 probe enabled required Windows features, but the runner needs a reboot before WSL2 can start."
|
||||
exit 1
|
||||
}
|
||||
Write-Error "WSL2 probe failed or WSL2 is unavailable on this Windows runner."
|
||||
exit 1
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
15
AGENTS.md
15
AGENTS.md
@@ -8,7 +8,6 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- Repo: `https://github.com/openclaw/openclaw`
|
||||
- Replies: repo-root refs only: `extensions/telegram/src/index.ts:80`. No absolute paths, no `~/`.
|
||||
- Docs/user-visible work: `pnpm docs:list`, then read relevant docs only.
|
||||
- Existing-solutions preflight: before proposing or building a custom system, feature, workflow, tool, integration, or automation, do a lightweight check for open-source projects, maintained libraries, existing OpenClaw plugins, or free platforms that already solve it well enough. Prefer those when adequate. Build custom only when existing options are unsuitable, too expensive, unmaintained, unsafe, non-compliant, or the user explicitly asks for custom. Avoid paid-service recommendations unless the user explicitly approves spend. Keep this to a brief preflight gate, not a broad research assignment.
|
||||
- Fix/triage answers need source, tests, current/shipped behavior, and dependency contract proof.
|
||||
- Reviews/answers: high confidence required. Default to exhaustive relevant codebase search/read, including owners, callers, siblings, tests, docs, and upstream/dependency contracts before verdict. Diff-only review is insufficient.
|
||||
- Review default: read the whole changed function/module plus callers, callees, sibling implementations, adjacent tests, scoped docs, and dependency/Codex contracts before saying `good`, `bad`, `best fix`, `proof sufficient`, or posting a comment. If challenged, keep reading first; do not defend the earlier verdict until the missing path is checked.
|
||||
@@ -36,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.
|
||||
|
||||
@@ -118,11 +117,11 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- Tests in a normal source checkout: `pnpm test <path-or-filter> [vitest args...]`, `pnpm test:changed`, `pnpm test:serial`, `pnpm test:coverage`; never raw `vitest`.
|
||||
- If raw Vitest is unavoidable, use `vitest run ...`; bare `vitest ...` starts local watch mode and will not exit on its own.
|
||||
- Tests in a Codex worktree or linked/sparse checkout: avoid direct local `pnpm test*`; use `node scripts/run-vitest.mjs <path-or-filter>` for tiny explicit-file proof, or Crabbox/Testbox for anything broader.
|
||||
- Checks/lint in a normal source checkout: `pnpm check:changed` delegates to Crabbox/Testbox; lanes: `pnpm changed:lanes --json`; staged/path-scoped: `pnpm check:changed --staged` or `pnpm check:changed -- <files...>`; full `pnpm check`/`pnpm lint` only when required.
|
||||
- Checks in a normal source checkout: `pnpm check:changed` delegates to Crabbox/Testbox; lanes: `pnpm changed:lanes --json`; staged: `pnpm check:changed --staged`; full: `pnpm check`.
|
||||
- Checks in a Codex worktree or linked/sparse checkout: avoid direct local `pnpm check*`; use `node scripts/crabbox-wrapper.mjs run ... -- env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 corepack pnpm check:changed` so pnpm runs inside Testbox, not locally.
|
||||
- Extension tests: `pnpm test:extensions`, `pnpm test extensions`, `pnpm test extensions/<id>`.
|
||||
- Typecheck: `tsgo` lanes only (`pnpm tsgo*`, `pnpm check:test-types`); never add `tsc --noEmit`, `typecheck`, `check:types`.
|
||||
- Formatting: `oxfmt`, not Prettier. Use repo wrappers (`pnpm format:*`, `scripts/run-oxlint.mjs`; full `pnpm lint:*` only when scope requires).
|
||||
- Formatting: `oxfmt`, not Prettier. Use repo wrappers (`pnpm format:*`, `pnpm lint:*`, `scripts/run-oxlint.mjs`).
|
||||
- Build before push when build output, packaging, lazy/module boundaries, dynamic imports, or published surfaces can change.
|
||||
|
||||
## Validation
|
||||
@@ -143,9 +142,6 @@ Skills own workflows; root owns hard policy and routing.
|
||||
|
||||
## GitHub / PRs
|
||||
|
||||
- Fresh GitHub items: read `CONTRIBUTING.md`, the issue chooser/form, PR template, and `.github/CODEOWNERS`; blank issues are disabled; preserve templates and evidence requirements.
|
||||
- Agent-authored/non-trivial work: create or reuse the issue first; tiny fixes may go direct. PRs use the template, link context, and keep durable problem/impact/evidence sections.
|
||||
- Route support to Discord and security through `SECURITY.md`. Use listed maintainer areas/`CODEOWNERS`; never guess mentions.
|
||||
- Use `$openclaw-pr-maintainer` immediately for maintainer-side OpenClaw issue/PR review, triage, duplicates, labels, comments, close, land, or evidence. Contributor PR creation/refresh follows the requested contributor workflow; linked refs alone do not require maintainer archive tooling.
|
||||
- Issue/PR start: `git status -sb`; if clean, `git pull --ff-only`; if dirty, yell before pull/rebase.
|
||||
- PR refs: `gh pr view/diff` or `gh api`, not web search. Prefer `gitcrawl` for maintainer discovery; missing/stale `gitcrawl` falls through to live `gh`, not contributor setup. Verify live with `gh` before mutation.
|
||||
@@ -169,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
|
||||
|
||||
3258
CHANGELOG.md
3258
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -97,23 +97,6 @@ Welcome to the lobster tank! 🦞
|
||||
4. **Test/CI-only PRs for known `main` failures** → Don't open a PR. The Maintainer team is already tracking those failures, and PRs that only tweak tests or CI to chase them will be closed unless they are required to validate a new fix.
|
||||
5. **Questions** → Discord [#help](https://discord.com/channels/1456350064065904867/1459642797895319552) / [#users-helping-users](https://discord.com/channels/1456350064065904867/1459007081603403828)
|
||||
|
||||
## Issue, PR, and Contact Routing
|
||||
|
||||
Start from this routing map before creating GitHub items:
|
||||
|
||||
| Situation | Use | Required evidence |
|
||||
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| Product bug, regression, crash, or behavior defect | [Bug report](https://github.com/openclaw/openclaw/issues/new?template=bug_report.yml) | Repro steps, expected vs actual behavior, version, OS, model/provider route when relevant, logs/screenshots, impact |
|
||||
| Documentation bug or missing/contradictory docs | [Docs bug report](https://github.com/openclaw/openclaw/issues/new?template=docs_bug_report.yml) | Affected docs path or URL, verification steps, expected docs content, actual docs content, impact, evidence |
|
||||
| New feature, architecture change, or product improvement | [Feature request](https://github.com/openclaw/openclaw/issues/new?template=feature_request.yml) or Discord first | Problem, proposed solution, alternatives, impact, examples or prior art |
|
||||
| Onboarding, setup help, or general support question | Discord [#help](https://discord.com/channels/1456350064065904867/1459642797895319552) / [#users-helping-users](https://discord.com/channels/1456350064065904867/1459007081603403828) | Do not open a GitHub issue unless there is a concrete product defect or docs gap |
|
||||
| Security vulnerability | See [Report a Vulnerability](#report-a-vulnerability) below | Do not file public issues for private security reports |
|
||||
| PR for an existing or newly filed issue | Use the [PR template](.github/pull_request_template.md) | Visible `Closes #<issue>` or `Related: #<issue>`, problem, shipped solution, user impact, validation evidence |
|
||||
|
||||
For agent-authored or otherwise non-trivial work, create or reuse the issue first, then open the PR against it. Bugs and very small fixes may go straight to PR, but still link existing context when it exists and fill out the PR template.
|
||||
|
||||
Do not guess who to tag. Let issue forms, labels/automation, `.github/CODEOWNERS`, and the maintainer areas above route the work. Mention a maintainer only when their listed area or owned path is directly relevant and you need a decision; otherwise rely on normal review. For coordinated change sets, ask in **#clawtributors** before opening more than the PR limit.
|
||||
|
||||
## PR Limits
|
||||
|
||||
We cap at **20 open PRs per author**. If you exceed this, the `r: too-many-prs` label is added and your PR is auto-closed. This is a hard limit.
|
||||
@@ -123,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`
|
||||
@@ -187,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
|
||||
|
||||
@@ -304,9 +304,6 @@ by Peter Steinberger and the community.
|
||||
## Community
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines, maintainers, and how to submit PRs.
|
||||
Use the [issue chooser](https://github.com/openclaw/openclaw/issues/new/choose) for bugs, docs bugs, and feature requests;
|
||||
ask setup/support questions in [Discord](https://discord.gg/clawd); and report vulnerabilities through [SECURITY.md](SECURITY.md).
|
||||
PRs should link the relevant issue when possible and follow the [PR template](.github/pull_request_template.md) with problem, impact, and evidence.
|
||||
AI/vibe-coded PRs welcome! 🤖
|
||||
|
||||
Special thanks to [Mario Zechner](https://mariozechner.at/) for his support and for
|
||||
|
||||
@@ -61,7 +61,7 @@ We prioritize secure defaults, but also expose clear knobs for trusted high-powe
|
||||
## Plugins & Memory
|
||||
|
||||
OpenClaw has an extensive plugin API.
|
||||
Core stays lean; optional capabilities should usually ship as plugins.
|
||||
Core stays lean; optional capability should usually ship as plugins.
|
||||
We are generally slimming down core while expanding what plugins can do.
|
||||
If a useful feature cannot be built as a plugin yet, we welcome PRs and design discussions that extend the plugin API instead of adding one-off core behavior.
|
||||
|
||||
|
||||
174
appcast.xml
174
appcast.xml
@@ -2,53 +2,6 @@
|
||||
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
||||
<channel>
|
||||
<title>OpenClaw</title>
|
||||
<item>
|
||||
<title>2026.6.10</title>
|
||||
<pubDate>Fri, 26 Jun 2026 23:37:36 +0000</pubDate>
|
||||
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
|
||||
<sparkle:version>2606001090</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.6.10</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>OpenClaw 2026.6.10</h2>
|
||||
<h3>Highlights</h3>
|
||||
<ul>
|
||||
<li><strong>Automatic fast mode for talks:</strong> OpenClaw can enable fast mode for short conversational turns, then return to normal mode for longer runs with bounded fallback and delivery behavior. (#85104) Thanks @alexph-dev and @vincentkoc.</li>
|
||||
<li><strong>More reliable model routing:</strong> Zai model synthesis, GLM overload failover, and native reasoning-level selection now follow the active model catalog more consistently. (#94461, #93241, #94067, #94136) Thanks @Pandah97, @chrysb, @0xghost42, @zhengli0922, @openperf, @civiltox, and @BorClaw.</li>
|
||||
<li><strong>Safer session and channel state:</strong> channel switches reset stale origin fields, and cron delivery awareness stays attached to the target session. (#95328, #93580) Thanks @ZengWen-DT, @jalehman, @gorkem2020, and @scotthuang.</li>
|
||||
<li><strong>Trusted policies survive hook composition:</strong> composed hook registries keep the trusted tool policies required by approval-sensitive flows. (#94545) Thanks @jesse-merhi.</li>
|
||||
</ul>
|
||||
<h3>Changes</h3>
|
||||
<ul>
|
||||
<li><strong>Agent and channel runtime:</strong> fast-mode state now survives retries, fallback transitions, progress events, and embedded/CLI/ACP normalization; session and channel routing retain the current target and delivery context. (#85104, #93580, #95328) Thanks @alexph-dev, @vincentkoc, @scotthuang, @ZengWen-DT, @jalehman, and @gorkem2020.</li>
|
||||
<li><strong>Provider behavior:</strong> model catalogs now supply the correct Zai base URL, overload classification, and native reasoning controls for live-discovered models. (#94461, #93241, #94067, #94136) Thanks @Pandah97, @chrysb, @0xghost42, @zhengli0922, @openperf, @civiltox, and @BorClaw.</li>
|
||||
</ul>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li><strong>Fast-mode and policy correctness:</strong> fallback cutoffs and reset notices are bounded, repeated progress events remain visible, Codex service-tier state is normalized, and trusted policies are not lost when hook registries are composed. (#85104, #94545) Thanks @alexph-dev, @vincentkoc, and @jesse-merhi.</li>
|
||||
<li><strong>Model and delivery edge cases:</strong> Zai and GLM failover paths use the right runtime metadata, while stale channel-origin state no longer leaks across session changes. (#94461, #93241, #95328) Thanks @Pandah97, @chrysb, @0xghost42, @zhengli0922, @ZengWen-DT, @jalehman, and @gorkem2020.</li>
|
||||
<li><strong>Provider plugin onboarding:</strong> setup refreshes provider plugin registry metadata after installing setup-selected provider plugins, so auth continuation uses the newly installed provider instead of stale registry state. (#95792) Thanks @snowzlmbot.</li>
|
||||
</ul>
|
||||
<h3>Complete contribution record</h3>
|
||||
This audited record covers the complete v2026.6.9..HEAD history: 12 merged PRs. The generation manifest also supplies direct commits as editorial input; the grouped notes above prioritize user impact.
|
||||
<h4>Pull requests</h4>
|
||||
<ul>
|
||||
<li><strong>PR #86627</strong> Keep core doctor health in contribution order. Thanks @giodl73-repo.</li>
|
||||
<li><strong>PR #93580</strong> fix: preserve cron delivery awareness for target sessions. Thanks @scotthuang and @jalehman.</li>
|
||||
<li><strong>PR #95030</strong> refactor: add SDK transcript identity target API. Thanks @jalehman.</li>
|
||||
<li><strong>PR #94838</strong> refactor(copilot): complete harness lifecycle parity. Thanks @vincentkoc.</li>
|
||||
<li><strong>PR #95328</strong> fix(sessions): reset stale per-channel origin fields on channel switch. Related #95325. Thanks @ZengWen-DT and @jalehman and @gorkem2020.</li>
|
||||
<li><strong>PR #94461</strong> fix(zai): fall back to manifest baseUrl for synthesized GLM-5 models. Related #94269. Thanks @Pandah97 and @chrysb.</li>
|
||||
<li><strong>PR #93241</strong> fix(agents): classify Zhipu GLM overload as overloaded for failover. Related #93211. Thanks @0xghost42 and @zhengli0922.</li>
|
||||
<li><strong>PR #94067</strong> fix(channels): resolve native /think menu levels via runtime catalog for live-discovered models. Related #93835. Thanks @openperf and @civiltox.</li>
|
||||
<li><strong>PR #94136</strong> fix(zai): expose GLM-5.2 reasoning levels [AI-assisted]. Thanks @BorClaw.</li>
|
||||
<li><strong>PR #85104</strong> feat: fast talks auto mode. Related #85087. Thanks @alexph-dev.</li>
|
||||
<li><strong>PR #94545</strong> fix: keep trusted policies with hook registry. Thanks @jesse-merhi.</li>
|
||||
<li><strong>PR #95792</strong> fix(onboard): refresh provider plugin registry after setup installs. Related #95765. Thanks @snowzlmbot.</li>
|
||||
</ul>
|
||||
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.6.10/OpenClaw-2026.6.10.zip" length="56115790" type="application/octet-stream" sparkle:edSignature="MEeGG8+WePhUg9uDShznmdhhAgy/WWe7bAwr4XRTauNdrM441iziQYIlwhfNrtHDHX+uE1/tkRtIMcELfuekAg=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.6.8</title>
|
||||
<pubDate>Tue, 16 Jun 2026 17:17:20 +0000</pubDate>
|
||||
@@ -171,5 +124,132 @@ This audited record covers the complete v2026.6.9..HEAD history: 12 merged PRs.
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.6.5/OpenClaw-2026.6.5.zip" length="55725877" type="application/octet-stream" sparkle:edSignature="EKr7gCfpEVStis9HSADJk1CWYbmH2MHMqSgNfZvLbBFCBWmk3pjBJS6K2qkxkq5lIbTj4H+Lo7Iri6ip/xTGDA=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.6.1</title>
|
||||
<pubDate>Wed, 03 Jun 2026 21:26:22 +0000</pubDate>
|
||||
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
|
||||
<sparkle:version>2026060190</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.6.1</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>OpenClaw 2026.6.1</h2>
|
||||
<h3>Highlights</h3>
|
||||
<ul>
|
||||
<li>Agents and CLI-backed runtimes recover more cleanly from interrupted tool calls, stale session bindings, compaction handoffs, and media delivery retries. (#88129, #88136, #88141, #88162, #88182)</li>
|
||||
<li>Channels and mobile delivery are steadier across Telegram, WhatsApp, iMessage, Slack, Discord, Microsoft Teams, Google Chat, Google Meet, and iOS realtime Talk. (#88096, #88105, #88183, #88231)</li>
|
||||
<li>Provider and plugin requests now bound more timers, retries, OAuth/device-code lifetimes, media downloads, local service probes, and generated-content polling paths before they can hang a run.</li>
|
||||
<li>Skills, session metadata, gateway runtime state, plugin metadata, memory watchers, and store writes do less repeated work on hot paths while keeping config, dispatch, and Linux file-watch behavior stable. (#89185, #89188, #85351) Thanks @RomneyDa and @NianJiuZst.</li>
|
||||
<li>Skills and plugin loading now handle stale disabled snapshots and loader failures more clearly, so channel turns avoid disabled SecretRefs and operators get better recovery guidance. (#79072, #79173) Thanks @zeus1959.</li>
|
||||
<li>Workboard, SecretRef plugin manifests, hosted iOS push relay, and external Copilot/Tokenjuice packaging add broader orchestration, integration, and plugin delivery surfaces. (#82326, #87469, #87796, #88107, #88117)</li>
|
||||
<li>Skill Workshop now has a fuller Control UI flow with proposal lists, today actions, revision handoff, searchable file previews, review states, locale coverage, and reusable session routing.</li>
|
||||
<li>Chat and Control UI startup paths keep sends alive through history loading, stream deltas incrementally, skip markdown work while streaming, keep drafts local while typing, clear the composer after sends, trace first-output latency, prioritize first connect, and expose calmer composer controls. (#88772, #88825, #88998, #89030, #89106) Thanks @vincentkoc and @sallyom.</li>
|
||||
<li>Provider coverage and model metadata now include MiniMax M3, account OAuth endpoints, Google/Vertex catalog fixes, OpenRouter SQLite model caching, Copilot Claude 1M capabilities, Foundry reasoning alignment, and OpenAI response replay guards. (#88480, #88512, #88851, #88860)</li>
|
||||
<li>iMessage monitor state, inbound queues, and plugin install ledgers moved toward SQLite-backed state so restarts and local monitors recover with less duplicate filesystem scanning. (#88794, #88797)</li>
|
||||
<li>Release, CI, Docker, E2E, plugin install, and diagnostics lanes now cap more logs, response bodies, readiness probes, artifact checks, status polling, child workflow waits, docker package cleanup, quiet test stalls, and rollback snapshots so failures report bounded proof instead of stalling. (#88966) Thanks @RomneyDa.</li>
|
||||
</ul>
|
||||
<h3>Changes</h3>
|
||||
<ul>
|
||||
<li>Docs: add a dedicated Skill Workshop guide covering governed skill creation, reviewable proposals, CLI, Gateway, agent tool behavior, approval policy, support files, and recovery, and refresh the ClawHub showcase cards. (#88734) Thanks @shakkernerd and @vyctorbrzezowski.</li>
|
||||
<li>Skills: let the <code>skill_workshop</code> agent tool apply, reject, and quarantine explicit proposals through the guarded review flow. Thanks @shakkernerd.</li>
|
||||
<li>Skills: let proposals carry approved support files under standard skill folders, with scanner, hash, and rollback safeguards. Thanks @shakkernerd.</li>
|
||||
<li>Skills: let pending proposals be revised in place with versioned, dated proposal frontmatter before approval. Thanks @shakkernerd.</li>
|
||||
<li>Skills: add Skill Workshop with pending proposals, CLI/Gateway review actions, rollback metadata, and the <code>skill_workshop</code> agent tool. Thanks @shakkernerd.</li>
|
||||
<li>Skill Workshop: add the Control UI navigation, styled dashboard, proposal today view, revision dialog, file preview modal, searchable preview files, reusable session handoff, and localized strings.</li>
|
||||
<li>Plugins: externalize Tokenjuice as the official <code>@openclaw/tokenjuice</code> plugin with npm and ClawHub publish metadata.</li>
|
||||
<li>Plugins: externalize the GitHub Copilot agent runtime as the official <code>@openclaw/copilot</code> plugin with npm and ClawHub publish metadata.</li>
|
||||
<li>iOS: add hosted push relay defaults, realtime Talk playback, and a guarded WebSocket ping path for more reliable mobile sessions. (#88096, #88105, #88231)</li>
|
||||
<li>iOS: support native iPad display layouts.</li>
|
||||
<li>Workboard: add orchestration primitives and agent coordination tools for multi-agent planning and run tracking. (#87469)</li>
|
||||
<li>Workboard: wire task-backed board runs and show task comments in the edit modal.</li>
|
||||
<li>Code mode: add internal namespaces for scoped agent/global sessions and exact namespace tool dispatch. (#88043)</li>
|
||||
<li>Code mode: add MCP API files and docs for code-mode integrations.</li>
|
||||
<li>Control UI: add a Dreaming-tab agent selector and propagate the selected agent through Dreaming status, diary, and diary actions. (#78748) Thanks @stevenepalmer.</li>
|
||||
<li>Control UI: add calmer chat composer controls, local draft typing state, and first-output latency instrumentation for active chat entry. (#88772, #88998) Thanks @vincentkoc.</li>
|
||||
<li>Plugins: add a SecretRef provider integration manifest contract and extract shared LLM core packages for provider/plugin reuse. (#82326, #88117)</li>
|
||||
<li>Plugins: persist the plugin install index in SQLite so installed package lookup survives reloads with less filesystem scanning. (#88794)</li>
|
||||
<li>Providers: add MiniMax M3 model support. (#88860)</li>
|
||||
<li>Doctor: add disk space health checks and stabilize post-upgrade JSON probes.</li>
|
||||
<li>Channels: store inbound queues in SQLite and migrate iMessage monitor state to SQLite-backed tracking. (#88797)</li>
|
||||
<li>Skills: add the core skills index and centralize skills runtime loading, status, filtering, and prompt formatting.</li>
|
||||
</ul>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>Release/CI/E2E: fail early when Crabbox sparse-sync full checkouts do not have enough local disk, with guidance for moving the sync root.</li>
|
||||
<li>Build: render independent CLI startup metadata help snapshots concurrently to cut cold build-all metadata time.</li>
|
||||
<li>Plugins: stop timed-out package-boundary prep steps by process group so descendant TypeScript/helper processes do not survive local check cleanup.</li>
|
||||
<li>Control UI: serve static assets asynchronously after safe-open checks so large UI files do not block Gateway request handling.</li>
|
||||
<li>Scripts/UI: forward direct wrapper SIGHUP shutdown to child processes so terminal hangups do not leave wrapped dev commands running.</li>
|
||||
<li>Gateway: return the post-expiration pending-work revision from node drains so reconnecting nodes do not observe stale queue revisions after expired items are pruned.</li>
|
||||
<li>Release/CI/E2E: keep temporary full-sync checkouts alive while slow Crabbox leases boot, so sparse worktree runs do not lose their sync source before file-list generation.</li>
|
||||
<li>Release/CI/E2E: normalize inherited Linux <code>C.UTF-8</code> locale settings before raw AWS macOS Crabbox bootstrap commands, avoiding macOS locale warnings during package-manager hydration.</li>
|
||||
<li>Release/CI/E2E: keep gateway watch regression checks from copying large static plugin assets inside the measured idle window.</li>
|
||||
<li>Update: keep core updates nonblocking when a missing external plugin repair download stalls, while still blocking installed active plugin payload smoke failures.</li>
|
||||
<li>Agents/providers: keep streaming tool-call argument parsing record-shaped when providers emit valid non-object JSON such as <code>null</code> or arrays.</li>
|
||||
<li>Release/CI/E2E: reset incremental log readers when watched log files rotate without shrinking, so same-size replacements do not hide new readiness or RPC lines.</li>
|
||||
<li>Talk: preserve explicit <code>null</code> payloads on controller-created turn and output-audio lifecycle events.</li>
|
||||
<li>Agents/TUI: keep local custom provider runs from loading plugin runtime and auth alias metadata when plugins are disabled.</li>
|
||||
<li>Agents/TUI: restore in-flight TUI run switch-back behavior, keep no-policy native hook fallback available, guard vanished workspaces, and keep lightweight isolated subagents lightweight.</li>
|
||||
<li>Agents/media: keep async image, music, and video generation starts from ending the Codex turn, so mixed requests can continue with summaries or other work while media renders in the background.</li>
|
||||
<li>Agents/Codex: keep public OpenAI API-key profiles from being treated as native Codex app-server auth while preserving persisted Codex OAuth sessions.</li>
|
||||
<li>Agents/Codex: stream Codex app-server final-answer partials to live reply previews, preserve ACP metadata in SQLite, prefer real tool results over synthetic repair output, prevent aborted app-server turn handles from lingering, migrate legacy OpenAI Codex <code>lastGood</code> auth state, and preserve workspace/session metadata through ACP runtime refactors. (#88405, #88724, #88730) Thanks @vincentkoc.</li>
|
||||
<li>Control UI: keep collapsed tool cards labeled with the tool name and action instead of generic output text. Thanks @shakkernerd.</li>
|
||||
<li>Agents/Codex: surface Skill Workshop guidance in Codex app-server prompts when <code>skill_workshop</code> is available. Thanks @shakkernerd.</li>
|
||||
<li>Skill Workshop: restore and localize the Control UI board/today view switcher so review workflows keep their intended layout toggle across locales. Thanks @shakkernerd.</li>
|
||||
<li>Agents/auth: write auth profiles atomically, dispatch auth failures by type, add force re-login recovery, preserve workspaces during state-only uninstall, and compact before oversized turns so recovery paths avoid partial state. (#89181) Thanks @RomneyDa.</li>
|
||||
<li>Skills: skip disabled skill env overrides from stale persisted snapshots so disabled skill <code>apiKey</code> SecretRefs cannot abort embedded or channel turns. (#79072, #79173) Thanks @zeus1959.</li>
|
||||
<li>Skill Workshop: render the Control UI tab from filtered navigation state and keep filtered fallback routing stable.</li>
|
||||
<li>CLI: avoid live catalog validation during <code>openclaw agents add</code>, so adding a secondary agent no longer depends on provider catalog availability. (#76284, #88314) Thanks @zhangguiping-xydt.</li>
|
||||
<li>CLI: keep <code>plugins list --json</code> on the snapshot-only path so plugin sweeps avoid loading the full runtime status graph.</li>
|
||||
<li>CLI/desktop: bridge WSL clipboard operations through the shell, recognize manual-update launchd jobs, and keep machine-readable startup output parseable during progress setup. (#88764, #88689) Thanks @alexzhu0.</li>
|
||||
<li>Plugins: make PixVerse external-plugin ClawHub metadata explicit and keep it out of bundled dist builds.</li>
|
||||
<li>Plugins: clarify plugin loader failure guidance so missing or incompatible plugin packages point operators at the right repair path.</li>
|
||||
<li>Plugins: preserve npm plugin roots after blocked installs, skip plugin-local <code>openclaw</code> peer symlinks during rollback snapshots, relink those peers after restore, isolate cached tool runtime siblings, and isolate web-provider factory failures so one bad plugin does not poison sibling runtime paths. (#77237, #88807)</li>
|
||||
<li>Cron: keep SQLite cron migrations compatible with legacy run-log tables, archived job stores, diagnostic cron names, and legacy one-shot delete-after-run behavior. (#88285)</li>
|
||||
<li>Cron: keep update delivery validation scoped, harden restart state, and retire MCP runtimes on isolated cron cleanup.</li>
|
||||
<li>Memory: serialize QMD update/embed writes per store, reduce Linux watcher fan-out, retry transient FileProvider-backed reads, preserve phase signals on read errors, harden envelope metadata sanitization, reattach Linux native watchers when directories are recreated, and rewrite generated transcript paths on rollover so memory/search state survives concurrent gateway and CLI activity. (#66339, #85931, #89185, #89188, #85351) Thanks @openperf, @amittell, @RomneyDa, and @NianJiuZst.</li>
|
||||
<li>Memory: keep vector-disabled FTS indexes from resolving embedding providers during sync and search.</li>
|
||||
<li>Providers: bound generated media downloads from OpenAI, Runway, xAI, MiniMax, BytePlus, DashScope-compatible, FAL, OpenRouter, Google, Vydra, and Comfy providers.</li>
|
||||
<li>Providers: resolve Google defaults to <code>google-generative-ai</code>, register Vertex static catalog rows, align Foundry reasoning metadata, skip DeepSeek V4 thinking params on Foundry fallback, use MiniMax account OAuth endpoints, preserve Copilot Claude 1M capabilities, suppress disabled Ollama reasoning output, forward Gemini stop sequences, strip Kimi-incompatible Anthropic cache markers, keep OpenAI stop-finished tool calls, and avoid replay ids when the Responses store is disabled. (#88480, #88512, #76612) Thanks @coder999999999, @BryanTegomoh, and @vliuyt.</li>
|
||||
<li>Providers: cap GitHub Copilot OAuth request timeouts before creating abort signals.</li>
|
||||
<li>Cron: retry recurring jobs after transient model rate limits before waiting for the next scheduled slot.</li>
|
||||
<li>Agents/Codex: keep live session locks during cleanup, recover interrupted CLI tool transcripts, preserve Codex auth and compaction session identity, clear orphan tool state, cap app-server idle timers, and keep media completion delivery retryable. (#88129, #88136, #88141, #88162, #88182)</li>
|
||||
<li>Chat/UI: show Gateway chat failures as visible assistant messages in the Control UI instead of only setting an invisible error state.</li>
|
||||
<li>Channels: cap Telegram, Discord, WhatsApp, Signal, Feishu, Google Chat, Microsoft Teams, QQBot, Nostr, Zalo, Zalouser, and Nextcloud-style request/retry timers; preserve SMS approval reply routes; and retry WhatsApp QR login 408 timeouts. (#88183)</li>
|
||||
<li>Security/config parsing: reject unsafe OAuth/token lifetimes, retry-after delays, inbound timestamps, response body sizes, command timeout config, sandbox observer token TTLs, and gateway WebSocket calls after close.</li>
|
||||
<li>Providers/media: cap local service, model, usage, queue, generated media, TTS, music, workflow polling, and provider OAuth request timers across hosted and local providers.</li>
|
||||
<li>Release/CI/E2E: bound release candidate reads, beta smoke REST calls, plugin npm verification commands, changelog restore, cross-OS process groups, kitchen-sink and bundled plugin readiness probes, secret-provider probes, Telegram credential timeouts, Control UI i18n and CLI startup metadata generation, Vitest routing, dependency guard admin approvals, child workflow failure detection, quiet Node test shard stalls, docker package cleanup, and mainline test flakes. (#88127, #88137, #88155, #88160, #88966) Thanks @RomneyDa.</li>
|
||||
<li>Release/CI/E2E: keep Kitchen Sink live plugin MCP probes resolving source-checkout workspace packages and align the live gauntlet with current Kitchen Sink diagnostics.</li>
|
||||
<li>Release/CI/E2E: run the secret-provider integration proof through the repo pnpm runner so native macOS and Windows validation use the hydrated package-manager shim.</li>
|
||||
<li>Release/CI/E2E: run the Telegram desktop proof gateway through the repo pnpm runner so native macOS proof uses the hydrated package-manager shim.</li>
|
||||
<li>Docs/CI: run Mintlify anchor checks through the repo pnpm runner so docs link validation works when pnpm is only available through the hydrated package-manager shim.</li>
|
||||
<li>Agents: keep configured fallback model metadata typed so provider params, context-token caps, and media input limits do not break changed-gate typechecks.</li>
|
||||
<li>Agents: accept hidden <code>sessions_send</code> body aliases before validation while keeping the model-facing <code>message</code> schema canonical. (#88229) Thanks @zhangguiping-xydt.</li>
|
||||
<li>Chat/UI: preserve startup chat sends during history loading, unblock the initial Control UI chat send, stream chat deltas incrementally, skip markdown parsing while streaming, keep drafts local while typing, guard composer rerenders, honor Chromium executable overrides, and detect system Chromium for E2E. (#88998) Thanks @vincentkoc.</li>
|
||||
<li>Channels: stop schema-padded poll modifiers from turning normal <code>send</code> actions into invalid poll sends. (#89601) Thanks @codezz.</li>
|
||||
<li>Channels: preserve long Feishu streaming replies, send visible fallbacks when accepted Feishu turns produce no final reply, tolerate iMessage self-chat timestamp skew, preserve colon-prefixed slash commands in mention parsing, decode Nostr <code>npub</code> allowlists correctly, and suppress raw provider errors during channel delivery. (#87896)</li>
|
||||
<li>Config/status/doctor: skip unresolved shell references in state-dir dotenv files, resolve gateway auth secrets during deep status audits, respect explicit PI runtime policy, report runtime tool-schema errors, and keep post-upgrade JSON stable. (#88288)</li>
|
||||
<li>Gateway/session state: list commands from the Gateway plugin registry, harden MCP loopback tool schemas, hide phantom agent-store rows from <code>sessions.list</code>, make task persistence failures explicit, and carry session UUIDs on interactive dispatch events.</li>
|
||||
<li>Gateway/plugins: narrow plugin lookup memoization to the stable plugin/runtime inputs, avoiding repeated lookup work without mixing disabled or filtered plugin state.</li>
|
||||
<li>OpenAI/TTS: handle speed directives for OpenAI TTS voices. (#74089)</li>
|
||||
<li>CI/Crabbox: keep default runner capacity on the Azure credit-backed on-demand D4 lane with the Azure SSH port and a Git-independent full check job, so broad validation avoids low-priority spot quota stalls, hydrate port mismatches, non-Git hydrated workspaces, and stale AWS region hints.</li>
|
||||
<li>CI/Crabbox: route Crabbox wrapper and Testbox workflow edits to their regression tests so changed-test gates do not silently run zero specs.</li>
|
||||
<li>CI/workflows: route workflow sanity helper edits to their guard tests and cover composite-action input interpolation checks.</li>
|
||||
<li>CI/tooling: route CI scope, dependency, changelog, and docs helper edits to their owner tests instead of silently skipping changed-test coverage.</li>
|
||||
<li>CI/tooling: route package, release, and install helper edits to their owner tests so changed-test gates cover publish and installer script changes.</li>
|
||||
<li>CI/tooling: route shared script library edits through their owner tests so lock, process, safety, and scan helpers do not skip changed-test coverage.</li>
|
||||
<li>CI/tooling: skip expensive import-graph scans once a changed diff already requires broad fallback, keeping local changed-test planning fast while still collecting explicit owner tests.</li>
|
||||
<li>CI/tooling: route script edits through conventional owner tests when matching <code>test/scripts</code> or <code>src/scripts</code> coverage already exists.</li>
|
||||
<li>CI/tooling: honor option terminators in the memory FD repro script so follow-on arguments are not reparsed.</li>
|
||||
<li>Release/CI/E2E: assert plugin lifecycle runtime inspect output instead of only capturing it.</li>
|
||||
<li>Release/CI/E2E: make gateway-network prove the advertised health RPC and retry early WebSocket closes without burning full open timeouts.</li>
|
||||
<li>Release/CI/E2E: honor option terminators across release, Parallels smoke, plugin gauntlet, and extension-memory scripts.</li>
|
||||
<li>Release/CI/E2E: fail plugin gateway gauntlet QA chunks when the requested suite summary is missing or invalid.</li>
|
||||
<li>Performance: prebuild QA runtime probes with generated plugin assets but without CLI startup metadata.</li>
|
||||
<li>Performance: skip declaration bundling for runtime-only CLI startup and gateway watch build profiles.</li>
|
||||
<li>Performance: reuse prepared provider handles, strict tool schemas, gateway runtime metadata, session maintenance config, plugin metadata, bundled skill allowlists, package-local plugin artifacts, single-entry store writes, and validated/serialized session prompt blobs.</li>
|
||||
</ul>
|
||||
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.6.1/OpenClaw-2026.6.1.zip" length="55062100" type="application/octet-stream" sparkle:edSignature="PVp8E2HBCvikB/0LCr36lFEyHPAzoFA2ScT6LW27FlzvP+m4r1AEuVN2UrtgWlpkGSsn4Eav0kPJe32u4ObNBw=="/>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -2,5 +2,5 @@
|
||||
# Source of truth: apps/android/version.json
|
||||
# Generated by scripts/android-sync-versioning.ts.
|
||||
|
||||
OPENCLAW_ANDROID_VERSION_NAME=2026.6.10
|
||||
OPENCLAW_ANDROID_VERSION_CODE=2026061001
|
||||
OPENCLAW_ANDROID_VERSION_NAME=2026.6.2
|
||||
OPENCLAW_ANDROID_VERSION_CODE=2026060201
|
||||
|
||||
@@ -69,17 +69,6 @@ Generate raw Google Play screenshots:
|
||||
pnpm android:screenshots
|
||||
```
|
||||
|
||||
To make screenshot capture own emulator startup, pass a named AVD:
|
||||
|
||||
```bash
|
||||
ANDROID_SCREENSHOT_AVD=OpenClaw_QA_API35 pnpm android:screenshots
|
||||
```
|
||||
|
||||
The screenshot script uses one connected ADB device when available. If none is
|
||||
connected and `ANDROID_SCREENSHOT_AVD` is set, it boots that emulator
|
||||
headlessly, waits for Android to finish booting, disables animations, captures
|
||||
the screenshots, then shuts down the emulator it started.
|
||||
|
||||
`pnpm android:release:archive` builds signed release artifacts into `apps/android/build/release-artifacts/` and writes `.sha256` checksum files:
|
||||
|
||||
- Play build: `openclaw-<version>-play-release.aab`
|
||||
|
||||
@@ -49,45 +49,13 @@ Recommended workflow:
|
||||
3. Update `apps/android/CHANGELOG.md`, then run `pnpm android:version:sync` again if needed.
|
||||
4. Run `MATCH_PASSWORD=<signing repo password> pnpm android:release:signing:sync:pull` to materialize encrypted Android signing assets from `apps-signing`.
|
||||
5. Run `pnpm android:release:preflight` to validate Play auth, signing, synced versioning, and release notes.
|
||||
6. Run `ANDROID_SCREENSHOT_AVD=<avd-name> pnpm android:screenshots` to refresh raw Google Play screenshots with a script-managed emulator, or run `pnpm android:screenshots` when exactly one ADB device is already connected.
|
||||
6. Run `pnpm android:screenshots` to refresh raw Google Play screenshots.
|
||||
7. Run `pnpm android:release:archive` to produce the signed Play AAB and third-party APK.
|
||||
8. Run `pnpm android:release:upload` to upload metadata, screenshots, and the Play AAB to Google Play internal testing.
|
||||
9. Promote to production manually in Google Play Console.
|
||||
|
||||
The third-party flavor is archived as a signed APK for non-Play distribution. It is not uploaded by the Play release lane.
|
||||
|
||||
## Release SHA tracking
|
||||
|
||||
Successful Play build uploads create a non-tag Git ref that records the source
|
||||
commit for the uploaded store build:
|
||||
|
||||
```text
|
||||
refs/openclaw/mobile-releases/android/<versionName>-<versionCode>
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
refs/openclaw/mobile-releases/android/2026.6.10-2026061008
|
||||
```
|
||||
|
||||
These refs are intentionally outside `refs/tags/*` and `refs/heads/*`. They do
|
||||
not appear on GitHub release or tag pages, and they do not participate in the
|
||||
core OpenClaw release machinery.
|
||||
|
||||
`pnpm android:release:upload` checks the ref before uploading the Play build and
|
||||
records it only after `upload_to_play_store` succeeds. Existing refs are
|
||||
immutable: the same ref at the same SHA is accepted, while the same ref at a
|
||||
different SHA fails. `GOOGLE_PLAY_VALIDATE_ONLY=1` still checks the ref but does
|
||||
not record it because no Play build is published.
|
||||
|
||||
Useful direct commands:
|
||||
|
||||
```bash
|
||||
pnpm mobile:release:preflight -- --platform android --version 2026.6.10 --version-code 2026061008
|
||||
pnpm mobile:release:resolve -- --platform android --version 2026.6.10 --version-code 2026061008
|
||||
```
|
||||
|
||||
## Signing model
|
||||
|
||||
`apps/android/Config/ReleaseSigning.json` pins the Android signing assets in the shared private `apps-signing` repo. The Android pipeline uses the same `MATCH_PASSWORD` release-owner secret as iOS, but the Android files are managed by `scripts/android-release-signing.mjs` instead of Fastlane `match`.
|
||||
|
||||
@@ -59,7 +59,7 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace = "ai.openclaw.app"
|
||||
compileSdk = 36
|
||||
compileSdk = 37
|
||||
|
||||
// Release signing is local-only; keep the keystore path and passwords out of the repo.
|
||||
signingConfigs {
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<permission
|
||||
android:name="${applicationId}.permission.RUN_VOICE_E2E"
|
||||
android:protectionLevel="signature" />
|
||||
|
||||
<uses-permission android:name="${applicationId}.permission.RUN_VOICE_E2E" />
|
||||
|
||||
<application>
|
||||
<receiver
|
||||
android:name=".VoiceE2eReceiver"
|
||||
android:permission="${applicationId}.permission.RUN_VOICE_E2E"
|
||||
android:exported="false">
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="ai.openclaw.app.debug.RUN_VOICE_E2E" />
|
||||
</intent-filter>
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
package ai.openclaw.app
|
||||
|
||||
import ai.openclaw.app.node.asObjectOrNull
|
||||
import ai.openclaw.app.node.asStringOrNull
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
|
||||
data class GatewayExecApprovalSummary(
|
||||
val id: String,
|
||||
val commandText: String,
|
||||
val commandPreview: String?,
|
||||
val allowedDecisions: List<String>,
|
||||
val host: String?,
|
||||
val nodeId: String?,
|
||||
val agentId: String?,
|
||||
val createdAtMs: Long?,
|
||||
val expiresAtMs: Long?,
|
||||
val resolvingDecision: String? = null,
|
||||
val errorText: String? = null,
|
||||
)
|
||||
|
||||
internal fun parseGatewayExecApprovalListPayload(
|
||||
payloadJson: String,
|
||||
json: Json,
|
||||
): List<GatewayExecApprovalSummary> =
|
||||
try {
|
||||
(json.parseToJsonElement(payloadJson) as? JsonArray)
|
||||
?.mapNotNull(::parseGatewayExecApprovalListEntry)
|
||||
?.sortedBy { it.createdAtMs ?: Long.MAX_VALUE }
|
||||
.orEmpty()
|
||||
} catch (_: Throwable) {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
internal fun parseGatewayExecApprovalListEntry(item: JsonElement): GatewayExecApprovalSummary? {
|
||||
val obj = item.asObjectOrNull() ?: return null
|
||||
val id = obj["id"].asStringOrNull()?.trim().orEmpty()
|
||||
if (id.isEmpty()) return null
|
||||
val request = obj["request"].asObjectOrNull()
|
||||
val commandText = gatewayExecApprovalListCommandText(obj, request)
|
||||
return GatewayExecApprovalSummary(
|
||||
id = id,
|
||||
commandText = commandText,
|
||||
commandPreview = gatewayExecApprovalListCommandPreview(obj, request, commandText),
|
||||
allowedDecisions = emptyList(),
|
||||
host =
|
||||
request
|
||||
?.get("host")
|
||||
.asStringOrNull()
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() },
|
||||
nodeId =
|
||||
request
|
||||
?.get("nodeId")
|
||||
.asStringOrNull()
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() },
|
||||
agentId =
|
||||
request
|
||||
?.get("agentId")
|
||||
.asStringOrNull()
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() },
|
||||
createdAtMs = obj.long("createdAtMs"),
|
||||
expiresAtMs = obj.long("expiresAtMs"),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun parseGatewayExecApprovalDetail(
|
||||
obj: JsonObject,
|
||||
createdAtMs: Long?,
|
||||
): GatewayExecApprovalSummary? {
|
||||
val id = obj["id"].asStringOrNull()?.trim().orEmpty()
|
||||
if (id.isEmpty()) return null
|
||||
return GatewayExecApprovalSummary(
|
||||
id = id,
|
||||
commandText =
|
||||
obj["commandText"]
|
||||
.asStringOrNull()
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?: "Command request",
|
||||
commandPreview =
|
||||
obj["commandPreview"]
|
||||
.asStringOrNull()
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() },
|
||||
allowedDecisions = gatewayExecApprovalAllowedDecisions(obj),
|
||||
host = obj["host"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() },
|
||||
nodeId = obj["nodeId"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() },
|
||||
agentId = obj["agentId"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() },
|
||||
createdAtMs = createdAtMs,
|
||||
expiresAtMs = obj.long("expiresAtMs"),
|
||||
)
|
||||
}
|
||||
|
||||
private fun gatewayExecApprovalListCommandText(obj: JsonObject, request: JsonObject?): String =
|
||||
obj["commandText"]
|
||||
.asStringOrNull()
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?: request
|
||||
?.get("command")
|
||||
.asStringOrNull()
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?: "Command request"
|
||||
|
||||
private fun gatewayExecApprovalListCommandPreview(
|
||||
obj: JsonObject,
|
||||
request: JsonObject?,
|
||||
commandText: String,
|
||||
): String? {
|
||||
val preview =
|
||||
obj["commandPreview"]
|
||||
.asStringOrNull()
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?: request
|
||||
?.get("commandPreview")
|
||||
.asStringOrNull()
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
return preview?.takeIf { it != commandText }
|
||||
}
|
||||
|
||||
private fun gatewayExecApprovalAllowedDecisions(request: JsonObject?): List<String> {
|
||||
val explicit = parseGatewayExecApprovalDecisions(request?.get("allowedDecisions") as? JsonArray)
|
||||
if (explicit.isNotEmpty()) return explicit
|
||||
val allowed =
|
||||
if (request
|
||||
?.get("ask")
|
||||
.asStringOrNull()
|
||||
?.trim()
|
||||
?.lowercase() == "always"
|
||||
) {
|
||||
listOf("allow-once", "deny")
|
||||
} else {
|
||||
listOf("allow-once", "allow-always", "deny")
|
||||
}
|
||||
val unavailable = parseGatewayExecApprovalDecisions(request?.get("unavailableDecisions") as? JsonArray).toSet()
|
||||
return allowed.filterNot { it == "allow-always" && it in unavailable }
|
||||
}
|
||||
|
||||
private fun parseGatewayExecApprovalDecisions(items: JsonArray?): List<String> =
|
||||
items
|
||||
?.mapNotNull { item ->
|
||||
when (item.asStringOrNull()?.trim()) {
|
||||
"allow-once" -> "allow-once"
|
||||
"allow-always" -> "allow-always"
|
||||
"deny" -> "deny"
|
||||
else -> null
|
||||
}
|
||||
}?.distinct()
|
||||
.orEmpty()
|
||||
|
||||
private fun JsonObject?.long(key: String): Long? = (this?.get(key) as? JsonPrimitive)?.content?.trim()?.toLongOrNull()
|
||||
@@ -204,9 +204,6 @@ class MainViewModel(
|
||||
val chatPendingToolCalls: StateFlow<List<ChatPendingToolCall>> = runtimeState(initial = emptyList()) { it.chatPendingToolCalls }
|
||||
val chatSessions: StateFlow<List<ChatSessionEntry>> = runtimeState(initial = emptyList()) { it.chatSessions }
|
||||
val pendingRunCount: StateFlow<Int> = runtimeState(initial = 0) { it.pendingRunCount }
|
||||
val execApprovals: StateFlow<List<GatewayExecApprovalSummary>> = runtimeState(initial = emptyList()) { it.execApprovals }
|
||||
val execApprovalsRefreshing: StateFlow<Boolean> = runtimeState(initial = false) { it.execApprovalsRefreshing }
|
||||
val execApprovalsErrorText: StateFlow<String?> = runtimeState(initial = null) { it.execApprovalsErrorText }
|
||||
|
||||
val canvas: CanvasController
|
||||
get() = ensureRuntime().canvas
|
||||
@@ -540,17 +537,6 @@ class MainViewModel(
|
||||
ensureRuntime().refreshNodesDevices()
|
||||
}
|
||||
|
||||
fun refreshExecApprovals() {
|
||||
ensureRuntime().refreshExecApprovals()
|
||||
}
|
||||
|
||||
fun resolveExecApproval(
|
||||
id: String,
|
||||
decision: String,
|
||||
) {
|
||||
ensureRuntime().resolveExecApproval(id = id, decision = decision)
|
||||
}
|
||||
|
||||
fun refreshChannels() {
|
||||
ensureRuntime().refreshChannels()
|
||||
}
|
||||
|
||||
@@ -223,11 +223,10 @@ class NodeForegroundService : Service() {
|
||||
|
||||
internal fun foregroundServiceTypesForVoiceMode(mode: VoiceCaptureMode): Int {
|
||||
val base = ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
|
||||
return when (mode) {
|
||||
VoiceCaptureMode.Off -> base
|
||||
VoiceCaptureMode.ManualMic,
|
||||
VoiceCaptureMode.TalkMode,
|
||||
-> base or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
||||
return if (mode == VoiceCaptureMode.TalkMode) {
|
||||
base or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
||||
} else {
|
||||
base
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import ai.openclaw.app.gateway.GatewayTlsProbeFailure
|
||||
import ai.openclaw.app.gateway.GatewayTlsProbeResult
|
||||
import ai.openclaw.app.gateway.GatewayUpdateAvailableSummary
|
||||
import ai.openclaw.app.gateway.normalizeGatewayTlsFingerprint
|
||||
import ai.openclaw.app.gateway.parseChatSendAck
|
||||
import ai.openclaw.app.gateway.probeGatewayTlsFingerprint
|
||||
import ai.openclaw.app.node.A2UIHandler
|
||||
import ai.openclaw.app.node.CalendarHandler
|
||||
@@ -74,9 +73,7 @@ import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import java.util.Collections
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
/**
|
||||
@@ -402,15 +399,6 @@ class NodeRuntime(
|
||||
private val _nodesDevicesErrorText = MutableStateFlow<String?>(null)
|
||||
val nodesDevicesErrorText: StateFlow<String?> = _nodesDevicesErrorText.asStateFlow()
|
||||
private val nodeApprovalRefreshGuard = GatewayNodeApprovalRefreshGuard()
|
||||
private val _execApprovals = MutableStateFlow<List<GatewayExecApprovalSummary>>(emptyList())
|
||||
val execApprovals: StateFlow<List<GatewayExecApprovalSummary>> = _execApprovals.asStateFlow()
|
||||
private val _execApprovalsRefreshing = MutableStateFlow(false)
|
||||
val execApprovalsRefreshing: StateFlow<Boolean> = _execApprovalsRefreshing.asStateFlow()
|
||||
private val _execApprovalsErrorText = MutableStateFlow<String?>(null)
|
||||
val execApprovalsErrorText: StateFlow<String?> = _execApprovalsErrorText.asStateFlow()
|
||||
private val execApprovalsRefreshSeq = AtomicLong(0)
|
||||
private val execApprovalsStateLock = Any()
|
||||
private val resolvedExecApprovalIds = Collections.newSetFromMap(ConcurrentHashMap<String, Boolean>())
|
||||
private val _channelsSummary = MutableStateFlow(GatewayChannelsSummary(channels = emptyList()))
|
||||
val channelsSummary: StateFlow<GatewayChannelsSummary> = _channelsSummary.asStateFlow()
|
||||
private val _channelsRefreshing = MutableStateFlow(false)
|
||||
@@ -460,7 +448,6 @@ class NodeRuntime(
|
||||
micCapture.onGatewayConnectionChanged(true)
|
||||
scope.launch {
|
||||
subscribeOperatorSessionEvents()
|
||||
refreshExecApprovalsFromGateway()
|
||||
refreshHomeCanvasOverviewIfConnected()
|
||||
if (voiceReplySpeakerLazy.isInitialized()) {
|
||||
voiceReplySpeaker.refreshConfig()
|
||||
@@ -490,11 +477,6 @@ class NodeRuntime(
|
||||
pendingDevices = emptyList(),
|
||||
pairedDevices = emptyList(),
|
||||
)
|
||||
invalidateExecApprovalRefreshes()
|
||||
resolvedExecApprovalIds.clear()
|
||||
_execApprovals.value = emptyList()
|
||||
_execApprovalsRefreshing.value = false
|
||||
_execApprovalsErrorText.value = null
|
||||
_channelsSummary.value = GatewayChannelsSummary(channels = emptyList())
|
||||
_dreamingSummary.value = GatewayDreamingSummary()
|
||||
_healthLogsSummary.value = GatewayHealthLogsSummary()
|
||||
@@ -650,11 +632,7 @@ class NodeRuntime(
|
||||
put("idempotencyKey", JsonPrimitive(idempotencyKey))
|
||||
}
|
||||
val response = operatorSession.request("chat.send", params.toString())
|
||||
val ack = parseChatSendAck(json, response)
|
||||
ack.copy(runId = ack.runId ?: idempotencyKey)
|
||||
},
|
||||
refreshAfterTerminalSuccess = {
|
||||
chat.refresh()
|
||||
parseChatSendRunId(response) ?: idempotencyKey
|
||||
},
|
||||
speakAssistantReply = { text ->
|
||||
// Voice-tab replies should speak through the dedicated reply speaker.
|
||||
@@ -842,24 +820,6 @@ class NodeRuntime(
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshExecApprovals() {
|
||||
scope.launch {
|
||||
refreshExecApprovalsFromGateway()
|
||||
}
|
||||
}
|
||||
|
||||
fun resolveExecApproval(
|
||||
id: String,
|
||||
decision: String,
|
||||
) {
|
||||
val normalizedId = id.trim()
|
||||
val normalizedDecision = decision.trim()
|
||||
if (normalizedId.isEmpty() || normalizedDecision.isEmpty()) return
|
||||
scope.launch {
|
||||
resolveExecApprovalOnGateway(id = normalizedId, decision = normalizedDecision)
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshChannels() {
|
||||
scope.launch {
|
||||
refreshChannelsFromGateway()
|
||||
@@ -1035,9 +995,6 @@ class NodeRuntime(
|
||||
_isForeground.value = value
|
||||
if (value) {
|
||||
reconnectPreferredGatewayOnForeground()
|
||||
scope.launch {
|
||||
refreshExecApprovalsFromGateway()
|
||||
}
|
||||
} else {
|
||||
stopManualVoiceSession()
|
||||
publishNodePresenceAliveBeacon(NodePresenceAliveBeacon.Trigger.Background, throttleRecentSuccess = true)
|
||||
@@ -1439,9 +1396,8 @@ class NodeRuntime(
|
||||
mode: VoiceCaptureMode,
|
||||
persistManualMic: Boolean = true,
|
||||
) {
|
||||
if (mode.requiresMicrophonePermission && !hasRecordAudioPermission()) {
|
||||
if (mode == VoiceCaptureMode.TalkMode && !hasRecordAudioPermission()) {
|
||||
_voiceCaptureMode.value = VoiceCaptureMode.Off
|
||||
prefs.setVoiceMicEnabled(false)
|
||||
externalAudioCaptureActive.value = false
|
||||
return
|
||||
}
|
||||
@@ -1512,9 +1468,6 @@ class NodeRuntime(
|
||||
}
|
||||
}
|
||||
|
||||
private val VoiceCaptureMode.requiresMicrophonePermission: Boolean
|
||||
get() = this == VoiceCaptureMode.ManualMic || this == VoiceCaptureMode.TalkMode
|
||||
|
||||
fun refreshGatewayConnection() {
|
||||
val endpoint = connectedEndpoint
|
||||
if (endpoint == null) {
|
||||
@@ -1867,47 +1820,11 @@ class NodeRuntime(
|
||||
if (event == "update.available") {
|
||||
_gatewayUpdateAvailable.value = parseGatewayUpdateAvailable(payloadJson)
|
||||
}
|
||||
handleExecApprovalGatewayEvent(event = event, payloadJson = payloadJson)
|
||||
micCapture.handleGatewayEvent(event, payloadJson)
|
||||
talkMode.handleGatewayEvent(event, payloadJson)
|
||||
chat.handleGatewayEvent(event, payloadJson)
|
||||
}
|
||||
|
||||
private fun handleExecApprovalGatewayEvent(
|
||||
event: String,
|
||||
payloadJson: String?,
|
||||
) {
|
||||
when (event) {
|
||||
"exec.approval.requested" -> {
|
||||
val approvalId = parseExecApprovalEventId(payloadJson)
|
||||
approvalId?.let(resolvedExecApprovalIds::remove)
|
||||
scope.launch {
|
||||
if (approvalId == null) {
|
||||
refreshExecApprovalsFromGateway()
|
||||
} else {
|
||||
refreshExecApprovalFromGateway(approvalId)
|
||||
}
|
||||
}
|
||||
}
|
||||
"exec.approval.resolved" -> {
|
||||
val approvalId = parseExecApprovalEventId(payloadJson) ?: return
|
||||
markExecApprovalResolved(approvalId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseExecApprovalEventId(payloadJson: String?): String? =
|
||||
try {
|
||||
payloadJson
|
||||
?.let { json.parseToJsonElement(it).asObjectOrNull() }
|
||||
?.get("id")
|
||||
.asStringOrNull()
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
|
||||
private fun parseGatewayUpdateAvailable(payloadJson: String?): GatewayUpdateAvailableSummary? {
|
||||
return try {
|
||||
val root = payloadJson?.let { json.parseToJsonElement(it).asObjectOrNull() }
|
||||
@@ -1922,6 +1839,15 @@ class NodeRuntime(
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseChatSendRunId(response: String): String? {
|
||||
return try {
|
||||
val root = json.parseToJsonElement(response).asObjectOrNull() ?: return null
|
||||
root["runId"].asStringOrNull()
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseTalkSessionId(response: String): String {
|
||||
val root = json.parseToJsonElement(response).asObjectOrNull()
|
||||
val sessionId =
|
||||
@@ -2154,196 +2080,6 @@ class NodeRuntime(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun refreshExecApprovalsFromGateway() {
|
||||
val refreshGeneration = execApprovalsRefreshSeq.incrementAndGet()
|
||||
_execApprovalsRefreshing.value = true
|
||||
_execApprovalsErrorText.value = null
|
||||
if (!operatorConnected) {
|
||||
if (execApprovalsRefreshSeq.get() == refreshGeneration) {
|
||||
_execApprovals.value = emptyList()
|
||||
_execApprovalsRefreshing.value = false
|
||||
}
|
||||
return
|
||||
}
|
||||
try {
|
||||
val res = operatorSession.request("exec.approval.list", "{}")
|
||||
val existing = _execApprovals.value.associateBy { it.id }
|
||||
val rows =
|
||||
parseGatewayExecApprovalListPayload(res, json)
|
||||
.filterNot { it.id in resolvedExecApprovalIds }
|
||||
.map { row ->
|
||||
val hydrated =
|
||||
try {
|
||||
fetchExecApprovalDetailFromGateway(
|
||||
id = row.id,
|
||||
createdAtMs = row.createdAtMs ?: System.currentTimeMillis(),
|
||||
)
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
} ?: row.copy(errorText = "Could not load approval details. Refresh and try again.")
|
||||
val current = existing[row.id]
|
||||
if (current == null) {
|
||||
hydrated
|
||||
} else {
|
||||
hydrated.copy(
|
||||
resolvingDecision = current.resolvingDecision,
|
||||
errorText = current.errorText ?: hydrated.errorText,
|
||||
)
|
||||
}
|
||||
}
|
||||
publishExecApprovalsIfCurrent(refreshGeneration, rows)
|
||||
} catch (_: Throwable) {
|
||||
if (execApprovalsRefreshSeq.get() == refreshGeneration) {
|
||||
_execApprovalsErrorText.value = "Could not load approvals."
|
||||
}
|
||||
} finally {
|
||||
if (execApprovalsRefreshSeq.get() == refreshGeneration) {
|
||||
_execApprovalsRefreshing.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun refreshExecApprovalFromGateway(id: String) {
|
||||
if (!operatorConnected) return
|
||||
if (id in resolvedExecApprovalIds) return
|
||||
try {
|
||||
val current = _execApprovals.value.firstOrNull { it.id == id }
|
||||
val row =
|
||||
fetchExecApprovalDetailFromGateway(
|
||||
id = id,
|
||||
createdAtMs = current?.createdAtMs ?: System.currentTimeMillis(),
|
||||
) ?: return
|
||||
if (id in resolvedExecApprovalIds) return
|
||||
invalidateExecApprovalRefreshes()
|
||||
upsertExecApproval(row)
|
||||
} catch (_: Throwable) {
|
||||
refreshExecApprovalsFromGateway()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchExecApprovalDetailFromGateway(
|
||||
id: String,
|
||||
createdAtMs: Long,
|
||||
): GatewayExecApprovalSummary? {
|
||||
val params = buildJsonObject { put("id", JsonPrimitive(id)) }.toString()
|
||||
val res = operatorSession.request("exec.approval.get", params)
|
||||
val root = json.parseToJsonElement(res).asObjectOrNull() ?: return null
|
||||
return parseGatewayExecApprovalDetail(root, createdAtMs = createdAtMs)
|
||||
}
|
||||
|
||||
private suspend fun resolveExecApprovalOnGateway(
|
||||
id: String,
|
||||
decision: String,
|
||||
) {
|
||||
synchronized(execApprovalsStateLock) {
|
||||
if (!operatorConnected || id in resolvedExecApprovalIds) return
|
||||
val currentRows = _execApprovals.value
|
||||
if (currentRows.none { it.id == id }) return
|
||||
invalidateExecApprovalRefreshes()
|
||||
_execApprovals.value =
|
||||
currentRows.map { row ->
|
||||
if (row.id == id) row.copy(resolvingDecision = decision, errorText = null) else row
|
||||
}
|
||||
}
|
||||
try {
|
||||
val params =
|
||||
buildJsonObject {
|
||||
put("id", JsonPrimitive(id))
|
||||
put("decision", JsonPrimitive(decision))
|
||||
}.toString()
|
||||
operatorSession.request("exec.approval.resolve", params)
|
||||
markExecApprovalResolved(id)
|
||||
} catch (_: Throwable) {
|
||||
synchronized(execApprovalsStateLock) {
|
||||
if (!operatorConnected || id in resolvedExecApprovalIds) return
|
||||
_execApprovals.value =
|
||||
_execApprovals.value.map { row ->
|
||||
if (row.id == id) {
|
||||
row.copy(resolvingDecision = null, errorText = "Could not resolve approval. Refresh and try again.")
|
||||
} else {
|
||||
row
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun upsertExecApproval(row: GatewayExecApprovalSummary) {
|
||||
synchronized(execApprovalsStateLock) {
|
||||
if (!operatorConnected || row.id in resolvedExecApprovalIds) return
|
||||
if (row.isExpiredExecApproval()) return
|
||||
val rows = _execApprovals.value
|
||||
val replaced = rows.any { it.id == row.id }
|
||||
val nextRows =
|
||||
(
|
||||
if (replaced) {
|
||||
rows.map { current ->
|
||||
if (current.id == row.id) {
|
||||
row.copy(
|
||||
resolvingDecision = current.resolvingDecision,
|
||||
errorText = current.errorText,
|
||||
)
|
||||
} else {
|
||||
current
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rows + row
|
||||
}
|
||||
).filterActiveExecApprovals()
|
||||
.sortedBy { it.createdAtMs ?: Long.MAX_VALUE }
|
||||
_execApprovals.value = nextRows
|
||||
scheduleExecApprovalExpiryPrune(nextRows)
|
||||
}
|
||||
}
|
||||
|
||||
private fun invalidateExecApprovalRefreshes() {
|
||||
execApprovalsRefreshSeq.incrementAndGet()
|
||||
_execApprovalsRefreshing.value = false
|
||||
}
|
||||
|
||||
private fun markExecApprovalResolved(id: String) {
|
||||
synchronized(execApprovalsStateLock) {
|
||||
resolvedExecApprovalIds.add(id)
|
||||
invalidateExecApprovalRefreshes()
|
||||
_execApprovals.value = _execApprovals.value.filterNot { it.id == id }
|
||||
}
|
||||
}
|
||||
|
||||
private fun publishExecApprovalsIfCurrent(
|
||||
refreshGeneration: Long,
|
||||
rows: List<GatewayExecApprovalSummary>,
|
||||
) {
|
||||
synchronized(execApprovalsStateLock) {
|
||||
if (execApprovalsRefreshSeq.get() == refreshGeneration && operatorConnected) {
|
||||
val nextRows = rows.filterNot { it.id in resolvedExecApprovalIds }.filterActiveExecApprovals()
|
||||
_execApprovals.value = nextRows
|
||||
scheduleExecApprovalExpiryPrune(nextRows)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleExecApprovalExpiryPrune(rows: List<GatewayExecApprovalSummary>) {
|
||||
val now = System.currentTimeMillis()
|
||||
val nextExpiry = rows.mapNotNull { it.expiresAtMs }.filter { it > now }.minOrNull() ?: return
|
||||
scope.launch {
|
||||
delay((nextExpiry - now + 250).coerceAtLeast(0))
|
||||
pruneExpiredExecApprovals()
|
||||
}
|
||||
}
|
||||
|
||||
private fun pruneExpiredExecApprovals() {
|
||||
synchronized(execApprovalsStateLock) {
|
||||
_execApprovals.value = _execApprovals.value.filterActiveExecApprovals()
|
||||
}
|
||||
}
|
||||
|
||||
private fun GatewayExecApprovalSummary.isExpiredExecApproval(nowMs: Long = System.currentTimeMillis()): Boolean = expiresAtMs?.let { it <= nowMs } == true
|
||||
|
||||
private fun List<GatewayExecApprovalSummary>.filterActiveExecApprovals(
|
||||
nowMs: Long = System.currentTimeMillis(),
|
||||
): List<GatewayExecApprovalSummary> = filterNot { it.isExpiredExecApproval(nowMs) }
|
||||
|
||||
private fun invalidateNodeCapabilityApprovalState() {
|
||||
val refreshGeneration = nodeApprovalRefreshGuard.begin()
|
||||
nodeApprovalRefreshGuard.publishIfCurrent(refreshGeneration) {
|
||||
@@ -2458,19 +2194,12 @@ class NodeRuntime(
|
||||
}.orEmpty()
|
||||
|
||||
private fun parseGatewayLogEntry(line: String): GatewayLogEntry {
|
||||
val sanitizedLine = sanitizeGatewayLogText(line)
|
||||
val root =
|
||||
try {
|
||||
json.parseToJsonElement(line).asObjectOrNull()
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
} ?: return GatewayLogEntry(
|
||||
time = null,
|
||||
level = null,
|
||||
subsystem = null,
|
||||
message = sanitizedLine.trim().ifEmpty { "Empty log entry" },
|
||||
raw = sanitizedLine,
|
||||
)
|
||||
} ?: return GatewayLogEntry(time = null, level = null, subsystem = null, message = line.trim().ifEmpty { "Empty log entry" })
|
||||
val meta = root["_meta"].asObjectOrNull()
|
||||
val time = root["time"].asStringOrNull() ?: meta?.get("date").asStringOrNull()
|
||||
val level = normalizeLogLevel(meta?.get("logLevelName").asStringOrNull() ?: meta?.get("level").asStringOrNull())
|
||||
@@ -2488,7 +2217,7 @@ class NodeRuntime(
|
||||
?: root["message"].asStringOrNull()
|
||||
?: line
|
||||
val normalizedMessage =
|
||||
sanitizeGatewayLogText(message)
|
||||
message
|
||||
.trim()
|
||||
.replace(Regex("\\s+"), " ")
|
||||
.take(240)
|
||||
@@ -2496,9 +2225,8 @@ class NodeRuntime(
|
||||
return GatewayLogEntry(
|
||||
time = time,
|
||||
level = level,
|
||||
subsystem = subsystem?.let(::sanitizeGatewayLogText)?.trim()?.takeIf { it.isNotEmpty() },
|
||||
subsystem = subsystem?.trim()?.takeIf { it.isNotEmpty() },
|
||||
message = normalizedMessage,
|
||||
raw = sanitizedLine,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2587,7 +2315,6 @@ class NodeRuntime(
|
||||
if (name.isEmpty()) return@mapNotNull null
|
||||
val missing = obj["missing"].asObjectOrNull()
|
||||
GatewaySkillSummary(
|
||||
skillKey = obj["skillKey"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() } ?: name,
|
||||
name = name,
|
||||
description = obj["description"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() },
|
||||
source = obj["source"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() } ?: "unknown",
|
||||
@@ -3038,6 +2765,11 @@ internal fun resolveOperatorSessionConnectAuth(
|
||||
)
|
||||
}
|
||||
|
||||
internal fun shouldConnectOperatorSession(
|
||||
auth: NodeRuntime.GatewayConnectAuth,
|
||||
storedOperatorToken: String?,
|
||||
): Boolean = resolveOperatorSessionConnectAuth(auth, storedOperatorToken) != null
|
||||
|
||||
private enum class HomeCanvasGatewayState {
|
||||
Connected,
|
||||
Connecting,
|
||||
@@ -3110,7 +2842,6 @@ data class GatewaySkillsSummary(
|
||||
)
|
||||
|
||||
data class GatewaySkillSummary(
|
||||
val skillKey: String,
|
||||
val name: String,
|
||||
val description: String?,
|
||||
val source: String,
|
||||
@@ -3308,19 +3039,8 @@ data class GatewayLogEntry(
|
||||
val level: String?,
|
||||
val subsystem: String?,
|
||||
val message: String,
|
||||
val raw: String,
|
||||
)
|
||||
|
||||
private val gatewayAnsiControlPattern = Regex("\\u001B\\[[0-?]*[ -/]*[@-~]")
|
||||
private val gatewayEscapedAnsiControlPattern = Regex("""\\u001[Bb]\[[0-?]*[ -/]*[@-~]""")
|
||||
private val gatewayVisibleSgrPattern = Regex("\\[(?:0|\\d{1,3}(?:;\\d{1,3})*)m(?!])")
|
||||
|
||||
internal fun sanitizeGatewayLogText(value: String): String =
|
||||
value
|
||||
.replace(gatewayAnsiControlPattern, "")
|
||||
.replace(gatewayEscapedAnsiControlPattern, "")
|
||||
.replace(gatewayVisibleSgrPattern, "")
|
||||
|
||||
private fun JsonObject?.long(key: String): Long? = (this?.get(key) as? JsonPrimitive)?.content?.trim()?.toLongOrNull()
|
||||
|
||||
private fun JsonObject?.double(key: String): Double? = (this?.get(key) as? JsonPrimitive)?.content?.trim()?.toDoubleOrNull()
|
||||
|
||||
@@ -393,6 +393,12 @@ class SecurePrefs(
|
||||
return stored?.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
/** Saves the paired gateway token under the current Android instance id. */
|
||||
fun saveGatewayToken(token: String) {
|
||||
val key = "gateway.token.${_instanceId.value}"
|
||||
securePrefs.edit { putString(key, token.trim()) }
|
||||
}
|
||||
|
||||
/** Loads the bootstrap token used during gateway setup and device-token handoff. */
|
||||
fun loadGatewayBootstrapToken(): String? {
|
||||
val key = "gateway.bootstrapToken.${_instanceId.value}"
|
||||
|
||||
@@ -6,6 +6,14 @@ internal fun normalizeMainKey(raw: String?): String {
|
||||
return if (!trimmed.isNullOrEmpty()) trimmed else "main"
|
||||
}
|
||||
|
||||
/** Accepts only gateway session keys that can represent the main chat stream. */
|
||||
internal fun isCanonicalMainSessionKey(raw: String?): Boolean {
|
||||
val trimmed = raw?.trim().orEmpty()
|
||||
if (trimmed.isEmpty()) return false
|
||||
if (trimmed == "global") return true
|
||||
return trimmed.startsWith("agent:")
|
||||
}
|
||||
|
||||
/** Extracts the agent id from canonical agent-scoped main session keys. */
|
||||
internal fun resolveAgentIdFromMainSessionKey(raw: String?): String? {
|
||||
val trimmed = raw?.trim().orEmpty()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package ai.openclaw.app.chat
|
||||
|
||||
import ai.openclaw.app.gateway.GatewaySession
|
||||
import ai.openclaw.app.gateway.parseChatSendAck
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -20,21 +19,11 @@ import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
class ChatController internal constructor(
|
||||
class ChatController(
|
||||
private val scope: CoroutineScope,
|
||||
private val session: GatewaySession,
|
||||
private val json: Json,
|
||||
private val requestGateway: suspend (method: String, paramsJson: String?) -> String,
|
||||
) {
|
||||
constructor(
|
||||
scope: CoroutineScope,
|
||||
session: GatewaySession,
|
||||
json: Json,
|
||||
) : this(
|
||||
scope = scope,
|
||||
json = json,
|
||||
requestGateway = { method, paramsJson -> session.request(method, paramsJson) },
|
||||
)
|
||||
|
||||
private var appliedMainSessionKey = "main"
|
||||
private val _sessionKey = MutableStateFlow("main")
|
||||
val sessionKey: StateFlow<String> = _sessionKey.asStateFlow()
|
||||
@@ -278,9 +267,8 @@ class ChatController internal constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
val res = requestGateway("chat.send", params.toString())
|
||||
val ack = parseChatSendAck(json, res)
|
||||
val actualRunId = ack.runId ?: runId
|
||||
val res = session.request("chat.send", params.toString())
|
||||
val actualRunId = parseRunId(res) ?: runId
|
||||
if (actualRunId != runId) {
|
||||
// Gateway may return a canonical run id; move all pending bookkeeping to that id.
|
||||
optimisticMessagesByRunId[actualRunId] = optimisticMessagesByRunId.remove(runId) ?: optimisticMessage
|
||||
@@ -291,24 +279,7 @@ class ChatController internal constructor(
|
||||
_pendingRunCount.value = pendingRuns.size
|
||||
}
|
||||
}
|
||||
if (ack.isTerminal) {
|
||||
clearPendingRun(actualRunId)
|
||||
removeOptimisticMessage(actualRunId)
|
||||
pendingToolCallsById.clear()
|
||||
publishPendingToolCalls()
|
||||
_streamingAssistantText.value = null
|
||||
if (ack.isTerminalSuccess) {
|
||||
refreshCurrentHistoryBestEffort()
|
||||
true
|
||||
} else {
|
||||
// Terminal timeout/error means the gateway did not accept a runnable turn.
|
||||
// Surface failed acceptance instead of letting a cleared composer look successful.
|
||||
_errorText.value = "Chat failed before the run started; try again."
|
||||
false
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
true
|
||||
} catch (err: Throwable) {
|
||||
clearPendingRun(runId)
|
||||
removeOptimisticMessage(runId)
|
||||
@@ -332,7 +303,7 @@ class ChatController internal constructor(
|
||||
put("sessionKey", JsonPrimitive(_sessionKey.value))
|
||||
put("runId", JsonPrimitive(runId))
|
||||
}
|
||||
requestGateway("chat.abort", params.toString())
|
||||
session.request("chat.abort", params.toString())
|
||||
} catch (_: Throwable) {
|
||||
// best-effort
|
||||
}
|
||||
@@ -385,7 +356,7 @@ class ChatController internal constructor(
|
||||
) {
|
||||
try {
|
||||
val historyJson =
|
||||
requestGateway(
|
||||
session.request(
|
||||
"chat.history",
|
||||
buildJsonObject { put("sessionKey", JsonPrimitive(sessionKey)) }.toString(),
|
||||
)
|
||||
@@ -420,7 +391,7 @@ class ChatController internal constructor(
|
||||
put("includeUnknown", JsonPrimitive(false))
|
||||
if (limit != null && limit > 0) put("limit", JsonPrimitive(limit))
|
||||
}
|
||||
val res = requestGateway("sessions.list", params.toString())
|
||||
val res = session.request("sessions.list", params.toString())
|
||||
_sessions.value = parseSessions(res)
|
||||
} catch (_: Throwable) {
|
||||
// best-effort
|
||||
@@ -437,7 +408,7 @@ class ChatController internal constructor(
|
||||
if (!force && last != null && now - last < 10_000) return
|
||||
lastHealthPollAtMs = now
|
||||
try {
|
||||
requestGateway("health", null)
|
||||
session.request("health", null)
|
||||
_healthOk.value = true
|
||||
} catch (_: Throwable) {
|
||||
_healthOk.value = false
|
||||
@@ -480,7 +451,7 @@ class ChatController internal constructor(
|
||||
val currentSessionKey = _sessionKey.value
|
||||
val currentGeneration = historyLoadGeneration.get()
|
||||
val historyJson =
|
||||
requestGateway(
|
||||
session.request(
|
||||
"chat.history",
|
||||
buildJsonObject { put("sessionKey", JsonPrimitive(currentSessionKey)) }.toString(),
|
||||
)
|
||||
@@ -538,7 +509,8 @@ class ChatController internal constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseEventSessionEntry(payload: JsonObject): ChatSessionEntry? = payload["session"].asObjectOrNull()?.let(::parseSessionEntry) ?: parseSessionEntry(payload)
|
||||
private fun parseEventSessionEntry(payload: JsonObject): ChatSessionEntry? =
|
||||
payload["session"].asObjectOrNull()?.let(::parseSessionEntry) ?: parseSessionEntry(payload)
|
||||
|
||||
private fun handleAgentEvent(payloadJson: String) {
|
||||
val payload = json.parseToJsonElement(payloadJson).asObjectOrNull() ?: return
|
||||
@@ -660,45 +632,6 @@ class ChatController internal constructor(
|
||||
optimisticMessagesByRunId.entries.removeAll { entry -> entry.value !in retained }
|
||||
}
|
||||
|
||||
private fun refreshCurrentHistoryBestEffort() {
|
||||
scope.launch {
|
||||
try {
|
||||
val currentSessionKey = _sessionKey.value
|
||||
val currentGeneration = historyLoadGeneration.get()
|
||||
val historyJson =
|
||||
requestGateway(
|
||||
"chat.history",
|
||||
buildJsonObject { put("sessionKey", JsonPrimitive(currentSessionKey)) }.toString(),
|
||||
)
|
||||
if (
|
||||
!isCurrentHistoryLoad(
|
||||
currentSessionKey,
|
||||
_sessionKey.value,
|
||||
currentGeneration,
|
||||
historyLoadGeneration.get(),
|
||||
)
|
||||
) {
|
||||
return@launch
|
||||
}
|
||||
val history =
|
||||
parseHistory(
|
||||
historyJson,
|
||||
sessionKey = currentSessionKey,
|
||||
previousMessages = _messages.value,
|
||||
)
|
||||
prunePersistedOptimisticMessages(history.messages)
|
||||
_messages.value = mergeOptimisticMessages(incoming = history.messages, optimistic = optimisticMessagesByRunId.values)
|
||||
_sessionId.value = history.sessionId
|
||||
history.thinkingLevel
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?.let { _thinkingLevel.value = it }
|
||||
} catch (_: Throwable) {
|
||||
// best-effort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseHistory(
|
||||
historyJson: String,
|
||||
sessionKey: String,
|
||||
@@ -746,16 +679,9 @@ class ChatController internal constructor(
|
||||
): ChatSessionEntry? {
|
||||
if (obj == null) return null
|
||||
val key =
|
||||
obj["key"]
|
||||
.asStringOrNull()
|
||||
?.trim()
|
||||
.orEmpty()
|
||||
.ifEmpty {
|
||||
obj["sessionKey"]
|
||||
.asStringOrNull()
|
||||
?.trim()
|
||||
.orEmpty()
|
||||
}.ifEmpty { fallbackKey?.trim().orEmpty() }
|
||||
obj["key"].asStringOrNull()?.trim().orEmpty()
|
||||
.ifEmpty { obj["sessionKey"].asStringOrNull()?.trim().orEmpty() }
|
||||
.ifEmpty { fallbackKey?.trim().orEmpty() }
|
||||
if (key.isEmpty()) return null
|
||||
return ChatSessionEntry(
|
||||
key = key,
|
||||
@@ -802,6 +728,17 @@ class ChatController internal constructor(
|
||||
_sessions.value = _sessions.value.filterNot { it.key == key }
|
||||
}
|
||||
|
||||
private fun parseRunId(resJson: String): String? =
|
||||
try {
|
||||
json
|
||||
.parseToJsonElement(resJson)
|
||||
.asObjectOrNull()
|
||||
?.get("runId")
|
||||
.asStringOrNull()
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
|
||||
private fun normalizeThinking(raw: String): String =
|
||||
when (raw.trim().lowercase()) {
|
||||
"low" -> "low"
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
package ai.openclaw.app.gateway
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
|
||||
internal data class ChatSendAck(
|
||||
val runId: String?,
|
||||
val status: String?,
|
||||
) {
|
||||
val normalizedStatus: String
|
||||
get() = status?.trim()?.lowercase().orEmpty()
|
||||
|
||||
val isTerminalSuccess: Boolean
|
||||
get() = normalizedStatus == "ok"
|
||||
|
||||
val isTerminalFailure: Boolean
|
||||
get() = normalizedStatus == "timeout" || normalizedStatus == "error"
|
||||
|
||||
val isTerminal: Boolean
|
||||
get() = isTerminalSuccess || isTerminalFailure
|
||||
}
|
||||
|
||||
internal fun chatSendAckHistorySinceSeconds(
|
||||
ack: ChatSendAck,
|
||||
startedAtSeconds: Double,
|
||||
): Double? = if (ack.isTerminalSuccess) null else startedAtSeconds
|
||||
|
||||
internal fun parseChatSendAck(
|
||||
json: Json,
|
||||
responseJson: String,
|
||||
): ChatSendAck =
|
||||
try {
|
||||
val obj = json.parseToJsonElement(responseJson).asObjectOrNull()
|
||||
ChatSendAck(
|
||||
runId = obj?.get("runId").asStringOrNull(),
|
||||
status = obj?.get("status").asStringOrNull(),
|
||||
)
|
||||
} catch (_: Throwable) {
|
||||
ChatSendAck(runId = null, status = null)
|
||||
}
|
||||
|
||||
private fun JsonElement?.asObjectOrNull(): JsonObject? = this as? JsonObject
|
||||
|
||||
private fun JsonElement?.asStringOrNull(): String? = (this as? JsonPrimitive)?.takeIf { it.isString }?.content
|
||||
@@ -1,5 +1,6 @@
|
||||
package ai.openclaw.app.gateway
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.DnsResolver
|
||||
@@ -11,7 +12,6 @@ import android.net.nsd.NsdServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.CancellationSignal
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -49,8 +49,18 @@ import java.util.concurrent.Executors
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
private fun createDnsResolver(context: Context): DnsResolver =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN) {
|
||||
createContextDnsResolver(context)
|
||||
} else {
|
||||
createLegacyDnsResolver()
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.CINNAMON_BUN)
|
||||
private fun createContextDnsResolver(context: Context): DnsResolver = DnsResolver(context, null)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun createDnsResolver(): DnsResolver = DnsResolver.getInstance()
|
||||
private fun createLegacyDnsResolver(): DnsResolver = DnsResolver.getInstance()
|
||||
|
||||
/**
|
||||
* Watches local DNS-SD and optional wide-area DNS-SD for reachable OpenClaw gateways.
|
||||
@@ -61,7 +71,7 @@ class GatewayDiscovery(
|
||||
) {
|
||||
private val nsd = context.getSystemService(NsdManager::class.java)
|
||||
private val connectivity = context.getSystemService(ConnectivityManager::class.java)
|
||||
private val dns = createDnsResolver()
|
||||
private val dns = createDnsResolver(context)
|
||||
private val serviceType = "_openclaw-gw._tcp."
|
||||
private val wideAreaDomain = System.getenv("OPENCLAW_WIDE_AREA_DOMAIN")
|
||||
private val logTag = "OpenClaw/GatewayDiscovery"
|
||||
@@ -156,6 +166,14 @@ class GatewayDiscovery(
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopLocalDiscovery() {
|
||||
try {
|
||||
nsd.stopServiceDiscovery(discoveryListener)
|
||||
} catch (_: Throwable) {
|
||||
// ignore (best-effort)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startUnicastDiscovery(domain: String) {
|
||||
unicastJob =
|
||||
scope.launch(Dispatchers.IO) {
|
||||
@@ -179,7 +197,7 @@ class GatewayDiscovery(
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
private fun resolveWithServiceInfoCallback(serviceInfo: NsdServiceInfo) {
|
||||
val serviceName = BonjourEscapes.decode(serviceInfo.serviceName)
|
||||
val id = stableId(serviceName, "local.")
|
||||
|
||||
@@ -260,6 +260,24 @@ class GatewaySession(
|
||||
currentConnection?.closeQuietly()
|
||||
}
|
||||
|
||||
fun currentCanvasHostUrl(): String? = pluginSurfaceUrls["canvas"]
|
||||
|
||||
/** Refreshes the canvas plugin surface URL and caches the normalized Android-reachable URL. */
|
||||
suspend fun refreshCanvasHostUrl(timeoutMs: Long = 8_000): String? {
|
||||
val refreshed =
|
||||
refreshPluginSurfaceUrl(
|
||||
method = "node.pluginSurface.refresh",
|
||||
params = buildJsonObject { put("surface", JsonPrimitive("canvas")) },
|
||||
timeoutMs = timeoutMs,
|
||||
)
|
||||
if (!refreshed.isNullOrBlank()) {
|
||||
pluginSurfaceUrls = pluginSurfaceUrls + ("canvas" to refreshed)
|
||||
}
|
||||
return refreshed
|
||||
}
|
||||
|
||||
fun currentMainSessionKey(): String? = mainSessionKey
|
||||
|
||||
/** Sends a best-effort node.event and returns false instead of throwing on failure. */
|
||||
suspend fun sendNodeEvent(
|
||||
event: String,
|
||||
@@ -279,6 +297,28 @@ class GatewaySession(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun refreshPluginSurfaceUrl(
|
||||
method: String,
|
||||
params: JsonElement?,
|
||||
timeoutMs: Long,
|
||||
): String? {
|
||||
val conn = currentConnection ?: return null
|
||||
return try {
|
||||
val res = conn.request(method, params, timeoutMs)
|
||||
if (!res.ok) return null
|
||||
val obj = res.payloadJson?.let { json.parseToJsonElement(it).asObjectOrNull() } ?: return null
|
||||
val raw =
|
||||
obj["pluginSurfaceUrls"]
|
||||
.asObjectOrNull()
|
||||
?.get("canvas")
|
||||
.asStringOrNull()
|
||||
normalizeCanvasHostUrl(raw, conn.endpoint, isTlsConnection = conn.tls != null)
|
||||
} catch (err: Throwable) {
|
||||
Log.d("OpenClawGateway", "$method failed: ${err.message ?: err::class.java.simpleName}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/** Sends node.event and preserves the gateway RPC error shape for callers that need diagnostics. */
|
||||
suspend fun sendNodeEventDetailed(
|
||||
event: String,
|
||||
|
||||
@@ -97,6 +97,8 @@ class CanvasController {
|
||||
|
||||
fun currentUrl(): String? = url
|
||||
|
||||
fun isDefaultCanvas(): Boolean = url == null
|
||||
|
||||
fun setDebugStatusEnabled(enabled: Boolean) {
|
||||
debugStatusEnabled = enabled
|
||||
applyDebugStatus()
|
||||
@@ -203,6 +205,24 @@ class CanvasController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun snapshotPngBase64(maxWidth: Int?): String =
|
||||
withContext(Dispatchers.Main) {
|
||||
val wv = webView ?: throw IllegalStateException("no webview")
|
||||
val bmp = wv.captureBitmap()
|
||||
try {
|
||||
val scaled = bmp.scaleForMaxWidth(maxWidth)
|
||||
try {
|
||||
val out = ByteArrayOutputStream()
|
||||
scaled.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||
Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP)
|
||||
} finally {
|
||||
if (scaled !== bmp) scaled.recycle()
|
||||
}
|
||||
} finally {
|
||||
bmp.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
/** Captures the WebView as PNG/JPEG base64 with optional width and quality bounds. */
|
||||
suspend fun snapshotBase64(
|
||||
format: SnapshotFormat,
|
||||
|
||||
@@ -4,7 +4,6 @@ import ai.openclaw.app.BuildConfig
|
||||
import ai.openclaw.app.SensitiveFeatureConfig
|
||||
import ai.openclaw.app.gateway.GatewaySession
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -64,7 +63,7 @@ private class AndroidDeviceAppSource(
|
||||
|
||||
val appInfos =
|
||||
if (includeNonLaunchable) {
|
||||
visibleInstalledApplications(packageManager)
|
||||
packageManager.getInstalledApplications(PackageManager.MATCH_ALL)
|
||||
} else {
|
||||
launchablePackages.mapNotNull { packageName ->
|
||||
runCatching { packageManager.getApplicationInfo(packageName, 0) }.getOrNull()
|
||||
@@ -91,13 +90,6 @@ private class AndroidDeviceAppSource(
|
||||
.sortedWith(compareBy<DeviceAppEntry> { it.label.lowercase() }.thenBy { it.packageName })
|
||||
.toList()
|
||||
}
|
||||
|
||||
@SuppressLint("QueryPermissionsNeeded")
|
||||
private fun visibleInstalledApplications(packageManager: PackageManager): List<ApplicationInfo> {
|
||||
// Android package visibility intentionally bounds this result to packages the app can see.
|
||||
// OpenClaw should not request QUERY_ALL_PACKAGES for this optional device-context surface.
|
||||
return packageManager.getInstalledApplications(PackageManager.MATCH_ALL)
|
||||
}
|
||||
}
|
||||
|
||||
private data class DeviceAppsRequest(
|
||||
|
||||
@@ -109,3 +109,6 @@ fun normalizeMainKey(raw: String?): String? {
|
||||
val trimmed = raw?.trim().orEmpty()
|
||||
return if (trimmed.isEmpty()) null else trimmed
|
||||
}
|
||||
|
||||
/** Returns true only for the canonical main-session key understood by gateway UI. */
|
||||
fun isCanonicalMainSessionKey(key: String): Boolean = key == "main"
|
||||
|
||||
@@ -114,12 +114,16 @@ private fun ConnectScene() {
|
||||
|
||||
@Composable
|
||||
private fun ChatScene() {
|
||||
ChatBubble(label = "You", text = "Hi Molty, are you there?")
|
||||
ChatBubble(label = "You", text = "Summarize the launch checklist before I start the release.")
|
||||
ChatBubble(
|
||||
label = "Molty",
|
||||
text = "Always. Lurking in the shadows, exfoliating.",
|
||||
label = "OpenClaw",
|
||||
text = "Android archive, Play metadata, and internal testing upload are ready. Screenshots are being refreshed now.",
|
||||
raised = true,
|
||||
)
|
||||
CompactList(
|
||||
title = "Working set",
|
||||
rows = listOf("Release notes", "Play bundle", "Device screenshots"),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -5,7 +5,6 @@ import ai.openclaw.app.GatewayModelSummary
|
||||
import ai.openclaw.app.MainViewModel
|
||||
import ai.openclaw.app.ui.design.ClawEmptyState
|
||||
import ai.openclaw.app.ui.design.ClawPanel
|
||||
import ai.openclaw.app.ui.design.ClawPlainIconButton
|
||||
import ai.openclaw.app.ui.design.ClawScaffold
|
||||
import ai.openclaw.app.ui.design.ClawSeparatedColumn
|
||||
import ai.openclaw.app.ui.design.ClawTextField
|
||||
@@ -95,11 +94,7 @@ internal fun CommandPalette(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(9.dp),
|
||||
) {
|
||||
ClawPlainIconButton(
|
||||
icon = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Close search",
|
||||
onClick = onDismiss,
|
||||
)
|
||||
CommandIconButton(icon = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Close search", onClick = onDismiss)
|
||||
Text(text = "Search", style = ClawTheme.type.title, color = ClawTheme.colors.text, modifier = Modifier.weight(1f), textAlign = TextAlign.Center)
|
||||
CommandAvatar(text = "OC")
|
||||
}
|
||||
@@ -267,6 +262,19 @@ private fun CommandSessionListRow(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CommandIconButton(
|
||||
icon: ImageVector,
|
||||
contentDescription: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Surface(onClick = onClick, modifier = Modifier.size(ClawTheme.spacing.touchTarget), shape = CircleShape, color = Color.Transparent, contentColor = ClawTheme.colors.text) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Icon(imageVector = icon, contentDescription = contentDescription, modifier = Modifier.size(18.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CommandAvatar(text: String) {
|
||||
Surface(
|
||||
|
||||
@@ -5,7 +5,8 @@ import ai.openclaw.app.GatewayDreamingSummary
|
||||
import ai.openclaw.app.MainViewModel
|
||||
import ai.openclaw.app.ui.design.ClawPanel
|
||||
import ai.openclaw.app.ui.design.ClawSecondaryButton
|
||||
import ai.openclaw.app.ui.design.ClawStatusRow
|
||||
import ai.openclaw.app.ui.design.ClawStatus
|
||||
import ai.openclaw.app.ui.design.ClawStatusPill
|
||||
import ai.openclaw.app.ui.design.ClawTheme
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -91,19 +92,19 @@ private fun DreamingPanel(summary: GatewayDreamingSummary) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
ClawPanel(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
|
||||
Column {
|
||||
ClawStatusRow(
|
||||
DreamingHealthRow(
|
||||
title = "Memory Store",
|
||||
value = if (summary.storeHealthy) "Healthy" else "Needs attention",
|
||||
healthy = summary.storeHealthy,
|
||||
)
|
||||
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
|
||||
ClawStatusRow(
|
||||
DreamingHealthRow(
|
||||
title = "Signal Index",
|
||||
value = if (summary.phaseSignalHealthy) "Healthy" else "Needs attention",
|
||||
healthy = summary.phaseSignalHealthy,
|
||||
)
|
||||
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
|
||||
ClawStatusRow(
|
||||
DreamingHealthRow(
|
||||
title = "Promoted",
|
||||
value = "${summary.promotedToday} today · ${summary.promotedTotal} total",
|
||||
healthy = true,
|
||||
@@ -114,6 +115,23 @@ private fun DreamingPanel(summary: GatewayDreamingSummary) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DreamingHealthRow(
|
||||
title: String,
|
||||
value: String,
|
||||
healthy: Boolean,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 7.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(9.dp),
|
||||
) {
|
||||
Box(modifier = Modifier.size(7.dp))
|
||||
Text(text = title, style = ClawTheme.type.body, color = ClawTheme.colors.text, modifier = Modifier.weight(1f), maxLines = 1)
|
||||
ClawStatusPill(text = value, status = if (healthy) ClawStatus.Success else ClawStatus.Warning)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DreamDiaryPanel(summary: GatewayDreamingSummary) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
|
||||
@@ -206,6 +206,9 @@ internal fun decodeGatewaySetupCode(rawInput: String): GatewaySetupCode? {
|
||||
}
|
||||
}
|
||||
|
||||
/** Extracts a setup code from QR scanner text when the embedded endpoint is valid. */
|
||||
internal fun resolveScannedSetupCode(rawInput: String): String? = resolveScannedSetupCodeResult(rawInput).setupCode
|
||||
|
||||
/** Resolves QR scanner text to setup-code or validation error for UI copy. */
|
||||
internal fun resolveScannedSetupCodeResult(rawInput: String): GatewayScannedSetupCodeResult {
|
||||
val setupCode =
|
||||
|
||||
@@ -7,10 +7,7 @@ import ai.openclaw.app.ui.design.ClawPanel
|
||||
import ai.openclaw.app.ui.design.ClawSecondaryButton
|
||||
import ai.openclaw.app.ui.design.ClawStatus
|
||||
import ai.openclaw.app.ui.design.ClawStatusPill
|
||||
import ai.openclaw.app.ui.design.ClawStatusRow
|
||||
import ai.openclaw.app.ui.design.ClawTheme
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -18,18 +15,13 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -51,7 +43,6 @@ internal fun HealthLogsSettingsScreen(
|
||||
val logsSummary by viewModel.healthLogsSummary.collectAsState()
|
||||
val logsRefreshing by viewModel.healthLogsRefreshing.collectAsState()
|
||||
val logsErrorText by viewModel.healthLogsErrorText.collectAsState()
|
||||
var selectedLogEntry by remember { mutableStateOf<GatewayLogEntry?>(null) }
|
||||
|
||||
LaunchedEffect(isConnected) {
|
||||
if (isConnected) {
|
||||
@@ -61,11 +52,6 @@ internal fun HealthLogsSettingsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
selectedLogEntry?.let { entry ->
|
||||
GatewayLogDetailSettingsScreen(entry = entry, onBack = { selectedLogEntry = null })
|
||||
return
|
||||
}
|
||||
|
||||
SettingsDetailFrame(
|
||||
title = "Health",
|
||||
subtitle = "Gateway status, phone node readiness, and recent log stream.",
|
||||
@@ -107,46 +93,7 @@ internal fun HealthLogsSettingsScreen(
|
||||
Text(text = error, style = ClawTheme.type.body, color = ClawTheme.colors.warning)
|
||||
}
|
||||
}
|
||||
GatewayLogsPanel(isConnected = isConnected, summary = logsSummary, onLogClick = { selectedLogEntry = it })
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GatewayLogDetailSettingsScreen(
|
||||
entry: GatewayLogEntry,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
BackHandler(onBack = onBack)
|
||||
SettingsDetailFrame(
|
||||
title = "Log Entry",
|
||||
subtitle = "Readable gateway log detail.",
|
||||
icon = Icons.Default.Settings,
|
||||
onBack = onBack,
|
||||
) {
|
||||
SettingsMetricPanel(
|
||||
rows =
|
||||
listOf(
|
||||
SettingsMetric("Time", compactLogTime(entry.time)),
|
||||
SettingsMetric("Level", entry.level?.uppercase() ?: "LOG"),
|
||||
SettingsMetric("Subsystem", entry.subsystem ?: "Unknown"),
|
||||
),
|
||||
)
|
||||
ClawPanel {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text(text = "Message", style = ClawTheme.type.section, color = ClawTheme.colors.text)
|
||||
Text(text = entry.message, style = ClawTheme.type.body, color = ClawTheme.colors.text)
|
||||
}
|
||||
}
|
||||
ClawPanel {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text(text = "Raw", style = ClawTheme.type.section, color = ClawTheme.colors.text)
|
||||
Text(
|
||||
text = entry.raw.take(4_000),
|
||||
style = ClawTheme.type.caption,
|
||||
color = ClawTheme.colors.textMuted,
|
||||
)
|
||||
}
|
||||
}
|
||||
GatewayLogsPanel(isConnected = isConnected, summary = logsSummary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,26 +113,41 @@ private fun HealthStatusPanel(
|
||||
) {
|
||||
ClawPanel(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
|
||||
Column {
|
||||
ClawStatusRow(title = "Gateway", value = gateway, healthy = isConnected)
|
||||
HealthStatusRow(title = "Gateway", value = gateway, healthy = isConnected)
|
||||
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
|
||||
ClawStatusRow(title = "Phone Node", value = node, healthy = isNodeConnected)
|
||||
HealthStatusRow(title = "Phone Node", value = node, healthy = isNodeConnected)
|
||||
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
|
||||
ClawStatusRow(title = "Chat", value = chat, healthy = chatHealthOk)
|
||||
HealthStatusRow(title = "Chat", value = chat, healthy = chatHealthOk)
|
||||
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
|
||||
ClawStatusRow(title = "Models", value = models, healthy = modelsReady)
|
||||
HealthStatusRow(title = "Models", value = models, healthy = modelsReady)
|
||||
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
|
||||
ClawStatusRow(title = "Voice", value = voice, healthy = voiceReady)
|
||||
HealthStatusRow(title = "Voice", value = voice, healthy = voiceReady)
|
||||
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
|
||||
ClawStatusRow(title = "Runs", value = runs, healthy = true)
|
||||
HealthStatusRow(title = "Runs", value = runs, healthy = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HealthStatusRow(
|
||||
title: String,
|
||||
value: String,
|
||||
healthy: Boolean,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 7.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(9.dp),
|
||||
) {
|
||||
Text(text = title, style = ClawTheme.type.body, color = ClawTheme.colors.text, modifier = Modifier.weight(1f), maxLines = 1)
|
||||
ClawStatusPill(text = value, status = if (healthy) ClawStatus.Success else ClawStatus.Warning)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GatewayLogsPanel(
|
||||
isConnected: Boolean,
|
||||
summary: GatewayHealthLogsSummary,
|
||||
onLogClick: (GatewayLogEntry) -> Unit,
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
||||
@@ -208,7 +170,7 @@ private fun GatewayLogsPanel(
|
||||
val entries = summary.entries.takeLast(12)
|
||||
Column {
|
||||
entries.forEachIndexed { index, entry ->
|
||||
GatewayLogRow(entry = entry, onClick = { onLogClick(entry) })
|
||||
GatewayLogRow(entry = entry)
|
||||
if (index != entries.lastIndex) {
|
||||
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
|
||||
}
|
||||
@@ -223,16 +185,9 @@ private fun GatewayLogsPanel(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GatewayLogRow(
|
||||
entry: GatewayLogEntry,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
private fun GatewayLogRow(entry: GatewayLogEntry) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClickLabel = "Open log entry", onClick = onClick)
|
||||
.padding(horizontal = 10.dp, vertical = 7.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 7.dp),
|
||||
verticalAlignment = Alignment.Top,
|
||||
horizontalArrangement = Arrangement.spacedBy(9.dp),
|
||||
) {
|
||||
@@ -244,11 +199,6 @@ private fun GatewayLogRow(
|
||||
}
|
||||
}
|
||||
ClawStatusPill(text = entry.level?.uppercase() ?: "LOG", status = logLevelStatus(entry.level))
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||
contentDescription = null,
|
||||
tint = ClawTheme.colors.textSubtle,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1378,12 +1378,7 @@ private fun rememberPermissionState(
|
||||
photosGranted = permissions[photosPermission] ?: photosGranted
|
||||
contactsGranted = permissions[Manifest.permission.READ_CONTACTS] ?: contactsGranted
|
||||
calendarGranted = permissions[Manifest.permission.READ_CALENDAR] ?: calendarGranted
|
||||
notificationsGranted =
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
permissions[Manifest.permission.POST_NOTIFICATIONS] ?: notificationsGranted
|
||||
} else {
|
||||
true
|
||||
}
|
||||
notificationsGranted = permissions[Manifest.permission.POST_NOTIFICATIONS] ?: notificationsGranted
|
||||
motionGranted = permissions[Manifest.permission.ACTIVITY_RECOGNITION] ?: motionGranted
|
||||
smsGranted =
|
||||
(permissions[Manifest.permission.SEND_SMS] ?: smsGranted) &&
|
||||
|
||||
@@ -9,10 +9,14 @@ import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.WindowCompat
|
||||
|
||||
private val LocalOpenClawDarkTheme = staticCompositionLocalOf { true }
|
||||
|
||||
/**
|
||||
* App theme wrapper that installs dynamic Material colors and legacy mobile color tokens.
|
||||
*/
|
||||
@@ -30,6 +34,7 @@ fun OpenClawTheme(
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalMobileColors provides mobileColors,
|
||||
LocalOpenClawDarkTheme provides isDark,
|
||||
) {
|
||||
MaterialTheme(colorScheme = colorScheme, content = content)
|
||||
}
|
||||
@@ -50,3 +55,21 @@ internal fun OpenClawSystemBarAppearance(lightAppearance: Boolean) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overlay background token tuned for panels floating over the mobile canvas.
|
||||
*/
|
||||
@Composable
|
||||
fun overlayContainerColor(): Color {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val isDark = LocalOpenClawDarkTheme.current
|
||||
val base = if (isDark) scheme.surfaceContainerLow else scheme.surfaceContainerHigh
|
||||
// Light mode keeps overlays away from pure-white glare on the app canvas.
|
||||
return if (isDark) base else base.copy(alpha = 0.88f)
|
||||
}
|
||||
|
||||
/**
|
||||
* Overlay icon token kept next to overlayContainerColor for callers outside the design package.
|
||||
*/
|
||||
@Composable
|
||||
fun overlayIconColor(): Color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
|
||||
@@ -2,7 +2,6 @@ package ai.openclaw.app.ui
|
||||
|
||||
import ai.openclaw.app.MainViewModel
|
||||
import ai.openclaw.app.ui.design.ClawEmptyState
|
||||
import ai.openclaw.app.ui.design.ClawPlainIconButton
|
||||
import ai.openclaw.app.ui.design.ClawPrimaryButton
|
||||
import ai.openclaw.app.ui.design.ClawScaffold
|
||||
import ai.openclaw.app.ui.design.ClawTheme
|
||||
@@ -56,7 +55,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
/** Session browser for recent and current chat sessions. */
|
||||
/** Session browser for recent and currently-live chat sessions. */
|
||||
@Composable
|
||||
internal fun SessionsScreen(
|
||||
viewModel: MainViewModel,
|
||||
@@ -74,7 +73,7 @@ internal fun SessionsScreen(
|
||||
.let { rows ->
|
||||
when (filter) {
|
||||
SessionFilter.Recent -> rows
|
||||
SessionFilter.Current -> rows.filter { it.key == chatSessionKey }
|
||||
SessionFilter.Live -> rows.filter { it.key == chatSessionKey }
|
||||
}
|
||||
}.let { rows ->
|
||||
if (recentFirst) {
|
||||
@@ -93,12 +92,12 @@ internal fun SessionsScreen(
|
||||
}
|
||||
|
||||
ClawScaffold(
|
||||
contentPadding = PaddingValues(start = 16.dp, top = 10.dp, end = 16.dp, bottom = 4.dp),
|
||||
contentPadding = PaddingValues(start = 20.dp, top = 14.dp, end = 20.dp, bottom = 6.dp),
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(9.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(7.dp),
|
||||
contentPadding = PaddingValues(bottom = 4.dp),
|
||||
) {
|
||||
item {
|
||||
@@ -107,16 +106,16 @@ internal fun SessionsScreen(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(text = "Sessions", style = ClawTheme.type.display.copy(fontSize = 24.sp, lineHeight = 28.sp), color = ClawTheme.colors.text, modifier = Modifier.weight(1f))
|
||||
ClawPlainIconButton(icon = Icons.Default.Search, contentDescription = "Search sessions", onClick = onOpenCommand)
|
||||
ClawPlainIconButton(icon = Icons.Default.SwapVert, contentDescription = "Reverse session sort", onClick = { recentFirst = !recentFirst })
|
||||
Text(text = "Sessions", style = ClawTheme.type.display.copy(fontSize = 17.4.sp, lineHeight = 21.sp), color = ClawTheme.colors.text, modifier = Modifier.weight(1f))
|
||||
SessionPlainIconButton(icon = Icons.Default.Search, contentDescription = "Search sessions", onClick = onOpenCommand)
|
||||
SessionPlainIconButton(icon = Icons.Default.SwapVert, contentDescription = "Reverse session sort", onClick = { recentFirst = !recentFirst })
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(5.dp)) {
|
||||
FilterPill(text = "Recent", icon = Icons.Outlined.AccessTime, active = filter == SessionFilter.Recent, onClick = { filter = SessionFilter.Recent })
|
||||
FilterPill(text = "Current", icon = Icons.Outlined.MicNone, active = filter == SessionFilter.Current, showDot = sessions.any { it.key == chatSessionKey }, onClick = { filter = SessionFilter.Current })
|
||||
FilterPill(text = "Live", icon = Icons.Outlined.MicNone, active = filter == SessionFilter.Live, live = sessions.any { it.key == chatSessionKey }, onClick = { filter = SessionFilter.Live })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +179,7 @@ private fun FilterPill(
|
||||
text: String,
|
||||
icon: ImageVector? = null,
|
||||
active: Boolean = false,
|
||||
showDot: Boolean = false,
|
||||
live: Boolean = false,
|
||||
dropdown: Boolean = false,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
@@ -199,7 +198,7 @@ private fun FilterPill(
|
||||
) {
|
||||
icon?.let { Icon(imageVector = it, contentDescription = null, modifier = Modifier.size(12.dp), tint = ClawTheme.colors.text) }
|
||||
Text(text = text, style = ClawTheme.type.label, color = ClawTheme.colors.text, maxLines = 1)
|
||||
if (showDot) {
|
||||
if (live) {
|
||||
Box(modifier = Modifier.size(4.dp).clip(CircleShape).background(ClawTheme.colors.success))
|
||||
}
|
||||
if (dropdown) {
|
||||
@@ -259,7 +258,7 @@ private fun SessionRow(
|
||||
Text(text = subtitle, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
SessionMiniTag(text = "Workspace")
|
||||
SessionMiniTag(text = if (active) "Current" else "OpenClaw")
|
||||
SessionMiniTag(text = if (active) "Active" else "OpenClaw")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -274,6 +273,19 @@ private fun SessionRow(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SessionPlainIconButton(
|
||||
icon: ImageVector,
|
||||
contentDescription: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Surface(onClick = onClick, modifier = Modifier.size(ClawTheme.spacing.touchTarget), shape = CircleShape, color = Color.Transparent, contentColor = ClawTheme.colors.text) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Icon(imageVector = icon, contentDescription = contentDescription, modifier = Modifier.size(18.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SessionOutlineIconButton(
|
||||
icon: ImageVector,
|
||||
@@ -308,21 +320,21 @@ private fun SessionMiniTag(text: String) {
|
||||
|
||||
private enum class SessionFilter {
|
||||
Recent,
|
||||
Current,
|
||||
Live,
|
||||
}
|
||||
|
||||
/** Empty-state title selected by the active session browser filter. */
|
||||
private fun emptySessionTitle(filter: SessionFilter): String =
|
||||
when (filter) {
|
||||
SessionFilter.Recent -> "No sessions yet"
|
||||
SessionFilter.Current -> "No current session"
|
||||
SessionFilter.Live -> "No live session"
|
||||
}
|
||||
|
||||
/** Empty-state body selected by the active session browser filter. */
|
||||
private fun emptySessionBody(filter: SessionFilter): String =
|
||||
when (filter) {
|
||||
SessionFilter.Recent -> "Start a new conversation and it will show up here."
|
||||
SessionFilter.Current -> "Open Chat to start or resume the current session."
|
||||
SessionFilter.Live -> "Open Chat to start or resume the current session."
|
||||
}
|
||||
|
||||
/** Formats session timestamps for compact mobile metadata. */
|
||||
|
||||
@@ -4,7 +4,6 @@ import ai.openclaw.app.AppearanceThemeMode
|
||||
import ai.openclaw.app.BuildConfig
|
||||
import ai.openclaw.app.GatewayAgentSummary
|
||||
import ai.openclaw.app.GatewayCronJobSummary
|
||||
import ai.openclaw.app.GatewayExecApprovalSummary
|
||||
import ai.openclaw.app.GatewayUsageProviderSummary
|
||||
import ai.openclaw.app.LocationMode
|
||||
import ai.openclaw.app.MainViewModel
|
||||
@@ -15,7 +14,6 @@ import ai.openclaw.app.ui.design.ClawDetailRow
|
||||
import ai.openclaw.app.ui.design.ClawIconBadge
|
||||
import ai.openclaw.app.ui.design.ClawListPanel
|
||||
import ai.openclaw.app.ui.design.ClawPanel
|
||||
import ai.openclaw.app.ui.design.ClawPlainIconButton
|
||||
import ai.openclaw.app.ui.design.ClawPrimaryButton
|
||||
import ai.openclaw.app.ui.design.ClawScaffold
|
||||
import ai.openclaw.app.ui.design.ClawSecondaryButton
|
||||
@@ -92,6 +90,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -107,7 +106,6 @@ internal enum class SettingsRoute {
|
||||
Profile,
|
||||
Voice,
|
||||
Agents,
|
||||
ProvidersModels,
|
||||
Approvals,
|
||||
CronJobs,
|
||||
Usage,
|
||||
@@ -138,7 +136,6 @@ internal fun SettingsDetailScreen(
|
||||
SettingsRoute.Profile -> ProfileSettingsScreen(viewModel = viewModel, onBack = onBack)
|
||||
SettingsRoute.Voice -> VoiceSettingsScreen(viewModel = viewModel, onBack = onBack)
|
||||
SettingsRoute.Agents -> AgentsSettingsScreen(viewModel = viewModel, onBack = onBack)
|
||||
SettingsRoute.ProvidersModels -> ProvidersModelsScreen(viewModel = viewModel, onBack = onBack)
|
||||
SettingsRoute.Approvals -> ApprovalsSettingsScreen(viewModel = viewModel, onBack = onBack)
|
||||
SettingsRoute.CronJobs -> CronJobsSettingsScreen(viewModel = viewModel, onBack = onBack)
|
||||
SettingsRoute.Usage -> UsageSettingsScreen(viewModel = viewModel, onBack = onBack)
|
||||
@@ -302,62 +299,29 @@ private fun ApprovalsSettingsScreen(
|
||||
viewModel: MainViewModel,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
val isConnected by viewModel.isConnected.collectAsState()
|
||||
val execApprovals by viewModel.execApprovals.collectAsState()
|
||||
val execApprovalsRefreshing by viewModel.execApprovalsRefreshing.collectAsState()
|
||||
val execApprovalsErrorText by viewModel.execApprovalsErrorText.collectAsState()
|
||||
val pendingToolCalls by viewModel.chatPendingToolCalls.collectAsState()
|
||||
val pendingRunCount by viewModel.pendingRunCount.collectAsState()
|
||||
val issueCount = execApprovals.count { it.errorText != null } + pendingToolCalls.count { it.isError == true }
|
||||
|
||||
LaunchedEffect(isConnected) {
|
||||
if (isConnected) {
|
||||
viewModel.refreshExecApprovals()
|
||||
}
|
||||
}
|
||||
val waitingCount = pendingToolCalls.count { it.isError != true }
|
||||
val issueCount = pendingToolCalls.count { it.isError == true }
|
||||
|
||||
SettingsDetailFrame(title = "Approvals", subtitle = "Review actions that need your attention.", icon = Icons.Default.Lock, onBack = onBack) {
|
||||
SettingsMetricPanel(
|
||||
rows =
|
||||
listOf(
|
||||
SettingsMetric("Gateway Pending", execApprovals.size.toString()),
|
||||
SettingsMetric("Session Activity", pendingToolCalls.size.toString()),
|
||||
SettingsMetric("Pending", waitingCount.toString()),
|
||||
SettingsMetric("Issues", issueCount.toString()),
|
||||
SettingsMetric("Active Runs", pendingRunCount.toString()),
|
||||
),
|
||||
)
|
||||
ClawSecondaryButton(
|
||||
text = if (execApprovalsRefreshing) "Refreshing" else "Refresh",
|
||||
onClick = viewModel::refreshExecApprovals,
|
||||
enabled = isConnected && !execApprovalsRefreshing,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
if (execApprovalsErrorText != null) {
|
||||
ClawPanel {
|
||||
Text(text = execApprovalsErrorText ?: "", style = ClawTheme.type.body, color = ClawTheme.colors.warning)
|
||||
}
|
||||
}
|
||||
if (!isConnected) {
|
||||
if (pendingToolCalls.isEmpty()) {
|
||||
ClawPanel {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(3.dp)) {
|
||||
Text(text = "Gateway disconnected.", style = ClawTheme.type.section, color = ClawTheme.colors.text)
|
||||
Text(text = "Connect the gateway to load approval requests in the app.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
|
||||
}
|
||||
}
|
||||
} else if (execApprovals.isEmpty()) {
|
||||
ClawPanel {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(3.dp)) {
|
||||
Text(text = "No gateway approvals.", style = ClawTheme.type.section, color = ClawTheme.colors.text)
|
||||
Text(text = "Exec approval requests will appear here while this phone is connected.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
|
||||
Text(text = "Nothing needs approval.", style = ClawTheme.type.section, color = ClawTheme.colors.text)
|
||||
Text(text = "OpenClaw will show action requests here when a session pauses for review.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ExecApprovalsPanel(approvals = execApprovals, onResolve = viewModel::resolveExecApproval)
|
||||
}
|
||||
if (pendingToolCalls.isNotEmpty()) {
|
||||
Text(text = "Session activity", style = ClawTheme.type.section, color = ClawTheme.colors.text)
|
||||
Text(text = "Chat tool calls waiting in the active session remain visible here.", style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted)
|
||||
SessionToolCallsPanel(toolCalls = pendingToolCalls)
|
||||
ApprovalsPanel(toolCalls = pendingToolCalls)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -856,7 +820,6 @@ private fun GatewaySettingsScreen(
|
||||
var bootstrapTokenInput by remember { mutableStateOf("") }
|
||||
var passwordInput by remember { mutableStateOf("") }
|
||||
var validationText by remember { mutableStateOf<String?>(null) }
|
||||
var showSetupCodeHelp by remember { mutableStateOf(false) }
|
||||
|
||||
SettingsDetailFrame(title = "Gateway", subtitle = "Connection between this phone and OpenClaw.", icon = Icons.Default.Cloud, onBack = onBack) {
|
||||
SettingsMetricPanel(
|
||||
@@ -877,17 +840,7 @@ private fun GatewaySettingsScreen(
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Text(text = "Pair New Gateway", style = ClawTheme.type.section, color = ClawTheme.colors.text)
|
||||
Text(text = "Clear this phone's saved gateway access and scan a fresh setup code.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
ClawSecondaryButton(text = "Pair New Gateway", onClick = viewModel::pairNewGateway, modifier = Modifier.weight(1f), icon = Icons.Default.QrCode2)
|
||||
ClawSecondaryButton(text = "Setup Code", onClick = { showSetupCodeHelp = !showSetupCodeHelp }, modifier = Modifier.weight(1f), icon = Icons.Default.Info)
|
||||
}
|
||||
if (showSetupCodeHelp) {
|
||||
Text(
|
||||
text = "Android can scan or paste an existing setup code, but this gateway does not expose setup-code generation to the app yet. Generate the QR/code on the gateway host with openclaw qr, then scan it here or paste the setup code below.",
|
||||
style = ClawTheme.type.caption,
|
||||
color = ClawTheme.colors.textMuted,
|
||||
)
|
||||
}
|
||||
ClawSecondaryButton(text = "Pair New Gateway", onClick = viewModel::pairNewGateway, modifier = Modifier.fillMaxWidth(), icon = Icons.Default.QrCode2)
|
||||
}
|
||||
}
|
||||
ClawPanel {
|
||||
@@ -1108,11 +1061,7 @@ internal fun SettingsDetailFrame(
|
||||
LazyColumn(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(10.dp), contentPadding = PaddingValues(bottom = 4.dp)) {
|
||||
item {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(9.dp)) {
|
||||
ClawPlainIconButton(
|
||||
icon = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
onClick = onBack,
|
||||
)
|
||||
SettingsBackButton(onClick = onBack)
|
||||
Text(text = title, style = ClawTheme.type.title, color = ClawTheme.colors.text, modifier = Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
SettingsIconMark(icon = icon)
|
||||
}
|
||||
@@ -1149,70 +1098,7 @@ internal data class SettingsMetric(
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun ExecApprovalsPanel(
|
||||
approvals: List<GatewayExecApprovalSummary>,
|
||||
onResolve: (String, String) -> Unit,
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
approvals.forEach { approval ->
|
||||
ExecApprovalCard(approval = approval, onResolve = onResolve)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExecApprovalCard(
|
||||
approval: GatewayExecApprovalSummary,
|
||||
onResolve: (String, String) -> Unit,
|
||||
) {
|
||||
val resolving = approval.resolvingDecision != null
|
||||
ClawPanel {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(9.dp)) {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(9.dp)) {
|
||||
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) {
|
||||
Text(text = approval.commandText, style = ClawTheme.type.body, color = ClawTheme.colors.text, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
||||
approval.commandPreview?.let { preview ->
|
||||
Text(text = preview, style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
}
|
||||
ClawStatusPill(text = if (resolving) "Sending" else "Review", status = if (resolving) ClawStatus.Warning else ClawStatus.Success)
|
||||
}
|
||||
Text(text = execApprovalMetadata(approval), style = ClawTheme.type.caption, color = ClawTheme.colors.textSubtle, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
||||
approval.errorText?.let { errorText ->
|
||||
Text(text = errorText, style = ClawTheme.type.caption, color = ClawTheme.colors.warning)
|
||||
}
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
if ("allow-once" in approval.allowedDecisions) {
|
||||
ClawPrimaryButton(
|
||||
text = if (approval.resolvingDecision == "allow-once") "Allowing" else "Allow Once",
|
||||
onClick = { onResolve(approval.id, "allow-once") },
|
||||
enabled = !resolving,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
if ("allow-always" in approval.allowedDecisions) {
|
||||
ClawSecondaryButton(
|
||||
text = if (approval.resolvingDecision == "allow-always") "Saving" else "Always",
|
||||
onClick = { onResolve(approval.id, "allow-always") },
|
||||
enabled = !resolving,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
if ("deny" in approval.allowedDecisions) {
|
||||
ClawSecondaryButton(
|
||||
text = if (approval.resolvingDecision == "deny") "Denying" else "Deny",
|
||||
onClick = { onResolve(approval.id, "deny") },
|
||||
enabled = !resolving,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SessionToolCallsPanel(toolCalls: List<ChatPendingToolCall>) {
|
||||
private fun ApprovalsPanel(toolCalls: List<ChatPendingToolCall>) {
|
||||
ClawListPanel(items = toolCalls) { toolCall ->
|
||||
ApprovalListRow(toolCall = toolCall)
|
||||
}
|
||||
@@ -1345,30 +1231,6 @@ private fun approvalSubtitle(
|
||||
return if (minutes < 1) "Waiting for review" else "Waiting ${minutes}m"
|
||||
}
|
||||
|
||||
private fun execApprovalMetadata(approval: GatewayExecApprovalSummary): String {
|
||||
val target =
|
||||
when {
|
||||
approval.host == "node" && approval.nodeId != null -> "Node ${approval.nodeId.take(8)}"
|
||||
approval.host != null -> approval.host.replaceFirstChar { it.uppercaseChar() }
|
||||
else -> "Gateway"
|
||||
}
|
||||
val agent = approval.agentId?.let { "Agent ${it.take(8)}" }
|
||||
val age = approval.createdAtMs?.let { "Waiting ${formatApprovalDuration(System.currentTimeMillis() - it)}" }
|
||||
val expires = approval.expiresAtMs?.let { "Expires ${formatApprovalDuration(it - System.currentTimeMillis())}" }
|
||||
return listOfNotNull(target, agent, age, expires).joinToString(" · ")
|
||||
}
|
||||
|
||||
private fun formatApprovalDuration(deltaMs: Long): String {
|
||||
val safeDelta = deltaMs.coerceAtLeast(0L)
|
||||
val minutes = safeDelta / 60_000L
|
||||
val hours = minutes / 60L
|
||||
return when {
|
||||
minutes < 1 -> "soon"
|
||||
hours < 1 -> "${minutes}m"
|
||||
else -> "${hours}h"
|
||||
}
|
||||
}
|
||||
|
||||
/** Builds the dense cron-job subtitle from schedule, next wake, and prompt preview. */
|
||||
private fun cronJobSubtitle(job: GatewayCronJobSummary): String = "${job.scheduleLabel} · ${formatCronWake(job.nextRunAtMs)} · ${job.promptPreview}"
|
||||
|
||||
@@ -1532,6 +1394,15 @@ internal fun SettingsMetricPanel(rows: List<SettingsMetric>) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsBackButton(onClick: () -> Unit) {
|
||||
Surface(onClick = onClick, modifier = Modifier.size(ClawTheme.spacing.touchTarget), shape = CircleShape, color = Color.Transparent, contentColor = ClawTheme.colors.text) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", modifier = Modifier.size(18.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsIconMark(icon: ImageVector) {
|
||||
Surface(
|
||||
|
||||
@@ -1253,6 +1253,16 @@ private fun settingsPrimaryButtonColors() =
|
||||
disabledContentColor = Color.White.copy(alpha = 0.9f),
|
||||
)
|
||||
|
||||
/** Destructive button colors for permission and capability settings actions. */
|
||||
@Composable
|
||||
private fun settingsDangerButtonColors() =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = mobileDanger,
|
||||
contentColor = Color.White,
|
||||
disabledContainerColor = mobileDanger.copy(alpha = 0.45f),
|
||||
disabledContentColor = Color.White.copy(alpha = 0.9f),
|
||||
)
|
||||
|
||||
/** Opens this app's Android settings page for permissions that require system UI. */
|
||||
private fun openAppSettings(context: Context) {
|
||||
val intent =
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,24 +10,17 @@ import ai.openclaw.app.ui.design.ClawStatus
|
||||
import ai.openclaw.app.ui.design.ClawStatusPill
|
||||
import ai.openclaw.app.ui.design.ClawTextBadge
|
||||
import ai.openclaw.app.ui.design.ClawTheme
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@@ -44,7 +37,6 @@ internal fun SkillsSettingsScreen(
|
||||
val skills = skillsSummary.skills
|
||||
val readyCount = skills.count { skillReady(it) }
|
||||
val needsSetupCount = skills.count { skillNeedsSetup(it) }
|
||||
var selectedSkillKey by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(isConnected) {
|
||||
if (isConnected) {
|
||||
@@ -52,17 +44,6 @@ internal fun SkillsSettingsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
selectedSkillKey?.let { skillKey ->
|
||||
val selectedSkill = skills.firstOrNull { it.skillKey == skillKey }
|
||||
SkillDetailSettingsScreen(
|
||||
skill = selectedSkill,
|
||||
skillKey = skillKey,
|
||||
isConnected = isConnected,
|
||||
onBack = { selectedSkillKey = null },
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
SettingsDetailFrame(
|
||||
title = "Skills",
|
||||
subtitle = "Installed capabilities available to OpenClaw.",
|
||||
@@ -102,117 +83,25 @@ internal fun SkillsSettingsScreen(
|
||||
Text(text = "Skills installed on the gateway will appear here.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
|
||||
}
|
||||
}
|
||||
else -> SkillsPanel(skills = skills, onSkillClick = { selectedSkillKey = it.skillKey })
|
||||
else -> SkillsPanel(skills = skills)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SkillDetailSettingsScreen(
|
||||
skill: GatewaySkillSummary?,
|
||||
skillKey: String,
|
||||
isConnected: Boolean,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
BackHandler(onBack = onBack)
|
||||
|
||||
SettingsDetailFrame(
|
||||
title = skill?.name ?: skillKey,
|
||||
subtitle = "Inspect installed skill capability and setup state.",
|
||||
icon = Icons.Default.Settings,
|
||||
onBack = onBack,
|
||||
) {
|
||||
skill?.let { summary ->
|
||||
SettingsMetricPanel(
|
||||
rows =
|
||||
listOf(
|
||||
SettingsMetric("Status", skillStatusText(summary)),
|
||||
SettingsMetric("Source", skillSourceLabel(summary)),
|
||||
SettingsMetric("Missing", summary.missingCount.toString()),
|
||||
),
|
||||
)
|
||||
SkillSetupPanel(summary)
|
||||
}
|
||||
SkillDetailPanel(skill = skill, isConnected = isConnected)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SkillSetupPanel(skill: GatewaySkillSummary) {
|
||||
ClawPanel {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text(text = "Setup", style = ClawTheme.type.section, color = ClawTheme.colors.text)
|
||||
Text(text = skillConfigurationText(skill), style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SkillDetailPanel(
|
||||
skill: GatewaySkillSummary?,
|
||||
isConnected: Boolean,
|
||||
) {
|
||||
if (!isConnected) {
|
||||
ClawPanel {
|
||||
Text(text = "Connect the gateway to load skill details.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (skill == null) {
|
||||
ClawPanel {
|
||||
Text(text = "Skill detail is not available in the current skills status.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
|
||||
}
|
||||
return
|
||||
}
|
||||
SettingsMetricPanel(
|
||||
rows =
|
||||
listOf(
|
||||
SettingsMetric("Skill Key", skill.skillKey),
|
||||
SettingsMetric("Display", skill.name),
|
||||
SettingsMetric("Source", skillSourceLabel(skill)),
|
||||
SettingsMetric("Install Options", skill.installCount.toString()),
|
||||
),
|
||||
)
|
||||
skill.description?.let { description ->
|
||||
ClawPanel {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text(text = "Description", style = ClawTheme.type.section, color = ClawTheme.colors.text)
|
||||
Text(text = description, style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SkillsPanel(
|
||||
skills: List<GatewaySkillSummary>,
|
||||
onSkillClick: (GatewaySkillSummary) -> Unit,
|
||||
) {
|
||||
private fun SkillsPanel(skills: List<GatewaySkillSummary>) {
|
||||
ClawListPanel(items = skills) { skill ->
|
||||
SkillListRow(skill = skill, onClick = { onSkillClick(skill) })
|
||||
SkillListRow(skill = skill)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SkillListRow(
|
||||
skill: GatewaySkillSummary,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
private fun SkillListRow(skill: GatewaySkillSummary) {
|
||||
ClawDetailRow(
|
||||
title = skill.name,
|
||||
subtitle = skillSubtitle(skill),
|
||||
modifier = Modifier.clickable(onClickLabel = "Open skill detail", onClick = onClick),
|
||||
leading = { ClawTextBadge(text = skillBadge(skill)) },
|
||||
trailing = {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
ClawStatusPill(text = skillStatusText(skill), status = skillStatus(skill))
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||
contentDescription = null,
|
||||
tint = ClawTheme.colors.textSubtle,
|
||||
)
|
||||
}
|
||||
},
|
||||
trailing = { ClawStatusPill(text = skillStatusText(skill), status = skillStatus(skill)) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -246,15 +135,6 @@ private fun skillSubtitle(skill: GatewaySkillSummary): String {
|
||||
return listOfNotNull(skill.description, skillSourceLabel(skill), issue).joinToString(" · ")
|
||||
}
|
||||
|
||||
private fun skillConfigurationText(skill: GatewaySkillSummary): String =
|
||||
when {
|
||||
skill.disabled -> "This skill is disabled on the gateway. Android shows detail only; enable or configure it from desktop or CLI."
|
||||
skill.blockedByAllowlist -> "This skill is blocked by the gateway allowlist. Android can inspect it, but allowlist changes stay on desktop or CLI."
|
||||
skill.missingCount > 0 -> "This skill needs ${skill.missingCount} setup item(s). Android shows what is installed; setup/config changes stay on desktop or CLI."
|
||||
!skill.eligible -> "This skill is installed but not currently eligible to run. Use desktop or CLI for configuration changes."
|
||||
else -> "Ready on this gateway. Android detail is read-only; install, update, and configuration changes stay on desktop or CLI."
|
||||
}
|
||||
|
||||
private fun skillSourceLabel(skill: GatewaySkillSummary): String =
|
||||
when (skill.source) {
|
||||
"openclaw-bundled" -> if (skill.bundled) "Built-in" else "Bundled"
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package ai.openclaw.app.ui
|
||||
|
||||
import ai.openclaw.app.MainViewModel
|
||||
import ai.openclaw.app.R
|
||||
import ai.openclaw.app.VoiceCaptureMode
|
||||
import ai.openclaw.app.ui.design.ClawPanel
|
||||
import ai.openclaw.app.ui.design.ClawPlainIconButton
|
||||
import ai.openclaw.app.ui.design.ClawPrimaryButton
|
||||
import ai.openclaw.app.ui.design.ClawSecondaryButton
|
||||
import ai.openclaw.app.ui.design.ClawStatus
|
||||
@@ -70,7 +68,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -180,8 +177,8 @@ fun VoiceScreen(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(9.dp),
|
||||
.padding(horizontal = 20.dp, vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
VoiceHeader(
|
||||
statusText = voiceAttentionStatus ?: if (voiceActive || !gatewayReady) activeStatus else "Your voice command center.",
|
||||
@@ -270,12 +267,12 @@ private fun DictationScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(9.dp)) {
|
||||
ClawPlainIconButton(icon = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back to voice", onClick = onCancel)
|
||||
VoicePlainIconButton(icon = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back to voice", onClick = onCancel)
|
||||
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(text = "Dictation", style = ClawTheme.type.title.copy(fontSize = 16.sp, lineHeight = 20.sp), color = ClawTheme.colors.text)
|
||||
Text(text = "Transcribe then send", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
|
||||
}
|
||||
ClawPlainIconButton(icon = Icons.Default.Settings, contentDescription = "Dictation settings", onClick = onOpenVoiceSettings)
|
||||
VoicePlainIconButton(icon = Icons.Default.Settings, contentDescription = "Dictation settings", onClick = onOpenVoiceSettings)
|
||||
}
|
||||
|
||||
Surface(
|
||||
@@ -407,7 +404,7 @@ private fun TalkSessionScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
ClawPlainIconButton(icon = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back to voice", onClick = onEndTalk)
|
||||
VoicePlainIconButton(icon = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back to voice", onClick = onEndTalk)
|
||||
Column(modifier = Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(3.dp)) {
|
||||
Text(text = "Realtime Talk", style = ClawTheme.type.title.copy(fontSize = 16.sp, lineHeight = 20.sp), color = ClawTheme.colors.text)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) {
|
||||
@@ -426,7 +423,7 @@ private fun TalkSessionScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
ClawPlainIconButton(icon = Icons.Default.Info, contentDescription = "Talk settings", onClick = onOpenVoiceSettings)
|
||||
VoicePlainIconButton(icon = Icons.Default.Info, contentDescription = "Talk settings", onClick = onOpenVoiceSettings)
|
||||
}
|
||||
|
||||
Surface(
|
||||
@@ -550,19 +547,14 @@ private fun VoiceHeader(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.openclaw_logo),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(25.dp),
|
||||
tint = ClawTheme.colors.text,
|
||||
)
|
||||
Text(
|
||||
text = "OpenClaw",
|
||||
style = ClawTheme.type.title.copy(fontSize = 17.sp, lineHeight = 21.sp),
|
||||
text = "O P E N C L A W",
|
||||
style = ClawTheme.type.title.copy(fontSize = 18.sp, lineHeight = 23.sp),
|
||||
color = ClawTheme.colors.text,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
ClawPlainIconButton(icon = Icons.Default.Search, contentDescription = "Search voice", onClick = onOpenCommand)
|
||||
VoicePlainIconButton(icon = Icons.Default.Search, contentDescription = "Search voice", onClick = onOpenCommand)
|
||||
VoiceAvatar(text = "OC")
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -570,7 +562,7 @@ private fun VoiceHeader(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) {
|
||||
Text(text = "Voice", style = ClawTheme.type.display.copy(fontSize = 24.sp, lineHeight = 28.sp), color = ClawTheme.colors.text)
|
||||
Text(text = "Voice", style = ClawTheme.type.display.copy(fontSize = 16.sp, lineHeight = 20.sp), color = ClawTheme.colors.text)
|
||||
Text(
|
||||
text = statusText,
|
||||
style = ClawTheme.type.body,
|
||||
@@ -579,7 +571,7 @@ private fun VoiceHeader(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
ClawPlainIconButton(
|
||||
VoicePlainIconButton(
|
||||
icon = if (speakerEnabled) Icons.AutoMirrored.Filled.VolumeUp else Icons.AutoMirrored.Filled.VolumeOff,
|
||||
contentDescription = if (speakerEnabled) "Mute speaker" else "Unmute speaker",
|
||||
onClick = onToggleSpeaker,
|
||||
@@ -588,6 +580,34 @@ private fun VoiceHeader(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VoiceAvatar(text: String) {
|
||||
Surface(
|
||||
modifier = Modifier.size(34.dp),
|
||||
shape = CircleShape,
|
||||
color = ClawTheme.colors.surfaceRaised,
|
||||
contentColor = ClawTheme.colors.text,
|
||||
border = BorderStroke(1.dp, ClawTheme.colors.border),
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Text(text = text.take(2).uppercase(), style = ClawTheme.type.label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VoicePlainIconButton(
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
contentDescription: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Surface(onClick = onClick, modifier = Modifier.size(ClawTheme.spacing.touchTarget), shape = CircleShape, color = Color.Transparent, contentColor = ClawTheme.colors.text) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Icon(imageVector = icon, contentDescription = contentDescription, modifier = Modifier.size(18.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VoiceHero(
|
||||
gatewayStatus: String,
|
||||
@@ -841,10 +861,8 @@ private fun VoiceOrb(
|
||||
Surface(
|
||||
modifier = Modifier.size(112.dp),
|
||||
shape = CircleShape,
|
||||
color = if (active || listening || speaking) Color(0xFF1976D2) else Color(0xFF123B63),
|
||||
contentColor = Color.White,
|
||||
tonalElevation = 3.dp,
|
||||
shadowElevation = 7.dp,
|
||||
color = if (active) ClawTheme.colors.surfacePressed else ClawTheme.colors.surface,
|
||||
border = BorderStroke(1.dp, if (active) ClawTheme.colors.borderStrong else ClawTheme.colors.border),
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
@@ -857,7 +875,7 @@ private fun VoiceOrb(
|
||||
},
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = Color.White,
|
||||
tint = ClawTheme.colors.text,
|
||||
)
|
||||
Waveform(active = active)
|
||||
}
|
||||
@@ -874,7 +892,7 @@ private fun Waveform(active: Boolean) {
|
||||
Modifier
|
||||
.size(width = 2.dp, height = (if (active) height else 6 + index % 3 * 3).dp)
|
||||
.clip(RoundedCornerShape(999.dp))
|
||||
.background(if (active) Color.White else Color.White.copy(alpha = 0.52f)),
|
||||
.background(if (active) ClawTheme.colors.text else ClawTheme.colors.textSubtle),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package ai.openclaw.app.ui.chat
|
||||
|
||||
import ai.openclaw.app.MainViewModel
|
||||
import ai.openclaw.app.R
|
||||
import ai.openclaw.app.chat.ChatMessage
|
||||
import ai.openclaw.app.chat.ChatMessageContent
|
||||
import ai.openclaw.app.chat.ChatPendingToolCall
|
||||
@@ -40,7 +39,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Send
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material.icons.filled.AttachFile
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Mic
|
||||
@@ -65,7 +63,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -156,11 +153,12 @@ fun ChatScreen(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
.padding(horizontal = 18.dp, vertical = 6.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(5.dp),
|
||||
) {
|
||||
ChatHeader(
|
||||
sessionTitle = currentSessionTitle(sessionKey = sessionKey, sessions = sessions),
|
||||
thinkingLevel = thinkingLevel,
|
||||
healthOk = healthOk,
|
||||
pendingRunCount = pendingRunCount,
|
||||
onMore = {
|
||||
@@ -263,11 +261,11 @@ private fun ChatSessionSwitcher(
|
||||
if (sessions.size > choices.size) {
|
||||
Surface(
|
||||
onClick = onOpenSessions,
|
||||
modifier = Modifier.heightIn(min = ClawTheme.spacing.touchTarget),
|
||||
modifier = Modifier.heightIn(min = 36.dp),
|
||||
shape = RoundedCornerShape(ClawTheme.radii.pill),
|
||||
color = ClawTheme.colors.surfaceRaised.copy(alpha = 0.72f),
|
||||
color = ClawTheme.colors.canvas,
|
||||
contentColor = ClawTheme.colors.textMuted,
|
||||
border = BorderStroke(1.dp, ClawTheme.colors.border.copy(alpha = 0.7f)),
|
||||
border = BorderStroke(1.dp, ClawTheme.colors.border),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 7.dp),
|
||||
@@ -290,11 +288,11 @@ private fun ChatSessionChip(
|
||||
) {
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.heightIn(min = ClawTheme.spacing.touchTarget),
|
||||
modifier = Modifier.heightIn(min = 36.dp),
|
||||
shape = RoundedCornerShape(ClawTheme.radii.pill),
|
||||
color = if (active) ClawTheme.colors.surfacePressed.copy(alpha = 0.9f) else ClawTheme.colors.surfaceRaised.copy(alpha = 0.72f),
|
||||
contentColor = ClawTheme.colors.text,
|
||||
border = BorderStroke(1.dp, if (active) ClawTheme.colors.borderStrong else ClawTheme.colors.border.copy(alpha = 0.7f)),
|
||||
color = if (active) ClawTheme.colors.primary else ClawTheme.colors.surfaceRaised,
|
||||
contentColor = if (active) ClawTheme.colors.primaryText else ClawTheme.colors.text,
|
||||
border = BorderStroke(1.dp, if (active) ClawTheme.colors.primary else ClawTheme.colors.border),
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
@@ -309,56 +307,48 @@ private fun ChatSessionChip(
|
||||
@Composable
|
||||
private fun ChatHeader(
|
||||
sessionTitle: String,
|
||||
thinkingLevel: String,
|
||||
healthOk: Boolean,
|
||||
pendingRunCount: Int,
|
||||
onMore: () -> Unit,
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
Box(modifier = Modifier.size(ClawTheme.spacing.touchTarget))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(3.dp),
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.openclaw_logo),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(25.dp),
|
||||
tint = ClawTheme.colors.text,
|
||||
)
|
||||
Text(
|
||||
text = "OpenClaw",
|
||||
style = ClawTheme.type.title.copy(fontSize = 17.sp, lineHeight = 21.sp),
|
||||
text = sessionTitle,
|
||||
style = ClawTheme.type.title.copy(fontSize = 18.sp, lineHeight = 23.sp),
|
||||
color = ClawTheme.colors.text,
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
ModelPill(
|
||||
text =
|
||||
when {
|
||||
pendingRunCount > 0 -> "Working"
|
||||
healthOk -> "Ready"
|
||||
else -> "Offline"
|
||||
healthOk -> "auto"
|
||||
else -> "offline"
|
||||
},
|
||||
status =
|
||||
when {
|
||||
pendingRunCount > 0 -> ClawStatus.Warning
|
||||
healthOk -> ClawStatus.Success
|
||||
healthOk -> ClawStatus.Neutral
|
||||
else -> ClawStatus.Danger
|
||||
},
|
||||
)
|
||||
HeaderIcon(icon = Icons.Default.Refresh, contentDescription = "Refresh chat", onClick = onMore)
|
||||
}
|
||||
Column(verticalArrangement = Arrangement.spacedBy(3.dp)) {
|
||||
Text(text = "Chat", style = ClawTheme.type.display.copy(fontSize = 24.sp, lineHeight = 28.sp), color = ClawTheme.colors.text, maxLines = 1)
|
||||
Text(
|
||||
text = sessionTitle,
|
||||
style = ClawTheme.type.caption.copy(fontSize = 13.sp, lineHeight = 17.sp),
|
||||
color = ClawTheme.colors.textMuted,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
HeaderIcon(icon = Icons.Default.Refresh, contentDescription = "Refresh chat", onClick = onMore)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,13 +365,7 @@ private fun ModelPill(
|
||||
}
|
||||
Surface(
|
||||
shape = RoundedCornerShape(ClawTheme.radii.pill),
|
||||
color =
|
||||
when (status) {
|
||||
ClawStatus.Success -> ClawTheme.colors.successSoft
|
||||
ClawStatus.Warning -> ClawTheme.colors.warningSoft
|
||||
ClawStatus.Danger -> ClawTheme.colors.dangerSoft
|
||||
ClawStatus.Neutral -> ClawTheme.colors.surfaceRaised
|
||||
},
|
||||
color = ClawTheme.colors.surfaceRaised,
|
||||
contentColor = ClawTheme.colors.textMuted,
|
||||
border = BorderStroke(1.dp, borderColor),
|
||||
) {
|
||||
@@ -593,15 +577,13 @@ private fun ChatBubble(
|
||||
horizontalArrangement = if (isUser) Arrangement.End else Arrangement.Start,
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(if (isUser) 0.84f else 0.94f),
|
||||
modifier = Modifier.fillMaxWidth(if (isUser) 0.64f else 0.56f),
|
||||
shape = RoundedCornerShape(7.dp),
|
||||
color = if (isUser) ClawTheme.colors.surfacePressed.copy(alpha = 0.86f) else ClawTheme.colors.surfaceRaised.copy(alpha = 0.84f),
|
||||
color = ClawTheme.colors.surfaceRaised,
|
||||
contentColor = ClawTheme.colors.text,
|
||||
border = BorderStroke(1.dp, if (live) ClawTheme.colors.borderStrong else ClawTheme.colors.border.copy(alpha = 0.45f)),
|
||||
tonalElevation = 1.dp,
|
||||
shadowElevation = 2.dp,
|
||||
border = BorderStroke(1.dp, if (live) ClawTheme.colors.borderStrong else ClawTheme.colors.border),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 11.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Column(modifier = Modifier.padding(horizontal = 7.dp, vertical = 3.5.dp), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(
|
||||
text =
|
||||
when {
|
||||
@@ -782,7 +764,7 @@ private fun ChatContextMeter(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
Icon(imageVector = Icons.Default.ArrowDropDown, contentDescription = null, modifier = Modifier.size(13.dp), tint = ClawTheme.colors.textSubtle)
|
||||
Icon(imageVector = Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(12.dp), tint = ClawTheme.colors.textSubtle)
|
||||
Text(
|
||||
text = contextMeterLabel(contextUsage, thinkingLevel),
|
||||
style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp),
|
||||
@@ -954,7 +936,7 @@ internal fun resolveChatContextUsage(
|
||||
sessionKey = sessionKey,
|
||||
mainSessionKey = mainSessionKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
return ChatContextUsage(
|
||||
totalTokens = entry?.totalTokens,
|
||||
totalTokensFresh = entry?.totalTokensFresh,
|
||||
@@ -991,6 +973,24 @@ private fun userFacingChatError(error: String): String {
|
||||
}
|
||||
}
|
||||
|
||||
/** Normalizes persisted thinking values into compact UI labels. */
|
||||
private fun thinkingDisplay(value: String): String =
|
||||
when (value.lowercase(Locale.US)) {
|
||||
"low" -> "Low"
|
||||
"medium" -> "Medium"
|
||||
"high" -> "High"
|
||||
else -> "Off"
|
||||
}
|
||||
|
||||
/** Converts displayed thinking labels back to gateway request values. */
|
||||
private fun thinkingValue(display: String): String =
|
||||
when (display.lowercase(Locale.US)) {
|
||||
"low" -> "low"
|
||||
"medium" -> "medium"
|
||||
"high" -> "high"
|
||||
else -> "off"
|
||||
}
|
||||
|
||||
/** Cycles through context budget presets from the compact composer control. */
|
||||
private fun nextThinkingValue(value: String): String =
|
||||
when (value.lowercase(Locale.US)) {
|
||||
|
||||
@@ -185,53 +185,6 @@ internal fun ClawIconButton(
|
||||
}
|
||||
}
|
||||
|
||||
/** Transparent circular icon button for low-emphasis toolbar actions. */
|
||||
@Composable
|
||||
internal fun ClawPlainIconButton(
|
||||
icon: ImageVector,
|
||||
contentDescription: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.size(ClawTheme.spacing.touchTarget),
|
||||
shape = CircleShape,
|
||||
color = Color.Transparent,
|
||||
contentColor = ClawTheme.colors.text,
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Icon(imageVector = icon, contentDescription = contentDescription, modifier = Modifier.size(18.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Compact label/value row for health and readiness summaries. */
|
||||
@Composable
|
||||
internal fun ClawStatusRow(
|
||||
title: String,
|
||||
value: String,
|
||||
healthy: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 7.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(9.dp),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = ClawTheme.type.body,
|
||||
color = ClawTheme.colors.text,
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 1,
|
||||
)
|
||||
ClawStatusPill(
|
||||
text = value,
|
||||
status = if (healthy) ClawStatus.Success else ClawStatus.Warning,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Compact status chip with a semantic color dot. */
|
||||
@Composable
|
||||
internal fun ClawStatusPill(
|
||||
|
||||
@@ -95,17 +95,15 @@ internal fun ClawBottomNav(
|
||||
Box(modifier = modifier.fillMaxWidth().background(ClawTheme.colors.canvas)) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = ClawTheme.colors.surface.copy(alpha = 0.92f),
|
||||
border = BorderStroke(1.dp, ClawTheme.colors.border.copy(alpha = 0.42f)),
|
||||
color = ClawTheme.colors.surface.copy(alpha = 0.96f),
|
||||
border = BorderStroke(1.dp, ClawTheme.colors.border),
|
||||
shape = RoundedCornerShape(topStart = ClawTheme.radii.sheet, topEnd = ClawTheme.radii.sheet),
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 8.dp,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.windowInsetsPadding(safeInsets)
|
||||
.padding(horizontal = 8.dp, vertical = 6.dp),
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
@@ -133,13 +131,13 @@ private fun ClawBottomNavItem(
|
||||
onClick = onClick,
|
||||
modifier = modifier.heightIn(min = 48.dp),
|
||||
shape = RoundedCornerShape(ClawTheme.radii.control),
|
||||
color = if (selected) ClawTheme.colors.surfacePressed.copy(alpha = 0.72f) else Color.Transparent,
|
||||
contentColor = if (selected) ClawTheme.colors.text else ClawTheme.colors.textMuted,
|
||||
color = if (selected) ClawTheme.colors.primary else Color.Transparent,
|
||||
contentColor = if (selected) ClawTheme.colors.primaryText else ClawTheme.colors.textMuted,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 5.dp, vertical = 5.dp),
|
||||
modifier = Modifier.padding(horizontal = 5.dp, vertical = 6.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(3.dp),
|
||||
) {
|
||||
Icon(imageVector = item.icon, contentDescription = item.label, modifier = Modifier.size(18.dp))
|
||||
Text(text = item.label, style = ClawTheme.type.caption, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
|
||||
@@ -27,11 +27,31 @@ internal fun ClawPanel(
|
||||
Surface(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(ClawTheme.radii.panel),
|
||||
color = ClawTheme.colors.surfaceRaised.copy(alpha = 0.82f),
|
||||
color = ClawTheme.colors.surfaceRaised,
|
||||
contentColor = ClawTheme.colors.text,
|
||||
border = null,
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 4.dp,
|
||||
border = BorderStroke(1.dp, ClawTheme.colors.border),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(contentPadding)) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bottom-sheet container with the app surface treatment and top-only rounding.
|
||||
*/
|
||||
@Composable
|
||||
internal fun ClawSheetSurface(
|
||||
modifier: Modifier = Modifier,
|
||||
contentPadding: PaddingValues = PaddingValues(18.dp),
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(topStart = ClawTheme.radii.sheet, topEnd = ClawTheme.radii.sheet),
|
||||
color = ClawTheme.colors.surface,
|
||||
contentColor = ClawTheme.colors.text,
|
||||
border = BorderStroke(1.dp, ClawTheme.colors.border),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(contentPadding)) {
|
||||
content()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user