mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-18 08:31:49 -06:00
Merge main into fix/control-ui-settings-defaults
This commit is contained in:
@@ -1,11 +1,25 @@
|
||||
---
|
||||
name: auto-qa
|
||||
description: "Continuously audit, live-test, and stress-test the current OpenClaw codebase across at least ten independently scoped subsystem lanes; reproduce and deduplicate genuine bugs, verify focused fixes, maintain an evidence-backed report, and prepare or land pull requests according to maintainer authorization and risk. Use for OpenClaw-wide autonomous QA, live-provider campaigns, gateway, CLI, web, plugin, native-app, scenario, packaging, and extended soak testing."
|
||||
description: "Continuously audit, live-test, and stress-test the current OpenClaw codebase across at least ten independently scoped subsystem lanes; default to 100 independently verified, landed root-cause fixes, maintain an evidence-backed report, and announce each merged pull request immediately. Use for OpenClaw-wide autonomous QA, live-provider campaigns, gateway, CLI, web, plugin, native-app, scenario, packaging, and extended soak testing."
|
||||
---
|
||||
|
||||
# OpenClaw Auto QA
|
||||
|
||||
Run a continuous, current-`main` OpenClaw product campaign. Treat a reviewer finding as a hypothesis, a passing test as evidence only for its actual head, and a merge as complete only when the canonical repository confirms it. Repair the actual root cause in its canonical owner; a smaller patch is not better if it leaves sibling paths, lifecycle invariants, or the defective abstraction intact.
|
||||
Run a continuous, current-`main` OpenClaw product campaign. Treat a reviewer finding as a hypothesis, a passing test as evidence only for its actual head, and a merge as complete only when the canonical repository confirms it. Always prefer a clean, appropriately scoped root-cause refactor over a quick fix or smaller diff. Repair the actual root cause in its canonical owner; a patch is not acceptable when it leaves sibling paths, lifecycle invariants, or the defective abstraction intact.
|
||||
|
||||
## Set the campaign goal
|
||||
|
||||
- Default to **100 distinct, independently verified, actually merged root-cause fixes** unless the operator explicitly specifies a different target. Count broken invariants, not pull requests, symptoms, proposed patches, skill updates, or merely passing tests.
|
||||
- When the operator changes the target during an active campaign, update the existing report, numbered ledger, progress denominator, and live status in place. Preserve every previously verified landing; never reset the investigation or claim that an unfinished target has been reached.
|
||||
- Exclude security-sensitive, persistent-state, protocol, public-contract, or otherwise review-required work from the accepted count until the appropriate owner explicitly authorizes and verifies its landing.
|
||||
|
||||
## Prefer clean refactors over quick fixes
|
||||
|
||||
- Identify the broken ownership boundary, abstraction, state transition, or dependency contract before choosing an implementation. Compare the canonical owner, callers, callees, and sibling paths; prefer the design that makes their shared invariant obvious and reliable.
|
||||
- Consolidate decisions and authoritative state in their actual owner. Propagate prepared facts through existing lifecycles, repair all affected siblings, and delete obsolete branches, duplicate policy, dead helpers, and stale abstractions when they are no longer needed.
|
||||
- Reject symptom-masking guards, one-off exceptions, observed-example literals, parallel code paths, extra caches, fallback stacks, compatibility shims, and tests that merely make a narrow reproduction pass. A smaller change is not safer when it preserves the cause or makes the architecture harder to understand.
|
||||
- Preserve shipped public contracts and ownership boundaries. If the clean refactor would affect security, persistent state, public configuration, plugin SDK compatibility, a protocol, or a product decision, mark it for maintainer review instead of substituting a tactical patch.
|
||||
- During independent review, explicitly ask whether the change is the cleanest appropriately bounded root-cause solution. Green tests, a minimal diff, and a plausible local fix are insufficient without that architectural judgment.
|
||||
|
||||
## Start with the moving source
|
||||
|
||||
@@ -48,13 +62,13 @@ Read [references/live-proof-routing.md](references/live-proof-routing.md) before
|
||||
|
||||
1. Deduplicate against the current ledger, `origin/main`, current open and merged GitHub work, and sibling root causes. Count one broken invariant once, even when it produces multiple model, platform, route, lifecycle, or UI symptoms.
|
||||
2. Independently reproduce the actual current-main user path. Map the entry point, canonical owner, callers, callees, sibling implementations, state lifecycle, existing regressions, shipped contracts, and relevant direct upstream source. Identify why the current design fails before proposing a repair.
|
||||
3. Refactor the canonical owner in an isolated worktree. Repair all affected sibling paths in the same coherent change, simplify or remove the defective abstraction, and carry authoritative facts through the existing lifecycle. Prefer the appropriately sized root-cause solution over a minimal guard, special case, extra cache, fallback, compatibility shim, or narrowly passing test.
|
||||
3. Refactor the canonical owner in an isolated worktree. Repair all affected sibling paths in the same coherent change, simplify or remove the defective abstraction, and carry authoritative facts through the existing lifecycle. Prefer the cleanest appropriately sized root-cause solution over a minimal diff; reject a guard, special case, extra cache, fallback, compatibility shim, or narrowly passing test that leaves the architectural defect behind.
|
||||
4. Preserve public configuration, plugin ownership, gateway protocol, migrations, provider contracts, persistent state, and external dependencies. When a correct root-cause repair would change a sensitive contract or requires a product decision, prepare it for operator review; do not disguise that risk as a small autonomous fix.
|
||||
5. Add authentic regression coverage for the original reproduction, affected siblings, lifecycle cleanup, and unchanged legitimate behavior. Run appropriately scoped proof on the exact candidate head. Route Docker, real providers, packaging, full checks, typechecking, broad suites, and browser work through the existing remote workflow; inspect actual exit status, nonzero scenario counts, and artifacts.
|
||||
6. Run a fresh `$autoreview` on the complete final refactor. Resolve actionable findings; rerun review after any production, test, or head change. Personally read the latest ClawSweeper review, satisfy each applicable rank-up move with real evidence, and update the existing PR body before landing.
|
||||
6. Run a fresh `$autoreview` on the complete final refactor. Require the reviewer to compare owner boundaries and sibling implementations, confirm this is the best clean root-cause solution, and reject quick-fix residue even when tests pass. Resolve actionable findings; rerun review after any production, test, or head change. Personally read the latest ClawSweeper review, satisfy each applicable rank-up move with real evidence, and update the existing PR body before landing.
|
||||
7. Check existing open PRs, current author counts, and the actual repository automation before publishing. Read both the current labeler and response policy; verify the authenticated author association, repository permission, account type, automation branch prefix, and actual override label. Apply only exemptions proved by that current policy, including eligible owners, maintainers, collaborators, bots or apps, approved automation branches, and explicit overrides. Never infer capacity from a truncated list or assume that one privileged role represents every exemption. Reuse and repair an existing candidate PR for the same cause. When a real cap applies, hold reviewed worktrees and finish or land existing verified work first.
|
||||
8. Create a focused PR with the repository's actual template, canonical cause, user impact, frozen head, completed proof, and risk. Use only the current repo-native `scripts/pr` review, artifact, prepare, and merge workflow for authorized main landing.
|
||||
9. Autonomously merge only when the user authorized it **and** the canonical root-cause refactor is individually reproduced, low-risk, independently reviewed, current-main-compatible, and has green required exact-head proof. Evaluate risk by ownership and behavioral impact, not by whether the diff is the smallest possible. Verify the resulting canonical merge SHA before incrementing the ledger.
|
||||
9. Autonomously merge only when the user authorized it **and** the canonical root-cause refactor is individually reproduced, low-risk, independently reviewed, current-main-compatible, and has green required exact-head proof. Evaluate risk by ownership and behavioral impact, not by whether the diff is the smallest possible. Verify the resulting canonical merge SHA before incrementing the ledger. Immediately report each verified landing to the operator with its pull-request URL and one or two concise sentences describing the actual root causes fixed; do not batch, defer, or announce unmerged pull requests as landed.
|
||||
|
||||
Prepare but **do not autonomously merge** security or authentication changes; SQLite integrity, migration, schema, or persistent-state changes; public configuration or plugin SDK compatibility changes; protocol changes; architectural or product decisions; broad high-impact refactors; uncertain diagnoses; or a candidate with pending, skipped, stale, conflicting, or failing proof. A bounded low-risk root-cause refactor may be autonomously landable when explicitly authorized; sensitive or uncertain impact is not. Mark higher-risk changes **user review required** and keep them outside the accepted-bug count.
|
||||
|
||||
|
||||
@@ -0,0 +1,361 @@
|
||||
---
|
||||
name: openclaw-autonomous-issue-sweep
|
||||
description: "Orchestrate 64 autonomous OpenClaw issue workers newest-to-oldest with isolated issue worktrees and resource-bounded parallelism; investigate bugs, simplify or refactor, review, land verified fixes, close already-fixed issues, and add meaningful evidence."
|
||||
---
|
||||
|
||||
# OpenClaw Autonomous Issue Sweep
|
||||
|
||||
Run an end-to-end maintainer campaign, not a candidate shortlist. The parent
|
||||
conversation is the orchestrator: delegate discovery, investigation, coding,
|
||||
testing, review, GitHub mutations, PR preparation, landing, and cleanup to
|
||||
subagents. Keep parent-thread updates to concise progress and clickable URLs.
|
||||
|
||||
## Authority and campaign shape
|
||||
|
||||
- Spawn exactly **64 first-class subagents** unless the user requests another
|
||||
count or available capacity makes that impossible; disclose the actual count.
|
||||
- Use full-history forks so every subagent inherits the orchestrator's model
|
||||
and **xhigh reasoning effort**. Never print, record, or disclose model
|
||||
identifiers; redact subprocess banners and diagnostics before reporting.
|
||||
- Begin every full-history child assignment with its explicit role and agent
|
||||
identity, require inherited **xhigh reasoning effort**, and forbid
|
||||
`create_goal`, visualizations, `spawn_agent`, or nested agents. Children
|
||||
return evidence to the orchestrator; never downgrade their model or effort.
|
||||
- Treat a request to run this workflow as authority to create lightweight,
|
||||
issue-scoped isolated Git worktrees and `codex/issue-<id>` branches, review,
|
||||
fix, refactor, commit, push, create/update PRs, land eligible changes,
|
||||
comment, and close issues individually. Do not ask for separate worktree or
|
||||
routine-operation confirmation again.
|
||||
- Never treat sweep authority as permission to publish releases, bump protocol
|
||||
or SQLite schema versions, weaken security, break shipped compatibility,
|
||||
change another owner's protected product surface, or execute untrusted code
|
||||
with local credentials.
|
||||
- Have subagents read the complete root `AGENTS.md`, relevant scoped guides,
|
||||
`VISION.md`, and companion skills before acting. Use `$gitcrawl`, Octopool,
|
||||
`$openclaw-pr-maintainer`, `$openclaw-testing`, `$crabbox`, and `$autoreview`
|
||||
where each owns the workflow.
|
||||
- Keep the parent out of operational work. It may spawn, assign, receive
|
||||
results, serialize shared resources, monitor host/pool health, prewarm and
|
||||
allocate needed remote leases, issue follow-up tasks, and report; it must
|
||||
not inspect issues, edit code, run tests, mutate GitHub, or land PRs.
|
||||
|
||||
## Coordinate 64 workers safely
|
||||
|
||||
1. Assign one subagent to maintain the live open-issue queue in descending
|
||||
`createdAt` order, one to coordinate landing/proof capacity, and no more
|
||||
than **3** to live issue closures or other GitHub mutations. Assign the
|
||||
remaining slots to issue investigations; idle coordinators also investigate.
|
||||
2. Claim issues from the newest unclaimed end only; replenish workers as they
|
||||
finish. Parallel completions may arrive out of order, but never knowingly
|
||||
start an older unclaimed issue ahead of a newer available issue.
|
||||
3. Deduplicate by canonical root cause, not merely by issue number. Let one
|
||||
owner fix a shared defect and link related issues/PRs to that outcome.
|
||||
4. Freeze the reviewed source SHA for each wave. Serialize only shared Git/ref
|
||||
mutations: fetches, branch/ref changes, `git worktree add`/remove, PR
|
||||
preparation and merges, and main-targeted pushes. Give each mutation a brief
|
||||
coordinator-owned exclusive slot; do not hold it across coding, proof,
|
||||
reviews, remote waits, or other independent issue work.
|
||||
5. Give every independent root-cause fix its own isolated, issue-scoped
|
||||
lightweight worktree and `codex/issue-<id>` branch. Create it from the
|
||||
frozen SHA, for example:
|
||||
|
||||
```bash
|
||||
git worktree add -b "codex/issue-$issue_id" \
|
||||
"$campaign_worktrees/issue-$issue_id" "$frozen_main_sha"
|
||||
```
|
||||
|
||||
Reuse a repo-native isolated PR worktree when repairing an existing PR;
|
||||
duplicate issues sharing one root cause share its single owner/worktree.
|
||||
Share Git objects; do not clone the repository or install dependencies per
|
||||
worktree merely for isolation. Never edit, switch, reset, or otherwise
|
||||
mutate the shared checkout while sibling workers are active. Once isolated
|
||||
worktrees exist, independent issue owners edit, inspect, and verify in
|
||||
parallel within their own checkout.
|
||||
|
||||
6. Keep all **64** inherited high-effort agents available, but distinguish idle
|
||||
agents from active local tool users. Start with bounded waves of **4–8**
|
||||
concurrently active code/test workers and continuously reduce or expand that
|
||||
limit according to usable CPU/load, memory/swap pressure, checkout and temp
|
||||
free disk, process count, operator-gateway health, and remote-pool capacity.
|
||||
Reserve capacity for the operator; count heavyweight proof proportionally,
|
||||
stop admitting new commands under sustained pressure, and resume in small
|
||||
waves after recovery. Never kill unrelated operator processes.
|
||||
7. Serialize merges and each Testbox lease, not independent worktree edits. A
|
||||
lease has one owner and one active command; never reclaim, sync, or change
|
||||
its head during a run.
|
||||
8. Respect GitHub rate limits, active assignees, repository ownership, and
|
||||
existing contributor work. Do not auto-assign broad-discovery candidates.
|
||||
9. Replace finished workers while the queue remains. Record actual active,
|
||||
parked, completed, failed, fixed, landed, verified-closed, queued-for-close,
|
||||
commented, and skipped counts. Persist that campaign checkpoint for resumed
|
||||
workers; never report launched, parked, or finished workers as still running.
|
||||
|
||||
## Conserve GitHub capacity and host resources
|
||||
|
||||
- Prefer local `$gitcrawl` archives and source history for queue discovery,
|
||||
issue/PR search, duplicate clusters, comments, and previously merged work.
|
||||
Check archive freshness; do not broadly sync, enrich, or re-embed merely to
|
||||
start a sweep.
|
||||
- Prefer `octopool gh ...` or narrowly bounded `octopool request` for
|
||||
necessary live GitHub reads and mutations. Check `octopool health` and
|
||||
`octopool stats` periodically; let repo-native PR wrappers retain their
|
||||
required GitHub transport and authenticated identity.
|
||||
- Use plain `gh` only when Octopool cannot support the operation or the
|
||||
canonical maintainer wrapper requires it. Request minimal fields, reuse
|
||||
results across workers, batch compatible reads, avoid unbounded pagination,
|
||||
and never use `gh run watch` or frequent unchanged CI polls.
|
||||
- Require a fresh live state check only before consequential mutations, final
|
||||
merge decisions, or a stale/contradictory cached result. Rate-limit and
|
||||
deduplicate worker requests instead of having 64 agents independently fetch
|
||||
the same issue, PR, author profile, or CI rollup.
|
||||
- Keep disk, CPU/load, memory pressure, active lease IDs, provider trust class,
|
||||
issue-worktree ownership, active local tool count, frozen heads, and pool
|
||||
capacity in the orchestration ledger. Dynamically cap concurrent code/test
|
||||
workers instead of serializing every independent fix. Pause or interrupt only
|
||||
campaign-owned work under host pressure, preserve each issue's claim and
|
||||
isolated checkout, then resume from that recorded state when capacity returns.
|
||||
Offload heavy proof before resource pressure threatens the host.
|
||||
- Worktree checkout and dependency use must respect free-disk headroom. Reuse
|
||||
shared Git objects and existing trusted dependency installs where safe; route
|
||||
dependency-missing or heavyweight proof to the selected remote box instead
|
||||
of multiplying local installs across issue checkouts.
|
||||
- The parent may prewarm a trusted Crabbox/Testbox lease when a concrete heavy
|
||||
proof is imminent, then hand its verified lease ID and checkout ownership to
|
||||
one subagent at a time. Avoid speculative fleets, respect path-scoped lease
|
||||
ownership, and stop campaign-owned leases before handoff or closeout.
|
||||
- Keep untrusted contributor proof on a separate sanitized direct-AWS lease;
|
||||
never transfer a credential-hydrated trusted lease to untrusted work.
|
||||
|
||||
## Search for existing work on every credible issue
|
||||
|
||||
Always investigate existing PRs before implementing a fix:
|
||||
|
||||
1. Read the live issue body, all material comments, labels, assignments,
|
||||
timeline/cross-references, repro details, affected versions, and ClawSweeper
|
||||
findings.
|
||||
2. Search `$gitcrawl` for the issue number, title, error text, affected
|
||||
subsystem, relevant symbols, duplicate symptoms, open PRs, merged PRs, and
|
||||
recently closed work.
|
||||
3. Verify candidates against Octopool-backed live GitHub search, directly
|
||||
linked PRs, current PR heads, `origin/main`, and commit history. Search
|
||||
exact issue references and symptom/root-cause terms; do not stop at the
|
||||
first plausible PR.
|
||||
4. Read competing implementations deeply enough to decide whether an existing
|
||||
PR already fixes the real defect, merely masks one symptom, has gone stale,
|
||||
or reveals a cleaner owner-boundary refactor.
|
||||
5. Preserve contributor commits, attribution, issue reporter credit, and useful
|
||||
ideas whenever repairing or replacing existing work.
|
||||
|
||||
Choose outcomes in this order:
|
||||
|
||||
1. **Fixed on main:** prove the original failure is resolved; close with the
|
||||
exact merged PR, commit, current source/test, or release proof.
|
||||
2. **Existing PR is the best fix:** improve it as needed, verify the exact
|
||||
final head, and land it through the repo-native maintainer workflow.
|
||||
3. **Existing PR is useful but incomplete:** finish it or create a cleaner
|
||||
replacement that preserves human attribution and links the original.
|
||||
4. **No suitable PR:** implement the best high-confidence root-cause repair or
|
||||
a justified simplifying refactor; create, verify, and land a focused PR.
|
||||
5. **Bug cannot be fixed, but simplification is real:** independently land a
|
||||
proven behavior-neutral refactor when it meaningfully removes complexity
|
||||
without pretending the original issue was fixed.
|
||||
6. **Cannot fix or close:** comment only if investigation uncovered concrete,
|
||||
material evidence missing from the issue and ClawSweeper's existing review.
|
||||
|
||||
## Prove the bug and choose the best design
|
||||
|
||||
- Trace the actual user path from entry point through caller, canonical owner,
|
||||
callee, sibling implementations, transport/lifecycle boundaries, tests,
|
||||
current `main`, shipped contracts, and direct dependency source or docs.
|
||||
- Personally inspect sibling `../codex` source before any Codex integration
|
||||
verdict or change, as required by the root guide; another agent's report is
|
||||
not sufficient for the agent making that decision.
|
||||
- Require a failing regression, reproducible command, real logs, live product
|
||||
behavior, dependency contract, or exact source-level proof. Never repair an
|
||||
issue on title, speculation, ClawSweeper output, or a plausible diff alone.
|
||||
- Prefer the correct owner-boundary refactor over a narrow guard, workaround,
|
||||
new fallback, duplicate policy, extra configuration, or compatibility shim.
|
||||
A larger refactor is appropriate when it fixes the whole bug class more
|
||||
clearly and its behavior/ownership risk remains understood and bounded.
|
||||
- While reading, look for dead branches, unused helpers, duplicate paths,
|
||||
stale abstractions, obsolete tests, and complexity that can be deleted as
|
||||
part of the same coherent change.
|
||||
- Measure `git diff --numstat`; aim to reduce **production LOC**, excluding
|
||||
tests. Production growth is acceptable only when clearly justified by fewer
|
||||
concepts, better ownership, essential product behavior, or stronger safety.
|
||||
- Allow small missing product affordances, such as an obviously expected CLI
|
||||
command, when adjacent behavior and docs establish the contract. Reject
|
||||
substantial new features, speculative redesign, new paid services,
|
||||
unsupported integrations, or unrelated drive-by changes.
|
||||
- Do not edit `CHANGELOG.md`; capture user impact, issue/PR references, and
|
||||
human credit in the PR body or commit message.
|
||||
|
||||
## Hard issue-closure gate
|
||||
|
||||
An issue stays open unless every step below passes. Similar wording, adjacent
|
||||
tests, merged PR dates, contributor suggestions, and confident review summaries
|
||||
are not closure proof.
|
||||
|
||||
1. Write down the reporter's exact **primary symptom**, desired user-visible
|
||||
outcome, every separately affected surface, reported version/build SHA, and
|
||||
all proposed alternatives. An optional mitigation or diagnostic suggestion
|
||||
does not replace the reported primary outcome.
|
||||
2. Personally trace both shipped and current behavior end to end: entry point,
|
||||
canonical owner, caller, callee, dependency contract, sibling surfaces, and
|
||||
existing tests. Reproduce the exact reported failure on the affected build
|
||||
and prove the same user action succeeds on current `main`. Use a runnable
|
||||
product or boundary-level regression; a nearby unit test, revised error text,
|
||||
or an unexecuted source inspection is insufficient.
|
||||
3. Prove Git ancestry rather than inferring it from dates:
|
||||
|
||||
```bash
|
||||
git merge-base --is-ancestor "$fix_sha" "$current_main_sha"
|
||||
git merge-base --is-ancestor "$fix_sha" "$reported_build_or_tag_sha"
|
||||
git tag --contains "$fix_sha"
|
||||
```
|
||||
|
||||
The fix must be an ancestor of current `main`. Compare it against **each**
|
||||
affected exact build/tag, account for diverged release branches, and identify
|
||||
the first containing release when known. A merge before a release date does
|
||||
not prove inclusion in that release. If the fix was already in an affected
|
||||
build, assume the report still reproduces until a later causal fix is proved.
|
||||
|
||||
4. Classify the candidate honestly: root-cause repair, mitigation, diagnostic
|
||||
improvement, unsupported contract, workaround, or product decision. Never
|
||||
close because a suggested fallback landed if the primary action still fails,
|
||||
any reported surface remains broken, an owner hold exists, or documented
|
||||
behavior requires an unresolved maintainer/security/product decision.
|
||||
5. Require a **different, independent subagent with inherited xhigh reasoning**
|
||||
to challenge the investigator's closure packet. The challenger personally
|
||||
verifies the primary outcome, every affected surface, runtime owner and
|
||||
contract, release ancestry, and before/after proof. The investigator cannot
|
||||
self-approve; only a separate authorized closure coordinator may grant the
|
||||
mutation after both reviewers agree. Any disagreement means **leave open**.
|
||||
6. Immediately recheck live GitHub state, labels/owner holds, current `main`,
|
||||
and exact proof. Do not close on stale state, an incomplete source map, an
|
||||
indirect main-only test, changed wording without changed behavior, or any
|
||||
unresolved facet. In **one sentence**, the closure comment must state the
|
||||
exact fixed behavior, fix SHA/PR, first containing version when known, and
|
||||
before/after evidence.
|
||||
7. If a closure is challenged or an incorrectly closed issue is reopened,
|
||||
**pause all closure mutations**. Audit earlier closures, correct the public
|
||||
record, reopen proven mistakes, and resume only after explicit root
|
||||
authorization. Continue safe investigation and verified code-fix work.
|
||||
|
||||
Required evidence map:
|
||||
|
||||
```text
|
||||
Primary symptom -> expected outcome -> every reported surface -> affected build/tag
|
||||
Entry -> caller -> canonical owner -> callee -> dependency -> sibling -> boundary proof
|
||||
Fix SHA -> current-main ancestry -> each affected-build ancestry -> containing release
|
||||
Affected-build failure -> current-main success -> independent challenge -> coordinator grant
|
||||
```
|
||||
|
||||
Reject example: a remote command fails because its explicit working directory
|
||||
does not exist on the target host. A merged change that only replaces a vague
|
||||
spawn error with an accurate invalid-directory diagnostic is useful, but the
|
||||
command still fails. If the primary expected outcome is successful execution,
|
||||
leave the issue open; changing that explicit-directory contract may need an
|
||||
owner decision.
|
||||
|
||||
## Verify behavior and obtain two independent reviews
|
||||
|
||||
For every non-trivial production change:
|
||||
|
||||
1. Add focused regression coverage for the original bug and affected sibling
|
||||
paths. Delete tests protecting removed obsolete implementation details.
|
||||
2. Choose proof with `$openclaw-testing`. Live-test the real user/provider/
|
||||
channel/CLI/package/UI path whenever feasible. Route heavy, packaging,
|
||||
Docker, E2E, or broad checks through `$crabbox`; report an unavailable live
|
||||
prerequisite accurately instead of calling a mock live proof.
|
||||
3. Classify source trust before executing anything. Never run contributor/fork
|
||||
scripts, hooks, config, tests, installs, or wrappers locally or on a
|
||||
credential-hydrated host; follow the sanitized untrusted-source workflow.
|
||||
4. Run `$autoreview` on the complete final change until no accepted actionable
|
||||
findings remain. Re-run it after any production, test, or reviewed-head
|
||||
change. Treat review findings as hypotheses and verify each against source.
|
||||
Prose-only skill files and other non-production internal notes do not need
|
||||
autoreview; validate their structure and formatting instead.
|
||||
5. Separately self-invoke an independent Codex reviewer. First verify the
|
||||
installed interface with `codex exec --help`, then run a bounded read-only,
|
||||
ephemeral review from a trusted checkout, for example:
|
||||
|
||||
```bash
|
||||
codex exec --json --sandbox read-only --ephemeral \
|
||||
-C "$trusted_checkout" --output-last-message "$review_result" \
|
||||
"Independently inspect the frozen candidate diff and its owner, callers,
|
||||
siblings, tests, current main, user behavior, and dependency contracts.
|
||||
Report only concrete correctness, architecture, simplification, or
|
||||
verification gaps. Do not modify files or expose secrets." \
|
||||
>/dev/null 2>/dev/null
|
||||
```
|
||||
|
||||
Point the reviewer at the exact immutable diff/head. Do not substitute the
|
||||
`$autoreview` Codex engine for this separate pass. Never run that reviewer
|
||||
from an untrusted project-controlled checkout. Read only the final review
|
||||
result; do not emit raw model banners. Verify actionable findings, make
|
||||
justified fixes, rerun proof, and refresh both independent reviews.
|
||||
|
||||
6. Read the latest ClawSweeper comment and address each applicable `Rank-up
|
||||
moves:` item with real evidence or an explicit reason for skipping it.
|
||||
|
||||
## Publish, land, and clean up
|
||||
|
||||
- Prefer an existing writable contributor PR. If its head is unsuitable or
|
||||
cannot be updated safely, open a focused replacement, explain the
|
||||
relationship, and preserve attribution.
|
||||
- Before opening replacement PRs, verify author association, active-PR counts,
|
||||
repository permission, branch policy, current auto-response exemptions, and
|
||||
override labels; never assume a privileged-role exemption. Reuse or land
|
||||
existing reviewed work before creating a burst of competing PRs.
|
||||
- Use the actual PR template and state the user impact, canonical root cause,
|
||||
rejected alternatives, production LOC delta, exact head SHA, focused/live
|
||||
proof, autoreview result, independent Codex result, CI state, and credit.
|
||||
- Read `$agent-transcript` for agent-created PRs, but do not include logs
|
||||
without the user's explicit transcript approval. During a fully autonomous
|
||||
sweep, omit transcripts rather than interrupting the user for consent.
|
||||
- Open new PRs as drafts, wait for a non-null mergeability result, mark them
|
||||
ready, and verify CI attached to the exact pushed head before landing.
|
||||
- Autonomously land only a reproduced, high-confidence, bounded-risk repair
|
||||
or behavior-neutral simplification with clean independent reviews and green
|
||||
exact-head required proof. Change size alone is not the risk criterion.
|
||||
- For main-targeted PRs use only the repo-native `scripts/pr` flow: initialize
|
||||
review, create/validate review artifacts, run
|
||||
`OPENCLAW_TESTBOX=1 scripts/pr prepare-run <number>`, then
|
||||
`scripts/pr merge-run <number>`. Verify the canonical merge SHA afterward.
|
||||
- Keep owner/security/auth/config/public-SDK/protocol/persistent-state/product
|
||||
decisions outside autonomous landing when the relevant guide requires owner
|
||||
judgment. Continue with the next issue instead of blocking the whole sweep.
|
||||
- Close a fixed issue only after the complete **Hard issue-closure gate**,
|
||||
independent challenger sign-off, coordinator grant, and fresh live recheck.
|
||||
Cite the exact causal PR/commit and first containing release when known.
|
||||
- Never close merely because a repro is difficult, the report is inconvenient,
|
||||
the behavior might be intentional, or the PR is stale. Product-decision and
|
||||
won't-implement closures require maintainer judgment.
|
||||
- If no fix is possible, comment only when supplying new reproducible steps,
|
||||
an exact failing owner/line, verified dependency behavior, previously
|
||||
unidentified duplicate/fixing PR, a concrete workaround, or another
|
||||
meaningful fact absent from prior discussion and ClawSweeper.
|
||||
- Recheck live state immediately before every mutation; avoid redundant,
|
||||
speculative, noisy, or duplicate comments. Handle closures individually and
|
||||
follow repository limits on bulk operations.
|
||||
- After verifying the canonical landed SHA and preserving contributor credit,
|
||||
remove only that campaign-owned isolated worktree during a brief serialized
|
||||
Git mutation slot. Delete its campaign-owned branch only when no unlanded
|
||||
work depends on it; never prune unrelated worktrees, refs, or user files.
|
||||
|
||||
## Parent-thread reporting
|
||||
|
||||
Send concise progress plus URLs only. Prefer updates such as:
|
||||
|
||||
```text
|
||||
64 agents active · 41 investigated · 3 landed · 5 already-fixed issues closed
|
||||
Landed: https://github.com/openclaw/openclaw/pull/123
|
||||
Closed: https://github.com/openclaw/openclaw/issues/456
|
||||
```
|
||||
|
||||
Do not narrate routine reads, pending hypotheses, unchanged CI, or candidate
|
||||
URLs that are not actually ready. Count only verified merged PRs, confirmed
|
||||
closures, and comments that were really posted. Continue until the user stops
|
||||
the sweep, the requested boundary is reached, or the live issue queue is
|
||||
genuinely exhausted.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "OpenClaw Autonomous Issue Sweep"
|
||||
short_description: "Autonomously fix, refactor, land, and close issues"
|
||||
default_prompt: "Use $openclaw-autonomous-issue-sweep to orchestrate 64 subagents through OpenClaw issues newest to oldest; reuse existing PRs, prove and land high-confidence fixes or refactors, close resolved issues, and report concise progress plus URLs."
|
||||
@@ -24,7 +24,6 @@ concurrency:
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
PNPM_CONFIG_STORE_DIR: "/tmp/openclaw-pnpm-store"
|
||||
PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: "false"
|
||||
|
||||
jobs:
|
||||
@@ -99,6 +98,10 @@ jobs:
|
||||
with:
|
||||
install-bun: "false"
|
||||
install-trufflehog: "true"
|
||||
# Real Testbox hydration reuses the protected dependency snapshot.
|
||||
# Pull-request validation runs on GitHub-hosted runners instead.
|
||||
sticky-disk: ${{ github.event_name == 'workflow_dispatch' && 'true' || 'false' }}
|
||||
use-actions-cache: ${{ github.event_name == 'workflow_dispatch' && 'false' || 'true' }}
|
||||
- name: Prepare Testbox shell
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -185,3 +188,23 @@ jobs:
|
||||
if: github.event_name == 'workflow_dispatch' && always()
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
- name: Close Testbox SSH sessions
|
||||
if: github.event_name == 'workflow_dispatch' && always()
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Testbox state stores Blacksmith's external forwarded port. Resolve
|
||||
# sshd's VM-local listener because that is the sport visible to ss.
|
||||
runner_ssh_local_port="$(sudo sshd -T 2>/dev/null | awk '$1 == "port" { print $2; exit }')"
|
||||
if [[ ! "$runner_ssh_local_port" =~ ^[0-9]+$ ]] ||
|
||||
(( runner_ssh_local_port < 1 || runner_ssh_local_port > 65535 )); then
|
||||
echo "No valid local SSH listener port found; skipping session cleanup"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# run-testbox has no post hook. Close only Testbox client sockets so
|
||||
# Blacksmith's runner teardown does not wait for its 290-second grace.
|
||||
timeout --signal=KILL 5s sudo ss -K state established \
|
||||
"( sport = :${runner_ssh_local_port} )" || true
|
||||
|
||||
@@ -352,7 +352,7 @@ jobs:
|
||||
}
|
||||
attempt_started_at="$(jq -er '.run_started_at | fromdateiso8601' <<< "$attempt_json")"
|
||||
if [[ "$ARTIFACT_RUN_ID" == "$GITHUB_RUN_ID" ]]; then
|
||||
jq -e '(.status == "pending" or .status == "queued" or .status == "in_progress") and .conclusion == null' \
|
||||
jq -e '(.status == "pending" or .status == "queued" or .status == "requested" or .status == "waiting" or .status == "in_progress") and .conclusion == null' \
|
||||
<<< "$attempt_json" >/dev/null || {
|
||||
echo "Current-run Package Telegram artifact is not from the active workflow attempt." >&2
|
||||
exit 1
|
||||
|
||||
@@ -154,8 +154,8 @@ jobs:
|
||||
deep_profile: "false"
|
||||
live: "false"
|
||||
managed_service: "true"
|
||||
include_filters: "scenario:fresh-install,scenario:gateway-performance,scenario:bundled-plugin-startup,scenario:bundled-runtime-deps,scenario:agent-cold-warm-message"
|
||||
expected_release_entries: "fresh-install:fresh,fresh-install:onboarded-user,bundled-runtime-deps:missing-plugin-index,bundled-plugin-startup:fresh,agent-cold-warm-message:mock-openai-provider,gateway-performance:many-bundled-plugins"
|
||||
include_filters: "scenario:fresh-install,scenario:gateway-performance,scenario:bundled-plugin-startup,scenario:agent-cold-warm-message"
|
||||
expected_release_entries: "fresh-install:fresh,fresh-install:onboarded-user,bundled-plugin-startup:fresh,agent-cold-warm-message:mock-openai-provider,gateway-performance:many-bundled-plugins"
|
||||
- lane: mock-deep-profile
|
||||
title: Kova mock provider deep profile
|
||||
auth: mock
|
||||
|
||||
@@ -480,7 +480,47 @@ jobs:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
install-bun: "true"
|
||||
|
||||
- name: Resolve Buzz QA runner
|
||||
id: resolve_buzz
|
||||
env:
|
||||
SELECTED_REVISION: ${{ needs.validate_selected_ref.outputs.selected_revision }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
output_dir=".artifacts/qa-e2e/buzz-live-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
|
||||
mkdir -p "${output_dir}"
|
||||
echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
OUTPUT_DIR="${output_dir}" node --input-type=module <<'NODE'
|
||||
import fs from "node:fs";
|
||||
|
||||
const manifestPath = "extensions/buzz/openclaw.plugin.json";
|
||||
const manifest = fs.existsSync(manifestPath)
|
||||
? JSON.parse(fs.readFileSync(manifestPath, "utf8"))
|
||||
: {};
|
||||
const available =
|
||||
Array.isArray(manifest.qaRunners) &&
|
||||
manifest.qaRunners.some((runner) => runner?.commandName === "buzz");
|
||||
fs.appendFileSync(process.env.GITHUB_OUTPUT, `available=${available}\n`);
|
||||
if (!available) {
|
||||
fs.writeFileSync(
|
||||
`${process.env.OUTPUT_DIR}/skipped.json`,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
status: "skipped",
|
||||
reason: "selected ref does not declare the Buzz QA runner",
|
||||
revision: process.env.SELECTED_REVISION,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
}
|
||||
NODE
|
||||
|
||||
- name: Validate required Buzz QA credential env
|
||||
if: steps.resolve_buzz.outputs.available == 'true'
|
||||
env:
|
||||
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
|
||||
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
|
||||
@@ -496,11 +536,13 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Build private QA runtime
|
||||
if: steps.resolve_buzz.outputs.available == 'true'
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=12288
|
||||
run: pnpm build
|
||||
|
||||
- name: Run Buzz live lane
|
||||
if: steps.resolve_buzz.outputs.available == 'true'
|
||||
id: run_lane
|
||||
shell: bash
|
||||
env:
|
||||
@@ -513,7 +555,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
output_dir=".artifacts/qa-e2e/buzz-live-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
|
||||
output_dir="${{ steps.resolve_buzz.outputs.output_dir }}"
|
||||
scenario_args=()
|
||||
|
||||
if [[ -n "${INPUT_SCENARIO// }" ]]; then
|
||||
@@ -541,7 +583,7 @@ jobs:
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: ${{ inputs.expected_sha != '' && format('release-qa-live-buzz-{0}-{1}', inputs.expected_sha, github.run_attempt) || format('qa-live-buzz-{0}-{1}', github.run_id, github.run_attempt) }}
|
||||
path: ${{ steps.run_lane.outputs.output_dir }}
|
||||
path: ${{ steps.resolve_buzz.outputs.output_dir }}
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
+2
-93
@@ -131,99 +131,8 @@ USER.md
|
||||
# local QA evidence mirrors; CI publishes canonical Mantis files as Actions artifacts
|
||||
/mantis/
|
||||
|
||||
# Local project-agent skill installs. Only repo-owned skills are visible by
|
||||
# default; keep every tracked repo skill allowlisted for Git-aware sync tools.
|
||||
.agents/skills/*
|
||||
!.agents/skills/agent-transcript/
|
||||
!.agents/skills/agent-transcript/**
|
||||
!.agents/skills/auto-qa/
|
||||
!.agents/skills/auto-qa/**
|
||||
!.agents/skills/autoreview/
|
||||
!.agents/skills/autoreview/**
|
||||
!.agents/skills/channel-message-flows/
|
||||
!.agents/skills/channel-message-flows/**
|
||||
!.agents/skills/claw-score/
|
||||
!.agents/skills/claw-score/**
|
||||
!.agents/skills/clawdtributor/
|
||||
!.agents/skills/clawdtributor/**
|
||||
!.agents/skills/clawsweeper/
|
||||
!.agents/skills/clawsweeper/**
|
||||
!.agents/skills/control-ui-e2e/
|
||||
!.agents/skills/control-ui-e2e/**
|
||||
!.agents/skills/crabbox/
|
||||
!.agents/skills/crabbox/**
|
||||
!.agents/skills/discord-clawd/
|
||||
!.agents/skills/discord-clawd/**
|
||||
!.agents/skills/discord-user-post/
|
||||
!.agents/skills/discord-user-post/**
|
||||
!.agents/skills/discrawl/
|
||||
!.agents/skills/discrawl/**
|
||||
!.agents/skills/gitcrawl/
|
||||
!.agents/skills/gitcrawl/**
|
||||
!.agents/skills/graincrawl/
|
||||
!.agents/skills/graincrawl/**
|
||||
!.agents/skills/notcrawl/
|
||||
!.agents/skills/notcrawl/**
|
||||
!.agents/skills/openclaw-changelog-update/
|
||||
!.agents/skills/openclaw-changelog-update/**
|
||||
!.agents/skills/openclaw-ci-limits/
|
||||
!.agents/skills/openclaw-ci-limits/**
|
||||
!.agents/skills/openclaw-debugging/
|
||||
!.agents/skills/openclaw-debugging/**
|
||||
!.agents/skills/openclaw-docker-e2e-authoring/
|
||||
!.agents/skills/openclaw-docker-e2e-authoring/**
|
||||
!.agents/skills/openclaw-ghsa-maintainer/
|
||||
!.agents/skills/openclaw-ghsa-maintainer/**
|
||||
!.agents/skills/openclaw-landable-bug-sweep/
|
||||
!.agents/skills/openclaw-landable-bug-sweep/**
|
||||
!.agents/skills/openclaw-live-updater/
|
||||
!.agents/skills/openclaw-live-updater/**
|
||||
!.agents/skills/openclaw-parallels-smoke/
|
||||
!.agents/skills/openclaw-parallels-smoke/**
|
||||
!.agents/skills/openclaw-pr-maintainer/
|
||||
!.agents/skills/openclaw-pr-maintainer/**
|
||||
!.agents/skills/prototype-openclaw-tui/
|
||||
!.agents/skills/prototype-openclaw-tui/**
|
||||
!.agents/skills/openclaw-qa-testing/
|
||||
!.agents/skills/openclaw-qa-testing/**
|
||||
!.agents/skills/openclaw-refactor-docs/
|
||||
!.agents/skills/openclaw-refactor-docs/**
|
||||
!.agents/skills/openclaw-secret-scanning-maintainer/
|
||||
!.agents/skills/openclaw-secret-scanning-maintainer/**
|
||||
!.agents/skills/openclaw-small-bugfix-sweep/
|
||||
!.agents/skills/openclaw-small-bugfix-sweep/**
|
||||
!.agents/skills/openclaw-test-heap-leaks/
|
||||
!.agents/skills/openclaw-test-heap-leaks/**
|
||||
!.agents/skills/openclaw-test-performance/
|
||||
!.agents/skills/openclaw-test-performance/**
|
||||
!.agents/skills/openclaw-testing/
|
||||
!.agents/skills/openclaw-testing/**
|
||||
!.agents/skills/parallels-discord-roundtrip/
|
||||
!.agents/skills/parallels-discord-roundtrip/**
|
||||
!.agents/skills/release-openclaw-announcement/
|
||||
!.agents/skills/release-openclaw-announcement/**
|
||||
!.agents/skills/release-openclaw-ci/
|
||||
!.agents/skills/release-openclaw-ci/**
|
||||
!.agents/skills/release-openclaw-mac/
|
||||
!.agents/skills/release-openclaw-mac/**
|
||||
!.agents/skills/release-openclaw-maintainer/
|
||||
!.agents/skills/release-openclaw-maintainer/**
|
||||
!.agents/skills/release-openclaw-nightly/
|
||||
!.agents/skills/release-openclaw-nightly/**
|
||||
!.agents/skills/release-openclaw-plugin-testing/
|
||||
!.agents/skills/release-openclaw-plugin-testing/**
|
||||
!.agents/skills/security-triage/
|
||||
!.agents/skills/security-triage/**
|
||||
!.agents/skills/slacrawl/
|
||||
!.agents/skills/slacrawl/**
|
||||
!.agents/skills/tag-duplicate-prs-issues/
|
||||
!.agents/skills/tag-duplicate-prs-issues/**
|
||||
!.agents/skills/technical-documentation/
|
||||
!.agents/skills/technical-documentation/**
|
||||
!.agents/skills/telegram-crabbox-e2e-proof/
|
||||
!.agents/skills/telegram-crabbox-e2e-proof/**
|
||||
!.agents/skills/verify-release/
|
||||
!.agents/skills/verify-release/**
|
||||
# Keep repository skills visible to Git-aware syncs; local-only skill ignores
|
||||
# belong in .git/info/exclude.
|
||||
.agents/skills/**/*.orig
|
||||
.agents/skills/**/__pycache__/
|
||||
.agents/skills/**/*.py[cod]
|
||||
|
||||
@@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai
|
||||
- **External gateway supervision:** add `OPENCLAW_SUPERVISOR_MODE=external` for lifecycle owners such as OCM, preserving verified restart and deferral behavior without exposing native service authority, blocking native service mutation and self-update, and providing a versioned atomic restart-handoff consume contract. Thanks @shakkernerd.
|
||||
- **Buzz message fidelity:** preserve Markdown output and accept Buzz normal, rich-content, and structured-diff room messages through the existing authorized inbound path. Thanks @shakkernerd.
|
||||
- **Buzz typing indicators:** show room- and thread-scoped typing during agent replies and heartbeat deliveries, refresh through the active authenticated connection without waiting for relay acknowledgement, and drop ephemeral updates safely during disconnects or shutdown. Thanks @shakkernerd.
|
||||
- **Buzz sender directory:** expose current bot, member, room, and room-member directory entries from bounded relay state; use current Buzz profile and room names in inbound context while preserving public keys and UUIDs as stable authorization and routing identities. Thanks @shakkernerd.
|
||||
- **ClickClack guided setup:** configure ClickClack from `openclaw onboard` or `openclaw channels add clickclack` with URL, token, and workspace prompts, default-account env fallback, nonfatal live connection validation, and gateway-aware next steps that connect automatically when OpenClaw is already running. Thanks @shakkernerd.
|
||||
- **ClickClack command menus:** publish each bot's native OpenClaw commands to ClickClack composer autocomplete at gateway startup, with per-account opt-out and nonfatal compatibility handling for older tokens and servers. Thanks @shakkernerd.
|
||||
- **Skill Workshop approvals:** run agent-initiated apply, reject, and quarantine actions without an additional approval prompt by default while preserving `skills.workshop.approvalPolicy: "pending"` as an opt-in approval gate. Thanks @shakkernerd.
|
||||
@@ -57,6 +58,10 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- **Buzz plugin packaging:** keep the live QA runner on the shipped QA runner SDK surface and remove the obsolete package shrinkwrap so standalone npm and ClawHub package builds use current host exports and dependency resolutions. Thanks @shakkernerd.
|
||||
- **Control UI sharing connection isolation:** discard stale visibility and membership mutation results after switching gateways or accounts so previous-connection refreshes and errors cannot update the replacement connection. Fixes #116800. Thanks @shakkernerd.
|
||||
- **Control UI session refreshes:** preserve explicitly queued list filters and background hydration across later Gateway event invalidation, while keeping append pagination followed by a canonical refresh. Fixes #116697. Thanks @shakkernerd.
|
||||
- **Gateway device clock skew:** sign device proofs with the Gateway-issued challenge timestamp across TypeScript, Control UI, browser extension, Android, Apple, Linux, and watchOS clients so incorrect local clocks no longer block authentication, while retaining no-challenge compatibility for pre-challenge Control UI servers and older watch-node HTTP endpoints and keeping nonce binding and freshness checks enforced. Fixes #103455.
|
||||
- **Control UI dynamic deep links:** reuse the initial route loader result when publishing real agent, session, dashboard, Workboard, Memory, and Plugins paths, avoiding redundant route-loader work during startup. Thanks @shakkernerd.
|
||||
- **Linux gateway service ownership:** refuse user-scope systemd publication and activation when the same gateway unit name is already owned or cannot be verified in the system scope, including `--force`, with actionable recovery guidance instead of creating restart-looping dual managers. Fixes #116129.
|
||||
- **macOS remote tunnel lifecycle:** prevent cancelled or superseded restart backoffs from recreating SSH tunnels, and join a tunnel create that another caller started while the actor was suspended.
|
||||
|
||||
@@ -1915,7 +1915,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 1034,
|
||||
"line": 1039,
|
||||
"path": "apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewaySession.kt",
|
||||
"source": "Accept",
|
||||
"surface": "android",
|
||||
@@ -1923,7 +1923,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 1834,
|
||||
"line": 1849,
|
||||
"path": "apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewaySession.kt",
|
||||
"source": "Connecting…",
|
||||
"surface": "android",
|
||||
@@ -1931,7 +1931,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 1834,
|
||||
"line": 1849,
|
||||
"path": "apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewaySession.kt",
|
||||
"source": "Reconnecting…",
|
||||
"surface": "android",
|
||||
@@ -28683,7 +28683,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 64,
|
||||
"line": 84,
|
||||
"path": "apps/ios/WatchApp/Sources/WatchDirectNode.swift",
|
||||
"source": "Gateway HTTP error (%@)",
|
||||
"surface": "apple",
|
||||
@@ -28691,7 +28691,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 121,
|
||||
"line": 141,
|
||||
"path": "apps/ios/WatchApp/Sources/WatchDirectNode.swift",
|
||||
"source": "Ready to connect",
|
||||
"surface": "apple",
|
||||
@@ -28699,7 +28699,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 132,
|
||||
"line": 152,
|
||||
"path": "apps/ios/WatchApp/Sources/WatchDirectNode.swift",
|
||||
"source": "Ignored an expired direct connection setup. Send setup again from iPhone.",
|
||||
"surface": "apple",
|
||||
@@ -28707,7 +28707,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 145,
|
||||
"line": 165,
|
||||
"path": "apps/ios/WatchApp/Sources/WatchDirectNode.swift",
|
||||
"source": "Direct mode requires a trusted HTTPS Gateway endpoint.",
|
||||
"surface": "apple",
|
||||
@@ -28715,7 +28715,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 163,
|
||||
"line": 183,
|
||||
"path": "apps/ios/WatchApp/Sources/WatchDirectNode.swift",
|
||||
"source": "Could not save direct connection securely.",
|
||||
"surface": "apple",
|
||||
@@ -28723,7 +28723,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 180,
|
||||
"line": 200,
|
||||
"path": "apps/ios/WatchApp/Sources/WatchDirectNode.swift",
|
||||
"source": "Setup received. Connecting…",
|
||||
"surface": "apple",
|
||||
@@ -28731,7 +28731,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 196,
|
||||
"line": 216,
|
||||
"path": "apps/ios/WatchApp/Sources/WatchDirectNode.swift",
|
||||
"source": "Direct connection is off",
|
||||
"surface": "apple",
|
||||
@@ -28739,7 +28739,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 197,
|
||||
"line": 217,
|
||||
"path": "apps/ios/WatchApp/Sources/WatchDirectNode.swift",
|
||||
"source": "Use iPhone Settings to enable direct connection.",
|
||||
"surface": "apple",
|
||||
@@ -28747,7 +28747,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 225,
|
||||
"line": 245,
|
||||
"path": "apps/ios/WatchApp/Sources/WatchDirectNode.swift",
|
||||
"source": "Reconnects when OpenClaw is active",
|
||||
"surface": "apple",
|
||||
@@ -28755,7 +28755,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 279,
|
||||
"line": 299,
|
||||
"path": "apps/ios/WatchApp/Sources/WatchDirectNode.swift",
|
||||
"source": "Direct connection failed: %@",
|
||||
"surface": "apple",
|
||||
@@ -28763,7 +28763,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 281,
|
||||
"line": 301,
|
||||
"path": "apps/ios/WatchApp/Sources/WatchDirectNode.swift",
|
||||
"source": "No usable Gateway endpoint",
|
||||
"surface": "apple",
|
||||
@@ -28771,7 +28771,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 297,
|
||||
"line": 317,
|
||||
"path": "apps/ios/WatchApp/Sources/WatchDirectNode.swift",
|
||||
"source": "Connecting directly…",
|
||||
"surface": "apple",
|
||||
@@ -28779,7 +28779,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 301,
|
||||
"line": 321,
|
||||
"path": "apps/ios/WatchApp/Sources/WatchDirectNode.swift",
|
||||
"source": "Could not save the watch device identity",
|
||||
"surface": "apple",
|
||||
@@ -28787,7 +28787,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 331,
|
||||
"line": 351,
|
||||
"path": "apps/ios/WatchApp/Sources/WatchDirectNode.swift",
|
||||
"source": "No watch device credential",
|
||||
"surface": "apple",
|
||||
@@ -28795,7 +28795,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 348,
|
||||
"line": 368,
|
||||
"path": "apps/ios/WatchApp/Sources/WatchDirectNode.swift",
|
||||
"source": "Could not save the watch device credential",
|
||||
"surface": "apple",
|
||||
@@ -28803,7 +28803,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 362,
|
||||
"line": 382,
|
||||
"path": "apps/ios/WatchApp/Sources/WatchDirectNode.swift",
|
||||
"source": "Connected directly",
|
||||
"surface": "apple",
|
||||
@@ -28811,7 +28811,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 466,
|
||||
"line": 488,
|
||||
"path": "apps/ios/WatchApp/Sources/WatchDirectNode.swift",
|
||||
"source": "Could not sign watch identity",
|
||||
"surface": "apple",
|
||||
@@ -28819,7 +28819,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 559,
|
||||
"line": 581,
|
||||
"path": "apps/ios/WatchApp/Sources/WatchDirectNode.swift",
|
||||
"source": "Invalid Gateway response",
|
||||
"surface": "apple",
|
||||
@@ -28827,7 +28827,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 584,
|
||||
"line": 606,
|
||||
"path": "apps/ios/WatchApp/Sources/WatchDirectNode.swift",
|
||||
"source": "Paired, but could not finish secure setup",
|
||||
"surface": "apple",
|
||||
@@ -39939,7 +39939,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 470,
|
||||
"line": 474,
|
||||
"path": "apps/macos/Sources/OpenClawMacCLI/WizardCommand.swift",
|
||||
"source": " [\\(initial)]",
|
||||
"surface": "apple",
|
||||
@@ -39947,7 +39947,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 517,
|
||||
"line": 521,
|
||||
"path": "apps/macos/Sources/OpenClawMacCLI/WizardCommand.swift",
|
||||
"source": " — \\(option.hint!)",
|
||||
"surface": "apple",
|
||||
@@ -39955,7 +39955,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 524,
|
||||
"line": 528,
|
||||
"path": "apps/macos/Sources/OpenClawMacCLI/WizardCommand.swift",
|
||||
"source": " [\\(initialIndices.map(String.init).joined(separator: \",\"))]",
|
||||
"surface": "apple",
|
||||
|
||||
@@ -21999,14 +21999,14 @@
|
||||
"translated": "إعداد نموذج محلي"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.506a932536c642c2",
|
||||
"source": "Download or prepare a local model on this Gateway.",
|
||||
"translated": "نزّل أو جهّز نموذجًا محليًا على هذا الـ Gateway."
|
||||
"id": "native.apple.780c1aa1c8868cc4",
|
||||
"source": "Connect a local model service, or prepare a model on this Gateway.",
|
||||
"translated": "اتصل بخدمة نموذج محلي، أو جهّز نموذجًا على هذا الـ Gateway."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.d31411a9f3c68c33",
|
||||
"source": "Set up / Download model",
|
||||
"translated": "إعداد / تنزيل النموذج"
|
||||
"id": "native.apple.c218bd844ba8a0ac",
|
||||
"source": "Connect / Set up",
|
||||
"translated": "الاتصال / الإعداد"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7fc409b6aaa6d3b9",
|
||||
@@ -22039,9 +22039,9 @@
|
||||
"translated": "تسجيل الدخول"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b62dfd708d5cfb14",
|
||||
"source": "The model is downloaded and prepared on this Gateway.",
|
||||
"translated": "تم تنزيل النموذج وتجهيزه على هذا الـ Gateway."
|
||||
"id": "native.apple.97b24c854f768998",
|
||||
"source": "OpenClaw will detect and verify the prepared model before using it.",
|
||||
"translated": "سيكتشف OpenClaw النموذج المُجهّز ويتحقق منه قبل استخدامه."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.869fe436119bb8a6",
|
||||
|
||||
@@ -21999,14 +21999,14 @@
|
||||
"translated": "Lokales Modell einrichten"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.506a932536c642c2",
|
||||
"source": "Download or prepare a local model on this Gateway.",
|
||||
"translated": "Laden Sie ein lokales Modell auf diesem Gateway herunter oder bereiten Sie es vor."
|
||||
"id": "native.apple.780c1aa1c8868cc4",
|
||||
"source": "Connect a local model service, or prepare a model on this Gateway.",
|
||||
"translated": "Verbinden Sie einen lokalen Modelldienst oder bereiten Sie ein Modell auf diesem Gateway vor."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.d31411a9f3c68c33",
|
||||
"source": "Set up / Download model",
|
||||
"translated": "Modell einrichten / herunterladen"
|
||||
"id": "native.apple.c218bd844ba8a0ac",
|
||||
"source": "Connect / Set up",
|
||||
"translated": "Verbinden / Einrichten"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7fc409b6aaa6d3b9",
|
||||
@@ -22039,9 +22039,9 @@
|
||||
"translated": "Anmelden"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b62dfd708d5cfb14",
|
||||
"source": "The model is downloaded and prepared on this Gateway.",
|
||||
"translated": "Das Modell wurde auf diesem Gateway heruntergeladen und vorbereitet."
|
||||
"id": "native.apple.97b24c854f768998",
|
||||
"source": "OpenClaw will detect and verify the prepared model before using it.",
|
||||
"translated": "OpenClaw erkennt und überprüft das vorbereitete Modell, bevor es verwendet wird."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.869fe436119bb8a6",
|
||||
|
||||
@@ -21999,14 +21999,14 @@
|
||||
"translated": "Configurar un modelo local"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.506a932536c642c2",
|
||||
"source": "Download or prepare a local model on this Gateway.",
|
||||
"translated": "Descarga o prepara un modelo local en este Gateway."
|
||||
"id": "native.apple.780c1aa1c8868cc4",
|
||||
"source": "Connect a local model service, or prepare a model on this Gateway.",
|
||||
"translated": "Conecta un servicio de modelo local o prepara un modelo en este Gateway."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.d31411a9f3c68c33",
|
||||
"source": "Set up / Download model",
|
||||
"translated": "Configurar / Descargar modelo"
|
||||
"id": "native.apple.c218bd844ba8a0ac",
|
||||
"source": "Connect / Set up",
|
||||
"translated": "Conectar / Configurar"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7fc409b6aaa6d3b9",
|
||||
@@ -22039,9 +22039,9 @@
|
||||
"translated": "Iniciar sesión"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b62dfd708d5cfb14",
|
||||
"source": "The model is downloaded and prepared on this Gateway.",
|
||||
"translated": "El modelo se ha descargado y preparado en este Gateway."
|
||||
"id": "native.apple.97b24c854f768998",
|
||||
"source": "OpenClaw will detect and verify the prepared model before using it.",
|
||||
"translated": "OpenClaw detectará y verificará el modelo preparado antes de usarlo."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.869fe436119bb8a6",
|
||||
|
||||
@@ -21999,14 +21999,14 @@
|
||||
"translated": "راهاندازی یک مدل محلی"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.506a932536c642c2",
|
||||
"source": "Download or prepare a local model on this Gateway.",
|
||||
"translated": "دانلود یا آمادهسازی یک مدل محلی روی این Gateway."
|
||||
"id": "native.apple.780c1aa1c8868cc4",
|
||||
"source": "Connect a local model service, or prepare a model on this Gateway.",
|
||||
"translated": "یک سرویس مدل محلی متصل کنید یا مدلی را روی این Gateway آماده کنید."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.d31411a9f3c68c33",
|
||||
"source": "Set up / Download model",
|
||||
"translated": "راهاندازی / دانلود مدل"
|
||||
"id": "native.apple.c218bd844ba8a0ac",
|
||||
"source": "Connect / Set up",
|
||||
"translated": "اتصال / راهاندازی"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7fc409b6aaa6d3b9",
|
||||
@@ -22039,9 +22039,9 @@
|
||||
"translated": "ورود"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b62dfd708d5cfb14",
|
||||
"source": "The model is downloaded and prepared on this Gateway.",
|
||||
"translated": "مدل روی این Gateway دانلود و آماده شده است."
|
||||
"id": "native.apple.97b24c854f768998",
|
||||
"source": "OpenClaw will detect and verify the prepared model before using it.",
|
||||
"translated": "OpenClaw قبل از استفاده، مدل آمادهشده را شناسایی و تأیید میکند."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.869fe436119bb8a6",
|
||||
|
||||
@@ -21999,14 +21999,14 @@
|
||||
"translated": "Configurer un modèle local"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.506a932536c642c2",
|
||||
"source": "Download or prepare a local model on this Gateway.",
|
||||
"translated": "Téléchargez ou préparez un modèle local sur ce Gateway."
|
||||
"id": "native.apple.780c1aa1c8868cc4",
|
||||
"source": "Connect a local model service, or prepare a model on this Gateway.",
|
||||
"translated": "Connectez un service de modèle local, ou préparez un modèle sur ce Gateway."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.d31411a9f3c68c33",
|
||||
"source": "Set up / Download model",
|
||||
"translated": "Configurer / Télécharger le modèle"
|
||||
"id": "native.apple.c218bd844ba8a0ac",
|
||||
"source": "Connect / Set up",
|
||||
"translated": "Connecter / Configurer"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7fc409b6aaa6d3b9",
|
||||
@@ -22039,9 +22039,9 @@
|
||||
"translated": "Se connecter"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b62dfd708d5cfb14",
|
||||
"source": "The model is downloaded and prepared on this Gateway.",
|
||||
"translated": "Le modèle est téléchargé et préparé sur ce Gateway."
|
||||
"id": "native.apple.97b24c854f768998",
|
||||
"source": "OpenClaw will detect and verify the prepared model before using it.",
|
||||
"translated": "OpenClaw détectera et vérifiera le modèle préparé avant de l'utiliser."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.869fe436119bb8a6",
|
||||
|
||||
@@ -21999,14 +21999,14 @@
|
||||
"translated": "लोकल मॉडल सेट करें"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.506a932536c642c2",
|
||||
"source": "Download or prepare a local model on this Gateway.",
|
||||
"translated": "इस Gateway पर एक लोकल मॉडल डाउनलोड या तैयार करें।"
|
||||
"id": "native.apple.780c1aa1c8868cc4",
|
||||
"source": "Connect a local model service, or prepare a model on this Gateway.",
|
||||
"translated": "एक स्थानीय मॉडल सेवा कनेक्ट करें, या इस Gateway पर एक मॉडल तैयार करें।"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.d31411a9f3c68c33",
|
||||
"source": "Set up / Download model",
|
||||
"translated": "मॉडल सेट अप / डाउनलोड करें"
|
||||
"id": "native.apple.c218bd844ba8a0ac",
|
||||
"source": "Connect / Set up",
|
||||
"translated": "कनेक्ट करें / सेट अप करें"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7fc409b6aaa6d3b9",
|
||||
@@ -22039,9 +22039,9 @@
|
||||
"translated": "साइन इन करें"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b62dfd708d5cfb14",
|
||||
"source": "The model is downloaded and prepared on this Gateway.",
|
||||
"translated": "मॉडल इस Gateway पर डाउनलोड और तैयार किया गया है।"
|
||||
"id": "native.apple.97b24c854f768998",
|
||||
"source": "OpenClaw will detect and verify the prepared model before using it.",
|
||||
"translated": "OpenClaw उपयोग करने से पहले तैयार किए गए मॉडल का पता लगाएगा और सत्यापित करेगा।"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.869fe436119bb8a6",
|
||||
|
||||
@@ -21999,14 +21999,14 @@
|
||||
"translated": "Siapkan model lokal"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.506a932536c642c2",
|
||||
"source": "Download or prepare a local model on this Gateway.",
|
||||
"translated": "Unduh atau siapkan model lokal di Gateway ini."
|
||||
"id": "native.apple.780c1aa1c8868cc4",
|
||||
"source": "Connect a local model service, or prepare a model on this Gateway.",
|
||||
"translated": "Sambungkan layanan model lokal, atau siapkan model di Gateway ini."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.d31411a9f3c68c33",
|
||||
"source": "Set up / Download model",
|
||||
"translated": "Siapkan / Unduh model"
|
||||
"id": "native.apple.c218bd844ba8a0ac",
|
||||
"source": "Connect / Set up",
|
||||
"translated": "Sambungkan / Siapkan"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7fc409b6aaa6d3b9",
|
||||
@@ -22039,9 +22039,9 @@
|
||||
"translated": "Masuk"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b62dfd708d5cfb14",
|
||||
"source": "The model is downloaded and prepared on this Gateway.",
|
||||
"translated": "Model diunduh dan disiapkan di Gateway ini."
|
||||
"id": "native.apple.97b24c854f768998",
|
||||
"source": "OpenClaw will detect and verify the prepared model before using it.",
|
||||
"translated": "OpenClaw akan mendeteksi dan memverifikasi model yang disiapkan sebelum menggunakannya."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.869fe436119bb8a6",
|
||||
|
||||
@@ -21999,14 +21999,14 @@
|
||||
"translated": "Configura un modello locale"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.506a932536c642c2",
|
||||
"source": "Download or prepare a local model on this Gateway.",
|
||||
"translated": "Scarica o prepara un modello locale su questo Gateway."
|
||||
"id": "native.apple.780c1aa1c8868cc4",
|
||||
"source": "Connect a local model service, or prepare a model on this Gateway.",
|
||||
"translated": "Connetti un servizio di modello locale o prepara un modello su questo Gateway."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.d31411a9f3c68c33",
|
||||
"source": "Set up / Download model",
|
||||
"translated": "Configura / Scarica modello"
|
||||
"id": "native.apple.c218bd844ba8a0ac",
|
||||
"source": "Connect / Set up",
|
||||
"translated": "Connetti / Configura"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7fc409b6aaa6d3b9",
|
||||
@@ -22039,9 +22039,9 @@
|
||||
"translated": "Accedi"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b62dfd708d5cfb14",
|
||||
"source": "The model is downloaded and prepared on this Gateway.",
|
||||
"translated": "Il modello è scaricato e preparato su questo Gateway."
|
||||
"id": "native.apple.97b24c854f768998",
|
||||
"source": "OpenClaw will detect and verify the prepared model before using it.",
|
||||
"translated": "OpenClaw rileverà e verificherà il modello preparato prima di utilizzarlo."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.869fe436119bb8a6",
|
||||
|
||||
@@ -21999,14 +21999,14 @@
|
||||
"translated": "ローカルモデルをセットアップ"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.506a932536c642c2",
|
||||
"source": "Download or prepare a local model on this Gateway.",
|
||||
"translated": "この Gateway でローカルモデルをダウンロードまたは準備します。"
|
||||
"id": "native.apple.780c1aa1c8868cc4",
|
||||
"source": "Connect a local model service, or prepare a model on this Gateway.",
|
||||
"translated": "ローカルモデルサービスを接続するか、この Gateway でモデルを準備します。"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.d31411a9f3c68c33",
|
||||
"source": "Set up / Download model",
|
||||
"translated": "モデルのセットアップ / ダウンロード"
|
||||
"id": "native.apple.c218bd844ba8a0ac",
|
||||
"source": "Connect / Set up",
|
||||
"translated": "接続 / セットアップ"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7fc409b6aaa6d3b9",
|
||||
@@ -22039,9 +22039,9 @@
|
||||
"translated": "サインイン"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b62dfd708d5cfb14",
|
||||
"source": "The model is downloaded and prepared on this Gateway.",
|
||||
"translated": "モデルはこの Gateway でダウンロードされ、準備されています。"
|
||||
"id": "native.apple.97b24c854f768998",
|
||||
"source": "OpenClaw will detect and verify the prepared model before using it.",
|
||||
"translated": "OpenClaw は使用する前に、準備されたモデルを検出して検証します。"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.869fe436119bb8a6",
|
||||
|
||||
@@ -21999,14 +21999,14 @@
|
||||
"translated": "로컬 모델 설정"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.506a932536c642c2",
|
||||
"source": "Download or prepare a local model on this Gateway.",
|
||||
"translated": "이 Gateway에서 로컬 모델을 다운로드하거나 준비합니다."
|
||||
"id": "native.apple.780c1aa1c8868cc4",
|
||||
"source": "Connect a local model service, or prepare a model on this Gateway.",
|
||||
"translated": "로컬 모델 서비스를 연결하거나 이 Gateway에서 모델을 준비하세요."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.d31411a9f3c68c33",
|
||||
"source": "Set up / Download model",
|
||||
"translated": "모델 설정 / 다운로드"
|
||||
"id": "native.apple.c218bd844ba8a0ac",
|
||||
"source": "Connect / Set up",
|
||||
"translated": "연결 / 설정"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7fc409b6aaa6d3b9",
|
||||
@@ -22039,9 +22039,9 @@
|
||||
"translated": "로그인"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b62dfd708d5cfb14",
|
||||
"source": "The model is downloaded and prepared on this Gateway.",
|
||||
"translated": "이 Gateway에서 모델이 다운로드되고 준비되었습니다."
|
||||
"id": "native.apple.97b24c854f768998",
|
||||
"source": "OpenClaw will detect and verify the prepared model before using it.",
|
||||
"translated": "OpenClaw는 준비된 모델을 사용하기 전에 이를 감지하고 검증합니다."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.869fe436119bb8a6",
|
||||
|
||||
@@ -21999,14 +21999,14 @@
|
||||
"translated": "Een lokaal model instellen"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.506a932536c642c2",
|
||||
"source": "Download or prepare a local model on this Gateway.",
|
||||
"translated": "Download of bereid een lokaal model voor op deze Gateway."
|
||||
"id": "native.apple.780c1aa1c8868cc4",
|
||||
"source": "Connect a local model service, or prepare a model on this Gateway.",
|
||||
"translated": "Verbind een lokale modelservice of bereid een model voor op deze Gateway."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.d31411a9f3c68c33",
|
||||
"source": "Set up / Download model",
|
||||
"translated": "Model instellen / downloaden"
|
||||
"id": "native.apple.c218bd844ba8a0ac",
|
||||
"source": "Connect / Set up",
|
||||
"translated": "Verbinden / Instellen"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7fc409b6aaa6d3b9",
|
||||
@@ -22039,9 +22039,9 @@
|
||||
"translated": "Aanmelden"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b62dfd708d5cfb14",
|
||||
"source": "The model is downloaded and prepared on this Gateway.",
|
||||
"translated": "Het model is gedownload en voorbereid op deze Gateway."
|
||||
"id": "native.apple.97b24c854f768998",
|
||||
"source": "OpenClaw will detect and verify the prepared model before using it.",
|
||||
"translated": "OpenClaw detecteert en verifieert het voorbereide model voordat het wordt gebruikt."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.869fe436119bb8a6",
|
||||
|
||||
@@ -21999,14 +21999,14 @@
|
||||
"translated": "Skonfiguruj model lokalny"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.506a932536c642c2",
|
||||
"source": "Download or prepare a local model on this Gateway.",
|
||||
"translated": "Pobierz lub przygotuj model lokalny na tym Gateway."
|
||||
"id": "native.apple.780c1aa1c8868cc4",
|
||||
"source": "Connect a local model service, or prepare a model on this Gateway.",
|
||||
"translated": "Połącz lokalną usługę modelu lub przygotuj model na tym Gateway."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.d31411a9f3c68c33",
|
||||
"source": "Set up / Download model",
|
||||
"translated": "Skonfiguruj / Pobierz model"
|
||||
"id": "native.apple.c218bd844ba8a0ac",
|
||||
"source": "Connect / Set up",
|
||||
"translated": "Połącz / Skonfiguruj"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7fc409b6aaa6d3b9",
|
||||
@@ -22039,9 +22039,9 @@
|
||||
"translated": "Zaloguj się"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b62dfd708d5cfb14",
|
||||
"source": "The model is downloaded and prepared on this Gateway.",
|
||||
"translated": "Model został pobrany i przygotowany na tym Gateway."
|
||||
"id": "native.apple.97b24c854f768998",
|
||||
"source": "OpenClaw will detect and verify the prepared model before using it.",
|
||||
"translated": "OpenClaw wykryje i zweryfikuje przygotowany model przed jego użyciem."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.869fe436119bb8a6",
|
||||
|
||||
@@ -21999,14 +21999,14 @@
|
||||
"translated": "Configurar um modelo local"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.506a932536c642c2",
|
||||
"source": "Download or prepare a local model on this Gateway.",
|
||||
"translated": "Baixe ou prepare um modelo local neste Gateway."
|
||||
"id": "native.apple.780c1aa1c8868cc4",
|
||||
"source": "Connect a local model service, or prepare a model on this Gateway.",
|
||||
"translated": "Conecte um serviço de modelo local ou prepare um modelo neste Gateway."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.d31411a9f3c68c33",
|
||||
"source": "Set up / Download model",
|
||||
"translated": "Configurar / Baixar modelo"
|
||||
"id": "native.apple.c218bd844ba8a0ac",
|
||||
"source": "Connect / Set up",
|
||||
"translated": "Conectar / Configurar"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7fc409b6aaa6d3b9",
|
||||
@@ -22039,9 +22039,9 @@
|
||||
"translated": "Entrar"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b62dfd708d5cfb14",
|
||||
"source": "The model is downloaded and prepared on this Gateway.",
|
||||
"translated": "O modelo foi baixado e preparado neste Gateway."
|
||||
"id": "native.apple.97b24c854f768998",
|
||||
"source": "OpenClaw will detect and verify the prepared model before using it.",
|
||||
"translated": "O OpenClaw detectará e verificará o modelo preparado antes de usá-lo."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.869fe436119bb8a6",
|
||||
|
||||
@@ -21999,14 +21999,14 @@
|
||||
"translated": "Настроить локальную модель"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.506a932536c642c2",
|
||||
"source": "Download or prepare a local model on this Gateway.",
|
||||
"translated": "Загрузите или подготовьте локальную модель на этом Gateway."
|
||||
"id": "native.apple.780c1aa1c8868cc4",
|
||||
"source": "Connect a local model service, or prepare a model on this Gateway.",
|
||||
"translated": "Подключите локальный сервис модели или подготовьте модель на этом Gateway."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.d31411a9f3c68c33",
|
||||
"source": "Set up / Download model",
|
||||
"translated": "Настроить / Загрузить модель"
|
||||
"id": "native.apple.c218bd844ba8a0ac",
|
||||
"source": "Connect / Set up",
|
||||
"translated": "Подключить / Настроить"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7fc409b6aaa6d3b9",
|
||||
@@ -22039,9 +22039,9 @@
|
||||
"translated": "Войти"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b62dfd708d5cfb14",
|
||||
"source": "The model is downloaded and prepared on this Gateway.",
|
||||
"translated": "Модель загружена и подготовлена на этом Gateway."
|
||||
"id": "native.apple.97b24c854f768998",
|
||||
"source": "OpenClaw will detect and verify the prepared model before using it.",
|
||||
"translated": "OpenClaw обнаружит и проверит подготовленную модель перед её использованием."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.869fe436119bb8a6",
|
||||
|
||||
@@ -21999,14 +21999,14 @@
|
||||
"translated": "Konfigurera en lokal modell"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.506a932536c642c2",
|
||||
"source": "Download or prepare a local model on this Gateway.",
|
||||
"translated": "Ladda ner eller förbered en lokal modell på denna Gateway."
|
||||
"id": "native.apple.780c1aa1c8868cc4",
|
||||
"source": "Connect a local model service, or prepare a model on this Gateway.",
|
||||
"translated": "Anslut en lokal modelltjänst eller förbered en modell på den här Gateway."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.d31411a9f3c68c33",
|
||||
"source": "Set up / Download model",
|
||||
"translated": "Konfigurera / Ladda ner modell"
|
||||
"id": "native.apple.c218bd844ba8a0ac",
|
||||
"source": "Connect / Set up",
|
||||
"translated": "Anslut / Konfigurera"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7fc409b6aaa6d3b9",
|
||||
@@ -22039,9 +22039,9 @@
|
||||
"translated": "Logga in"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b62dfd708d5cfb14",
|
||||
"source": "The model is downloaded and prepared on this Gateway.",
|
||||
"translated": "Modellen har laddats ner och förberetts på denna Gateway."
|
||||
"id": "native.apple.97b24c854f768998",
|
||||
"source": "OpenClaw will detect and verify the prepared model before using it.",
|
||||
"translated": "OpenClaw upptäcker och verifierar den förberedda modellen innan den används."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.869fe436119bb8a6",
|
||||
|
||||
@@ -21999,14 +21999,14 @@
|
||||
"translated": "ตั้งค่าโมเดลในเครื่อง"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.506a932536c642c2",
|
||||
"source": "Download or prepare a local model on this Gateway.",
|
||||
"translated": "ดาวน์โหลดหรือเตรียมโมเดลในเครื่องบน Gateway นี้"
|
||||
"id": "native.apple.780c1aa1c8868cc4",
|
||||
"source": "Connect a local model service, or prepare a model on this Gateway.",
|
||||
"translated": "เชื่อมต่อบริการโมเดลในเครื่อง หรือเตรียมโมเดลบน Gateway นี้"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.d31411a9f3c68c33",
|
||||
"source": "Set up / Download model",
|
||||
"translated": "ตั้งค่า / ดาวน์โหลดโมเดล"
|
||||
"id": "native.apple.c218bd844ba8a0ac",
|
||||
"source": "Connect / Set up",
|
||||
"translated": "เชื่อมต่อ / ตั้งค่า"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7fc409b6aaa6d3b9",
|
||||
@@ -22039,9 +22039,9 @@
|
||||
"translated": "ลงชื่อเข้าใช้"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b62dfd708d5cfb14",
|
||||
"source": "The model is downloaded and prepared on this Gateway.",
|
||||
"translated": "ดาวน์โหลดและเตรียมโมเดลบน Gateway นี้แล้ว"
|
||||
"id": "native.apple.97b24c854f768998",
|
||||
"source": "OpenClaw will detect and verify the prepared model before using it.",
|
||||
"translated": "OpenClaw จะตรวจจับและตรวจสอบโมเดลที่เตรียมไว้ก่อนใช้งาน"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.869fe436119bb8a6",
|
||||
|
||||
@@ -21999,14 +21999,14 @@
|
||||
"translated": "Yerel bir model kurun"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.506a932536c642c2",
|
||||
"source": "Download or prepare a local model on this Gateway.",
|
||||
"translated": "Bu Gateway'de yerel bir model indirin veya hazırlayın."
|
||||
"id": "native.apple.780c1aa1c8868cc4",
|
||||
"source": "Connect a local model service, or prepare a model on this Gateway.",
|
||||
"translated": "Yerel bir model hizmeti bağlayın veya bu Gateway üzerinde bir model hazırlayın."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.d31411a9f3c68c33",
|
||||
"source": "Set up / Download model",
|
||||
"translated": "Modeli kur / indir"
|
||||
"id": "native.apple.c218bd844ba8a0ac",
|
||||
"source": "Connect / Set up",
|
||||
"translated": "Bağlan / Kur"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7fc409b6aaa6d3b9",
|
||||
@@ -22039,9 +22039,9 @@
|
||||
"translated": "Oturum aç"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b62dfd708d5cfb14",
|
||||
"source": "The model is downloaded and prepared on this Gateway.",
|
||||
"translated": "Model bu Gateway'de indirildi ve hazırlandı."
|
||||
"id": "native.apple.97b24c854f768998",
|
||||
"source": "OpenClaw will detect and verify the prepared model before using it.",
|
||||
"translated": "OpenClaw, hazırlanan modeli kullanmadan önce algılayıp doğrulayacaktır."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.869fe436119bb8a6",
|
||||
|
||||
@@ -21999,14 +21999,14 @@
|
||||
"translated": "Налаштувати локальну модель"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.506a932536c642c2",
|
||||
"source": "Download or prepare a local model on this Gateway.",
|
||||
"translated": "Завантажте або підготуйте локальну модель на цьому Gateway."
|
||||
"id": "native.apple.780c1aa1c8868cc4",
|
||||
"source": "Connect a local model service, or prepare a model on this Gateway.",
|
||||
"translated": "Підключіть локальну службу моделей або підготуйте модель на цьому Gateway."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.d31411a9f3c68c33",
|
||||
"source": "Set up / Download model",
|
||||
"translated": "Налаштувати / Завантажити модель"
|
||||
"id": "native.apple.c218bd844ba8a0ac",
|
||||
"source": "Connect / Set up",
|
||||
"translated": "Підключити / Налаштувати"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7fc409b6aaa6d3b9",
|
||||
@@ -22039,9 +22039,9 @@
|
||||
"translated": "Увійти"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b62dfd708d5cfb14",
|
||||
"source": "The model is downloaded and prepared on this Gateway.",
|
||||
"translated": "Модель завантажено та підготовлено на цьому Gateway."
|
||||
"id": "native.apple.97b24c854f768998",
|
||||
"source": "OpenClaw will detect and verify the prepared model before using it.",
|
||||
"translated": "OpenClaw виявить і перевірить підготовлену модель перед її використанням."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.869fe436119bb8a6",
|
||||
|
||||
@@ -21999,14 +21999,14 @@
|
||||
"translated": "Thiết lập mô hình cục bộ"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.506a932536c642c2",
|
||||
"source": "Download or prepare a local model on this Gateway.",
|
||||
"translated": "Tải xuống hoặc chuẩn bị một mô hình cục bộ trên Gateway này."
|
||||
"id": "native.apple.780c1aa1c8868cc4",
|
||||
"source": "Connect a local model service, or prepare a model on this Gateway.",
|
||||
"translated": "Kết nối dịch vụ mô hình cục bộ, hoặc chuẩn bị một mô hình trên Gateway này."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.d31411a9f3c68c33",
|
||||
"source": "Set up / Download model",
|
||||
"translated": "Thiết lập / Tải xuống mô hình"
|
||||
"id": "native.apple.c218bd844ba8a0ac",
|
||||
"source": "Connect / Set up",
|
||||
"translated": "Kết nối / Thiết lập"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7fc409b6aaa6d3b9",
|
||||
@@ -22039,9 +22039,9 @@
|
||||
"translated": "Đăng nhập"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b62dfd708d5cfb14",
|
||||
"source": "The model is downloaded and prepared on this Gateway.",
|
||||
"translated": "Mô hình đã được tải xuống và chuẩn bị trên Gateway này."
|
||||
"id": "native.apple.97b24c854f768998",
|
||||
"source": "OpenClaw will detect and verify the prepared model before using it.",
|
||||
"translated": "OpenClaw sẽ phát hiện và xác minh mô hình đã chuẩn bị trước khi sử dụng."
|
||||
},
|
||||
{
|
||||
"id": "native.apple.869fe436119bb8a6",
|
||||
|
||||
@@ -21999,14 +21999,14 @@
|
||||
"translated": "设置本地模型"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.506a932536c642c2",
|
||||
"source": "Download or prepare a local model on this Gateway.",
|
||||
"translated": "在此 Gateway 上下载或准备本地模型。"
|
||||
"id": "native.apple.780c1aa1c8868cc4",
|
||||
"source": "Connect a local model service, or prepare a model on this Gateway.",
|
||||
"translated": "连接本地模型服务,或在此 Gateway 上准备模型。"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.d31411a9f3c68c33",
|
||||
"source": "Set up / Download model",
|
||||
"translated": "设置 / 下载模型"
|
||||
"id": "native.apple.c218bd844ba8a0ac",
|
||||
"source": "Connect / Set up",
|
||||
"translated": "连接 / 设置"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7fc409b6aaa6d3b9",
|
||||
@@ -22039,9 +22039,9 @@
|
||||
"translated": "登录"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b62dfd708d5cfb14",
|
||||
"source": "The model is downloaded and prepared on this Gateway.",
|
||||
"translated": "模型已在此 Gateway 上下载并准备就绪。"
|
||||
"id": "native.apple.97b24c854f768998",
|
||||
"source": "OpenClaw will detect and verify the prepared model before using it.",
|
||||
"translated": "OpenClaw 会在使用前检测并验证已准备好的模型。"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.869fe436119bb8a6",
|
||||
|
||||
@@ -21999,14 +21999,14 @@
|
||||
"translated": "設定本機模型"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.506a932536c642c2",
|
||||
"source": "Download or prepare a local model on this Gateway.",
|
||||
"translated": "在此 Gateway 上下載或準備本機模型。"
|
||||
"id": "native.apple.780c1aa1c8868cc4",
|
||||
"source": "Connect a local model service, or prepare a model on this Gateway.",
|
||||
"translated": "連接本地模型服務,或在此 Gateway 上準備模型。"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.d31411a9f3c68c33",
|
||||
"source": "Set up / Download model",
|
||||
"translated": "設定/下載模型"
|
||||
"id": "native.apple.c218bd844ba8a0ac",
|
||||
"source": "Connect / Set up",
|
||||
"translated": "連接/設定"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7fc409b6aaa6d3b9",
|
||||
@@ -22039,9 +22039,9 @@
|
||||
"translated": "登入"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b62dfd708d5cfb14",
|
||||
"source": "The model is downloaded and prepared on this Gateway.",
|
||||
"translated": "模型已在此 Gateway 上下載並準備完成。"
|
||||
"id": "native.apple.97b24c854f768998",
|
||||
"source": "OpenClaw will detect and verify the prepared model before using it.",
|
||||
"translated": "OpenClaw 會在使用前偵測並驗證已準備的模型。"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.869fe436119bb8a6",
|
||||
|
||||
@@ -907,6 +907,11 @@ class GatewaySession(
|
||||
val hello: GatewayHelloSummary,
|
||||
)
|
||||
|
||||
private data class ConnectChallenge(
|
||||
val nonce: String,
|
||||
val issuedAtMs: Long,
|
||||
)
|
||||
|
||||
private enum class ConnectionState {
|
||||
CONNECTING,
|
||||
READY,
|
||||
@@ -926,7 +931,7 @@ class GatewaySession(
|
||||
private val state = AtomicReference(ConnectionState.CONNECTING)
|
||||
private val connectDeferred = CompletableDeferred<ConnectedGateway>()
|
||||
private val closedDeferred = CompletableDeferred<Unit>()
|
||||
private val connectNonceDeferred = CompletableDeferred<String>()
|
||||
private val connectChallengeDeferred = CompletableDeferred<ConnectChallenge>()
|
||||
private val terminalCallbackClaimed = AtomicBoolean(false)
|
||||
private val connectResponseAccepted = AtomicBoolean(false)
|
||||
|
||||
@@ -1224,7 +1229,7 @@ class GatewaySession(
|
||||
if (connectResponseAccepted.get()) {
|
||||
connectHandshakeJob?.join()
|
||||
} else {
|
||||
connectNonceDeferred.completeExceptionally(connectError)
|
||||
connectChallengeDeferred.completeExceptionally(connectError)
|
||||
}
|
||||
if (shouldNotify) onDisconnected(message)
|
||||
} finally {
|
||||
@@ -1262,8 +1267,8 @@ class GatewaySession(
|
||||
connectHandshakeJob =
|
||||
connectionScope.launch {
|
||||
try {
|
||||
val nonce = awaitConnectNonce()
|
||||
sendConnect(nonce)
|
||||
val challenge = awaitConnectChallenge()
|
||||
sendConnect(challenge)
|
||||
} catch (err: Throwable) {
|
||||
connectDeferred.completeExceptionally(err)
|
||||
closeQuietly()
|
||||
@@ -1310,7 +1315,7 @@ class GatewaySession(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendConnect(connectNonce: String) {
|
||||
private suspend fun sendConnect(connectChallenge: ConnectChallenge) {
|
||||
val identity = identityStore.loadOrCreate()
|
||||
val storedEntry = deviceAuthStore.loadEntry(endpoint.stableId, identity.deviceId, options.role)
|
||||
val storedToken = storedEntry?.token?.trim()
|
||||
@@ -1331,7 +1336,7 @@ class GatewaySession(
|
||||
val payload =
|
||||
buildConnectParams(
|
||||
identity = identity,
|
||||
connectNonce = connectNonce,
|
||||
connectChallenge = connectChallenge,
|
||||
selectedAuth = selectedAuth,
|
||||
)
|
||||
val res = request(GatewayMethod.Connect.rawValue, payload, timeoutMs = CONNECT_RPC_TIMEOUT_MS)
|
||||
@@ -1512,7 +1517,7 @@ class GatewaySession(
|
||||
|
||||
private fun buildConnectParams(
|
||||
identity: DeviceIdentity,
|
||||
connectNonce: String,
|
||||
connectChallenge: ConnectChallenge,
|
||||
selectedAuth: SelectedConnectAuth,
|
||||
): JsonObject {
|
||||
val client = options.client
|
||||
@@ -1548,7 +1553,8 @@ class GatewaySession(
|
||||
}
|
||||
|
||||
val connectScopes = resolveConnectScopes(selectedAuth)
|
||||
val signedAtMs = System.currentTimeMillis()
|
||||
val signedAtMs = connectChallenge.issuedAtMs
|
||||
val connectNonce = connectChallenge.nonce
|
||||
// V3 signatures bind the auth token, nonce, role, and scopes so replayed connect frames fail.
|
||||
val payload =
|
||||
DeviceAuthPayload.buildV3(
|
||||
@@ -1681,9 +1687,15 @@ class GatewaySession(
|
||||
val payloadJson =
|
||||
frame["payload"]?.toString() ?: frame["payloadJSON"].asStringOrNull()
|
||||
if (event == GatewayEvent.ConnectChallenge.rawValue) {
|
||||
val nonce = extractConnectNonce(payloadJson)
|
||||
if (!connectNonceDeferred.isCompleted && !nonce.isNullOrBlank()) {
|
||||
connectNonceDeferred.complete(nonce.trim())
|
||||
if (!connectChallengeDeferred.isCompleted) {
|
||||
val challenge = extractConnectChallenge(payloadJson)
|
||||
if (challenge == null) {
|
||||
connectChallengeDeferred.completeExceptionally(
|
||||
IllegalStateException("gateway connect challenge invalid"),
|
||||
)
|
||||
} else {
|
||||
connectChallengeDeferred.complete(challenge)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1696,17 +1708,20 @@ class GatewaySession(
|
||||
onEvent(event, payloadJson)
|
||||
}
|
||||
|
||||
private suspend fun awaitConnectNonce(): String =
|
||||
private suspend fun awaitConnectChallenge(): ConnectChallenge =
|
||||
try {
|
||||
withTimeout(2_000) { connectNonceDeferred.await() }
|
||||
} catch (err: Throwable) {
|
||||
withTimeout(2_000) { connectChallengeDeferred.await() }
|
||||
} catch (err: TimeoutCancellationException) {
|
||||
throw IllegalStateException("connect challenge timeout", err)
|
||||
}
|
||||
|
||||
private fun extractConnectNonce(payloadJson: String?): String? {
|
||||
private fun extractConnectChallenge(payloadJson: String?): ConnectChallenge? {
|
||||
if (payloadJson.isNullOrBlank()) return null
|
||||
val obj = parseJsonOrNull(payloadJson)?.asObjectOrNull() ?: return null
|
||||
return obj["nonce"].asStringOrNull()
|
||||
val nonce = obj["nonce"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() } ?: return null
|
||||
val issuedAtMs =
|
||||
obj["ts"].asJsonIntegerLongOrNull()?.takeIf { it >= 0 } ?: return null
|
||||
return ConnectChallenge(nonce = nonce, issuedAtMs = issuedAtMs)
|
||||
}
|
||||
|
||||
private fun handleInvokeEvent(payloadJson: String) {
|
||||
@@ -2215,6 +2230,12 @@ private fun JsonElement?.asLongOrNull(): Long? =
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun JsonElement?.asJsonIntegerLongOrNull(): Long? =
|
||||
when (this) {
|
||||
is JsonPrimitive -> if (isString) null else content.toLongOrNull()
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun JsonElement?.asIntOrNull(): Int? =
|
||||
when (this) {
|
||||
is JsonPrimitive -> content.toIntOrNull()
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
private const val TEST_TIMEOUT_MS = 8_000L
|
||||
private const val CONNECT_CHALLENGE_FRAME =
|
||||
"""{"type":"event","event":"connect.challenge","payload":{"nonce":"android-test-nonce"}}"""
|
||||
"""{"type":"event","event":"connect.challenge","payload":{"nonce":"android-test-nonce","ts":1700000000123}}"""
|
||||
|
||||
private class NoopDeviceAuthStore : DeviceAuthTokenStore {
|
||||
override fun loadEntry(
|
||||
|
||||
@@ -41,8 +41,9 @@ import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
private const val TEST_TIMEOUT_MS = 8_000L
|
||||
private const val CONNECT_CHALLENGE_TS = 1_700_000_000_123L
|
||||
private const val CONNECT_CHALLENGE_FRAME =
|
||||
"""{"type":"event","event":"connect.challenge","payload":{"nonce":"android-test-nonce"}}"""
|
||||
"""{"type":"event","event":"connect.challenge","payload":{"nonce":"android-test-nonce","ts":$CONNECT_CHALLENGE_TS}}"""
|
||||
|
||||
private class InMemoryDeviceAuthStore : DeviceAuthTokenStore {
|
||||
private val tokens = mutableMapOf<String, DeviceAuthEntry>()
|
||||
@@ -92,6 +93,76 @@ private data class InvokeScenarioResult(
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34])
|
||||
class GatewaySessionInvokeTest {
|
||||
@Test
|
||||
fun connect_usesGatewayChallengeTimestamp() =
|
||||
runBlocking {
|
||||
val json = testJson()
|
||||
val connected = CompletableDeferred<Unit>()
|
||||
val lastDisconnect = AtomicReference("")
|
||||
val server =
|
||||
startGatewayServer(json) { webSocket, id, method, frame ->
|
||||
if (method == "connect") {
|
||||
assertEquals(
|
||||
CONNECT_CHALLENGE_TS,
|
||||
frame["params"]
|
||||
?.jsonObject
|
||||
?.get("device")
|
||||
?.jsonObject
|
||||
?.get("signedAt")
|
||||
?.jsonPrimitive
|
||||
?.content
|
||||
?.toLong(),
|
||||
)
|
||||
webSocket.send(connectResponseFrame(id))
|
||||
}
|
||||
}
|
||||
val harness =
|
||||
createNodeHarness(
|
||||
connected = connected,
|
||||
lastDisconnect = lastDisconnect,
|
||||
) { GatewaySession.InvokeResult.ok("""{"handled":true}""") }
|
||||
|
||||
try {
|
||||
connectNodeSession(harness.session, server.port)
|
||||
awaitConnectedOrThrow(connected, lastDisconnect, server)
|
||||
} finally {
|
||||
shutdownHarness(harness, server)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun connect_rejectsChallengeWithoutTimestamp() =
|
||||
runBlocking {
|
||||
val json = testJson()
|
||||
val connected = CompletableDeferred<Unit>()
|
||||
val lastDisconnect = AtomicReference("")
|
||||
val connectRequests = AtomicInteger()
|
||||
val server =
|
||||
startGatewayServer(
|
||||
json = json,
|
||||
challengeFrame =
|
||||
"""{"type":"event","event":"connect.challenge","payload":{"nonce":"android-test-nonce"}}""",
|
||||
) { _, _, method, _ ->
|
||||
if (method == "connect") connectRequests.incrementAndGet()
|
||||
}
|
||||
val harness =
|
||||
createNodeHarness(
|
||||
connected = connected,
|
||||
lastDisconnect = lastDisconnect,
|
||||
) { GatewaySession.InvokeResult.ok("""{"handled":true}""") }
|
||||
|
||||
try {
|
||||
connectNodeSession(harness.session, server.port)
|
||||
withTimeout(TEST_TIMEOUT_MS) {
|
||||
while (lastDisconnect.get().isEmpty()) delay(10)
|
||||
}
|
||||
assertFalse(connected.isCompleted)
|
||||
assertEquals(0, connectRequests.get())
|
||||
} finally {
|
||||
shutdownHarness(harness, server)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun canvasRoutePinsOnlyTheConnectedTlsEndpoint() {
|
||||
val fingerprint = "ab".repeat(32)
|
||||
@@ -1484,6 +1555,7 @@ class GatewaySessionInvokeTest {
|
||||
|
||||
private fun startGatewayServer(
|
||||
json: Json,
|
||||
challengeFrame: String = CONNECT_CHALLENGE_FRAME,
|
||||
onHandshake: ((RecordedRequest) -> Unit)? = null,
|
||||
onRequestFrame: (webSocket: WebSocket, id: String, method: String, frame: JsonObject) -> Unit,
|
||||
): MockWebServer =
|
||||
@@ -1498,7 +1570,7 @@ class GatewaySessionInvokeTest {
|
||||
webSocket: WebSocket,
|
||||
response: Response,
|
||||
) {
|
||||
webSocket.send(CONNECT_CHALLENGE_FRAME)
|
||||
webSocket.send(challengeFrame)
|
||||
}
|
||||
|
||||
override fun onMessage(
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
private const val LIFECYCLE_TEST_TIMEOUT_MS = 8_000L
|
||||
private const val LIFECYCLE_CONNECT_CHALLENGE_FRAME =
|
||||
"""{"type":"event","event":"connect.challenge","payload":{"nonce":"android-test-nonce"}}"""
|
||||
"""{"type":"event","event":"connect.challenge","payload":{"nonce":"android-test-nonce","ts":1700000000123}}"""
|
||||
|
||||
private class ReconnectDeviceAuthStore : DeviceAuthTokenStore {
|
||||
override fun loadEntry(
|
||||
|
||||
+29
-28
@@ -56,7 +56,8 @@ class WearRealtimeChannelRegistryTest {
|
||||
|
||||
val secondClaim = checkNotNull(registry.claim("watch-a", "attempt-b"))
|
||||
withTimeout(2_000L) {
|
||||
while (transport.closeCount(first) != 1 || synchronized(stoppedOwners) { stoppedOwners.size } != 1) {
|
||||
transport.awaitClosed(first)
|
||||
while (synchronized(stoppedOwners) { stoppedOwners.size } != 1) {
|
||||
kotlinx.coroutines.yield()
|
||||
}
|
||||
}
|
||||
@@ -294,7 +295,8 @@ class WearRealtimeChannelRegistryTest {
|
||||
registry.accept(reconnect, appendAudio = { _, _ -> }, stopTalk = stopTalk)
|
||||
transport.awaitOpened(reconnect)
|
||||
withTimeout(1_000L) {
|
||||
while (transport.closeCount(active) != 1 || !transport.hasStartedReading(reconnect)) yield()
|
||||
transport.awaitClosed(active)
|
||||
while (!transport.hasStartedReading(reconnect)) yield()
|
||||
}
|
||||
|
||||
val repeated = checkNotNull(registry.claim("watch-a", "attempt-a"))
|
||||
@@ -337,7 +339,8 @@ class WearRealtimeChannelRegistryTest {
|
||||
releaseWrite.countDown()
|
||||
withTimeout(1_000L) { send.await() }
|
||||
withTimeout(1_000L) {
|
||||
while (transport.closeCount(active) != 1 || !transport.hasStartedReading(reconnect)) yield()
|
||||
transport.awaitClosed(active)
|
||||
while (!transport.hasStartedReading(reconnect)) yield()
|
||||
}
|
||||
assertTrue(registry.isCurrent(owner))
|
||||
registry.close(owner)
|
||||
@@ -435,9 +438,7 @@ class WearRealtimeChannelRegistryTest {
|
||||
assertEquals(0, transport.closeCount(channel))
|
||||
scope.cancel()
|
||||
releaseClose.complete(Unit)
|
||||
withTimeout(1_000L) {
|
||||
while (transport.closeCount(channel) != 1) yield()
|
||||
}
|
||||
withTimeout(1_000L) { transport.awaitClosed(channel) }
|
||||
} finally {
|
||||
releaseClose.complete(Unit)
|
||||
scope.cancel()
|
||||
@@ -483,9 +484,7 @@ class WearRealtimeChannelRegistryTest {
|
||||
val replacementOwner = checkNotNull(withTimeout(1_000L) { replacementClaim.await() }).owner
|
||||
assertEquals(listOf(activeOwner), synchronized(stoppedOwners) { stoppedOwners.toList() })
|
||||
assertTrue(registry.isCurrent(replacementOwner))
|
||||
withTimeout(1_000L) {
|
||||
while (transport.closeCount(staleReconnect) != 1) yield()
|
||||
}
|
||||
withTimeout(1_000L) { transport.awaitClosed(staleReconnect) }
|
||||
registry.close(replacementOwner)
|
||||
} finally {
|
||||
releaseStop.complete(Unit)
|
||||
@@ -553,9 +552,7 @@ class WearRealtimeChannelRegistryTest {
|
||||
try {
|
||||
registry.accept(expired, appendAudio = { _, _ -> }, stopTalk = {})
|
||||
transport.awaitOpened(expired)
|
||||
withTimeout(1_000L) {
|
||||
while (transport.closeCount(expired) != 1) yield()
|
||||
}
|
||||
withTimeout(1_000L) { transport.awaitClosed(expired) }
|
||||
|
||||
val claim = async { registry.claim("watch-a", "attempt-a") }
|
||||
delay(100L)
|
||||
@@ -655,6 +652,7 @@ class WearRealtimeChannelRegistryTest {
|
||||
val releaseStop = CompletableDeferred<Unit>()
|
||||
val active = FakeChannel("watch-a", "channel-a", "attempt-a")
|
||||
val reserved = FakeChannel("watch-a", "channel-b-reserved", "attempt-b")
|
||||
val supersededReconnect = FakeChannel("watch-a", "channel-b-superseded", "attempt-b")
|
||||
val reconnect = FakeChannel("watch-a", "channel-b-reconnect", "attempt-b")
|
||||
val stopTalk: suspend (WearRealtimeAttemptOwner) -> Unit = { owner ->
|
||||
if (owner.attemptId == "attempt-a") {
|
||||
@@ -672,13 +670,18 @@ class WearRealtimeChannelRegistryTest {
|
||||
val replacementClaim = async { registry.claim("watch-a", "attempt-b") }
|
||||
withTimeout(1_000L) { stopStarted.await() }
|
||||
|
||||
registry.accept(supersededReconnect, appendAudio = { _, _ -> }, stopTalk = stopTalk)
|
||||
transport.awaitOpened(supersededReconnect)
|
||||
registry.accept(reconnect, appendAudio = { _, _ -> }, stopTalk = stopTalk)
|
||||
transport.awaitOpened(reconnect)
|
||||
// The older reconnect cannot close until the newest channel is published in the registry.
|
||||
withTimeout(1_000L) { transport.awaitClosed(supersededReconnect) }
|
||||
releaseStop.complete(Unit)
|
||||
|
||||
val owner = checkNotNull(withTimeout(1_000L) { replacementClaim.await() }).owner
|
||||
assertEquals(3L, owner.channelGeneration)
|
||||
assertEquals(4L, owner.channelGeneration)
|
||||
assertEquals(1, transport.closeCount(reserved))
|
||||
assertEquals(1, transport.closeCount(supersededReconnect))
|
||||
assertEquals(0, transport.closeCount(reconnect))
|
||||
assertTrue(registry.isCurrent(owner))
|
||||
registry.close(owner)
|
||||
@@ -728,13 +731,9 @@ class WearRealtimeChannelRegistryTest {
|
||||
|
||||
val owner = checkNotNull(withTimeout(1_000L) { replacementClaim.await() }).owner
|
||||
withTimeout(1_000L) {
|
||||
while (
|
||||
transport.closeCount(reserved) != 1 ||
|
||||
transport.closeCount(firstReconnect) != 1 ||
|
||||
!transport.hasStartedReading(latestReconnect)
|
||||
) {
|
||||
yield()
|
||||
}
|
||||
transport.awaitClosed(reserved)
|
||||
transport.awaitClosed(firstReconnect)
|
||||
while (!transport.hasStartedReading(latestReconnect)) yield()
|
||||
}
|
||||
assertEquals("attempt-b", owner.attemptId)
|
||||
assertEquals(0, transport.closeCount(latestReconnect))
|
||||
@@ -781,9 +780,7 @@ class WearRealtimeChannelRegistryTest {
|
||||
val cancelledClaim = async { registry.claim("watch-a", "attempt-b") }
|
||||
withTimeout(1_000L) { stopStarted.await() }
|
||||
cancelledClaim.cancelAndJoin()
|
||||
withTimeout(1_000L) {
|
||||
while (transport.closeCount(cancelled) != 1) yield()
|
||||
}
|
||||
withTimeout(1_000L) { transport.awaitClosed(cancelled) }
|
||||
|
||||
registry.accept(retry, appendAudio = { _, _ -> }, stopTalk = stopTalk)
|
||||
transport.awaitOpened(retry)
|
||||
@@ -836,9 +833,7 @@ class WearRealtimeChannelRegistryTest {
|
||||
|
||||
releaseFirst.complete(Unit)
|
||||
transport.awaitOpened(first)
|
||||
withTimeout(1_000L) {
|
||||
while (transport.closeCount(first) != 1) yield()
|
||||
}
|
||||
withTimeout(1_000L) { transport.awaitClosed(first) }
|
||||
|
||||
val repeated = checkNotNull(registry.claim("watch-a", "attempt-b"))
|
||||
assertFalse(repeated.newlyAcquired)
|
||||
@@ -926,7 +921,7 @@ class WearRealtimeChannelRegistryTest {
|
||||
transport.awaitOpened(first)
|
||||
|
||||
registry.accept(sameNodeExcess, appendAudio = { _, _ -> }, stopTalk = {})
|
||||
transport.awaitCloseStarted(sameNodeExcess)
|
||||
transport.awaitClosed(sameNodeExcess)
|
||||
assertFalse(transport.wasOpened(sameNodeExcess))
|
||||
assertEquals(1, transport.closeCount(sameNodeExcess))
|
||||
|
||||
@@ -934,7 +929,7 @@ class WearRealtimeChannelRegistryTest {
|
||||
transport.awaitOpened(secondNode)
|
||||
|
||||
registry.accept(globalExcess, appendAudio = { _, _ -> }, stopTalk = {})
|
||||
transport.awaitCloseStarted(globalExcess)
|
||||
transport.awaitClosed(globalExcess)
|
||||
assertFalse(transport.wasOpened(globalExcess))
|
||||
assertEquals(1, transport.closeCount(globalExcess))
|
||||
} finally {
|
||||
@@ -948,6 +943,7 @@ private class FakeChannelTransport : WearRealtimeChannelTransport {
|
||||
private val openGates = ConcurrentHashMap<ChannelClient.Channel, CompletableDeferred<Unit>>()
|
||||
private val closeGates = ConcurrentHashMap<ChannelClient.Channel, CompletableDeferred<Unit>>()
|
||||
private val closeStarted = ConcurrentHashMap<ChannelClient.Channel, CompletableDeferred<Unit>>()
|
||||
private val closeCompleted = ConcurrentHashMap<ChannelClient.Channel, CompletableDeferred<Unit>>()
|
||||
private val closeCounts = ConcurrentHashMap<ChannelClient.Channel, Int>()
|
||||
private val writeGates = ConcurrentHashMap<ChannelClient.Channel, CountDownLatch>()
|
||||
private val writeStarted = ConcurrentHashMap<ChannelClient.Channel, CompletableDeferred<Unit>>()
|
||||
@@ -977,6 +973,7 @@ private class FakeChannelTransport : WearRealtimeChannelTransport {
|
||||
resources?.input?.close()
|
||||
resources?.output?.close()
|
||||
closeCounts.compute(channel) { _, count -> (count ?: 0) + 1 }
|
||||
closeCompleted.computeIfAbsent(channel) { CompletableDeferred() }.complete(Unit)
|
||||
}
|
||||
|
||||
suspend fun awaitOpened(channel: ChannelClient.Channel) {
|
||||
@@ -999,6 +996,10 @@ private class FakeChannelTransport : WearRealtimeChannelTransport {
|
||||
closeStarted.computeIfAbsent(channel) { CompletableDeferred() }.await()
|
||||
}
|
||||
|
||||
suspend fun awaitClosed(channel: ChannelClient.Channel) {
|
||||
closeCompleted.computeIfAbsent(channel) { CompletableDeferred() }.await()
|
||||
}
|
||||
|
||||
fun holdWrite(channel: ChannelClient.Channel): CountDownLatch =
|
||||
CountDownLatch(1).also { gate ->
|
||||
writeGates[channel] = gate
|
||||
|
||||
@@ -36,6 +36,26 @@ final class WatchDirectNode {
|
||||
|
||||
private struct ChallengeResponse: Decodable {
|
||||
let nonce: String
|
||||
let ts: Int64?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case nonce
|
||||
case ts
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.nonce = try container.decode(String.self, forKey: .nonce)
|
||||
self.ts = container.contains(.ts)
|
||||
? try container.decode(Int64.self, forKey: .ts)
|
||||
: nil
|
||||
if let ts, ts < 0 {
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: .ts,
|
||||
in: container,
|
||||
debugDescription: "Gateway challenge timestamp must be non-negative")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct PollResponse: Decodable {
|
||||
@@ -427,6 +447,8 @@ final class WatchDirectNode {
|
||||
let params = try connectParams(
|
||||
identity: identity,
|
||||
nonce: challenge.nonce,
|
||||
// Older watch-node Gateways omitted ts; retain their original local-clock behavior.
|
||||
signedAtMs: challenge.ts ?? Int64(Date().timeIntervalSince1970 * 1000),
|
||||
credential: credential,
|
||||
notificationsAuthorized: notificationSettings.authorizationStatus == .authorized
|
||||
|| notificationSettings.authorizationStatus == .provisional)
|
||||
@@ -442,10 +464,10 @@ final class WatchDirectNode {
|
||||
private func connectParams(
|
||||
identity: DeviceIdentity,
|
||||
nonce: String,
|
||||
signedAtMs: Int64,
|
||||
credential: ConnectCredential,
|
||||
notificationsAuthorized: Bool) throws -> ConnectParams
|
||||
{
|
||||
let signedAtMs = Int64(Date().timeIntervalSince1970 * 1000)
|
||||
let payload = GatewayDeviceAuthPayload.buildV3(
|
||||
fields: .init(
|
||||
deviceId: identity.deviceId,
|
||||
|
||||
@@ -16,7 +16,7 @@ use std::fmt;
|
||||
use std::io::ErrorKind;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
use std::time::{Duration, Instant};
|
||||
use subtle::ConstantTimeEq;
|
||||
use tauri::{AppHandle, Emitter, Manager, Webview};
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
@@ -721,8 +721,7 @@ impl GatewayClient {
|
||||
let mut socket = tokio::time::timeout(CONNECT_TIMEOUT, connect_gateway_socket(config))
|
||||
.await
|
||||
.map_err(|_| RequestFailure::transport("Gateway connection timed out."))??;
|
||||
let nonce = wait_for_connect_challenge(&mut socket).await?;
|
||||
let signed_at_ms = unix_time_ms().map_err(RequestFailure::transport)?;
|
||||
let challenge = wait_for_connect_challenge(&mut socket).await?;
|
||||
// Native child WebViews use platform HTTP trust and cannot bind the optional
|
||||
// WebSocket leaf pin, so pinned Gateway connections remain capability-free.
|
||||
let inline_widgets_available = config
|
||||
@@ -732,8 +731,8 @@ impl GatewayClient {
|
||||
let params = connect_params(
|
||||
&identity,
|
||||
&auth,
|
||||
&nonce,
|
||||
signed_at_ms,
|
||||
&challenge.nonce,
|
||||
challenge.issued_at_ms,
|
||||
inline_widgets_available,
|
||||
)
|
||||
.map_err(RequestFailure::transport)?;
|
||||
@@ -1127,22 +1126,42 @@ fn request_frame(id: &str, method: &str, params: Value) -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
async fn wait_for_connect_challenge(socket: &mut GatewaySocket) -> Result<String, RequestFailure> {
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct ConnectChallenge {
|
||||
nonce: String,
|
||||
issued_at_ms: u64,
|
||||
}
|
||||
|
||||
fn parse_connect_challenge(value: &Value) -> Result<ConnectChallenge, RequestFailure> {
|
||||
let nonce = value
|
||||
.get("payload")
|
||||
.and_then(|payload| payload.get("nonce"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|nonce| !nonce.is_empty());
|
||||
let issued_at_ms = value
|
||||
.get("payload")
|
||||
.and_then(|payload| payload.get("ts"))
|
||||
.and_then(Value::as_u64)
|
||||
.ok_or_else(|| RequestFailure::transport("Gateway challenge timestamp was invalid."))?;
|
||||
nonce
|
||||
.map(|nonce| ConnectChallenge {
|
||||
nonce: nonce.to_owned(),
|
||||
issued_at_ms,
|
||||
})
|
||||
.ok_or_else(|| RequestFailure::transport("Gateway challenge omitted nonce."))
|
||||
}
|
||||
|
||||
async fn wait_for_connect_challenge(
|
||||
socket: &mut GatewaySocket,
|
||||
) -> Result<ConnectChallenge, RequestFailure> {
|
||||
tokio::time::timeout(HANDSHAKE_TIMEOUT, async {
|
||||
loop {
|
||||
let value = next_json(socket).await?;
|
||||
if value.get("type").and_then(Value::as_str) == Some("event")
|
||||
&& value.get("event").and_then(Value::as_str) == Some("connect.challenge")
|
||||
{
|
||||
let nonce = value
|
||||
.get("payload")
|
||||
.and_then(|payload| payload.get("nonce"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|nonce| !nonce.is_empty());
|
||||
return nonce
|
||||
.map(ToOwned::to_owned)
|
||||
.ok_or_else(|| RequestFailure::transport("Gateway challenge omitted nonce."));
|
||||
return parse_connect_challenge(&value);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1470,13 +1489,6 @@ fn dispatch_chat_event(app: &AppHandle, frame: &Value) {
|
||||
}
|
||||
}
|
||||
|
||||
fn unix_time_ms() -> Result<u64, String> {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_millis() as u64)
|
||||
.map_err(|error| format!("Could not read system time: {error}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1665,6 +1677,34 @@ mod tests {
|
||||
std::fs::remove_dir_all(directory).expect("remove connect fixture");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connect_challenge_uses_gateway_timestamp() {
|
||||
let Ok(challenge) = parse_connect_challenge(&json!({
|
||||
"payload": {
|
||||
"nonce": " fixture-nonce ",
|
||||
"ts": 1_700_000_000_123_u64
|
||||
}
|
||||
})) else {
|
||||
panic!("expected valid challenge");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
challenge,
|
||||
ConnectChallenge {
|
||||
nonce: "fixture-nonce".to_string(),
|
||||
issued_at_ms: 1_700_000_000_123,
|
||||
}
|
||||
);
|
||||
assert!(parse_connect_challenge(&json!({
|
||||
"payload": { "nonce": "missing-time" }
|
||||
}))
|
||||
.is_err());
|
||||
assert!(parse_connect_challenge(&json!({
|
||||
"payload": { "nonce": "fixture-nonce", "ts": "1700000000123" }
|
||||
}))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hello_tick_policy_sets_two_interval_watchdog() {
|
||||
let hello = validate_hello(json!({
|
||||
|
||||
@@ -28561,6 +28561,142 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Connect / Set up": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Connect / Set up"
|
||||
}
|
||||
},
|
||||
"zh-CN": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "连接 / 设置"
|
||||
}
|
||||
},
|
||||
"zh-TW": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "連接/設定"
|
||||
}
|
||||
},
|
||||
"pt-BR": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Conectar / Configurar"
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Verbinden / Einrichten"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Conectar / Configurar"
|
||||
}
|
||||
},
|
||||
"ja-JP": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "接続 / セットアップ"
|
||||
}
|
||||
},
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "연결 / 설정"
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Connecter / Configurer"
|
||||
}
|
||||
},
|
||||
"hi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "कनेक्ट करें / सेट अप करें"
|
||||
}
|
||||
},
|
||||
"ar": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "الاتصال / الإعداد"
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Connetti / Configura"
|
||||
}
|
||||
},
|
||||
"tr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Bağlan / Kur"
|
||||
}
|
||||
},
|
||||
"uk": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Підключити / Налаштувати"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Sambungkan / Siapkan"
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Połącz / Skonfiguruj"
|
||||
}
|
||||
},
|
||||
"th": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "เชื่อมต่อ / ตั้งค่า"
|
||||
}
|
||||
},
|
||||
"vi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Kết nối / Thiết lập"
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Verbinden / Instellen"
|
||||
}
|
||||
},
|
||||
"fa": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "اتصال / راهاندازی"
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Подключить / Настроить"
|
||||
}
|
||||
},
|
||||
"sv": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Anslut / Konfigurera"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Connect Discord, Slack, Telegram, WhatsApp, …": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
@@ -28697,6 +28833,142 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Connect a local model service, or prepare a model on this Gateway.": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Connect a local model service, or prepare a model on this Gateway."
|
||||
}
|
||||
},
|
||||
"zh-CN": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "连接本地模型服务,或在此 Gateway 上准备模型。"
|
||||
}
|
||||
},
|
||||
"zh-TW": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "連接本地模型服務,或在此 Gateway 上準備模型。"
|
||||
}
|
||||
},
|
||||
"pt-BR": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Conecte um serviço de modelo local ou prepare um modelo neste Gateway."
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Verbinden Sie einen lokalen Modelldienst oder bereiten Sie ein Modell auf diesem Gateway vor."
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Conecta un servicio de modelo local o prepara un modelo en este Gateway."
|
||||
}
|
||||
},
|
||||
"ja-JP": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ローカルモデルサービスを接続するか、この Gateway でモデルを準備します。"
|
||||
}
|
||||
},
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "로컬 모델 서비스를 연결하거나 이 Gateway에서 모델을 준비하세요."
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Connectez un service de modèle local, ou préparez un modèle sur ce Gateway."
|
||||
}
|
||||
},
|
||||
"hi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "एक स्थानीय मॉडल सेवा कनेक्ट करें, या इस Gateway पर एक मॉडल तैयार करें।"
|
||||
}
|
||||
},
|
||||
"ar": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "اتصل بخدمة نموذج محلي، أو جهّز نموذجًا على هذا الـ Gateway."
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Connetti un servizio di modello locale o prepara un modello su questo Gateway."
|
||||
}
|
||||
},
|
||||
"tr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Yerel bir model hizmeti bağlayın veya bu Gateway üzerinde bir model hazırlayın."
|
||||
}
|
||||
},
|
||||
"uk": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Підключіть локальну службу моделей або підготуйте модель на цьому Gateway."
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Sambungkan layanan model lokal, atau siapkan model di Gateway ini."
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Połącz lokalną usługę modelu lub przygotuj model na tym Gateway."
|
||||
}
|
||||
},
|
||||
"th": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "เชื่อมต่อบริการโมเดลในเครื่อง หรือเตรียมโมเดลบน Gateway นี้"
|
||||
}
|
||||
},
|
||||
"vi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Kết nối dịch vụ mô hình cục bộ, hoặc chuẩn bị một mô hình trên Gateway này."
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Verbind een lokale modelservice of bereid een model voor op deze Gateway."
|
||||
}
|
||||
},
|
||||
"fa": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "یک سرویس مدل محلی متصل کنید یا مدلی را روی این Gateway آماده کنید."
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Подключите локальный сервис модели или подготовьте модель на этом Gateway."
|
||||
}
|
||||
},
|
||||
"sv": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Anslut en lokal modelltjänst eller förbered en modell på den här Gateway."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Connect a provider below with an API key or token, then check again.": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
@@ -44337,142 +44609,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Download or prepare a local model on this Gateway.": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Download or prepare a local model on this Gateway."
|
||||
}
|
||||
},
|
||||
"zh-CN": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "在此 Gateway 上下载或准备本地模型。"
|
||||
}
|
||||
},
|
||||
"zh-TW": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "在此 Gateway 上下載或準備本機模型。"
|
||||
}
|
||||
},
|
||||
"pt-BR": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Baixe ou prepare um modelo local neste Gateway."
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Laden Sie ein lokales Modell auf diesem Gateway herunter oder bereiten Sie es vor."
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Descarga o prepara un modelo local en este Gateway."
|
||||
}
|
||||
},
|
||||
"ja-JP": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "この Gateway でローカルモデルをダウンロードまたは準備します。"
|
||||
}
|
||||
},
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "이 Gateway에서 로컬 모델을 다운로드하거나 준비합니다."
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Téléchargez ou préparez un modèle local sur ce Gateway."
|
||||
}
|
||||
},
|
||||
"hi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "इस Gateway पर एक लोकल मॉडल डाउनलोड या तैयार करें।"
|
||||
}
|
||||
},
|
||||
"ar": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "نزّل أو جهّز نموذجًا محليًا على هذا الـ Gateway."
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Scarica o prepara un modello locale su questo Gateway."
|
||||
}
|
||||
},
|
||||
"tr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Bu Gateway'de yerel bir model indirin veya hazırlayın."
|
||||
}
|
||||
},
|
||||
"uk": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Завантажте або підготуйте локальну модель на цьому Gateway."
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Unduh atau siapkan model lokal di Gateway ini."
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Pobierz lub przygotuj model lokalny na tym Gateway."
|
||||
}
|
||||
},
|
||||
"th": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ดาวน์โหลดหรือเตรียมโมเดลในเครื่องบน Gateway นี้"
|
||||
}
|
||||
},
|
||||
"vi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Tải xuống hoặc chuẩn bị một mô hình cục bộ trên Gateway này."
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Download of bereid een lokaal model voor op deze Gateway."
|
||||
}
|
||||
},
|
||||
"fa": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "دانلود یا آمادهسازی یک مدل محلی روی این Gateway."
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Загрузите или подготовьте локальную модель на этом Gateway."
|
||||
}
|
||||
},
|
||||
"sv": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Ladda ner eller förbered en lokal modell på denna Gateway."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Each selected thread and its transcript will be removed from the gateway.": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
@@ -91801,6 +91937,142 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"OpenClaw will detect and verify the prepared model before using it.": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "OpenClaw will detect and verify the prepared model before using it."
|
||||
}
|
||||
},
|
||||
"zh-CN": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "OpenClaw 会在使用前检测并验证已准备好的模型。"
|
||||
}
|
||||
},
|
||||
"zh-TW": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "OpenClaw 會在使用前偵測並驗證已準備的模型。"
|
||||
}
|
||||
},
|
||||
"pt-BR": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "O OpenClaw detectará e verificará o modelo preparado antes de usá-lo."
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "OpenClaw erkennt und überprüft das vorbereitete Modell, bevor es verwendet wird."
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "OpenClaw detectará y verificará el modelo preparado antes de usarlo."
|
||||
}
|
||||
},
|
||||
"ja-JP": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "OpenClaw は使用する前に、準備されたモデルを検出して検証します。"
|
||||
}
|
||||
},
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "OpenClaw는 준비된 모델을 사용하기 전에 이를 감지하고 검증합니다."
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "OpenClaw détectera et vérifiera le modèle préparé avant de l'utiliser."
|
||||
}
|
||||
},
|
||||
"hi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "OpenClaw उपयोग करने से पहले तैयार किए गए मॉडल का पता लगाएगा और सत्यापित करेगा।"
|
||||
}
|
||||
},
|
||||
"ar": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "سيكتشف OpenClaw النموذج المُجهّز ويتحقق منه قبل استخدامه."
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "OpenClaw rileverà e verificherà il modello preparato prima di utilizzarlo."
|
||||
}
|
||||
},
|
||||
"tr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "OpenClaw, hazırlanan modeli kullanmadan önce algılayıp doğrulayacaktır."
|
||||
}
|
||||
},
|
||||
"uk": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "OpenClaw виявить і перевірить підготовлену модель перед її використанням."
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "OpenClaw akan mendeteksi dan memverifikasi model yang disiapkan sebelum menggunakannya."
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "OpenClaw wykryje i zweryfikuje przygotowany model przed jego użyciem."
|
||||
}
|
||||
},
|
||||
"th": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "OpenClaw จะตรวจจับและตรวจสอบโมเดลที่เตรียมไว้ก่อนใช้งาน"
|
||||
}
|
||||
},
|
||||
"vi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "OpenClaw sẽ phát hiện và xác minh mô hình đã chuẩn bị trước khi sử dụng."
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "OpenClaw detecteert en verifieert het voorbereide model voordat het wordt gebruikt."
|
||||
}
|
||||
},
|
||||
"fa": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "OpenClaw قبل از استفاده، مدل آمادهشده را شناسایی و تأیید میکند."
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "OpenClaw обнаружит и проверит подготовленную модель перед её использованием."
|
||||
}
|
||||
},
|
||||
"sv": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "OpenClaw upptäcker och verifierar den förberedda modellen innan den används."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"OpenClaw — setup helper": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
@@ -122401,142 +122673,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Set up / Download model": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Set up / Download model"
|
||||
}
|
||||
},
|
||||
"zh-CN": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "设置 / 下载模型"
|
||||
}
|
||||
},
|
||||
"zh-TW": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "設定/下載模型"
|
||||
}
|
||||
},
|
||||
"pt-BR": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Configurar / Baixar modelo"
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Modell einrichten / herunterladen"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Configurar / Descargar modelo"
|
||||
}
|
||||
},
|
||||
"ja-JP": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "モデルのセットアップ / ダウンロード"
|
||||
}
|
||||
},
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "모델 설정 / 다운로드"
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Configurer / Télécharger le modèle"
|
||||
}
|
||||
},
|
||||
"hi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "मॉडल सेट अप / डाउनलोड करें"
|
||||
}
|
||||
},
|
||||
"ar": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "إعداد / تنزيل النموذج"
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Configura / Scarica modello"
|
||||
}
|
||||
},
|
||||
"tr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Modeli kur / indir"
|
||||
}
|
||||
},
|
||||
"uk": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Налаштувати / Завантажити модель"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Siapkan / Unduh model"
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Skonfiguruj / Pobierz model"
|
||||
}
|
||||
},
|
||||
"th": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ตั้งค่า / ดาวน์โหลดโมเดล"
|
||||
}
|
||||
},
|
||||
"vi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Thiết lập / Tải xuống mô hình"
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Model instellen / downloaden"
|
||||
}
|
||||
},
|
||||
"fa": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "راهاندازی / دانلود مدل"
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Настроить / Загрузить модель"
|
||||
}
|
||||
},
|
||||
"sv": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Konfigurera / Ladda ner modell"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Set up a local model": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
@@ -142121,142 +142257,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"The model is downloaded and prepared on this Gateway.": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "The model is downloaded and prepared on this Gateway."
|
||||
}
|
||||
},
|
||||
"zh-CN": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "模型已在此 Gateway 上下载并准备就绪。"
|
||||
}
|
||||
},
|
||||
"zh-TW": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "模型已在此 Gateway 上下載並準備完成。"
|
||||
}
|
||||
},
|
||||
"pt-BR": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "O modelo foi baixado e preparado neste Gateway."
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Das Modell wurde auf diesem Gateway heruntergeladen und vorbereitet."
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "El modelo se ha descargado y preparado en este Gateway."
|
||||
}
|
||||
},
|
||||
"ja-JP": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "モデルはこの Gateway でダウンロードされ、準備されています。"
|
||||
}
|
||||
},
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "이 Gateway에서 모델이 다운로드되고 준비되었습니다."
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Le modèle est téléchargé et préparé sur ce Gateway."
|
||||
}
|
||||
},
|
||||
"hi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "मॉडल इस Gateway पर डाउनलोड और तैयार किया गया है।"
|
||||
}
|
||||
},
|
||||
"ar": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "تم تنزيل النموذج وتجهيزه على هذا الـ Gateway."
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Il modello è scaricato e preparato su questo Gateway."
|
||||
}
|
||||
},
|
||||
"tr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Model bu Gateway'de indirildi ve hazırlandı."
|
||||
}
|
||||
},
|
||||
"uk": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Модель завантажено та підготовлено на цьому Gateway."
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Model diunduh dan disiapkan di Gateway ini."
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Model został pobrany i przygotowany na tym Gateway."
|
||||
}
|
||||
},
|
||||
"th": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ดาวน์โหลดและเตรียมโมเดลบน Gateway นี้แล้ว"
|
||||
}
|
||||
},
|
||||
"vi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Mô hình đã được tải xuống và chuẩn bị trên Gateway này."
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Het model is gedownload en voorbereid op deze Gateway."
|
||||
}
|
||||
},
|
||||
"fa": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "مدل روی این Gateway دانلود و آماده شده است."
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Модель загружена и подготовлена на этом Gateway."
|
||||
}
|
||||
},
|
||||
"sv": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Modellen har laddats ner och förberetts på denna Gateway."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"The node service restarted but did not remain running.": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
|
||||
@@ -156,6 +156,7 @@ private func resolvedPassword(opts: WizardCliOptions, config: GatewayConfig) ->
|
||||
|
||||
actor GatewayWizardClient {
|
||||
private enum ConnectChallengeError: Error {
|
||||
case invalid
|
||||
case timeout
|
||||
}
|
||||
|
||||
@@ -271,14 +272,15 @@ actor GatewayWizardClient {
|
||||
} else if let password = self.password {
|
||||
params["auth"] = ProtoAnyCodable(["password": ProtoAnyCodable(password)])
|
||||
}
|
||||
let connectNonce = try await self.waitForConnectChallenge()
|
||||
let connectChallenge = try await self.waitForConnectChallenge()
|
||||
let connectNonce = connectChallenge.nonce
|
||||
guard let identity = DeviceIdentityStore.loadOrCreatePersisted() else {
|
||||
throw NSError(
|
||||
domain: "OpenClawMacCLI",
|
||||
code: 1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Could not access the persisted device identity"])
|
||||
}
|
||||
let signedAtMs = Int64(Date().timeIntervalSince1970 * 1000)
|
||||
let signedAtMs = connectChallenge.issuedAtMs
|
||||
let payload = GatewayDeviceAuthPayload.buildConnectCompatibilityPayload(
|
||||
fields: .init(
|
||||
deviceId: identity.deviceId,
|
||||
@@ -320,7 +322,7 @@ actor GatewayWizardClient {
|
||||
}
|
||||
}
|
||||
|
||||
private func waitForConnectChallenge() async throws -> String {
|
||||
private func waitForConnectChallenge() async throws -> GatewayConnectChallenge {
|
||||
guard let task = self.task else { throw ConnectChallengeError.timeout }
|
||||
return try await AsyncTimeout.withTimeout(
|
||||
seconds: self.connectChallengeTimeoutSeconds,
|
||||
@@ -329,11 +331,13 @@ actor GatewayWizardClient {
|
||||
while true {
|
||||
let message = try await task.receive()
|
||||
let frame = try await self.decodeFrame(message)
|
||||
if case let .event(evt) = frame, evt.event == "connect.challenge",
|
||||
let payload = evt.payload?.value as? [String: ProtoAnyCodable],
|
||||
let nonce = GatewayConnectChallengeSupport.nonce(from: payload)
|
||||
{
|
||||
return nonce
|
||||
if case let .event(evt) = frame, evt.event == "connect.challenge" {
|
||||
guard let payload = evt.payload?.value as? [String: ProtoAnyCodable],
|
||||
let challenge = GatewayConnectChallengeSupport.challenge(from: payload)
|
||||
else {
|
||||
throw ConnectChallengeError.invalid
|
||||
}
|
||||
return challenge
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -57,7 +57,7 @@ private final class FakeWebSocketTask: WebSocketTasking, @unchecked Sendable {
|
||||
if !sentChallenge {
|
||||
sentChallenge = true
|
||||
return .string("""
|
||||
{"type":"event","event":"connect.challenge","payload":{"nonce":"test-nonce"}}
|
||||
{"type":"event","event":"connect.challenge","payload":{"nonce":"test-nonce","ts":1777777777000}}
|
||||
""")
|
||||
}
|
||||
if let request = latestUnrespondedRequest() {
|
||||
|
||||
@@ -9,12 +9,15 @@ extension WebSocketTasking {
|
||||
}
|
||||
|
||||
enum GatewayWebSocketTestSupport {
|
||||
static func connectChallengeData(nonce: String = "test-nonce") -> Data {
|
||||
static func connectChallengeData(
|
||||
nonce: String = "test-nonce",
|
||||
ts: Int64 = 1_800_000_000_000) -> Data
|
||||
{
|
||||
let json = """
|
||||
{
|
||||
"type": "event",
|
||||
"event": "connect.challenge",
|
||||
"payload": { "nonce": "\(nonce)" }
|
||||
"payload": { "nonce": "\(nonce)", "ts": \(ts) }
|
||||
}
|
||||
"""
|
||||
return Data(json.utf8)
|
||||
|
||||
@@ -510,8 +510,9 @@ public actor GatewayChannelActor {
|
||||
deviceId: identity?.deviceId,
|
||||
connectionGeneration: connectionGeneration,
|
||||
to: ¶ms)
|
||||
let signedAtMs = Int64(Date().timeIntervalSince1970 * 1000)
|
||||
let connectNonce = try await self.waitForConnectChallenge(task: task, attemptID: attemptID)
|
||||
let connectChallenge = try await self.waitForConnectChallenge(task: task, attemptID: attemptID)
|
||||
let signedAtMs = connectChallenge.issuedAtMs
|
||||
let connectNonce = connectChallenge.nonce
|
||||
try self.ensureCurrentConnectAttempt(attemptID, task: task)
|
||||
try self.requireCurrentConnection(connectionGeneration)
|
||||
if includeDeviceIdentity, let identity {
|
||||
@@ -1146,7 +1147,10 @@ extension GatewayChannelActor {
|
||||
}
|
||||
}
|
||||
|
||||
private func waitForConnectChallenge(task: WebSocketTaskBox, attemptID: UUID) async throws -> String {
|
||||
private func waitForConnectChallenge(
|
||||
task: WebSocketTaskBox,
|
||||
attemptID: UUID) async throws -> GatewayConnectChallenge
|
||||
{
|
||||
try await AsyncTimeout.withTimeout(
|
||||
seconds: self.connectChallengeTimeoutSeconds,
|
||||
onTimeout: { ConnectChallengeError.timeout },
|
||||
@@ -1157,11 +1161,13 @@ extension GatewayChannelActor {
|
||||
try await self.ensureCurrentConnectAttempt(attemptID, task: task)
|
||||
guard let data = self.decodeMessageData(msg) else { continue }
|
||||
guard let frame = try? self.decoder.decode(GatewayFrame.self, from: data) else { continue }
|
||||
if case let .event(evt) = frame, evt.event == "connect.challenge",
|
||||
let payload = evt.payload?.value as? [String: ProtoAnyCodable],
|
||||
let nonce = GatewayConnectChallengeSupport.nonce(from: payload)
|
||||
{
|
||||
return nonce
|
||||
if case let .event(evt) = frame, evt.event == "connect.challenge" {
|
||||
guard let payload = evt.payload?.value as? [String: ProtoAnyCodable],
|
||||
let challenge = GatewayConnectChallengeSupport.challenge(from: payload)
|
||||
else {
|
||||
throw ConnectChallengeError.invalid
|
||||
}
|
||||
return challenge
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -44,6 +44,7 @@ final class GatewayRequestCancellationGate: @unchecked Sendable {
|
||||
|
||||
extension GatewayChannelActor {
|
||||
enum ConnectChallengeError: Error {
|
||||
case invalid
|
||||
case timeout
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,40 @@
|
||||
import Foundation
|
||||
import OpenClawProtocol
|
||||
|
||||
public struct GatewayConnectChallenge: Sendable, Equatable {
|
||||
public let nonce: String
|
||||
public let issuedAtMs: Int64
|
||||
|
||||
public init(nonce: String, issuedAtMs: Int64) {
|
||||
self.nonce = nonce
|
||||
self.issuedAtMs = issuedAtMs
|
||||
}
|
||||
}
|
||||
|
||||
public enum GatewayConnectChallengeSupport {
|
||||
public static func nonce(from payload: [String: OpenClawProtocol.AnyCodable]?) -> String? {
|
||||
public static func challenge(
|
||||
from payload: [String: OpenClawProtocol.AnyCodable]?) -> GatewayConnectChallenge?
|
||||
{
|
||||
guard let nonce = payload?["nonce"]?.value as? String else { return nil }
|
||||
let trimmed = nonce.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return nil }
|
||||
return trimmed
|
||||
guard let rawTimestamp = payload?["ts"]?.value,
|
||||
let issuedAtMs = self.integerMilliseconds(rawTimestamp),
|
||||
issuedAtMs >= 0
|
||||
else { return nil }
|
||||
return GatewayConnectChallenge(nonce: trimmed, issuedAtMs: issuedAtMs)
|
||||
}
|
||||
|
||||
private static func integerMilliseconds(_ value: Any?) -> Int64? {
|
||||
switch value {
|
||||
case let value as Int:
|
||||
Int64(exactly: value)
|
||||
case let value as Int64:
|
||||
value
|
||||
case let value as Double where value.isFinite && value.rounded() == value:
|
||||
Int64(exactly: value)
|
||||
default:
|
||||
nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11956,6 +11956,7 @@ public struct ModelChoice: Codable, Sendable {
|
||||
public let available: Bool?
|
||||
public let contextwindow: Int?
|
||||
public let reasoning: Bool?
|
||||
public let supportstools: Bool?
|
||||
public let agentruntime: [String: AnyCodable]?
|
||||
public let apikeysupported: Bool?
|
||||
public let input: [AnyCodable]?
|
||||
@@ -11968,6 +11969,7 @@ public struct ModelChoice: Codable, Sendable {
|
||||
available: Bool? = nil,
|
||||
contextwindow: Int? = nil,
|
||||
reasoning: Bool? = nil,
|
||||
supportstools: Bool? = nil,
|
||||
agentruntime: [String: AnyCodable]? = nil,
|
||||
apikeysupported: Bool? = nil,
|
||||
input: [AnyCodable]? = nil)
|
||||
@@ -11979,6 +11981,7 @@ public struct ModelChoice: Codable, Sendable {
|
||||
self.available = available
|
||||
self.contextwindow = contextwindow
|
||||
self.reasoning = reasoning
|
||||
self.supportstools = supportstools
|
||||
self.agentruntime = agentruntime
|
||||
self.apikeysupported = apikeysupported
|
||||
self.input = input
|
||||
@@ -11992,6 +11995,7 @@ public struct ModelChoice: Codable, Sendable {
|
||||
case available
|
||||
case contextwindow = "contextWindow"
|
||||
case reasoning
|
||||
case supportstools = "supportsTools"
|
||||
case agentruntime = "agentRuntime"
|
||||
case apikeysupported = "apiKeySupported"
|
||||
case input
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import Foundation
|
||||
import OpenClawKit
|
||||
import OpenClawProtocol
|
||||
import Testing
|
||||
|
||||
struct GatewayConnectChallengeSupportTests {
|
||||
@Test func `parses gateway issued timestamp`() {
|
||||
let challenge = GatewayConnectChallengeSupport.challenge(from: [
|
||||
"nonce": AnyCodable(" nonce-1 "),
|
||||
"ts": AnyCodable(1_700_000_000_123),
|
||||
])
|
||||
|
||||
#expect(challenge == GatewayConnectChallenge(
|
||||
nonce: "nonce-1",
|
||||
issuedAtMs: 1_700_000_000_123))
|
||||
}
|
||||
|
||||
@Test func `rejects malformed challenge`() {
|
||||
let payloads: [[String: AnyCodable]] = [
|
||||
["nonce": AnyCodable("nonce-1"), "ts": AnyCodable("1700000000123")],
|
||||
["nonce": AnyCodable("nonce-1"), "ts": AnyCodable(-1)],
|
||||
["nonce": AnyCodable(" "), "ts": AnyCodable(1_700_000_000_123)],
|
||||
]
|
||||
for payload in payloads {
|
||||
#expect(GatewayConnectChallengeSupport.challenge(from: payload) == nil)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func `rejects challenge without timestamp`() {
|
||||
#expect(GatewayConnectChallengeSupport.challenge(from: [
|
||||
"nonce": AnyCodable("nonce-1"),
|
||||
]) == nil)
|
||||
}
|
||||
}
|
||||
@@ -397,7 +397,7 @@ private final class FakeGatewayWebSocketTask: WebSocketTasking, @unchecked Senda
|
||||
let frame: [String: Any] = [
|
||||
"type": "event",
|
||||
"event": "connect.challenge",
|
||||
"payload": ["nonce": nonce],
|
||||
"payload": ["nonce": nonce, "ts": 1_800_000_000_000],
|
||||
]
|
||||
return (try? JSONSerialization.data(withJSONObject: frame)) ?? Data()
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Distinct OPENCLAW_* names in production source under src, packages, and extensions.
|
||||
# Ratchet: lower this number when cleanup removes names; never raise it.
|
||||
518
|
||||
517
|
||||
|
||||
+48
-2
@@ -20,6 +20,8 @@ in a hosted or self-hosted Buzz workspace.
|
||||
`message` tool
|
||||
- Supports mention requirements and sender allowlists
|
||||
- Discovers rooms after the bot has been approved
|
||||
- Resolves current Buzz profile names, avatars, room names, and room membership
|
||||
through OpenClaw's directory commands
|
||||
- Reconnects and avoids processing the same message twice
|
||||
|
||||
The current plugin supports group rooms, Markdown text, and inbound structured
|
||||
@@ -183,6 +185,49 @@ openclaw message send \
|
||||
--message "Hello from OpenClaw"
|
||||
```
|
||||
|
||||
### Directory and sender labels
|
||||
|
||||
OpenClaw keeps a bounded snapshot of the configured rooms, their current
|
||||
relay-signed member lists, room metadata, and kind `0` member profiles. Incoming
|
||||
agent context uses the current profile and room names when available, while the
|
||||
sender public key remains the stable authorization, routing, and session
|
||||
identity.
|
||||
|
||||
Inspect the same data from the CLI:
|
||||
|
||||
```bash
|
||||
openclaw directory self --channel buzz
|
||||
openclaw directory peers list --channel buzz --query "alice"
|
||||
openclaw directory groups list --channel buzz --query "engineering"
|
||||
openclaw directory groups members \
|
||||
--channel buzz \
|
||||
--group-id buzz:<ROOM_UUID>
|
||||
```
|
||||
|
||||
When the Gateway is connected, directory reads reuse its authenticated Buzz
|
||||
connection and in-memory snapshot. A standalone directory command opens one
|
||||
bounded authenticated connection, loads the current snapshot, and closes it.
|
||||
Ordinary directory errors are logged without reconnecting. If a directory or
|
||||
profile subscription does not reach EOSE within 10 seconds, OpenClaw treats the
|
||||
Buzz relay session as stalled and recycles only that Buzz account connection;
|
||||
the Gateway keeps running.
|
||||
|
||||
Archived rooms are omitted from directory results and live room subscriptions.
|
||||
If a configured room is archived or restored while OpenClaw is connected, the
|
||||
plugin recycles only its Buzz connection so the subscription set matches the
|
||||
relay's current metadata. The Gateway keeps running.
|
||||
|
||||
Each configured room uses one room-scoped relay subscription. OpenClaw reserves
|
||||
four of Buzz's 1,024 connection subscriptions for membership notifications and
|
||||
concurrent profile, membership, and metadata queries, so one account can
|
||||
configure up to 1,020 rooms. Near that limit, optional member profile
|
||||
subscriptions are reduced first; directory entries continue to work with stable
|
||||
public keys and deterministic fallback labels.
|
||||
|
||||
Unique current room names can resolve as outbound targets through OpenClaw's
|
||||
shared directory lookup. The canonical `buzz:<ROOM_UUID>` target remains the
|
||||
safest choice for automation and for rooms with duplicate names.
|
||||
|
||||
### Route rooms to different agents
|
||||
|
||||
Standard OpenClaw bindings can send each Buzz room to a different agent,
|
||||
@@ -279,8 +324,9 @@ For a narrower sender policy:
|
||||
}
|
||||
```
|
||||
|
||||
Room targets are UUIDs. Use the room UUID shown during discovery or ask a room
|
||||
admin for it; a display name such as `general` is not a valid target.
|
||||
Room UUIDs are the canonical targets. Use the UUID shown during discovery or ask
|
||||
a room admin for it. A unique current room name can resolve through the live
|
||||
directory, but automation should use `buzz:<ROOM_UUID>` to avoid ambiguity.
|
||||
|
||||
For manual configuration, `groupAllowFrom` entries must use the 64-character
|
||||
hexadecimal form.
|
||||
|
||||
@@ -265,8 +265,10 @@ a single OpenClaw authority tool plus the inert native planning utility. In all
|
||||
three cases, setup writes remain confined to OpenClaw's audited approval
|
||||
contract.
|
||||
|
||||
Gemini CLI remains available for normal agents, but it cannot enforce the
|
||||
tool-free probe required by the inference gate, so it cannot host OpenClaw.
|
||||
Gemini CLI remains available as an explicitly configured runtime for normal
|
||||
agents, but Gemini CLI and Antigravity are not inference-gate setup routes.
|
||||
Use AI Studio API-key or Vertex AI for the inference gate. The optional Gemini
|
||||
CLI runtime specifically requires an AI Studio API-key profile.
|
||||
|
||||
## Switching to an agent
|
||||
|
||||
|
||||
+3
-2
@@ -41,8 +41,9 @@ automatic pass. Detected local runtimes are auto-tested after CLI and API-key
|
||||
candidates; when several local models are available, OpenClaw prefers the
|
||||
strongest tool-calling instruct family. The selected candidate must answer a
|
||||
real completion before its provider and model configuration is saved.
|
||||
Installed Gemini, Antigravity, Pi, and OpenCode CLIs are also reported when
|
||||
they cannot serve as the reusable inference route for guided setup.
|
||||
Pi and OpenCode CLIs may also be reported for context when they cannot serve as
|
||||
the reusable inference route for guided setup. Gemini CLI and Antigravity are
|
||||
not offered as detected setup routes.
|
||||
|
||||
`setup` accepts the same onboarding flags as `openclaw onboard`, including
|
||||
auth (`--auth-choice`, `--token`, provider key flags), Gateway
|
||||
|
||||
@@ -234,49 +234,18 @@ Claude CLI reuse (`claude -p`) is a sanctioned OpenClaw integration path. Anthro
|
||||
- Thinking: `/think adaptive` uses Google dynamic thinking. Gemini 3/3.1 omit a fixed `thinkingLevel`; Gemini 2.5 sends `thinkingBudget: -1`.
|
||||
- Direct Gemini runs also accept `agents.defaults.models["google/<model>"].params.cachedContent` (or legacy `cached_content`) to forward a provider-native `cachedContents/...` handle; Gemini cache hits surface as OpenClaw `cacheRead`
|
||||
|
||||
### Google Vertex and Gemini CLI
|
||||
### Google Vertex and Gemini CLI runtime
|
||||
|
||||
- Providers: `google-vertex`, `google-gemini-cli`
|
||||
- Auth: Vertex uses gcloud ADC; Gemini CLI uses its OAuth flow
|
||||
- `google-vertex`: managed Google Cloud access through gcloud Application
|
||||
Default Credentials.
|
||||
- `google-gemini-cli`: optional local runtime for an explicitly configured
|
||||
canonical `google/*` model.
|
||||
|
||||
<Warning>
|
||||
Gemini CLI OAuth in OpenClaw is an unofficial integration. Some users have reported Google account restrictions after using third-party clients. Review Google terms and use a non-critical account if you choose to proceed.
|
||||
</Warning>
|
||||
|
||||
Gemini CLI OAuth is shipped as part of the bundled `google` plugin.
|
||||
|
||||
<Steps>
|
||||
<Step title="Install Gemini CLI">
|
||||
<Tabs>
|
||||
<Tab title="brew">
|
||||
```bash
|
||||
brew install gemini-cli
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="npm">
|
||||
```bash
|
||||
npm install -g @google/gemini-cli
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
<Step title="Enable plugin">
|
||||
```bash
|
||||
openclaw plugins enable google
|
||||
```
|
||||
</Step>
|
||||
<Step title="Login">
|
||||
```bash
|
||||
openclaw models auth login --provider google-gemini-cli --set-default
|
||||
```
|
||||
|
||||
Default model: `google-gemini-cli/gemini-3-flash-preview`. You do **not** paste a client id or secret into `openclaw.json`. The CLI login flow stores tokens in auth profiles on the gateway host.
|
||||
|
||||
</Step>
|
||||
<Step title="Set project (if needed)">
|
||||
If requests fail after login, set `GOOGLE_CLOUD_PROJECT` or `GOOGLE_CLOUD_PROJECT_ID` on the gateway host.
|
||||
</Step>
|
||||
</Steps>
|
||||
OpenClaw does not create Gemini CLI OAuth or Antigravity OAuth profiles. Connect
|
||||
Google through an AI Studio API key or Vertex AI. If you explicitly choose the
|
||||
Gemini CLI runtime, it can use the selected Google API-key profile. Existing
|
||||
valid Gemini CLI OAuth profiles remain runtime-compatible, but they are not a
|
||||
setup or recovery route.
|
||||
|
||||
Gemini CLI uses `stream-json` by default. OpenClaw reads assistant stream
|
||||
messages and normalizes `stats.cached` into `cacheRead`; legacy
|
||||
@@ -473,11 +442,19 @@ In onboarding/configure model pickers, the Volcengine auth choice prefers both `
|
||||
|
||||
BytePlus ARK provides access to the same models as Volcano Engine for international users.
|
||||
|
||||
- Plugin: `@openclaw/byteplus-provider`
|
||||
- Provider: `byteplus` (coding: `byteplus-plan`)
|
||||
- Auth: `BYTEPLUS_API_KEY`
|
||||
- Example model: `byteplus-plan/ark-code-latest`
|
||||
- CLI: `openclaw onboard --auth-choice byteplus-api-key`
|
||||
|
||||
Install the official plugin and restart the Gateway:
|
||||
|
||||
```bash
|
||||
openclaw plugins install @openclaw/byteplus-provider
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
|
||||
@@ -685,8 +685,9 @@ Slack YAML module scenarios (`qa/scenarios/channels/slack-*.yaml`):
|
||||
|
||||
- `slack-canary`
|
||||
- `slack-mention-gating`
|
||||
- `slack-mpim-app-mention-dedupe` - opens a real C-prefixed group DM, sends one
|
||||
mention, verifies exactly one SUT reply in that MPIM, then closes it.
|
||||
- `slack-mpim-app-mention-dedupe` - opens a real C-prefixed group DM, verifies
|
||||
exactly one SUT reply after message/app-mention twin delivery, confirms a
|
||||
native threaded follow-up can recall that bot reply, then closes the MPIM.
|
||||
- `slack-allowlist-block`
|
||||
- `slack-channel-disabled-warning` - opt-in real-Slack probe that confirms a
|
||||
configured disabled channel emits a structured warning without replying.
|
||||
@@ -1259,8 +1260,7 @@ Preferred generic helpers for new scenarios:
|
||||
- `waitForChannelReady`
|
||||
- `injectInboundMessage`
|
||||
- `injectOutboundMessage`
|
||||
- `waitForTransportOutboundMessage`
|
||||
- `waitForChannelOutboundMessage`
|
||||
- `waitForOutboundMessage`
|
||||
- `waitForNoTransportOutbound`
|
||||
- `getTransportSnapshot`
|
||||
- `readTransportMessage`
|
||||
@@ -1269,10 +1269,10 @@ Preferred generic helpers for new scenarios:
|
||||
- `resetTransport`
|
||||
|
||||
Compatibility aliases remain available for existing scenarios -
|
||||
`waitForQaChannelReady`, `waitForOutboundMessage`, `waitForNoOutbound`,
|
||||
`formatConversationTranscript`, `resetBus` - but new scenario authoring
|
||||
should use the generic names. The aliases exist to avoid a flag-day
|
||||
migration, not as the model going forward.
|
||||
`waitForQaChannelReady`, `waitForNoOutbound`, `formatConversationTranscript`,
|
||||
and `resetBus` - but new scenario authoring should use the generic names.
|
||||
Use the canonical `waitForOutboundMessage` for outbound checks instead of
|
||||
adding transport- or channel-specific outbound wait aliases.
|
||||
|
||||
## Reporting
|
||||
|
||||
|
||||
@@ -315,7 +315,7 @@ provider-neutral for CLI, app, and Control UI consumers.
|
||||
- **DeepSeek**: API key via env/config/auth store (`DEEPSEEK_API_KEY`).
|
||||
Shows each provider-reported currency balance.
|
||||
- **GitHub Copilot**: OAuth tokens in auth profiles.
|
||||
- **Gemini CLI**: OAuth tokens in auth profiles.
|
||||
- **Gemini CLI**: existing OAuth profiles or supported Google API-key profiles.
|
||||
- **MiniMax**: API key or MiniMax OAuth auth profile. OpenClaw treats
|
||||
`minimax`, `minimax-cn`, and `minimax-portal` as the same MiniMax quota
|
||||
surface, prefers stored MiniMax OAuth when present, and otherwise falls back
|
||||
|
||||
+2
-1
@@ -308,6 +308,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
|
||||
- H2: Guided setup
|
||||
- H3: Bot approval
|
||||
- H2: Agent tools and messaging
|
||||
- H3: Directory and sender labels
|
||||
- H3: Route rooms to different agents
|
||||
- H2: Access control
|
||||
- H2: Manual configuration
|
||||
@@ -2800,7 +2801,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
|
||||
- H3: Other subscription-style hosted options
|
||||
- H3: OpenCode
|
||||
- H3: Google Gemini (API key)
|
||||
- H3: Google Vertex and Gemini CLI
|
||||
- H3: Google Vertex and Gemini CLI runtime
|
||||
- H3: Z.AI (GLM)
|
||||
- H3: Vercel AI Gateway
|
||||
- H3: Other bundled provider plugins
|
||||
|
||||
@@ -231,7 +231,11 @@ The bundled Google plugin registers for `google-gemini-cli`:
|
||||
| `sessionMode` | `existing` |
|
||||
| `sessionIdFields` | `["session_id", "sessionId"]` |
|
||||
|
||||
Prerequisite: the local Gemini CLI must be installed and on `PATH` as `gemini` (`brew install gemini-cli` or `npm install -g @google/gemini-cli`).
|
||||
Prerequisites: the local Gemini CLI must be installed and on `PATH` as `gemini`
|
||||
(`brew install gemini-cli` or `npm install -g @google/gemini-cli`), and the
|
||||
selected model must have a supported Google AI Studio API-key profile. Existing
|
||||
valid legacy Gemini CLI OAuth profiles remain runtime-compatible, but OpenClaw
|
||||
does not create or repair them.
|
||||
|
||||
Gemini CLI output notes:
|
||||
|
||||
|
||||
@@ -65,9 +65,12 @@ gateway` or the `openclaw onboard --gateway-auth ...` options, then let device
|
||||
pairing mint the client token:
|
||||
|
||||
1. Persist an Ed25519 device identity in the client.
|
||||
2. Wait for `connect.challenge`, sign the challenge-bound device payload, and send
|
||||
`connect` with the requested operator role, scopes, and the shared Gateway token
|
||||
or password for bootstrap authentication.
|
||||
2. Wait for `connect.challenge`, use its `ts` as the device proof's `signedAt`,
|
||||
sign the challenge-bound device payload, and send `connect` with the requested
|
||||
operator role, scopes, and the shared Gateway token or password for bootstrap
|
||||
authentication. A received WebSocket challenge without a non-negative integer
|
||||
`ts` is invalid. Clients that explicitly support Gateways from before
|
||||
`connect.challenge` existed may use local time only on their no-challenge path.
|
||||
3. If the Gateway returns structured `PAIRING_REQUIRED` details, show the request
|
||||
ID and pause or retry according to `error.details.recommendedNextStep`.
|
||||
4. On the Gateway host, review the request with `openclaw devices list`, then
|
||||
|
||||
@@ -92,6 +92,12 @@ Gateway sends a pre-connect challenge:
|
||||
}
|
||||
```
|
||||
|
||||
Device-auth clients use the challenge `ts` as `connect.params.device.signedAt`.
|
||||
For WebSocket challenges, `ts` must be a non-negative integer. Clients that
|
||||
explicitly support Gateways from before `connect.challenge` existed may use local
|
||||
time only when no challenge arrives; a received challenge with an absent or
|
||||
malformed `ts` is invalid.
|
||||
|
||||
Client replies with `connect`:
|
||||
|
||||
```json
|
||||
@@ -1165,6 +1171,7 @@ Common migration failures:
|
||||
Migration target:
|
||||
|
||||
- Always wait for `connect.challenge`.
|
||||
- Use `connect.challenge.payload.ts` as `connect.params.device.signedAt`.
|
||||
- Sign the v2 payload that includes the server nonce.
|
||||
- Send the same nonce in `connect.params.device.nonce`.
|
||||
- Preferred signature payload is `v3`
|
||||
|
||||
@@ -606,18 +606,16 @@ and troubleshooting see the main [FAQ](/help/faq).
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="How do I set up Gemini CLI OAuth?">
|
||||
Gemini CLI uses a **plugin auth flow**, not a client id or secret in `openclaw.json`.
|
||||
<Accordion title="Can I use Gemini CLI or Antigravity OAuth?">
|
||||
OpenClaw does not offer new Gemini CLI OAuth or Antigravity OAuth setup.
|
||||
Connect Google with an AI Studio API key or Vertex AI instead.
|
||||
|
||||
1. Install Gemini CLI locally so `gemini` is on `PATH`:
|
||||
- Homebrew: `brew install gemini-cli`
|
||||
- npm: `npm install -g @google/gemini-cli`
|
||||
2. Enable the plugin: `openclaw plugins enable google`
|
||||
3. Login: `openclaw models auth login --provider google-gemini-cli --set-default`
|
||||
4. Default model after login: `google/gemini-3.1-pro-preview` (runtime `google-gemini-cli`)
|
||||
5. Requests failing after login? Set `GOOGLE_CLOUD_PROJECT` or `GOOGLE_CLOUD_PROJECT_ID` on the gateway host and retry.
|
||||
The optional `google-gemini-cli` runtime remains available for advanced
|
||||
setups using a supported Google API-key profile. Existing valid legacy
|
||||
Gemini CLI OAuth profiles remain executable for compatibility, but OpenClaw
|
||||
cannot create or repair them.
|
||||
|
||||
OAuth tokens are stored in auth profiles on the gateway host. Details: [Google](/providers/google), [Model providers](/concepts/model-providers).
|
||||
Details: [Google](/providers/google), [Model providers](/concepts/model-providers).
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
@@ -650,7 +650,7 @@ Docker runners below with an explicit `OPENCLAW_PROFILE_FILE`.
|
||||
- Test: `extensions/comfy/comfy.live.test.ts`
|
||||
- Enable: `OPENCLAW_LIVE_TEST=1 COMFY_LIVE_TEST=1 pnpm test:live -- extensions/comfy/comfy.live.test.ts`
|
||||
- Scope:
|
||||
- Exercises the bundled comfy image, video, and `music_generate` paths
|
||||
- Exercises the comfy image, video, and `music_generate` paths
|
||||
- Skips each capability unless `plugins.entries.comfy.config.<capability>` is configured
|
||||
- Useful after changing comfy workflow submission, polling, downloads, or plugin registration
|
||||
|
||||
|
||||
@@ -108,6 +108,12 @@ for an efficient source-server update: it restores tracked build outputs that
|
||||
`main` (or rebases a local server branch onto `origin/main`), installs
|
||||
dependencies, builds clean, and restarts the gateway.
|
||||
|
||||
Generated output roots such as `dist`, `dist-runtime`, and package-local
|
||||
`dist` directories must be real directories. Builds refuse symbolic-link roots
|
||||
before reading or mutating their contents so cleanup cannot affect the link
|
||||
target. Replace an output-root symlink with a real directory before updating or
|
||||
building a source checkout.
|
||||
|
||||
```bash
|
||||
ssh you@server 'cd /path/to/openclaw && scripts/update-gateway.sh'
|
||||
```
|
||||
|
||||
+2
-1
@@ -37,7 +37,8 @@ If you have not configured models and `tools.media.audio.enabled` is not `false`
|
||||
Install/link provenance is capability evidence, not execution evidence. It never moves a candidate ahead of CPU sherpa by itself. OpenClaw does not load a model during setup or status checks just to probe a backend.
|
||||
Auto-detected whisper.cpp keeps its normal model-run logs enabled so OpenClaw can record the upstream `using … backend` line. Explicit CLI entries keep their configured output flags.
|
||||
|
||||
Gemini CLI auto-detect for media understanding was replaced by a sandboxed Antigravity CLI (`agy`) fallback for image/video; audio does not use a CLI fallback beyond the local binaries above.
|
||||
Gemini CLI and Antigravity are not auto-detected for media understanding. Audio
|
||||
does not use a CLI fallback beyond the local binaries above.
|
||||
|
||||
To disable auto-detection, set `tools.media.audio.enabled: false`. To customize, add capability-tagged entries to `tools.media.models`.
|
||||
|
||||
|
||||
@@ -174,9 +174,6 @@ When `tools.media.<capability>.enabled` is not `false` and no models are configu
|
||||
- Video: Google → Qwen → Moonshot
|
||||
|
||||
</Step>
|
||||
<Step title="Antigravity CLI (image/video only)">
|
||||
First installed `agy` or `antigravity` binary (override with `OPENCLAW_ANTIGRAVITY_CLI`), sandboxed against the media's directory.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
To disable auto-detection for a capability:
|
||||
|
||||
@@ -51,7 +51,7 @@ Each entry lists the package, distribution route, and description.
|
||||
|
||||
## Core npm package
|
||||
|
||||
67 plugins
|
||||
59 plugins
|
||||
|
||||
- **[admin-http-rpc](/plugins/reference/admin-http-rpc)** (`@openclaw/admin-http-rpc`) - included in OpenClaw. OpenClaw admin HTTP RPC endpoint.
|
||||
|
||||
@@ -67,14 +67,10 @@ Each entry lists the package, distribution route, and description.
|
||||
|
||||
- **[browser](/plugins/reference/browser)** (`@openclaw/browser-plugin`) - included in OpenClaw. Adds agent-callable tools.
|
||||
|
||||
- **[byteplus](/plugins/reference/byteplus)** (`@openclaw/byteplus-provider`) - included in OpenClaw. Adds BytePlus, BytePlus Plan model provider support to OpenClaw.
|
||||
|
||||
- **[canvas](/plugins/reference/canvas)** (`@openclaw/canvas-plugin`) - included in OpenClaw. Experimental Canvas control and A2UI rendering surfaces for paired nodes.
|
||||
|
||||
- **[clawrouter](/plugins/reference/clawrouter)** (`@openclaw/clawrouter`) - included in OpenClaw. Adds ClawRouter model provider support to OpenClaw.
|
||||
|
||||
- **[comfy](/plugins/reference/comfy)** (`@openclaw/comfy-provider`) - included in OpenClaw. Adds ComfyUI model provider support to OpenClaw.
|
||||
|
||||
- **[copilot-proxy](/plugins/reference/copilot-proxy)** (`@openclaw/copilot-proxy`) - included in OpenClaw. Adds Copilot Proxy model provider support to OpenClaw.
|
||||
|
||||
- **[crabbox](/plugins/reference/crabbox)** (`@openclaw/crabbox-provider`) - included in OpenClaw. Cloud worker provider backed by the Crabbox CLI.
|
||||
@@ -85,8 +81,6 @@ Each entry lists the package, distribution route, and description.
|
||||
|
||||
- **[document-extract](/plugins/reference/document-extract)** (`@openclaw/document-extract-plugin`) - included in OpenClaw. Extract text and fallback page images from local document attachments.
|
||||
|
||||
- **[duckduckgo](/plugins/reference/duckduckgo)** (`@openclaw/duckduckgo-plugin`) - included in OpenClaw. Adds web search provider support.
|
||||
|
||||
- **[elevenlabs](/plugins/reference/elevenlabs)** (`@openclaw/elevenlabs-speech`) - included in OpenClaw. Adds media understanding provider support. Adds realtime transcription provider support. Adds text-to-speech provider support.
|
||||
|
||||
- **[fal](/plugins/reference/fal)** (`@openclaw/fal-provider`) - included in OpenClaw. Adds fal model provider support to OpenClaw.
|
||||
@@ -159,8 +153,6 @@ Each entry lists the package, distribution route, and description.
|
||||
|
||||
- **[sglang](/plugins/reference/sglang)** (`@openclaw/sglang-provider`) - included in OpenClaw. Adds SGLang model provider support to OpenClaw.
|
||||
|
||||
- **[synthetic](/plugins/reference/synthetic)** (`@openclaw/synthetic-provider`) - included in OpenClaw. Adds Synthetic model provider support to OpenClaw.
|
||||
|
||||
- **[telegram](/plugins/reference/telegram)** (`@openclaw/telegram`) - included in OpenClaw. Adds the Telegram channel surface for sending and receiving OpenClaw messages.
|
||||
|
||||
- **[together](/plugins/reference/together)** (`@openclaw/together-provider`) - included in OpenClaw. Adds Together model provider support to OpenClaw.
|
||||
@@ -171,12 +163,6 @@ Each entry lists the package, distribution route, and description.
|
||||
|
||||
- **[vllm](/plugins/reference/vllm)** (`@openclaw/vllm-provider`) - included in OpenClaw. Adds vLLM model provider support to OpenClaw.
|
||||
|
||||
- **[volcengine](/plugins/reference/volcengine)** (`@openclaw/volcengine-provider`) - included in OpenClaw. Adds Volcengine, Volcengine Plan model provider support to OpenClaw.
|
||||
|
||||
- **[voyage](/plugins/reference/voyage)** (`@openclaw/voyage-provider`) - included in OpenClaw. Adds memory embedding provider support.
|
||||
|
||||
- **[vydra](/plugins/reference/vydra)** (`@openclaw/vydra-provider`) - included in OpenClaw. Adds Vydra model provider support to OpenClaw.
|
||||
|
||||
- **[web-readability](/plugins/reference/web-readability)** (`@openclaw/web-readability-plugin`) - included in OpenClaw. Extract readable article content from local HTML web fetch responses.
|
||||
|
||||
- **[webhooks](/plugins/reference/webhooks)** (`@openclaw/webhooks`) - included in OpenClaw. Authenticated inbound webhooks that bind external automation to OpenClaw TaskFlows.
|
||||
@@ -185,11 +171,9 @@ Each entry lists the package, distribution route, and description.
|
||||
|
||||
- **[xai](/plugins/reference/xai)** (`@openclaw/xai-plugin`) - included in OpenClaw. Adds xAI model provider support to OpenClaw.
|
||||
|
||||
- **[xiaomi](/plugins/reference/xiaomi)** (`@openclaw/xiaomi-provider`) - included in OpenClaw. Adds Xiaomi, Xiaomi Token Plan model provider support to OpenClaw.
|
||||
|
||||
## Official external packages
|
||||
|
||||
78 plugins
|
||||
86 plugins
|
||||
|
||||
- **[acpx](/plugins/reference/acpx)** (`@openclaw/acpx`) - npm; ClawHub. OpenClaw ACP runtime backend with plugin-owned session and transport management.
|
||||
|
||||
@@ -207,6 +191,8 @@ Each entry lists the package, distribution route, and description.
|
||||
|
||||
- **[buzz](/plugins/reference/buzz)** (`@openclaw/buzz`) - npm; ClawHub: `clawhub:@openclaw/buzz`. Connect OpenClaw agents to Buzz rooms.
|
||||
|
||||
- **[byteplus](/plugins/reference/byteplus)** (`@openclaw/byteplus-provider`) - npm; ClawHub: `clawhub:@openclaw/byteplus-provider`. Adds BytePlus, BytePlus Plan model provider support to OpenClaw.
|
||||
|
||||
- **[cerebras](/plugins/reference/cerebras)** (`@openclaw/cerebras-provider`) - npm; ClawHub: `clawhub:@openclaw/cerebras-provider`. Adds Cerebras model provider support to OpenClaw.
|
||||
|
||||
- **[chutes](/plugins/reference/chutes)** (`@openclaw/chutes-provider`) - npm; ClawHub: `clawhub:@openclaw/chutes-provider`. Adds Chutes model provider support to OpenClaw.
|
||||
@@ -219,6 +205,8 @@ Each entry lists the package, distribution route, and description.
|
||||
|
||||
- **[cohere](/plugins/reference/cohere)** (`@openclaw/cohere-provider`) - npm; ClawHub: `clawhub:@openclaw/cohere-provider`. OpenClaw Cohere provider plugin.
|
||||
|
||||
- **[comfy](/plugins/reference/comfy)** (`@openclaw/comfy-provider`) - npm; ClawHub: `clawhub:@openclaw/comfy-provider`. Adds ComfyUI model provider support to OpenClaw.
|
||||
|
||||
- **[copilot](/plugins/reference/copilot)** (`@openclaw/copilot`) - npm; ClawHub: `clawhub:@openclaw/copilot`. Registers the GitHub Copilot agent runtime.
|
||||
|
||||
- **[deepinfra](/plugins/reference/deepinfra)** (`@openclaw/deepinfra-provider`) - npm; ClawHub: `clawhub:@openclaw/deepinfra-provider`. Adds DeepInfra model provider support to OpenClaw.
|
||||
@@ -235,6 +223,8 @@ Each entry lists the package, distribution route, and description.
|
||||
|
||||
- **[discord](/plugins/reference/discord)** (`@openclaw/discord`) - npm; ClawHub. OpenClaw Discord channel plugin for channels, DMs, commands, and app events.
|
||||
|
||||
- **[duckduckgo](/plugins/reference/duckduckgo)** (`@openclaw/duckduckgo-plugin`) - npm; ClawHub: `clawhub:@openclaw/duckduckgo-plugin`. Adds web search provider support.
|
||||
|
||||
- **[exa](/plugins/reference/exa)** (`@openclaw/exa-plugin`) - npm; ClawHub: `clawhub:@openclaw/exa-plugin`. Adds web search provider support.
|
||||
|
||||
- **[featherless](/plugins/reference/featherless)** (`@openclaw/featherless-provider`) - npm; ClawHub: `clawhub:@openclaw/featherless-provider`. OpenClaw Featherless AI provider plugin.
|
||||
@@ -319,6 +309,8 @@ Each entry lists the package, distribution route, and description.
|
||||
|
||||
- **[synology-chat](/plugins/reference/synology-chat)** (`@openclaw/synology-chat`) - npm; ClawHub. Synology Chat channel plugin for OpenClaw channels and direct messages.
|
||||
|
||||
- **[synthetic](/plugins/reference/synthetic)** (`@openclaw/synthetic-provider`) - npm; ClawHub: `clawhub:@openclaw/synthetic-provider`. Adds Synthetic model provider support to OpenClaw.
|
||||
|
||||
- **[tavily](/plugins/reference/tavily)** (`@openclaw/tavily-plugin`) - npm; ClawHub: `clawhub:@openclaw/tavily-plugin`. Adds agent-callable tools. Adds web search provider support.
|
||||
|
||||
- **[teams-meetings](/plugins/reference/teams-meetings)** (`@openclaw/teams-meetings`) - npm; ClawHub: `clawhub:@openclaw/teams-meetings`. Join Microsoft Teams meetings as a Chrome browser guest.
|
||||
@@ -337,8 +329,16 @@ Each entry lists the package, distribution route, and description.
|
||||
|
||||
- **[voice-call](/plugins/reference/voice-call)** (`@openclaw/voice-call`) - npm; ClawHub. OpenClaw voice-call plugin for Twilio, Telnyx, and Plivo phone calls.
|
||||
|
||||
- **[volcengine](/plugins/reference/volcengine)** (`@openclaw/volcengine-provider`) - npm; ClawHub: `clawhub:@openclaw/volcengine-provider`. Adds Volcengine, Volcengine Plan model provider support to OpenClaw.
|
||||
|
||||
- **[voyage](/plugins/reference/voyage)** (`@openclaw/voyage-provider`) - npm; ClawHub: `clawhub:@openclaw/voyage-provider`. Adds memory embedding provider support.
|
||||
|
||||
- **[vydra](/plugins/reference/vydra)** (`@openclaw/vydra-provider`) - npm; ClawHub: `clawhub:@openclaw/vydra-provider`. Adds Vydra model provider support to OpenClaw.
|
||||
|
||||
- **[whatsapp](/plugins/reference/whatsapp)** (`@openclaw/whatsapp`) - ClawHub: `clawhub:@openclaw/whatsapp`; npm. OpenClaw WhatsApp channel plugin for WhatsApp Web chats.
|
||||
|
||||
- **[xiaomi](/plugins/reference/xiaomi)** (`@openclaw/xiaomi-provider`) - npm; ClawHub: `clawhub:@openclaw/xiaomi-provider`. Adds Xiaomi, Xiaomi Token Plan model provider support to OpenClaw.
|
||||
|
||||
- **[zai](/plugins/reference/zai)** (`@openclaw/zai-provider`) - npm; ClawHub: `clawhub:@openclaw/zai-provider`. Adds Z.AI model provider support to OpenClaw.
|
||||
|
||||
- **[zalo](/plugins/reference/zalo)** (`@openclaw/zalo`) - npm; ClawHub. OpenClaw Zalo channel plugin for bot and webhook chats.
|
||||
|
||||
@@ -12,7 +12,7 @@ Adds BytePlus, BytePlus Plan model provider support to OpenClaw.
|
||||
## Distribution
|
||||
|
||||
- Package: `@openclaw/byteplus-provider`
|
||||
- Install route: included in OpenClaw
|
||||
- Install route: npm; ClawHub: `clawhub:@openclaw/byteplus-provider`
|
||||
|
||||
## Surface
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Adds ComfyUI model provider support to OpenClaw.
|
||||
## Distribution
|
||||
|
||||
- Package: `@openclaw/comfy-provider`
|
||||
- Install route: included in OpenClaw
|
||||
- Install route: npm; ClawHub: `clawhub:@openclaw/comfy-provider`
|
||||
|
||||
## Surface
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Adds web search provider support.
|
||||
## Distribution
|
||||
|
||||
- Package: `@openclaw/duckduckgo-plugin`
|
||||
- Install route: included in OpenClaw
|
||||
- Install route: npm; ClawHub: `clawhub:@openclaw/duckduckgo-plugin`
|
||||
|
||||
## Surface
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Adds Synthetic model provider support to OpenClaw.
|
||||
## Distribution
|
||||
|
||||
- Package: `@openclaw/synthetic-provider`
|
||||
- Install route: included in OpenClaw
|
||||
- Install route: npm; ClawHub: `clawhub:@openclaw/synthetic-provider`
|
||||
|
||||
## Surface
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Adds Volcengine, Volcengine Plan model provider support to OpenClaw.
|
||||
## Distribution
|
||||
|
||||
- Package: `@openclaw/volcengine-provider`
|
||||
- Install route: included in OpenClaw
|
||||
- Install route: npm; ClawHub: `clawhub:@openclaw/volcengine-provider`
|
||||
|
||||
## Surface
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Adds memory embedding provider support.
|
||||
## Distribution
|
||||
|
||||
- Package: `@openclaw/voyage-provider`
|
||||
- Install route: included in OpenClaw
|
||||
- Install route: npm; ClawHub: `clawhub:@openclaw/voyage-provider`
|
||||
|
||||
## Surface
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Adds Vydra model provider support to OpenClaw.
|
||||
## Distribution
|
||||
|
||||
- Package: `@openclaw/vydra-provider`
|
||||
- Install route: included in OpenClaw
|
||||
- Install route: npm; ClawHub: `clawhub:@openclaw/vydra-provider`
|
||||
|
||||
## Surface
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Adds Xiaomi, Xiaomi Token Plan model provider support to OpenClaw.
|
||||
## Distribution
|
||||
|
||||
- Package: `@openclaw/xiaomi-provider`
|
||||
- Install route: included in OpenClaw
|
||||
- Install route: npm; ClawHub: `clawhub:@openclaw/xiaomi-provider`
|
||||
|
||||
## Surface
|
||||
|
||||
|
||||
@@ -4,11 +4,17 @@ title: "ComfyUI"
|
||||
read_when:
|
||||
- You want to use local ComfyUI workflows with OpenClaw
|
||||
- You want to use Comfy Cloud with image, video, or music workflows
|
||||
- You need the bundled comfy plugin config keys
|
||||
- You need the comfy plugin config keys
|
||||
---
|
||||
|
||||
OpenClaw ships a bundled `comfy` plugin for workflow-driven ComfyUI runs. The
|
||||
plugin is entirely workflow-driven: OpenClaw does not map generic `size`,
|
||||
Install the official `comfy` plugin for workflow-driven ComfyUI runs:
|
||||
|
||||
```bash
|
||||
openclaw plugins install @openclaw/comfy-provider
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
The plugin is entirely workflow-driven: OpenClaw does not map generic `size`,
|
||||
`aspectRatio`, `resolution`, `durationSeconds`, or TTS-style controls onto
|
||||
your graph.
|
||||
|
||||
|
||||
+49
-50
@@ -1,9 +1,9 @@
|
||||
---
|
||||
summary: "Google Gemini setup (API key + OAuth, image generation, media understanding, TTS, web search)"
|
||||
summary: "Google Gemini setup (AI Studio API key, Vertex AI, optional CLI runtime, and multimodal tools)"
|
||||
title: "Google (Gemini)"
|
||||
read_when:
|
||||
- You want to use Google Gemini models with OpenClaw
|
||||
- You need the API key or OAuth auth flow
|
||||
- You need Google AI Studio, Vertex AI, or Gemini CLI runtime guidance
|
||||
---
|
||||
|
||||
The Google plugin provides access to Gemini models through Google AI Studio, plus image generation, media understanding (image/audio/video), text-to-speech, and web search via Gemini Grounding.
|
||||
@@ -11,15 +11,17 @@ The Google plugin provides access to Gemini models through Google AI Studio, plu
|
||||
- Provider: `google`
|
||||
- Auth: `GEMINI_API_KEY` or `GOOGLE_API_KEY`
|
||||
- API: Google Gemini API
|
||||
- Runtime option: `agentRuntime.id: "google-gemini-cli"` reuses Gemini CLI OAuth while keeping model refs canonical as `google/*`.
|
||||
- Managed-cloud provider: `google-vertex` with Google Cloud Application Default Credentials
|
||||
- Optional runtime: `agentRuntime.id: "google-gemini-cli"` runs an explicitly configured model through the local Gemini CLI
|
||||
|
||||
## Getting started
|
||||
|
||||
Choose your preferred auth method and follow the setup steps.
|
||||
For most installations, use a Google AI Studio API key. Use `google-vertex` when
|
||||
the Gateway already runs inside a managed Google Cloud environment.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="API key">
|
||||
**Best for:** standard Gemini API access through Google AI Studio.
|
||||
<Tab title="AI Studio API key">
|
||||
**Recommended for:** standard Gemini API access.
|
||||
|
||||
<Steps>
|
||||
<Step title="Get an API key">
|
||||
@@ -70,16 +72,23 @@ Choose your preferred auth method and follow the setup steps.
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="Gemini CLI (OAuth)">
|
||||
**Best for:** signing in with your Google account through Gemini CLI OAuth instead of using a separate API key.
|
||||
<Tab title="Gemini CLI runtime">
|
||||
**Advanced use only:** run a canonical `google/*` model through an installed
|
||||
Gemini CLI while keeping authentication on the supported AI Studio API-key
|
||||
path.
|
||||
|
||||
<Warning>
|
||||
The `google-gemini-cli` provider is an unofficial integration. Some users
|
||||
report account restrictions when using OAuth this way. Use at your own risk.
|
||||
</Warning>
|
||||
OpenClaw does not offer new Gemini CLI OAuth or Antigravity OAuth setup.
|
||||
[Google ended consumer Gemini CLI Login with Google access on June 18, 2026](https://developers.google.com/gemini-code-assist/docs/deprecations/code-assist-individuals),
|
||||
and the [Antigravity terms](https://antigravity.google/terms) prohibit
|
||||
third-party tools from accessing the service through Antigravity OAuth. Use
|
||||
an AI Studio API key or Vertex AI instead.
|
||||
|
||||
<Steps>
|
||||
<Step title="Install the Gemini CLI">
|
||||
<Step title="Configure Google AI Studio">
|
||||
Complete the API-key setup in the first tab. OpenClaw must have a usable
|
||||
`google` API-key profile before the CLI runtime can be selected.
|
||||
</Step>
|
||||
<Step title="Install Gemini CLI">
|
||||
The local `gemini` command must be available on `PATH`.
|
||||
|
||||
```bash
|
||||
@@ -93,46 +102,37 @@ Choose your preferred auth method and follow the setup steps.
|
||||
OpenClaw supports both Homebrew installs and global npm installs, including
|
||||
common Windows/npm layouts.
|
||||
</Step>
|
||||
<Step title="Log in via OAuth">
|
||||
```bash
|
||||
openclaw models auth login --provider google-gemini-cli --set-default
|
||||
```
|
||||
</Step>
|
||||
<Step title="Verify the model is available">
|
||||
```bash
|
||||
openclaw models list --provider google
|
||||
<Step title="Select the CLI runtime">
|
||||
Keep the canonical Google model ref and opt that model into the CLI
|
||||
runtime:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "google/gemini-3.1-pro-preview" },
|
||||
models: {
|
||||
"google/gemini-3.1-pro-preview": {
|
||||
agentRuntime: { id: "google-gemini-cli" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
- Default model: `google/gemini-3.1-pro-preview`
|
||||
- Runtime: `google-gemini-cli`
|
||||
- Alias: `gemini-cli`
|
||||
- Auth: selected Google AI Studio API-key profile
|
||||
- Model refs: canonical `google/*`
|
||||
|
||||
Gemini 3.1 Pro's Gemini API model id is `gemini-3.1-pro-preview`. OpenClaw accepts the shorter `google/gemini-3.1-pro` as a convenience alias and normalizes it before provider calls.
|
||||
Existing valid Gemini CLI OAuth profiles remain executable for compatibility,
|
||||
but OpenClaw cannot create or repair them. If one breaks, replace it with a
|
||||
Google AI Studio API-key profile.
|
||||
|
||||
**Environment variables:**
|
||||
|
||||
- `OPENCLAW_GEMINI_OAUTH_CLIENT_ID` / `GEMINI_CLI_OAUTH_CLIENT_ID`
|
||||
- `OPENCLAW_GEMINI_OAUTH_CLIENT_SECRET` / `GEMINI_CLI_OAUTH_CLIENT_SECRET`
|
||||
|
||||
<Note>
|
||||
If Gemini CLI OAuth requests fail after login, set `GOOGLE_CLOUD_PROJECT` or
|
||||
`GOOGLE_CLOUD_PROJECT_ID` on the gateway host and retry.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
If login fails before the browser flow starts, make sure the local `gemini`
|
||||
command is installed and on `PATH`.
|
||||
</Note>
|
||||
|
||||
Onboarding auto-detection lists an existing Gemini CLI login but never
|
||||
auto-tests it because Gemini CLI has no tool-free probe. Choose Gemini CLI
|
||||
OAuth or a Gemini API key to continue.
|
||||
|
||||
`google-gemini-cli/*` model refs are legacy compatibility aliases. New
|
||||
configs should use `google/*` model refs plus the `google-gemini-cli`
|
||||
runtime when they want local Gemini CLI execution.
|
||||
`google-gemini-cli/*` refs remain legacy compatibility aliases. New configs
|
||||
should use `google/*` model refs plus the explicit runtime selection above.
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
@@ -468,10 +468,9 @@ roundtrip; pass `--openai-audio-cycles 3` for a short repeated lifecycle soak.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Gemini CLI usage notes">
|
||||
When using the `google-gemini-cli` OAuth provider, OpenClaw uses Gemini
|
||||
CLI `stream-json` output by default and normalizes usage from the final
|
||||
`stats` payload. Legacy `--output-format json` overrides still use the
|
||||
JSON parser.
|
||||
The optional `google-gemini-cli` runtime uses Gemini CLI `stream-json`
|
||||
output by default and normalizes usage from the final `stats` payload.
|
||||
Legacy `--output-format json` overrides still use the JSON parser.
|
||||
|
||||
- Streamed reply text comes from assistant `message` events.
|
||||
- For legacy JSON output, reply text comes from the CLI JSON `response` field.
|
||||
|
||||
@@ -87,7 +87,7 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/Mattermost (plugi
|
||||
|
||||
## Shared overview pages
|
||||
|
||||
- [Additional provider variants](/providers/models#additional-provider-variants) - Anthropic Vertex, Copilot Proxy, and Gemini CLI OAuth
|
||||
- [Additional provider variants](/providers/models#additional-provider-variants) - Anthropic Vertex, Copilot Proxy, and the optional Gemini CLI runtime
|
||||
- [Image Generation](/tools/image-generation) - Shared `image_generate` tool, provider selection, and failover
|
||||
- [Music Generation](/tools/music-generation) - Shared `music_generate` tool, provider selection, and failover
|
||||
- [Video Generation](/tools/video-generation) - Shared `video_generate` tool, provider selection, and failover
|
||||
|
||||
@@ -57,7 +57,7 @@ For the full provider catalog and advanced configuration, see
|
||||
|
||||
- `anthropic-vertex` - install `@openclaw/anthropic-vertex-provider` for implicit Anthropic on Google Vertex support when Vertex credentials are available; no separate onboarding auth choice
|
||||
- `copilot-proxy` - local VS Code Copilot Proxy bridge; use `openclaw onboard --auth-choice copilot-proxy`
|
||||
- `google-gemini-cli` - unofficial Gemini CLI OAuth flow; requires a local `gemini` install (`brew install gemini-cli` or `npm install -g @google/gemini-cli`); default model `google-gemini-cli/gemini-3-flash-preview`; use `openclaw onboard --auth-choice google-gemini-cli` or `openclaw models auth login --provider google-gemini-cli --set-default`
|
||||
- `google-gemini-cli` - optional explicit runtime for canonical `google/*` models; requires a local `gemini` install and a supported Google AI Studio API-key profile; new Gemini CLI or Antigravity OAuth setup is not offered
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ title: "Synthetic"
|
||||
---
|
||||
|
||||
[Synthetic](https://synthetic.new) exposes Anthropic-compatible endpoints.
|
||||
OpenClaw bundles it as the `synthetic` provider and uses the Anthropic
|
||||
Messages API.
|
||||
OpenClaw provides it through the official `@openclaw/synthetic-provider`
|
||||
plugin and uses the Anthropic Messages API.
|
||||
|
||||
| Property | Value |
|
||||
| -------- | ------------------------------------- |
|
||||
@@ -20,6 +20,12 @@ Messages API.
|
||||
## Getting started
|
||||
|
||||
<Steps>
|
||||
<Step title="Install the plugin">
|
||||
```bash
|
||||
openclaw plugins install @openclaw/synthetic-provider
|
||||
openclaw gateway restart
|
||||
```
|
||||
</Step>
|
||||
<Step title="Get an API key">
|
||||
Get a `SYNTHETIC_API_KEY` from your Synthetic account, or let onboarding
|
||||
prompt you for one.
|
||||
|
||||
@@ -7,7 +7,7 @@ read_when:
|
||||
- You want to use Volcengine Speech text-to-speech
|
||||
---
|
||||
|
||||
The Volcengine provider gives access to Doubao models and third-party models hosted on Volcano Engine, with separate endpoints for general and coding workloads. The same bundled plugin also registers Volcengine Speech as a TTS provider.
|
||||
The Volcengine provider gives access to Doubao models and third-party models hosted on Volcano Engine, with separate endpoints for general and coding workloads. The same official plugin also registers Volcengine Speech as a TTS provider.
|
||||
|
||||
| Detail | Value |
|
||||
| ---------- | ---------------------------------------------------------- |
|
||||
@@ -19,6 +19,12 @@ The Volcengine provider gives access to Doubao models and third-party models hos
|
||||
## Getting started
|
||||
|
||||
<Steps>
|
||||
<Step title="Install the plugin">
|
||||
```bash
|
||||
openclaw plugins install @openclaw/volcengine-provider
|
||||
openclaw gateway restart
|
||||
```
|
||||
</Step>
|
||||
<Step title="Set the API key">
|
||||
Run interactive onboarding:
|
||||
|
||||
|
||||
+15
-8
@@ -6,7 +6,7 @@ read_when:
|
||||
title: "Vydra"
|
||||
---
|
||||
|
||||
The bundled Vydra plugin adds:
|
||||
The official Vydra plugin adds:
|
||||
|
||||
- Image generation via `vydra/grok-imagine`
|
||||
- Video generation via `vydra/veo3` (text-to-video) and `vydra/kling` (image-to-video)
|
||||
@@ -17,7 +17,7 @@ OpenClaw uses the same `VYDRA_API_KEY` for all three capabilities.
|
||||
| Property | Value |
|
||||
| --------------- | ------------------------------------------------------------------------- |
|
||||
| Provider id | `vydra` |
|
||||
| Plugin | bundled, `enabledByDefault: true` |
|
||||
| Plugin | `@openclaw/vydra-provider` |
|
||||
| Auth env var | `VYDRA_API_KEY` |
|
||||
| Onboarding flag | `--auth-choice vydra-api-key` |
|
||||
| Direct CLI flag | `--vydra-api-key <key>` |
|
||||
@@ -31,6 +31,13 @@ Use `https://www.vydra.ai/api/v1` as the base URL. Vydra's apex host (`https://v
|
||||
## Setup
|
||||
|
||||
<Steps>
|
||||
<Step title="Install the plugin">
|
||||
```bash
|
||||
openclaw plugins install @openclaw/vydra-provider
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
</Step>
|
||||
<Step title="Run interactive onboarding">
|
||||
```bash
|
||||
openclaw onboard --auth-choice vydra-api-key
|
||||
@@ -52,7 +59,7 @@ Use `https://www.vydra.ai/api/v1` as the base URL. Vydra's apex host (`https://v
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Image generation">
|
||||
Default and only bundled image model:
|
||||
Default and only Vydra image model:
|
||||
|
||||
- `vydra/grok-imagine`
|
||||
|
||||
@@ -70,7 +77,7 @@ Use `https://www.vydra.ai/api/v1` as the base URL. Vydra's apex host (`https://v
|
||||
}
|
||||
```
|
||||
|
||||
Bundled support is text-to-image only, at most one image per request. Vydra's hosted edit routes expect remote image URLs, and the bundled plugin does not add a Vydra-specific upload bridge.
|
||||
Vydra support is text-to-image only, at most one image per request. Vydra's hosted edit routes expect remote image URLs, and the plugin does not add a Vydra-specific upload bridge.
|
||||
|
||||
<Note>
|
||||
See [Image Generation](/tools/image-generation) for shared tool parameters, provider selection, and failover behavior.
|
||||
@@ -101,8 +108,8 @@ Use `https://www.vydra.ai/api/v1` as the base URL. Vydra's apex host (`https://v
|
||||
Notes:
|
||||
|
||||
- `vydra/kling` rejects local file uploads up front; only a remote image URL reference works.
|
||||
- Vydra's `kling` HTTP route has been inconsistent about whether it requires `image_url` or `video_url`; the bundled provider sends the same remote image URL in both fields.
|
||||
- The bundled plugin stays conservative and does not forward undocumented style knobs such as aspect ratio, resolution, watermark, or generated audio.
|
||||
- Vydra's `kling` HTTP route has been inconsistent about whether it requires `image_url` or `video_url`; the plugin sends the same remote image URL in both fields.
|
||||
- The plugin stays conservative and does not forward undocumented style knobs such as aspect ratio, resolution, watermark, or generated audio.
|
||||
|
||||
<Note>
|
||||
See [Video Generation](/tools/video-generation) for shared tool parameters, provider selection, and failover behavior.
|
||||
@@ -119,7 +126,7 @@ Use `https://www.vydra.ai/api/v1` as the base URL. Vydra's apex host (`https://v
|
||||
pnpm test:live -- extensions/vydra/vydra.live.test.ts
|
||||
```
|
||||
|
||||
The bundled Vydra live file covers:
|
||||
The Vydra live file covers:
|
||||
|
||||
- `vydra/veo3` text-to-video
|
||||
- `vydra/kling` image-to-video using a remote image URL
|
||||
@@ -154,7 +161,7 @@ Use `https://www.vydra.ai/api/v1` as the base URL. Vydra's apex host (`https://v
|
||||
- Model: `elevenlabs/tts`
|
||||
- Voice id: `21m00Tcm4TlvDq8ikWAM` ("Rachel")
|
||||
|
||||
The bundled plugin exposes this one known-good default voice and returns MP3 audio files.
|
||||
The plugin exposes this one known-good default voice and returns MP3 audio files.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
@@ -6,9 +6,8 @@ read_when:
|
||||
title: "Xiaomi MiMo"
|
||||
---
|
||||
|
||||
Xiaomi MiMo is the API platform for **MiMo** models. The bundled `xiaomi`
|
||||
plugin (`enabledByDefault: true`, no install step) registers two text
|
||||
providers plus a speech (TTS) provider:
|
||||
Xiaomi MiMo is the API platform for **MiMo** models. The official external
|
||||
`xiaomi` plugin registers two text providers plus a speech (TTS) provider:
|
||||
|
||||
- `xiaomi` - pay-as-you-go keys (`sk-...`)
|
||||
- `xiaomi-token-plan` - Token Plan keys (`tp-...`) with regional endpoint presets
|
||||
@@ -28,6 +27,13 @@ providers plus a speech (TTS) provider:
|
||||
## Getting started
|
||||
|
||||
<Steps>
|
||||
<Step title="Install the plugin">
|
||||
```bash
|
||||
openclaw plugins install @openclaw/xiaomi-provider
|
||||
openclaw gateway restart
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Get the right key">
|
||||
Create a pay-as-you-go key in the [Xiaomi MiMo console](https://platform.xiaomimimo.com/#/console/api-keys), or open your Token Plan subscription page and copy the regional OpenAI-compatible base URL plus the matching `tp-...` key.
|
||||
</Step>
|
||||
@@ -88,7 +94,7 @@ Choose the Token Plan auth choice that matches the regional base URL shown in Xi
|
||||
| `xiaomi-token-plan/mimo-v2.5` | text, image | 1,048,576 | 131,072 | Yes | Multimodal |
|
||||
|
||||
`xiaomi-token-plan` needs a regional base URL to resolve. The supported path
|
||||
is a bundled Token Plan onboarding choice or an explicit
|
||||
is a Token Plan onboarding choice or an explicit
|
||||
`models.providers.xiaomi-token-plan` config block with `baseUrl` set; the
|
||||
provider is not offered without one of those.
|
||||
|
||||
@@ -100,7 +106,7 @@ OpenClaw's [`/think` directive](/tools/thinking) with levels `off`,
|
||||
|
||||
## Text-to-speech
|
||||
|
||||
The bundled `xiaomi` plugin also registers Xiaomi MiMo as a speech provider
|
||||
The `xiaomi` plugin also registers Xiaomi MiMo as a speech provider
|
||||
for `tts`. It calls Xiaomi's chat-completions TTS contract with the
|
||||
text as an `assistant` message and optional style guidance as a `user`
|
||||
message.
|
||||
@@ -197,7 +203,8 @@ mono Opus with `ffmpeg` before delivery.
|
||||
}
|
||||
```
|
||||
|
||||
Pricing and compat flags come from the bundled plugin manifest, so the config example omits `cost` and `compat` to avoid diverging from runtime behavior.
|
||||
Pricing and compat flags come from the plugin manifest, so the config example
|
||||
omits `cost` and `compat` to avoid diverging from runtime behavior.
|
||||
|
||||
Token Plan:
|
||||
|
||||
@@ -236,11 +243,13 @@ Token Plan:
|
||||
}
|
||||
```
|
||||
|
||||
Token Plan charges against a fixed subscription's Credits rather than per-token USD pricing, so its bundled catalog rows use zero USD cost and the config example omits `cost`.
|
||||
Token Plan charges against a fixed subscription's Credits rather than per-token
|
||||
USD pricing, so its catalog rows use zero USD cost and the config example omits
|
||||
`cost`.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Auto-injection behavior">
|
||||
The `xiaomi` provider is auto-enabled when `XIAOMI_API_KEY` is set in your environment or an auth profile exists. `xiaomi-token-plan` needs a regional base URL, so the supported path is the bundled Token Plan onboarding choice or an explicit `models.providers.xiaomi-token-plan` config block.
|
||||
The `xiaomi` provider is auto-enabled when `XIAOMI_API_KEY` is set in your environment or an auth profile exists. `xiaomi-token-plan` needs a regional base URL, so the supported path is the Token Plan onboarding choice or an explicit `models.providers.xiaomi-token-plan` config block.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Model details">
|
||||
|
||||
@@ -93,8 +93,10 @@ offering a verified manual API-key step when nothing is found. Sensitive
|
||||
credentials use masked input. Once inference passes, OpenClaw starts and
|
||||
helps configure the rest.
|
||||
|
||||
Gemini CLI remains available for normal agents after setup, but it is not
|
||||
offered for this inference gate because it cannot enforce the tool-free probe.
|
||||
Gemini CLI remains available as an explicitly configured runtime after setup,
|
||||
but Gemini CLI and Antigravity are not offered as detected inference routes.
|
||||
Use Google AI Studio API-key or Vertex AI for guided setup. The optional Gemini
|
||||
CLI runtime specifically requires an AI Studio API-key profile.
|
||||
|
||||
Full reference: [Onboarding (macOS App)](/start/onboarding)
|
||||
|
||||
|
||||
@@ -91,15 +91,16 @@ To use a Claude subscription when the Gateway host has no Claude CLI login, run
|
||||
printed token as **Anthropic setup-token** under **Connect with an API key or
|
||||
token**.
|
||||
|
||||
Installed Gemini CLI, Antigravity, Pi, and OpenCode CLIs are shown for context
|
||||
when they cannot be selected as the reusable guided-setup inference route.
|
||||
Gemini and Antigravity cannot enforce the tool-free inference probe. Pi and
|
||||
OpenCode are whole-agent harnesses rather than setup inference routes; their
|
||||
session integrations require separate runtime and plugin setup.
|
||||
Pi and OpenCode installs may be shown for context when they cannot be selected
|
||||
as the reusable guided-setup inference route. They are whole-agent harnesses,
|
||||
not setup inference routes; their session integrations require separate runtime
|
||||
and plugin setup. Gemini CLI and Antigravity are not offered as detected setup
|
||||
routes.
|
||||
|
||||
You can also sign in through the provider's own OAuth or device-pairing flow.
|
||||
The built-in choices include OpenAI/ChatGPT, OpenRouter, GitHub Copilot, Google
|
||||
Gemini CLI, xAI, MiniMax Global and CN, and Chutes. The list comes from the
|
||||
The built-in choices include OpenAI/ChatGPT, OpenRouter, GitHub Copilot, xAI,
|
||||
MiniMax Global and CN, and Chutes. Google is available through the supported AI
|
||||
Studio API-key route. The list comes from the
|
||||
Gateway's active text-inference provider plugins rather than a fixed app list,
|
||||
so another provider can opt in without adding provider-specific macOS code.
|
||||
|
||||
|
||||
@@ -92,10 +92,9 @@ Plain `openclaw onboard` follows this path:
|
||||
2. Detect configured models, API-key environment variables, supported local AI
|
||||
CLIs, and already installed tool-capable models from reachable Ollama or LM
|
||||
Studio servers on the Gateway host. This read-only pass never downloads a
|
||||
model. Gemini CLI, Antigravity, Pi, and OpenCode installs are also reported
|
||||
when they cannot serve as the reusable inference route for guided setup.
|
||||
Gemini and Antigravity cannot enforce the tool-free probe; Pi and OpenCode
|
||||
are whole-agent harnesses rather than setup inference routes.
|
||||
model. Pi and OpenCode installs may also be reported for context when they
|
||||
cannot serve as the reusable inference route. Gemini CLI and Antigravity are
|
||||
not offered as detected setup routes.
|
||||
3. Test the first detected candidate with a real completion. On failure, show the
|
||||
reason and continue to the next usable candidate.
|
||||
4. If detection is exhausted, choose OpenAI, Anthropic, xAI (Grok), Google, or
|
||||
|
||||
@@ -18,6 +18,12 @@ OpenClaw supports DuckDuckGo as a **key-free** `web_search` provider. No API key
|
||||
DuckDuckGo is never auto-selected, since auto-detection only considers providers with usable credentials. Set it explicitly:
|
||||
|
||||
<Steps>
|
||||
<Step title="Install the plugin">
|
||||
```bash
|
||||
openclaw plugins install @openclaw/duckduckgo-plugin
|
||||
openclaw gateway restart
|
||||
```
|
||||
</Step>
|
||||
<Step title="Configure">
|
||||
```bash
|
||||
openclaw configure --section web
|
||||
|
||||
@@ -271,9 +271,8 @@ Automatic fallback across authenticated providers is always enabled. A per-call
|
||||
<AccordionGroup>
|
||||
<Accordion title="ComfyUI">
|
||||
Workflow-driven and depends on the configured graph plus node mapping
|
||||
for prompt/output fields. The bundled `comfy` plugin plugs into the
|
||||
shared `music_generate` tool through the music-generation provider
|
||||
registry.
|
||||
for prompt/output fields. The `comfy` plugin plugs into the shared
|
||||
`music_generate` tool through the music-generation provider registry.
|
||||
</Accordion>
|
||||
<Accordion title="fal">
|
||||
Uses fal model endpoints through the shared provider auth path. The
|
||||
|
||||
@@ -105,7 +105,7 @@ openclaw tasks cancel <lookup>
|
||||
| Provider | Default model | Text | Image ref | Video ref | Auth |
|
||||
| --------------------- | ------------------------------- | :--: | ---------------------------------------------------- | ----------------------------------------------- | ---------------------------------------- |
|
||||
| Alibaba | `wan2.6-t2v` | ✓ | Yes (remote URL) | Yes (remote URL) | `MODELSTUDIO_API_KEY` |
|
||||
| BytePlus (bundled) | `seedance-1-0-pro-250528` | ✓ | Up to 2 images (first + last frame) | - | `BYTEPLUS_API_KEY` |
|
||||
| BytePlus plugin | `seedance-1-0-pro-250528` | ✓ | Up to 2 images (first + last frame) | - | `BYTEPLUS_API_KEY` |
|
||||
| BytePlus 1.5 plugin | `seedance-1-5-pro-251215` | ✓ | Up to 2 images (first + last frame via role) | - | `BYTEPLUS_API_KEY` |
|
||||
| BytePlus Seedance 2.0 | `dreamina-seedance-2-0-260128` | ✓ | Up to 9 reference images | Up to 3 videos | `BYTEPLUS_API_KEY` |
|
||||
| ComfyUI | `workflow` | ✓ | 1 image | - | `COMFY_API_KEY` or `COMFY_CLOUD_API_KEY` |
|
||||
@@ -146,7 +146,7 @@ the shared live sweep:
|
||||
| Qwen | ✓ | ✓ | ✓ | `generate`, `imageToVideo`; `videoToVideo` skipped because this provider needs remote `http(s)` video URLs |
|
||||
| Runway | ✓ | ✓ | ✓ | `generate`, `imageToVideo`; `videoToVideo` runs only when the selected model is `runway/gen4_aleph` |
|
||||
| Together | ✓ | ✓ | - | `generate`, `imageToVideo` |
|
||||
| Vydra | ✓ | ✓ | - | `generate`; shared `imageToVideo` skipped because bundled `veo3` is text-only and bundled `kling` requires a remote image URL |
|
||||
| Vydra | ✓ | ✓ | - | `generate`; shared `imageToVideo` skipped because `veo3` is text-only and `kling` requires a remote image URL |
|
||||
| xAI | ✓ | ✓ | ✓ | Classic supports all modes; Video 1.5 is image-to-video only; remote MP4 input keeps `videoToVideo` out of the shared sweep |
|
||||
|
||||
## Tool parameters
|
||||
@@ -322,7 +322,8 @@ Automatic fallback across authenticated providers is always enabled. A per-call
|
||||
Uses DashScope / Model Studio async endpoint. Reference images and
|
||||
videos must be remote `http(s)` URLs.
|
||||
</Accordion>
|
||||
<Accordion title="BytePlus (bundled)">
|
||||
<Accordion title="BytePlus plugin">
|
||||
Requires the official `@openclaw/byteplus-provider` plugin.
|
||||
Provider id: `byteplus`.
|
||||
|
||||
Models: `seedance-1-0-pro-250528` (default),
|
||||
@@ -416,7 +417,7 @@ Automatic fallback across authenticated providers is always enabled. A per-call
|
||||
</Accordion>
|
||||
<Accordion title="Vydra">
|
||||
Uses `https://www.vydra.ai/api/v1` directly to avoid auth-dropping
|
||||
redirects. `veo3` is bundled as text-to-video only; `kling` requires
|
||||
redirects. `veo3` is text-to-video only; `kling` requires
|
||||
a remote image URL.
|
||||
</Accordion>
|
||||
<Accordion title="xAI">
|
||||
|
||||
@@ -123,6 +123,74 @@ describe("Bedrock inbound image base64", () => {
|
||||
});
|
||||
|
||||
describe("Bedrock tool-result replay", () => {
|
||||
it("replays unsupported audio attachments as their canonical text placeholder", () => {
|
||||
const messages = testing.convertMessages(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "call_audio",
|
||||
toolName: "listen",
|
||||
content: [{ type: "audio", mimeType: "audio/wav", data: "YXVkaW8=" }],
|
||||
isError: false,
|
||||
},
|
||||
],
|
||||
} as never,
|
||||
bedrockModel({ input: ["text", "image"] }),
|
||||
"none",
|
||||
);
|
||||
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages[0]).toMatchObject({
|
||||
role: ConversationRole.USER,
|
||||
content: [
|
||||
{
|
||||
toolResult: {
|
||||
toolUseId: "call_audio",
|
||||
content: [{ text: "(see attached audio)" }],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves valid text and image attachments alongside unsupported audio", () => {
|
||||
const messages = testing.convertMessages(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "call_media",
|
||||
toolName: "inspect",
|
||||
content: [
|
||||
{ type: "audio", mimeType: "audio/wav", data: "YXVkaW8=" },
|
||||
{ type: "text", text: "actual tool output" },
|
||||
{ type: "image", mimeType: "image/png", data: "aW1hZ2U=" },
|
||||
],
|
||||
isError: false,
|
||||
},
|
||||
],
|
||||
} as never,
|
||||
bedrockModel({ input: ["text", "image"] }),
|
||||
"none",
|
||||
);
|
||||
|
||||
expect(messages[0]).toMatchObject({
|
||||
role: ConversationRole.USER,
|
||||
content: [
|
||||
{
|
||||
toolResult: {
|
||||
toolUseId: "call_media",
|
||||
content: [
|
||||
{ text: "actual tool output" },
|
||||
{ image: { format: "png", source: { bytes: expect.any(Uint8Array) } } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("drops payload-less image husks from consecutive tool results", () => {
|
||||
const messages = testing.convertMessages(
|
||||
{
|
||||
@@ -335,6 +403,61 @@ describe("Bedrock profile endpoint resolution", () => {
|
||||
});
|
||||
|
||||
describe("Bedrock stop reasons", () => {
|
||||
it.each([
|
||||
{
|
||||
name: "text",
|
||||
events: [
|
||||
{ contentBlockDelta: { contentBlockIndex: 0, delta: { text: "truncated response" } } },
|
||||
{ contentBlockStop: { contentBlockIndex: 0 } },
|
||||
],
|
||||
contentType: "text",
|
||||
},
|
||||
{
|
||||
name: "tool call",
|
||||
events: [
|
||||
{
|
||||
contentBlockStart: {
|
||||
contentBlockIndex: 0,
|
||||
start: { toolUse: { toolUseId: "call_lookup", name: "lookup" } },
|
||||
},
|
||||
},
|
||||
{
|
||||
contentBlockDelta: {
|
||||
contentBlockIndex: 0,
|
||||
delta: { toolUse: { input: '{"query":"partial"}' } },
|
||||
},
|
||||
},
|
||||
{ contentBlockStop: { contentBlockIndex: 0 } },
|
||||
],
|
||||
contentType: "toolCall",
|
||||
},
|
||||
])(
|
||||
"reports truncated $name streams without a terminal messageStop",
|
||||
async ({ events, contentType }) => {
|
||||
vi.spyOn(BedrockRuntimeClient.prototype, "send").mockResolvedValue({
|
||||
$metadata: { httpStatusCode: 200 },
|
||||
stream: streamEvents([{ messageStart: { role: ConversationRole.ASSISTANT } }, ...events]),
|
||||
} as never);
|
||||
|
||||
const stream = streamBedrockForTest(bedrockModel({}), {
|
||||
messages: [{ role: "user", content: "Hello", timestamp: 0 }],
|
||||
} as never);
|
||||
const eventTypes: string[] = [];
|
||||
for await (const event of stream) {
|
||||
eventTypes.push(event.type);
|
||||
}
|
||||
const result = await stream.result();
|
||||
|
||||
expect(eventTypes.at(-1)).toBe("error");
|
||||
expect(eventTypes).not.toContain("done");
|
||||
expect(result.stopReason).toBe("error");
|
||||
expect(result.errorMessage).toBe("Bedrock stream ended before messageStop");
|
||||
expect(result.content).toEqual([expect.objectContaining({ type: contentType })]);
|
||||
expect(result.content[0]).not.toHaveProperty("index");
|
||||
expect(result.content[0]).not.toHaveProperty("partialJson");
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
BedrockStopReason.CONTENT_FILTERED,
|
||||
BedrockStopReason.GUARDRAIL_INTERVENED,
|
||||
|
||||
@@ -332,7 +332,7 @@ const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOptions> =
|
||||
}
|
||||
}
|
||||
|
||||
if (refusalBuffer && !sawMessageStop) {
|
||||
if (!sawMessageStop) {
|
||||
throw new Error("Bedrock stream ended before messageStop");
|
||||
}
|
||||
if (options.signal?.aborted) {
|
||||
@@ -812,7 +812,7 @@ function createBedrockToolResult(message: ToolResultMessage): ContentBlock.ToolR
|
||||
content.push({ text: sanitizeSurrogates(block.text) });
|
||||
continue;
|
||||
}
|
||||
if (describeToolResultMediaPlaceholder([block])) {
|
||||
if (block.type === "image" && describeToolResultMediaPlaceholder([block])) {
|
||||
content.push({ image: createImageBlock(block.mimeType, block.data) });
|
||||
}
|
||||
}
|
||||
@@ -820,7 +820,10 @@ function createBedrockToolResult(message: ToolResultMessage): ContentBlock.ToolR
|
||||
return {
|
||||
toolResult: {
|
||||
toolUseId: message.toolCallId,
|
||||
content: content.length > 0 ? content : [{ text: "(no output)" }],
|
||||
content:
|
||||
content.length > 0
|
||||
? content
|
||||
: [{ text: describeToolResultMediaPlaceholder(message.content) ?? "(no output)" }],
|
||||
status: message.isError ? ToolResultStatus.ERROR : ToolResultStatus.SUCCESS,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -102,6 +102,19 @@ describe("anthropic-vertex provider plugin", () => {
|
||||
expect(result.provider.models[4]?.thinkingLevelMap).toEqual({ xhigh: null, max: "max" });
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ region: "global", baseUrl: "https://aiplatform.googleapis.com" },
|
||||
{ region: "us", baseUrl: "https://aiplatform.us.rep.googleapis.com" },
|
||||
{ region: "eu", baseUrl: "https://aiplatform.eu.rep.googleapis.com" },
|
||||
{ region: "us-east5", baseUrl: "https://us-east5-aiplatform.googleapis.com" },
|
||||
])("publishes the SDK endpoint for the $region location", ({ region, baseUrl }) => {
|
||||
expect(
|
||||
buildAnthropicVertexProvider({
|
||||
env: { GOOGLE_CLOUD_LOCATION: region },
|
||||
}).baseUrl,
|
||||
).toBe(baseUrl);
|
||||
});
|
||||
|
||||
it.each(["global", "us", "eu"])("publishes Opus 5 for the %s endpoint", (region) => {
|
||||
const provider = buildAnthropicVertexProvider({
|
||||
env: { GOOGLE_CLOUD_LOCATION: region },
|
||||
@@ -194,7 +207,7 @@ describe("anthropic-vertex provider plugin", () => {
|
||||
name: "Claude Sonnet 5",
|
||||
api: "anthropic-messages",
|
||||
provider: "anthropic-vertex",
|
||||
baseUrl: "https://us-aiplatform.googleapis.com",
|
||||
baseUrl: "https://aiplatform.us.rep.googleapis.com",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 1_000_000,
|
||||
@@ -235,7 +248,7 @@ describe("anthropic-vertex provider plugin", () => {
|
||||
name: "Claude Opus 5",
|
||||
api: "anthropic-messages",
|
||||
provider: "anthropic-vertex",
|
||||
baseUrl: "https://us-aiplatform.googleapis.com",
|
||||
baseUrl: "https://aiplatform.us.rep.googleapis.com",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
||||
|
||||
@@ -238,7 +238,9 @@ export function buildAnthropicVertexProvider(params?: {
|
||||
const baseUrl =
|
||||
normalizeLowercaseStringOrEmpty(region) === "global"
|
||||
? "https://aiplatform.googleapis.com"
|
||||
: `https://${region}-aiplatform.googleapis.com`;
|
||||
: region === "us" || region === "eu"
|
||||
? `https://aiplatform.${region}.rep.googleapis.com`
|
||||
: `https://${region}-aiplatform.googleapis.com`;
|
||||
|
||||
return {
|
||||
baseUrl,
|
||||
|
||||
@@ -25,6 +25,12 @@ describe("anthropic vertex region helpers", () => {
|
||||
).toBe("europe-west4");
|
||||
});
|
||||
|
||||
it.each(["us", "eu"])("parses the %s multi-region Vertex endpoint", (region) => {
|
||||
expect(
|
||||
resolveAnthropicVertexRegionFromBaseUrl(`https://aiplatform.${region}.rep.googleapis.com`),
|
||||
).toBe(region);
|
||||
});
|
||||
|
||||
it("treats the global Vertex endpoint as global", () => {
|
||||
expect(resolveAnthropicVertexRegionFromBaseUrl("https://aiplatform.googleapis.com")).toBe(
|
||||
"global",
|
||||
|
||||
@@ -589,6 +589,24 @@ describe("createAnthropicVertexStreamFn", () => {
|
||||
});
|
||||
|
||||
describe("createAnthropicVertexStreamFnForModel", () => {
|
||||
it.each(["us", "eu"])("preserves the %s multi-region SDK endpoint", (region) => {
|
||||
const { deps, anthropicVertexCtorMock, googleAuthClient } = createStreamDeps();
|
||||
const streamFn = createAnthropicVertexStreamFnForModel(
|
||||
{ baseUrl: `https://aiplatform.${region}.rep.googleapis.com` },
|
||||
{ GOOGLE_CLOUD_PROJECT_ID: "vertex-project" } as NodeJS.ProcessEnv,
|
||||
deps,
|
||||
);
|
||||
|
||||
void streamFn(makeModel({ id: "claude-sonnet-5", maxTokens: 128_000 }), { messages: [] }, {});
|
||||
|
||||
expect(anthropicVertexCtorMock).toHaveBeenCalledWith({
|
||||
googleAuth: googleAuthClient,
|
||||
projectId: "vertex-project",
|
||||
region,
|
||||
baseURL: `https://aiplatform.${region}.rep.googleapis.com/v1`,
|
||||
});
|
||||
});
|
||||
|
||||
it("derives project and region from the model and env", () => {
|
||||
const { deps, anthropicVertexCtorMock, googleAuthClient } = createStreamDeps();
|
||||
const streamFn = createAnthropicVertexStreamFnForModel(
|
||||
|
||||
@@ -158,7 +158,7 @@ export class CopilotGatewayClient {
|
||||
const protocol = new GatewayProtocolClient({
|
||||
createSocket: (handlers) => createBrowserSocket(gatewayScope, handlers, this.WebSocketImpl),
|
||||
createRequestId: () => crypto.randomUUID(),
|
||||
buildConnectPlan: ({ nonce }) =>
|
||||
buildConnectPlan: ({ nonce, challengeTs }) =>
|
||||
lifecycle.buildPlan({
|
||||
client: {
|
||||
id: CLIENT_ID,
|
||||
@@ -170,6 +170,7 @@ export class CopilotGatewayClient {
|
||||
role: ROLE,
|
||||
defaultScopes: SCOPES,
|
||||
nonce,
|
||||
challengeTs,
|
||||
}),
|
||||
buildConnectParams: (plan) => ({
|
||||
minProtocol: MIN_CLIENT_PROTOCOL_VERSION,
|
||||
|
||||
@@ -273,7 +273,7 @@ describe("browser copilot Gateway custody", () => {
|
||||
first?.message({
|
||||
type: "event",
|
||||
event: "connect.challenge",
|
||||
payload: { nonce: "first-nonce" },
|
||||
payload: { nonce: "first-nonce", ts: 1_777_777_777_000 },
|
||||
});
|
||||
await vi.waitFor(() => expect(first?.sent).toHaveLength(1));
|
||||
const firstConnect = first?.sent[0] as {
|
||||
@@ -303,7 +303,7 @@ describe("browser copilot Gateway custody", () => {
|
||||
second?.message({
|
||||
type: "event",
|
||||
event: "connect.challenge",
|
||||
payload: { nonce: "second-nonce" },
|
||||
payload: { nonce: "second-nonce", ts: 1_777_777_778_000 },
|
||||
});
|
||||
await vi.waitFor(() => expect(second?.sent).toHaveLength(1));
|
||||
const secondConnect = second?.sent[0] as { params?: { auth?: { token?: string } } };
|
||||
@@ -351,6 +351,37 @@ describe("browser copilot Gateway custody", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects a device challenge with a malformed Gateway timestamp", async () => {
|
||||
FakeWebSocket.instances = [];
|
||||
vi.stubGlobal("chrome", { runtime: { getManifest: () => ({ version: "test" }) } });
|
||||
vi.stubGlobal("navigator", { language: "en", userAgent: "copilot-test" });
|
||||
const client = new CopilotGatewayClient({
|
||||
storage: storageArea(),
|
||||
WebSocketImpl: FakeWebSocket as never,
|
||||
});
|
||||
|
||||
try {
|
||||
client.start("ws://127.0.0.1:28789/");
|
||||
await vi.waitFor(() => expect(FakeWebSocket.instances).toHaveLength(1));
|
||||
const socket = FakeWebSocket.instances[0];
|
||||
socket?.message({
|
||||
type: "event",
|
||||
event: "connect.challenge",
|
||||
payload: { nonce: "invalid-time", ts: "not-a-number" },
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
expect(socket?.closeCalls).toContainEqual({
|
||||
code: 4008,
|
||||
reason: "connect failed",
|
||||
}),
|
||||
);
|
||||
expect(socket?.sent).toHaveLength(0);
|
||||
} finally {
|
||||
client.stop();
|
||||
vi.unstubAllGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
it("closes and reconnects when the browser socket never opens", async () => {
|
||||
vi.useFakeTimers();
|
||||
FakeWebSocket.instances = [];
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -205,7 +205,7 @@ async function createGatewayHarness(): Promise<GatewayHarness> {
|
||||
JSON.stringify({
|
||||
type: "event",
|
||||
event: "connect.challenge",
|
||||
payload: { nonce: "browser-copilot-e2e-nonce" },
|
||||
payload: { nonce: "browser-copilot-e2e-nonce", ts: 1_777_777_777_000 },
|
||||
}),
|
||||
);
|
||||
socket.on("message", (data) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Browser tests cover browser cli manage plugin behavior.
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
createBrowserManageProgram,
|
||||
getBrowserManageCallBrowserRequestMock,
|
||||
@@ -15,10 +15,26 @@ function lastRuntimeLog(): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseSingleRuntimeJson(): unknown {
|
||||
const logs = getBrowserCliRuntimeCapture().runtimeLogs;
|
||||
expect(logs).toHaveLength(1);
|
||||
return JSON.parse(logs[0] ?? "");
|
||||
}
|
||||
|
||||
describe("browser manage output", () => {
|
||||
let previousExitCode: typeof process.exitCode;
|
||||
|
||||
beforeEach(() => {
|
||||
previousExitCode = process.exitCode;
|
||||
process.exitCode = undefined;
|
||||
getBrowserManageCallBrowserRequestMock().mockClear();
|
||||
getBrowserCliRuntimeCapture().resetRuntimeCapture();
|
||||
getBrowserCliRuntime().exit.mockClear();
|
||||
getBrowserCliRuntime().writeJson.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.exitCode = previousExitCode;
|
||||
});
|
||||
|
||||
it("shows chrome-mcp transport for existing-session status without fake CDP fields", async () => {
|
||||
@@ -524,6 +540,72 @@ describe("browser manage output", () => {
|
||||
expect(output).toContain("OK gateway: browser control endpoint reachable");
|
||||
expect(output).toContain("OK graphics: software");
|
||||
expect(output).toContain("OK tabs: 1 visible, use tab reference t1");
|
||||
expect(getBrowserCliRuntime().writeJson).not.toHaveBeenCalled();
|
||||
expect(getBrowserCliRuntime().exit).not.toHaveBeenCalled();
|
||||
expect(process.exitCode).toBeUndefined();
|
||||
});
|
||||
|
||||
it("prints one complete JSON browser doctor failure before setting exit status", async () => {
|
||||
getBrowserManageCallBrowserRequestMock().mockImplementation(async (_opts: unknown, req) => {
|
||||
if (req.path === "/") {
|
||||
return {
|
||||
enabled: false,
|
||||
profile: "openclaw",
|
||||
transport: "cdp",
|
||||
running: false,
|
||||
};
|
||||
}
|
||||
if (req.path === "/profiles") {
|
||||
return { profiles: [] };
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const program = createBrowserManageProgram();
|
||||
await program.parseAsync(["browser", "--json", "doctor"], { from: "user" });
|
||||
|
||||
expect(parseSingleRuntimeJson()).toEqual(
|
||||
expect.objectContaining({
|
||||
ok: false,
|
||||
checks: expect.arrayContaining([
|
||||
expect.objectContaining({ name: "gateway", ok: true }),
|
||||
expect.objectContaining({ name: "plugin", ok: false }),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
expect(getBrowserCliRuntimeCapture().runtimeErrors).toEqual([]);
|
||||
expect(getBrowserCliRuntime().writeJson).toHaveBeenCalledTimes(1);
|
||||
expect(getBrowserCliRuntime().exit).not.toHaveBeenCalled();
|
||||
expect(process.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it("prints one JSON browser doctor report and succeeds when every check passes", async () => {
|
||||
getBrowserManageCallBrowserRequestMock().mockImplementation(async (_opts: unknown, req) => {
|
||||
if (req.path === "/") {
|
||||
return {
|
||||
enabled: true,
|
||||
profile: "openclaw",
|
||||
transport: "cdp",
|
||||
running: true,
|
||||
};
|
||||
}
|
||||
if (req.path === "/profiles") {
|
||||
return { profiles: [{ name: "openclaw", running: true }] };
|
||||
}
|
||||
if (req.path === "/tabs") {
|
||||
return { running: true, tabs: [] };
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const program = createBrowserManageProgram();
|
||||
await program.parseAsync(["browser", "--json", "doctor"], { from: "user" });
|
||||
|
||||
expect(parseSingleRuntimeJson()).toMatchObject({ ok: true });
|
||||
expect(getBrowserCliRuntimeCapture().runtimeErrors).toEqual([]);
|
||||
expect(getBrowserCliRuntime().writeJson).toHaveBeenCalledTimes(1);
|
||||
expect(getBrowserCliRuntime().exit).not.toHaveBeenCalled();
|
||||
expect(process.exitCode).toBeUndefined();
|
||||
});
|
||||
|
||||
it("prints a readable browser doctor failure when gateway auth SecretRefs are unavailable", async () => {
|
||||
@@ -534,9 +616,7 @@ describe("browser manage output", () => {
|
||||
getBrowserManageCallBrowserRequestMock().mockRejectedValueOnce(error);
|
||||
|
||||
const program = createBrowserManageProgram();
|
||||
await expect(program.parseAsync(["browser", "doctor"], { from: "user" })).rejects.toThrow(
|
||||
"__exit__:1",
|
||||
);
|
||||
await program.parseAsync(["browser", "doctor"], { from: "user" });
|
||||
|
||||
const output = lastRuntimeLog();
|
||||
expect(output).toContain(
|
||||
@@ -544,5 +624,8 @@ describe("browser manage output", () => {
|
||||
);
|
||||
expect(output).toContain("OPENCLAW_GATEWAY_TOKEN");
|
||||
expect(output).not.toContain("GatewaySecretRefUnavailableError");
|
||||
expect(getBrowserCliRuntime().writeJson).not.toHaveBeenCalled();
|
||||
expect(getBrowserCliRuntime().exit).not.toHaveBeenCalled();
|
||||
expect(process.exitCode).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -413,12 +413,11 @@ export function registerBrowserManageCommands(
|
||||
const profile = parent?.browserProfile;
|
||||
await runBrowserCommand(async () => {
|
||||
const result = await runBrowserDoctor(parent, profile, opts.deep === true);
|
||||
if (printJsonResult(parent, result)) {
|
||||
return;
|
||||
if (!printJsonResult(parent, result)) {
|
||||
defaultRuntime.log(result.checks.map(formatDoctorLine).join("\n"));
|
||||
}
|
||||
defaultRuntime.log(result.checks.map(formatDoctorLine).join("\n"));
|
||||
if (!result.ok) {
|
||||
defaultRuntime.exit(1);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,6 +39,20 @@ Restart the Gateway if it was already running.
|
||||
openclaw channels status --probe
|
||||
```
|
||||
|
||||
Inspect the current bot, approved rooms, and room members:
|
||||
|
||||
```bash
|
||||
openclaw directory self --channel buzz
|
||||
openclaw directory peers list --channel buzz
|
||||
openclaw directory groups list --channel buzz
|
||||
openclaw directory groups members --channel buzz --group-id buzz:<ROOM_UUID>
|
||||
```
|
||||
|
||||
Buzz profile and room names are used as display labels, while public keys and
|
||||
room UUIDs remain the stable identities. Archived rooms are omitted; an
|
||||
archive or restore event rebuilds only the Buzz connection's room
|
||||
subscriptions and does not stop the Gateway.
|
||||
|
||||
Send a test message:
|
||||
|
||||
```bash
|
||||
@@ -53,7 +67,8 @@ openclaw message send \
|
||||
- Never give OpenClaw a human owner's private key.
|
||||
- The generated bot private key is stored in OpenClaw configuration; only its public key is displayed.
|
||||
- Treat Buzz messages as untrusted agent input.
|
||||
- Currently supported: text conversations in group rooms.
|
||||
- Currently supported: text conversations, threads, typing, and directory
|
||||
lookup in group rooms.
|
||||
- Not yet supported: DMs, media, reactions, or creating rooms from OpenClaw.
|
||||
|
||||
Full documentation: https://docs.openclaw.ai/channels/buzz
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user