mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 14:01:24 +08:00
Compare commits
2 Commits
codex/mark
...
codex/smal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1c0e492b5 | ||
|
|
aa170419e7 |
@@ -187,13 +187,11 @@ gh pr view <number> --json additions,deletions,changedFiles \
|
||||
## Read beyond the diff
|
||||
|
||||
- Review the surrounding code path, not just changed lines. Open the caller, callee, data contracts, adjacent tests, and owner module.
|
||||
- Before any verdict, read enough code to fill this map: changed surface, runtime entry point, owner boundary, one caller, one callee, sibling implementations sharing the invariant, adjacent tests, current `main` behavior, and shipped/dependency/Codex contracts when relevant.
|
||||
- For large-codebase PRs, sample enough related files to understand the runtime boundary before deciding. Default to more code reading when the change touches agents, gateway, plugins, auth, sessions, process, config, or provider/runtime seams.
|
||||
- Compare the PR against current `origin/main` behavior. Check whether recent main already changed the same surface.
|
||||
- Dependency-backed behavior: MUST read upstream docs/source/types before judging API use, defaults, output shapes, errors, timeouts, memory behavior, or compatibility. Do not assume dependency contracts from memory or PR text.
|
||||
- Judge solution quality, not only correctness. Ask whether the PR is the clean owner-boundary fix or a wart/workaround that should be replaced by a small refactor, moved seam, contract change, or deletion of duplicate logic.
|
||||
- Mention the main files read when the verdict depends on code-path evidence.
|
||||
- If the user challenges the verdict or asks whether the idea is really good, resume code reading first. Do not defend, soften, or reverse the verdict until the missing caller/callee/sibling/dependency path is checked.
|
||||
|
||||
## Best-fix review loop
|
||||
|
||||
@@ -208,7 +206,6 @@ Before verdict:
|
||||
5. Compare against current `origin/main` and shipped behavior when regression/compat matters.
|
||||
6. Inspect upstream dependency/Codex source or docs for dependency-backed behavior.
|
||||
7. Identify at least one alternative fix location or shape, then reject it with evidence.
|
||||
8. If any required path above is uninspected, keep reading or mark `Remaining uncertainty`; do not call the PR best, blocked, proof-sufficient, or merge-ready.
|
||||
|
||||
Review output must include:
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Secret scanning alert handler for OpenClaw maintainers.
|
||||
// Usage: node secret-scanning.mjs <command> [options]
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
@@ -39,9 +39,7 @@ function gh(args, { json = true, allowFailure = false } = {}) {
|
||||
stderr: proc.stderr,
|
||||
};
|
||||
}
|
||||
if (!json) {
|
||||
return proc.stdout;
|
||||
}
|
||||
if (!json) return proc.stdout;
|
||||
try {
|
||||
return JSON.parse(proc.stdout);
|
||||
} catch {
|
||||
@@ -72,9 +70,7 @@ export function loadBodyRedactionResult(locationType, resultFile) {
|
||||
if (!resultFile) {
|
||||
fail("Body notifications require a redaction result file from redact-body-if-needed");
|
||||
}
|
||||
if (!fs.existsSync(resultFile)) {
|
||||
fail(`File not found: ${resultFile}`);
|
||||
}
|
||||
if (!fs.existsSync(resultFile)) fail(`File not found: ${resultFile}`);
|
||||
|
||||
const result = JSON.parse(fs.readFileSync(resultFile, "utf8"));
|
||||
if (typeof result.notify_required !== "boolean") {
|
||||
@@ -186,11 +182,10 @@ function fetchDiscussionComment(discussionNumber, discussionCommentDbId) {
|
||||
failOnGraphQLFailure(gql, `Failed to fetch discussion #${discussionNumber}`);
|
||||
|
||||
const discussion = gql?.data?.repository?.discussion;
|
||||
if (!discussion) {
|
||||
if (!discussion)
|
||||
fail(
|
||||
`Discussion #${discussionNumber} not found — it may have been deleted. The alert cannot be processed via this skill.`,
|
||||
);
|
||||
}
|
||||
|
||||
discussionId = discussion.id;
|
||||
|
||||
@@ -210,18 +205,15 @@ function fetchDiscussionComment(discussionNumber, discussionCommentDbId) {
|
||||
`Failed to fetch replies for discussion comment ${topLevelComment.id}`,
|
||||
);
|
||||
const replies = replyPage?.data?.node?.replies;
|
||||
if (!replies) {
|
||||
if (!replies)
|
||||
fail(`Failed to paginate replies for discussion comment ${topLevelComment.id}`);
|
||||
}
|
||||
|
||||
reply = findDiscussionCommentNode(replies.nodes, discussionCommentDbId);
|
||||
hasMoreReplies = replies.pageInfo.hasNextPage;
|
||||
replyCursor = replies.pageInfo.endCursor;
|
||||
}
|
||||
|
||||
if (reply) {
|
||||
return { discussionId, comment: reply };
|
||||
}
|
||||
if (reply) return { discussionId, comment: reply };
|
||||
}
|
||||
|
||||
hasNextPage = discussion.comments.pageInfo.hasNextPage;
|
||||
@@ -249,9 +241,7 @@ function createDiscussionComment(discussionNodeId, body, replyToNodeId) {
|
||||
* Fetch alert metadata + locations. Never exposes .secret.
|
||||
*/
|
||||
function cmdFetchAlert(alertNumber) {
|
||||
if (!alertNumber) {
|
||||
fail("Usage: fetch-alert <number>");
|
||||
}
|
||||
if (!alertNumber) fail("Usage: fetch-alert <number>");
|
||||
|
||||
const alert = gh(["api", `repos/${REPO}/secret-scanning/alerts/${alertNumber}?hide_secret=true`]);
|
||||
|
||||
@@ -290,23 +280,17 @@ function cmdFetchAlert(alertNumber) {
|
||||
* Saves full body to a temp file. Prints metadata + file path to stdout.
|
||||
*/
|
||||
function cmdFetchContent(locationJson) {
|
||||
if (!locationJson) {
|
||||
fail("Usage: fetch-content '<location-json>'");
|
||||
}
|
||||
if (!locationJson) fail("Usage: fetch-content '<location-json>'");
|
||||
const location = JSON.parse(locationJson);
|
||||
const type = location.type;
|
||||
const details = location.details;
|
||||
|
||||
if (type === "discussion_comment") {
|
||||
const commentUrl = details.discussion_comment_url;
|
||||
if (!commentUrl) {
|
||||
fail("No discussion_comment_url in location details");
|
||||
}
|
||||
if (!commentUrl) fail("No discussion_comment_url in location details");
|
||||
|
||||
const urlMatch = commentUrl.match(/discussions\/(\d+)#discussioncomment-(\d+)/);
|
||||
if (!urlMatch) {
|
||||
fail(`Cannot parse discussion comment URL: ${commentUrl}`);
|
||||
}
|
||||
if (!urlMatch) fail(`Cannot parse discussion comment URL: ${commentUrl}`);
|
||||
const discussionNumber = urlMatch[1];
|
||||
const discussionCommentDbId = urlMatch[2];
|
||||
|
||||
@@ -314,11 +298,10 @@ function cmdFetchContent(locationJson) {
|
||||
discussionNumber,
|
||||
discussionCommentDbId,
|
||||
);
|
||||
if (!comment) {
|
||||
if (!comment)
|
||||
fail(
|
||||
`Discussion comment #${discussionCommentDbId} not found in discussion #${discussionNumber}`,
|
||||
);
|
||||
}
|
||||
|
||||
const bodyFile = tmpFile("body.md");
|
||||
fs.writeFileSync(bodyFile, comment.body || "");
|
||||
@@ -351,9 +334,7 @@ function cmdFetchContent(locationJson) {
|
||||
details.issue_comment_url ||
|
||||
details.pull_request_comment_url ||
|
||||
details.pull_request_review_comment_url;
|
||||
if (!commentUrl) {
|
||||
fail(`No comment URL in location details`);
|
||||
}
|
||||
if (!commentUrl) fail(`No comment URL in location details`);
|
||||
|
||||
const comment = gh(["api", commentUrl]);
|
||||
const bodyFile = tmpFile("body.md");
|
||||
@@ -397,9 +378,7 @@ function cmdFetchContent(locationJson) {
|
||||
);
|
||||
} else if (type === "issue_body") {
|
||||
const issueUrl = details.issue_body_url || details.issue_url;
|
||||
if (!issueUrl) {
|
||||
fail("No issue URL in location details");
|
||||
}
|
||||
if (!issueUrl) fail("No issue URL in location details");
|
||||
|
||||
const issue = gh(["api", issueUrl]);
|
||||
const bodyFile = tmpFile("body.md");
|
||||
@@ -435,9 +414,7 @@ function cmdFetchContent(locationJson) {
|
||||
);
|
||||
} else if (type === "pull_request_body") {
|
||||
const prUrl = details.pull_request_body_url || details.pull_request_url;
|
||||
if (!prUrl) {
|
||||
fail("No PR URL in location details");
|
||||
}
|
||||
if (!prUrl) fail("No PR URL in location details");
|
||||
|
||||
const pr = gh(["api", prUrl]);
|
||||
const bodyFile = tmpFile("body.md");
|
||||
@@ -513,9 +490,7 @@ function cmdRedactBody(kind, number, bodyFile) {
|
||||
if (!kind || !number || !bodyFile) {
|
||||
fail("Usage: redact-body <issue|pr> <number> <redacted-body-file>");
|
||||
}
|
||||
if (!fs.existsSync(bodyFile)) {
|
||||
fail(`File not found: ${bodyFile}`);
|
||||
}
|
||||
if (!fs.existsSync(bodyFile)) fail(`File not found: ${bodyFile}`);
|
||||
|
||||
const endpoint =
|
||||
kind === "pr" ? `repos/${REPO}/pulls/${number}` : `repos/${REPO}/issues/${number}`;
|
||||
@@ -534,12 +509,8 @@ function cmdRedactBodyIfNeeded(kind, number, currentBodyFile, redactedBodyFile,
|
||||
"Usage: redact-body-if-needed <issue|pr> <number> <current-body-file> <redacted-body-file> <result-file>",
|
||||
);
|
||||
}
|
||||
if (!fs.existsSync(currentBodyFile)) {
|
||||
fail(`File not found: ${currentBodyFile}`);
|
||||
}
|
||||
if (!fs.existsSync(redactedBodyFile)) {
|
||||
fail(`File not found: ${redactedBodyFile}`);
|
||||
}
|
||||
if (!fs.existsSync(currentBodyFile)) fail(`File not found: ${currentBodyFile}`);
|
||||
if (!fs.existsSync(redactedBodyFile)) fail(`File not found: ${redactedBodyFile}`);
|
||||
|
||||
const currentBody = fs.readFileSync(currentBodyFile, "utf8");
|
||||
const redactedBody = fs.readFileSync(redactedBodyFile, "utf8");
|
||||
@@ -570,9 +541,7 @@ function cmdRedactBodyIfNeeded(kind, number, currentBodyFile, redactedBodyFile,
|
||||
* Delete a comment (and all its edit history).
|
||||
*/
|
||||
function cmdDeleteComment(commentId) {
|
||||
if (!commentId) {
|
||||
fail("Usage: delete-comment <comment-id>");
|
||||
}
|
||||
if (!commentId) fail("Usage: delete-comment <comment-id>");
|
||||
gh(["api", `repos/${REPO}/issues/comments/${commentId}`, "-X", "DELETE"], { json: false });
|
||||
console.log(JSON.stringify({ ok: true, deleted_comment_id: Number(commentId) }));
|
||||
}
|
||||
@@ -582,9 +551,7 @@ function cmdDeleteComment(commentId) {
|
||||
* Delete a discussion comment via GraphQL (and all its edit history).
|
||||
*/
|
||||
function cmdDeleteDiscussionComment(nodeId) {
|
||||
if (!nodeId) {
|
||||
fail("Usage: delete-discussion-comment <node-id>");
|
||||
}
|
||||
if (!nodeId) fail("Usage: delete-discussion-comment <node-id>");
|
||||
const result = ghGraphQL(
|
||||
`mutation { deleteDiscussionComment(input: { id: "${nodeId}" }) { comment { id } } }`,
|
||||
);
|
||||
@@ -599,12 +566,9 @@ function cmdDeleteDiscussionComment(nodeId) {
|
||||
* Create a new discussion comment via GraphQL.
|
||||
*/
|
||||
function cmdRecreateDiscussionComment(discussionNodeId, bodyFile, replyToNodeId) {
|
||||
if (!discussionNodeId || !bodyFile) {
|
||||
if (!discussionNodeId || !bodyFile)
|
||||
fail("Usage: recreate-discussion-comment <discussion-node-id> <body-file> [reply-to-node-id]");
|
||||
}
|
||||
if (!fs.existsSync(bodyFile)) {
|
||||
fail(`File not found: ${bodyFile}`);
|
||||
}
|
||||
if (!fs.existsSync(bodyFile)) fail(`File not found: ${bodyFile}`);
|
||||
|
||||
const body = fs.readFileSync(bodyFile, "utf8");
|
||||
const newComment = createDiscussionComment(discussionNodeId, body, replyToNodeId);
|
||||
@@ -622,12 +586,8 @@ function cmdRecreateDiscussionComment(discussionNodeId, bodyFile, replyToNodeId)
|
||||
* Create a new comment from a file.
|
||||
*/
|
||||
function cmdRecreateComment(issueNumber, bodyFile) {
|
||||
if (!issueNumber || !bodyFile) {
|
||||
fail("Usage: recreate-comment <issue-number> <body-file>");
|
||||
}
|
||||
if (!fs.existsSync(bodyFile)) {
|
||||
fail(`File not found: ${bodyFile}`);
|
||||
}
|
||||
if (!issueNumber || !bodyFile) fail("Usage: recreate-comment <issue-number> <body-file>");
|
||||
if (!fs.existsSync(bodyFile)) fail(`File not found: ${bodyFile}`);
|
||||
|
||||
const result = gh([
|
||||
"api",
|
||||
@@ -755,9 +715,7 @@ function cmdNotify(target, author, locationType, secretTypes, replyToNodeId) {
|
||||
* Close a secret scanning alert.
|
||||
*/
|
||||
function cmdResolve(alertNumber, resolution, comment) {
|
||||
if (!alertNumber) {
|
||||
fail("Usage: resolve <alert-number> [resolution] [comment]");
|
||||
}
|
||||
if (!alertNumber) fail("Usage: resolve <alert-number> [resolution] [comment]");
|
||||
|
||||
const res = resolution || "revoked";
|
||||
const resComment = comment || "Content redacted and author notified to rotate credentials.";
|
||||
@@ -815,12 +773,8 @@ function cmdListOpen() {
|
||||
* Print a formatted summary table from a JSON results file.
|
||||
*/
|
||||
function cmdSummary(jsonFile) {
|
||||
if (!jsonFile) {
|
||||
fail("Usage: summary <json-file>");
|
||||
}
|
||||
if (!fs.existsSync(jsonFile)) {
|
||||
fail(`File not found: ${jsonFile}`);
|
||||
}
|
||||
if (!jsonFile) fail("Usage: summary <json-file>");
|
||||
if (!fs.existsSync(jsonFile)) fail(`File not found: ${jsonFile}`);
|
||||
|
||||
const results = JSON.parse(fs.readFileSync(jsonFile, "utf8"));
|
||||
const lines = [];
|
||||
|
||||
@@ -4,11 +4,11 @@ profile: openclaw-check
|
||||
provider: azure
|
||||
class: standard
|
||||
capacity:
|
||||
market: on-demand
|
||||
market: spot
|
||||
strategy: most-available
|
||||
# The Azure-backed billing account carries the OpenClaw runner credits; use
|
||||
# explicit on-demand capacity instead of low-priority spot, whose regional
|
||||
# quota is too small for broad maintainer proof or parallel Crabbox lanes.
|
||||
# Fail closed instead of silently falling back to on-demand while the
|
||||
# Azure-backed billing account is the default runner path.
|
||||
fallback: spot-only
|
||||
hints: true
|
||||
actions:
|
||||
workflow: .github/workflows/crabbox-hydrate.yml
|
||||
@@ -48,10 +48,6 @@ aws:
|
||||
# leaking AWS region names into the Azure default capacity fallback list.
|
||||
region: eu-west-1
|
||||
rootGB: 400
|
||||
azure:
|
||||
# The OpenClaw Azure subscription is reliable in eastus2; eastus rejects the
|
||||
# same SKUs and can stall provisioning.
|
||||
location: eastus2
|
||||
sync:
|
||||
delete: true
|
||||
checksum: false
|
||||
@@ -71,16 +67,13 @@ env:
|
||||
- OPENCLAW_*
|
||||
ssh:
|
||||
user: crabbox
|
||||
# Azure coordinator leases expose SSH on 22. The run wrapper can fall back
|
||||
# from 2222, but `crabbox job run` hydrates via the configured port directly.
|
||||
port: "22"
|
||||
port: "2222"
|
||||
jobs:
|
||||
prewarm:
|
||||
provider: azure
|
||||
target: linux
|
||||
class: standard
|
||||
type: Standard_D4ads_v6
|
||||
market: on-demand
|
||||
market: spot
|
||||
idleTimeout: 90m
|
||||
hydrate:
|
||||
actions: true
|
||||
@@ -97,8 +90,7 @@ jobs:
|
||||
provider: azure
|
||||
target: linux
|
||||
class: standard
|
||||
type: Standard_D4ads_v6
|
||||
market: on-demand
|
||||
market: spot
|
||||
idleTimeout: 90m
|
||||
hydrate:
|
||||
actions: true
|
||||
@@ -107,18 +99,7 @@ jobs:
|
||||
workflow: .github/workflows/crabbox-hydrate.yml
|
||||
job: hydrate
|
||||
ref: main
|
||||
shell: true
|
||||
command: |
|
||||
set -euo pipefail
|
||||
if ! git status --short >/dev/null 2>&1; then
|
||||
rm -rf .git
|
||||
git init -q
|
||||
git add -A
|
||||
if ! git diff --cached --quiet; then
|
||||
git -c user.name=OpenClaw -c user.email=ci@openclaw.local commit -q --no-gpg-sign -m remote-check-tree
|
||||
fi
|
||||
fi
|
||||
env CI=1 corepack pnpm check --timed
|
||||
command: env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 CI=1 corepack pnpm check:changed
|
||||
stop: always
|
||||
testbox-changed:
|
||||
provider: blacksmith-testbox
|
||||
|
||||
136
.github/workflows/ci-check-testbox.yml
vendored
136
.github/workflows/ci-check-testbox.yml
vendored
@@ -139,139 +139,3 @@ jobs:
|
||||
if: success()
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
check-arm:
|
||||
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
|
||||
permissions:
|
||||
contents: read
|
||||
name: "check-arm"
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404-arm
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- name: Begin Testbox
|
||||
uses: useblacksmith/begin-testbox@d0e04585c26905fdd92c94a09c159544c7ee1b67
|
||||
with:
|
||||
testbox_id: ${{ inputs.testbox_id }}
|
||||
- name: Verify ARM runner
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
runner_arch="$(uname -m)"
|
||||
echo "check-arm runner architecture: ${runner_arch}"
|
||||
case "$runner_arch" in
|
||||
aarch64 | arm64)
|
||||
;;
|
||||
*)
|
||||
echo "check-arm requires an ARM64 runner; got ${runner_arch}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
- name: Checkout
|
||||
shell: bash
|
||||
env:
|
||||
CHECKOUT_REPO: ${{ github.repository }}
|
||||
CHECKOUT_SHA: ${{ github.sha }}
|
||||
CHECKOUT_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
workdir="$GITHUB_WORKSPACE"
|
||||
if [[ -z "$CHECKOUT_TOKEN" ]]; then
|
||||
echo "checkout token is missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
|
||||
|
||||
reset_checkout_dir() {
|
||||
mkdir -p "$workdir"
|
||||
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
||||
}
|
||||
|
||||
checkout_attempt() {
|
||||
local attempt="$1"
|
||||
|
||||
reset_checkout_dir
|
||||
git init "$workdir" >/dev/null
|
||||
git config --global --add safe.directory "$workdir"
|
||||
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
|
||||
git -C "$workdir" config gc.auto 0
|
||||
|
||||
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
|
||||
-c protocol.version=2 \
|
||||
-c "http.extraheader=AUTHORIZATION: basic ${auth_header}" \
|
||||
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
||||
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
|
||||
|
||||
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
|
||||
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
|
||||
echo "checkout attempt ${attempt}/5 succeeded"
|
||||
}
|
||||
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if checkout_attempt "$attempt"; then
|
||||
exit 0
|
||||
fi
|
||||
echo "checkout attempt ${attempt}/5 failed"
|
||||
sleep $((attempt * 5))
|
||||
done
|
||||
|
||||
echo "checkout failed after 5 attempts" >&2
|
||||
exit 1
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
- name: Prepare Testbox shell
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
timeout --signal=TERM --kill-after=10s 30s git \
|
||||
-c protocol.version=2 \
|
||||
fetch --no-tags --prune --no-recurse-submodules --depth=50 origin \
|
||||
"+refs/heads/main:refs/remotes/origin/main"
|
||||
|
||||
node_bin="$(dirname "$(node -p 'process.execPath')")"
|
||||
sudo ln -sf "$node_bin/node" /usr/local/bin/node
|
||||
sudo ln -sf "$node_bin/npm" /usr/local/bin/npm
|
||||
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
|
||||
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
|
||||
sudo tee /usr/local/bin/pnpm >/dev/null <<'PNPM'
|
||||
#!/usr/bin/env bash
|
||||
exec /usr/local/bin/corepack pnpm "$@"
|
||||
PNPM
|
||||
sudo chmod 0755 /usr/local/bin/pnpm
|
||||
|
||||
- name: Hydrate Testbox provider env helper
|
||||
shell: bash
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
|
||||
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
|
||||
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
|
||||
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
|
||||
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
|
||||
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
|
||||
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
|
||||
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
|
||||
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
|
||||
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
|
||||
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
|
||||
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
|
||||
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
|
||||
run: bash scripts/ci-hydrate-testbox-env.sh
|
||||
|
||||
- name: Run Testbox
|
||||
uses: useblacksmith/run-testbox@5ca05834db1d3813554d1dd109e5f2087a8d7cbc
|
||||
if: success()
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -1202,9 +1202,6 @@ jobs:
|
||||
- check_name: check-guards
|
||||
task: guards
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- check_name: check-shrinkwrap
|
||||
task: shrinkwrap
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- check_name: check-prod-types
|
||||
task: prod-types
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
@@ -1280,6 +1277,7 @@ jobs:
|
||||
pnpm tool-display:check
|
||||
pnpm check:host-env-policy:swift
|
||||
pnpm dup:check:coverage
|
||||
pnpm deps:shrinkwrap:check
|
||||
pnpm deps:patches:check
|
||||
pnpm lint:webhook:no-low-level-body-read
|
||||
pnpm lint:auth:no-pairing-store-group
|
||||
@@ -1288,9 +1286,6 @@ jobs:
|
||||
# build-artifacts already runs the tsdown/runtime build for the same Node-relevant changes.
|
||||
NODE_OPTIONS=--max-old-space-size=8192 pnpm build:plugin-sdk:strict-smoke
|
||||
;;
|
||||
shrinkwrap)
|
||||
pnpm deps:shrinkwrap:check
|
||||
;;
|
||||
prod-types)
|
||||
pnpm tsgo:prod
|
||||
;;
|
||||
|
||||
23
.github/workflows/crabbox-hydrate.yml
vendored
23
.github/workflows/crabbox-hydrate.yml
vendored
@@ -358,8 +358,8 @@ jobs:
|
||||
$env:COREPACK_HOME = Join-Path $env:XDG_CACHE_HOME "corepack"
|
||||
$env:PNPM_HOME = Join-Path $cacheRoot "pnpm-home"
|
||||
$env:PNPM_CONFIG_STORE_DIR = Join-Path $cacheRoot "openclaw-pnpm-store"
|
||||
$env:PNPM_CONFIG_MODULES_DIR = Join-Path $cacheRoot "openclaw-pnpm-node-modules"
|
||||
$env:PNPM_CONFIG_VIRTUAL_STORE_DIR = Join-Path $env:PNPM_CONFIG_MODULES_DIR ".pnpm"
|
||||
$env:PNPM_CONFIG_MODULES_DIR = Join-Path $workspace "node_modules"
|
||||
$env:PNPM_CONFIG_VIRTUAL_STORE_DIR = Join-Path $workspace "node_modules\.pnpm"
|
||||
$env:PNPM_CONFIG_CHILD_CONCURRENCY = "4"
|
||||
$env:PNPM_CONFIG_NETWORK_CONCURRENCY = "8"
|
||||
$env:PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN = "false"
|
||||
@@ -431,25 +431,6 @@ jobs:
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
$workspaceNodeModules = Join-Path $workspace "node_modules"
|
||||
if (Test-Path $workspaceNodeModules) {
|
||||
$workspaceNodeModulesItem = Get-Item $workspaceNodeModules -Force
|
||||
if (($workspaceNodeModulesItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -eq 0) {
|
||||
$nodeModulesChildren = @(Get-ChildItem -LiteralPath $workspaceNodeModules -Force)
|
||||
$hasOnlyPnpmWorkspaceState = $nodeModulesChildren.Count -eq 1 -and $nodeModulesChildren[0].Name -eq ".pnpm-workspace-state-v1.json"
|
||||
if ($nodeModulesChildren.Count -ne 0 -and -not $hasOnlyPnpmWorkspaceState) {
|
||||
throw "workspace node_modules exists and is not a link: $workspaceNodeModules"
|
||||
}
|
||||
foreach ($nodeModulesChild in $nodeModulesChildren) {
|
||||
Remove-Item -LiteralPath $nodeModulesChild.FullName -Force
|
||||
}
|
||||
Remove-Item -LiteralPath $workspaceNodeModules -Force
|
||||
New-Item -ItemType Junction -Path $workspaceNodeModules -Target $env:PNPM_CONFIG_MODULES_DIR | Out-Null
|
||||
}
|
||||
} else {
|
||||
New-Item -ItemType Junction -Path $workspaceNodeModules -Target $env:PNPM_CONFIG_MODULES_DIR | Out-Null
|
||||
}
|
||||
|
||||
$corepackShimDir = Join-Path $nodeBin "node_modules\corepack\shims"
|
||||
if (Test-Path $corepackShimDir) {
|
||||
$env:PNPM_HOME = $corepackShimDir
|
||||
|
||||
@@ -1953,7 +1953,7 @@ jobs:
|
||||
profiles: stable full
|
||||
- suite_id: native-live-src-gateway-profiles-minimax
|
||||
label: Native live gateway profiles MiniMax
|
||||
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M3,minimax-portal/MiniMax-M3 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
|
||||
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M2.7,minimax-portal/MiniMax-M2.7 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
|
||||
timeout_minutes: 60
|
||||
profile_env_only: false
|
||||
profiles: stable full
|
||||
@@ -2252,7 +2252,7 @@ jobs:
|
||||
profiles: stable full
|
||||
- suite_id: live-gateway-minimax-docker
|
||||
label: Docker live gateway MiniMax
|
||||
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M3,minimax-portal/MiniMax-M3 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
|
||||
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M2.7,minimax-portal/MiniMax-M2.7 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
|
||||
timeout_minutes: 40
|
||||
profile_env_only: false
|
||||
profiles: stable full
|
||||
|
||||
3
.github/workflows/openclaw-npm-release.yml
vendored
3
.github/workflows/openclaw-npm-release.yml
vendored
@@ -257,8 +257,7 @@ jobs:
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (const match of input.matchAll(/\[/g)) {
|
||||
const start = match.index;
|
||||
for (let start = input.indexOf("["); start !== -1; start = input.indexOf("[", start + 1)) {
|
||||
const end = arrayEndFrom(start);
|
||||
if (end === -1) {
|
||||
continue;
|
||||
|
||||
16
.github/workflows/openclaw-release-checks.yml
vendored
16
.github/workflows/openclaw-release-checks.yml
vendored
@@ -798,7 +798,7 @@ jobs:
|
||||
- name: Build private QA runtime
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: node scripts/build-all.mjs qaRuntime
|
||||
run: pnpm build
|
||||
|
||||
- name: Run parity lane
|
||||
env:
|
||||
@@ -876,7 +876,7 @@ jobs:
|
||||
- name: Build private QA runtime
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: node scripts/build-all.mjs qaRuntime
|
||||
run: pnpm build
|
||||
|
||||
- name: Generate parity report
|
||||
run: |
|
||||
@@ -934,7 +934,7 @@ jobs:
|
||||
- name: Build private QA runtime
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: node scripts/build-all.mjs qaRuntime
|
||||
run: pnpm build
|
||||
|
||||
- name: Run runtime parity lane
|
||||
id: runtime_parity_lane
|
||||
@@ -1101,7 +1101,7 @@ jobs:
|
||||
- name: Build private QA runtime
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: node scripts/build-all.mjs qaRuntime
|
||||
run: pnpm build
|
||||
|
||||
- name: Run Matrix live lane
|
||||
id: run_lane
|
||||
@@ -1199,7 +1199,7 @@ jobs:
|
||||
- name: Build private QA runtime
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: node scripts/build-all.mjs qaRuntime
|
||||
run: pnpm build
|
||||
|
||||
- name: Run Telegram live lane
|
||||
id: run_lane
|
||||
@@ -1295,7 +1295,7 @@ jobs:
|
||||
- name: Build private QA runtime
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: node scripts/build-all.mjs qaRuntime
|
||||
run: pnpm build
|
||||
|
||||
- name: Run Discord live lane
|
||||
id: run_lane
|
||||
@@ -1393,7 +1393,7 @@ jobs:
|
||||
- name: Build private QA runtime
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: node scripts/build-all.mjs qaRuntime
|
||||
run: pnpm build
|
||||
|
||||
- name: Run WhatsApp live lane
|
||||
id: run_lane
|
||||
@@ -1488,7 +1488,7 @@ jobs:
|
||||
- name: Build private QA runtime
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: node scripts/build-all.mjs qaRuntime
|
||||
run: pnpm build
|
||||
|
||||
- name: Run Slack live lane
|
||||
id: run_lane
|
||||
|
||||
2
.github/workflows/update-migration.yml
vendored
2
.github/workflows/update-migration.yml
vendored
@@ -43,4 +43,4 @@ jobs:
|
||||
published_upgrade_survivor_baselines: ${{ inputs.baselines }}
|
||||
published_upgrade_survivor_scenarios: ${{ inputs.scenarios }}
|
||||
telegram_mode: none
|
||||
secrets: inherit # zizmor: ignore[secrets-inherit] Maintainer-dispatched package acceptance lane intentionally forwards its declared live-test secret matrix.
|
||||
secrets: inherit
|
||||
|
||||
4
.github/workflows/windows-testbox-probe.yml
vendored
4
.github/workflows/windows-testbox-probe.yml
vendored
@@ -61,14 +61,12 @@ jobs:
|
||||
submodules: false
|
||||
|
||||
- name: Probe native Windows
|
||||
env:
|
||||
TARGET_REF: ${{ inputs.target_ref || github.ref }}
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
Write-Host "runner=$env:RUNNER_NAME"
|
||||
Write-Host "machine=$env:COMPUTERNAME"
|
||||
Write-Host "workspace=$env:GITHUB_WORKSPACE"
|
||||
Write-Host "target_ref=$env:TARGET_REF"
|
||||
Write-Host "target_ref=${{ inputs.target_ref || github.ref }}"
|
||||
Write-Host ("os=" + [System.Environment]::OSVersion.VersionString)
|
||||
Write-Host ("arch=" + [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)
|
||||
Write-Host ("powershell=" + $PSVersionTable.PSVersion.ToString())
|
||||
|
||||
68
.github/workflows/workflow-sanity.yml
vendored
68
.github/workflows/workflow-sanity.yml
vendored
@@ -84,65 +84,6 @@ jobs:
|
||||
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
|
||||
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Prepare trusted workflow audit configs
|
||||
if: github.event_name == 'pull_request'
|
||||
env:
|
||||
BASE_REF: ${{ github.event.pull_request.base.ref }}
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
trusted_config="$RUNNER_TEMP/pre-commit-base.yaml"
|
||||
trusted_zizmor_config="$RUNNER_TEMP/zizmor-base.yml"
|
||||
|
||||
if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
|
||||
timeout --signal=TERM --kill-after=10s 30s git fetch --no-tags --depth=1 origin \
|
||||
"+${BASE_SHA}:refs/remotes/origin/security-base" ||
|
||||
timeout --signal=TERM --kill-after=10s 30s git fetch --no-tags --depth=1 origin \
|
||||
"+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}"
|
||||
fi
|
||||
|
||||
if git cat-file -e "${BASE_SHA}:.pre-commit-config.yaml" 2>/dev/null; then
|
||||
git show "${BASE_SHA}:.pre-commit-config.yaml" > "$trusted_config"
|
||||
elif git show "refs/remotes/origin/${BASE_REF}:.pre-commit-config.yaml" \
|
||||
> "$trusted_config" 2>/dev/null; then
|
||||
echo "Base SHA ${BASE_SHA} does not expose .pre-commit-config.yaml; using origin/${BASE_REF} instead."
|
||||
else
|
||||
echo "::error title=trusted pre-commit config unavailable::Could not read .pre-commit-config.yaml from ${BASE_SHA} or origin/${BASE_REF}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if git cat-file -e "${BASE_SHA}:.github/zizmor.yml" 2>/dev/null; then
|
||||
git show "${BASE_SHA}:.github/zizmor.yml" > "$trusted_zizmor_config"
|
||||
elif git show "refs/remotes/origin/${BASE_REF}:.github/zizmor.yml" \
|
||||
> "$trusted_zizmor_config" 2>/dev/null; then
|
||||
echo "Base SHA ${BASE_SHA} does not expose .github/zizmor.yml; using origin/${BASE_REF} instead."
|
||||
else
|
||||
echo "::error title=trusted zizmor config unavailable::Could not read .github/zizmor.yml from ${BASE_SHA} or origin/${BASE_REF}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 - "$trusted_config" "$trusted_zizmor_config" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
config_path = Path(sys.argv[1])
|
||||
zizmor_config_path = sys.argv[2]
|
||||
text = config_path.read_text()
|
||||
if ".github/zizmor.yml" not in text:
|
||||
raise SystemExit("trusted pre-commit config does not reference .github/zizmor.yml")
|
||||
config_path.write_text(text.replace(".github/zizmor.yml", zizmor_config_path))
|
||||
PY
|
||||
|
||||
echo "PRE_COMMIT_CONFIG_PATH=$trusted_config" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install pre-commit
|
||||
run: python -m pip install --disable-pip-version-check pre-commit==4.2.0
|
||||
|
||||
- name: Install actionlint
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -162,15 +103,6 @@ jobs:
|
||||
- name: Lint workflows
|
||||
run: actionlint
|
||||
|
||||
- name: Audit all workflows with zizmor
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mapfile -t workflow_files < <(
|
||||
find .github/workflows -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) | sort
|
||||
)
|
||||
pre-commit run --config "${PRE_COMMIT_CONFIG_PATH:-.pre-commit-config.yaml}" zizmor --files "${workflow_files[@]}"
|
||||
|
||||
- name: Disallow direct inputs interpolation in composite run blocks
|
||||
run: python3 scripts/check-composite-action-input-interpolation.py
|
||||
|
||||
|
||||
@@ -22,12 +22,11 @@
|
||||
"eslint/no-object-constructor": "error",
|
||||
"eslint/no-param-reassign": "error",
|
||||
"eslint/no-proto": "error",
|
||||
"eslint/no-promise-executor-return": "error",
|
||||
"eslint/no-regex-spaces": "error",
|
||||
"eslint/no-return-assign": "error",
|
||||
"eslint/no-sequences": "error",
|
||||
"eslint/no-self-compare": "error",
|
||||
"eslint/no-shadow": "error",
|
||||
"eslint/no-shadow": "off",
|
||||
"eslint/no-implicit-coercion": "error",
|
||||
"eslint/no-var": "error",
|
||||
"eslint/no-useless-call": "error",
|
||||
@@ -36,8 +35,7 @@
|
||||
"eslint/no-useless-constructor": "error",
|
||||
"eslint/no-useless-rename": "error",
|
||||
"eslint/no-useless-return": "error",
|
||||
"eslint/no-useless-assignment": "error",
|
||||
"eslint/no-unused-vars": "error",
|
||||
"eslint/no-unused-vars": "off",
|
||||
"eslint/no-warning-comments": "error",
|
||||
"eslint/no-unmodified-loop-condition": "error",
|
||||
"eslint/no-new-wrappers": "error",
|
||||
@@ -80,12 +78,8 @@
|
||||
"typescript/no-extraneous-class": "error",
|
||||
"typescript/no-import-type-side-effects": "error",
|
||||
"typescript/no-meaningless-void-operator": "error",
|
||||
"typescript/no-misused-promises": "error",
|
||||
"typescript/no-inferrable-types": "error",
|
||||
"typescript/only-throw-error": "error",
|
||||
"typescript/no-non-null-asserted-nullish-coalescing": "error",
|
||||
"typescript/prefer-promise-reject-errors": "error",
|
||||
"typescript/restrict-plus-operands": "error",
|
||||
"typescript/no-unnecessary-qualifier": "error",
|
||||
"typescript/no-unnecessary-type-assertion": "error",
|
||||
"typescript/no-unnecessary-type-arguments": "error",
|
||||
@@ -112,8 +106,6 @@
|
||||
"typescript/require-array-sort-compare": "error",
|
||||
"typescript/restrict-template-expressions": "error",
|
||||
"typescript/triple-slash-reference": "error",
|
||||
"typescript/unbound-method": "error",
|
||||
"typescript/use-unknown-in-catch-callback-variable": "error",
|
||||
"unicorn/consistent-date-clone": "error",
|
||||
"unicorn/consistent-empty-array-spread": "error",
|
||||
"unicorn/consistent-function-scoping": "off",
|
||||
@@ -133,7 +125,6 @@
|
||||
"unicorn/no-unnecessary-slice-end": "error",
|
||||
"unicorn/no-useless-error-capture-stack-trace": "error",
|
||||
"unicorn/no-useless-promise-resolve-reject": "error",
|
||||
"unicorn/no-useless-switch-case": "error",
|
||||
"unicorn/no-zero-fractions": "error",
|
||||
"unicorn/prefer-date-now": "error",
|
||||
"unicorn/prefer-dom-node-text-content": "error",
|
||||
@@ -238,7 +229,15 @@
|
||||
"**/*test-support.ts"
|
||||
],
|
||||
"rules": {
|
||||
"typescript/no-explicit-any": "off"
|
||||
"typescript/no-explicit-any": "off",
|
||||
"typescript/unbound-method": "off",
|
||||
"eslint/no-unsafe-optional-chaining": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["src/agents/embedded-agent-runner/run/attempt.ts"],
|
||||
"rules": {
|
||||
"eslint/no-shadow": "error"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
11
AGENTS.md
11
AGENTS.md
@@ -10,10 +10,9 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- Docs/user-visible work: `pnpm docs:list`, then read relevant docs only.
|
||||
- Fix/triage answers need source, tests, current/shipped behavior, and dependency contract proof.
|
||||
- Reviews/answers: high confidence required. Default to exhaustive relevant codebase search/read, including owners, callers, siblings, tests, docs, and upstream/dependency contracts before verdict. Diff-only review is insufficient.
|
||||
- Review default: read the whole changed function/module plus callers, callees, sibling implementations, adjacent tests, scoped docs, and dependency/Codex contracts before saying `good`, `bad`, `best fix`, `proof sufficient`, or posting a comment. If challenged, keep reading first; do not defend the earlier verdict until the missing path is checked.
|
||||
- Dependency-touching work: direct dependency inspection is mandatory when feasible; do not rely on assumptions, wrappers, or memory. Most dependencies are OSS, so read their source/docs/types. Codex-related work has a hard gate: the acting agent must personally inspect sibling `../codex` source for the exact protocol/runtime behavior before any verdict, comment, approval, merge recommendation, code change, or `proof sufficient` claim. If missing, clone `https://github.com/openai/codex.git` there first. Subagent reports, PR text, OpenClaw wrappers, generated schemas, memory, and prior bot reviews do not satisfy this gate. No direct `../codex` check means no Codex verdict. Cite Codex files/lines checked in final/review/comment.
|
||||
- Dependency-touching work: direct dependency inspection is mandatory when feasible; do not rely on assumptions, wrappers, or memory. Most dependencies are OSS, so read their source/docs/types. Codex-related work: before any verdict, comment, approval, merge recommendation, or `proof sufficient` claim, inspect sibling `../codex` source for the exact protocol/runtime behavior involved; if missing, clone `https://github.com/openai/codex.git` there first. Do not rely on PR text, OpenClaw wrappers, generated schemas, memory, or prior bot reviews as a substitute. Cite Codex files/lines checked in final/review/comment.
|
||||
- Dependency-backed behavior: read upstream docs/source/types first. No API/default/error/timing guesses.
|
||||
- External API work: live test required. Google/search for additional proof. Prefer official docs/source/types; cite current proof. No memory-only API claims.
|
||||
- External API work: Google/search for additional proof. Prefer official docs/source/types; cite current proof. No memory-only API claims.
|
||||
- Live-verify when feasible. Never print secrets.
|
||||
- Missing deps: `pnpm install`, retry once, then report first actionable error.
|
||||
- CODEOWNERS: maint/refactor/tests ok. Larger behavior/product/security/ownership: owner ask/review.
|
||||
@@ -31,7 +30,6 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- For PRs that add, remove, or change config/default surfaces with possible compatibility, upgrade, provider/plugin, operator, setup, startup, or fallback impact, ClawSweeper review should emit a `reviewMetrics` entry when practical. The metric should name the count and direction of the changes, such as added, changed, or removed config/default surfaces, and explain why the metric matters before merge. When the metric indicates concrete merge risk, also surface the concern in `risks`, use `mergeRiskLabels` when the risk matches the label rubric, make `bestSolution` name the desired pre-merge state, and ensure `labelJustifications` explain the specific reason rather than restating the label.
|
||||
- Review whole decision surfaces, not only the touched runtime, provider, channel, harness, plugin seam, or context path. Check sibling Codex/Pi-style runtimes, provider/model routing, channel delivery, gateway/protocol, plugin SDK, and context-management paths when relevant.
|
||||
- Every PR review must explicitly ask whether the PR is the best fix, not merely a plausible fix. Verdicts need a best-fix judgment backed by enough code reading to compare owner boundaries, callers, siblings, tests, docs, current `main`, shipped behavior when relevant, and dependency/Codex contracts when involved.
|
||||
- Before a PR verdict, build a small evidence map: changed surface, entry point, owner boundary, at least one caller and callee, sibling surfaces that share the invariant, existing tests, and current `main` behavior. If any cell is missing, say the gap instead of concluding.
|
||||
- One-sided fixes need sibling-surface proof, an explanation for why siblings are unaffected, or explicit follow-up work.
|
||||
- Changelog findings: see Docs / Changelog.
|
||||
- Public ClawSweeper comments prefer `https://docs.openclaw.ai/...` when a public docs page exists; structured evidence still cites repo files, lines, SHAs.
|
||||
@@ -63,7 +61,6 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- External official plugins own package/deps and are excluded from core dist; core uses registry-aware `facade-runtime` or generic contracts.
|
||||
- Externalizing a bundled plugin: update package excludes, official catalogs, docs, tests, and prove core runtime paths resolve installed plugin roots before root-dep removal.
|
||||
- Runtime reads canonical config only. No silent compat for old/malformed config keys. If a config change invalidates existing files, add a matching `openclaw doctor --fix` migration. Core/auth config repairs live in core doctor; plugin-owned config repairs live in that plugin's doctor contract (`legacyConfigRules` / `normalizeCompatibilityConfig`).
|
||||
- OpenAI Codex is folded into `openai`. No new/live `openai-codex` provider/plugin/auth/model routes; treat them as legacy input only. Runtime/setup/auth/catalog use `openai` + `openai/*`; doctor/migrations repair stale `openai-codex/*` profiles/metadata.
|
||||
- Config/env surface bar is high; `openclaw.json` and environment variables are already large. Before adding a config option or env var, first prove existing product behavior, provider selection, defaults, or doctor migration cannot solve it. Prefer removing or consolidating config/env options when touching these surfaces. Core supports only the latest config shape; `openclaw doctor --fix` migrates older shipped shapes into the current one.
|
||||
- CLI setup flows are public API when external docs, installers, or integrations can copy them. Changes to `openclaw onboard`, `openclaw configure`, their documented flags, non-interactive behavior, or generated config shape are compatibility-sensitive API contract changes; prefer additive flags/aliases, deprecation windows, and backward-preserving migrations over breaking existing snippets.
|
||||
- Fix shape: default to clean bounded refactor, not smallest patch. Move ownership to right boundary; delete stale abstractions, duplicate policy, dead branches, wrappers, fallback stacks.
|
||||
@@ -74,7 +71,6 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- Core runtime consumes only current canonical shapes/config/data. Legacy or retired shapes normalize only in doctor/migration code before runtime; no runtime shims, aliases, or fallback readers.
|
||||
- State/storage migrations are database-first. Runtime reads/writes the canonical store only. Old file stores, sidecars, aliases, and fallback readers belong in `openclaw doctor --fix` migration code only, never steady-state runtime.
|
||||
- Storage default: SQLite only. Do not add JSON/JSONL/TXT/sidecar files for OpenClaw-owned runtime state, caches, queues, registries, indexes, cursors, checkpoints, or plugin scratch data.
|
||||
- SQLite runtime access uses Kysely helpers, not raw SQL statement strings, except schema DDL, migrations, low-level DB bootstrap, or narrowly justified SQLite primitives.
|
||||
- Use the shared state DB (`state/openclaw.sqlite`) for global runtime state and plugin KV data. Use the per-agent DB (`agents/<agentId>/agent/openclaw-agent.sqlite`) for agent-scoped state/cache. Use a dedicated SQLite DB only when schema, volume, or lifecycle clearly does not fit those stores.
|
||||
- Legacy state/cache files are migration debt. When touching code that reads/writes them, prefer moving the data into SQLite or calling out the refactor follow-up; do not add parallel file paths.
|
||||
- File storage must be a named product artifact: import/export, user attachment, log, backup, or external tool contract. If it is app state or cache, it belongs in SQLite.
|
||||
@@ -128,7 +124,6 @@ Skills own workflows; root owns hard policy and routing.
|
||||
|
||||
- Use `$openclaw-testing` for test/CI choice and `$crabbox` for remote/full/E2E proof.
|
||||
- Crabbox request means real scenario proof: install/update/call/repro user path; not just copy tests and run them remotely.
|
||||
- Visual proof: use Crabbox, set up like a user, then screenshot-verify. No harness/bypass/shortcut unless explicitly asked.
|
||||
- Small/narrow tests, lints, format checks, and type probes are fine locally only in a healthy normal checkout.
|
||||
- In Codex worktrees, direct local `pnpm test*`, `pnpm check*`, `pnpm crabbox:run`, and `scripts/committer` can trigger pnpm dependency reconciliation or install prompts. Prefer `node` wrappers locally and Crabbox/Testbox for pnpm-gated proof.
|
||||
- Full suites, broad changed gates, Docker/package/E2E/live/cross-OS proof, or anything that bogs down the Mac: Crabbox/Testbox.
|
||||
@@ -150,7 +145,6 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- Maintainer decision closes the cluster: if deciding reported behavior/proposed fix is not planned, comment+close all directly associated open issues/PRs unless explicitly told to keep one open. Associated means linked PRs/issues, duplicates, companion workaround PRs, and the canonical issue for the rejected behavior.
|
||||
- Do not leave associated issues open for hypothetical future repros. Close with rationale; ask for a new issue or reopen only if concrete new evidence appears. Close comment states: decision, why, supported alternative, and what evidence would change the decision.
|
||||
- Issue/PR work: search strong related issues/PRs before final; close proven dupes/fixed siblings. If none close, suggest one next related follow-up.
|
||||
- PR superseded by `main`: if code proof shows `main` already has same-or-better behavior, comment canonical commit/PR + focused proof, then close. Bar high: inspect PR diff, current code/tests, linked issue, caller/sibling path. If unsure, leave open.
|
||||
- Issue/PR numbers need a short summary every time; assume the reader has not opened or read them.
|
||||
- Before presenting a batch of issues/PRs, use smart subagents to verify live state and current `main`; omit closed/fixed items, and comment+close items already fixed on `main` when maintainer action is authorized.
|
||||
- PR review answer: bug/behavior, URL(s), affected surface, provenance for regressions when traceable, best-fix judgment, evidence from code/tests/CI/current or shipped behavior.
|
||||
@@ -265,7 +259,6 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- Version bump surfaces live in `$release-openclaw-maintainer`.
|
||||
- Parallels: `$openclaw-parallels-smoke`; Discord roundtrip: `$parallels-discord-roundtrip`.
|
||||
- Crabbox/WebVNC human demos: keep remote desktop visible/windowed; no fullscreen remote browser unless video/capture-style output.
|
||||
- Before sharing WebVNC links, use Crabbox screenshot first; verify real app/path works and target UI is not broken.
|
||||
- ClawSweeper ops: `$clawsweeper`. Deployed hook sessions may post one concise `#clawsweeper` note only when surprising/actionable/risky; if using message tool, reply exactly `NO_REPLY`.
|
||||
- Generated-media completions wake the requester agent first. Requester visible-reply config decides final text vs message tool; direct media send is fallback/recovery only.
|
||||
- `message_tool_only`: normal agent final visible reply = current-source `message(action=send)` only. No `NO_REPLY` prompt/contract; no message call = no source reply. Plugin-owned bound-thread reply = plugin return value; no message tool needed. Never auto-publish private final.
|
||||
|
||||
40
CHANGELOG.md
40
CHANGELOG.md
@@ -2,7 +2,7 @@
|
||||
|
||||
Docs: https://docs.openclaw.ai
|
||||
|
||||
## 2026.6.1
|
||||
## 2026.5.31
|
||||
|
||||
### Highlights
|
||||
|
||||
@@ -10,13 +10,8 @@ Docs: https://docs.openclaw.ai
|
||||
- Channels and mobile delivery are steadier across Telegram, WhatsApp, iMessage, Slack, Discord, Microsoft Teams, Google Chat, Google Meet, and iOS realtime Talk. (#88096, #88105, #88183, #88231)
|
||||
- Provider and plugin requests now bound more timers, retries, OAuth/device-code lifetimes, media downloads, local service probes, and generated-content polling paths before they can hang a run.
|
||||
- Skills, session metadata, gateway runtime state, plugin metadata, and store writes do less repeated work on hot paths while keeping config and dispatch behavior stable.
|
||||
- Skills and plugin loading now handle stale disabled snapshots and loader failures more clearly, so channel turns avoid disabled SecretRefs and operators get better recovery guidance. (#79072, #79173) Thanks @zeus1959.
|
||||
- Workboard, SecretRef plugin manifests, hosted iOS push relay, and external Copilot/Tokenjuice packaging add broader orchestration, integration, and plugin delivery surfaces. (#82326, #87469, #87796, #88107, #88117)
|
||||
- Skill Workshop now has a fuller Control UI flow with proposal lists, today actions, revision handoff, searchable file previews, review states, locale coverage, and reusable session routing.
|
||||
- Chat and Control UI startup paths keep sends alive through history loading, stream deltas incrementally, skip markdown work while streaming, keep drafts local while typing, trace first-output latency, and expose calmer composer controls. (#88772, #88825, #88998) Thanks @vincentkoc.
|
||||
- Provider coverage and model metadata now include MiniMax M3, account OAuth endpoints, Google/Vertex catalog fixes, OpenRouter SQLite model caching, Copilot Claude 1M capabilities, Foundry reasoning alignment, and OpenAI response replay guards. (#88480, #88512, #88851, #88860)
|
||||
- iMessage monitor state, inbound queues, and plugin install ledgers moved toward SQLite-backed state so restarts and local monitors recover with less duplicate filesystem scanning. (#88794, #88797)
|
||||
- Release, CI, Docker, E2E, plugin install, and diagnostics lanes now cap more logs, response bodies, readiness probes, artifact checks, status polling, and rollback snapshots so failures report bounded proof instead of stalling.
|
||||
- Release, CI, Docker, E2E, and diagnostics lanes now cap more logs, response bodies, readiness probes, artifact checks, and status polling so failures report bounded proof instead of stalling.
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -25,46 +20,25 @@ Docs: https://docs.openclaw.ai
|
||||
- Skills: let proposals carry approved support files under standard skill folders, with scanner, hash, and rollback safeguards. Thanks @shakkernerd.
|
||||
- Skills: let pending proposals be revised in place with versioned, dated proposal frontmatter before approval. Thanks @shakkernerd.
|
||||
- Skills: add Skill Workshop with pending proposals, CLI/Gateway review actions, rollback metadata, and the `skill_workshop` agent tool. Thanks @shakkernerd.
|
||||
- Skill Workshop: add the Control UI navigation, styled dashboard, proposal today view, revision dialog, file preview modal, searchable preview files, reusable session handoff, and localized strings.
|
||||
- Plugins: externalize Tokenjuice as the official `@openclaw/tokenjuice` plugin with npm and ClawHub publish metadata.
|
||||
- Plugins: externalize the GitHub Copilot agent runtime as the official `@openclaw/copilot` plugin with npm and ClawHub publish metadata.
|
||||
- iOS: add hosted push relay defaults, realtime Talk playback, and a guarded WebSocket ping path for more reliable mobile sessions. (#88096, #88105, #88231)
|
||||
- iOS: support native iPad display layouts.
|
||||
- Workboard: add orchestration primitives and agent coordination tools for multi-agent planning and run tracking. (#87469)
|
||||
- Workboard: wire task-backed board runs and show task comments in the edit modal.
|
||||
- Code mode: add internal namespaces for scoped agent/global sessions and exact namespace tool dispatch. (#88043)
|
||||
- Code mode: add MCP API files and docs for code-mode integrations.
|
||||
- Control UI: add a Dreaming-tab agent selector and propagate the selected agent through Dreaming status, diary, and diary actions. (#78748) Thanks @stevenepalmer.
|
||||
- Control UI: add calmer chat composer controls, local draft typing state, and first-output latency instrumentation for active chat entry. (#88772, #88998) Thanks @vincentkoc.
|
||||
- Plugins: add a SecretRef provider integration manifest contract and extract shared LLM core packages for provider/plugin reuse. (#82326, #88117)
|
||||
- Plugins: persist the plugin install index in SQLite so installed package lookup survives reloads with less filesystem scanning. (#88794)
|
||||
- Providers: add MiniMax M3 model support. (#88860)
|
||||
- Doctor: add disk space health checks and stabilize post-upgrade JSON probes.
|
||||
- Channels: store inbound queues in SQLite and migrate iMessage monitor state to SQLite-backed tracking. (#88797)
|
||||
- Skills: add the core skills index and centralize skills runtime loading, status, filtering, and prompt formatting.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Agents/TUI: keep local custom provider runs from loading plugin runtime and auth alias metadata when plugins are disabled.
|
||||
- Agents/TUI: restore in-flight TUI run switch-back behavior, keep no-policy native hook fallback available, guard vanished workspaces, and keep lightweight isolated subagents lightweight.
|
||||
- Agents/media: keep async image, music, and video generation starts from ending the Codex turn, so mixed requests can continue with summaries or other work while media renders in the background.
|
||||
- Agents/Codex: keep public OpenAI API-key profiles from being treated as native Codex app-server auth while preserving persisted Codex OAuth sessions.
|
||||
- Agents/Codex: stream Codex app-server final-answer partials to live reply previews, preserve ACP metadata in SQLite, prefer real tool results over synthetic repair output, prevent aborted app-server turn handles from lingering, migrate legacy OpenAI Codex `lastGood` auth state, and preserve workspace/session metadata through ACP runtime refactors. (#88405, #88724, #88730) Thanks @vincentkoc.
|
||||
- Control UI: keep collapsed tool cards labeled with the tool name and action instead of generic output text. Thanks @shakkernerd.
|
||||
- Agents/Codex: surface Skill Workshop guidance in Codex app-server prompts when `skill_workshop` is available. Thanks @shakkernerd.
|
||||
- Agents/auth: write auth profiles atomically, add force re-login recovery, preserve workspaces during state-only uninstall, and compact before oversized turns so recovery paths avoid partial state.
|
||||
- Skills: skip disabled skill env overrides from stale persisted snapshots so disabled skill `apiKey` SecretRefs cannot abort embedded or channel turns. (#79072, #79173) Thanks @zeus1959.
|
||||
- CLI: avoid live catalog validation during `openclaw agents add`, so adding a secondary agent no longer depends on provider catalog availability. (#76284, #88314) Thanks @zhangguiping-xydt.
|
||||
- CLI: keep `plugins list --json` on the snapshot-only path so plugin sweeps avoid loading the full runtime status graph.
|
||||
- CLI/desktop: bridge WSL clipboard operations through the shell and recognize manual-update launchd jobs. (#88764)
|
||||
- Plugins: make PixVerse external-plugin ClawHub metadata explicit and keep it out of bundled dist builds.
|
||||
- Plugins: clarify plugin loader failure guidance so missing or incompatible plugin packages point operators at the right repair path.
|
||||
- Plugins: preserve npm plugin roots after blocked installs, skip plugin-local `openclaw` peer symlinks during rollback snapshots, relink those peers after restore, isolate cached tool runtime siblings, and isolate web-provider factory failures so one bad plugin does not poison sibling runtime paths. (#77237, #88807)
|
||||
- Cron: keep SQLite cron migrations compatible with legacy run-log tables, archived job stores, diagnostic cron names, and legacy one-shot delete-after-run behavior. (#88285)
|
||||
- Cron: keep update delivery validation scoped, harden restart state, and retire MCP runtimes on isolated cron cleanup.
|
||||
- Memory: serialize QMD update/embed writes per store, preserve phase signals on read errors, harden envelope metadata sanitization, and rewrite generated transcript paths on rollover so memory/search state survives concurrent gateway and CLI activity. (#66339, #85931) Thanks @openperf and @amittell.
|
||||
- Providers: bound generated media downloads from OpenAI, Runway, xAI, MiniMax, BytePlus, DashScope-compatible, FAL, OpenRouter, Google, Vydra, and Comfy providers.
|
||||
- Providers: resolve Google defaults to `google-generative-ai`, register Vertex static catalog rows, align Foundry reasoning metadata, skip DeepSeek V4 thinking params on Foundry fallback, use MiniMax account OAuth endpoints, preserve Copilot Claude 1M capabilities, suppress disabled Ollama reasoning output, keep OpenAI stop-finished tool calls, and avoid replay ids when the Responses store is disabled. (#88480, #88512)
|
||||
- Providers: cap GitHub Copilot OAuth request timeouts before creating abort signals.
|
||||
- Cron: retry recurring jobs after transient model rate limits before waiting for the next scheduled slot.
|
||||
- Agents/Codex: keep live session locks during cleanup, recover interrupted CLI tool transcripts, preserve Codex auth and compaction session identity, clear orphan tool state, cap app-server idle timers, and keep media completion delivery retryable. (#88129, #88136, #88141, #88162, #88182)
|
||||
@@ -72,19 +46,13 @@ Docs: https://docs.openclaw.ai
|
||||
- Channels: cap Telegram, Discord, WhatsApp, Signal, Feishu, Google Chat, Microsoft Teams, QQBot, Nostr, Zalo, Zalouser, and Nextcloud-style request/retry timers; preserve SMS approval reply routes; and retry WhatsApp QR login 408 timeouts. (#88183)
|
||||
- Security/config parsing: reject unsafe OAuth/token lifetimes, retry-after delays, inbound timestamps, response body sizes, command timeout config, sandbox observer token TTLs, and gateway WebSocket calls after close.
|
||||
- Providers/media: cap local service, model, usage, queue, generated media, TTS, music, workflow polling, and provider OAuth request timers across hosted and local providers.
|
||||
- Release/CI/E2E: bound release candidate reads, beta smoke REST calls, plugin npm verification commands, changelog restore, cross-OS process groups, kitchen-sink and bundled plugin readiness probes, secret-provider probes, Telegram credential timeouts, Control UI i18n and CLI startup metadata generation, Vitest routing, and mainline test flakes. (#88127, #88137, #88155, #88160)
|
||||
- Release/CI/E2E: keep Kitchen Sink live plugin MCP probes resolving source-checkout workspace packages and align the live gauntlet with current Kitchen Sink diagnostics.
|
||||
- Release/CI/E2E: bound release candidate reads, beta smoke REST calls, changelog restore, kitchen-sink and bundled plugin readiness probes, secret-provider probes, Vitest routing, and mainline test flakes. (#88127, #88137, #88155, #88160)
|
||||
- Release/CI/E2E: run the secret-provider integration proof through the repo pnpm runner so native macOS and Windows validation use the hydrated package-manager shim.
|
||||
- Release/CI/E2E: run the Telegram desktop proof gateway through the repo pnpm runner so native macOS proof uses the hydrated package-manager shim.
|
||||
- Docs/CI: run Mintlify anchor checks through the repo pnpm runner so docs link validation works when pnpm is only available through the hydrated package-manager shim.
|
||||
- Agents: keep configured fallback model metadata typed so provider params, context-token caps, and media input limits do not break changed-gate typechecks.
|
||||
- Agents: accept hidden `sessions_send` body aliases before validation while keeping the model-facing `message` schema canonical. (#88229) Thanks @zhangguiping-xydt.
|
||||
- Chat/UI: preserve startup chat sends during history loading, unblock the initial Control UI chat send, stream chat deltas incrementally, skip markdown parsing while streaming, keep drafts local while typing, guard composer rerenders, honor Chromium executable overrides, and detect system Chromium for E2E. (#88998) Thanks @vincentkoc.
|
||||
- Channels: preserve long Feishu streaming replies, send visible fallbacks when accepted Feishu turns produce no final reply, tolerate iMessage self-chat timestamp skew, preserve colon-prefixed slash commands in mention parsing, decode Nostr `npub` allowlists correctly, and suppress raw provider errors during channel delivery. (#87896)
|
||||
- Config/status/doctor: skip unresolved shell references in state-dir dotenv files, resolve gateway auth secrets during deep status audits, respect explicit PI runtime policy, report runtime tool-schema errors, and keep post-upgrade JSON stable. (#88288)
|
||||
- Gateway/session state: list commands from the Gateway plugin registry, harden MCP loopback tool schemas, hide phantom agent-store rows from `sessions.list`, make task persistence failures explicit, and carry session UUIDs on interactive dispatch events.
|
||||
- OpenAI/TTS: handle speed directives for OpenAI TTS voices. (#74089)
|
||||
- CI/Crabbox: keep default runner capacity on the Azure credit-backed on-demand D4 lane with the Azure SSH port and a Git-independent full check job, so broad validation avoids low-priority spot quota stalls, hydrate port mismatches, non-Git hydrated workspaces, and stale AWS region hints.
|
||||
- CI/Crabbox: keep default runner capacity spot-only and provider-neutral so OpenClaw remote validation does not silently fall back to on-demand leases or stale AWS region hints.
|
||||
- CI/Crabbox: route Crabbox wrapper and Testbox workflow edits to their regression tests so changed-test gates do not silently run zero specs.
|
||||
- CI/workflows: route workflow sanity helper edits to their guard tests and cover composite-action input interpolation checks.
|
||||
- CI/tooling: route CI scope, dependency, changelog, and docs helper edits to their owner tests instead of silently skipping changed-test coverage.
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
# Build stages use full bookworm; the runtime image is always bookworm-slim.
|
||||
ARG OPENCLAW_EXTENSIONS=""
|
||||
ARG OPENCLAW_BUNDLED_PLUGIN_DIR=extensions
|
||||
ARG OPENCLAW_NODE_BOOKWORM_IMAGE="node:24-bookworm@sha256:8530f76a96d88820d288761f022e318970dda93d01536919fbc16076b7983e63"
|
||||
ARG OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE="node:24-bookworm-slim@sha256:242549cd46785b480c832479a730f4f2a20865d61ea2e404fdb2a5c3d3b73ecf"
|
||||
ARG OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST="sha256:242549cd46785b480c832479a730f4f2a20865d61ea2e404fdb2a5c3d3b73ecf"
|
||||
ARG OPENCLAW_NODE_BOOKWORM_IMAGE="node:24-bookworm@sha256:3a09aa6354567619221ef6c45a5051b671f953f0a1924d1f819ffb236e520e6b"
|
||||
ARG OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE="node:24-bookworm-slim@sha256:e8e2e91b1378f83c5b2dd15f0247f34110e2fe895f6ca7719dbb780f929368eb"
|
||||
ARG OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST="sha256:e8e2e91b1378f83c5b2dd15f0247f34110e2fe895f6ca7719dbb780f929368eb"
|
||||
# Keep in sync with .github/actions/setup-node-env/action.yml bun-version.
|
||||
# To update: docker buildx imagetools inspect oven/bun:<version> and use the manifest-list digest.
|
||||
ARG OPENCLAW_BUN_IMAGE="oven/bun:1.3.13@sha256:87416c977a612a204eb54ab9f3927023c2a3c971f4f345a01da08ea6262ae30e"
|
||||
|
||||
@@ -66,7 +66,7 @@ android {
|
||||
minSdk = 31
|
||||
targetSdk = 36
|
||||
versionCode = 2026053101
|
||||
versionName = "2026.6.1"
|
||||
versionName = "2026.5.31"
|
||||
ndk {
|
||||
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
package ai.openclaw.app.ui
|
||||
|
||||
import ai.openclaw.app.node.DeviceNotificationListenerService
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
|
||||
/** App entry shown in the notification-forwarding package picker. */
|
||||
data class InstalledApp(
|
||||
val label: String,
|
||||
val packageName: String,
|
||||
val isSystemApp: Boolean,
|
||||
)
|
||||
|
||||
/** Reads launcher, recent-notification, and configured packages for the picker. */
|
||||
internal fun queryInstalledApps(
|
||||
context: Context,
|
||||
configuredPackages: Set<String>,
|
||||
): List<InstalledApp> {
|
||||
val packageManager = context.packageManager
|
||||
val launcherIntent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_LAUNCHER) }
|
||||
|
||||
val launcherPackages =
|
||||
packageManager
|
||||
.queryIntentActivities(launcherIntent, PackageManager.MATCH_ALL)
|
||||
.asSequence()
|
||||
.mapNotNull {
|
||||
it.activityInfo
|
||||
?.packageName
|
||||
?.trim()
|
||||
?.takeIf(String::isNotEmpty)
|
||||
}.toMutableSet()
|
||||
|
||||
val recentNotificationPackages =
|
||||
DeviceNotificationListenerService
|
||||
.recentPackages(context)
|
||||
.asSequence()
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
.toList()
|
||||
|
||||
val candidatePackages =
|
||||
resolveNotificationCandidatePackages(
|
||||
launcherPackages = launcherPackages,
|
||||
recentPackages = recentNotificationPackages,
|
||||
configuredPackages = configuredPackages,
|
||||
appPackageName = context.packageName,
|
||||
)
|
||||
|
||||
return candidatePackages
|
||||
.asSequence()
|
||||
.mapNotNull { packageName ->
|
||||
runCatching {
|
||||
val appInfo = packageManager.getApplicationInfo(packageName, 0)
|
||||
val label = packageManager.getApplicationLabel(appInfo).toString().trim()
|
||||
InstalledApp(
|
||||
label = if (label.isEmpty()) packageName else label,
|
||||
packageName = packageName,
|
||||
isSystemApp = (appInfo.flags and android.content.pm.ApplicationInfo.FLAG_SYSTEM) != 0,
|
||||
)
|
||||
}.getOrNull()
|
||||
}.sortedWith(compareBy<InstalledApp> { it.label.lowercase() }.thenBy { it.packageName })
|
||||
.toList()
|
||||
}
|
||||
|
||||
/** Merges package sources while excluding OpenClaw from its own forwarding filter. */
|
||||
internal fun resolveNotificationCandidatePackages(
|
||||
launcherPackages: Set<String>,
|
||||
recentPackages: List<String>,
|
||||
configuredPackages: Set<String>,
|
||||
appPackageName: String,
|
||||
): Set<String> {
|
||||
val blockedPackage = appPackageName.trim()
|
||||
return sequenceOf(
|
||||
configuredPackages.asSequence(),
|
||||
launcherPackages.asSequence(),
|
||||
recentPackages.asSequence(),
|
||||
).flatten()
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() && it != blockedPackage }
|
||||
.toSet()
|
||||
}
|
||||
@@ -493,8 +493,6 @@ private fun playVoiceSetupTone() {
|
||||
Handler(Looper.getMainLooper()).postDelayed({ tone.release() }, 300L)
|
||||
}
|
||||
|
||||
private const val NOTIFICATION_PICKER_RESULT_LIMIT = 40
|
||||
|
||||
@Composable
|
||||
private fun NotificationSettingsScreen(
|
||||
viewModel: MainViewModel,
|
||||
@@ -509,19 +507,6 @@ private fun NotificationSettingsScreen(
|
||||
val quietEnd by viewModel.notificationForwardingQuietEnd.collectAsState()
|
||||
val maxEventsPerMinute by viewModel.notificationForwardingMaxEventsPerMinute.collectAsState()
|
||||
val modeLabel = if (mode == NotificationPackageFilterMode.Blocklist) "Blocklist" else "Allowlist"
|
||||
val installedApps = remember(context, packages) { queryInstalledApps(context, packages) }
|
||||
var notificationPickerExpanded by remember { mutableStateOf(false) }
|
||||
var notificationAppSearch by remember { mutableStateOf("") }
|
||||
var notificationShowSystemApps by remember { mutableStateOf(false) }
|
||||
val filteredApps =
|
||||
remember(installedApps, packages, notificationAppSearch, notificationShowSystemApps) {
|
||||
filterNotificationAppsForPicker(
|
||||
apps = installedApps,
|
||||
selectedPackages = packages,
|
||||
query = notificationAppSearch,
|
||||
showSystemApps = notificationShowSystemApps,
|
||||
)
|
||||
}
|
||||
var listenerEnabled by remember { mutableStateOf(DeviceNotificationListenerService.isAccessEnabled(context)) }
|
||||
val notificationPermissionLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||
@@ -582,124 +567,6 @@ private fun NotificationSettingsScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
NotificationPackagePickerPanel(
|
||||
mode = mode,
|
||||
selectedPackages = packages,
|
||||
apps = filteredApps,
|
||||
search = notificationAppSearch,
|
||||
showSystemApps = notificationShowSystemApps,
|
||||
expanded = notificationPickerExpanded,
|
||||
onSearchChange = { notificationAppSearch = it },
|
||||
onShowSystemAppsChange = { notificationShowSystemApps = it },
|
||||
onExpandedChange = { notificationPickerExpanded = it },
|
||||
onPackageSelectionChange = { packageName, selected ->
|
||||
val next = packages.toMutableSet()
|
||||
if (selected) {
|
||||
next.add(packageName)
|
||||
} else {
|
||||
next.remove(packageName)
|
||||
}
|
||||
viewModel.setNotificationForwardingPackagesCsv(next.sorted().joinToString(","))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotificationPackagePickerPanel(
|
||||
mode: NotificationPackageFilterMode,
|
||||
selectedPackages: Set<String>,
|
||||
apps: List<InstalledApp>,
|
||||
search: String,
|
||||
showSystemApps: Boolean,
|
||||
expanded: Boolean,
|
||||
onSearchChange: (String) -> Unit,
|
||||
onShowSystemAppsChange: (Boolean) -> Unit,
|
||||
onExpandedChange: (Boolean) -> Unit,
|
||||
onPackageSelectionChange: (String, Boolean) -> Unit,
|
||||
) {
|
||||
val visibleApps = apps.take(NOTIFICATION_PICKER_RESULT_LIMIT)
|
||||
ClawPanel {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Text(text = "App Filter", style = ClawTheme.type.section, color = ClawTheme.colors.text)
|
||||
Text(
|
||||
text = notificationPackageSelectionSummary(mode = mode, selectedCount = selectedPackages.size),
|
||||
style = ClawTheme.type.body,
|
||||
color = ClawTheme.colors.textMuted,
|
||||
)
|
||||
ClawSecondaryButton(
|
||||
text = if (expanded) "Close App Picker" else "Open App Picker",
|
||||
onClick = { onExpandedChange(!expanded) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
if (expanded) {
|
||||
ClawTextField(value = search, onValueChange = onSearchChange, placeholder = "Search apps")
|
||||
SettingsToggleListRow(
|
||||
SettingsToggleRow(
|
||||
title = "Show System Apps",
|
||||
subtitle = "Include Android and background packages.",
|
||||
icon = Icons.Default.Storage,
|
||||
checked = showSystemApps,
|
||||
onCheckedChange = onShowSystemAppsChange,
|
||||
),
|
||||
)
|
||||
if (visibleApps.isEmpty()) {
|
||||
Text(text = "No matching apps.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
|
||||
} else {
|
||||
ClawSeparatedColumn(items = visibleApps) { app ->
|
||||
NotificationPackageAppRow(
|
||||
app = app,
|
||||
selected = selectedPackages.contains(app.packageName),
|
||||
onSelectedChange = { selected -> onPackageSelectionChange(app.packageName, selected) },
|
||||
)
|
||||
}
|
||||
if (apps.size > visibleApps.size) {
|
||||
Text(
|
||||
text = "Showing ${visibleApps.size} of ${apps.size}. Refine search for more.",
|
||||
style = ClawTheme.type.caption,
|
||||
color = ClawTheme.colors.textMuted,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotificationPackageAppRow(
|
||||
app: InstalledApp,
|
||||
selected: Boolean,
|
||||
onSelectedChange: (Boolean) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 58.dp)
|
||||
.clickable { onSelectedChange(!selected) }
|
||||
.padding(vertical = 7.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(9.dp),
|
||||
) {
|
||||
ClawTextBadge(text = notificationAppBadge(app.label))
|
||||
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(1.dp)) {
|
||||
Text(
|
||||
text = app.label,
|
||||
style = ClawTheme.type.body,
|
||||
color = ClawTheme.colors.text,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
text = app.packageName,
|
||||
style = ClawTheme.type.caption,
|
||||
color = ClawTheme.colors.textMuted,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
Switch(checked = selected, onCheckedChange = onSelectedChange)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1245,55 +1112,6 @@ private fun cronJobStatus(job: GatewayCronJobSummary): ClawStatus {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun filterNotificationAppsForPicker(
|
||||
apps: List<InstalledApp>,
|
||||
selectedPackages: Set<String>,
|
||||
query: String,
|
||||
showSystemApps: Boolean,
|
||||
): List<InstalledApp> {
|
||||
val normalizedQuery = query.trim().lowercase()
|
||||
return apps.filter { app ->
|
||||
val selected = app.packageName in selectedPackages
|
||||
val visibleByType = showSystemApps || !app.isSystemApp || selected
|
||||
val visibleBySearch =
|
||||
normalizedQuery.isEmpty() ||
|
||||
app.label.lowercase().contains(normalizedQuery) ||
|
||||
app.packageName.lowercase().contains(normalizedQuery)
|
||||
visibleByType && visibleBySearch
|
||||
}
|
||||
}
|
||||
|
||||
private fun notificationPackageSelectionSummary(
|
||||
mode: NotificationPackageFilterMode,
|
||||
selectedCount: Int,
|
||||
): String =
|
||||
when (mode) {
|
||||
NotificationPackageFilterMode.Allowlist ->
|
||||
if (selectedCount == 0) {
|
||||
"No apps selected. Nothing forwards until you add apps."
|
||||
} else {
|
||||
"$selectedCount ${if (selectedCount == 1) "app" else "apps"} allowed to forward."
|
||||
}
|
||||
NotificationPackageFilterMode.Blocklist ->
|
||||
if (selectedCount == 0) {
|
||||
"No apps blocked. Apps can forward unless you add blocks."
|
||||
} else {
|
||||
"$selectedCount ${if (selectedCount == 1) "app" else "apps"} blocked from forwarding."
|
||||
}
|
||||
}
|
||||
|
||||
private fun notificationAppBadge(label: String): String {
|
||||
val initials =
|
||||
label
|
||||
.split(' ', '-', '_', '.')
|
||||
.asSequence()
|
||||
.filter { it.isNotBlank() }
|
||||
.take(2)
|
||||
.mapNotNull { it.firstOrNull()?.uppercaseChar()?.toString() }
|
||||
.joinToString("")
|
||||
return initials.ifBlank { "A" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts cron wake times into short relative labels for scheduled-work rows.
|
||||
*/
|
||||
|
||||
@@ -1222,6 +1222,82 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
||||
}
|
||||
}
|
||||
|
||||
/** App entry shown in the notification-forwarding package picker. */
|
||||
data class InstalledApp(
|
||||
val label: String,
|
||||
val packageName: String,
|
||||
val isSystemApp: Boolean,
|
||||
)
|
||||
|
||||
/** Reads launcher, recent-notification, and configured packages for the picker. */
|
||||
private fun queryInstalledApps(
|
||||
context: Context,
|
||||
configuredPackages: Set<String>,
|
||||
): List<InstalledApp> {
|
||||
val packageManager = context.packageManager
|
||||
val launcherIntent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_LAUNCHER) }
|
||||
|
||||
val launcherPackages =
|
||||
packageManager
|
||||
.queryIntentActivities(launcherIntent, PackageManager.MATCH_ALL)
|
||||
.asSequence()
|
||||
.mapNotNull {
|
||||
it.activityInfo
|
||||
?.packageName
|
||||
?.trim()
|
||||
?.takeIf(String::isNotEmpty)
|
||||
}.toMutableSet()
|
||||
|
||||
val recentNotificationPackages =
|
||||
DeviceNotificationListenerService
|
||||
.recentPackages(context)
|
||||
.asSequence()
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
.toList()
|
||||
|
||||
val candidatePackages =
|
||||
resolveNotificationCandidatePackages(
|
||||
launcherPackages = launcherPackages,
|
||||
recentPackages = recentNotificationPackages,
|
||||
configuredPackages = configuredPackages,
|
||||
appPackageName = context.packageName,
|
||||
)
|
||||
|
||||
return candidatePackages
|
||||
.asSequence()
|
||||
.mapNotNull { packageName ->
|
||||
runCatching {
|
||||
val appInfo = packageManager.getApplicationInfo(packageName, 0)
|
||||
val label = packageManager.getApplicationLabel(appInfo).toString().trim()
|
||||
InstalledApp(
|
||||
label = if (label.isEmpty()) packageName else label,
|
||||
packageName = packageName,
|
||||
isSystemApp = (appInfo.flags and android.content.pm.ApplicationInfo.FLAG_SYSTEM) != 0,
|
||||
)
|
||||
}.getOrNull()
|
||||
}.sortedWith(compareBy<InstalledApp> { it.label.lowercase() }.thenBy { it.packageName })
|
||||
.toList()
|
||||
}
|
||||
|
||||
/** Merges package sources while excluding OpenClaw from its own forwarding filter. */
|
||||
internal fun resolveNotificationCandidatePackages(
|
||||
launcherPackages: Set<String>,
|
||||
recentPackages: List<String>,
|
||||
configuredPackages: Set<String>,
|
||||
appPackageName: String,
|
||||
): Set<String> {
|
||||
val blockedPackage = appPackageName.trim()
|
||||
return sequenceOf(
|
||||
configuredPackages.asSequence(),
|
||||
launcherPackages.asSequence(),
|
||||
recentPackages.asSequence(),
|
||||
).flatten()
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() && it != blockedPackage }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
/** Shared Material text-field colors for the legacy mobile settings sheet. */
|
||||
@Composable
|
||||
private fun settingsTextFieldColors() =
|
||||
|
||||
@@ -32,46 +32,4 @@ class SettingsSheetNotificationAppsTest {
|
||||
|
||||
assertEquals(setOf("com.example.recent", "com.example.configured"), packages)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun filterNotificationAppsForPicker_keepsSelectedSystemPackagesVisible() {
|
||||
val apps =
|
||||
listOf(
|
||||
InstalledApp(label = "Android System", packageName = "android", isSystemApp = true),
|
||||
InstalledApp(label = "Phone Services", packageName = "com.android.phone", isSystemApp = true),
|
||||
InstalledApp(label = "Gmail", packageName = "com.google.android.gm", isSystemApp = false),
|
||||
)
|
||||
|
||||
val filtered =
|
||||
filterNotificationAppsForPicker(
|
||||
apps = apps,
|
||||
selectedPackages = setOf("com.android.phone"),
|
||||
query = "",
|
||||
showSystemApps = false,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
listOf("com.android.phone", "com.google.android.gm"),
|
||||
filtered.map { it.packageName },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun filterNotificationAppsForPicker_matchesLabelsAndPackageNames() {
|
||||
val apps =
|
||||
listOf(
|
||||
InstalledApp(label = "Gmail", packageName = "com.google.android.gm", isSystemApp = false),
|
||||
InstalledApp(label = "Calendar", packageName = "com.google.android.calendar", isSystemApp = false),
|
||||
)
|
||||
|
||||
val filtered =
|
||||
filterNotificationAppsForPicker(
|
||||
apps = apps,
|
||||
selectedPackages = emptySet(),
|
||||
query = "gm",
|
||||
showSystemApps = false,
|
||||
)
|
||||
|
||||
assertEquals(listOf("com.google.android.gm"), filtered.map { it.packageName })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# OpenClaw iOS Changelog
|
||||
|
||||
## 2026.6.1 - 2026-06-01
|
||||
## 2026.5.31 - 2026-05-31
|
||||
|
||||
Maintenance update for the current OpenClaw release.
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Source of truth: apps/ios/version.json
|
||||
// Generated by scripts/ios-sync-versioning.ts.
|
||||
|
||||
OPENCLAW_IOS_VERSION = 2026.6.1
|
||||
OPENCLAW_MARKETING_VERSION = 2026.6.1
|
||||
OPENCLAW_IOS_VERSION = 2026.5.31
|
||||
OPENCLAW_MARKETING_VERSION = 2026.5.31
|
||||
OPENCLAW_BUILD_VERSION = 1
|
||||
|
||||
#include? "../build/Version.xcconfig"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# OpenClaw iOS (Super Alpha)
|
||||
|
||||
This iOS app is super-alpha and internal-use only. It connects to an OpenClaw Gateway as a `role: node` on iPhone and iPad.
|
||||
This iPhone app is super-alpha and internal-use only. It connects to an OpenClaw Gateway as a `role: node`.
|
||||
|
||||
## Distribution Status
|
||||
|
||||
@@ -34,7 +34,7 @@ open OpenClaw.xcodeproj
|
||||
|
||||
3. In Xcode:
|
||||
- Scheme: `OpenClaw`
|
||||
- Destination: connected iPhone or iPad (recommended for real behavior)
|
||||
- Destination: connected iPhone (recommended for real behavior)
|
||||
- Build configuration: `Debug`
|
||||
- Run (`Product` -> `Run`)
|
||||
4. If signing fails on a personal team:
|
||||
@@ -245,13 +245,13 @@ gateway can only send pushes for iOS devices that paired with that gateway.
|
||||
- Pairing via QR or setup code flow (`/pair qr` or `/pair`, then `/pair approve` in Telegram).
|
||||
- Gateway connection via discovery or manual host/port with TLS fingerprint trust prompt.
|
||||
- Chat + Talk surfaces through the operator gateway session.
|
||||
- iOS node commands in foreground: camera snap/clip, canvas present/navigate/eval/snapshot, screen record, location, contacts, calendar, reminders, photos, motion, local notifications.
|
||||
- iPhone node commands in foreground: camera snap/clip, canvas present/navigate/eval/snapshot, screen record, location, contacts, calendar, reminders, photos, motion, local notifications.
|
||||
- Authenticated background `node.presence.alive` beacons that update gateway last-seen metadata when the app moves between foreground and background, without treating suspended sockets as connected.
|
||||
- Share extension deep-link forwarding into the connected gateway session.
|
||||
|
||||
## Computer Use Relationship
|
||||
|
||||
The iOS app is not a Codex Computer Use backend. Computer Use and `cua-driver mcp` are macOS desktop-control paths; iOS exposes device capabilities as OpenClaw node commands through the gateway. Agents can drive the iPhone or iPad canvas, camera, screen, location, voice, and other node capabilities with `node.invoke`, subject to iOS foreground/background limits.
|
||||
The iOS app is not a Codex Computer Use backend. Computer Use and `cua-driver mcp` are macOS desktop-control paths; iOS exposes device capabilities as OpenClaw node commands through the gateway. Agents can drive the iPhone canvas, camera, screen, location, voice, and other node capabilities with `node.invoke`, subject to iOS foreground/background limits.
|
||||
|
||||
## Location Automation Use Case (Testing)
|
||||
|
||||
|
||||
@@ -50,11 +50,6 @@ struct ChatProTab: View {
|
||||
.onChange(of: self.appModel.chatSessionKey) { _, _ in
|
||||
self.syncChatViewModel()
|
||||
}
|
||||
.onChange(of: self.appModel.isOperatorGatewayConnected) { _, connected in
|
||||
guard connected else { return }
|
||||
self.syncChatViewModel()
|
||||
self.viewModel?.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
@@ -156,8 +151,7 @@ struct ChatProTab: View {
|
||||
}
|
||||
|
||||
private var gatewayConnected: Bool {
|
||||
GatewayStatusBuilder.build(appModel: self.appModel) == .connected &&
|
||||
self.appModel.isOperatorGatewayConnected
|
||||
GatewayStatusBuilder.build(appModel: self.appModel) == .connected
|
||||
}
|
||||
|
||||
private var chatUserAccent: Color {
|
||||
|
||||
@@ -45,7 +45,6 @@ struct SettingsProTab: View {
|
||||
@State var gatewayPassword = ""
|
||||
@State var manualGatewayPortText = ""
|
||||
@State var setupStatusText: String?
|
||||
@State var stagedGatewaySetupLink: GatewayConnectDeepLink?
|
||||
@State var pendingManualAuthOverride: GatewayConnectionController.ManualAuthOverride?
|
||||
@State var defaultShareInstruction = ""
|
||||
@State var showGatewayProblemDetails = false
|
||||
@@ -83,7 +82,6 @@ struct SettingsProTab: View {
|
||||
self.previousLocationModeRaw = self.locationModeRaw
|
||||
self.syncSettingsState()
|
||||
self.refreshNotificationSettings()
|
||||
self.applyPendingGatewaySetupLinkIfNeeded()
|
||||
}
|
||||
.onChange(of: self.scenePhase) { _, phase in
|
||||
if phase == .active {
|
||||
@@ -109,17 +107,9 @@ struct SettingsProTab: View {
|
||||
.onChange(of: self.gatewayPassword) { _, newValue in
|
||||
self.persistGatewayPassword(newValue)
|
||||
}
|
||||
.onChange(of: self.setupCode) { _, newValue in
|
||||
if !newValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
self.stagedGatewaySetupLink = nil
|
||||
}
|
||||
}
|
||||
.onChange(of: self.defaultShareInstruction) { _, newValue in
|
||||
ShareToAgentSettings.saveDefaultInstruction(newValue)
|
||||
}
|
||||
.onChange(of: self.appModel.gatewaySetupRequestID) { _, _ in
|
||||
self.applyPendingGatewaySetupLinkIfNeeded()
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: self.$showGatewayProblemDetails) {
|
||||
if let gatewayProblem = self.appModel.lastGatewayProblem {
|
||||
|
||||
@@ -202,29 +202,17 @@ extension SettingsProTab {
|
||||
await self.connectManual()
|
||||
}
|
||||
|
||||
func applyPendingGatewaySetupLinkIfNeeded() {
|
||||
guard let link = self.appModel.consumePendingGatewaySetupLink() else { return }
|
||||
self.setupCode = ""
|
||||
self.setupStatusText = nil
|
||||
self.stagedGatewaySetupLink = link
|
||||
let security = link.tls ? "TLS" : "plain"
|
||||
self.setupStatusText = "Setup link loaded for \(link.host):\(link.port) (\(security)). Tap Connect to apply."
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func applySetupCode() -> Bool {
|
||||
let raw = self.setupCode.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let stagedLink = self.stagedGatewaySetupLink
|
||||
guard !raw.isEmpty || stagedLink != nil else {
|
||||
guard !raw.isEmpty else {
|
||||
self.setupStatusText = "Paste a setup code to continue."
|
||||
return false
|
||||
}
|
||||
|
||||
guard let link = raw.isEmpty ? stagedLink : GatewayConnectDeepLink.fromSetupInput(raw) else {
|
||||
guard let link = GatewayConnectDeepLink.fromSetupInput(raw) else {
|
||||
self.setupStatusText = "Setup code not recognized or uses an insecure ws:// gateway URL."
|
||||
return false
|
||||
}
|
||||
self.stagedGatewaySetupLink = nil
|
||||
self.applyGatewayLink(link)
|
||||
return true
|
||||
}
|
||||
@@ -311,7 +299,7 @@ extension SettingsProTab {
|
||||
let trimmed = host.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return false }
|
||||
if Self.isTailnetHostOrIP(trimmed), !Self.hasTailnetIPv4() {
|
||||
self.setupStatusText = "Tailscale is off on this device. Turn it on, then try again."
|
||||
self.setupStatusText = "Tailscale is off on this iPhone. Turn it on, then try again."
|
||||
return false
|
||||
}
|
||||
self.setupStatusText = "Checking gateway reachability..."
|
||||
@@ -522,15 +510,10 @@ extension SettingsProTab {
|
||||
return gatewayStatus
|
||||
}
|
||||
|
||||
var canApplyGatewaySetup: Bool {
|
||||
!self.setupCode.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
|| self.stagedGatewaySetupLink != nil
|
||||
}
|
||||
|
||||
var tailnetWarningText: String? {
|
||||
let host = self.manualGatewayHost.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !host.isEmpty, Self.isTailnetHostOrIP(host), !Self.hasTailnetIPv4() else { return nil }
|
||||
return "This gateway is on your tailnet. Turn on Tailscale on this device, then tap Connect."
|
||||
return "This gateway is on your tailnet. Turn on Tailscale on this iPhone, then tap Connect."
|
||||
}
|
||||
|
||||
func friendlyGatewayMessage(from raw: String) -> String? {
|
||||
|
||||
@@ -542,7 +542,7 @@ extension SettingsProTab {
|
||||
{
|
||||
Task { await self.applySetupCodeAndConnect() }
|
||||
}
|
||||
.disabled(!self.canApplyGatewaySetup)
|
||||
.disabled(self.setupCode.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
||||
}
|
||||
if let status = self.setupStatusLine {
|
||||
Text(status)
|
||||
|
||||
@@ -111,7 +111,7 @@ struct GatewayProblemBanner: View {
|
||||
case .gateway:
|
||||
"Fix on gateway"
|
||||
case .iphone:
|
||||
"Fix on this device"
|
||||
"Fix on iPhone"
|
||||
case .both:
|
||||
"Check both"
|
||||
case .network:
|
||||
@@ -227,9 +227,9 @@ struct GatewayProblemDetailsSheet: View {
|
||||
case .gateway:
|
||||
"Primary fix: gateway"
|
||||
case .iphone:
|
||||
"Primary fix: this device"
|
||||
"Primary fix: this iPhone"
|
||||
case .both:
|
||||
"Primary fix: check both this device and the gateway"
|
||||
"Primary fix: check both this iPhone and the gateway"
|
||||
case .network:
|
||||
"Primary fix: network or remote access"
|
||||
case .unknown:
|
||||
|
||||
@@ -138,9 +138,7 @@ final class NodeAppModel {
|
||||
var homeCanvasRevision: Int = 0
|
||||
var lastShareEventText: String = "No share events yet."
|
||||
var openChatRequestID: Int = 0
|
||||
var gatewaySetupRequestID: Int = 0
|
||||
private(set) var pendingAgentDeepLinkPrompt: AgentDeepLinkPrompt?
|
||||
private var pendingGatewaySetupLink: GatewayConnectDeepLink?
|
||||
private(set) var pendingExecApprovalPrompt: ExecApprovalPrompt?
|
||||
private(set) var pendingExecApprovalPromptResolving: Bool = false
|
||||
private(set) var pendingExecApprovalPromptErrorText: String?
|
||||
@@ -4136,23 +4134,11 @@ extension NodeAppModel {
|
||||
switch route {
|
||||
case let .agent(link):
|
||||
await self.handleAgentDeepLink(link, originalURL: url)
|
||||
case let .gateway(link):
|
||||
self.stageGatewaySetupLink(link)
|
||||
case .dashboard:
|
||||
case .gateway, .dashboard:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func stageGatewaySetupLink(_ link: GatewayConnectDeepLink) {
|
||||
self.pendingGatewaySetupLink = link
|
||||
self.gatewaySetupRequestID &+= 1
|
||||
}
|
||||
|
||||
func consumePendingGatewaySetupLink() -> GatewayConnectDeepLink? {
|
||||
defer { self.pendingGatewaySetupLink = nil }
|
||||
return self.pendingGatewaySetupLink
|
||||
}
|
||||
|
||||
private func handleAgentDeepLink(_ link: AgentDeepLink, originalURL: URL) async {
|
||||
let message = link.message.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !message.isEmpty else { return }
|
||||
|
||||
@@ -7,7 +7,7 @@ struct OnboardingIntroStep: View {
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
|
||||
Image(systemName: UIDevice.current.userInterfaceIdiom == .pad ? "ipad" : "iphone.gen3")
|
||||
Image(systemName: "iphone.gen3")
|
||||
.font(.system(size: 60, weight: .semibold))
|
||||
.foregroundStyle(.tint)
|
||||
.padding(.bottom, 18)
|
||||
@@ -17,7 +17,7 @@ struct OnboardingIntroStep: View {
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
Text("Turn this device into a secure OpenClaw node for chat, voice, camera, and device tools.")
|
||||
Text("Turn this iPhone into a secure OpenClaw node for chat, voice, camera, and device tools.")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
@@ -114,7 +114,7 @@ struct OnboardingWelcomeStep: View {
|
||||
.foregroundStyle(.secondary)
|
||||
Text("/pair qr")
|
||||
.font(.system(.footnote, design: .monospaced).weight(.semibold))
|
||||
Text("Then scan the QR code here to connect this device.")
|
||||
Text("Then scan the QR code here to connect this iPhone.")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
@@ -669,8 +669,8 @@ extension OpenClawApp {
|
||||
switch route {
|
||||
case .agent, .dashboard:
|
||||
await self.appModel.handleDeepLink(url: url)
|
||||
case let .gateway(link):
|
||||
self.appModel.stageGatewaySetupLink(link)
|
||||
case .gateway:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ struct RootTabs: View {
|
||||
@State private var didAutoOpenSettings: Bool = false
|
||||
@State private var didApplyInitialAppearance: Bool = false
|
||||
@State private var didApplyInitialChatSession: Bool = false
|
||||
@State private var handledGatewaySetupRequestID: Int = 0
|
||||
|
||||
private enum AppTab: Hashable {
|
||||
case control
|
||||
@@ -238,7 +237,6 @@ struct RootTabs: View {
|
||||
.onAppear { self.updateCanvasState() }
|
||||
.onAppear { self.evaluateOnboardingPresentation(force: false) }
|
||||
.onAppear { self.maybeAutoOpenSettings() }
|
||||
.onAppear { self.maybeOpenSettingsForGatewaySetup() }
|
||||
.onAppear { self.maybeShowQuickSetup() }
|
||||
.onAppear { self.applyInitialAppearanceIfNeeded() }
|
||||
.onAppear { self.applyInitialChatSessionIfNeeded() }
|
||||
@@ -298,9 +296,6 @@ struct RootTabs: View {
|
||||
.onChange(of: self.appModel.openChatRequestID) { _, _ in
|
||||
self.selectedTab = .chat
|
||||
}
|
||||
.onChange(of: self.appModel.gatewaySetupRequestID) { _, _ in
|
||||
self.maybeOpenSettingsForGatewaySetup()
|
||||
}
|
||||
}
|
||||
|
||||
private func rootPresentation(_ content: some View) -> some View {
|
||||
@@ -565,16 +560,6 @@ struct RootTabs: View {
|
||||
self.selectedTab = .settings
|
||||
}
|
||||
|
||||
private func maybeOpenSettingsForGatewaySetup() {
|
||||
let requestID = self.appModel.gatewaySetupRequestID
|
||||
guard requestID != 0, requestID != self.handledGatewaySetupRequestID else { return }
|
||||
self.handledGatewaySetupRequestID = requestID
|
||||
self.showOnboarding = false
|
||||
self.presentedSheet = nil
|
||||
self.didAutoOpenSettings = true
|
||||
self.selectedTab = .settings
|
||||
}
|
||||
|
||||
private func applyInitialChatSessionIfNeeded() {
|
||||
guard !self.didApplyInitialChatSession else { return }
|
||||
self.didApplyInitialChatSession = true
|
||||
|
||||
@@ -147,8 +147,8 @@ struct TalkPermissionPromptView: View {
|
||||
case .upgradeRequested:
|
||||
"Approve this request on your gateway. Talk will start automatically when approval lands."
|
||||
default:
|
||||
"This device needs gateway approval before Talk can use realtime voice. Audio will go directly from " +
|
||||
"this device to the voice provider."
|
||||
"This iPhone needs gateway approval before Talk can use realtime voice. Audio will go directly from " +
|
||||
"this phone to the voice provider."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
OpenClaw is a personal AI assistant you run on your own devices.
|
||||
|
||||
Pair this iOS app with your OpenClaw Gateway to use your iPhone or iPad as a secure node for chat, voice, approvals, sharing, and device-aware automation.
|
||||
Pair this iPhone app with your OpenClaw Gateway to use your phone as a secure node for chat, voice, approvals, sharing, and device-aware automation.
|
||||
|
||||
What you can do:
|
||||
- Pair with your private OpenClaw Gateway by QR code or setup code
|
||||
- Chat with your assistant from iPhone or iPad
|
||||
- Chat with your assistant from iPhone
|
||||
- Use realtime Talk mode and push-to-talk
|
||||
- Review Gateway action approvals from your iPhone or iPad
|
||||
- Review Gateway action approvals from your phone
|
||||
- Share text, links, and media directly from iOS into OpenClaw
|
||||
- Enable device capabilities such as camera, screen, location, photos, contacts, calendar, and reminders when you choose
|
||||
- Receive push wakes and node status updates for connected workflows
|
||||
@@ -16,4 +16,4 @@ OpenClaw is local-first: you control your gateway, keys, configuration, and perm
|
||||
Getting started:
|
||||
1) Set up your OpenClaw Gateway
|
||||
2) Open the iOS app and pair with your gateway
|
||||
3) Start using chat, Talk mode, approvals, and automations from your iPhone or iPad
|
||||
3) Start using chat, Talk mode, approvals, and automations from your phone
|
||||
|
||||
@@ -1 +1 @@
|
||||
Pair your iPhone or iPad with your OpenClaw Gateway for chat, realtime voice, approvals, device capabilities, and private automation.
|
||||
Pair your iPhone with your OpenClaw Gateway for chat, realtime voice, approvals, device capabilities, and private automation.
|
||||
|
||||
@@ -97,7 +97,7 @@ targets:
|
||||
DEVELOPMENT_TEAM: "$(OPENCLAW_DEVELOPMENT_TEAM)"
|
||||
PRODUCT_BUNDLE_IDENTIFIER: "$(OPENCLAW_APP_BUNDLE_ID)"
|
||||
PROVISIONING_PROFILE_SPECIFIER: "$(OPENCLAW_APP_PROFILE)"
|
||||
TARGETED_DEVICE_FAMILY: "1,2"
|
||||
TARGETED_DEVICE_FAMILY: "1"
|
||||
SWIFT_VERSION: "6.0"
|
||||
SWIFT_STRICT_CONCURRENCY: complete
|
||||
SUPPORTS_LIVE_ACTIVITIES: YES
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2026.6.1"
|
||||
"version": "2026.5.31"
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.6.1</string>
|
||||
<string>2026.5.31</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2026053100</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
|
||||
@@ -213,7 +213,7 @@ public enum GatewayConnectionProblemMapper {
|
||||
owner: .both,
|
||||
title: authError.titleOverride ?? "Gateway token required",
|
||||
message: authError.userMessageOverride
|
||||
?? "This gateway requires an auth token, but this device did not send one.",
|
||||
?? "This gateway requires an auth token, but this iPhone did not send one.",
|
||||
actionLabel: authError.actionLabel ?? "Open Settings",
|
||||
actionCommand: authError.actionCommand,
|
||||
docsURL: self.docsURL(
|
||||
@@ -229,7 +229,7 @@ public enum GatewayConnectionProblemMapper {
|
||||
owner: .both,
|
||||
title: authError.titleOverride ?? "Gateway token is out of date",
|
||||
message: authError.userMessageOverride
|
||||
?? "The token on this device does not match the gateway token.",
|
||||
?? "The token on this iPhone does not match the gateway token.",
|
||||
actionLabel: authError
|
||||
.actionLabel ?? (authError.canRetryWithDeviceToken ? "Retry once" : "Update gateway token"),
|
||||
actionCommand: authError.actionCommand,
|
||||
@@ -262,7 +262,7 @@ public enum GatewayConnectionProblemMapper {
|
||||
owner: .both,
|
||||
title: authError.titleOverride ?? "Gateway password required",
|
||||
message: authError.userMessageOverride
|
||||
?? "This gateway requires a password, but this device did not send one.",
|
||||
?? "This gateway requires a password, but this iPhone did not send one.",
|
||||
actionLabel: authError.actionLabel ?? "Open Settings",
|
||||
actionCommand: authError.actionCommand,
|
||||
docsURL: self.docsURL(
|
||||
@@ -278,7 +278,7 @@ public enum GatewayConnectionProblemMapper {
|
||||
owner: .both,
|
||||
title: authError.titleOverride ?? "Gateway password is out of date",
|
||||
message: authError.userMessageOverride
|
||||
?? "The saved password on this device does not match the gateway password.",
|
||||
?? "The saved password on this iPhone does not match the gateway password.",
|
||||
actionLabel: authError.actionLabel ?? "Update password",
|
||||
actionCommand: authError.actionCommand,
|
||||
docsURL: self.docsURL(
|
||||
@@ -322,7 +322,7 @@ public enum GatewayConnectionProblemMapper {
|
||||
return self.problem(
|
||||
kind: .deviceTokenMismatch,
|
||||
owner: .both,
|
||||
title: authError.titleOverride ?? "This device's saved device token is no longer valid",
|
||||
title: authError.titleOverride ?? "This iPhone's saved device token is no longer valid",
|
||||
message: authError.userMessageOverride
|
||||
?? "The gateway rejected the stored device token for this role.",
|
||||
actionLabel: authError.actionLabel ?? "Repair pairing",
|
||||
@@ -355,7 +355,7 @@ public enum GatewayConnectionProblemMapper {
|
||||
title: authError.titleOverride ?? "Secure device identity is required",
|
||||
message: authError.userMessageOverride
|
||||
??
|
||||
"This connection must include a signed device identity before the gateway can bind permissions to this device.",
|
||||
"This connection must include a signed device identity before the gateway can bind permissions to this iPhone.",
|
||||
actionLabel: authError.actionLabel ?? "Retry from the app",
|
||||
actionCommand: authError.actionCommand,
|
||||
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/platforms/ios"),
|
||||
@@ -369,7 +369,7 @@ public enum GatewayConnectionProblemMapper {
|
||||
owner: .iphone,
|
||||
title: authError.titleOverride ?? "Secure handshake expired",
|
||||
message: authError.userMessageOverride ?? "The device signature is too old to use.",
|
||||
actionLabel: authError.actionLabel ?? "Check device time",
|
||||
actionLabel: authError.actionLabel ?? "Check iPhone time",
|
||||
actionCommand: authError.actionCommand,
|
||||
docsURL: self.docsURL(
|
||||
authError.docsURLString,
|
||||
@@ -415,8 +415,8 @@ public enum GatewayConnectionProblemMapper {
|
||||
owner: .iphone,
|
||||
title: authError.titleOverride ?? "This device identity could not be verified",
|
||||
message: authError.userMessageOverride
|
||||
?? "The gateway could not verify the identity this device presented.",
|
||||
actionLabel: authError.actionLabel ?? "Re-pair this device",
|
||||
?? "The gateway could not verify the identity this iPhone presented.",
|
||||
actionLabel: authError.actionLabel ?? "Re-pair this iPhone",
|
||||
actionCommand: authError.actionCommand,
|
||||
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
|
||||
requestId: authError.requestId,
|
||||
@@ -429,8 +429,8 @@ public enum GatewayConnectionProblemMapper {
|
||||
owner: .iphone,
|
||||
title: authError.titleOverride ?? "This device identity could not be verified",
|
||||
message: authError.userMessageOverride
|
||||
?? "The gateway could not verify the public key this device presented.",
|
||||
actionLabel: authError.actionLabel ?? "Re-pair this device",
|
||||
?? "The gateway could not verify the public key this iPhone presented.",
|
||||
actionLabel: authError.actionLabel ?? "Re-pair this iPhone",
|
||||
actionCommand: authError.actionCommand,
|
||||
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
|
||||
requestId: authError.requestId,
|
||||
@@ -444,7 +444,7 @@ public enum GatewayConnectionProblemMapper {
|
||||
title: authError.titleOverride ?? "This device identity could not be verified",
|
||||
message: authError.userMessageOverride
|
||||
?? "The gateway rejected the device identity because the device ID did not match.",
|
||||
actionLabel: authError.actionLabel ?? "Re-pair this device",
|
||||
actionLabel: authError.actionLabel ?? "Re-pair this iPhone",
|
||||
actionCommand: authError.actionCommand,
|
||||
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
|
||||
requestId: authError.requestId,
|
||||
@@ -745,7 +745,7 @@ public enum GatewayConnectionProblemMapper {
|
||||
title: authError.titleOverride ?? "Additional approval required",
|
||||
message: authError.userMessageOverride
|
||||
??
|
||||
"This device is already paired, but it is requesting a new role that was not previously approved.",
|
||||
"This iPhone is already paired, but it is requesting a new role that was not previously approved.",
|
||||
actionLabel: authError.actionLabel ?? "Approve on gateway",
|
||||
actionCommand: authError.actionCommand ?? pairingCommand,
|
||||
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
|
||||
@@ -759,7 +759,7 @@ public enum GatewayConnectionProblemMapper {
|
||||
owner: .gateway,
|
||||
title: authError.titleOverride ?? "Additional permissions required",
|
||||
message: authError.userMessageOverride
|
||||
?? "This device is already paired, but it is requesting new permissions that require approval.",
|
||||
?? "This iPhone is already paired, but it is requesting new permissions that require approval.",
|
||||
actionLabel: authError.actionLabel ?? "Approve on gateway",
|
||||
actionCommand: authError.actionCommand ?? pairingCommand,
|
||||
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
|
||||
@@ -786,7 +786,7 @@ public enum GatewayConnectionProblemMapper {
|
||||
return self.problem(
|
||||
kind: .pairingRequired,
|
||||
owner: .gateway,
|
||||
title: authError.titleOverride ?? "This device is not approved yet",
|
||||
title: authError.titleOverride ?? "This iPhone is not approved yet",
|
||||
message: authError.userMessageOverride
|
||||
?? "The gateway received the connection request, but this device must be approved first.",
|
||||
actionLabel: authError.actionLabel ?? "Approve on gateway",
|
||||
|
||||
@@ -5528,7 +5528,6 @@ public struct SkillsProposalRecordResult: Codable, Sendable {
|
||||
public let createdat: String
|
||||
public let updatedat: String
|
||||
public let createdby: AnyCodable
|
||||
public let origin: [String: AnyCodable]?
|
||||
public let proposedversion: String
|
||||
public let draftfile: String
|
||||
public let drafthash: String
|
||||
@@ -5553,7 +5552,6 @@ public struct SkillsProposalRecordResult: Codable, Sendable {
|
||||
createdat: String,
|
||||
updatedat: String,
|
||||
createdby: AnyCodable,
|
||||
origin: [String: AnyCodable]?,
|
||||
proposedversion: String,
|
||||
draftfile: String,
|
||||
drafthash: String,
|
||||
@@ -5577,7 +5575,6 @@ public struct SkillsProposalRecordResult: Codable, Sendable {
|
||||
self.createdat = createdat
|
||||
self.updatedat = updatedat
|
||||
self.createdby = createdby
|
||||
self.origin = origin
|
||||
self.proposedversion = proposedversion
|
||||
self.draftfile = draftfile
|
||||
self.drafthash = drafthash
|
||||
@@ -5603,7 +5600,6 @@ public struct SkillsProposalRecordResult: Codable, Sendable {
|
||||
case createdat = "createdAt"
|
||||
case updatedat = "updatedAt"
|
||||
case createdby = "createdBy"
|
||||
case origin
|
||||
case proposedversion = "proposedVersion"
|
||||
case draftfile = "draftFile"
|
||||
case drafthash = "draftHash"
|
||||
|
||||
@@ -165,8 +165,6 @@ const config = {
|
||||
"vite.config.ts!",
|
||||
"vitest*.ts!",
|
||||
],
|
||||
// Workboard lazy-loads Three.js at runtime; Knip's dependency pass misses it.
|
||||
ignoreDependencies: ["three"],
|
||||
project: ["src/**/*.{ts,tsx}!"],
|
||||
},
|
||||
"packages/sdk": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
cc0fb4e3f1a7e8f233626adb80d686608ddac8c177fe6a55b33970c2baf4ace4 config-baseline.json
|
||||
042ca98e6200a365accda00e5a6f3e72bdae5853f39ff0cdc3b2cb9c0d6f8f3e config-baseline.core.json
|
||||
cbf81829dcc8cfd0a16435912da709f8c1d508707385b6493f94cafe211ec67c config-baseline.channel.json
|
||||
4012b1f8de6f9527c47320a6c7120f30dc30ac1b5524ed63dadef890aad44b20 config-baseline.plugin.json
|
||||
f4a00ada9d154a4d3a54e109aa6e9f73f22b09d7df9ab6745e87f88724eec06b config-baseline.json
|
||||
5ee177382cf32c2816dca0a4e67cd6c01df1045d600b21a6e9c11639ddb10ce8 config-baseline.core.json
|
||||
0e654bad3f1ef9100f76e512c4453c1f26b6bc1f5ee121ce505d0624a1dad4cd config-baseline.channel.json
|
||||
e6a1d6f51f0d9c04bd92d51deebfaca8c7917dd28d7998d225c0074e0a095348 config-baseline.plugin.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
bdcf661ec680f79819096950295bdb04805aac9639477058d8855f294f6d8034 plugin-sdk-api-baseline.json
|
||||
6b8c92cc5a9277f90973370102fa31efb23ffd93008c3ed961d38e4a8a3073b0 plugin-sdk-api-baseline.jsonl
|
||||
f1da4b7930475e4be33cb05b8c239f728c7338eb1e8df9b7905bbae94d62da9e plugin-sdk-api-baseline.json
|
||||
6fd007eede80893680d65c6f245eafb9e6301a1e4306530b0134fd5b3da0cddb plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -65,7 +65,7 @@ Use this checklist when you already know your old BlueBubbles config and want th
|
||||
imsg rpc --help
|
||||
```
|
||||
|
||||
Replace `42` with a real chat id from `imsg chats`. Sending requires Automation permission for Messages.app. If OpenClaw will run through SSH, run these commands through the same SSH wrapper or user context that OpenClaw will use. If reads/probes work but sends fail with AppleEvents `-1743`, check whether Automation landed on `/usr/libexec/sshd-keygen-wrapper`; see [SSH wrapper sends fail with AppleEvents -1743](/channels/imessage#ssh-wrapper-sends-fail-with-appleevents-1743).
|
||||
Replace `42` with a real chat id from `imsg chats`. Sending requires Automation permission for Messages.app. If OpenClaw will run through SSH, run these commands through the same SSH wrapper or user context that OpenClaw will use.
|
||||
|
||||
3. Enable the private API bridge when you need advanced actions:
|
||||
|
||||
@@ -248,7 +248,7 @@ iMessage catchup is now available as an opt-in feature on the bundled plugin. On
|
||||
|
||||
There is no supported BlueBubbles runtime to switch back to. If iMessage verification fails, set `channels.imessage.enabled: false`, restart the Gateway, fix the `imsg` blocker, and retry the cutover.
|
||||
|
||||
The reply cache lives in SQLite plugin state. `openclaw doctor --fix` imports and archives the old `imessage/reply-cache.jsonl` sidecar when present.
|
||||
The reply cache lives at `~/.openclaw/state/imessage/reply-cache.jsonl` (mode `0600`, parent dir `0700`). It is safe to delete if you want a clean slate.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -151,29 +151,6 @@ imsg send <handle> "test"
|
||||
|
||||
</Tip>
|
||||
|
||||
<Accordion title="SSH wrapper sends fail with AppleEvents -1743">
|
||||
A remote-SSH setup can read chats, pass `channels status --probe`, and process inbound messages while outbound sends still fail with an AppleEvents authorization error:
|
||||
|
||||
```text
|
||||
Not authorized to send Apple events to Messages. (-1743)
|
||||
```
|
||||
|
||||
Check the signed-in Mac user's TCC database or System Settings > Privacy & Security > Automation. If the Automation entry is recorded for `/usr/libexec/sshd-keygen-wrapper` instead of the `imsg` or local shell process, macOS may not expose a usable Messages toggle for that SSH server-side client:
|
||||
|
||||
```text
|
||||
kTCCServiceAppleEvents | /usr/libexec/sshd-keygen-wrapper | auth_value=0 | com.apple.MobileSMS
|
||||
```
|
||||
|
||||
In that state, repeating `tccutil reset AppleEvents` or rerunning `imsg send` through the same SSH wrapper may keep failing because the process context that needs Messages Automation is the SSH wrapper, not an app the UI can grant.
|
||||
|
||||
Use one of the supported `imsg` process contexts instead:
|
||||
|
||||
- Run the Gateway, or at least the `imsg` bridge, in the logged-in Messages user's local session.
|
||||
- Start the Gateway with a LaunchAgent for that user after granting Full Disk Access and Automation from the same session.
|
||||
- If you keep the two-user SSH topology, verify that a real outbound `imsg send` succeeds through the exact wrapper before enabling the channel. If it cannot be granted Automation, reconfigure to a single-user `imsg` setup instead of relying on the SSH wrapper for sends.
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Enabling the imsg private API
|
||||
|
||||
`imsg` ships in two operational modes:
|
||||
@@ -556,7 +533,7 @@ When `imsg launch` is running and `openclaw channels status --probe` reports `pr
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Message IDs">
|
||||
Inbound iMessage context includes both short `MessageSid` values and full message GUIDs when available. Short IDs are scoped to the recent SQLite-backed reply cache and are checked against the current chat before use. If a short ID has expired or belongs to another chat, retry with the full `MessageSidFull`.
|
||||
Inbound iMessage context includes both short `MessageSid` values and full message GUIDs when available. Short IDs are scoped to the recent in-memory reply cache and are checked against the current chat before use. If a short ID has expired or belongs to another chat, retry with the full `MessageSidFull`.
|
||||
|
||||
</Accordion>
|
||||
|
||||
@@ -737,7 +714,7 @@ Each replayed row is fed through the live dispatch path (`evaluateIMessageInboun
|
||||
|
||||
### Cursor and retry semantics
|
||||
|
||||
Catchup keeps a per-account cursor in SQLite plugin state:
|
||||
Catchup keeps a per-account cursor at `<openclawStateDir>/imessage/catchup/<account>__<hash>.json` (the OpenClaw state dir defaults to `~/.openclaw`, overridable with `OPENCLAW_STATE_DIR`):
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -752,7 +729,6 @@ Catchup keeps a per-account cursor in SQLite plugin state:
|
||||
- After the startup catchup query succeeds, later live-handled rows also advance the same cursor so a gateway restart does not replay messages that were already handled live. Live cursor writes do not jump past catchup failures that are still below `maxFailureRetries`.
|
||||
- After `maxFailureRetries` consecutive throws against the same `guid`, catchup logs a `warn` and force-advances the cursor past the wedged message so subsequent startups can make progress.
|
||||
- Already-given-up guids are skipped on sight (no dispatch attempt) on later runs and counted under `skippedGivenUp` in the run summary.
|
||||
- `openclaw doctor --fix` imports legacy `<openclawStateDir>/imessage/catchup/*.json` cursor files into SQLite plugin state and archives the old files.
|
||||
|
||||
### Operator-visible signals
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ OpenClaw CI runs on every push to `main` and every pull request. The `preflight`
|
||||
| Job | Purpose | When it runs |
|
||||
| ---------------------------------- | --------------------------------------------------------------------------------------------------------- | ---------------------------------- |
|
||||
| `preflight` | Detect docs-only changes, changed scopes, changed extensions, and build the CI manifest | Always on non-draft pushes and PRs |
|
||||
| `security-fast` | Private key detection, changed-workflow audit via `zizmor`, and production lockfile audit | Always on non-draft pushes and PRs |
|
||||
| `security-fast` | Private key detection, workflow audit via `zizmor`, and production lockfile audit | Always on non-draft pushes and PRs |
|
||||
| `check-dependencies` | Production Knip dependency-only pass plus the unused-file allowlist guard | Node-relevant changes |
|
||||
| `build-artifacts` | Build `dist/`, Control UI, built-CLI smoke checks, embedded built-artifact checks, and reusable artifacts | Node-relevant changes |
|
||||
| `checks-fast-core` | Fast Linux correctness lanes such as bundled, protocol, and CI-routing checks | Node-relevant changes |
|
||||
@@ -80,7 +80,6 @@ apply to that PR.
|
||||
Scope logic lives in `scripts/ci-changed-scope.mjs` and is covered by unit tests in `src/scripts/ci-changed-scope.test.ts`. Manual dispatch skips changed-scope detection and makes the preflight manifest act as if every scoped area changed.
|
||||
|
||||
- **CI workflow edits** validate the Node CI graph plus workflow linting, but do not force Windows, Android, or macOS native builds by themselves; those platform lanes stay scoped to platform source changes.
|
||||
- **Workflow Sanity** runs `actionlint`, `zizmor` over all workflow YAML files, the composite-action interpolation guard, and the conflict-marker guard. The PR-scoped `security-fast` job also runs `zizmor` over changed workflow files so workflow security findings fail early in the main CI graph.
|
||||
- **Docs on `main` pushes** are checked by the standalone `Docs` workflow with the same ClawHub docs mirror used by CI, so mixed code+docs pushes do not also queue the CI `check-docs` shard. Pull requests and manual CI still run `check-docs` from CI when docs changed.
|
||||
- **TUI PTY** is a focused workflow for TUI changes. It runs `node scripts/run-vitest.mjs run --config test/vitest/vitest.tui-pty.config.ts` on Linux Node 24 for `src/tui/**`, the watch harness, package script, lockfile, and workflow edits. The required lane uses a deterministic `TuiBackend` fixture; the slower `tui --local` smoke is opt-in with `OPENCLAW_TUI_PTY_INCLUDE_LOCAL=1` and mocks only the external model endpoint.
|
||||
- **CI routing-only edits, selected cheap core-test fixture edits, and narrow plugin contract helper/test-routing edits** use a fast Node-only manifest path: `preflight`, security, and a single `checks-fast-core` task. That path skips build artifacts, Node 22 compatibility, channel contracts, full core shards, bundled-plugin shards, and additional guard matrices when the change is limited to the routing or helper surfaces the fast task exercises directly.
|
||||
|
||||
@@ -45,8 +45,6 @@ openclaw doctor --deep
|
||||
openclaw doctor --fix
|
||||
openclaw doctor --fix --non-interactive
|
||||
openclaw doctor --generate-gateway-token
|
||||
openclaw doctor --post-upgrade
|
||||
openclaw doctor --post-upgrade --json
|
||||
```
|
||||
|
||||
For channel-specific permissions, use the channel probes instead of `doctor`:
|
||||
@@ -70,8 +68,7 @@ The targeted Discord capabilities probe reports the bot's effective channel perm
|
||||
- `--allow-exec`: allow doctor to execute configured exec SecretRefs while verifying secrets
|
||||
- `--deep`: scan system services for extra gateway installs and report recent Gateway supervisor restart handoffs
|
||||
- `--lint`: run modernized health checks in read-only mode and emit diagnostic findings
|
||||
- `--post-upgrade`: run post-upgrade plugin compatibility probes; emits findings to stdout; exits with code 1 if any error-level findings are present
|
||||
- `--json`: with `--lint`, emit JSON findings instead of human output; with `--post-upgrade`, emit a machine-readable JSON envelope (`{ probesRun, findings }`)
|
||||
- `--json`: with `--lint`, emit JSON findings instead of human output
|
||||
- `--severity-min <level>`: with `--lint`, drop findings below `info`, `warning`, or `error`
|
||||
- `--skip <id>`: with `--lint`, skip a check id; repeat to skip more than one
|
||||
- `--only <id>`: with `--lint`, run only a check id; repeat to run a small selected set
|
||||
@@ -191,16 +188,6 @@ id is not registered, no check runs for that id; use the command's `checksRun`
|
||||
and `checksSkipped` fields to verify a focused gate is selecting the checks you
|
||||
expect.
|
||||
|
||||
## Post-upgrade mode
|
||||
|
||||
`openclaw doctor --post-upgrade` runs plugin compatibility probes intended to be
|
||||
chained after a build or upgrade. Findings are emitted to stdout; the command
|
||||
exits with code 1 if any finding has `level: "error"`. Add `--json` to receive a
|
||||
machine-readable envelope (`{ probesRun, findings }`) suitable for CI, the
|
||||
community `fork-upgrade` skill, and other post-upgrade smoke tooling. If the
|
||||
installed plugin index is missing or malformed, JSON mode still emits that
|
||||
envelope with a `plugin.index_unavailable` error finding.
|
||||
|
||||
Notes:
|
||||
|
||||
- In Nix mode (`OPENCLAW_NIX_MODE=1`), read-only doctor checks still work, but `doctor --fix`, `doctor --repair`, `doctor --yes`, and `doctor --generate-gateway-token` are disabled because `openclaw.json` is immutable. Edit the Nix source for this install instead; for nix-openclaw, use the agent-first [Quick Start](https://github.com/openclaw/nix-openclaw#quick-start).
|
||||
|
||||
@@ -329,19 +329,6 @@ openclaw plugins install -l ./my-plugin
|
||||
Standalone plugin files must be listed in `plugins.load.paths` rather than placed directly in `~/.openclaw/extensions` or `<workspace>/.openclaw/extensions`. Those auto-discovered roots load plugin package or bundle directories, while top-level script files are treated as local helpers and skipped.
|
||||
|
||||
<Note>
|
||||
Workspace-origin plugins discovered from a workspace extensions root are not
|
||||
imported or executed until they are explicitly enabled. For local development,
|
||||
run `openclaw plugins enable <plugin-id>` or set
|
||||
`plugins.entries.<plugin-id>.enabled: true`; if your config uses
|
||||
`plugins.allow`, include the same plugin id there too. This fail-closed rule
|
||||
also applies when channel setup explicitly targets a workspace-origin plugin for
|
||||
setup-only loading, so local channel plugin setup code will not run while that
|
||||
workspace plugin remains disabled or excluded from the allowlist. Linked installs
|
||||
and explicit `plugins.load.paths` entries follow the normal policy for their
|
||||
resolved plugin origin. See
|
||||
[Configure plugin policy](/tools/plugin#configure-plugin-policy)
|
||||
and [Configuration reference](/gateway/configuration-reference#plugins).
|
||||
|
||||
`--force` is not supported with `--link` because linked installs reuse the source path instead of copying over a managed install target.
|
||||
|
||||
Use `--pin` on npm installs to save the resolved exact spec (`name@version`) in the managed plugin index while keeping the default behavior unpinned.
|
||||
@@ -349,7 +336,7 @@ Use `--pin` on npm installs to save the resolved exact spec (`name@version`) in
|
||||
|
||||
### Plugin index
|
||||
|
||||
Plugin install metadata is machine-managed state, not user config. Installs and updates write it to the shared SQLite state database under the active OpenClaw state directory. The `installed_plugin_index` row stores durable `installRecords` metadata, including records for broken or missing plugin manifests, plus a manifest-derived cold registry cache used by `openclaw plugins update`, uninstall, diagnostics, and the cold plugin registry.
|
||||
Plugin install metadata is machine-managed state, not user config. Installs and updates write it to `plugins/installs.json` under the active OpenClaw state directory. Its top-level `installRecords` map is the durable source of install metadata, including records for broken or missing plugin manifests. The `plugins` array is the manifest-derived cold registry cache. The file includes a do-not-edit warning and is used by `openclaw plugins update`, uninstall, diagnostics, and the cold plugin registry.
|
||||
|
||||
When OpenClaw sees shipped legacy `plugins.installs` records in config, runtime reads treat them as compatibility input without rewriting `openclaw.json`. Explicit plugin writes and `openclaw doctor --fix` move those records into the plugin index and remove the config key when config writes are allowed; if either write fails, the config records are kept so the install metadata is not lost.
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ Initialize the baseline config and agent workspace. With any onboarding flag pre
|
||||
| `--workspace <dir>` | Agent workspace directory (default `~/.openclaw/workspace`; stored as `agents.defaults.workspace`). |
|
||||
| `--wizard` | Run interactive onboarding. |
|
||||
| `--non-interactive` | Run onboarding without prompts. |
|
||||
| `--accept-risk` | Acknowledge full-system agent access risk; required with `--non-interactive`. |
|
||||
| `--mode <mode>` | Onboarding mode: `local` or `remote`. |
|
||||
| `--import-from <provider>` | Migration provider to run during onboarding. |
|
||||
| `--import-source <path>` | Source agent home for `--import-from`. |
|
||||
@@ -34,7 +33,7 @@ Initialize the baseline config and agent workspace. With any onboarding flag pre
|
||||
|
||||
`openclaw setup` runs the wizard when any of these flags are explicitly present, even without `--wizard`:
|
||||
|
||||
`--wizard`, `--non-interactive`, `--accept-risk`, `--mode`, `--import-from`, `--import-source`, `--import-secrets`, `--remote-url`, `--remote-token`.
|
||||
`--wizard`, `--non-interactive`, `--mode`, `--import-from`, `--import-source`, `--import-secrets`, `--remote-url`, `--remote-token`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -43,7 +42,7 @@ openclaw setup
|
||||
openclaw setup --workspace ~/.openclaw/workspace
|
||||
openclaw setup --wizard
|
||||
openclaw setup --wizard --import-from hermes --import-source ~/.hermes
|
||||
openclaw setup --non-interactive --accept-risk --mode remote --remote-url wss://gateway-host:18789 --remote-token <token>
|
||||
openclaw setup --non-interactive --mode remote --remote-url wss://gateway-host:18789 --remote-token <token>
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
@@ -35,7 +35,6 @@ openclaw uninstall --dry-run
|
||||
Notes:
|
||||
|
||||
- Run `openclaw backup create` first if you want a restorable snapshot before removing state or workspaces.
|
||||
- `--state` preserves configured workspace directories unless `--workspace` is also selected.
|
||||
- `--all` is shorthand for removing service, state, workspace, and app together.
|
||||
- `--non-interactive` requires `--yes`.
|
||||
|
||||
|
||||
@@ -99,10 +99,7 @@ openclaw workboard dispatch --url http://127.0.0.1:18789 --token "$OPENCLAW_GATE
|
||||
|
||||
`dispatch` first calls the running Gateway RPC method
|
||||
`workboard.cards.dispatch`. That path uses the same subagent runtime as the
|
||||
dashboard dispatch action, so ready cards become task-tracked worker runs with
|
||||
linked session keys. Cards with an assigned agent use agent-scoped subagent
|
||||
session keys; unassigned cards keep an unscoped subagent key so the Gateway's
|
||||
configured default agent is preserved.
|
||||
dashboard dispatch action, so ready cards can become real worker sessions.
|
||||
|
||||
The dispatch loop:
|
||||
|
||||
@@ -113,8 +110,8 @@ The dispatch loop:
|
||||
5. Claims each selected card for the dispatcher or assigned agent.
|
||||
6. Starts a subagent worker run with bounded card context and the card claim
|
||||
token.
|
||||
7. Stores the worker run id, session key, task linkage when the Gateway task
|
||||
ledger reports it, execution status, and worker log on the card.
|
||||
7. Stores the worker run id, session key, execution status, and worker log on
|
||||
the card.
|
||||
|
||||
Selection is intentionally conservative. One dispatch starts at most three
|
||||
workers by default, skips archived or already-claimed cards, and starts only one
|
||||
@@ -149,10 +146,6 @@ JSON output includes the dispatch result. Gateway-backed dispatch can include
|
||||
`started` and `startFailures`; data-only fallback includes
|
||||
`gatewayUnavailable: true`. Claim tokens are redacted from card JSON output.
|
||||
|
||||
In the dashboard, the same dispatch result is shown as a short summary so an
|
||||
operator can see how many cards started, promoted, blocked, reclaimed, or
|
||||
failed without opening card details.
|
||||
|
||||
## Slash Command Parity
|
||||
|
||||
Command-capable channels can use the matching slash command:
|
||||
|
||||
@@ -99,7 +99,7 @@ These are the standard files OpenClaw expects inside the workspace:
|
||||
</AccordionGroup>
|
||||
|
||||
<Note>
|
||||
If any bootstrap file is missing, OpenClaw injects a "missing file" marker into the session and continues. Large bootstrap files are truncated when injected; adjust limits with `agents.defaults.bootstrapMaxChars` (default: 20000) and `agents.defaults.bootstrapTotalMaxChars` (default: 60000). `openclaw setup` can recreate missing defaults without overwriting existing files.
|
||||
If any bootstrap file is missing, OpenClaw injects a "missing file" marker into the session and continues. Large bootstrap files are truncated when injected; adjust limits with `agents.defaults.bootstrapMaxChars` (default: 12000) and `agents.defaults.bootstrapTotalMaxChars` (default: 60000). `openclaw setup` can recreate missing defaults without overwriting existing files.
|
||||
</Note>
|
||||
|
||||
## What is NOT in the workspace
|
||||
|
||||
@@ -41,8 +41,6 @@ If a file is missing, OpenClaw injects a single "missing file" marker line (and
|
||||
|
||||
`BOOTSTRAP.md` is only created for a **brand new workspace** (no other bootstrap files present). While it is pending, OpenClaw keeps it in Project Context and adds system-prompt bootstrap guidance for the initial ritual instead of copying it into the user message. If you delete it after completing the ritual, it should not be recreated on later restarts.
|
||||
|
||||
After a workspace has been observed, OpenClaw also keeps a state-dir attestation marker for the workspace path. If a recently attested workspace disappears or is wiped, startup refuses to silently re-seed `BOOTSTRAP.md`; restore the workspace or use a full onboard reset so the workspace and marker are cleared together.
|
||||
|
||||
To disable bootstrap file creation entirely (for pre-seeded workspaces), set:
|
||||
|
||||
```json5
|
||||
|
||||
@@ -91,7 +91,7 @@ For the bundled non-ACP Codex harness, OpenClaw applies the same lifecycle by pr
|
||||
OpenClaw calls two optional subagent lifecycle hooks:
|
||||
|
||||
<ParamField path="prepareSubagentSpawn" type="method">
|
||||
Prepare shared context state before a child run starts. The hook receives parent/child session keys, `contextMode` (`isolated` or `fork`), available transcript ids/files, and optional TTL. If it returns a rollback handle, OpenClaw calls it when spawn fails after preparation succeeds. Native subagent spawns that request `lightContext` and resolve to `contextMode="isolated"` intentionally skip this hook so the child starts from the lightweight bootstrap context without context-engine-managed pre-spawn state.
|
||||
Prepare shared context state before a child run starts. The hook receives parent/child session keys, `contextMode` (`isolated` or `fork`), available transcript ids/files, and optional TTL. If it returns a rollback handle, OpenClaw calls it when spawn fails after preparation succeeds.
|
||||
</ParamField>
|
||||
<ParamField path="onSubagentEnded" type="method">
|
||||
Clean up when a subagent session completes or is swept.
|
||||
|
||||
@@ -122,7 +122,7 @@ By default, OpenClaw injects a fixed set of workspace files (if present):
|
||||
- `HEARTBEAT.md`
|
||||
- `BOOTSTRAP.md` (first-run only)
|
||||
|
||||
Large files are truncated per-file using `agents.defaults.bootstrapMaxChars` (default `20000` chars). OpenClaw also enforces a total bootstrap injection cap across files with `agents.defaults.bootstrapTotalMaxChars` (default `60000` chars). `/context` shows **raw vs injected** sizes and whether truncation happened.
|
||||
Large files are truncated per-file using `agents.defaults.bootstrapMaxChars` (default `12000` chars). OpenClaw also enforces a total bootstrap injection cap across files with `agents.defaults.bootstrapTotalMaxChars` (default `60000` chars). `/context` shows **raw vs injected** sizes and whether truncation happened.
|
||||
|
||||
When truncation occurs, the runtime can inject an in-prompt warning block under Project Context. Configure this with `agents.defaults.bootstrapPromptTruncationWarning` (`off`, `once`, `always`; default `always`).
|
||||
|
||||
|
||||
@@ -107,13 +107,6 @@ Deep ranking uses six weighted base signals plus phase reinforcement:
|
||||
|
||||
Light and REM phase hits add a small recency-decayed boost from `memory/.dreams/phase-signals.json`.
|
||||
|
||||
Shadow-trial results can be layered on top of that base score as a review
|
||||
signal before any durable write. A helpful trial gives the candidate a small
|
||||
bounded boost, a neutral trial keeps it deferred, and a harmful trial marks it
|
||||
as rejected for that scoring pass. This signal is still report-only: it can
|
||||
change candidate ordering or review metadata, but it does not write to
|
||||
`MEMORY.md` or promote the candidate by itself.
|
||||
|
||||
## QA shadow trial report coverage
|
||||
|
||||
QA Lab includes a report-only scenario for exploring how a future dreaming
|
||||
|
||||
@@ -67,27 +67,6 @@ OpenClaw separates the selected provider/model from why it was selected. That so
|
||||
|
||||
The auto fallback primary-probe interval is five minutes and is not configurable. OpenClaw remembers recent probes per session and primary model so a failing primary is not retried on every turn. OpenClaw sends a visible notice when a session moves onto fallback and another notice when it returns to the selected primary; it does not repeat the notice on every sticky fallback turn.
|
||||
|
||||
## Auth failure skip cache
|
||||
|
||||
By default, every new turn keeps the existing fallback retry behavior: OpenClaw
|
||||
will try each configured fallback candidate again, including non-primary
|
||||
candidates that recently failed with `auth` or `auth_permanent`.
|
||||
|
||||
Operators who prefer to suppress those repeat auth failures can opt in with:
|
||||
|
||||
```bash
|
||||
OPENCLAW_FALLBACK_SKIP_TTL_MS=60000
|
||||
```
|
||||
|
||||
When enabled, OpenClaw records an in-memory, session-scoped skip marker for a
|
||||
non-primary fallback candidate after an auth-class failure. The marker is keyed
|
||||
by session id, provider, and model. Primary candidates are never skipped, so an
|
||||
explicit user model selection still surfaces the real auth error. The cache is
|
||||
process-local and clears on Gateway restart.
|
||||
|
||||
The value is a TTL in milliseconds. `0` or an unset value disables the cache.
|
||||
Positive values are clamped between 1 second and 10 minutes.
|
||||
|
||||
## User-visible fallback notices
|
||||
|
||||
When a session moves onto an auto-selected fallback, OpenClaw sends a status notice in the same reply surface:
|
||||
|
||||
@@ -303,7 +303,7 @@ See [/providers/kilocode](/providers/kilocode) for setup details.
|
||||
| Hugging Face Inference | `huggingface` | `HUGGINGFACE_HUB_TOKEN` or `HF_TOKEN` | `huggingface/deepseek-ai/DeepSeek-R1` |
|
||||
| Kilo Gateway | `kilocode` | `KILOCODE_API_KEY` | `kilocode/kilo/auto` |
|
||||
| Kimi Coding | `kimi` | `KIMI_API_KEY` or `KIMICODE_API_KEY` | `kimi/kimi-for-coding` |
|
||||
| MiniMax | `minimax` / `minimax-portal` | `MINIMAX_API_KEY` / `MINIMAX_OAUTH_TOKEN` | `minimax/MiniMax-M3` |
|
||||
| MiniMax | `minimax` / `minimax-portal` | `MINIMAX_API_KEY` / `MINIMAX_OAUTH_TOKEN` | `minimax/MiniMax-M2.7` |
|
||||
| Mistral | `mistral` | `MISTRAL_API_KEY` | `mistral/mistral-large-latest` |
|
||||
| Moonshot | `moonshot` | `MOONSHOT_API_KEY` | `moonshot/kimi-k2.6` |
|
||||
| NVIDIA | `nvidia` | `NVIDIA_API_KEY` | `nvidia/nvidia/nemotron-3-super-120b-a12b` |
|
||||
@@ -331,7 +331,7 @@ See [/providers/kilocode](/providers/kilocode) for setup details.
|
||||
Gemini-backed refs follow the same proxy-Gemini sanitation path; `kilocode/kilo/auto` and other proxy-reasoning-unsupported refs skip proxy reasoning injection.
|
||||
</Accordion>
|
||||
<Accordion title="MiniMax">
|
||||
API-key onboarding writes explicit M3 and M2.7 chat model definitions; image understanding stays on the plugin-owned `MiniMax-VL-01` media provider.
|
||||
API-key onboarding writes explicit text-only M2.7 chat model definitions; image understanding stays on the plugin-owned `MiniMax-VL-01` media provider.
|
||||
</Accordion>
|
||||
<Accordion title="NVIDIA">
|
||||
Model ids use a `nvidia/<vendor>/<model>` namespace (for example `nvidia/nvidia/nemotron-...` alongside `nvidia/moonshotai/kimi-k2.5`); pickers preserve the literal `<provider>/<model-id>` composition while the canonical key sent to the API stays single-prefixed.
|
||||
@@ -537,7 +537,7 @@ On MiniMax's Anthropic-compatible streaming path, OpenClaw disables thinking by
|
||||
|
||||
Plugin-owned capability split:
|
||||
|
||||
- Text/chat defaults stay on `minimax/MiniMax-M3`
|
||||
- Text/chat defaults stay on `minimax/MiniMax-M2.7`
|
||||
- Image generation is `minimax/image-01` or `minimax-portal/image-01`
|
||||
- Image understanding is plugin-owned `MiniMax-VL-01` on both MiniMax auth paths
|
||||
- Web search stays on provider id `minimax`
|
||||
|
||||
@@ -120,19 +120,6 @@ sparse token/cache counters from the latest transcript usage entry, and
|
||||
the caller's current session; visible client labels such as `openclaw-tui` are
|
||||
not session keys.
|
||||
|
||||
When route metadata is available, `session_status` also includes a visible
|
||||
`Route context` JSON block and matching structured `details` fields. These
|
||||
fields disambiguate the session key from the route that is currently handling
|
||||
the live run:
|
||||
|
||||
- `origin` is where the session was created, or the provider inferred from a
|
||||
deliverable session-key prefix when older state lacks stored origin metadata.
|
||||
- `active` is the current live-run route. It is only reported for the live or
|
||||
current session being handled now.
|
||||
- `deliveryContext` is the persisted delivery route stored on the session,
|
||||
which OpenClaw can reuse for later delivery even when the active surface
|
||||
differs.
|
||||
|
||||
`sessions_yield` intentionally ends the current turn so the next message can be
|
||||
the follow-up event you are waiting for. Use it after spawning sub-agents when
|
||||
you want completion results to arrive as the next message instead of building
|
||||
|
||||
@@ -208,7 +208,7 @@ because of the bootstrap file limits below.
|
||||
</Note>
|
||||
|
||||
Large files are truncated with a marker. The max per-file size is controlled by
|
||||
`agents.defaults.bootstrapMaxChars` (default: 20000). Total injected bootstrap
|
||||
`agents.defaults.bootstrapMaxChars` (default: 12000). Total injected bootstrap
|
||||
content across files is capped by `agents.defaults.bootstrapTotalMaxChars`
|
||||
(default: 60000). Missing files inject a short missing-file marker. When truncation
|
||||
occurs, OpenClaw can inject a concise system-prompt warning notice; control this with
|
||||
|
||||
@@ -206,17 +206,6 @@ openclaw models auth login --provider openai --profile-id openai:lain
|
||||
This is the easiest way to keep multiple OAuth logins for the same provider
|
||||
separate inside one agent.
|
||||
|
||||
Use `--force` when a saved provider profile is stuck, expired, or tied to the
|
||||
wrong account and the normal login command keeps reusing it. `--force` deletes
|
||||
the saved auth profiles for that provider in the selected agent directory, then
|
||||
runs the same provider auth flow again. It does not revoke credentials at the
|
||||
provider; rotate or revoke them in the provider dashboard when you need
|
||||
provider-side invalidation.
|
||||
|
||||
```bash
|
||||
openclaw models auth login --provider anthropic --force
|
||||
```
|
||||
|
||||
### Per-session (chat command)
|
||||
|
||||
Use `/model <alias-or-id>@<profileId>` to pin a specific provider credential for the current session (example profile ids: `anthropic:default`, `anthropic:work`).
|
||||
|
||||
@@ -103,11 +103,11 @@ Per-agent override: `agents.list[].contextInjection`. Omitted values inherit
|
||||
|
||||
### `agents.defaults.bootstrapMaxChars`
|
||||
|
||||
Max characters per workspace bootstrap file before truncation. Default: `20000`.
|
||||
Max characters per workspace bootstrap file before truncation. Default: `12000`.
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: { defaults: { bootstrapMaxChars: 20000 } },
|
||||
agents: { defaults: { bootstrapMaxChars: 12000 } },
|
||||
}
|
||||
```
|
||||
|
||||
@@ -138,7 +138,7 @@ injection behavior from the shared defaults. Omitted fields inherit from
|
||||
agents: {
|
||||
defaults: {
|
||||
contextInjection: "continuation-skip",
|
||||
bootstrapMaxChars: 20000,
|
||||
bootstrapMaxChars: 12000,
|
||||
bootstrapTotalMaxChars: 60000,
|
||||
},
|
||||
list: [
|
||||
|
||||
@@ -597,8 +597,6 @@ BlueBubbles support was removed. `channels.bluebubbles` is not a supported runti
|
||||
|
||||
If the Gateway is not running on the signed-in Messages Mac, keep `channels.imessage.enabled=true` and set `channels.imessage.cliPath` to an SSH wrapper that runs `imsg "$@"` on that Mac. The default local `imsg` path is macOS-only.
|
||||
|
||||
Before relying on an SSH wrapper for production sends, verify an outbound `imsg send` through that exact wrapper. Some macOS TCC states assign Messages Automation to `/usr/libexec/sshd-keygen-wrapper`, which can make reads and probes work while sends fail with AppleEvents `-1743`; see [SSH wrapper sends fail with AppleEvents -1743](/channels/imessage#ssh-wrapper-sends-fail-with-appleevents-1743).
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
|
||||
@@ -76,37 +76,6 @@ Server globs use the provider-safe MCP server prefix, not necessarily the raw `m
|
||||
|
||||
Without that sandbox-layer entry, the MCP server can still load successfully while its tools are filtered before the provider request. Use `openclaw doctor` to catch this shape for OpenClaw-managed servers in `mcp.servers`. MCP servers loaded from bundled plugin manifests or Claude `.mcp.json` use the same sandbox gate, but this diagnostic does not enumerate those sources yet; use the same allowlist entries if their tools disappear in sandboxed turns.
|
||||
|
||||
### `tools.codeMode`
|
||||
|
||||
`tools.codeMode` enables the generic OpenClaw code-mode surface. When enabled
|
||||
for a run with tools, the model sees only `exec` and `wait`; normal OpenClaw
|
||||
tools move behind the in-sandbox `tools.*` catalog bridge, and MCP tools are
|
||||
available through the generated `MCP` namespace.
|
||||
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
codeMode: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The shorthand is also accepted:
|
||||
|
||||
```json5
|
||||
{
|
||||
tools: { codeMode: true },
|
||||
}
|
||||
```
|
||||
|
||||
MCP declarations are exposed through the read-only virtual API file surface in
|
||||
code mode. Guest code can call `API.list("mcp")` and
|
||||
`API.read("mcp/<server>.d.ts")` to inspect TypeScript-style signatures before
|
||||
calling `MCP.<server>.<tool>()`. See [Code mode](/reference/code-mode) for the
|
||||
runtime contract, limits, and debugging steps.
|
||||
|
||||
### `tools.allow` / `tools.deny`
|
||||
|
||||
Global tool allow/deny policy (deny wins). Case-insensitive, supports `*` wildcards. Applied even when Docker sandbox is off.
|
||||
@@ -645,14 +614,14 @@ Interactive custom-provider onboarding infers image input for common vision mode
|
||||
<Accordion title="Local models (LM Studio)">
|
||||
See [Local Models](/gateway/local-models). TL;DR: run a large local model via LM Studio Responses API on serious hardware; keep hosted models merged for fallback.
|
||||
</Accordion>
|
||||
<Accordion title="MiniMax M3 (direct)">
|
||||
<Accordion title="MiniMax M2.7 (direct)">
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "minimax/MiniMax-M3" },
|
||||
model: { primary: "minimax/MiniMax-M2.7" },
|
||||
models: {
|
||||
"minimax/MiniMax-M3": { alias: "Minimax" },
|
||||
"minimax/MiniMax-M2.7": { alias: "Minimax" },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -665,12 +634,12 @@ Interactive custom-provider onboarding infers image input for common vision mode
|
||||
api: "anthropic-messages",
|
||||
models: [
|
||||
{
|
||||
id: "MiniMax-M3",
|
||||
name: "MiniMax M3",
|
||||
id: "MiniMax-M2.7",
|
||||
name: "MiniMax M2.7",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0.6, output: 2.4, cacheRead: 0.12, cacheWrite: 0 },
|
||||
contextWindow: 1000000,
|
||||
input: ["text"],
|
||||
cost: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0.375 },
|
||||
contextWindow: 204800,
|
||||
maxTokens: 131072,
|
||||
},
|
||||
],
|
||||
@@ -680,7 +649,7 @@ Interactive custom-provider onboarding infers image input for common vision mode
|
||||
}
|
||||
```
|
||||
|
||||
Set `MINIMAX_API_KEY`. Shortcuts: `openclaw onboard --auth-choice minimax-global-api` or `openclaw onboard --auth-choice minimax-cn-api`. The model catalog defaults to M3 and also includes the M2.7 variants. On the Anthropic-compatible streaming path, OpenClaw disables MiniMax thinking by default unless you explicitly set `thinking` yourself. `/fast on` or `params.fastMode: true` rewrites `MiniMax-M2.7` to `MiniMax-M2.7-highspeed`.
|
||||
Set `MINIMAX_API_KEY`. Shortcuts: `openclaw onboard --auth-choice minimax-global-api` or `openclaw onboard --auth-choice minimax-cn-api`. The model catalog defaults to M2.7 only. On the Anthropic-compatible streaming path, OpenClaw disables MiniMax thinking by default unless you explicitly set `thinking` yourself. `/fast on` or `params.fastMode: true` rewrites `MiniMax-M2.7` to `MiniMax-M2.7-highspeed`.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Moonshot AI (Kimi)">
|
||||
|
||||
@@ -580,11 +580,6 @@ See [Inferred commitments](/concepts/commitments).
|
||||
value, so repeated failures from one localhost origin do not automatically
|
||||
lock out a different origin.
|
||||
- `tailscale.mode`: `serve` (tailnet only, loopback bind) or `funnel` (public, requires auth).
|
||||
- `tailscale.serviceName`: optional Tailscale Service name for Serve mode, such
|
||||
as `svc:openclaw`. When set, OpenClaw passes it to `tailscale serve
|
||||
--service` so the Control UI can be exposed through a named Service instead
|
||||
of the device hostname. The value must use Tailscale's `svc:<dns-label>`
|
||||
Service name format; startup reports the derived Service URL.
|
||||
- `tailscale.preserveFunnel`: when `true` and `tailscale.mode = "serve"`, OpenClaw
|
||||
checks `tailscale funnel status` before re-applying Serve at startup and skips
|
||||
it if an externally configured Funnel route already covers the gateway port.
|
||||
|
||||
@@ -67,25 +67,6 @@ and use `gateway.auth.mode: "token"` or `"password"`.
|
||||
|
||||
Open: `https://<magicdns>/` (or your configured `gateway.controlUi.basePath`)
|
||||
|
||||
To expose the Control UI through a named Tailscale Service instead of the
|
||||
device hostname, set `gateway.tailscale.serviceName` to the Service name:
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
bind: "loopback",
|
||||
tailscale: { mode: "serve", serviceName: "svc:openclaw" },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
With the example above, startup reports the Service URL as
|
||||
`https://openclaw.<tailnet-name>.ts.net/` instead of the device hostname.
|
||||
Tailscale Services require the host to be an approved tagged node in your
|
||||
tailnet. Configure the tag and approve the Service in Tailscale before enabling
|
||||
this option, otherwise `tailscale serve --service=...` will fail during gateway
|
||||
startup.
|
||||
|
||||
### Tailnet-only (bind to Tailnet IP)
|
||||
|
||||
Use this when you want the Gateway to listen directly on the Tailnet IP (no Serve/Funnel).
|
||||
@@ -133,11 +114,6 @@ openclaw gateway --tailscale funnel --auth password
|
||||
|
||||
- Tailscale Serve/Funnel requires the `tailscale` CLI to be installed and logged in.
|
||||
- `tailscale.mode: "funnel"` refuses to start unless auth mode is `password` to avoid public exposure.
|
||||
- `gateway.tailscale.serviceName` applies only to Serve mode and is passed to
|
||||
`tailscale serve --service=<name>`. The value must use Tailscale's
|
||||
`svc:<dns-label>` Service name format, for example `svc:openclaw`.
|
||||
Tailscale requires Service hosts to be tagged nodes, and the Service may need
|
||||
approval in the admin console before Serve can publish it.
|
||||
- Set `gateway.tailscale.resetOnExit` if you want OpenClaw to undo `tailscale serve`
|
||||
or `tailscale funnel` configuration on shutdown.
|
||||
- Set `gateway.tailscale.preserveFunnel: true` to keep an externally configured
|
||||
|
||||
@@ -215,7 +215,7 @@ troubleshooting, see the main [FAQ](/help/faq).
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title='Why do I see "Unknown model: minimax/MiniMax-M3"?'>
|
||||
<Accordion title='Why do I see "Unknown model: minimax/MiniMax-M2.7"?'>
|
||||
This means the **provider isn't configured** (no MiniMax provider config or auth
|
||||
profile was found), so the model can't be resolved.
|
||||
|
||||
@@ -227,9 +227,8 @@ troubleshooting, see the main [FAQ](/help/faq).
|
||||
(`MINIMAX_API_KEY` for `minimax`, `MINIMAX_OAUTH_TOKEN` or stored MiniMax
|
||||
OAuth for `minimax-portal`).
|
||||
3. Use the exact model id (case-sensitive) for your auth path:
|
||||
`minimax/MiniMax-M3`, `minimax/MiniMax-M2.7`, or
|
||||
`minimax/MiniMax-M2.7-highspeed` for API-key setup, or
|
||||
`minimax-portal/MiniMax-M3`, `minimax-portal/MiniMax-M2.7`, or
|
||||
`minimax/MiniMax-M2.7` or `minimax/MiniMax-M2.7-highspeed` for API-key
|
||||
setup, or `minimax-portal/MiniMax-M2.7` /
|
||||
`minimax-portal/MiniMax-M2.7-highspeed` for OAuth setup.
|
||||
4. Run:
|
||||
|
||||
@@ -254,9 +253,9 @@ troubleshooting, see the main [FAQ](/help/faq).
|
||||
env: { MINIMAX_API_KEY: "sk-...", OPENAI_API_KEY: "sk-..." },
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "minimax/MiniMax-M3" },
|
||||
model: { primary: "minimax/MiniMax-M2.7" },
|
||||
models: {
|
||||
"minimax/MiniMax-M3": { alias: "minimax" },
|
||||
"minimax/MiniMax-M2.7": { alias: "minimax" },
|
||||
"openai/gpt-5.5": { alias: "gpt" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -631,48 +631,6 @@ lives on the [First-run FAQ](/help/faq-first-run).
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Can I make SOUL.md bigger?">
|
||||
Yes. `SOUL.md` is one of the workspace bootstrap files injected into the
|
||||
agent context. The default per-file injection limit is `20000` characters,
|
||||
and the total bootstrap budget across files is `60000` characters.
|
||||
|
||||
Change the shared defaults in your OpenClaw config:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
bootstrapMaxChars: 50000,
|
||||
bootstrapTotalMaxChars: 300000,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Or override one agent:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
bootstrapMaxChars: 50000,
|
||||
bootstrapTotalMaxChars: 300000,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Use `/context` to check raw vs injected sizes and whether truncation happened.
|
||||
Keep `SOUL.md` focused on voice, stance, and personality; put operating rules
|
||||
in `AGENTS.md` and durable facts in memory.
|
||||
|
||||
See [Context](/concepts/context) and [Agent config](/gateway/config-agents).
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Recommended backup strategy">
|
||||
Put your **agent workspace** in a **private** git repo and back it up somewhere
|
||||
private (for example GitHub private). This captures memory + AGENTS/SOUL/USER
|
||||
@@ -1817,7 +1775,7 @@ lives on the [Models FAQ](/help/faq-models).
|
||||
- The target channel supports outbound media and isn't blocked by allowlists.
|
||||
- The file is within the provider's size limits (images are resized to max 2048px).
|
||||
- `tools.fs.workspaceOnly=true` keeps local-path sends limited to workspace, temp/media-store, and sandbox-validated files.
|
||||
- `tools.fs.workspaceOnly=false` lets structured local media sends use host-local files the agent can already read, but only for media plus safe document types (images, audio, video, PDF, Office docs, and validated text documents such as Markdown/MD, TXT, JSON, YAML, and YML). This is not a secret scanner: an agent-readable `secret.txt` or `config.json` can be attached when the extension and content validation match. Keep sensitive files outside agent-readable paths, or keep `tools.fs.workspaceOnly=true` for stricter local-path sends.
|
||||
- `tools.fs.workspaceOnly=false` lets structured local media sends use host-local files the agent can already read, but only for media plus safe document types (images, audio, video, PDF, and Office docs). Plain text and secret-like files are still blocked.
|
||||
|
||||
See [Images](/nodes/images).
|
||||
|
||||
|
||||
@@ -73,11 +73,10 @@ Live tests are split into two layers so we can isolate failures:
|
||||
- `pnpm test:live` (or `OPENCLAW_LIVE_TEST=1` if invoking Vitest directly)
|
||||
- Set `OPENCLAW_LIVE_MODELS=modern`, `small`, or `all` (alias for modern) to actually run this suite; otherwise it skips to keep `pnpm test:live` focused on gateway smoke
|
||||
- How to select models:
|
||||
- `OPENCLAW_LIVE_MODELS=modern` to run the modern allowlist (Opus/Sonnet 4.6+, GPT-5.2 + Codex, Gemini 3, DeepSeek V4, GLM 4.7, MiniMax M3, Grok 4.3)
|
||||
- `OPENCLAW_LIVE_MODELS=small` to run the constrained small-model allowlist (Qwen 8B/9B local-compatible routes, Ollama Gemma, OpenRouter Qwen/GLM, and Z.AI GLM)
|
||||
- `OPENCLAW_LIVE_MODELS=modern` to run the modern allowlist (Opus/Sonnet 4.6+, GPT-5.2 + Codex, Gemini 3, DeepSeek V4, GLM 4.7, MiniMax M2.7, Grok 4.3)
|
||||
- `OPENCLAW_LIVE_MODELS=small` to run the constrained small-model allowlist (Qwen 8B/9B local-compatible routes, OpenRouter Qwen/GLM, and Z.AI GLM)
|
||||
- `OPENCLAW_LIVE_MODELS=all` is an alias for the modern allowlist
|
||||
- or `OPENCLAW_LIVE_MODELS="openai/gpt-5.5,anthropic/claude-opus-4-6,..."` (comma allowlist)
|
||||
- Local Ollama small-model runs default to `http://127.0.0.1:11434`; set `OPENCLAW_LIVE_OLLAMA_BASE_URL` only for LAN, custom, or Ollama Cloud endpoints.
|
||||
- Modern/all and small sweeps default to their curated caps; set `OPENCLAW_LIVE_MAX_MODELS=0` for an exhaustive selected-profile sweep or a positive number for a smaller cap.
|
||||
- Exhaustive sweeps use `OPENCLAW_LIVE_TEST_TIMEOUT_MS` for the whole direct-model test timeout. Default: 60 minutes.
|
||||
- Direct-model probes run with 20-way parallelism by default; set `OPENCLAW_LIVE_MODEL_CONCURRENCY` to override.
|
||||
@@ -109,7 +108,7 @@ Live tests are split into two layers so we can isolate failures:
|
||||
- How to enable:
|
||||
- `pnpm test:live` (or `OPENCLAW_LIVE_TEST=1` if invoking Vitest directly)
|
||||
- How to select models:
|
||||
- Default: modern allowlist (Opus/Sonnet 4.6+, GPT-5.2 + Codex, Gemini 3, DeepSeek V4, GLM 4.7, MiniMax M3, Grok 4.3)
|
||||
- Default: modern allowlist (Opus/Sonnet 4.6+, GPT-5.2 + Codex, Gemini 3, DeepSeek V4, GLM 4.7, MiniMax M2.7, Grok 4.3)
|
||||
- `OPENCLAW_LIVE_GATEWAY_MODELS=all` is an alias for the modern allowlist
|
||||
- Or set `OPENCLAW_LIVE_GATEWAY_MODELS="provider/model"` (or comma list) to narrow
|
||||
- Modern/all gateway sweeps default to a curated high-signal cap; set `OPENCLAW_LIVE_GATEWAY_MAX_MODELS=0` for an exhaustive modern sweep or a positive number for a smaller cap.
|
||||
@@ -351,7 +350,7 @@ Narrow, explicit allowlists are fastest and least flaky:
|
||||
- `OPENCLAW_LIVE_GATEWAY_MODELS="openai/gpt-5.5" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts`
|
||||
|
||||
- Tool calling across several providers:
|
||||
- `OPENCLAW_LIVE_GATEWAY_MODELS="openai/gpt-5.5,anthropic/claude-opus-4-6,google/gemini-3-flash-preview,deepseek/deepseek-v4-flash,zai/glm-5.1,minimax/MiniMax-M3" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts`
|
||||
- `OPENCLAW_LIVE_GATEWAY_MODELS="openai/gpt-5.5,anthropic/claude-opus-4-6,google/gemini-3-flash-preview,deepseek/deepseek-v4-flash,zai/glm-5.1,minimax/MiniMax-M2.7" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts`
|
||||
|
||||
- Google focus (Gemini API key + Antigravity):
|
||||
- Gemini (API key): `OPENCLAW_LIVE_GATEWAY_MODELS="google/gemini-3-flash-preview" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts`
|
||||
@@ -385,10 +384,10 @@ This is the "common models" run we expect to keep working:
|
||||
- Google (Antigravity): `google-antigravity/claude-opus-4-6-thinking` and `google-antigravity/gemini-3-flash`
|
||||
- DeepSeek: `deepseek/deepseek-v4-flash` and `deepseek/deepseek-v4-pro`
|
||||
- Z.AI (GLM): `zai/glm-5.1`
|
||||
- MiniMax: `minimax/MiniMax-M3`
|
||||
- MiniMax: `minimax/MiniMax-M2.7`
|
||||
|
||||
Run gateway smoke with tools + image:
|
||||
`OPENCLAW_LIVE_GATEWAY_MODELS="openai/gpt-5.5,anthropic/claude-opus-4-6,google/gemini-3.1-pro-preview,google/gemini-3-flash-preview,google-antigravity/claude-opus-4-6-thinking,google-antigravity/gemini-3-flash,deepseek/deepseek-v4-flash,zai/glm-5.1,minimax/MiniMax-M3" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts`
|
||||
`OPENCLAW_LIVE_GATEWAY_MODELS="openai/gpt-5.5,anthropic/claude-opus-4-6,google/gemini-3.1-pro-preview,google/gemini-3-flash-preview,google-antigravity/claude-opus-4-6-thinking,google-antigravity/gemini-3-flash,deepseek/deepseek-v4-flash,zai/glm-5.1,minimax/MiniMax-M2.7" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts`
|
||||
|
||||
### Baseline: tool calling (Read + optional Exec)
|
||||
|
||||
@@ -399,7 +398,7 @@ Pick at least one per provider family:
|
||||
- Google: `google/gemini-3-flash-preview` (or `google/gemini-3.1-pro-preview`)
|
||||
- DeepSeek: `deepseek/deepseek-v4-flash`
|
||||
- Z.AI (GLM): `zai/glm-5.1`
|
||||
- MiniMax: `minimax/MiniMax-M3`
|
||||
- MiniMax: `minimax/MiniMax-M2.7`
|
||||
|
||||
Optional additional coverage (nice to have):
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@ Recommended: use the built-in uninstaller:
|
||||
openclaw uninstall
|
||||
```
|
||||
|
||||
When using the CLI, state removal preserves configured workspace directories unless you also select `--workspace`.
|
||||
|
||||
Non-interactive (automation / npx):
|
||||
|
||||
```bash
|
||||
@@ -49,7 +47,6 @@ rm -rf "${OPENCLAW_STATE_DIR:-$HOME/.openclaw}"
|
||||
```
|
||||
|
||||
If you set `OPENCLAW_CONFIG_PATH` to a custom location outside the state dir, delete that file too.
|
||||
If you want to keep a workspace inside the state dir, such as `~/.openclaw/workspace`, move it aside before running `rm -rf` or delete state contents selectively.
|
||||
|
||||
4. Delete your workspace (optional, removes agent files):
|
||||
|
||||
|
||||
@@ -245,7 +245,7 @@ Notes:
|
||||
|
||||
The iOS app is a mobile node surface, not a Codex Computer Use backend. Codex
|
||||
Computer Use and `cua-driver mcp` control a local macOS desktop through MCP
|
||||
tools; the iOS app exposes iPhone and iPad capabilities through OpenClaw node commands
|
||||
tools; the iOS app exposes iPhone capabilities through OpenClaw node commands
|
||||
such as `canvas.*`, `camera.*`, `screen.*`, `location.*`, and `talk.*`.
|
||||
|
||||
Agents can still operate the iOS app through OpenClaw by invoking node
|
||||
|
||||
@@ -1021,10 +1021,10 @@ plugin index entry with `source: "path"` and a workspace-relative
|
||||
`plugins.load.paths`; the install record avoids duplicating local workstation
|
||||
paths into long-lived config. This keeps local development installs visible to
|
||||
source-plane diagnostics without adding a second raw filesystem-path disclosure
|
||||
surface. The persisted `installed_plugin_index` SQLite row is the install
|
||||
surface. The persisted `plugins/installs.json` plugin index is the install
|
||||
source of truth and can be refreshed without loading plugin runtime modules.
|
||||
Its `installRecords` map is durable even when a plugin manifest is missing or
|
||||
invalid; its `plugins` payload is a rebuildable manifest view.
|
||||
invalid; its `plugins` array is a rebuildable manifest view.
|
||||
|
||||
## Context engine plugins
|
||||
|
||||
|
||||
@@ -368,7 +368,7 @@ If discovery fails or times out, OpenClaw uses a bundled fallback catalog for:
|
||||
- GPT-5.4 mini
|
||||
- GPT-5.2
|
||||
|
||||
The current bundled harness is `@openai/codex` `0.135.0`. A `model/list` probe
|
||||
The current bundled harness is `@openai/codex` `0.134.0`. A `model/list` probe
|
||||
against that bundled app-server returned:
|
||||
|
||||
| Model id | Default | Hidden | Input modalities | Reasoning efforts |
|
||||
|
||||
@@ -190,10 +190,11 @@ plugins, channels, and core code only see the standard
|
||||
|
||||
When `harness.compact` runs, the Copilot SDK harness:
|
||||
|
||||
1. Resumes the tracked SDK session without continuing pending work.
|
||||
2. Calls the SDK's session-scoped history compaction RPC.
|
||||
3. Returns the SDK compaction outcome without writing compatibility marker
|
||||
files under the workspace.
|
||||
1. Enables `infiniteSessions` on the SDK session.
|
||||
2. Lets the SDK perform its native compaction.
|
||||
3. Writes an OpenClaw-shaped marker at
|
||||
`workspacePath/files/openclaw-compaction-<ts>.json` so existing OpenClaw
|
||||
transcript readers still see a familiar artifact.
|
||||
|
||||
The OpenClaw side transcript mirror (see below) continues to receive the
|
||||
post-compaction messages, so user-facing chat history stays consistent.
|
||||
|
||||
@@ -399,10 +399,8 @@ media caption.
|
||||
|
||||
Message hook contexts expose stable correlation fields when available:
|
||||
`ctx.sessionKey`, `ctx.runId`, `ctx.messageId`, `ctx.senderId`, `ctx.trace`,
|
||||
`ctx.traceId`, `ctx.spanId`, `ctx.parentSpanId`, and `ctx.callDepth`. Inbound
|
||||
and `before_dispatch` contexts also expose reply metadata when the channel has
|
||||
visibility-filtered quoted message data: `replyToId`, `replyToBody`, and
|
||||
`replyToSender`. Prefer these first-class fields before reading legacy metadata.
|
||||
`ctx.traceId`, `ctx.spanId`, `ctx.parentSpanId`, and `ctx.callDepth`. Prefer
|
||||
these first-class fields before reading legacy metadata.
|
||||
|
||||
Prefer typed `threadId` and `replyToId` fields before using channel-specific
|
||||
metadata.
|
||||
|
||||
@@ -52,14 +52,8 @@ type MessagePresentationBlock =
|
||||
| { type: "buttons"; buttons: MessagePresentationButton[] }
|
||||
| { type: "select"; placeholder?: string; options: MessagePresentationOption[] };
|
||||
|
||||
type MessagePresentationAction =
|
||||
| { type: "command"; command: string }
|
||||
| { type: "callback"; value: string };
|
||||
|
||||
type MessagePresentationButton = {
|
||||
label: string;
|
||||
action?: MessagePresentationAction;
|
||||
/** Legacy callback value. Prefer action for new controls. */
|
||||
value?: string;
|
||||
url?: string;
|
||||
webApp?: { url: string };
|
||||
@@ -73,9 +67,7 @@ type MessagePresentationButton = {
|
||||
|
||||
type MessagePresentationOption = {
|
||||
label: string;
|
||||
action?: MessagePresentationAction;
|
||||
/** Legacy callback value. Prefer action for new controls. */
|
||||
value?: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type ReplyPayloadDelivery = {
|
||||
@@ -91,13 +83,8 @@ type ReplyPayloadDelivery = {
|
||||
|
||||
Button semantics:
|
||||
|
||||
- `action.type: "command"` runs a native slash command through core's command
|
||||
path. Use this for built-in command buttons and menus.
|
||||
- `action.type: "callback"` carries opaque plugin data through the channel's
|
||||
interaction path. Channel plugins must not reinterpret callback data as slash
|
||||
commands.
|
||||
- `value` is the legacy opaque callback value. New controls should use `action`
|
||||
so channel plugins can map commands and callbacks without guessing from text.
|
||||
- `value` is an application action value routed back through the channel's
|
||||
existing interaction path when the channel supports clickable controls.
|
||||
- `url` is a link button. It can exist without `value`.
|
||||
- `webApp` describes a channel-native web app button. Telegram renders this
|
||||
as `web_app` and only supports it in private chats. `web_app` is still
|
||||
@@ -119,8 +106,7 @@ Button semantics:
|
||||
|
||||
Select semantics:
|
||||
|
||||
- `options[].action` has the same command/callback meaning as button `action`.
|
||||
- `options[].value` is the legacy selected application value.
|
||||
- `options[].value` is the selected application value.
|
||||
- `placeholder` is advisory and may be ignored by channels without native
|
||||
select support.
|
||||
- If a channel does not support selects, fallback text lists the labels.
|
||||
|
||||
@@ -107,7 +107,7 @@ commands.
|
||||
| [oc-path](/plugins/reference/oc-path) | Adds the openclaw path CLI for oc:// workspace file addressing. | `@openclaw/oc-path`<br />included in OpenClaw | plugin |
|
||||
| [ollama](/plugins/reference/ollama) | Adds Ollama, Ollama Cloud model provider support to OpenClaw. | `@openclaw/ollama-provider`<br />included in OpenClaw | providers: ollama, ollama-cloud; contracts: memoryEmbeddingProviders, webSearchProviders |
|
||||
| [open-prose](/plugins/reference/open-prose) | OpenProse VM skill pack with a /prose slash command. | `@openclaw/open-prose`<br />included in OpenClaw | skills |
|
||||
| [openai](/plugins/reference/openai) | Adds OpenAI model provider support to OpenClaw. | `@openclaw/openai-provider`<br />included in OpenClaw | providers: openai; contracts: imageGenerationProviders, mediaUnderstandingProviders, memoryEmbeddingProviders, realtimeTranscriptionProviders, realtimeVoiceProviders, speechProviders, videoGenerationProviders |
|
||||
| [openai](/plugins/reference/openai) | Adds OpenAI model provider support to OpenClaw, including ChatGPT/Codex OAuth. | `@openclaw/openai-provider`<br />included in OpenClaw | providers: openai; contracts: imageGenerationProviders, mediaUnderstandingProviders, memoryEmbeddingProviders, realtimeTranscriptionProviders, realtimeVoiceProviders, speechProviders, videoGenerationProviders |
|
||||
| [opencode](/plugins/reference/opencode) | Adds OpenCode model provider support to OpenClaw. | `@openclaw/opencode-provider`<br />included in OpenClaw | providers: opencode; contracts: mediaUnderstandingProviders |
|
||||
| [opencode-go](/plugins/reference/opencode-go) | Adds OpenCode Go model provider support to OpenClaw. | `@openclaw/opencode-go-provider`<br />included in OpenClaw | providers: opencode-go; contracts: mediaUnderstandingProviders |
|
||||
| [openrouter](/plugins/reference/openrouter) | Adds OpenRouter model provider support to OpenClaw. | `@openclaw/openrouter-provider`<br />included in OpenClaw | providers: openrouter; contracts: imageGenerationProviders, mediaUnderstandingProviders, musicGenerationProviders, speechProviders, videoGenerationProviders |
|
||||
|
||||
@@ -95,7 +95,7 @@ pnpm plugins:inventory:gen
|
||||
| [oc-path](/plugins/reference/oc-path) | Adds the openclaw path CLI for oc:// workspace file addressing. | `@openclaw/oc-path`<br />included in OpenClaw | plugin |
|
||||
| [ollama](/plugins/reference/ollama) | Adds Ollama, Ollama Cloud model provider support to OpenClaw. | `@openclaw/ollama-provider`<br />included in OpenClaw | providers: ollama, ollama-cloud; contracts: memoryEmbeddingProviders, webSearchProviders |
|
||||
| [open-prose](/plugins/reference/open-prose) | OpenProse VM skill pack with a /prose slash command. | `@openclaw/open-prose`<br />included in OpenClaw | skills |
|
||||
| [openai](/plugins/reference/openai) | Adds OpenAI model provider support to OpenClaw. | `@openclaw/openai-provider`<br />included in OpenClaw | providers: openai; contracts: imageGenerationProviders, mediaUnderstandingProviders, memoryEmbeddingProviders, realtimeTranscriptionProviders, realtimeVoiceProviders, speechProviders, videoGenerationProviders |
|
||||
| [openai](/plugins/reference/openai) | Adds OpenAI model provider support to OpenClaw, including ChatGPT/Codex OAuth. | `@openclaw/openai-provider`<br />included in OpenClaw | providers: openai; contracts: imageGenerationProviders, mediaUnderstandingProviders, memoryEmbeddingProviders, realtimeTranscriptionProviders, realtimeVoiceProviders, speechProviders, videoGenerationProviders |
|
||||
| [opencode](/plugins/reference/opencode) | Adds OpenCode model provider support to OpenClaw. | `@openclaw/opencode-provider`<br />included in OpenClaw | providers: opencode; contracts: mediaUnderstandingProviders |
|
||||
| [opencode-go](/plugins/reference/opencode-go) | Adds OpenCode Go model provider support to OpenClaw. | `@openclaw/opencode-go-provider`<br />included in OpenClaw | providers: opencode-go; contracts: mediaUnderstandingProviders |
|
||||
| [openrouter](/plugins/reference/openrouter) | Adds OpenRouter model provider support to OpenClaw. | `@openclaw/openrouter-provider`<br />included in OpenClaw | providers: openrouter; contracts: imageGenerationProviders, mediaUnderstandingProviders, musicGenerationProviders, speechProviders, videoGenerationProviders |
|
||||
|
||||
@@ -17,15 +17,3 @@ Adds syntax highlighting for languages outside the default diffs viewer set.
|
||||
## Surface
|
||||
|
||||
plugin
|
||||
|
||||
<!-- openclaw-plugin-reference:manual-start -->
|
||||
|
||||
## Added languages
|
||||
|
||||
The base `diffs` plugin already highlights the common languages documented in [Diffs](/tools/diffs). Install this language pack when you want syntax highlighting for a broader set of Shiki-supported languages. If the pack is not installed, those files still render as readable plain text.
|
||||
|
||||
Examples include Astro, Vue, Svelte, MDX, GraphQL, Terraform/HCL, Nix, Clojure, Elixir, Haskell, OCaml, Scala, Zig, Solidity, Verilog/VHDL, Fortran, MATLAB, LaTeX, Mermaid, Sass/Less/SCSS, Nginx, Apache, CSV, dotenv, INI, and diff files.
|
||||
|
||||
See [Shiki languages](https://shiki.style/languages) for Shiki's upstream language and alias catalog.
|
||||
|
||||
<!-- openclaw-plugin-reference:manual-end -->
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
summary: "Adds OpenAI model provider support to OpenClaw."
|
||||
summary: "Adds OpenAI model provider support to OpenClaw, including ChatGPT/Codex OAuth."
|
||||
read_when:
|
||||
- You are installing, configuring, or auditing the openai plugin
|
||||
title: "OpenAI plugin"
|
||||
@@ -7,7 +7,7 @@ title: "OpenAI plugin"
|
||||
|
||||
# OpenAI plugin
|
||||
|
||||
Adds OpenAI model provider support to OpenClaw.
|
||||
Adds OpenAI model provider support to OpenClaw, including ChatGPT/Codex OAuth.
|
||||
|
||||
## Distribution
|
||||
|
||||
|
||||
@@ -652,7 +652,6 @@ releases.
|
||||
| `plugin-sdk/zod` | Deprecated Zod compatibility re-export | Import `zod` from `zod` directly |
|
||||
| `plugin-sdk/memory-core` | Bundled memory-core helpers | Memory manager/config/file/CLI helper surface |
|
||||
| `plugin-sdk/memory-core-engine-runtime` | Memory engine runtime facade | Memory index/search runtime facade |
|
||||
| `plugin-sdk/memory-core-host-embedding-registry` | Memory embedding registry | Lightweight memory embedding provider registry helpers |
|
||||
| `plugin-sdk/memory-core-host-engine-foundation` | Memory host foundation engine | Memory host foundation engine exports |
|
||||
| `plugin-sdk/memory-core-host-engine-embeddings` | Memory host embedding engine | Memory embedding contracts, registry access, local provider, and generic batch/remote helpers; concrete remote providers live in their owning plugins |
|
||||
| `plugin-sdk/memory-core-host-engine-qmd` | Memory host QMD engine | Memory host QMD engine exports |
|
||||
|
||||
@@ -76,7 +76,6 @@ by package contract guardrails.
|
||||
| `plugin-sdk/channel-config-helpers` | `createHybridChannelConfigAdapter`, `resolveChannelDmAccess`, `resolveChannelDmAllowFrom`, `resolveChannelDmPolicy`, `normalizeChannelDmPolicy`, `normalizeLegacyDmAliases` |
|
||||
| `plugin-sdk/channel-config-schema` | Shared channel config schema primitives plus Zod and direct JSON/TypeBox builders |
|
||||
| `plugin-sdk/bundled-channel-config-schema` | Bundled OpenClaw channel config schemas for maintained bundled plugins only |
|
||||
| `plugin-sdk/chat-channel-ids` | `BUNDLED_CHAT_CHANNEL_IDS`, `BUNDLED_CHAT_CHANNEL_ENVELOPE_PREFIXES`, `ChatChannelId`. Canonical bundled/official chat channel ids plus formatter labels/aliases for plugins that need to recognize envelope-prefixed text without hardcoding their own table. |
|
||||
| `plugin-sdk/channel-config-schema-legacy` | Deprecated compatibility alias for bundled-channel config schemas |
|
||||
| `plugin-sdk/telegram-command-config` | Telegram custom-command normalization/validation helpers with bundled-contract fallback |
|
||||
| `plugin-sdk/command-gating` | Narrow command authorization gate helpers |
|
||||
@@ -356,7 +355,6 @@ usage endpoint failed or returned no usable usage data.
|
||||
| --- | --- |
|
||||
| `plugin-sdk/memory-core` | Bundled memory-core helper surface for manager/config/file/CLI helpers |
|
||||
| `plugin-sdk/memory-core-engine-runtime` | Memory index/search runtime facade |
|
||||
| `plugin-sdk/memory-core-host-embedding-registry` | Lightweight memory embedding provider registry helpers |
|
||||
| `plugin-sdk/memory-core-host-engine-foundation` | Memory host foundation engine exports |
|
||||
| `plugin-sdk/memory-core-host-engine-embeddings` | Memory host embedding contracts, registry access, local provider, and generic batch/remote helpers. `registerMemoryEmbeddingProvider` on this surface is deprecated; use the generic embedding provider API for new providers. |
|
||||
| `plugin-sdk/memory-core-host-engine-qmd` | Memory host QMD engine exports |
|
||||
|
||||
@@ -9,8 +9,7 @@ title: "Workboard plugin"
|
||||
|
||||
The Workboard plugin adds an optional Kanban-style board to the
|
||||
[Control UI](/web/control-ui). Use it to collect agent-sized work cards, assign
|
||||
them to agents, and track the linked background task, run, and dashboard
|
||||
session from one card.
|
||||
them to agents, and jump from a card into the linked dashboard session.
|
||||
|
||||
Workboard is intentionally small. It tracks local operating work for an
|
||||
OpenClaw Gateway; it is not a replacement for GitHub Issues, Linear, Jira, or
|
||||
@@ -48,8 +47,8 @@ Each card stores:
|
||||
- priority: `low`, `normal`, `high`, or `urgent`
|
||||
- labels
|
||||
- optional agent id
|
||||
- optional linked task, run, session, or source URL
|
||||
- optional execution metadata for a Codex or Claude run started from the card
|
||||
- optional linked session, run, task, or source URL
|
||||
- optional execution metadata for a Codex or Claude session started from the card
|
||||
- compact metadata for attempts, comments, links, proof, artifacts, automation,
|
||||
attachments, worker logs, worker protocol state, claims, diagnostics,
|
||||
notifications, templates, archive state, and stale-session detection
|
||||
@@ -66,35 +65,26 @@ proof snippets, related links, comments, archive markers, and stale-session
|
||||
markers are intentionally local metadata; they do not replace session
|
||||
transcripts or GitHub issue history.
|
||||
|
||||
## Card executions and tasks
|
||||
## Card executions
|
||||
|
||||
Unlinked cards can start work from the card. Autonomous starts use the
|
||||
Gateway's task-tracked agent run path, then Workboard links the resulting task,
|
||||
run id, and session key back onto the card. Start uses the Gateway's configured
|
||||
Unlinked cards can start work from the card. Start uses the Gateway's configured
|
||||
default agent and model. Codex and Claude actions are optional explicit model
|
||||
choices:
|
||||
|
||||
- Run Codex or Run Claude starts a task-backed agent run, sends the card
|
||||
prompt, and marks the card `running`.
|
||||
- Run Codex or Run Claude creates a dashboard session, sends the card prompt,
|
||||
and marks the card `running`.
|
||||
- Open Codex or Open Claude creates a linked dashboard session without sending
|
||||
the card prompt or moving the card, so you can work manually while it stays
|
||||
attached to the board.
|
||||
|
||||
Execution metadata stores the selected engine, mode, model ref, session key,
|
||||
run id, task id when available, and lifecycle status on the card. Codex
|
||||
executions use `openai/gpt-5.5`; Claude executions use
|
||||
`anthropic/claude-sonnet-4-6`.
|
||||
run id, and lifecycle status on the card. Codex executions use
|
||||
`openai/gpt-5.5`; Claude executions use `anthropic/claude-sonnet-4-6`.
|
||||
|
||||
Each linked execution also records an attempt summary on the same card record.
|
||||
The attempt summary keeps the engine, mode, model, run id, timestamps, status,
|
||||
and rolling failure count so repeated failures remain visible on the board.
|
||||
|
||||
The dashboard refreshes task status from the Gateway task ledger and matches
|
||||
tasks back to cards by task id, run id, or linked session key. If a task is
|
||||
queued or running, the card lifecycle shows active task state. If the task
|
||||
finishes, fails, times out, or is cancelled, the card lifecycle moves toward
|
||||
review or blocked status using the same lifecycle sync as linked sessions.
|
||||
|
||||
## Agent coordination
|
||||
|
||||
Workboard also exposes optional agent tools for board-aware workflows:
|
||||
@@ -170,15 +160,13 @@ blocked cards that need attention, repeated failures, done cards without proof,
|
||||
and running cards that only have a loose session link.
|
||||
|
||||
Dispatch is intentionally Gateway-local. It does not spawn arbitrary operating
|
||||
system processes; normal OpenClaw subagent sessions still own execution. The
|
||||
dispatch action promotes dependency-ready cards, records dispatch metadata on
|
||||
system processes; normal OpenClaw subagent sessions still own execution. A
|
||||
dispatch nudge promotes dependency-ready cards, records dispatch metadata on
|
||||
ready cards, blocks expired claims or timed-out runs, marks board-configured
|
||||
triage cards as orchestration candidates, then claims a small batch of ready
|
||||
cards and starts worker runs through the Gateway subagent runtime. Assigned
|
||||
cards use `agent:<id>:subagent:workboard-*` worker session keys; unassigned
|
||||
cards use unscoped `subagent:workboard-*` keys so the Gateway still resolves the
|
||||
configured default agent. Workers get bounded card context plus the claim token
|
||||
they need to heartbeat, complete, or block the card through the Workboard tools.
|
||||
cards and starts worker runs through the Gateway subagent runtime. Workers get
|
||||
bounded card context plus the claim token they need to heartbeat, complete, or
|
||||
block the card through the Workboard tools.
|
||||
|
||||
### Dispatch worker selection
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ read_when:
|
||||
title: "MiniMax"
|
||||
---
|
||||
|
||||
OpenClaw's MiniMax provider defaults to **MiniMax M3**.
|
||||
OpenClaw's MiniMax provider defaults to **MiniMax M2.7**.
|
||||
|
||||
MiniMax also provides:
|
||||
|
||||
@@ -26,8 +26,7 @@ Provider split:
|
||||
|
||||
| Model | Type | Description |
|
||||
| ------------------------ | ---------------- | ---------------------------------------- |
|
||||
| `MiniMax-M3` | Chat (reasoning) | Default hosted reasoning model |
|
||||
| `MiniMax-M2.7` | Chat (reasoning) | Previous hosted reasoning model |
|
||||
| `MiniMax-M2.7` | Chat (reasoning) | Default hosted reasoning model |
|
||||
| `MiniMax-M2.7-highspeed` | Chat (reasoning) | Faster M2.7 reasoning tier |
|
||||
| `MiniMax-VL-01` | Vision | Image understanding model |
|
||||
| `image-01` | Image generation | Text-to-image and image-to-image editing |
|
||||
@@ -80,7 +79,7 @@ Choose your preferred auth method and follow the setup steps.
|
||||
</Tabs>
|
||||
|
||||
<Note>
|
||||
OAuth setups use the `minimax-portal` provider id. Model refs follow the form `minimax-portal/MiniMax-M3`.
|
||||
OAuth setups use the `minimax-portal` provider id. Model refs follow the form `minimax-portal/MiniMax-M2.7`.
|
||||
</Note>
|
||||
|
||||
<Tip>
|
||||
@@ -132,7 +131,7 @@ Choose your preferred auth method and follow the setup steps.
|
||||
```json5
|
||||
{
|
||||
env: { MINIMAX_API_KEY: "sk-..." },
|
||||
agents: { defaults: { model: { primary: "minimax/MiniMax-M3" } } },
|
||||
agents: { defaults: { model: { primary: "minimax/MiniMax-M2.7" } } },
|
||||
models: {
|
||||
mode: "merge",
|
||||
providers: {
|
||||
@@ -141,15 +140,6 @@ Choose your preferred auth method and follow the setup steps.
|
||||
apiKey: "${MINIMAX_API_KEY}",
|
||||
api: "anthropic-messages",
|
||||
models: [
|
||||
{
|
||||
id: "MiniMax-M3",
|
||||
name: "MiniMax M3",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0.6, output: 2.4, cacheRead: 0.12, cacheWrite: 0 },
|
||||
contextWindow: 1000000,
|
||||
maxTokens: 131072,
|
||||
},
|
||||
{
|
||||
id: "MiniMax-M2.7",
|
||||
name: "MiniMax M2.7",
|
||||
@@ -180,7 +170,7 @@ Choose your preferred auth method and follow the setup steps.
|
||||
</Warning>
|
||||
|
||||
<Note>
|
||||
API-key setups use the `minimax` provider id. Model refs follow the form `minimax/MiniMax-M3`.
|
||||
API-key setups use the `minimax` provider id. Model refs follow the form `minimax/MiniMax-M2.7`.
|
||||
</Note>
|
||||
|
||||
</Tab>
|
||||
@@ -253,10 +243,9 @@ through the CN endpoint; the default global endpoint is
|
||||
`https://api.minimax.io`.
|
||||
|
||||
When onboarding or API-key setup writes explicit `models.providers.minimax`
|
||||
entries, OpenClaw materializes `MiniMax-M3`, `MiniMax-M2.7`, and
|
||||
`MiniMax-M2.7-highspeed` as chat models. M3 advertises text and image input;
|
||||
image understanding remains exposed separately through the plugin-owned
|
||||
`MiniMax-VL-01` media provider.
|
||||
entries, OpenClaw materializes `MiniMax-M2.7` and
|
||||
`MiniMax-M2.7-highspeed` as text-only chat models. Image understanding is
|
||||
exposed separately through the plugin-owned `MiniMax-VL-01` media provider.
|
||||
|
||||
<Note>
|
||||
See [Image Generation](/tools/image-generation) for shared tool parameters, provider selection, and failover behavior.
|
||||
@@ -364,7 +353,7 @@ catalog:
|
||||
| `minimax-portal` | `MiniMax-VL-01` |
|
||||
|
||||
That is why automatic media routing can use MiniMax image understanding even
|
||||
when the bundled text-provider catalog also includes M3 image-capable chat refs.
|
||||
when the bundled text-provider catalog still shows text-only M2.7 chat refs.
|
||||
|
||||
### Web search
|
||||
|
||||
@@ -448,12 +437,12 @@ See [MiniMax Search](/tools/minimax-search) for full web search configuration an
|
||||
- Model refs follow the auth path:
|
||||
- API-key setup: `minimax/<model>`
|
||||
- OAuth setup: `minimax-portal/<model>`
|
||||
- Default chat model: `MiniMax-M3`
|
||||
- Alternate chat models: `MiniMax-M2.7`, `MiniMax-M2.7-highspeed`
|
||||
- Onboarding and direct API-key setup write model definitions for M3 and both M2.7 variants
|
||||
- Default chat model: `MiniMax-M2.7`
|
||||
- Alternate chat model: `MiniMax-M2.7-highspeed`
|
||||
- Onboarding and direct API-key setup write text-only model definitions for both M2.7 variants
|
||||
- Image understanding uses the plugin-owned `MiniMax-VL-01` media provider
|
||||
- Update pricing values in `models.json` if you need exact cost tracking
|
||||
- Use `openclaw models list` to confirm the current provider id, then switch with `openclaw models set minimax/MiniMax-M3` or `openclaw models set minimax-portal/MiniMax-M3`
|
||||
- Use `openclaw models list` to confirm the current provider id, then switch with `openclaw models set minimax/MiniMax-M2.7` or `openclaw models set minimax-portal/MiniMax-M2.7`
|
||||
|
||||
<Tip>
|
||||
Referral link for MiniMax Coding Plan (10% off): [MiniMax Coding Plan](https://platform.minimax.io/subscribe/coding-plan?code=DbXJTRClnb&source=link)
|
||||
@@ -466,7 +455,7 @@ See [Model providers](/concepts/model-providers) for provider rules.
|
||||
## Troubleshooting
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title='"Unknown model: minimax/MiniMax-M3"'>
|
||||
<Accordion title='"Unknown model: minimax/MiniMax-M2.7"'>
|
||||
This usually means the **MiniMax provider is not configured** (no matching provider entry and no MiniMax auth profile/env key found). A fix for this detection is in **2026.1.12**. Fix by:
|
||||
|
||||
- Upgrading to **2026.1.12** (or run from source `main`), then restarting the gateway.
|
||||
@@ -476,8 +465,8 @@ See [Model providers](/concepts/model-providers) for provider rules.
|
||||
|
||||
Make sure the model id is **case-sensitive**:
|
||||
|
||||
- API-key path: `minimax/MiniMax-M3`, `minimax/MiniMax-M2.7`, or `minimax/MiniMax-M2.7-highspeed`
|
||||
- OAuth path: `minimax-portal/MiniMax-M3`, `minimax-portal/MiniMax-M2.7`, or `minimax-portal/MiniMax-M2.7-highspeed`
|
||||
- API-key path: `minimax/MiniMax-M2.7` or `minimax/MiniMax-M2.7-highspeed`
|
||||
- OAuth path: `minimax-portal/MiniMax-M2.7` or `minimax-portal/MiniMax-M2.7-highspeed`
|
||||
|
||||
Then recheck with:
|
||||
|
||||
|
||||
@@ -311,8 +311,7 @@ The branch already has a real shared SQLite base:
|
||||
`delivery_queue_entries`, `model_capability_cache`,
|
||||
`workspace_setup_state`, `native_hook_relay_bridges`,
|
||||
`current_conversation_bindings`, `plugin_binding_approvals`,
|
||||
`tui_last_sessions`, `acp_sessions`, `acp_replay_sessions`,
|
||||
`acp_replay_events`, `task_runs`, `task_delivery_state`, `flow_runs`,
|
||||
`tui_last_sessions`, `task_runs`, `task_delivery_state`, `flow_runs`,
|
||||
`subagent_runs`, `migration_runs`, and `backup_runs`.
|
||||
- Arbitrary plugin-owned state does not get host-owned typed tables. Installed
|
||||
plugins use `plugin_state_entries` for versioned JSON payloads and
|
||||
@@ -457,10 +456,6 @@ The branch already has a real shared SQLite base:
|
||||
- GitHub Copilot token exchange cache uses the shared SQLite plugin-state table
|
||||
under `github-copilot/token-cache/default`. It is provider-owned cache state,
|
||||
so it intentionally does not add a host schema table.
|
||||
- GitHub Copilot compaction no longer writes `openclaw-compaction-*.json`
|
||||
workspace sidecars. The harness calls the SDK history compaction RPC for the
|
||||
tracked SDK session, and OpenClaw keeps durable session/transcript state in
|
||||
SQLite instead of compatibility marker files.
|
||||
- The shared Swift runtime (`OpenClawKit`) uses the same
|
||||
`state/openclaw.sqlite` rows for device identity and device auth. macOS app
|
||||
helpers import the shared SQLite helpers instead of owning a second JSON or
|
||||
@@ -1674,8 +1669,6 @@ Move these into agent databases:
|
||||
- ACP replay ledger sessions. Done for runtime writes via
|
||||
`acp_replay_sessions` and `acp_replay_events`; legacy `acp/event-ledger.json`
|
||||
remains only as doctor input.
|
||||
- ACP session metadata. Done for runtime writes via `acp_sessions`; legacy
|
||||
`entry.acp` blocks in `sessions.json` are doctor migration input only.
|
||||
- Trajectory sidecars when they are not explicit export files. Done for runtime
|
||||
writes: trajectory capture writes agent-database `trajectory_runtime_events`
|
||||
rows and mirrors run-scoped artifacts into SQLite. Legacy sidecars are doctor
|
||||
|
||||
@@ -100,11 +100,6 @@ The shorthand is also accepted:
|
||||
Code mode remains off when `tools.codeMode` is omitted, `false`, or an object
|
||||
without `enabled: true`.
|
||||
|
||||
When you use sandboxed agents with configured MCP servers, also make sure the
|
||||
sandbox tool policy allows the bundled MCP plugin, for example with
|
||||
`tools.sandbox.tools.alsoAllow: ["bundle-mcp"]`. See
|
||||
[Configuration - tools and custom providers](/gateway/config-tools#mcp-and-plugin-tools-inside-sandbox-tool-policy).
|
||||
|
||||
Use explicit limits when you want tighter bounds:
|
||||
|
||||
```json5
|
||||
@@ -446,13 +441,12 @@ const hits = await tools.web_search({ query: "OpenClaw code mode" });
|
||||
|
||||
MCP catalog entries are not callable through `tools.call(...)` or convenience
|
||||
functions in code mode. They are exposed only through the generated `MCP`
|
||||
namespace. TypeScript-style declaration files are available through the
|
||||
read-only `API` virtual file surface, so agents can inspect MCP signatures
|
||||
without adding MCP schemas to the prompt:
|
||||
namespace, which includes TypeScript-style API headers for discovery:
|
||||
|
||||
```typescript
|
||||
const files = await API.list("mcp");
|
||||
const githubApi = await API.read("mcp/github.d.ts");
|
||||
const servers = await MCP.$api();
|
||||
const githubApi = await MCP.github.$api();
|
||||
const createIssueApi = await MCP.github.$api("createIssue", { schema: true });
|
||||
|
||||
const issue = await MCP.github.createIssue({
|
||||
owner: "openclaw",
|
||||
@@ -468,8 +462,7 @@ const prompt = await MCP.docs.prompts.get({
|
||||
});
|
||||
```
|
||||
|
||||
`API.read("mcp/<server>.d.ts")` returns compact declarations inferred from MCP
|
||||
tool metadata:
|
||||
`MCP.<server>.$api()` returns a compact header inferred from MCP tool metadata:
|
||||
|
||||
```typescript
|
||||
type McpToolResult = {
|
||||
@@ -498,20 +491,6 @@ declare namespace MCP.github {
|
||||
}
|
||||
```
|
||||
|
||||
The declaration files are virtual, not files written under the workspace or
|
||||
state directory. For each code-mode `exec` call, OpenClaw builds the run-scoped
|
||||
tool catalog, keeps the visible MCP entries, renders `mcp/index.d.ts` plus one
|
||||
`mcp/<server>.d.ts` declaration per visible server, and injects that small
|
||||
read-only table into the QuickJS worker. Guest code sees only the `API` object:
|
||||
`API.list(prefix?)` returns file metadata and `API.read(path)` returns the
|
||||
selected declaration content. Unknown paths and `.` / `..` segments are rejected.
|
||||
|
||||
This keeps large MCP schemas out of the model prompt. The agent learns that the
|
||||
virtual API exists from the `exec` tool description, reads only the needed
|
||||
declaration file, and then calls `MCP.<server>.<tool>()` with one object argument.
|
||||
`MCP.<server>.$api()` remains available as an inline fallback when the agent
|
||||
needs a single-tool schema response inside the program.
|
||||
|
||||
The guest runtime must not expose host objects directly. Inputs and outputs cross
|
||||
the bridge as JSON-compatible values with explicit size caps.
|
||||
|
||||
@@ -1002,9 +981,8 @@ Code mode coverage should prove:
|
||||
- all effective non-MCP tools appear in `ALL_TOOLS`
|
||||
- denied tools do not appear in `ALL_TOOLS`
|
||||
- `tools.search`, `tools.describe`, and `tools.call` work for OpenClaw tools
|
||||
- `API.list("mcp")` and `API.read("mcp/<server>.d.ts")` expose TypeScript-style
|
||||
MCP declarations without a bridge/tool call
|
||||
- MCP namespace `$api()` remains available as an inline fallback for schemas
|
||||
- MCP namespace `$api()` returns TypeScript-style headers inferred from MCP
|
||||
schemas
|
||||
- MCP namespace calls work for visible MCP tools with one object input, while
|
||||
direct MCP catalog entries are absent from `tools.*`
|
||||
- Tool Search control tools are hidden from both the model surface and the hidden
|
||||
@@ -1036,8 +1014,8 @@ Run these as integration or end-to-end tests when changing the runtime:
|
||||
7. In `exec`, read `ALL_TOOLS` and assert the effective test tools are present.
|
||||
8. In `exec`, call OpenClaw/plugin/client tools through `tools.search`,
|
||||
`tools.describe`, and `tools.call`.
|
||||
9. In `exec`, call `API.list("mcp")` and `API.read("mcp/<server>.d.ts")` and
|
||||
assert the declaration files describe visible MCP tools.
|
||||
9. In `exec`, call `MCP.$api()` and `MCP.<server>.$api()` and assert the headers
|
||||
describe visible MCP tools.
|
||||
10. In `exec`, call MCP tools through `MCP.<server>.<tool>({ ...input })` and
|
||||
assert direct MCP catalog entries are absent from `ALL_TOOLS` and `tools.*`.
|
||||
11. Assert denied tools are absent and cannot be called by guessed id.
|
||||
|
||||
@@ -20,7 +20,7 @@ OpenClaw assembles its own system prompt on every run. It includes:
|
||||
prompt surface. It is bounded by `skills.limits.maxSkillsPromptChars`, with
|
||||
optional per-agent override at `agents.list[].skillsLimits.maxSkillsPromptChars`.
|
||||
- Self-update instructions
|
||||
- Workspace + bootstrap files (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md` when new, plus `MEMORY.md` when present). Native Codex turns do not paste raw `MEMORY.md` from the configured agent workspace when memory tools are available for that workspace; they include a small memory pointer in turn-scoped collaboration developer instructions and use memory tools on demand. If tools are disabled, memory search is unavailable, or the active workspace differs from the agent memory workspace, `MEMORY.md` uses the normal bounded turn-context path. Lowercase root `memory.md` is not injected; it is legacy repair input for `openclaw doctor --fix` when paired with `MEMORY.md`. Large injected files are truncated by `agents.defaults.bootstrapMaxChars` (default: 20000), and total bootstrap injection is capped by `agents.defaults.bootstrapTotalMaxChars` (default: 60000). `memory/*.md` daily files are not part of the normal bootstrap prompt; they remain on-demand via memory tools on ordinary turns, but reset/startup model runs can prepend a one-shot startup-context block with recent daily memory for that first turn. Bare chat `/new` and `/reset` commands are acknowledged without invoking the model. The startup prelude is controlled by `agents.defaults.startupContext`. Post-compaction AGENTS.md excerpts are separate and require explicit `agents.defaults.compaction.postCompactionSections` opt-in.
|
||||
- Workspace + bootstrap files (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md` when new, plus `MEMORY.md` when present). Native Codex turns do not paste raw `MEMORY.md` from the configured agent workspace when memory tools are available for that workspace; they include a small memory pointer in turn-scoped collaboration developer instructions and use memory tools on demand. If tools are disabled, memory search is unavailable, or the active workspace differs from the agent memory workspace, `MEMORY.md` uses the normal bounded turn-context path. Lowercase root `memory.md` is not injected; it is legacy repair input for `openclaw doctor --fix` when paired with `MEMORY.md`. Large injected files are truncated by `agents.defaults.bootstrapMaxChars` (default: 12000), and total bootstrap injection is capped by `agents.defaults.bootstrapTotalMaxChars` (default: 60000). `memory/*.md` daily files are not part of the normal bootstrap prompt; they remain on-demand via memory tools on ordinary turns, but reset/startup model runs can prepend a one-shot startup-context block with recent daily memory for that first turn. Bare chat `/new` and `/reset` commands are acknowledged without invoking the model. The startup prelude is controlled by `agents.defaults.startupContext`. Post-compaction AGENTS.md excerpts are separate and require explicit `agents.defaults.compaction.postCompactionSections` opt-in.
|
||||
- Time (UTC + user timezone)
|
||||
- Reply tags + heartbeat behavior
|
||||
- Runtime metadata (host/OS/model/thinking)
|
||||
|
||||
@@ -47,7 +47,7 @@ For a high-level overview, see [Onboarding (CLI)](/start/wizard).
|
||||
- More detail: [Vercel AI Gateway](/providers/vercel-ai-gateway)
|
||||
- **Cloudflare AI Gateway**: prompts for Account ID, Gateway ID, and `CLOUDFLARE_AI_GATEWAY_API_KEY`.
|
||||
- More detail: [Cloudflare AI Gateway](/providers/cloudflare-ai-gateway)
|
||||
- **MiniMax**: config is auto-written; hosted default is `MiniMax-M3`.
|
||||
- **MiniMax**: config is auto-written; hosted default is `MiniMax-M2.7`.
|
||||
API-key setup uses `minimax/...`, and OAuth setup uses
|
||||
`minimax-portal/...`.
|
||||
- More detail: [MiniMax](/providers/minimax)
|
||||
|
||||
@@ -212,9 +212,9 @@ Local-path behavior follows the same file-read trust model as the agent:
|
||||
- If `tools.fs.workspaceOnly` is `true`, outbound local media paths stay restricted to the OpenClaw temp root, the media cache, agent workspace paths, and sandbox-generated files.
|
||||
- If `tools.fs.workspaceOnly` is `false`, outbound local media can use host-local files the agent is already allowed to read.
|
||||
- Local paths can be absolute, workspace-relative, or home-relative with `~/`.
|
||||
- Host-local sends still only allow media and safe document types (images, audio, video, PDF, Office documents, and validated text documents such as Markdown/MD, TXT, JSON, YAML, and YML). This is an extension of the existing host-read trust boundary, not a secret scanner: if the agent can read a host-local `secret.txt` or `config.json`, it can attach that file when the extension and content validation match.
|
||||
- Host-local sends still only allow media and safe document types (images, audio, video, PDF, and Office documents). Plain text and secret-like files are not treated as sendable media.
|
||||
|
||||
That means generated images/files outside the workspace can now send when your fs policy already allows those reads, while arbitrary host-local text extensions remain blocked. Keep sensitive files outside the agent-readable filesystem, or keep `tools.fs.workspaceOnly=true` for stricter local-path sends.
|
||||
That means generated images/files outside the workspace can now send when your fs policy already allows those reads, without reopening arbitrary host-text attachment exfiltration.
|
||||
|
||||
## Operations checklist
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ What you set:
|
||||
More detail: [Cloudflare AI Gateway](/providers/cloudflare-ai-gateway).
|
||||
</Accordion>
|
||||
<Accordion title="MiniMax">
|
||||
Config is auto-written. Hosted default is `MiniMax-M3`; API-key setup uses
|
||||
Config is auto-written. Hosted default is `MiniMax-M2.7`; API-key setup uses
|
||||
`minimax/...`, and OAuth setup uses `minimax-portal/...`.
|
||||
More detail: [MiniMax](/providers/minimax).
|
||||
</Accordion>
|
||||
|
||||
@@ -216,9 +216,7 @@ Install the Diff Viewer Language Pack plugin to highlight other languages:
|
||||
openclaw plugins install clawhub:@openclaw/diffs-language-pack
|
||||
```
|
||||
|
||||
With the language pack available, OpenClaw can highlight many more languages. If the pack is not installed, files outside the default list still render as readable plain text. Examples include Astro, Vue, Svelte, MDX, GraphQL, Terraform/HCL, Nix, Clojure, Elixir, Haskell, OCaml, Scala, Zig, Solidity, Verilog/VHDL, Fortran, MATLAB, LaTeX, Mermaid, Sass/Less/SCSS, Nginx, Apache, CSV, dotenv, INI, and diff files.
|
||||
|
||||
See [Diffs Language Pack plugin](/plugins/reference/diffs-language-pack) for details and [Shiki languages](https://shiki.style/languages) for Shiki's upstream language and alias catalog.
|
||||
With the language pack available, OpenClaw automatically uses it for languages outside the default list. Without it, those files stay readable as plain text.
|
||||
|
||||
## Output details contract
|
||||
|
||||
|
||||
4
extensions/acpx/npm-shrinkwrap.json
generated
4
extensions/acpx/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/acpx",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.5.31",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/acpx",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.5.31",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/claude-agent-acp": "0.39.0",
|
||||
"@zed-industries/codex-acp": "0.15.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/acpx",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.5.31",
|
||||
"description": "OpenClaw ACP runtime backend with plugin-owned session and transport management.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -26,10 +26,10 @@
|
||||
"minHostVersion": ">=2026.4.25"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.1"
|
||||
"pluginApi": ">=2026.5.31"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.1",
|
||||
"openclawVersion": "2026.5.31",
|
||||
"staticAssets": [
|
||||
{
|
||||
"source": "./src/runtime-internals/mcp-proxy.mjs",
|
||||
|
||||
@@ -37,8 +37,8 @@ async function startRealService(state: DeferredServiceState): Promise<AcpRuntime
|
||||
throw new Error("ACPX runtime service is not started");
|
||||
}
|
||||
state.startPromise ??= (async () => {
|
||||
const { createAcpxRuntimeService: createAcpxRuntimeServiceLocal } = await loadServiceModule();
|
||||
const service = createAcpxRuntimeServiceLocal(state.params);
|
||||
const { createAcpxRuntimeService } = await loadServiceModule();
|
||||
const service = createAcpxRuntimeService(state.params);
|
||||
state.realService = service;
|
||||
await service.start(state.ctx as OpenClawPluginServiceContext);
|
||||
const backend = getAcpRuntimeBackend(ACPX_BACKEND_ID);
|
||||
|
||||
@@ -312,120 +312,6 @@ describe("prepareAcpxCodexAuthConfig", () => {
|
||||
expect(path.resolve(String(launched.codexHome))).toBe(expectedCodexHome);
|
||||
});
|
||||
|
||||
it("writes API-key auth into the isolated Codex ACP home when env auth is present", async () => {
|
||||
const root = await makeTempDir();
|
||||
const stateDir = path.join(root, "state");
|
||||
const generated = generatedCodexPaths(stateDir);
|
||||
const installedBinPath = path.join(root, "codex-acp-bin.js");
|
||||
await fs.writeFile(
|
||||
installedBinPath,
|
||||
"console.log(JSON.stringify({ codexHome: process.env.CODEX_HOME }));\n",
|
||||
"utf8",
|
||||
);
|
||||
const pluginConfig = resolveAcpxPluginConfig({
|
||||
rawConfig: {},
|
||||
workspaceDir: root,
|
||||
});
|
||||
|
||||
await prepareAcpxCodexAuthConfig({
|
||||
pluginConfig,
|
||||
stateDir,
|
||||
resolveInstalledCodexAcpBinPath: async () => installedBinPath,
|
||||
});
|
||||
|
||||
await execFileAsync(process.execPath, [generated.wrapperPath], {
|
||||
cwd: root,
|
||||
env: { ...process.env, CODEX_API_KEY: "", OPENAI_API_KEY: "sk-test-api-key" },
|
||||
});
|
||||
|
||||
const authPath = path.join(stateDir, "acpx", "codex-home", "auth.json");
|
||||
const auth = JSON.parse(await fs.readFile(authPath, "utf8")) as {
|
||||
auth_mode?: unknown;
|
||||
OPENAI_API_KEY?: unknown;
|
||||
};
|
||||
expect(auth).toMatchObject({
|
||||
OPENAI_API_KEY: "sk-test-api-key",
|
||||
});
|
||||
expect(auth).not.toHaveProperty("auth_mode");
|
||||
if (process.platform !== "win32") {
|
||||
const mode = (await fs.stat(authPath)).mode & 0o777;
|
||||
expect(mode).toBe(0o600);
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves existing isolated Codex auth when env auth is present", async () => {
|
||||
const root = await makeTempDir();
|
||||
const stateDir = path.join(root, "state");
|
||||
const generated = generatedCodexPaths(stateDir);
|
||||
const installedBinPath = path.join(root, "codex-acp-bin.js");
|
||||
await fs.writeFile(installedBinPath, "console.log('ok');\n", "utf8");
|
||||
const pluginConfig = resolveAcpxPluginConfig({
|
||||
rawConfig: {},
|
||||
workspaceDir: root,
|
||||
});
|
||||
|
||||
await prepareAcpxCodexAuthConfig({
|
||||
pluginConfig,
|
||||
stateDir,
|
||||
resolveInstalledCodexAcpBinPath: async () => installedBinPath,
|
||||
});
|
||||
|
||||
const authPath = path.join(stateDir, "acpx", "codex-home", "auth.json");
|
||||
const existingAuth = {
|
||||
auth_mode: "chatgpt",
|
||||
tokens: { access_token: "existing-token" },
|
||||
last_refresh: null,
|
||||
};
|
||||
await fs.writeFile(authPath, `${JSON.stringify(existingAuth)}\n`, { mode: 0o600 });
|
||||
|
||||
await execFileAsync(process.execPath, [generated.wrapperPath], {
|
||||
cwd: root,
|
||||
env: { ...process.env, OPENAI_API_KEY: "sk-test-api-key" },
|
||||
});
|
||||
|
||||
expect(JSON.parse(await fs.readFile(authPath, "utf8"))).toEqual(existingAuth);
|
||||
});
|
||||
|
||||
it("updates existing isolated Codex API-key auth when env auth changes", async () => {
|
||||
const root = await makeTempDir();
|
||||
const stateDir = path.join(root, "state");
|
||||
const generated = generatedCodexPaths(stateDir);
|
||||
const installedBinPath = path.join(root, "codex-acp-bin.js");
|
||||
await fs.writeFile(installedBinPath, "console.log('ok');\n", "utf8");
|
||||
const pluginConfig = resolveAcpxPluginConfig({
|
||||
rawConfig: {},
|
||||
workspaceDir: root,
|
||||
});
|
||||
|
||||
await prepareAcpxCodexAuthConfig({
|
||||
pluginConfig,
|
||||
stateDir,
|
||||
resolveInstalledCodexAcpBinPath: async () => installedBinPath,
|
||||
});
|
||||
|
||||
const authPath = path.join(stateDir, "acpx", "codex-home", "auth.json");
|
||||
await fs.writeFile(
|
||||
authPath,
|
||||
`${JSON.stringify({
|
||||
OPENAI_API_KEY: "sk-old-api-key",
|
||||
tokens: null,
|
||||
last_refresh: null,
|
||||
})}\n`,
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
|
||||
await execFileAsync(process.execPath, [generated.wrapperPath], {
|
||||
cwd: root,
|
||||
env: { ...process.env, CODEX_API_KEY: "sk-new-api-key", OPENAI_API_KEY: "sk-other-key" },
|
||||
});
|
||||
|
||||
expect(JSON.parse(await fs.readFile(authPath, "utf8"))).toMatchObject({
|
||||
OPENAI_API_KEY: "sk-new-api-key",
|
||||
tokens: null,
|
||||
last_refresh: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("launches the locally installed Claude ACP bin without going through npm", async () => {
|
||||
const root = await makeTempDir();
|
||||
const stateDir = path.join(root, "state");
|
||||
|
||||
@@ -4,11 +4,11 @@ import { createRequire } from "node:module";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { readJsonFileWithFallback } from "openclaw/plugin-sdk/json-store";
|
||||
import { quoteCommandPart, splitCommandParts } from "./command-line.js";
|
||||
import {
|
||||
extractTrustedCodexProjectPaths,
|
||||
renderIsolatedCodexConfig,
|
||||
} from "./codex-trust-config.js";
|
||||
import { quoteCommandPart, splitCommandParts } from "./command-line.js";
|
||||
import { resolveAcpxPluginRoot } from "./config.js";
|
||||
import type { ResolvedAcpxPluginConfig } from "./config.js";
|
||||
import {
|
||||
@@ -528,35 +528,6 @@ function buildCodexAcpWrapperScript(installedBinPath?: string): string {
|
||||
installedBinPath,
|
||||
stderrLogFileNamePrefix: "codex-acp-wrapper.stderr",
|
||||
envSetup: `const codexHome = fileURLToPath(new URL("./codex-home/", import.meta.url));
|
||||
const codexAuthPath = fileURLToPath(new URL("./codex-home/auth.json", import.meta.url));
|
||||
const codexApiKey = (process.env.CODEX_API_KEY || process.env.OPENAI_API_KEY || "").trim();
|
||||
let shouldWriteCodexApiKeyAuth = false;
|
||||
if (codexApiKey) {
|
||||
if (!existsSync(codexAuthPath)) {
|
||||
shouldWriteCodexApiKeyAuth = true;
|
||||
} else {
|
||||
try {
|
||||
const existingCodexAuth = JSON.parse(readFileSync(codexAuthPath, "utf8"));
|
||||
shouldWriteCodexApiKeyAuth =
|
||||
!existingCodexAuth ||
|
||||
typeof existingCodexAuth !== "object" ||
|
||||
typeof existingCodexAuth.OPENAI_API_KEY === "string";
|
||||
} catch {
|
||||
shouldWriteCodexApiKeyAuth = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldWriteCodexApiKeyAuth) {
|
||||
writeFileSync(
|
||||
codexAuthPath,
|
||||
JSON.stringify({
|
||||
OPENAI_API_KEY: codexApiKey,
|
||||
tokens: null,
|
||||
last_refresh: null,
|
||||
}) + "\\n",
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
}
|
||||
const env = {
|
||||
...process.env,
|
||||
CODEX_HOME: codexHome,
|
||||
|
||||
@@ -262,12 +262,7 @@ async function terminatePids(
|
||||
deps: AcpxProcessCleanupDeps | undefined,
|
||||
): Promise<number[]> {
|
||||
const killProcess = deps?.killProcess ?? ((pid, signal) => process.kill(pid, signal));
|
||||
const sleep =
|
||||
deps?.sleep ??
|
||||
((ms) =>
|
||||
new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
}));
|
||||
const sleep = deps?.sleep ?? ((ms) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
|
||||
const terminated: number[] = [];
|
||||
|
||||
for (const pid of pids) {
|
||||
@@ -307,7 +302,7 @@ export async function cleanupOpenClawOwnedAcpxProcessTree(params: {
|
||||
return { inspectedPids: [], terminatedPids: [], skippedReason: "missing-root" };
|
||||
}
|
||||
|
||||
let processes: AcpxProcessInfo[];
|
||||
let processes: AcpxProcessInfo[] = [];
|
||||
try {
|
||||
processes = await (params.deps?.listProcesses ?? listPlatformProcesses)();
|
||||
} catch {
|
||||
|
||||
@@ -68,7 +68,7 @@ class LegacyRunTurnEventQueue {
|
||||
return item;
|
||||
}
|
||||
if (this.error) {
|
||||
throw toLintErrorObject(this.error, "Non-Error thrown");
|
||||
throw this.error;
|
||||
}
|
||||
if (this.closed) {
|
||||
return null;
|
||||
@@ -178,17 +178,3 @@ export function lazyStartRuntimeTurn(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function toLintErrorObject(value: unknown, fallbackMessage: string): Error {
|
||||
if (value instanceof Error) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return new Error(value);
|
||||
}
|
||||
const error = new Error(fallbackMessage, { cause: value });
|
||||
if ((typeof value === "object" && value !== null) || typeof value === "function") {
|
||||
Object.assign(error, value);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
@@ -286,7 +286,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
})
|
||||
.then(
|
||||
() => ({ status: "resolved" as const }),
|
||||
(error: unknown) => ({ status: "rejected" as const, error }),
|
||||
(error) => ({ status: "rejected" as const, error }),
|
||||
);
|
||||
|
||||
expect(outcome.status).toBe("rejected");
|
||||
@@ -298,12 +298,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
code: "ACP_SESSION_INIT_FAILED",
|
||||
message: expect.stringContaining("deployment missing"),
|
||||
});
|
||||
const error = outcome.error;
|
||||
expect(error).toBeInstanceOf(AcpRuntimeError);
|
||||
if (!(error instanceof AcpRuntimeError)) {
|
||||
throw new Error("expected AcpRuntimeError");
|
||||
}
|
||||
expect(error.message).not.toContain("sk-testsecret1234567890");
|
||||
expect(outcome.error.message).not.toContain("sk-testsecret1234567890");
|
||||
});
|
||||
|
||||
it("adds Codex wrapper stderr tail to generic first-turn failures", async () => {
|
||||
@@ -340,7 +335,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
});
|
||||
|
||||
await expect(async () => {
|
||||
for await (const ignoredEventValue of runtime.runTurn({
|
||||
for await (const eventValue of runtime.runTurn({
|
||||
handle: {
|
||||
sessionKey: "agent:codex:acp:test",
|
||||
backend: "acpx",
|
||||
@@ -351,7 +346,6 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
mode: "prompt",
|
||||
requestId: "turn-1",
|
||||
})) {
|
||||
void ignoredEventValue;
|
||||
// no-op
|
||||
}
|
||||
}).rejects.toMatchObject({
|
||||
@@ -574,7 +568,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
for await (const ignoredEventValue of runtime.runTurn({
|
||||
for await (const eventValue of runtime.runTurn({
|
||||
handle: {
|
||||
sessionKey: "agent:codex:acp:test",
|
||||
backend: "acpx",
|
||||
@@ -585,7 +579,6 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
mode: "prompt",
|
||||
requestId: "turn-1",
|
||||
})) {
|
||||
void ignoredEventValue;
|
||||
// no-op
|
||||
}
|
||||
|
||||
@@ -606,8 +599,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
mode: "prompt",
|
||||
requestId: "turn-2",
|
||||
});
|
||||
for await (const ignoredEventValue of turn.events) {
|
||||
void ignoredEventValue;
|
||||
for await (const eventValue of turn.events) {
|
||||
// no-op
|
||||
}
|
||||
await turn.result;
|
||||
@@ -955,16 +947,16 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
expect(await wrappedStore.load("agent:codex:acp:binding:test")).toEqual({
|
||||
acpxRecordId: "stale",
|
||||
});
|
||||
expect(baseStore["load"]).toHaveBeenCalledTimes(1);
|
||||
expect(baseStore.load).toHaveBeenCalledTimes(1);
|
||||
|
||||
await runtime.prepareFreshSession({
|
||||
sessionKey: "agent:codex:acp:binding:test",
|
||||
});
|
||||
|
||||
expect(await wrappedStore.load("agent:codex:acp:binding:test")).toBeUndefined();
|
||||
expect(baseStore["load"]).toHaveBeenCalledTimes(1);
|
||||
expect(baseStore.load).toHaveBeenCalledTimes(1);
|
||||
expect(await wrappedStore.load("agent:codex:acp:binding:test")).toBeUndefined();
|
||||
expect(baseStore["load"]).toHaveBeenCalledTimes(1);
|
||||
expect(baseStore.load).toHaveBeenCalledTimes(1);
|
||||
|
||||
await wrappedStore.save({
|
||||
acpxRecordId: "fresh-record",
|
||||
@@ -974,7 +966,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
expect(await wrappedStore.load("agent:codex:acp:binding:test")).toEqual({
|
||||
acpxRecordId: "stale",
|
||||
});
|
||||
expect(baseStore["load"]).toHaveBeenCalledTimes(2);
|
||||
expect(baseStore.load).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("marks the session fresh after discardPersistentState close", async () => {
|
||||
@@ -1006,7 +998,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
discardPersistentState: true,
|
||||
});
|
||||
expect(await wrappedStore.load("agent:codex:acp:binding:test")).toBeUndefined();
|
||||
expect(baseStore["load"]).toHaveBeenCalledOnce();
|
||||
expect(baseStore.load).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("cleans up OpenClaw-owned ACPX process trees after close", async () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user