mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-26 01:01:58 +08:00
Compare commits
1 Commits
dependabot
...
codex/tool
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d44260b927 |
33
.github/workflows/ci.yml
vendored
33
.github/workflows/ci.yml
vendored
@@ -251,6 +251,7 @@ jobs:
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const createMatrix = (include) => ({ include });
|
||||
const outputPath = process.env.GITHUB_OUTPUT;
|
||||
const isCanonicalRepository = process.env.OPENCLAW_CI_REPOSITORY === "openclaw/openclaw";
|
||||
@@ -284,7 +285,6 @@ jobs:
|
||||
if (runNodeFull) {
|
||||
checksFastCoreTasks.push(
|
||||
{ check_name: "checks-fast-bundled-protocol", runtime: "node", task: "bundled-protocol" },
|
||||
{ check_name: "QA Smoke CI", runtime: "node", task: "qa-smoke-ci" },
|
||||
{ check_name: "checks-fast-bun-launcher", runtime: "bun", task: "bun-launcher" },
|
||||
);
|
||||
} else {
|
||||
@@ -922,26 +922,6 @@ jobs:
|
||||
pnpm test:bundled
|
||||
pnpm protocol:check
|
||||
;;
|
||||
qa-smoke-ci)
|
||||
output_dir=".artifacts/qa-e2e/smoke-ci-profile"
|
||||
export OPENCLAW_BUILD_PRIVATE_QA=1
|
||||
export OPENCLAW_ENABLE_PRIVATE_QA_CLI=1
|
||||
export OPENCLAW_DISABLE_BUNDLED_PLUGINS=0
|
||||
export OPENCLAW_QA_REDACT_PUBLIC_METADATA=1
|
||||
export OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS=180000
|
||||
NODE_OPTIONS=--max-old-space-size=8192 node scripts/build-all.mjs qaRuntime
|
||||
qa_exit_code=0
|
||||
pnpm openclaw qa run \
|
||||
--repo-root . \
|
||||
--qa-profile smoke-ci \
|
||||
--concurrency 8 \
|
||||
--output-dir "$output_dir" || qa_exit_code=$?
|
||||
echo "QA smoke profile evidence: \`${output_dir}\`" >> "$GITHUB_STEP_SUMMARY"
|
||||
if [ "$qa_exit_code" -ne 0 ]; then
|
||||
echo "::error title=QA smoke profile failed::smoke-ci exited ${qa_exit_code}; evidence upload will still run"
|
||||
exit "$qa_exit_code"
|
||||
fi
|
||||
;;
|
||||
contracts-plugins-ci-routing)
|
||||
pnpm test:contracts:plugins
|
||||
pnpm test src/commands/status.scan-result.test.ts src/scripts/ci-changed-scope.test.ts test/scripts/changed-lanes.test.ts test/scripts/ci-workflow-guards.test.ts test/scripts/run-vitest.test.ts test/scripts/test-projects.test.ts
|
||||
@@ -958,15 +938,6 @@ jobs:
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Upload QA smoke profile evidence
|
||||
if: always() && matrix.task == 'qa-smoke-ci'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: qa-smoke-profile-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: .artifacts/qa-e2e/smoke-ci-profile/
|
||||
if-no-files-found: warn
|
||||
retention-days: 7
|
||||
|
||||
checks-fast-plugin-contracts-shard:
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -1843,7 +1814,7 @@ jobs:
|
||||
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
|
||||
4
.github/workflows/clawsweeper-dispatch.yml
vendored
4
.github/workflows/clawsweeper-dispatch.yml
vendored
@@ -73,7 +73,7 @@ jobs:
|
||||
- name: Create ClawSweeper dispatch token
|
||||
id: token
|
||||
if: ${{ env.HAS_CLAWSWEEPER_APP_PRIVATE_KEY == 'true' }}
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }}
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
steps.comment_filter.outputs.is_command == 'true' &&
|
||||
env.HAS_CLAWSWEEPER_APP_PRIVATE_KEY == 'true'
|
||||
}}
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }}
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
submodules: false
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21"
|
||||
|
||||
4
.github/workflows/dependency-guard.yml
vendored
4
.github/workflows/dependency-guard.yml
vendored
@@ -57,7 +57,7 @@ jobs:
|
||||
- name: Create autoscrub app token
|
||||
id: app-token
|
||||
continue-on-error: true
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
app-id: "2729701"
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
id: app-token-fallback
|
||||
continue-on-error: true
|
||||
if: steps.app-token.outcome == 'failure'
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
app-id: "2971289"
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
|
||||
|
||||
2
.github/workflows/docs-agent.yml
vendored
2
.github/workflows/docs-agent.yml
vendored
@@ -149,7 +149,7 @@ jobs:
|
||||
|
||||
- name: Run Codex docs agent
|
||||
if: steps.gate.outputs.run_agent == 'true'
|
||||
uses: openai/codex-action@10cb888d2ed3b99867f7e7ccff174a861a75aeb6
|
||||
uses: openai/codex-action@e0fdf01220eb9a88167c4898839d273e3f2609d1
|
||||
env:
|
||||
DOCS_AGENT_BASE_SHA: ${{ steps.gate.outputs.review_base_sha }}
|
||||
DOCS_AGENT_HEAD_SHA: ${{ steps.gate.outputs.review_head_sha }}
|
||||
|
||||
@@ -260,7 +260,7 @@ jobs:
|
||||
run: pnpm build
|
||||
|
||||
- name: Setup Go for Crabbox CLI
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
with:
|
||||
go-version: "1.26.x"
|
||||
cache: false
|
||||
|
||||
@@ -250,7 +250,7 @@ jobs:
|
||||
run: pnpm build
|
||||
|
||||
- name: Setup Go for Crabbox CLI
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
with:
|
||||
go-version: "1.26.x"
|
||||
cache: false
|
||||
|
||||
@@ -190,7 +190,7 @@ jobs:
|
||||
mantis-slack-pnpm-${{ runner.os }}-${{ env.NODE_VERSION }}-
|
||||
|
||||
- name: Setup Go for Crabbox CLI
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
with:
|
||||
go-version: "1.26.x"
|
||||
cache: false
|
||||
|
||||
@@ -362,7 +362,7 @@ jobs:
|
||||
install-bun: "true"
|
||||
|
||||
- name: Setup Go for Crabbox CLI
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
with:
|
||||
go-version: "1.26.x"
|
||||
cache: false
|
||||
@@ -445,7 +445,7 @@ jobs:
|
||||
sudo chown -R codex:codex "$GITHUB_WORKSPACE"
|
||||
|
||||
- name: Run Codex Mantis Telegram agent
|
||||
uses: openai/codex-action@10cb888d2ed3b99867f7e7ccff174a861a75aeb6
|
||||
uses: openai/codex-action@e0fdf01220eb9a88167c4898839d273e3f2609d1
|
||||
env:
|
||||
BASELINE_REF: ${{ needs.resolve_request.outputs.baseline_ref }}
|
||||
BASELINE_SHA: ${{ needs.validate_refs.outputs.baseline_revision }}
|
||||
|
||||
2
.github/workflows/mantis-telegram-live.yml
vendored
2
.github/workflows/mantis-telegram-live.yml
vendored
@@ -337,7 +337,7 @@ jobs:
|
||||
mantis-telegram-pnpm-${{ runner.os }}-${{ env.NODE_VERSION }}-
|
||||
|
||||
- name: Setup Go for Crabbox CLI
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
with:
|
||||
go-version: "1.26.x"
|
||||
cache: false
|
||||
|
||||
16
.github/workflows/maturity-scorecard.yml
vendored
16
.github/workflows/maturity-scorecard.yml
vendored
@@ -134,7 +134,7 @@ jobs:
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
expected_sha: ${{ needs.validate_selected_ref.outputs.selected_revision }}
|
||||
qa_profile: all
|
||||
qa_profile: release
|
||||
secrets:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
|
||||
@@ -238,8 +238,8 @@ jobs:
|
||||
}
|
||||
|
||||
const evidence = JSON.parse(fs.readFileSync(evidencePath, "utf8"));
|
||||
if (evidence.profile !== "all") {
|
||||
throw new Error(`qa-evidence.json profile must be all, got ${JSON.stringify(evidence.profile)}`);
|
||||
if (evidence.profile !== "release") {
|
||||
throw new Error(`qa-evidence.json profile must be release, got ${JSON.stringify(evidence.profile)}`);
|
||||
}
|
||||
|
||||
const artifactDir = path.dirname(evidencePath);
|
||||
@@ -256,8 +256,8 @@ jobs:
|
||||
const manifestPath = path.join(artifactDir, manifestNames[0]);
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
||||
const manifestProfile = manifest.qaProfile ?? evidence.profile;
|
||||
if (manifestProfile !== "all") {
|
||||
throw new Error(`QA evidence manifest profile must be all, got ${JSON.stringify(manifestProfile)}`);
|
||||
if (manifestProfile !== "release") {
|
||||
throw new Error(`QA evidence manifest profile must be release, got ${JSON.stringify(manifestProfile)}`);
|
||||
}
|
||||
if (manifest.targetSha !== targetSha) {
|
||||
throw new Error(`QA evidence manifest targetSha ${manifest.targetSha} does not match selected ref ${targetSha}`);
|
||||
@@ -275,7 +275,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Run Codex maturity scorecard agent
|
||||
uses: openai/codex-action@10cb888d2ed3b99867f7e7ccff174a861a75aeb6
|
||||
uses: openai/codex-action@e0fdf01220eb9a88167c4898839d273e3f2609d1
|
||||
env:
|
||||
MATURITY_EVIDENCE_DIR: .artifacts/maturity-evidence
|
||||
MATURITY_SCORES_PATH: qa/maturity-scores.yaml
|
||||
@@ -428,14 +428,14 @@ jobs:
|
||||
cat > "$body_file" <<BODY
|
||||
## Summary
|
||||
|
||||
- render maturity scorecard docs from \`qa/maturity-scores.yaml\` and full taxonomy QA evidence
|
||||
- render maturity scorecard docs from \`qa/maturity-scores.yaml\` and release QA evidence
|
||||
- maturity source ref: ${REF_INPUT}
|
||||
- QA evidence run: ${evidence_run_id}
|
||||
|
||||
## Verification
|
||||
|
||||
- QA Lab maturity score validation passed
|
||||
- Maturity scorecard workflow rendered docs from all profile qa-evidence.json artifacts with strict inputs
|
||||
- Maturity scorecard workflow rendered docs from release profile qa-evidence.json artifacts with strict inputs
|
||||
BODY
|
||||
|
||||
pr_url="$(gh pr list --head "$branch" --state open --json url --jq '.[0].url // ""')"
|
||||
|
||||
2
.github/workflows/qa-profile-evidence.yml
vendored
2
.github/workflows/qa-profile-evidence.yml
vendored
@@ -18,7 +18,7 @@ on:
|
||||
qa_profile:
|
||||
description: Taxonomy QA profile id to run (for example release or all)
|
||||
required: true
|
||||
default: all
|
||||
default: release
|
||||
type: string
|
||||
workflow_call:
|
||||
inputs:
|
||||
|
||||
5
.github/workflows/sandbox-common-smoke.yml
vendored
5
.github/workflows/sandbox-common-smoke.yml
vendored
@@ -57,10 +57,11 @@ jobs:
|
||||
BASE_IMAGE="openclaw-sandbox-smoke-base:bookworm-slim" \
|
||||
TARGET_IMAGE="openclaw-sandbox-common-smoke:bookworm-slim" \
|
||||
PACKAGES="ca-certificates" \
|
||||
INSTALL_PNPM=0 \
|
||||
INSTALL_BUN=0 \
|
||||
INSTALL_BREW=0 \
|
||||
FINAL_USER=sandbox \
|
||||
scripts/sandbox-common-setup.sh
|
||||
|
||||
timeout --kill-after=30s 2m docker run --rm openclaw-sandbox-common-smoke:bookworm-slim sh -lc \
|
||||
'set -e; test "$(id -un)" = sandbox; node --version; pnpm --version'
|
||||
u="$(timeout --kill-after=30s 2m docker run --rm openclaw-sandbox-common-smoke:bookworm-slim sh -lc 'id -un')"
|
||||
test "$u" = "sandbox"
|
||||
|
||||
2
.github/workflows/test-performance-agent.yml
vendored
2
.github/workflows/test-performance-agent.yml
vendored
@@ -129,7 +129,7 @@ jobs:
|
||||
|
||||
- name: Run Codex test performance agent
|
||||
if: steps.gate.outputs.run_agent == 'true'
|
||||
uses: openai/codex-action@10cb888d2ed3b99867f7e7ccff174a861a75aeb6
|
||||
uses: openai/codex-action@e0fdf01220eb9a88167c4898839d273e3f2609d1
|
||||
with:
|
||||
openai-api-key: ${{ secrets.OPENCLAW_TEST_PERF_AGENT_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
|
||||
prompt-file: .github/codex/prompts/test-performance-agent.md
|
||||
|
||||
2
.github/workflows/workflow-sanity.yml
vendored
2
.github/workflows/workflow-sanity.yml
vendored
@@ -115,7 +115,7 @@ jobs:
|
||||
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
|
||||
@@ -118,11 +118,11 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- Tests in a normal source checkout: `pnpm test <path-or-filter> [vitest args...]`, `pnpm test:changed`, `pnpm test:serial`, `pnpm test:coverage`; never raw `vitest`.
|
||||
- If raw Vitest is unavoidable, use `vitest run ...`; bare `vitest ...` starts local watch mode and will not exit on its own.
|
||||
- Tests in a Codex worktree or linked/sparse checkout: avoid direct local `pnpm test*`; use `node scripts/run-vitest.mjs <path-or-filter>` for tiny explicit-file proof, or Crabbox/Testbox for anything broader.
|
||||
- Checks/lint in a normal source checkout: `pnpm check:changed` delegates to Crabbox/Testbox; lanes: `pnpm changed:lanes --json`; staged/path-scoped: `pnpm check:changed --staged` or `pnpm check:changed -- <files...>`; full `pnpm check`/`pnpm lint` only when required.
|
||||
- Checks in a normal source checkout: `pnpm check:changed` delegates to Crabbox/Testbox; lanes: `pnpm changed:lanes --json`; staged: `pnpm check:changed --staged`; full: `pnpm check`.
|
||||
- Checks in a Codex worktree or linked/sparse checkout: avoid direct local `pnpm check*`; use `node scripts/crabbox-wrapper.mjs run ... -- env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 corepack pnpm check:changed` so pnpm runs inside Testbox, not locally.
|
||||
- Extension tests: `pnpm test:extensions`, `pnpm test extensions`, `pnpm test extensions/<id>`.
|
||||
- Typecheck: `tsgo` lanes only (`pnpm tsgo*`, `pnpm check:test-types`); never add `tsc --noEmit`, `typecheck`, `check:types`.
|
||||
- Formatting: `oxfmt`, not Prettier. Use repo wrappers (`pnpm format:*`, `scripts/run-oxlint.mjs`; full `pnpm lint:*` only when scope requires).
|
||||
- Formatting: `oxfmt`, not Prettier. Use repo wrappers (`pnpm format:*`, `pnpm lint:*`, `scripts/run-oxlint.mjs`).
|
||||
- Build before push when build output, packaging, lazy/module boundaries, dynamic imports, or published surfaces can change.
|
||||
|
||||
## Validation
|
||||
|
||||
@@ -105,19 +105,6 @@ Reopen OpenClaw, confirm Talk is still active, then tap `Stop Talk`.
|
||||
4. Confirm at least one `agent` row is connected.
|
||||
5. Confirm the iPhone review device appears in the connected instances list.
|
||||
|
||||
## Live Activity / Dynamic Island
|
||||
|
||||
1. Tap `Settings`.
|
||||
2. Tap `Reconnect`.
|
||||
3. Immediately send OpenClaw to the background by returning to the Home Screen
|
||||
or locking the iPhone.
|
||||
4. Watch the Lock Screen or Dynamic Island while the Gateway reconnects.
|
||||
|
||||
Expected result: while reconnecting, iOS can show an `OpenClaw` Live Activity
|
||||
with connection status such as `Connecting...` or `Reconnecting...`. On a fast
|
||||
network this status may be brief because OpenClaw ends the Live Activity after
|
||||
the Gateway reconnects successfully.
|
||||
|
||||
## Push Notification
|
||||
|
||||
1. Tap the `Chat` tab.
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<key>NSCalendarsWriteOnlyAccessUsageDescription</key>
|
||||
<string>OpenClaw uses your calendars to add events when you enable calendar access.</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>OpenClaw uses the camera when you scan a Gateway setup QR code or ask your paired Gateway or assistant to capture a photo or short video from this iPhone, for example to connect to your Gateway or show your assistant a document, device screen, or workspace.</string>
|
||||
<string>OpenClaw can capture photos or short video clips when requested via the gateway.</string>
|
||||
<key>NSContactsUsageDescription</key>
|
||||
<string>OpenClaw uses your contacts so you can search and reference people while using the assistant.</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
|
||||
@@ -156,7 +156,7 @@ targets:
|
||||
NSAllowsLocalNetworking: true
|
||||
NSBonjourServices:
|
||||
- _openclaw-gw._tcp
|
||||
NSCameraUsageDescription: OpenClaw uses the camera when you scan a Gateway setup QR code or ask your paired Gateway or assistant to capture a photo or short video from this iPhone, for example to connect to your Gateway or show your assistant a document, device screen, or workspace.
|
||||
NSCameraUsageDescription: OpenClaw can capture photos or short video clips when requested via the gateway.
|
||||
NSCalendarsUsageDescription: OpenClaw uses your calendars to show events and scheduling context when you enable calendar access.
|
||||
NSCalendarsFullAccessUsageDescription: OpenClaw uses your calendars to show events and scheduling context when you enable calendar access.
|
||||
NSCalendarsWriteOnlyAccessUsageDescription: OpenClaw uses your calendars to add events when you enable calendar access.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
f5a5855ddd7aa8c23a732f257eceaa20fd163b1d5f342c909f4aef15aa8643cf config-baseline.json
|
||||
b8dffdb1a328aaf728a0707ab04d21c65f1a225a2360042e10832aa608699716 config-baseline.core.json
|
||||
9246475f5771612a5fd12de38b153783c4a4cbb8b2682a5c40115916661c90f2 config-baseline.json
|
||||
6349131baaa1828f2a071f42e4d7b17c8966c59b6588c8a4c1a32ea5ea4dcd5e config-baseline.core.json
|
||||
671979e86e4c4f59415d0a20879e838f9bbd883b3d29eeb02cb5131db8d187fe config-baseline.channel.json
|
||||
94529978588d6e3776a86780b22cf9ff46a6f9957f2f178d3829403fad451ca7 config-baseline.plugin.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
9d5b34975270bb2d16748002c1441ab48fde81af8eb12cc8eb3e341c862232ff plugin-sdk-api-baseline.json
|
||||
f1a6ff189498d955cad6d6fb912eb4cad7aeb628f89c51d0745e146fe0d163d6 plugin-sdk-api-baseline.jsonl
|
||||
212b76ef72779add8f18be4848e143e61b6ae42a1c7daeefdc42d91e0a1152e9 plugin-sdk-api-baseline.json
|
||||
976179e09e9e46a9b9259bd20ca1cafc8883c8e281a099a9aaa5fceab3c2983b plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -579,7 +579,7 @@ When `imsg launch` is running and `openclaw channels status --probe` reports `pr
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Read receipts and typing">
|
||||
When the private API bridge is up, accepted inbound chats are marked read and direct chats show a typing bubble as soon as the turn is accepted, while the agent prepares context and generates. Disable read-marking with:
|
||||
When the private API bridge is up, accepted inbound chats are marked read before dispatch and a typing bubble is shown to the sender while the agent generates. Disable read-marking with:
|
||||
|
||||
```json5
|
||||
{
|
||||
|
||||
@@ -30,7 +30,7 @@ or an explicit manual dispatch.
|
||||
| `security-fast` | Private key detection, changed-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, QA Smoke CI, and CI-routing checks | Node-relevant changes |
|
||||
| `checks-fast-core` | Fast Linux correctness lanes such as bundled, protocol, and CI-routing checks | Node-relevant changes |
|
||||
| `checks-fast-contracts-plugins-*` | Two sharded plugin contract checks | Node-relevant changes |
|
||||
| `checks-fast-contracts-channels-*` | Two sharded channel contract checks | Node-relevant changes |
|
||||
| `checks-node-core-*` | Core Node test shards, excluding channel, bundled, contract, and extension lanes | Node-relevant changes |
|
||||
|
||||
@@ -120,7 +120,6 @@ openclaw sessions cleanup --json
|
||||
|
||||
- Scope note: `openclaw sessions cleanup` maintains session stores, transcripts, and trajectory sidecars. It does not prune cron run history, which is managed by `cron.runLog.keepLines` in [Cron configuration](/automation/cron-jobs#configuration) and explained in [Cron maintenance](/automation/cron-jobs#maintenance).
|
||||
- Cleanup also prunes unreferenced primary transcripts, compaction checkpoints, and trajectory sidecars older than `session.maintenance.pruneAfter`; files still referenced by `sessions.json` are preserved.
|
||||
- Cleanup reports short-lived gateway model-run probe cleanup separately as `modelRunPruned`. This only matches strict explicit keys shaped like `agent:*:explicit:model-run-<uuid>`. The fixed retention is `24h`, but it is pressure-gated: it only removes stale probe rows when session-entry maintenance/cap pressure is reached. When it runs, model-run cleanup happens before global stale cleanup and capping.
|
||||
|
||||
- `--dry-run`: preview how many entries would be pruned/capped without writing.
|
||||
- In text mode, dry-run prints a per-session action table (`Action`, `Key`, `Age`, `Model`, `Flags`) plus a summary grouped by session label so you can see what would be kept vs removed.
|
||||
|
||||
@@ -127,14 +127,6 @@ in `enforce` mode and applies cleanup during maintenance. Set
|
||||
|
||||
For production-sized `maxEntries` limits, Gateway runtime writes use a small high-water buffer and clean back down to the configured cap in batches. Session store reads do not prune or cap entries during Gateway startup. This avoids running full store cleanup on every startup or isolated cron session. `openclaw sessions cleanup --enforce` applies the cap immediately.
|
||||
|
||||
Gateway model-run probe sessions are short-lived by default. Matching rows with
|
||||
strict explicit keys like `agent:*:explicit:model-run-<uuid>` use fixed `24h`
|
||||
retention, but cleanup is pressure-gated: it only removes stale probe rows when
|
||||
session-entry maintenance/cap pressure is reached. When model-run cleanup runs,
|
||||
it runs before the broader stale-entry age cutoff and entry cap. Normal direct,
|
||||
group, thread, cron, hook, heartbeat, ACP, and sub-agent sessions do not inherit
|
||||
this 24h retention.
|
||||
|
||||
Maintenance preserves durable external conversation pointers, including group
|
||||
sessions and thread-scoped chat sessions, while still allowing synthetic cron,
|
||||
hook, heartbeat, ACP, and sub-agent entries to age out.
|
||||
|
||||
@@ -15,8 +15,7 @@ When `agents.defaults.typingMode` is **unset**, OpenClaw keeps the legacy behavi
|
||||
|
||||
- **Direct chats**: typing starts immediately once the model loop begins.
|
||||
- **Group chats with a mention**: typing starts immediately.
|
||||
- **Group chats without a mention**: typing starts when the admitted run has
|
||||
user-visible activity, such as harness execution activity or message text.
|
||||
- **Group chats without a mention**: typing starts only when message text begins streaming.
|
||||
- **Heartbeat runs**: typing starts when the heartbeat run begins if the
|
||||
resolved heartbeat target is a typing-capable chat and typing is not disabled.
|
||||
|
||||
@@ -27,14 +26,13 @@ Set `agents.defaults.typingMode` to one of:
|
||||
- `never` - no typing indicator, ever.
|
||||
- `instant` - start typing **as soon as the model loop begins**, even if the run
|
||||
later returns only the silent reply token.
|
||||
- `thinking` - start typing on the **first reasoning delta** or on active
|
||||
harness execution after the turn is accepted.
|
||||
- `message` - start typing on the **first user-visible reply activity**, such as
|
||||
active harness execution or a non-silent text delta. Silent reply tokens such
|
||||
as `NO_REPLY` do not count as text activity.
|
||||
- `thinking` - start typing on the **first reasoning delta** (requires
|
||||
`reasoningLevel: "stream"` for the run).
|
||||
- `message` - start typing on the **first non-silent text delta** (ignores
|
||||
the `NO_REPLY` silent token).
|
||||
|
||||
Order of "how early it fires":
|
||||
`never` → `message`/`thinking` → `instant`
|
||||
`never` → `message` → `thinking` → `instant`
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -64,10 +62,11 @@ Override mode or cadence per session:
|
||||
|
||||
## Notes
|
||||
|
||||
- `message` mode does not start from silent reply tokens, but active execution
|
||||
can still show typing before any assistant text is available.
|
||||
- `thinking` still reacts to streamed reasoning (`reasoningLevel: "stream"`),
|
||||
and it can also start from active execution before reasoning deltas arrive.
|
||||
- `message` mode won't show typing for silent-only replies when the whole
|
||||
payload is the exact silent token (for example `NO_REPLY` / `no_reply`,
|
||||
matched case-insensitively).
|
||||
- `thinking` only fires if the run streams reasoning (`reasoningLevel: "stream"`).
|
||||
If the model doesn't emit reasoning deltas, typing won't start.
|
||||
- Heartbeat typing is a liveness signal for the resolved delivery target. It
|
||||
starts at heartbeat run start instead of following `message` or `thinking`
|
||||
stream timing. Set `typingMode: "never"` to disable it.
|
||||
|
||||
@@ -30,68 +30,6 @@ title: "Usage tracking"
|
||||
- CLI: `openclaw channels list` prints the same usage snapshot alongside provider config (use `--no-usage` to skip).
|
||||
- macOS menu bar: "Usage" section under Context (only if available).
|
||||
|
||||
## Default usage footer mode
|
||||
|
||||
`/usage off|tokens|full` sets the footer for a session and is remembered for that
|
||||
session. `messages.responseUsage` seeds that mode for sessions that have not
|
||||
chosen one, so the footer can be on by default without typing `/usage` each time.
|
||||
|
||||
Set one mode for every channel, or a per-channel map with a `default` fallback:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"messages": {
|
||||
"responseUsage": "tokens",
|
||||
// or: { "default": "off", "discord": "full" }
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Three distinct session states
|
||||
|
||||
A session's `responseUsage` field has three representable states, each with
|
||||
different semantics:
|
||||
|
||||
| State | Stored value | Effective mode |
|
||||
| ------------------- | ------------------------------- | --------------------------------------------------------------------- |
|
||||
| **Unset / inherit** | `undefined` (absent) | Falls through to `messages.responseUsage` config default, then `off`. |
|
||||
| **Explicit off** | `"off"` (stored) | Always off — a non-off config default cannot re-enable the footer. |
|
||||
| **Explicit on** | `"tokens"` or `"full"` (stored) | That mode, regardless of config default. |
|
||||
|
||||
### Precedence
|
||||
|
||||
Effective mode = session override → channel config entry → `default` → `off`.
|
||||
|
||||
An explicit `/usage off` is **persisted** as the literal value `"off"` in the
|
||||
session, not the same as "unset." This means a non-off `messages.responseUsage`
|
||||
default cannot turn the footer back on once the user has explicitly disabled it.
|
||||
|
||||
### Resetting vs. turning off
|
||||
|
||||
- `/usage off` — forces the footer off and persists that choice. A configured
|
||||
non-off default cannot override this.
|
||||
- `/usage reset` (aliases: `inherit`, `clear`, `default`) — clears the session
|
||||
override. The session then **inherits** the effective config default
|
||||
(`messages.responseUsage`). If no default is configured, the footer is off
|
||||
(unchanged from before). Use this to "go back to default" without explicitly
|
||||
turning the footer on.
|
||||
- A full session reset (`/reset` or `/new`) or a session rollover **preserves**
|
||||
the explicit usage-mode preference so the user's display choice survives
|
||||
session rollovers. Only `/usage reset` (and its aliases) actually clears the
|
||||
override.
|
||||
|
||||
### Toggle behavior
|
||||
|
||||
`/usage` with no arguments cycles: off → tokens → full → off. The starting point
|
||||
for the cycle is the **effective** current mode (session override falling through
|
||||
to the config default when unset), so the cycle is always consistent with what
|
||||
the user sees in the footer.
|
||||
|
||||
### Config
|
||||
|
||||
With no config the prior behavior holds (footer off until `/usage`). Use
|
||||
`/usage reset` to clear a session override and re-inherit the configured default.
|
||||
|
||||
## Custom `/usage full` footer
|
||||
|
||||
`/usage full` shows a built-in compact footer with model, reasoning, fast/slow,
|
||||
|
||||
@@ -1316,7 +1316,6 @@ See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for preceden
|
||||
- `mode`: `enforce` applies cleanup and is the default; `warn` emits warnings only.
|
||||
- `pruneAfter`: age cutoff for stale entries (default `30d`).
|
||||
- `maxEntries`: maximum number of entries in `sessions.json` (default `500`). Runtime writes batch cleanup with a small high-water buffer for production-sized caps; `openclaw sessions cleanup --enforce` applies the cap immediately.
|
||||
- Short-lived gateway model-run probe sessions use fixed `24h` retention, but cleanup is pressure-gated: it only removes stale strict model-run probe rows when session-entry maintenance/cap pressure is reached. Only strict explicit probe keys matching `agent:*:explicit:model-run-<uuid>` are eligible; normal direct, group, thread, cron, hook, heartbeat, ACP, and sub-agent sessions do not inherit this 24h retention. When model-run cleanup runs, it runs before the broader `pruneAfter` stale-entry cleanup and `maxEntries` cap.
|
||||
- `rotateBytes`: deprecated and ignored; `openclaw doctor --fix` removes it from older configs.
|
||||
- `resetArchiveRetention`: retention for `*.reset.<timestamp>` transcript archives. Defaults to `pruneAfter`; set `false` to disable.
|
||||
- `maxDiskBytes`: optional sessions-directory disk budget. In `warn` mode it logs warnings; in `enforce` mode it removes oldest artifacts/sessions first.
|
||||
|
||||
@@ -415,7 +415,7 @@ If you installed OpenClaw via `npm install -g openclaw`, use the inline `docker
|
||||
|
||||
</Step>
|
||||
<Step title="Optional: build the common image">
|
||||
For a more functional sandbox image with common tooling (for example `curl`, `jq`, Node 24, pnpm, `python3`, and `git`):
|
||||
For a more functional sandbox image with common tooling (for example `curl`, `jq`, `nodejs`, `python3`, `git`):
|
||||
|
||||
From a source checkout:
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -308,7 +308,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Normal setup and repair paths are documented across install, CLI, and gateway docs. Platform-specific Windows paths are tracked in the Windows via WSL2 and Native Windows rows.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 4%</span><span>Quality Stable - 83%</span><span>Completeness Stable - 90%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 6</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 2%</span><span>Quality Stable - 83%</span><span>Completeness Stable - 90%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 6</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -317,7 +317,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">CLI Setup</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>17%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "17%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Index](/install/index), [Installer](/install/installer), [Node](/install/node), [Updating](/install/updating)</div>
|
||||
@@ -327,7 +327,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Onboarding and Auth Setup</span>
|
||||
<span>5 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>75%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "75%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Onboard](/cli/onboard), [Configure](/cli/configure), [Onboarding Overview](/start/onboarding-overview)</div>
|
||||
@@ -337,7 +337,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Plugin and Channel Setup</span>
|
||||
<span>5 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>75%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "75%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Onboard](/cli/onboard), [Plugins](/cli/plugins), [Channels](/cli/channels)</div>
|
||||
@@ -347,7 +347,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Gateway Service Management</span>
|
||||
<span>5 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>14%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "14%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>87%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "87%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Gateway](/cli/gateway), [Updating](/install/updating), [Troubleshooting](/gateway/troubleshooting)</div>
|
||||
@@ -357,7 +357,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">CLI Observability</span>
|
||||
<span>5 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Status](/cli/status), [Health](/cli/health), [Logs](/cli/logs), [Diagnostics](/gateway/diagnostics)</div>
|
||||
@@ -367,7 +367,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Doctor</span>
|
||||
<span>10 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Doctor](/cli/doctor), [Doctor](/gateway/doctor), [Secrets](/gateway/secrets), [Troubleshooting](/gateway/troubleshooting)</div>
|
||||
@@ -377,7 +377,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Updates and Upgrades</span>
|
||||
<span>5 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>75%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "75%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Updating](/install/updating), [Update](/cli/update), [Troubleshooting](/gateway/troubleshooting)</div>
|
||||
@@ -391,7 +391,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Core architecture, auth, pairing, protocol docs, daemon docs, and CLI runbooks are broad and current.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 6%</span><span>Quality Stable - 81%</span><span>Completeness Stable - 89%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 12</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 3%</span><span>Quality Stable - 81%</span><span>Completeness Stable - 89%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 12</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -400,7 +400,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Approvals and Remote Execution</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>75%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "75%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Protocol](/gateway/protocol), [Index](/gateway/security/index)</div>
|
||||
@@ -410,7 +410,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">HTTP APIs</span>
|
||||
<span>4 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>25%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "25%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Index](/gateway/index), [Openai Http Api](/gateway/openai-http-api), [Openresponses Http Api](/gateway/openresponses-http-api), [Tools Invoke Http Api](/gateway/tools-invoke-http-api), [Hooks](/automation/hooks), [Index](/web/index)</div>
|
||||
@@ -420,7 +420,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Hosted Web Surface</span>
|
||||
<span>4 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Index](/gateway/index), [Architecture](/concepts/architecture), [Control Ui](/web/control-ui), [Webchat](/web/webchat), [Canvas](/refactor/canvas)</div>
|
||||
@@ -430,7 +430,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Gateway RPC APIs and Events</span>
|
||||
<span>20 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>9%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "9%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Protocol](/gateway/protocol), [Index](/gateway/index), [Architecture](/concepts/architecture)</div>
|
||||
@@ -440,7 +440,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Device Auth and Pairing</span>
|
||||
<span>10 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>75%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "75%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Protocol](/gateway/protocol), [Pairing](/gateway/pairing), [Index](/gateway/security/index)</div>
|
||||
@@ -450,7 +450,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Network Access and Discovery</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>75%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "75%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Index](/gateway/index), [Discovery](/gateway/discovery), [Protocol](/gateway/protocol)</div>
|
||||
@@ -460,7 +460,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Nodes and Remote Capabilities</span>
|
||||
<span>8 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>75%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "75%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Protocol](/gateway/protocol), [Architecture](/concepts/architecture), [Index](/nodes/index)</div>
|
||||
@@ -470,7 +470,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Health, Diagnostics, and Repair</span>
|
||||
<span>7 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>75%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "75%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Index](/gateway/index), [Diagnostics](/gateway/diagnostics), [Doctor](/gateway/doctor)</div>
|
||||
@@ -480,7 +480,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Protocol Compatibility</span>
|
||||
<span>7 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>75%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "75%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Protocol](/gateway/protocol), [Architecture](/concepts/architecture), [Typebox](/concepts/typebox), [Bridge Protocol](/gateway/bridge-protocol)</div>
|
||||
@@ -490,7 +490,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Roles and Permissions</span>
|
||||
<span>5 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>75%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "75%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Protocol](/gateway/protocol), [Index](/gateway/security/index)</div>
|
||||
@@ -500,7 +500,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Gateway Lifecycle</span>
|
||||
<span>7 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>33%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "33%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Index](/gateway/index), [Architecture](/concepts/architecture)</div>
|
||||
@@ -510,7 +510,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Security Controls</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>75%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "75%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Index](/gateway/security/index), [Protocol](/gateway/protocol), [Discovery](/gateway/discovery)</div>
|
||||
@@ -520,7 +520,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">WebSocket Connection</span>
|
||||
<span>8 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>13%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "13%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Protocol](/gateway/protocol), [Architecture](/concepts/architecture)</div>
|
||||
@@ -534,7 +534,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Main loop, models, provider routing, and tool streaming are first-class, but provider behavior shifts weekly and needs scenario proof per release.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 33%</span><span>Quality Beta - 78%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 6</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 2%</span><span>Quality Beta - 78%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 6</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -543,7 +543,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Agent Turn Execution</span>
|
||||
<span>3 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>29%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "29%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Agent Loop](/concepts/agent-loop), [Agent](/cli/agent), [Agent Runtimes](/concepts/agent-runtimes)</div>
|
||||
@@ -553,7 +553,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">External Runtimes and Subagents</span>
|
||||
<span>4 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>30%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "30%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Agent Runtimes](/concepts/agent-runtimes), [Anthropic](/providers/anthropic), [Google](/providers/google), [Subagents](/tools/subagents)</div>
|
||||
@@ -563,7 +563,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Hosted Provider Execution</span>
|
||||
<span>5 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>20%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "20%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Openai](/providers/openai), [Anthropic](/providers/anthropic), [Google](/providers/google), [Models](/concepts/models)</div>
|
||||
@@ -573,7 +573,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Local and Self-hosted Providers</span>
|
||||
<span>5 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Ollama](/providers/ollama), [Models](/concepts/models), [Agent](/cli/agent)</div>
|
||||
@@ -583,7 +583,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Model and Runtime Selection</span>
|
||||
<span>4 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>25%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "25%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Models](/concepts/models), [Models](/cli/models), [Openai](/providers/openai), [Agent Runtimes](/concepts/agent-runtimes)</div>
|
||||
@@ -593,7 +593,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Provider Auth</span>
|
||||
<span>10 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>24%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "24%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Models](/concepts/models), [Agent](/cli/agent), [Models](/cli/models), [Openai](/providers/openai), [Anthropic](/providers/anthropic), [Google](/providers/google), [Subagents](/tools/subagents)</div>
|
||||
@@ -603,7 +603,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Streaming and Progress</span>
|
||||
<span>2 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>56%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "56%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Streaming](/concepts/streaming), [Agent Loop](/concepts/agent-loop)</div>
|
||||
@@ -613,7 +613,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Tool Calls and Response Handling</span>
|
||||
<span>3 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>65%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "65%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Agent Loop](/concepts/agent-loop), [Ollama](/providers/ollama)</div>
|
||||
@@ -623,7 +623,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Tool Execution Controls</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>50%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "50%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Sandbox Vs Tool Policy Vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated), [Agent Loop](/concepts/agent-loop), [Subagents](/tools/subagents)</div>
|
||||
@@ -637,7 +637,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Strong docs and active implementation. Maturity depends on transcript durability, compaction quality, and cross-client parity.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 30%</span><span>Quality Beta - 77%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 6</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 0%</span><span>Quality Beta - 77%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 6</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -656,7 +656,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Token Management</span>
|
||||
<span>3 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>20%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "20%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Compaction](/concepts/compaction), [Context](/concepts/context), [Session Management Compaction](/reference/session-management-compaction)</div>
|
||||
@@ -666,7 +666,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Context Engine</span>
|
||||
<span>2 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>57%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "57%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Context](/concepts/context), [Context Engine](/concepts/context-engine), [Codex Context Engine Harness](/plan/codex-context-engine-harness)</div>
|
||||
@@ -676,7 +676,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Cross-client History and Session Parity</span>
|
||||
<span>2 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>40%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "40%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Webchat](/web/webchat), [Android](/platforms/android), [Channel Routing](/channels/channel-routing)</div>
|
||||
@@ -686,7 +686,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Diagnostics, Maintenance, and Recovery</span>
|
||||
<span>3 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>40%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "40%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Diagnostics](/gateway/diagnostics), [Session Management Compaction](/reference/session-management-compaction), [Flags](/diagnostics/flags)</div>
|
||||
@@ -696,7 +696,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Core Prompts and Context</span>
|
||||
<span>2 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>38%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "38%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Context](/concepts/context), [Transcript Hygiene](/reference/transcript-hygiene), [Discord](/channels/discord)</div>
|
||||
@@ -706,7 +706,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Memory</span>
|
||||
<span>5 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>46%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "46%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Memory Config](/reference/memory-config), [Memory Qmd](/concepts/memory-qmd), [Memory](/concepts/memory), [Discord](/channels/discord)</div>
|
||||
@@ -716,7 +716,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Session Routing</span>
|
||||
<span>2 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>25%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "25%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Session](/concepts/session), [Channel Routing](/channels/channel-routing), [Discord](/channels/discord)</div>
|
||||
@@ -740,7 +740,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Many channels share Gateway delivery and routing contracts, but channel behavior varies by upstream API and account-policy constraints.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 13%</span><span>Quality Beta - 76%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 5</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 0%</span><span>Quality Beta - 76%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 5</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -759,7 +759,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Channel Setup</span>
|
||||
<span>5 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>14%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "14%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Index](/channels/index), [Pairing](/channels/pairing), [Troubleshooting](/channels/troubleshooting), [Sdk Channel Plugins](/plugins/sdk-channel-plugins)</div>
|
||||
@@ -769,7 +769,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Group Thread and Ambient Room Behavior</span>
|
||||
<span>5 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>36%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "36%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Groups](/channels/groups), [Group Messages](/channels/group-messages), [Ambient Room Events](/channels/ambient-room-events), [Broadcast Groups](/channels/broadcast-groups), [Discord](/channels/discord)</div>
|
||||
@@ -799,7 +799,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Outbound Delivery and Reply Pipeline</span>
|
||||
<span>4 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>38%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "38%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Groups](/channels/groups), [Ambient Room Events](/channels/ambient-room-events), [Discord](/channels/discord), [Matrix](/channels/matrix), [Config Channels](/gateway/config-channels)</div>
|
||||
@@ -809,7 +809,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Conversation Routing and Delivery</span>
|
||||
<span>10 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>19%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "19%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Channel Routing](/channels/channel-routing), [Groups](/channels/groups), [Discord](/channels/discord), [Matrix](/channels/matrix), [Troubleshooting](/channels/troubleshooting), [Configuration Reference](/gateway/configuration-reference)</div>
|
||||
@@ -833,7 +833,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
OTel, Prometheus, logging, and diagnostics docs exist. Needs a public "what operators should look at first" maturity pass.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 18%</span><span>Quality Beta - 75%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 3</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 6%</span><span>Quality Beta - 75%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 3</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -842,7 +842,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Health and Repair</span>
|
||||
<span>12 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>28%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "28%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>6%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "6%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Health](/gateway/health), [Telegram](/channels/telegram), [Doctor](/cli/doctor), [Doctor](/gateway/doctor), [Sdk Subpaths](/plugins/sdk-subpaths), [Health](/cli/health), [Protocol](/gateway/protocol)</div>
|
||||
@@ -852,7 +852,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Logging</span>
|
||||
<span>5 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>6%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "6%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Logging](/logging), [Logging](/gateway/logging), [Logs](/cli/logs)</div>
|
||||
@@ -862,7 +862,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Diagnostic Collection</span>
|
||||
<span>8 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>30%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "30%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>6%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "6%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Diagnostics](/gateway/diagnostics), [Health](/gateway/health), [Codex Harness](/plugins/codex-harness), [Protocol](/gateway/protocol)</div>
|
||||
@@ -872,7 +872,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Telemetry Export</span>
|
||||
<span>13 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>33%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "33%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>6%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "6%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Hooks](/plugins/hooks), [Opentelemetry](/gateway/opentelemetry), [Logging](/logging), [Sdk Subpaths](/plugins/sdk-subpaths), [Diagnostics Otel](/plugins/reference/diagnostics-otel), [Prometheus](/gateway/prometheus), [Diagnostics Prometheus](/plugins/reference/diagnostics-prometheus)</div>
|
||||
@@ -882,7 +882,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Session Diagnostics</span>
|
||||
<span>4 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>6%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "6%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Opentelemetry](/gateway/opentelemetry), [Prometheus](/gateway/prometheus), [Diagnostics](/gateway/diagnostics), [Protocol](/gateway/protocol)</div>
|
||||
@@ -896,7 +896,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Web UI is documented with pairing, chat, PWA, Talk, push, and remote Gateway flows. Promote after cross-browser and mobile-PWA scorecards.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 4%</span><span>Quality Beta - 74%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-none">None</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 0%</span><span>Quality Beta - 74%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-none">None</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -935,7 +935,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Browser UI</span>
|
||||
<span>10 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>8%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "8%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Control Ui](/web/control-ui), [Index](/web/index), [Dashboard](/web/dashboard), [Protocol](/gateway/protocol)</div>
|
||||
@@ -945,7 +945,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">WebChat Conversations</span>
|
||||
<span>15 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>10%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "10%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Control Ui](/web/control-ui), [Webchat](/web/webchat), [Getting Started](/start/getting-started), [Channel Routing](/channels/channel-routing), [Secure File Operations](/gateway/security/secure-file-operations)</div>
|
||||
@@ -955,7 +955,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Operator Console</span>
|
||||
<span>10 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>8%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "8%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Control Ui](/web/control-ui), [Health](/gateway/health), [Protocol](/gateway/protocol), [Dashboard](/web/dashboard)</div>
|
||||
@@ -969,7 +969,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Broad docs and strong internal runtime evidence exist across manifests, discovery, loading, provider/tool architecture, and approval boundaries. Keep the row at beta until public SDK API/subpaths and external distribution proof are stronger.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 12%</span><span>Quality Beta - 72%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 7</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 2%</span><span>Quality Beta - 72%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 7</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -978,7 +978,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Authoring and Packaging plugins</span>
|
||||
<span>8 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Building Plugins](/plugins/building-plugins), [Sdk Overview](/plugins/sdk-overview), [Sdk Entrypoints](/plugins/sdk-entrypoints), [Sdk Subpaths](/plugins/sdk-subpaths), [Manifest](/plugins/manifest), [Reference](/plugins/reference)</div>
|
||||
@@ -988,7 +988,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Bundled plugins</span>
|
||||
<span>5 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Plugin Inventory](/plugins/plugin-inventory), [Plugins](/cli/plugins), [Architecture Internals](/plugins/architecture-internals)</div>
|
||||
@@ -998,7 +998,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Canvas plugin</span>
|
||||
<span>6 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Canvas](/plugins/reference/canvas), [Canvas](/refactor/canvas), [Configuration Reference](/gateway/configuration-reference)</div>
|
||||
@@ -1008,7 +1008,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Installing and running plugins</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>35%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "35%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Architecture](/plugins/architecture), [Architecture Internals](/plugins/architecture-internals), [Plugins](/cli/plugins)</div>
|
||||
@@ -1018,7 +1018,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Channel plugins</span>
|
||||
<span>5 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Sdk Channel Plugins](/plugins/sdk-channel-plugins), [Sdk Channel Inbound](/plugins/sdk-channel-inbound), [Sdk Channel Outbound](/plugins/sdk-channel-outbound)</div>
|
||||
@@ -1028,7 +1028,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Provider and tool plugins</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>43%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "43%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Sdk Provider Plugins](/plugins/sdk-provider-plugins), [Tool Plugins](/plugins/tool-plugins), [Adding Capabilities](/plugins/adding-capabilities)</div>
|
||||
@@ -1038,7 +1038,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Plugin approvals</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Plugin Permission Requests](/plugins/plugin-permission-requests), [Exec Approvals](/tools/exec-approvals), [Sdk Channel Plugins](/plugins/sdk-channel-plugins)</div>
|
||||
@@ -1048,7 +1048,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Publishing plugins</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Plugins](/cli/plugins), [Compatibility](/plugins/compatibility), [Publishing](/clawhub/publishing)</div>
|
||||
@@ -1058,7 +1058,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Testing plugins</span>
|
||||
<span>6 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>27%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "27%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Sdk Testing](/plugins/sdk-testing), [Sdk Setup](/plugins/sdk-setup), [Codex Harness](/plugins/codex-harness)</div>
|
||||
@@ -1072,7 +1072,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Good docs and hardening surfaces exist. Promote after regular upgrade/security scenario runs prove no setup regressions.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 16%</span><span>Quality Beta - 72%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 5</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 0%</span><span>Quality Beta - 72%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 5</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -1081,7 +1081,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Approval Policy and Tool Safeguards</span>
|
||||
<span>2 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>50%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "50%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Exec Approvals](/tools/exec-approvals), [Approvals](/cli/approvals), [Plugin Permission Requests](/plugins/plugin-permission-requests), [Audit Checks](/gateway/security/audit-checks)</div>
|
||||
@@ -1131,7 +1131,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Credential and Secret Hygiene</span>
|
||||
<span>5 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>46%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "46%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Authentication](/gateway/authentication), [Models](/cli/models), [Openai](/providers/openai), [Oauth](/concepts/oauth), [Secrets](/gateway/secrets), [Secrets](/cli/secrets), [Secretref Credential Surface](/reference/secretref-credential-surface), [Audit Checks](/gateway/security/audit-checks)</div>
|
||||
@@ -1145,7 +1145,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Documented and usable, but scenario proof should cover unattended delivery, retries, and failure visibility.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 2%</span><span>Quality Beta - 72%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-none">None</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 0%</span><span>Quality Beta - 72%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-none">None</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -1194,7 +1194,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Heartbeat</span>
|
||||
<span>5 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>14%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "14%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Index](/automation/index), [Heartbeat](/gateway/heartbeat), [Commitments](/concepts/commitments)</div>
|
||||
@@ -1218,7 +1218,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Broad capability surface exists, but provider variance, file limits, and node/app parity make this not stable yet.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 2%</span><span>Quality Alpha - 64%</span><span>Completeness Alpha - 68%</span><span><span className="maturity-lts maturity-lts-none">None</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 1%</span><span>Quality Alpha - 64%</span><span>Completeness Alpha - 68%</span><span><span className="maturity-lts maturity-lts-none">None</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -1227,7 +1227,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Media Intake and Access</span>
|
||||
<span>8 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>1%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "1%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>61%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "61%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Media Overview](/tools/media-overview), [Media Understanding](/nodes/media-understanding), [Secure File Operations](/gateway/security/secure-file-operations), [Pdf](/tools/pdf), [Image Generation](/tools/image-generation), [Qr](/cli/qr), [Line](/channels/line), [Whatsapp](/channels/whatsapp)</div>
|
||||
@@ -1237,7 +1237,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Channel Media Handling</span>
|
||||
<span>5 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>1%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "1%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>61%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "61%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Images](/nodes/images), [Media Overview](/tools/media-overview), [Discord](/channels/discord)</div>
|
||||
@@ -1247,7 +1247,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Media Configuration</span>
|
||||
<span>1 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>1%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "1%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>61%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "61%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Media Overview](/tools/media-overview), [Image Generation](/tools/image-generation), [Manifest](/plugins/manifest), [Codex Harness](/plugins/codex-harness)</div>
|
||||
@@ -1257,7 +1257,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Text-to-Speech Delivery</span>
|
||||
<span>2 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>1%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "1%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>61%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "61%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Tts](/tools/tts), [Media Overview](/tools/media-overview), [Discord](/channels/discord)</div>
|
||||
@@ -1267,7 +1267,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Media Understanding</span>
|
||||
<span>12 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>7%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "7%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>1%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "1%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>69%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "69%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>69%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "69%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Audio](/nodes/audio), [Media Understanding](/nodes/media-understanding), [Media Overview](/tools/media-overview), [Whatsapp](/channels/whatsapp), [Images](/nodes/images), [Infer](/cli/infer), [Pdf](/tools/pdf)</div>
|
||||
@@ -1277,7 +1277,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Media Generation</span>
|
||||
<span>17 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>5%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "5%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>1%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "1%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>69%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "69%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>69%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "69%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Image Generation](/tools/image-generation), [Media Overview](/tools/media-overview), [Skills](/tools/skills), [Music Generation](/tools/music-generation), [Video Generation](/tools/video-generation)</div>
|
||||
@@ -1480,7 +1480,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
OpenClaw App SDK is a distinct external app contract separate from Gateway runtime and Plugin SDK. Current scoring shows a real `@openclaw/sdk` path with gaps around public packaging, auto-discovery, approvals, helpers, and compatibility.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 3%</span><span>Quality Alpha - 54%</span><span>Completeness Alpha - 53%</span><span><span className="maturity-lts maturity-lts-none">None</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 0%</span><span>Quality Alpha - 54%</span><span>Completeness Alpha - 53%</span><span><span className="maturity-lts maturity-lts-none">None</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -1529,7 +1529,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Resource Helpers</span>
|
||||
<span>5 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>17%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "17%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>62%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "62%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>53%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "53%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Openclaw Sdk](/gateway/external-apps), [Openclaw Sdk Api Design](/gateway/external-apps)</div>
|
||||
@@ -1704,7 +1704,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Install docs exist and are common deployment paths. Promote after recurring release smoke captures upgrade and volume behavior.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 7%</span><span>Quality Beta - 71%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-none">None</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 5%</span><span>Quality Beta - 71%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-none">None</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -1713,7 +1713,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Container Setup</span>
|
||||
<span>6 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>5%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "5%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Docker](/install/docker), [Podman](/install/podman)</div>
|
||||
@@ -1723,7 +1723,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Container Operations</span>
|
||||
<span>11 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>5%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "5%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Podman](/install/podman), [Docker Vm Runtime](/install/docker-vm-runtime), [Docker](/install/docker), [Hetzner](/install/hetzner), [Hostinger](/install/hostinger)</div>
|
||||
@@ -1733,7 +1733,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Image Release and Validation</span>
|
||||
<span>5 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>29%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "29%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>5%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "5%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Docker](/install/docker), [Docker Vm Runtime](/install/docker-vm-runtime), [Full Release Validation](/reference/full-release-validation)</div>
|
||||
@@ -1743,7 +1743,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Agent Sandbox and Tooling</span>
|
||||
<span>3 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>5%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "5%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Docker](/install/docker), [Docker Vm Runtime](/install/docker-vm-runtime)</div>
|
||||
@@ -1757,7 +1757,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Recommended Windows path with systemd/user-service guidance and boot-chain docs. Promote after repeated install/update scorecards.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 6%</span><span>Quality Alpha - 69%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 5</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 3%</span><span>Quality Alpha - 69%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 5</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -1766,7 +1766,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">WSL Setup</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>67%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "67%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Windows](/platforms/windows), [Getting Started](/start/getting-started)</div>
|
||||
@@ -1776,7 +1776,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">CLI</span>
|
||||
<span>8 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>67%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "67%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Windows](/platforms/windows), [Getting Started](/start/getting-started), [Updating](/install/updating), [Onboard](/cli/onboard), [Doctor](/cli/doctor), [Status](/cli/status), [Logs](/cli/logs)</div>
|
||||
@@ -1786,7 +1786,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Gateway Service Lifecycle</span>
|
||||
<span>10 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>67%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "67%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Windows](/platforms/windows), [Index](/gateway/index), [Doctor](/gateway/doctor)</div>
|
||||
@@ -1796,7 +1796,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Gateway Access and Exposure</span>
|
||||
<span>11 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>67%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "67%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Authentication](/gateway/authentication), [Secrets](/gateway/secrets), [Remote](/gateway/remote), [Exposure Runbook](/gateway/security/exposure-runbook), [Windows](/platforms/windows)</div>
|
||||
@@ -1806,7 +1806,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Diagnostics and Repair</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>38%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "38%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Windows](/platforms/windows), [Status](/cli/status), [Logs](/cli/logs), [Doctor](/cli/doctor), [Doctor](/gateway/doctor)</div>
|
||||
@@ -1816,7 +1816,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Browser and Control UI</span>
|
||||
<span>6 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>67%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "67%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Browser Wsl2 Windows Remote Cdp Troubleshooting](/tools/browser-wsl2-windows-remote-cdp-troubleshooting), [Browser](/tools/browser), [Control Ui](/web/control-ui)</div>
|
||||
@@ -3276,7 +3276,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Core tools are documented, but host security and permission UX should stay under active scorecard review.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 21%</span><span>Quality Beta - 75%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 2</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 15%</span><span>Quality Beta - 75%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 2</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -3285,7 +3285,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Browser Automation</span>
|
||||
<span>8 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>13%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "13%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>15%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "15%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Browser Control](/tools/browser-control), [Testing](/help/testing), [Browser](/tools/browser), [Index](/gateway/security/index), [Audit Checks](/gateway/security/audit-checks)</div>
|
||||
@@ -3295,7 +3295,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Tool Invocation and Execution</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>50%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "50%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>15%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "15%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Exec](/tools/exec), [Background Process](/gateway/background-process), [Tools Invoke Http Api](/gateway/tools-invoke-http-api), [Operator Scopes](/gateway/operator-scopes), [Protocol](/gateway/protocol), [Exec Approvals](/tools/exec-approvals), [Exec Approvals Advanced](/tools/exec-approvals-advanced), [Elevated](/tools/elevated)</div>
|
||||
@@ -3305,7 +3305,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Sandbox and Tool Policy</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>15%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "15%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Sandboxing](/gateway/sandboxing), [Sandbox Vs Tool Policy Vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated), [Multi Agent Sandbox Tools](/tools/multi-agent-sandbox-tools), [Codex Harness Reference](/plugins/codex-harness-reference), [Config Tools](/gateway/config-tools)</div>
|
||||
@@ -3319,7 +3319,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Deep docs, OAuth/subscription path, realtime voice, image, and compatibility behavior. Provider churn keeps this from Stable without release-scorecard proof.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 26%</span><span>Quality Beta - 74%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 3</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 8%</span><span>Quality Beta - 74%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 3</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -3328,7 +3328,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Model and Auth</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>44%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "44%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>8%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "8%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Openai](/providers/openai), [Codex Harness](/plugins/codex-harness), [Models](/concepts/models), [Oauth](/concepts/oauth), [Codex Harness Reference](/plugins/codex-harness-reference), [Auth Monitoring](/automation/auth-monitoring)</div>
|
||||
@@ -3338,7 +3338,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Responses and Tool Compatibility</span>
|
||||
<span>4 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>40%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "40%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>8%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "8%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Openai](/providers/openai), [Openresponses Http Api](/gateway/openresponses-http-api), [Openai Http Api](/gateway/openai-http-api), [Codex Native Plugins](/plugins/codex-native-plugins)</div>
|
||||
@@ -3348,7 +3348,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Native Codex Harness</span>
|
||||
<span>2 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>44%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "44%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>8%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "8%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Codex Harness](/plugins/codex-harness), [Codex Harness Runtime](/plugins/codex-harness-runtime), [Codex Harness Reference](/plugins/codex-harness-reference), [Codex Native Plugins](/plugins/codex-native-plugins)</div>
|
||||
@@ -3358,7 +3358,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Image and Multimodal Input</span>
|
||||
<span>2 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>8%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "8%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>67%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "67%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Openai](/providers/openai), [Image Generation](/tools/image-generation), [Images](/nodes/images)</div>
|
||||
@@ -3368,7 +3368,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Voice and Realtime Audio</span>
|
||||
<span>2 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>8%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "8%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>67%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "67%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Openai](/providers/openai), [Discord](/channels/discord), [Voice Call](/plugins/voice-call)</div>
|
||||
@@ -3382,7 +3382,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Multiple providers and docs exist. Needs quota/error/SSRF proof per provider family.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 9%</span><span>Quality Beta - 74%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-none">None</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 7%</span><span>Quality Beta - 74%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-none">None</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -3391,7 +3391,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Search Providers</span>
|
||||
<span>19 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>11%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "11%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>7%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "7%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Web](/tools/web), [Brave Search](/tools/brave-search), [Tavily](/tools/tavily), [Exa Search](/tools/exa-search), [Firecrawl](/tools/firecrawl), [Perplexity Search](/tools/perplexity-search), [Duckduckgo Search](/tools/duckduckgo-search), [Searxng Search](/tools/searxng-search), [Gemini Search](/tools/gemini-search), [Grok Search](/tools/grok-search), [Kimi Search](/tools/kimi-search), [Minimax Search](/tools/minimax-search), [Ollama Search](/tools/ollama-search), [Sdk Subpaths](/plugins/sdk-subpaths), [Sdk Overview](/plugins/sdk-overview), [Manifest](/plugins/manifest)</div>
|
||||
@@ -3401,7 +3401,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Setup and Diagnostics</span>
|
||||
<span>9 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>7%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "7%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Web](/tools/web), [Web Fetch](/tools/web-fetch), [Faq](/help/faq), [Api Usage Costs](/reference/api-usage-costs), [Brave Search](/tools/brave-search), [Perplexity Search](/tools/perplexity-search), [Tavily](/tools/tavily), [Firecrawl](/tools/firecrawl)</div>
|
||||
@@ -3411,7 +3411,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Network Safety</span>
|
||||
<span>4 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>7%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "7%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Web](/tools/web), [Web Fetch](/tools/web-fetch), [Firecrawl](/tools/firecrawl), [Searxng Search](/tools/searxng-search)</div>
|
||||
@@ -3421,7 +3421,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Tool Availability and Fetch</span>
|
||||
<span>11 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>25%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "25%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>7%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "7%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Config Tools](/gateway/config-tools), [Web Fetch](/tools/web-fetch), [Web](/tools/web), [Faq](/help/faq)</div>
|
||||
|
||||
@@ -186,8 +186,12 @@ file.
|
||||
- optional `event.runId`
|
||||
- optional `event.toolCallId`
|
||||
- context fields such as `ctx.agentId`, `ctx.sessionKey`, `ctx.sessionId`,
|
||||
`ctx.runId`, `ctx.jobId` (set on cron-driven runs), `ctx.toolKind`,
|
||||
`ctx.toolInputKind`, and diagnostic `ctx.trace`
|
||||
`ctx.runId`, `ctx.jobId` (set on cron-driven runs), `ctx.trigger`,
|
||||
`ctx.toolKind`, `ctx.toolInputKind`, and diagnostic `ctx.trace`
|
||||
- for channel-originated calls, origin fields such as `ctx.channel`,
|
||||
`ctx.messageProvider`, `ctx.channelId`, `ctx.chatId`, `ctx.senderId`, and
|
||||
extensible `ctx.channelContext` sender/chat metadata. These use the same
|
||||
identity semantics described below for agent hook contexts.
|
||||
|
||||
It can return:
|
||||
|
||||
|
||||
@@ -71,11 +71,6 @@ OpenProse registers `/prose` as a user-invocable skill command:
|
||||
`/prose run <handle/slug>` resolves to `https://p.prose.md/<handle>/<slug>`.
|
||||
Direct URLs are fetched as-is using the `web_fetch` tool.
|
||||
|
||||
Top-level remote runs are explicit. Remote imports inside a `.prose` program are
|
||||
transitive code dependencies: before OpenProse fetches any remote `use` target,
|
||||
it shows the resolved import list and requires the operator to reply exactly
|
||||
`approve remote prose imports` for that run.
|
||||
|
||||
## What it can do
|
||||
|
||||
- Multi-agent research and synthesis with explicit parallelism.
|
||||
@@ -172,12 +167,9 @@ User-level persistent agents live at:
|
||||
|
||||
## Security
|
||||
|
||||
Treat `.prose` files like code. Review them before running, including remote
|
||||
`use` imports. Top-level `/prose run https://...` requests are explicit, but
|
||||
transitive remote imports require per-run approval before they are fetched or
|
||||
executed. Use OpenClaw tool allowlists and approval gates to control side
|
||||
effects. For deterministic, approval-gated workflows, compare with
|
||||
[Lobster](/tools/lobster).
|
||||
Treat `.prose` files like code. Review them before running. Use OpenClaw tool
|
||||
allowlists and approval gates to control side effects. For deterministic,
|
||||
approval-gated workflows, compare with [Lobster](/tools/lobster).
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -81,7 +81,6 @@ Session persistence has automatic maintenance controls (`session.maintenance`) f
|
||||
- `mode`: `enforce` (default) or `warn`
|
||||
- `pruneAfter`: stale-entry age cutoff (default `30d`)
|
||||
- `maxEntries`: cap entries in `sessions.json` (default `500`)
|
||||
- Short-lived gateway model-run probe retention is fixed at `24h`, but it is pressure-gated: it only removes stale strict probe rows when session-entry maintenance/cap pressure is reached. This applies only to strict explicit probe keys matching `agent:*:explicit:model-run-<uuid>` and runs before global stale-entry cleanup/capping when it runs.
|
||||
- `resetArchiveRetention`: retention for `*.reset.<timestamp>` transcript archives (default: same as `pruneAfter`; `false` disables cleanup)
|
||||
- `maxDiskBytes`: optional sessions-directory budget
|
||||
- `highWaterBytes`: optional target after cleanup (default `80%` of `maxDiskBytes`)
|
||||
@@ -91,12 +90,7 @@ Normal Gateway writes flow through a per-store session writer that serializes in
|
||||
Maintenance keeps durable external conversation pointers such as group sessions
|
||||
and thread-scoped chat sessions, but synthetic runtime entries for cron, hooks,
|
||||
heartbeat, ACP, and sub-agents can still be removed when they exceed the
|
||||
configured age, count, or disk budget. Gateway model-run probe sessions use the
|
||||
separate `24h` model-run retention only when their key exactly matches
|
||||
`agent:*:explicit:model-run-<uuid>`; other explicit sessions are not part of
|
||||
that retention. The model-run cleanup is applied only under session-entry cap
|
||||
pressure. Isolated cron runs keep their own `cron.sessionRetention` control,
|
||||
independent of model-run probe retention.
|
||||
configured age, count, or disk budget.
|
||||
|
||||
OpenClaw no longer creates automatic `sessions.json.bak.*` rotation backups during Gateway writes. The legacy `session.maintenance.rotateBytes` key is ignored and `openclaw doctor --fix` removes it from older configs.
|
||||
|
||||
|
||||
@@ -76,8 +76,6 @@ Use these in chat:
|
||||
configured for the active model.
|
||||
- `/usage off|tokens|full` → appends a **per-response usage footer** to every reply.
|
||||
- Persists per session (stored as `responseUsage`).
|
||||
- `/usage reset` (aliases: `inherit`, `clear`, `default`) — clears the session
|
||||
override so the session re-inherits the configured default.
|
||||
- `/usage full` shows estimated cost only when OpenClaw has usage metadata and
|
||||
local pricing for the active model. Otherwise it shows tokens only.
|
||||
- `/usage cost` → shows a local cost summary from OpenClaw session logs.
|
||||
|
||||
@@ -269,7 +269,7 @@ html.dark .nav-tabs-underline {
|
||||
|
||||
.maturity-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
margin: 14px 0 20px;
|
||||
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 18%, transparent);
|
||||
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 18%, transparent);
|
||||
|
||||
@@ -240,7 +240,7 @@ plugins.
|
||||
| `/tasks` | List active/recent background tasks for the current session |
|
||||
| `/context [list\|detail\|map\|json]` | Explain how context is assembled |
|
||||
| `/whoami` | Show your sender id. Alias: `/id` |
|
||||
| `/usage off\|tokens\|full\|reset\|cost` | Control the per-response usage footer (`reset`/`inherit`/`clear`/`default` clears the session override to re-inherit the configured default) or print a local cost summary |
|
||||
| `/usage off\|tokens\|full\|cost` | Control the per-response usage footer or print a local cost summary |
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Skills, allowlists, approvals">
|
||||
|
||||
@@ -126,7 +126,7 @@ Session controls:
|
||||
- `/verbose <on|full|off>`
|
||||
- `/trace <on|off>`
|
||||
- `/reasoning <on|off|stream>`
|
||||
- `/usage <off|tokens|full|reset>` (`reset`/`inherit`/`clear`/`default` clears the session override)
|
||||
- `/usage <off|tokens|full>`
|
||||
- `/goal [status] | /goal start <objective> | /goal pause|resume|complete|block|clear`
|
||||
- `/elevated <on|off|ask|full>` (alias: `/elev`)
|
||||
- `/activation <mention|always>`
|
||||
|
||||
@@ -12,7 +12,11 @@ import {
|
||||
type EmbeddedRunAttemptParams,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { buildApprovalResponse, handleCodexAppServerApprovalRequest } from "./approval-bridge.js";
|
||||
import {
|
||||
buildApprovalResponse,
|
||||
handleCodexAppServerApprovalRequest as handleCodexAppServerApprovalRequestImpl,
|
||||
} from "./approval-bridge.js";
|
||||
import { buildCodexToolHookRunContext } from "./tool-hook-context.js";
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/agent-harness-runtime", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("openclaw/plugin-sdk/agent-harness-runtime")>()),
|
||||
@@ -42,6 +46,20 @@ const mockResolveNativeHookRelayDeferredToolApproval = vi.mocked(
|
||||
const mockReviewExecRequestWithConfiguredModel = vi.mocked(reviewExecRequestWithConfiguredModel);
|
||||
const mockRunBeforeToolCallHook = vi.mocked(runBeforeToolCallHook);
|
||||
|
||||
type ApprovalRequestParams = Parameters<typeof handleCodexAppServerApprovalRequestImpl>[0];
|
||||
|
||||
function handleCodexAppServerApprovalRequest(
|
||||
params: Omit<ApprovalRequestParams, "toolHookContext"> & {
|
||||
toolHookContext?: ApprovalRequestParams["toolHookContext"];
|
||||
},
|
||||
) {
|
||||
return handleCodexAppServerApprovalRequestImpl({
|
||||
...params,
|
||||
toolHookContext:
|
||||
params.toolHookContext ?? buildCodexToolHookRunContext({ attempt: params.paramsForRun }),
|
||||
});
|
||||
}
|
||||
|
||||
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new Error(`Expected ${label}`);
|
||||
@@ -243,6 +261,8 @@ describe("Codex app-server approval bridge", () => {
|
||||
ctx: {
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:session-1",
|
||||
messageProvider: "telegram",
|
||||
channel: "telegram",
|
||||
channelId: "chat-1",
|
||||
},
|
||||
});
|
||||
@@ -1164,11 +1184,18 @@ describe("Codex app-server approval bridge", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes prefixed channel targets for OpenClaw tool policy context", async () => {
|
||||
it("uses the caller-resolved hook context for approval fallback policy", async () => {
|
||||
const params = createParams();
|
||||
params.messageChannel = "telegram";
|
||||
params.messageProvider = "telegram";
|
||||
params.currentChannelId = "telegram:-100123";
|
||||
params.agentId = "raw-agent";
|
||||
params.sessionId = "raw-session";
|
||||
params.sessionKey = "agent:raw:session";
|
||||
params.runId = "raw-run";
|
||||
params.messageChannel = "discord";
|
||||
params.messageProvider = "discord";
|
||||
params.currentChannelId = "discord:raw-target";
|
||||
params.jobId = "raw-job";
|
||||
params.senderId = "raw-user";
|
||||
params.chatId = "raw-chat";
|
||||
mockCallGatewayTool
|
||||
.mockResolvedValueOnce({ id: "plugin:approval-prefixed", status: "accepted" })
|
||||
.mockResolvedValueOnce({ id: "plugin:approval-prefixed", decision: "allow-once" });
|
||||
@@ -1182,6 +1209,27 @@ describe("Codex app-server approval bridge", () => {
|
||||
command: "pnpm test extensions/codex/src/app-server",
|
||||
},
|
||||
paramsForRun: params,
|
||||
toolHookContext: {
|
||||
agentId: "resolved-agent",
|
||||
sessionId: "resolved-session",
|
||||
sessionKey: "agent:resolved:session",
|
||||
runId: "resolved-run",
|
||||
jobId: "resolved-job",
|
||||
trigger: "user",
|
||||
messageProvider: "telegram-voice",
|
||||
channel: "telegram",
|
||||
channelId: "-100123",
|
||||
chatId: "native-chat-1",
|
||||
senderId: "user-1",
|
||||
channelContext: {
|
||||
sender: {
|
||||
id: "user-1",
|
||||
displayName: "Ada",
|
||||
providerUserId: "provider-user-1",
|
||||
},
|
||||
chat: { id: "native-chat-1", providerThreadKey: "thread-key-1" },
|
||||
},
|
||||
},
|
||||
threadId: "thread-1",
|
||||
turnId: "turn-1",
|
||||
});
|
||||
@@ -1189,11 +1237,29 @@ describe("Codex app-server approval bridge", () => {
|
||||
expect(mockRunBeforeToolCallHook).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
ctx: expect.objectContaining({
|
||||
agentId: "resolved-agent",
|
||||
sessionId: "resolved-session",
|
||||
sessionKey: "agent:resolved:session",
|
||||
runId: "resolved-run",
|
||||
jobId: "resolved-job",
|
||||
trigger: "user",
|
||||
messageProvider: "telegram-voice",
|
||||
channel: "telegram",
|
||||
channelId: "-100123",
|
||||
chatId: "native-chat-1",
|
||||
senderId: "user-1",
|
||||
channelContext: {
|
||||
sender: {
|
||||
id: "user-1",
|
||||
displayName: "Ada",
|
||||
providerUserId: "provider-user-1",
|
||||
},
|
||||
chat: { id: "native-chat-1", providerThreadKey: "thread-key-1" },
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(gatewayRequestPayload().turnSourceTo).toBe("telegram:-100123");
|
||||
expect(gatewayRequestPayload().turnSourceTo).toBe("discord:raw-target");
|
||||
});
|
||||
|
||||
it("denies command approvals before prompting when OpenClaw tool policy blocks", async () => {
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
*/
|
||||
import {
|
||||
type AgentApprovalEventData,
|
||||
buildAgentHookContextChannelFields,
|
||||
formatApprovalDisplayPath,
|
||||
hasNativeHookRelayInvocation,
|
||||
invokeNativeHookRelay,
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
type NativeHookRelayProcessResponse,
|
||||
type NativeHookRelayRegistrationHandle,
|
||||
runBeforeToolCallHook,
|
||||
type ToolHookRunContext,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
||||
import { normalizeTrimmedStringList } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
@@ -75,6 +75,7 @@ export async function handleCodexAppServerApprovalRequest(params: {
|
||||
method: string;
|
||||
requestParams: JsonValue | undefined;
|
||||
paramsForRun: EmbeddedRunAttemptParams;
|
||||
toolHookContext: ToolHookRunContext;
|
||||
threadId: string;
|
||||
turnId: string;
|
||||
nativeHookRelay?: Pick<
|
||||
@@ -106,6 +107,7 @@ export async function handleCodexAppServerApprovalRequest(params: {
|
||||
method: params.method,
|
||||
requestParams,
|
||||
paramsForRun: params.paramsForRun,
|
||||
toolHookContext: params.toolHookContext,
|
||||
context,
|
||||
nativeHookRelay: params.nativeHookRelay,
|
||||
signal: params.signal,
|
||||
@@ -619,6 +621,7 @@ async function runOpenClawToolPolicyForApprovalRequest(params: {
|
||||
method: string;
|
||||
requestParams: JsonObject | undefined;
|
||||
paramsForRun: EmbeddedRunAttemptParams;
|
||||
toolHookContext: ToolHookRunContext;
|
||||
context: ApprovalContext;
|
||||
nativeHookRelay?: Pick<
|
||||
NativeHookRelayRegistrationHandle,
|
||||
@@ -652,13 +655,6 @@ async function runOpenClawToolPolicyForApprovalRequest(params: {
|
||||
if (nativeRelayOutcome?.handled) {
|
||||
return { outcome: "no-decision" };
|
||||
}
|
||||
const hookChannelId = buildAgentHookContextChannelFields({
|
||||
sessionKey: params.paramsForRun.sessionKey,
|
||||
messageChannel: params.paramsForRun.messageChannel,
|
||||
messageProvider: params.paramsForRun.messageProvider,
|
||||
currentChannelId: params.paramsForRun.currentChannelId,
|
||||
messageTo: params.paramsForRun.messageTo,
|
||||
}).channelId;
|
||||
const outcome = await runBeforeToolCallHook({
|
||||
toolName: policyRequest.toolName,
|
||||
params: policyRequest.params,
|
||||
@@ -666,13 +662,9 @@ async function runOpenClawToolPolicyForApprovalRequest(params: {
|
||||
approvalMode: "request",
|
||||
signal: params.signal,
|
||||
ctx: {
|
||||
...(params.paramsForRun.agentId ? { agentId: params.paramsForRun.agentId } : {}),
|
||||
...params.toolHookContext,
|
||||
...(params.paramsForRun.config ? { config: params.paramsForRun.config } : {}),
|
||||
...(cwd ? { cwd } : {}),
|
||||
...(params.paramsForRun.sessionKey ? { sessionKey: params.paramsForRun.sessionKey } : {}),
|
||||
...(params.paramsForRun.sessionId ? { sessionId: params.paramsForRun.sessionId } : {}),
|
||||
...(params.paramsForRun.runId ? { runId: params.paramsForRun.runId } : {}),
|
||||
...(hookChannelId ? { channelId: hookChannelId } : {}),
|
||||
},
|
||||
});
|
||||
if (outcome.blocked) {
|
||||
|
||||
@@ -944,8 +944,16 @@ describe("Codex app-server dynamic tool build", () => {
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.disableTools = false;
|
||||
params.currentChannelId = "D123";
|
||||
params.messageChannel = "discord";
|
||||
params.messageProvider = "discord-voice";
|
||||
params.currentChannelId = "discord:D123";
|
||||
params.currentMessagingTarget = "user:U123";
|
||||
params.chatId = "chat-123";
|
||||
params.senderId = "user-123";
|
||||
params.channelContext = {
|
||||
sender: { id: "user-123" },
|
||||
chat: { id: "chat-123" },
|
||||
};
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
const factoryOptions: unknown[] = [];
|
||||
setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
@@ -956,9 +964,19 @@ describe("Codex app-server dynamic tool build", () => {
|
||||
await buildDynamicToolsForTest(params, workspaceDir, { sandbox: null as never });
|
||||
|
||||
expect(factoryOptions[0]).toMatchObject({
|
||||
currentChannelId: "D123",
|
||||
messageChannel: "discord",
|
||||
messageProvider: "discord",
|
||||
toolPolicyMessageProvider: "discord-voice",
|
||||
currentChannelId: "discord:D123",
|
||||
currentMessagingTarget: "user:U123",
|
||||
chatId: "chat-123",
|
||||
senderId: "user-123",
|
||||
hookChannelContext: {
|
||||
sender: { id: "user-123" },
|
||||
chat: { id: "chat-123" },
|
||||
},
|
||||
});
|
||||
expect((factoryOptions[0] as { channelContext?: unknown }).channelContext).toBeUndefined();
|
||||
});
|
||||
|
||||
it("passes the approval reviewer device into Codex dynamic tools", async () => {
|
||||
|
||||
@@ -125,7 +125,7 @@ export function resolveCodexAppServerHookChannelId(
|
||||
messageChannel: params.messageChannel,
|
||||
messageProvider: params.messageProvider,
|
||||
currentChannelId: params.currentChannelId,
|
||||
messageTo: params.messageTo,
|
||||
messageTo: params.currentMessagingTarget ?? params.messageTo,
|
||||
}).channelId;
|
||||
}
|
||||
|
||||
@@ -239,6 +239,7 @@ export async function buildDynamicTools(input: DynamicToolBuildParams) {
|
||||
elevated: params.bashElevated,
|
||||
},
|
||||
sandbox: input.sandbox,
|
||||
messageChannel: params.messageChannel,
|
||||
messageProvider: resolveCodexMessageToolProvider(params),
|
||||
toolPolicyMessageProvider: params.messageProvider ?? params.messageChannel,
|
||||
agentAccountId: params.agentAccountId,
|
||||
@@ -249,6 +250,7 @@ export async function buildDynamicTools(input: DynamicToolBuildParams) {
|
||||
groupSpace: params.groupSpace,
|
||||
spawnedBy: params.spawnedBy,
|
||||
senderId: params.senderId,
|
||||
hookChannelContext: params.channelContext,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
@@ -290,6 +292,7 @@ export async function buildDynamicTools(input: DynamicToolBuildParams) {
|
||||
),
|
||||
suppressManagedWebSearch: false,
|
||||
currentChannelId: params.currentChannelId,
|
||||
chatId: params.chatId,
|
||||
currentMessagingTarget: params.currentMessagingTarget,
|
||||
hookChannelId: resolveCodexAppServerHookChannelId(params, input.sandboxSessionKey),
|
||||
currentThreadTs: params.currentThreadTs,
|
||||
|
||||
@@ -1846,6 +1846,17 @@ describe("createCodexDynamicToolBridge", () => {
|
||||
sessionId: "session-1",
|
||||
sessionKey: "agent:agent-1:session-1",
|
||||
runId: "run-1",
|
||||
jobId: "job-1",
|
||||
trigger: "user",
|
||||
messageProvider: "discord-voice",
|
||||
channel: "discord",
|
||||
chatId: "channel-1",
|
||||
senderId: "user-1",
|
||||
channelId: "channel-1",
|
||||
channelContext: {
|
||||
sender: { id: "user-1", displayName: "Ada" },
|
||||
chat: { id: "channel-1" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1949,6 +1960,17 @@ describe("createCodexDynamicToolBridge", () => {
|
||||
sessionId: "session-1",
|
||||
sessionKey: "agent:agent-1:session-1",
|
||||
runId: "run-1",
|
||||
jobId: "job-1",
|
||||
trigger: "user",
|
||||
messageProvider: "discord-voice",
|
||||
channel: "discord",
|
||||
chatId: "channel-1",
|
||||
senderId: "user-1",
|
||||
channelId: "channel-1",
|
||||
channelContext: {
|
||||
sender: { id: "user-1", displayName: "Ada" },
|
||||
chat: { id: "channel-1" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1975,6 +1997,17 @@ describe("createCodexDynamicToolBridge", () => {
|
||||
sessionId: "session-1",
|
||||
sessionKey: "agent:agent-1:session-1",
|
||||
runId: "run-1",
|
||||
jobId: "job-1",
|
||||
trigger: "user",
|
||||
messageProvider: "discord-voice",
|
||||
channel: "discord",
|
||||
chatId: "channel-1",
|
||||
senderId: "user-1",
|
||||
channelId: "channel-1",
|
||||
channelContext: {
|
||||
sender: { id: "user-1", displayName: "Ada" },
|
||||
chat: { id: "channel-1" },
|
||||
},
|
||||
toolCallId: "call-1",
|
||||
});
|
||||
expectExecuteCall(execute, { callId: "call-1", args: { command: "pwd", mode: "safe" } });
|
||||
@@ -1997,6 +2030,17 @@ describe("createCodexDynamicToolBridge", () => {
|
||||
sessionId: "session-1",
|
||||
sessionKey: "agent:agent-1:session-1",
|
||||
runId: "run-1",
|
||||
jobId: "job-1",
|
||||
trigger: "user",
|
||||
messageProvider: "discord-voice",
|
||||
channel: "discord",
|
||||
chatId: "channel-1",
|
||||
senderId: "user-1",
|
||||
channelId: "channel-1",
|
||||
channelContext: {
|
||||
sender: { id: "user-1", displayName: "Ada" },
|
||||
chat: { id: "channel-1" },
|
||||
},
|
||||
toolCallId: "call-1",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
type HeartbeatToolResponse,
|
||||
type MessagingToolSend,
|
||||
type MessagingToolSourceReplyPayload,
|
||||
type ToolHookRunContext,
|
||||
wrapToolWithBeforeToolCallHook,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { emitTrustedDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime";
|
||||
@@ -53,13 +54,8 @@ import type {
|
||||
JsonValue,
|
||||
} from "./protocol.js";
|
||||
|
||||
type CodexDynamicToolHookContext = {
|
||||
agentId?: string;
|
||||
type CodexDynamicToolHookContext = ToolHookRunContext & {
|
||||
config?: EmbeddedRunAttemptParams["config"];
|
||||
sessionId?: string;
|
||||
sessionKey?: string;
|
||||
runId?: string;
|
||||
channelId?: string;
|
||||
currentChannelProvider?: string;
|
||||
currentChannelId?: string;
|
||||
currentMessagingTarget?: string;
|
||||
@@ -70,7 +66,7 @@ type CodexDynamicToolHookContext = {
|
||||
allocateToolOutcomeOrdinal?: EmbeddedRunAttemptParams["allocateToolOutcomeOrdinal"];
|
||||
};
|
||||
|
||||
type CodexToolResultHookContext = Omit<CodexDynamicToolHookContext, "config">;
|
||||
type CodexToolResultHookContext = ToolHookRunContext;
|
||||
|
||||
type ProjectedCodexDynamicTool = {
|
||||
tool: AnyAgentTool;
|
||||
@@ -310,11 +306,7 @@ export function createCodexDynamicToolBridge(params: {
|
||||
void runAgentHarnessAfterToolCallHook({
|
||||
toolName,
|
||||
toolCallId: call.callId,
|
||||
runId: toolResultHookContext.runId,
|
||||
agentId: toolResultHookContext.agentId,
|
||||
sessionId: toolResultHookContext.sessionId,
|
||||
sessionKey: toolResultHookContext.sessionKey,
|
||||
channelId: toolResultHookContext.channelId,
|
||||
...toolResultHookContext,
|
||||
startArgs: executedArgs,
|
||||
result,
|
||||
startedAt,
|
||||
@@ -407,11 +399,7 @@ export function createCodexDynamicToolBridge(params: {
|
||||
void runAgentHarnessAfterToolCallHook({
|
||||
toolName,
|
||||
toolCallId: call.callId,
|
||||
runId: toolResultHookContext.runId,
|
||||
agentId: toolResultHookContext.agentId,
|
||||
sessionId: toolResultHookContext.sessionId,
|
||||
sessionKey: toolResultHookContext.sessionKey,
|
||||
channelId: toolResultHookContext.channelId,
|
||||
...toolResultHookContext,
|
||||
startArgs: executedArgs,
|
||||
error: errorMessage,
|
||||
startedAt,
|
||||
@@ -702,13 +690,35 @@ function dedupeQuarantinedDynamicTools(
|
||||
function toToolResultHookContext(
|
||||
ctx: CodexDynamicToolHookContext | undefined,
|
||||
): CodexToolResultHookContext {
|
||||
const { agentId, sessionId, sessionKey, runId, channelId } = ctx ?? {};
|
||||
const {
|
||||
agentId,
|
||||
sessionId,
|
||||
sessionKey,
|
||||
runId,
|
||||
jobId,
|
||||
trace,
|
||||
trigger,
|
||||
messageProvider,
|
||||
channel,
|
||||
chatId,
|
||||
senderId,
|
||||
channelId,
|
||||
channelContext,
|
||||
} = ctx ?? {};
|
||||
return {
|
||||
...(agentId && { agentId }),
|
||||
...(sessionId && { sessionId }),
|
||||
...(sessionKey && { sessionKey }),
|
||||
...(runId && { runId }),
|
||||
...(jobId && { jobId }),
|
||||
...(trace && { trace }),
|
||||
...(trigger && { trigger }),
|
||||
...(messageProvider && { messageProvider }),
|
||||
...(channel && { channel }),
|
||||
...(chatId && { chatId }),
|
||||
...(senderId && { senderId }),
|
||||
...(channelId && { channelId }),
|
||||
...(channelContext && { channelContext }),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2587,15 +2587,36 @@ describe("CodexAppServerEventProjector", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("emits after_tool_call observations for Codex-native tool item completions", async () => {
|
||||
it("keeps resolved hook identity authoritative for Codex-native tool completions", async () => {
|
||||
const afterToolCall = vi.fn();
|
||||
initializeGlobalHookRunner(
|
||||
createMockPluginRegistry([{ hookName: "after_tool_call", handler: afterToolCall }]),
|
||||
);
|
||||
const projector = await createProjector({
|
||||
const projectorParams = {
|
||||
...(await createParams()),
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:session-1",
|
||||
agentId: "raw-agent",
|
||||
sessionId: "raw-session",
|
||||
sessionKey: "agent:raw:session-1",
|
||||
runId: "raw-run",
|
||||
};
|
||||
const projector = await createProjector(projectorParams, {
|
||||
toolHookContext: {
|
||||
agentId: "main",
|
||||
sessionId: "session-1",
|
||||
sessionKey: "agent:main:session-1",
|
||||
runId: "run-1",
|
||||
jobId: "job-1",
|
||||
trigger: "user",
|
||||
messageProvider: "discord-voice",
|
||||
channel: "discord",
|
||||
chatId: "channel-1",
|
||||
senderId: "user-1",
|
||||
channelId: "channel-1",
|
||||
channelContext: {
|
||||
sender: { id: "user-1" },
|
||||
chat: { id: "channel-1" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await projector.handleNotification(
|
||||
@@ -2652,6 +2673,17 @@ describe("CodexAppServerEventProjector", () => {
|
||||
expect(context.sessionId).toBe("session-1");
|
||||
expect(context.sessionKey).toBe("agent:main:session-1");
|
||||
expect(context.runId).toBe("run-1");
|
||||
expect(context.jobId).toBe("job-1");
|
||||
expect(context.trigger).toBe("user");
|
||||
expect(context.messageProvider).toBe("discord-voice");
|
||||
expect(context.channel).toBe("discord");
|
||||
expect(context.chatId).toBe("channel-1");
|
||||
expect(context.senderId).toBe("user-1");
|
||||
expect(context.channelId).toBe("channel-1");
|
||||
expect(context.channelContext).toEqual({
|
||||
sender: { id: "user-1" },
|
||||
chat: { id: "channel-1" },
|
||||
});
|
||||
expect(context.toolName).toBe("bash");
|
||||
expect(context.toolCallId).toBe("cmd-observed");
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
type HeartbeatToolResponse,
|
||||
type MessagingToolSend,
|
||||
type MessagingToolSourceReplyPayload,
|
||||
type ToolHookRunContext,
|
||||
type ToolProgressDetailMode,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { emitTrustedDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime";
|
||||
@@ -65,6 +66,7 @@ export type CodexAppServerToolTelemetry = {
|
||||
|
||||
export type CodexAppServerEventProjectorOptions = {
|
||||
nativePostToolUseRelayEnabled?: boolean;
|
||||
toolHookContext?: ToolHookRunContext;
|
||||
onNativeToolResultRecorded?: () => void | Promise<void>;
|
||||
trajectoryRecorder?: CodexTrajectoryRecorder | null;
|
||||
};
|
||||
@@ -1374,6 +1376,9 @@ export class CodexAppServerEventProjector {
|
||||
agentId: this.params.agentId,
|
||||
sessionId: this.params.sessionId,
|
||||
sessionKey: this.params.sessionKey,
|
||||
// The attempt boundary resolves aliases and sandbox session identity once.
|
||||
// Keep that canonical snapshot authoritative over optional raw projector params.
|
||||
...this.options.toolHookContext,
|
||||
startArgs: itemToolArgs(item) ?? {},
|
||||
...(result !== undefined ? { result } : {}),
|
||||
...(error ? { error } : {}),
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type EmbeddedRunAttemptParams,
|
||||
type NativeHookRelayEvent,
|
||||
type NativeHookRelayRegistrationHandle,
|
||||
type ToolHookRunContext,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import {
|
||||
addTimerTimeoutGraceMs,
|
||||
@@ -121,6 +122,7 @@ export function createCodexNativeHookRelay(params: {
|
||||
config: EmbeddedRunAttemptParams["config"];
|
||||
runId: string;
|
||||
channelId?: string;
|
||||
toolHookContext?: ToolHookRunContext;
|
||||
attemptTimeoutMs: number;
|
||||
startupTimeoutMs: number;
|
||||
turnStartTimeoutMs: number;
|
||||
@@ -146,6 +148,7 @@ export function createCodexNativeHookRelay(params: {
|
||||
...(params.config ? { config: params.config } : {}),
|
||||
runId: params.runId,
|
||||
...(params.channelId ? { channelId: params.channelId } : {}),
|
||||
...(params.toolHookContext ? { toolHookContext: params.toolHookContext } : {}),
|
||||
allowedEvents: params.events,
|
||||
ttlMs: resolveCodexNativeHookRelayTtlMs({
|
||||
explicitTtlMs: params.options?.ttlMs,
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
// Codex tests cover run attemptynamic tools plugin behavior.
|
||||
import path from "node:path";
|
||||
import {
|
||||
onAgentEvent,
|
||||
type AgentEventPayload,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { onAgentEvent, type AgentEventPayload } from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import {
|
||||
emitTrustedDiagnosticEvent,
|
||||
onInternalDiagnosticEvent,
|
||||
@@ -609,6 +606,21 @@ describe("runCodexAppServerAttempt dynamic tools", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("prefers the current messaging target for hook channel fallback", () => {
|
||||
const params = createParams(
|
||||
path.join(tempDir, "session.jsonl"),
|
||||
path.join(tempDir, "workspace"),
|
||||
);
|
||||
params.messageChannel = "telegram";
|
||||
params.messageProvider = "telegram";
|
||||
params.messageTo = "telegram:stale-target";
|
||||
params.currentMessagingTarget = "telegram:current-target";
|
||||
|
||||
expect(testing.resolveCodexAppServerHookChannelId(params, "agent:main:session-1")).toBe(
|
||||
"current-target",
|
||||
);
|
||||
});
|
||||
|
||||
it("passes normalized channel context to app-server dynamic tool result hooks", async () => {
|
||||
const afterToolCall = vi.fn();
|
||||
initializeGlobalHookRunner(
|
||||
|
||||
@@ -30,9 +30,7 @@ const DISABLED_CODEX_WEB_SEARCH_THREAD_CONFIG_FINGERPRINT = JSON.stringify({
|
||||
web_search: "disabled",
|
||||
});
|
||||
|
||||
function writeCodexAppServerBinding(
|
||||
...args: Parameters<typeof writeRawCodexAppServerBinding>
|
||||
) {
|
||||
function writeCodexAppServerBinding(...args: Parameters<typeof writeRawCodexAppServerBinding>) {
|
||||
const [sessionFile, binding, lookup] = args;
|
||||
return writeRawCodexAppServerBinding(
|
||||
sessionFile,
|
||||
@@ -95,7 +93,15 @@ describe("runCodexAppServerAttempt native hook relay", () => {
|
||||
const harness = createStartedThreadHarness();
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.messageChannel = "discord";
|
||||
params.messageProvider = "discord-voice";
|
||||
params.currentChannelId = "channel:target";
|
||||
params.trigger = "user";
|
||||
params.senderId = "user-1";
|
||||
params.chatId = "native-target";
|
||||
params.channelContext = {
|
||||
sender: { id: "user-1", providerUserId: "discord-user-1" },
|
||||
chat: { id: "native-target", guildId: "guild-1" },
|
||||
};
|
||||
|
||||
const run = runCodexAppServerAttempt(params, {
|
||||
nativeHookRelay: {
|
||||
@@ -135,6 +141,22 @@ describe("runCodexAppServerAttempt native hook relay", () => {
|
||||
threadId: "thread-1",
|
||||
turnId: "turn-1",
|
||||
autoApprove: true,
|
||||
toolHookContext: {
|
||||
agentId: "main",
|
||||
sessionId: "session-1",
|
||||
sessionKey: "agent:main:session-1",
|
||||
runId: "run-1",
|
||||
trigger: "user",
|
||||
messageProvider: "discord-voice",
|
||||
channel: "discord",
|
||||
channelId: "target",
|
||||
chatId: "native-target",
|
||||
senderId: "user-1",
|
||||
channelContext: {
|
||||
sender: { id: "user-1", providerUserId: "discord-user-1" },
|
||||
chat: { id: "native-target", guildId: "guild-1" },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(approvalArgs?.nativeHookRelay).toMatchObject({
|
||||
relayId,
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
type EmbeddedRunAttemptResult,
|
||||
type NativeHookRelayEvent,
|
||||
type NativeHookRelayRegistrationHandle,
|
||||
type ToolHookRunContext,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { resolveAgentDir } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import {
|
||||
@@ -248,6 +249,7 @@ import {
|
||||
type CodexAppServerThreadLifecycleBinding,
|
||||
type CodexContextEngineThreadBootstrapProjection,
|
||||
} from "./thread-lifecycle.js";
|
||||
import { buildCodexToolHookRunContext } from "./tool-hook-context.js";
|
||||
import {
|
||||
inferCodexDynamicToolMeta,
|
||||
resolveCodexToolProgressDetailMode,
|
||||
@@ -717,6 +719,14 @@ export async function runCodexAppServerAttempt(
|
||||
});
|
||||
}
|
||||
const hookChannelId = resolveCodexAppServerHookChannelId(params, sandboxSessionKey);
|
||||
const toolHookRunContext = buildCodexToolHookRunContext({
|
||||
attempt: params,
|
||||
agentId: sessionAgentId,
|
||||
sessionId: params.sessionId,
|
||||
sessionKey: sandboxSessionKey,
|
||||
runId: params.runId,
|
||||
channelId: hookChannelId,
|
||||
});
|
||||
preDynamicStartupStages.mark("context-engine-support");
|
||||
const preDynamicSummary = preDynamicStartupStages.snapshot();
|
||||
if (shouldWarnCodexDynamicToolBuildStageSummary(preDynamicSummary)) {
|
||||
@@ -832,12 +842,8 @@ export async function runCodexAppServerAttempt(
|
||||
}),
|
||||
directToolNames: resolveCodexDynamicToolDirectNames(params),
|
||||
hookContext: {
|
||||
agentId: sessionAgentId,
|
||||
...toolHookRunContext,
|
||||
config: params.config,
|
||||
sessionId: params.sessionId,
|
||||
sessionKey: sandboxSessionKey,
|
||||
runId: params.runId,
|
||||
channelId: hookChannelId,
|
||||
currentChannelProvider: resolveCodexMessageToolProvider(params),
|
||||
currentChannelId: params.currentChannelId,
|
||||
currentMessagingTarget: params.currentMessagingTarget,
|
||||
@@ -1444,6 +1450,7 @@ export async function runCodexAppServerAttempt(
|
||||
config: params.config,
|
||||
runId: params.runId,
|
||||
channelId: hookChannelId,
|
||||
toolHookContext: toolHookRunContext,
|
||||
attemptTimeoutMs: params.timeoutMs,
|
||||
startupTimeoutMs,
|
||||
turnStartTimeoutMs: params.timeoutMs,
|
||||
@@ -2150,6 +2157,7 @@ export async function runCodexAppServerAttempt(
|
||||
method: request.method,
|
||||
params: request.params,
|
||||
paramsForRun: params,
|
||||
toolHookContext: toolHookRunContext,
|
||||
threadId: thread.threadId,
|
||||
turnId,
|
||||
nativeHookRelay,
|
||||
@@ -2761,6 +2769,7 @@ export async function runCodexAppServerAttempt(
|
||||
nativePostToolUseRelayEnabled:
|
||||
nativeHookRelay?.allowedEvents.includes("post_tool_use") === true &&
|
||||
nativeHookRelay.shouldRelayEvent("post_tool_use"),
|
||||
toolHookContext: toolHookRunContext,
|
||||
trajectoryRecorder,
|
||||
onNativeToolResultRecorded: maybeAnnounceFastModeAutoOff,
|
||||
},
|
||||
@@ -3430,6 +3439,7 @@ function handleApprovalRequest(params: {
|
||||
method: string;
|
||||
params: JsonValue | undefined;
|
||||
paramsForRun: EmbeddedRunAttemptParams;
|
||||
toolHookContext: ToolHookRunContext;
|
||||
threadId: string;
|
||||
turnId: string;
|
||||
nativeHookRelay?: NativeHookRelayRegistrationHandle;
|
||||
@@ -3443,6 +3453,7 @@ function handleApprovalRequest(params: {
|
||||
method: params.method,
|
||||
requestParams: params.params,
|
||||
paramsForRun: params.paramsForRun,
|
||||
toolHookContext: params.toolHookContext,
|
||||
threadId: params.threadId,
|
||||
turnId: params.turnId,
|
||||
nativeHookRelay: params.nativeHookRelay,
|
||||
|
||||
@@ -861,9 +861,12 @@ describe("runCodexAppServerSideQuestion", () => {
|
||||
).toMatchObject({
|
||||
agentId: "main",
|
||||
sessionId: "session-1",
|
||||
sessionKey: "agent:main:session-1",
|
||||
sessionKey: "agent:main:runtime-policy",
|
||||
runId: "run-side-1",
|
||||
channelId: "voice-room",
|
||||
toolHookContext: {
|
||||
sessionKey: "agent:main:runtime-policy",
|
||||
},
|
||||
allowedEvents: ["pre_tool_use", "post_tool_use", "before_agent_finalize"],
|
||||
});
|
||||
return threadResult("side-thread");
|
||||
@@ -889,6 +892,7 @@ describe("runCodexAppServerSideQuestion", () => {
|
||||
runCodexAppServerSideQuestion(
|
||||
sideParams({
|
||||
sessionKey: "agent:main:session-1",
|
||||
sandboxSessionKey: "agent:main:runtime-policy",
|
||||
messageChannel: "discord",
|
||||
messageProvider: "discord-voice",
|
||||
currentChannelId: "discord:voice-room",
|
||||
@@ -971,6 +975,7 @@ describe("runCodexAppServerSideQuestion", () => {
|
||||
runCodexAppServerSideQuestion(
|
||||
sideParams({
|
||||
sessionKey: "agent:main:session-1",
|
||||
sandboxSessionKey: "agent:main:runtime-policy",
|
||||
messageChannel: "discord",
|
||||
messageProvider: "discord-voice",
|
||||
opts: { runId: "run-side-approval" },
|
||||
@@ -988,6 +993,7 @@ describe("runCodexAppServerSideQuestion", () => {
|
||||
threadId?: string;
|
||||
turnId?: string;
|
||||
paramsForRun?: { messageChannel?: string; messageProvider?: string };
|
||||
toolHookContext?: { sessionKey?: string };
|
||||
nativeHookRelay?: { relayId?: string; allowedEvents?: readonly string[] };
|
||||
}
|
||||
| undefined;
|
||||
@@ -1007,6 +1013,9 @@ describe("runCodexAppServerSideQuestion", () => {
|
||||
messageChannel: "discord",
|
||||
messageProvider: "discord-voice",
|
||||
},
|
||||
toolHookContext: {
|
||||
sessionKey: "agent:main:runtime-policy",
|
||||
},
|
||||
});
|
||||
expect(approvalArgs?.nativeHookRelay).toMatchObject({
|
||||
relayId: relayIdDuringFork,
|
||||
@@ -1482,6 +1491,14 @@ describe("runCodexAppServerSideQuestion", () => {
|
||||
});
|
||||
|
||||
it("bridges side-thread dynamic tool requests to OpenClaw tools", async () => {
|
||||
const beforeToolCall = vi.fn();
|
||||
const afterToolCall = vi.fn();
|
||||
initializeGlobalHookRunner(
|
||||
createMockPluginRegistry([
|
||||
{ hookName: "before_tool_call", handler: beforeToolCall },
|
||||
{ hookName: "after_tool_call", handler: afterToolCall },
|
||||
]),
|
||||
);
|
||||
const client = createFakeClient();
|
||||
let toolResponse: unknown;
|
||||
client.request.mockImplementation(async (method: string) => {
|
||||
@@ -1527,6 +1544,13 @@ describe("runCodexAppServerSideQuestion", () => {
|
||||
expect(toolArguments).toEqual({ topic: "AGENTS.md" });
|
||||
expect(toolSignal).toBeInstanceOf(AbortSignal);
|
||||
expect(toolOptions).toBeUndefined();
|
||||
expect(beforeToolCall).toHaveBeenCalledTimes(1);
|
||||
expect(mockCall(beforeToolCall)[1]).toMatchObject({ sessionKey: "session-1" });
|
||||
await vi.waitFor(() => expect(afterToolCall).toHaveBeenCalledTimes(1));
|
||||
expect(mockCall(afterToolCall)[1]).toMatchObject({ sessionKey: "session-1" });
|
||||
expect(createOpenClawCodingToolsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ sessionKey: "session-1" }),
|
||||
);
|
||||
expect(toolResponse).toEqual({
|
||||
success: true,
|
||||
contentItems: [{ type: "inputText", text: "tool output" }],
|
||||
@@ -1610,14 +1634,29 @@ describe("runCodexAppServerSideQuestion", () => {
|
||||
expect(activeDiagnosticToolKeys(diagnosticEvents)).toEqual(new Set());
|
||||
});
|
||||
|
||||
it("normalizes hook channel ids for side-thread dynamic tool requests", async () => {
|
||||
it("preserves requester identity while normalizing side-thread hook channels", async () => {
|
||||
const afterToolCall = vi.fn();
|
||||
const beforeToolCall = vi.fn((...args: unknown[]) => {
|
||||
const context = args[1] as { channelId?: string };
|
||||
expect(context.channelId).toBe("voice-room");
|
||||
const context = args[1] as Record<string, unknown>;
|
||||
expect(context).toMatchObject({
|
||||
sessionKey: "agent:main:runtime-policy",
|
||||
messageProvider: "discord-voice",
|
||||
channel: "discord",
|
||||
channelId: "voice-room",
|
||||
chatId: "native-voice-chat",
|
||||
senderId: "sender-1",
|
||||
channelContext: {
|
||||
sender: { id: "sender-1", providerUserId: "discord-user-1" },
|
||||
chat: { id: "native-voice-chat", guildId: "guild-1" },
|
||||
},
|
||||
});
|
||||
return undefined;
|
||||
});
|
||||
initializeGlobalHookRunner(
|
||||
createMockPluginRegistry([{ hookName: "before_tool_call", handler: beforeToolCall }]),
|
||||
createMockPluginRegistry([
|
||||
{ hookName: "before_tool_call", handler: beforeToolCall },
|
||||
{ hookName: "after_tool_call", handler: afterToolCall },
|
||||
]),
|
||||
);
|
||||
const client = createFakeClient();
|
||||
client.request.mockImplementation(async (method: string) => {
|
||||
@@ -1657,17 +1696,48 @@ describe("runCodexAppServerSideQuestion", () => {
|
||||
await expect(
|
||||
runCodexAppServerSideQuestion(
|
||||
sideParams({
|
||||
sessionKey: "agent:main:conversation",
|
||||
sandboxSessionKey: "agent:main:runtime-policy",
|
||||
messageChannel: "discord",
|
||||
messageProvider: "discord-voice",
|
||||
currentChannelId: "discord:voice-room",
|
||||
chatId: "native-voice-chat",
|
||||
senderId: "sender-1",
|
||||
channelContext: {
|
||||
sender: { id: "sender-1", providerUserId: "discord-user-1" },
|
||||
chat: { id: "native-voice-chat", guildId: "guild-1" },
|
||||
},
|
||||
}),
|
||||
),
|
||||
).resolves.toEqual({ text: "Tool answer." });
|
||||
|
||||
expect(beforeToolCall).toHaveBeenCalledTimes(1);
|
||||
await vi.waitFor(() => expect(afterToolCall).toHaveBeenCalledTimes(1));
|
||||
expect(mockCall(afterToolCall)[1]).toMatchObject({
|
||||
sessionKey: "agent:main:runtime-policy",
|
||||
messageProvider: "discord-voice",
|
||||
channel: "discord",
|
||||
channelId: "voice-room",
|
||||
chatId: "native-voice-chat",
|
||||
});
|
||||
expect(createOpenClawCodingToolsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ hookChannelId: "voice-room" }),
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main:runtime-policy",
|
||||
runSessionKey: "agent:main:conversation",
|
||||
messageChannel: "discord",
|
||||
messageProvider: "discord",
|
||||
toolPolicyMessageProvider: "discord-voice",
|
||||
hookChannelId: "voice-room",
|
||||
chatId: "native-voice-chat",
|
||||
hookChannelContext: {
|
||||
sender: { id: "sender-1", providerUserId: "discord-user-1" },
|
||||
chat: { id: "native-voice-chat", guildId: "guild-1" },
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
(mockCall(createOpenClawCodingToolsMock)[0] as { channelContext?: unknown }).channelContext,
|
||||
).toBeUndefined();
|
||||
expect(toolExecuteMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Codex plugin module implements side question behavior.
|
||||
import {
|
||||
buildAgentHookContextChannelFields,
|
||||
embeddedAgentLog,
|
||||
formatErrorMessage,
|
||||
resolveAgentDir,
|
||||
@@ -16,6 +15,7 @@ import {
|
||||
type EmbeddedRunAttemptParams,
|
||||
type NativeHookRelayEvent,
|
||||
type NativeHookRelayRegistrationHandle,
|
||||
type ToolHookRunContext,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { loadExecApprovals } from "openclaw/plugin-sdk/exec-approvals-runtime";
|
||||
import { resolveCodexAppServerForModelProvider } from "./app-server-policy.js";
|
||||
@@ -89,6 +89,7 @@ import {
|
||||
resolveCodexBindingModelProviderFallback,
|
||||
resolveReasoningEffort,
|
||||
} from "./thread-lifecycle.js";
|
||||
import { buildCodexToolHookRunContext } from "./tool-hook-context.js";
|
||||
import { filterToolsForVisionInputs } from "./vision-tools.js";
|
||||
import {
|
||||
resolveCodexWebSearchPlan,
|
||||
@@ -206,9 +207,21 @@ export async function runCodexAppServerSideQuestion(
|
||||
});
|
||||
const cwd = binding.cwd || params.workspaceDir || process.cwd();
|
||||
const sideRunParams = buildSideRunAttemptParams(params, { cwd, authProfileId });
|
||||
const toolHookSessionKey =
|
||||
sideRunParams.sandboxSessionKey?.trim() ||
|
||||
sideRunParams.sessionKey?.trim() ||
|
||||
sideRunParams.sessionId ||
|
||||
sessionAgentId;
|
||||
const toolHookRunContext = buildCodexToolHookRunContext({
|
||||
attempt: sideRunParams,
|
||||
agentId: sessionAgentId,
|
||||
sessionId: sideRunParams.sessionId,
|
||||
sessionKey: toolHookSessionKey,
|
||||
runId: sideRunParams.runId,
|
||||
});
|
||||
const nativeExecutionBlock = resolveCodexNativeExecutionBlock({
|
||||
config: sideRunParams.config,
|
||||
sessionKey: sideRunParams.sandboxSessionKey?.trim() || sideRunParams.sessionKey,
|
||||
sessionKey: toolHookSessionKey,
|
||||
sessionId: sideRunParams.sessionId,
|
||||
surface: "/btw side-question mode",
|
||||
});
|
||||
@@ -287,6 +300,7 @@ export async function runCodexAppServerSideQuestion(
|
||||
nativeToolSurfaceEnabled,
|
||||
nativeProviderWebSearchSupport,
|
||||
signal: runAbortController.signal,
|
||||
toolHookContext: toolHookRunContext,
|
||||
});
|
||||
removeRequestHandler = client.addRequestHandler(async (request) => {
|
||||
if (request.method === "account/chatgptAuthTokens/refresh") {
|
||||
@@ -319,19 +333,20 @@ export async function runCodexAppServerSideQuestion(
|
||||
method: request.method,
|
||||
requestParams: request.params,
|
||||
paramsForRun: sideRunParams,
|
||||
toolHookContext: toolHookRunContext,
|
||||
threadId: childThreadId,
|
||||
turnId,
|
||||
nativeHookRelay,
|
||||
execPolicy,
|
||||
execReviewerAgentId: sessionAgentId,
|
||||
internalExecAutoReview: modelScopedAppServer.approvalsReviewer === "user",
|
||||
autoApprove: shouldAutoApproveCodexAppServerApprovals({
|
||||
approvalPolicy,
|
||||
networkProxy: modelScopedAppServer.networkProxy,
|
||||
sandbox,
|
||||
}),
|
||||
signal: runAbortController.signal,
|
||||
});
|
||||
execPolicy,
|
||||
execReviewerAgentId: sessionAgentId,
|
||||
internalExecAutoReview: modelScopedAppServer.approvalsReviewer === "user",
|
||||
autoApprove: shouldAutoApproveCodexAppServerApprovals({
|
||||
approvalPolicy,
|
||||
networkProxy: modelScopedAppServer.networkProxy,
|
||||
sandbox,
|
||||
}),
|
||||
signal: runAbortController.signal,
|
||||
});
|
||||
}
|
||||
if (request.method !== "item/tool/call") {
|
||||
return undefined;
|
||||
@@ -388,15 +403,11 @@ export async function runCodexAppServerSideQuestion(
|
||||
events: nativeHookRelayEvents,
|
||||
agentId: sessionAgentId,
|
||||
sessionId: params.sessionId,
|
||||
sessionKey: params.sessionKey,
|
||||
sessionKey: toolHookRunContext.sessionKey,
|
||||
config: params.cfg,
|
||||
runId: sideRunParams.runId,
|
||||
channelId: buildAgentHookContextChannelFields({
|
||||
sessionKey: params.sessionKey,
|
||||
messageChannel: params.messageChannel,
|
||||
messageProvider: params.messageProvider,
|
||||
currentChannelId: params.currentChannelId,
|
||||
}).channelId,
|
||||
channelId: toolHookRunContext.channelId,
|
||||
toolHookContext: toolHookRunContext,
|
||||
requestTimeoutMs: appServer.requestTimeoutMs,
|
||||
completionTimeoutMs: Math.max(
|
||||
appServer.turnCompletionIdleTimeoutMs,
|
||||
@@ -419,12 +430,12 @@ export async function runCodexAppServerSideQuestion(
|
||||
nativeCodeModeEnabled: nativeToolSurfaceEnabled,
|
||||
nativeCodeModeOnlyEnabled: appServer.codeModeOnly,
|
||||
});
|
||||
const threadConfig =
|
||||
mergeCodexThreadConfigs(
|
||||
nativeHookRelayConfig,
|
||||
runtimeThreadConfig,
|
||||
modelScopedAppServer.networkProxy?.configPatch,
|
||||
) ?? runtimeThreadConfig;
|
||||
const threadConfig =
|
||||
mergeCodexThreadConfigs(
|
||||
nativeHookRelayConfig,
|
||||
runtimeThreadConfig,
|
||||
modelScopedAppServer.networkProxy?.configPatch,
|
||||
) ?? runtimeThreadConfig;
|
||||
const forkResponse = assertCodexThreadForkResponse(
|
||||
await forkCodexSideThread(
|
||||
client,
|
||||
@@ -436,7 +447,7 @@ export async function runCodexAppServerSideQuestion(
|
||||
cwd,
|
||||
approvalPolicy,
|
||||
approvalsReviewer: modelScopedAppServer.approvalsReviewer,
|
||||
...(modelScopedAppServer.networkProxy ? {} : { sandbox }),
|
||||
...(modelScopedAppServer.networkProxy ? {} : { sandbox }),
|
||||
...(serviceTier ? { serviceTier } : {}),
|
||||
config: threadConfig,
|
||||
developerInstructions: SIDE_DEVELOPER_INSTRUCTIONS,
|
||||
@@ -542,6 +553,7 @@ function registerCodexSideNativeHookRelay(params: {
|
||||
config: EmbeddedRunAttemptParams["config"];
|
||||
runId: string;
|
||||
channelId?: string;
|
||||
toolHookContext?: ToolHookRunContext;
|
||||
requestTimeoutMs: number;
|
||||
completionTimeoutMs: number;
|
||||
signal: AbortSignal;
|
||||
@@ -557,6 +569,7 @@ function registerCodexSideNativeHookRelay(params: {
|
||||
...(params.config ? { config: params.config } : {}),
|
||||
runId: params.runId,
|
||||
...(params.channelId ? { channelId: params.channelId } : {}),
|
||||
...(params.toolHookContext ? { toolHookContext: params.toolHookContext } : {}),
|
||||
allowedEvents: params.events,
|
||||
ttlMs: resolveCodexSideNativeHookRelayTtlMs({
|
||||
explicitTtlMs: params.options.ttlMs,
|
||||
@@ -596,6 +609,7 @@ function buildSideRunAttemptParams(
|
||||
provider: params.provider,
|
||||
modelId: params.model,
|
||||
model: params.runtimeModel ?? ({ id: params.model, provider: params.provider } as never),
|
||||
trigger: "user" as const,
|
||||
sessionId: params.sessionId,
|
||||
sessionFile: params.sessionFile,
|
||||
sessionKey: params.sessionKey,
|
||||
@@ -616,6 +630,8 @@ function buildSideRunAttemptParams(
|
||||
...(params.senderUsername !== undefined ? { senderUsername: params.senderUsername } : {}),
|
||||
...(params.senderE164 !== undefined ? { senderE164: params.senderE164 } : {}),
|
||||
...(params.senderIsOwner !== undefined ? { senderIsOwner: params.senderIsOwner } : {}),
|
||||
...(params.chatId ? { chatId: params.chatId } : {}),
|
||||
...(params.channelContext ? { channelContext: params.channelContext } : {}),
|
||||
...(params.currentChannelId ? { currentChannelId: params.currentChannelId } : {}),
|
||||
...(params.toolsAllow ? { toolsAllow: params.toolsAllow } : {}),
|
||||
workspaceDir: options.cwd,
|
||||
@@ -647,6 +663,7 @@ async function createCodexSideToolBridge(input: {
|
||||
nativeToolSurfaceEnabled: boolean;
|
||||
nativeProviderWebSearchSupport: CodexNativeWebSearchSupport;
|
||||
signal: AbortSignal;
|
||||
toolHookContext: ToolHookRunContext;
|
||||
}): Promise<{ toolBridge: CodexDynamicToolBridge; webSearchPlan: CodexWebSearchPlan }> {
|
||||
const runtimeModel =
|
||||
input.params.runtimeModel ??
|
||||
@@ -657,10 +674,7 @@ async function createCodexSideToolBridge(input: {
|
||||
const createOpenClawCodingTools = (await import("openclaw/plugin-sdk/agent-harness"))
|
||||
.createOpenClawCodingTools;
|
||||
const sandboxSessionKey =
|
||||
input.params.sandboxSessionKey?.trim() ||
|
||||
input.params.sessionKey?.trim() ||
|
||||
input.params.sessionId ||
|
||||
input.sessionAgentId;
|
||||
input.toolHookContext.sessionKey || input.params.sessionId || input.sessionAgentId;
|
||||
const sandbox = await resolveSandboxContext({
|
||||
config: input.params.cfg,
|
||||
sessionKey: sandboxSessionKey,
|
||||
@@ -696,6 +710,9 @@ async function createCodexSideToolBridge(input: {
|
||||
workspaceDir: input.cwd,
|
||||
}),
|
||||
suppressManagedWebSearch: false,
|
||||
trigger: input.toolHookContext.trigger,
|
||||
jobId: input.toolHookContext.jobId,
|
||||
messageChannel: input.params.messageChannel,
|
||||
...(input.params.messageProvider || input.params.messageChannel
|
||||
? {
|
||||
messageProvider: messageToolProvider,
|
||||
@@ -715,6 +732,8 @@ async function createCodexSideToolBridge(input: {
|
||||
...(input.params.memberRoleIds ? { memberRoleIds: input.params.memberRoleIds } : {}),
|
||||
...(input.params.spawnedBy !== undefined ? { spawnedBy: input.params.spawnedBy } : {}),
|
||||
...(input.params.senderId !== undefined ? { senderId: input.params.senderId } : {}),
|
||||
chatId: input.toolHookContext.chatId,
|
||||
hookChannelContext: input.toolHookContext.channelContext,
|
||||
...(input.params.senderName !== undefined ? { senderName: input.params.senderName } : {}),
|
||||
...(input.params.senderUsername !== undefined
|
||||
? { senderUsername: input.params.senderUsername }
|
||||
@@ -724,12 +743,7 @@ async function createCodexSideToolBridge(input: {
|
||||
? { senderIsOwner: input.params.senderIsOwner }
|
||||
: {}),
|
||||
...(input.params.currentChannelId ? { currentChannelId: input.params.currentChannelId } : {}),
|
||||
hookChannelId: buildAgentHookContextChannelFields({
|
||||
sessionKey: input.params.sessionKey,
|
||||
messageChannel: input.params.messageChannel,
|
||||
messageProvider: input.params.messageProvider,
|
||||
currentChannelId: input.params.currentChannelId,
|
||||
}).channelId,
|
||||
hookChannelId: input.toolHookContext.channelId,
|
||||
sandbox,
|
||||
emitBeforeToolCallDiagnostics: false,
|
||||
modelHasVision: runtimeModel.input?.includes("image") ?? false,
|
||||
@@ -757,25 +771,15 @@ async function createCodexSideToolBridge(input: {
|
||||
})
|
||||
: requestedWebSearchPlan;
|
||||
const exposedTools = tools.filter((tool) => tool.name !== "web_search");
|
||||
const hookChannelFields = buildAgentHookContextChannelFields({
|
||||
sessionKey: input.params.sessionKey,
|
||||
messageChannel: input.params.messageChannel,
|
||||
messageProvider: input.params.messageProvider,
|
||||
currentChannelId: input.params.currentChannelId,
|
||||
});
|
||||
return {
|
||||
toolBridge: createCodexDynamicToolBridge({
|
||||
tools: exposedTools,
|
||||
signal: input.signal,
|
||||
loading: resolveCodexDynamicToolsLoading(input.pluginConfig),
|
||||
hookContext: {
|
||||
agentId: input.sessionAgentId,
|
||||
...input.toolHookContext,
|
||||
config: input.params.cfg,
|
||||
sessionId: input.params.sessionId,
|
||||
sessionKey: input.params.sessionKey,
|
||||
runId: input.params.opts?.runId ?? `codex-btw:${input.params.sessionId}`,
|
||||
currentChannelProvider: messageToolProvider,
|
||||
...hookChannelFields,
|
||||
},
|
||||
}),
|
||||
webSearchPlan,
|
||||
|
||||
41
extensions/codex/src/app-server/tool-hook-context.ts
Normal file
41
extensions/codex/src/app-server/tool-hook-context.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/** Builds one canonical requester-origin snapshot for Codex tool hook paths. */
|
||||
import {
|
||||
buildAgentHookContextOriginFields,
|
||||
type EmbeddedRunAttemptParams,
|
||||
type ToolHookRunContext,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
|
||||
/** Build the plain run metadata shared by Codex before/after tool hook owners. */
|
||||
export function buildCodexToolHookRunContext(params: {
|
||||
attempt: EmbeddedRunAttemptParams;
|
||||
agentId?: string;
|
||||
sessionId?: string;
|
||||
sessionKey?: string;
|
||||
runId?: string;
|
||||
channelId?: string;
|
||||
}): ToolHookRunContext {
|
||||
const attempt = params.attempt;
|
||||
const agentId = params.agentId ?? attempt.agentId;
|
||||
const sessionKey = params.sessionKey ?? attempt.sessionKey;
|
||||
const sessionId = params.sessionId ?? attempt.sessionId;
|
||||
const runId = params.runId ?? attempt.runId;
|
||||
return {
|
||||
...(agentId ? { agentId } : {}),
|
||||
...(sessionKey ? { sessionKey } : {}),
|
||||
...(sessionId ? { sessionId } : {}),
|
||||
...(runId ? { runId } : {}),
|
||||
...(attempt.jobId ? { jobId: attempt.jobId } : {}),
|
||||
...(attempt.trigger ? { trigger: attempt.trigger } : {}),
|
||||
...buildAgentHookContextOriginFields({
|
||||
sessionKey,
|
||||
messageChannel: attempt.messageChannel,
|
||||
messageProvider: attempt.messageProvider ?? attempt.messageChannel,
|
||||
currentChannelId: params.channelId ?? attempt.currentChannelId,
|
||||
messageTo: attempt.currentMessagingTarget ?? attempt.messageTo,
|
||||
trigger: attempt.trigger,
|
||||
senderId: attempt.senderId,
|
||||
chatId: attempt.chatId,
|
||||
channelContext: attempt.channelContext,
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -11,7 +11,11 @@ import type {
|
||||
PluginHookInboundClaimEvent,
|
||||
} from "openclaw/plugin-sdk/plugin-entry";
|
||||
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-payload";
|
||||
import { getSessionEntry, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import {
|
||||
loadSessionStore,
|
||||
resolveSessionStoreEntry,
|
||||
resolveStorePath,
|
||||
} from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { resolveCodexAppServerForModelProvider } from "./app-server/app-server-policy.js";
|
||||
import { resolveCodexAppServerAuthProfileIdForAgent } from "./app-server/auth-bridge.js";
|
||||
import { CODEX_CONTROL_METHODS } from "./app-server/capabilities.js";
|
||||
@@ -877,11 +881,10 @@ function readSessionExecOverrides(params: {
|
||||
return undefined;
|
||||
}
|
||||
const storePath = resolveStorePath(params.config.session?.store, { agentId: params.agentId });
|
||||
const entry = getSessionEntry({
|
||||
storePath,
|
||||
const entry = resolveSessionStoreEntry({
|
||||
store: loadSessionStore(storePath, { skipCache: true }),
|
||||
sessionKey,
|
||||
readConsistency: "latest",
|
||||
});
|
||||
}).existing;
|
||||
if (!entry?.execSecurity && !entry?.execAsk) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -340,7 +340,22 @@ describe("runCopilotAttempt", () => {
|
||||
return { sdkTools: [], sourceTools: [] };
|
||||
});
|
||||
|
||||
await runCopilotAttempt(makeParams(), {
|
||||
const params = makeParams();
|
||||
Object.assign(params, {
|
||||
jobId: "job-1",
|
||||
trigger: "user",
|
||||
messageChannel: "slack",
|
||||
messageProvider: "slack-voice",
|
||||
currentChannelId: "C123",
|
||||
chatId: "C123",
|
||||
senderId: "U123",
|
||||
channelContext: {
|
||||
sender: { id: "U123", displayName: "Ada" },
|
||||
chat: { id: "C123" },
|
||||
},
|
||||
});
|
||||
|
||||
await runCopilotAttempt(params, {
|
||||
createToolBridge,
|
||||
pool: makeFakePool(sdk),
|
||||
});
|
||||
@@ -387,7 +402,21 @@ describe("runCopilotAttempt", () => {
|
||||
toolCallId: "tool-call-1",
|
||||
toolName: "read",
|
||||
}),
|
||||
expect.objectContaining({ agentId: "agent-1", sessionId: "session-1" }),
|
||||
expect.objectContaining({
|
||||
agentId: "agent-1",
|
||||
sessionId: "session-1",
|
||||
jobId: "job-1",
|
||||
trigger: "user",
|
||||
messageProvider: "slack-voice",
|
||||
channel: "slack",
|
||||
chatId: "C123",
|
||||
senderId: "U123",
|
||||
channelId: "C123",
|
||||
channelContext: {
|
||||
sender: { id: "U123", displayName: "Ada" },
|
||||
chat: { id: "C123" },
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1287,6 +1316,32 @@ describe("runCopilotAttempt", () => {
|
||||
).toBe(sdkTools);
|
||||
});
|
||||
|
||||
it("passes the session-resolved agent id to the tool bridge", async () => {
|
||||
const sdk = makeFakeSdk();
|
||||
const pool = makeFakePool(sdk);
|
||||
const createToolBridge = vi.fn(async () => ({ sdkTools: [], sourceTools: [] }));
|
||||
|
||||
await runCopilotAttempt(
|
||||
makeParams({
|
||||
agentId: undefined,
|
||||
sessionKey: "agent:beta:main",
|
||||
config: {
|
||||
agents: {
|
||||
list: [{ id: "main" }, { id: "beta" }],
|
||||
},
|
||||
} as never,
|
||||
}),
|
||||
{ createToolBridge, pool },
|
||||
);
|
||||
|
||||
expect(createToolBridge).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentId: "beta",
|
||||
sessionKey: "agent:beta:main",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("F6: sessionRef is populated after createSession so the tool bridge's onYield can abort the live SDK session", async () => {
|
||||
const sdk = makeFakeSdk();
|
||||
const pool = makeFakePool(sdk);
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import {
|
||||
buildAgentHookContextChannelFields,
|
||||
buildAgentHookContextOriginFields,
|
||||
detectAndLoadAgentHarnessPromptImages,
|
||||
getModelProviderRequestTransport,
|
||||
resolveAgentHarnessBeforePromptBuildResult,
|
||||
@@ -409,6 +410,25 @@ export async function runCopilotAttempt(
|
||||
...hookContextWindowFields,
|
||||
...buildAgentHookContextChannelFields(input),
|
||||
};
|
||||
const toolHookRunContext = {
|
||||
runId: input.runId,
|
||||
jobId: input.jobId,
|
||||
agentId: sessionAgentId,
|
||||
sessionKey: sandboxSessionKey,
|
||||
sessionId: input.sessionId,
|
||||
trigger: input.trigger,
|
||||
...buildAgentHookContextOriginFields({
|
||||
sessionKey: sandboxSessionKey,
|
||||
messageChannel: input.messageChannel,
|
||||
messageProvider: input.messageProvider ?? input.messageChannel,
|
||||
currentChannelId: input.currentChannelId,
|
||||
messageTo: input.currentMessagingTarget ?? input.messageTo,
|
||||
trigger: input.trigger,
|
||||
senderId: input.senderId,
|
||||
chatId: input.chatId,
|
||||
channelContext: input.channelContext,
|
||||
}),
|
||||
};
|
||||
const finishAttempt = (result: AgentHarnessAttemptResult) =>
|
||||
finalizeCopilotAttempt(input, result, hookContext, attemptStartedAt, now);
|
||||
|
||||
@@ -626,7 +646,7 @@ export async function runCopilotAttempt(
|
||||
allowModelTools: poolAcquire.provider.mode === "byok",
|
||||
modelProvider: modelRef.provider,
|
||||
modelId: modelRef.id,
|
||||
agentId: readString(params.agentId) ?? "copilot",
|
||||
agentId: sessionAgentId,
|
||||
sessionId: readString(input.sessionId) ?? "copilot-session",
|
||||
sessionKey: readString((input as { sessionKey?: unknown }).sessionKey),
|
||||
agentDir: readString(input.agentDir),
|
||||
@@ -652,11 +672,7 @@ export async function runCopilotAttempt(
|
||||
runAgentHarnessAfterToolCallHook({
|
||||
toolName,
|
||||
toolCallId,
|
||||
runId: input.runId,
|
||||
agentId: sessionAgentId,
|
||||
sessionId: input.sessionId,
|
||||
sessionKey: sandboxSessionKey,
|
||||
channelId: hookContext.channelId,
|
||||
...toolHookRunContext,
|
||||
startArgs: args,
|
||||
...(result !== undefined ? { result } : {}),
|
||||
...(error ? { error } : {}),
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
// Copilot tests cover tool bridge plugin behavior.
|
||||
import type { Tool as SdkTool, ToolInvocation, ToolResultObject } from "@github/copilot-sdk";
|
||||
import type { AnyAgentTool, SandboxContext } from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import {
|
||||
initializeGlobalHookRunner,
|
||||
resetGlobalHookRunner,
|
||||
} from "openclaw/plugin-sdk/hook-runtime";
|
||||
import { createMockPluginRegistry } from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createCopilotToolBridge,
|
||||
convertOpenClawToolToSdkTool,
|
||||
supportsModelTools,
|
||||
testing,
|
||||
} from "./tool-bridge.js";
|
||||
|
||||
type FakeTool = AnyAgentTool & {
|
||||
@@ -77,6 +83,7 @@ function runSdkTool(tool: SdkTool, args: unknown, invocation = makeInvocation())
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
resetGlobalHookRunner();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
@@ -309,6 +316,79 @@ describe("createCopilotToolBridge", () => {
|
||||
expect(result.sdkTools.map((tool) => tool.name)).toEqual(["exec", "wait"]);
|
||||
});
|
||||
|
||||
it("runs requester-aware policy before code-mode exec controls", async () => {
|
||||
const beforeToolCall = vi.fn(() => ({
|
||||
block: true,
|
||||
blockReason: "blocked before code-mode execution",
|
||||
}));
|
||||
initializeGlobalHookRunner(
|
||||
createMockPluginRegistry([{ hookName: "before_tool_call", handler: beforeToolCall }]),
|
||||
);
|
||||
const createOpenClawCodingTools = vi.fn(async () => [makeTool({ name: "read" })]);
|
||||
|
||||
const result = await createCopilotToolBridge({
|
||||
agentId: "agent-1",
|
||||
attemptParams: {
|
||||
config: { tools: { codeMode: true } },
|
||||
runId: "run-code-mode",
|
||||
sessionId: "session-1",
|
||||
sessionKey: "agent:main:main",
|
||||
jobId: "job-1",
|
||||
trigger: "user",
|
||||
messageChannel: "slack",
|
||||
messageProvider: "slack-voice",
|
||||
currentChannelId: "slack:C123",
|
||||
senderId: "U123",
|
||||
channelContext: { sender: { id: "U123", displayName: "Ada" } },
|
||||
} as never,
|
||||
createOpenClawCodingTools,
|
||||
modelId: "gpt-4o",
|
||||
modelProvider: "github-copilot",
|
||||
sessionId: "session-1",
|
||||
});
|
||||
const exec = result.sdkTools.find((tool) => tool.name === "exec");
|
||||
if (!exec) {
|
||||
throw new Error("missing code-mode exec control");
|
||||
}
|
||||
|
||||
await runSdkTool(
|
||||
exec,
|
||||
{ code: "return 1;" },
|
||||
makeInvocation({ toolCallId: "code-call-1", toolName: "exec" }),
|
||||
);
|
||||
|
||||
expect(beforeToolCall).toHaveBeenCalledTimes(1);
|
||||
expect(beforeToolCall).toHaveBeenCalledWith(
|
||||
{
|
||||
toolName: "exec",
|
||||
params: { code: "return 1;", command: "return 1;" },
|
||||
toolKind: "code_mode_exec",
|
||||
toolInputKind: "javascript",
|
||||
runId: "run-code-mode",
|
||||
toolCallId: "code-call-1",
|
||||
},
|
||||
{
|
||||
toolName: "exec",
|
||||
toolKind: "code_mode_exec",
|
||||
toolInputKind: "javascript",
|
||||
agentId: "agent-1",
|
||||
sessionKey: "agent:main:main",
|
||||
sessionId: "session-1",
|
||||
runId: "run-code-mode",
|
||||
jobId: "job-1",
|
||||
trigger: "user",
|
||||
messageProvider: "slack-voice",
|
||||
channel: "slack",
|
||||
senderId: "U123",
|
||||
toolCallId: "code-call-1",
|
||||
channelId: "C123",
|
||||
channelContext: {
|
||||
sender: { id: "U123", displayName: "Ada" },
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps code-mode controls visible when a narrow allowlist is active", async () => {
|
||||
const createOpenClawCodingTools = vi.fn(async () => [
|
||||
makeTool({ name: "fake_hidden" }),
|
||||
@@ -443,7 +523,13 @@ describe("createCopilotToolBridge", () => {
|
||||
currentMessagingTarget: "user:U123",
|
||||
currentThreadTs: "1700000000.000100",
|
||||
currentMessageId: "M-1",
|
||||
messageProvider: "slack",
|
||||
messageChannel: "slack",
|
||||
messageProvider: "slack-voice",
|
||||
chatId: "chat-1",
|
||||
channelContext: {
|
||||
sender: { id: "sender-1", displayName: "Ada" },
|
||||
chat: { id: "chat-1", kind: "channel" },
|
||||
},
|
||||
messageTo: "U-1",
|
||||
messageThreadId: "1700000000.000100",
|
||||
replyToMode: "first",
|
||||
@@ -477,7 +563,13 @@ describe("createCopilotToolBridge", () => {
|
||||
currentMessagingTarget: "user:U123",
|
||||
currentThreadTs: "1700000000.000100",
|
||||
currentMessageId: "M-1",
|
||||
messageProvider: "slack",
|
||||
messageChannel: "slack",
|
||||
messageProvider: "slack-voice",
|
||||
chatId: "chat-1",
|
||||
hookChannelContext: {
|
||||
sender: { id: "sender-1", displayName: "Ada" },
|
||||
chat: { id: "chat-1", kind: "channel" },
|
||||
},
|
||||
messageTo: "U-1",
|
||||
messageThreadId: "1700000000.000100",
|
||||
replyToMode: "first",
|
||||
@@ -485,6 +577,7 @@ describe("createCopilotToolBridge", () => {
|
||||
forceMessageTool: true,
|
||||
enableHeartbeatTool: true,
|
||||
});
|
||||
expect(opts.channelContext).toBeUndefined();
|
||||
});
|
||||
|
||||
it("falls back messageProvider to attemptParams.messageChannel when messageProvider is absent (codex parity)", async () => {
|
||||
@@ -502,6 +595,63 @@ describe("createCopilotToolBridge", () => {
|
||||
expect(getOpts().messageProvider).toBe("telegram");
|
||||
});
|
||||
|
||||
it("uses messageTo when currentMessagingTarget is absent in tool hook routing", () => {
|
||||
const context = testing.buildCopilotToolHookContext({
|
||||
agentId: "agent-1",
|
||||
messageChannel: "slack",
|
||||
messageProvider: "slack",
|
||||
messageTo: "user:U-only",
|
||||
trigger: "user",
|
||||
});
|
||||
|
||||
expect(context).toMatchObject({
|
||||
channel: "slack",
|
||||
messageProvider: "slack",
|
||||
channelId: "U-only",
|
||||
turnSourceChannel: "slack",
|
||||
turnSourceTo: "user:U-only",
|
||||
});
|
||||
expect(context.chatId).toBeUndefined();
|
||||
expect(context.channelContext).toBeUndefined();
|
||||
});
|
||||
|
||||
it("resolves per-agent loop detection overrides for generated code-mode controls", () => {
|
||||
const context = testing.buildCopilotToolHookContext({
|
||||
agentId: "agent-1",
|
||||
config: {
|
||||
tools: {
|
||||
loopDetection: {
|
||||
enabled: true,
|
||||
warningThreshold: 7,
|
||||
detectors: { genericRepeat: true },
|
||||
postCompactionGuard: { windowSize: 4 },
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "agent-1",
|
||||
tools: {
|
||||
loopDetection: {
|
||||
enabled: false,
|
||||
detectors: { pingPong: false },
|
||||
postCompactionGuard: { windowSize: 2 },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(context.loopDetection).toEqual({
|
||||
enabled: false,
|
||||
warningThreshold: 7,
|
||||
detectors: { genericRepeat: true, pingPong: false },
|
||||
postCompactionGuard: { windowSize: 2 },
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards authProfileStore, runId, config, and run hooks (onToolOutcome) from attemptParams", async () => {
|
||||
const { createOpenClawCodingTools, getOpts } = captureCall();
|
||||
const authProfileStore = { kind: "fake-store" } as never;
|
||||
|
||||
@@ -7,15 +7,19 @@ import type {
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import {
|
||||
applyEmbeddedAttemptToolsAllow,
|
||||
buildAgentHookContextOriginFields,
|
||||
buildEmbeddedAttemptToolRunContext,
|
||||
extractToolErrorMessage,
|
||||
getPluginToolMeta,
|
||||
isSubagentSessionKey,
|
||||
isToolWrappedWithBeforeToolCallHook,
|
||||
isToolResultError,
|
||||
resolveAttemptSpawnWorkspaceDir,
|
||||
resolveEmbeddedAttemptToolConstructionPlan,
|
||||
resolveModelAuthMode,
|
||||
resolveToolLoopDetectionConfig,
|
||||
sanitizeToolResult,
|
||||
wrapToolWithBeforeToolCallHook,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { createAgentHarnessToolSurfaceRuntime } from "openclaw/plugin-sdk/agent-harness-tool-runtime";
|
||||
|
||||
@@ -144,6 +148,7 @@ export interface CopilotToolBridge {
|
||||
export const SUPPORTED_TOOL_PROVIDERS: ReadonlySet<string> = new Set(["github-copilot"]);
|
||||
const BASE_COPILOT_CODING_TOOL_NAMES = new Set(["edit", "read", "write"]);
|
||||
const SHELL_COPILOT_CODING_TOOL_NAMES = new Set(["apply_patch", "exec", "process"]);
|
||||
const CODE_MODE_CONTROL_TOOL_NAMES = new Set(["exec", "wait"]);
|
||||
|
||||
export function supportsModelTools(modelProvider: string): boolean {
|
||||
return SUPPORTED_TOOL_PROVIDERS.has(modelProvider);
|
||||
@@ -210,6 +215,7 @@ export async function createCopilotToolBridge(
|
||||
},
|
||||
toolSurfaceRuntime,
|
||||
);
|
||||
const toolHookContext = buildCopilotToolHookContext(toolOptions);
|
||||
|
||||
let sourceTools: unknown;
|
||||
try {
|
||||
@@ -231,9 +237,18 @@ export async function createCopilotToolBridge(
|
||||
sourceTools as AnyAgentTool[],
|
||||
toolSurfaceRuntime.runtimeToolAllowlist,
|
||||
);
|
||||
const compactedTools = toolSurfaceRuntime.compactTools(allowedSourceTools);
|
||||
const compactedTools = toolSurfaceRuntime.compactTools(allowedSourceTools, {
|
||||
hookContext: toolHookContext,
|
||||
});
|
||||
const hookedCompactedTools = compactedTools.tools.map((tool) =>
|
||||
!toolSurfaceRuntime.codeModeControlsEnabled ||
|
||||
!CODE_MODE_CONTROL_TOOL_NAMES.has(tool.name) ||
|
||||
isToolWrappedWithBeforeToolCallHook(tool)
|
||||
? tool
|
||||
: wrapToolWithBeforeToolCallHook(tool, toolHookContext),
|
||||
);
|
||||
const plannedTools = filterCopilotToolsForConstructionPlan(
|
||||
compactedTools.tools,
|
||||
hookedCompactedTools,
|
||||
effectiveToolPlan.codingToolConstructionPlan,
|
||||
{ preserveToolNames: toolSurfaceRuntime.runtimeToolAllowlist },
|
||||
);
|
||||
@@ -264,6 +279,51 @@ export async function createCopilotToolBridge(
|
||||
};
|
||||
}
|
||||
|
||||
function buildCopilotToolHookContext(toolOptions: OpenClawCodingToolsOptions) {
|
||||
const turnSourceChannel = toolOptions.messageChannel ?? toolOptions.messageProvider;
|
||||
const messageTo = toolOptions.currentMessagingTarget ?? toolOptions.messageTo;
|
||||
const turnSourceTo = messageTo ?? toolOptions.currentChannelId;
|
||||
return {
|
||||
agentId: toolOptions.agentId,
|
||||
config: toolOptions.config,
|
||||
cwd: toolOptions.cwd,
|
||||
workspaceDir: toolOptions.workspaceDir,
|
||||
sessionKey: toolOptions.sessionKey,
|
||||
sessionId: toolOptions.sessionId,
|
||||
runId: toolOptions.runId,
|
||||
jobId: toolOptions.jobId,
|
||||
trace: toolOptions.trace,
|
||||
trigger: toolOptions.trigger,
|
||||
...buildAgentHookContextOriginFields({
|
||||
sessionKey: toolOptions.sessionKey,
|
||||
messageChannel: toolOptions.messageChannel,
|
||||
messageProvider: toolOptions.toolPolicyMessageProvider ?? toolOptions.messageProvider,
|
||||
currentChannelId: toolOptions.hookChannelId ?? toolOptions.currentChannelId,
|
||||
messageTo,
|
||||
trigger: toolOptions.trigger,
|
||||
senderId: toolOptions.senderId,
|
||||
chatId: toolOptions.chatId,
|
||||
channelContext: toolOptions.hookChannelContext ?? toolOptions.channelContext,
|
||||
}),
|
||||
...(turnSourceChannel ? { turnSourceChannel } : {}),
|
||||
...(turnSourceTo ? { turnSourceTo } : {}),
|
||||
...(toolOptions.agentAccountId ? { turnSourceAccountId: toolOptions.agentAccountId } : {}),
|
||||
...(toolOptions.currentThreadTs ? { turnSourceThreadId: toolOptions.currentThreadTs } : {}),
|
||||
loopDetection: resolveToolLoopDetectionConfig({
|
||||
cfg: toolOptions.config,
|
||||
agentId: toolOptions.agentId,
|
||||
}),
|
||||
onToolOutcome: toolOptions.onToolOutcome,
|
||||
allocateToolOutcomeOrdinal: toolOptions.allocateToolOutcomeOrdinal,
|
||||
};
|
||||
}
|
||||
|
||||
/** Test-only access to requester-context construction. */
|
||||
export const testing = {
|
||||
buildCopilotToolHookContext: (toolOptions: unknown): Record<string, unknown> =>
|
||||
buildCopilotToolHookContext(toolOptions as OpenClawCodingToolsOptions),
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds the full `createOpenClawCodingTools` options bag mirroring the
|
||||
* PI in-tree call at `src/agents/pi-embedded-runner/run/attempt.ts:1029-1117`.
|
||||
@@ -350,7 +410,9 @@ function buildOpenClawCodingToolsOptions(
|
||||
...a.execOverrides,
|
||||
elevated: a.bashElevated,
|
||||
},
|
||||
messageChannel: a.messageChannel,
|
||||
messageProvider: a.messageProvider ?? a.messageChannel,
|
||||
toolPolicyMessageProvider: a.messageProvider ?? a.messageChannel,
|
||||
agentAccountId: a.agentAccountId,
|
||||
messageTo: a.messageTo,
|
||||
messageThreadId: a.messageThreadId,
|
||||
@@ -360,6 +422,7 @@ function buildOpenClawCodingToolsOptions(
|
||||
memberRoleIds: a.memberRoleIds,
|
||||
spawnedBy: a.spawnedBy,
|
||||
senderId: a.senderId,
|
||||
hookChannelContext: a.channelContext,
|
||||
senderName: a.senderName,
|
||||
senderUsername: a.senderUsername,
|
||||
senderE164: a.senderE164,
|
||||
@@ -395,6 +458,7 @@ function buildOpenClawCodingToolsOptions(
|
||||
workspaceDir,
|
||||
}),
|
||||
currentChannelId: a.currentChannelId,
|
||||
chatId: a.chatId,
|
||||
currentMessagingTarget: a.currentMessagingTarget,
|
||||
currentThreadTs: a.currentThreadTs,
|
||||
currentMessageId: a.currentMessageId,
|
||||
|
||||
@@ -175,7 +175,6 @@ type DispatchInboundParams = {
|
||||
}) => Promise<void> | void;
|
||||
onReplyStart?: () => Promise<void> | void;
|
||||
sourceReplyDeliveryMode?: "automatic" | "message_tool_only";
|
||||
typingKeepalive?: boolean;
|
||||
disableBlockStreaming?: boolean;
|
||||
suppressDefaultToolProgressMessages?: boolean;
|
||||
queuedDeliveryCorrelations?: Array<{ begin: () => () => void }>;
|
||||
@@ -945,7 +944,6 @@ describe("processDiscordMessage ack reactions", () => {
|
||||
expect(replyTypingFeedback.onReplyStart).toHaveBeenCalledTimes(1);
|
||||
expect(replyTypingFeedback.onIdle).toHaveBeenCalledTimes(1);
|
||||
expect(replyTypingFeedback.onCleanup).toHaveBeenCalledTimes(1);
|
||||
expect(getLastDispatchReplyOptions()?.typingKeepalive).toBe(false);
|
||||
expect(typingMocks.sendTyping).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -986,33 +984,6 @@ describe("processDiscordMessage ack reactions", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps one typing refresh loop for default message-tool replies", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
|
||||
await params?.replyOptions?.onReplyStart?.();
|
||||
await vi.advanceTimersByTimeAsync(3_500);
|
||||
return createNoQueuedDispatchResult();
|
||||
});
|
||||
const ctx = await createBaseContext({
|
||||
shouldRequireMention: false,
|
||||
effectiveWasMentioned: false,
|
||||
cfg: {
|
||||
messages: { groupChat: { visibleReplies: "message_tool" } },
|
||||
session: { store: "/tmp/openclaw-discord-process-test-sessions.json" },
|
||||
},
|
||||
route: BASE_CHANNEL_ROUTE,
|
||||
});
|
||||
|
||||
await runProcessDiscordMessage(ctx);
|
||||
|
||||
expect(getLastDispatchReplyOptions()?.typingKeepalive).toBe(false);
|
||||
expect(typingMocks.sendTyping).toHaveBeenCalledTimes(2);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("debounces intermediate phase reactions and jumps to done for short runs", async () => {
|
||||
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
|
||||
await params?.replyOptions?.onReasoningStream?.();
|
||||
@@ -1561,7 +1532,6 @@ describe("processDiscordMessage session routing", () => {
|
||||
|
||||
expectRecordFields(requireRecord(getLastDispatchReplyOptions(), "dispatch reply options"), {
|
||||
sourceReplyDeliveryMode: "message_tool_only",
|
||||
typingKeepalive: false,
|
||||
disableBlockStreaming: true,
|
||||
});
|
||||
expect(createDiscordDraftStream).not.toHaveBeenCalled();
|
||||
|
||||
@@ -251,14 +251,6 @@ async function processDiscordMessageInner(
|
||||
},
|
||||
});
|
||||
const sourceRepliesAreToolOnly = sourceReplyDeliveryMode === "message_tool_only";
|
||||
const configuredTypingMode = cfg.session?.typingMode ?? cfg.agents?.defaults?.typingMode;
|
||||
const configuredTypingInterval =
|
||||
cfg.agents?.defaults?.typingIntervalSeconds ?? cfg.session?.typingIntervalSeconds;
|
||||
const shouldDisableCoreTypingKeepalive =
|
||||
Boolean(replyTypingFeedback) ||
|
||||
(sourceRepliesAreToolOnly &&
|
||||
configuredTypingMode === undefined &&
|
||||
configuredTypingInterval === undefined);
|
||||
const ackReaction = resolveAckReaction(cfg, route.agentId, {
|
||||
channel: "discord",
|
||||
accountId,
|
||||
@@ -468,7 +460,6 @@ async function processDiscordMessageInner(
|
||||
channelId: typingChannelId,
|
||||
rest: feedbackRest,
|
||||
log: logVerbose,
|
||||
keepaliveIntervalMs: shouldDisableCoreTypingKeepalive ? undefined : 0,
|
||||
});
|
||||
if (replyTypingFeedback) {
|
||||
// A carried prestart only covers queue wait time; dispatch needs a fresh
|
||||
@@ -964,7 +955,6 @@ async function processDiscordMessageInner(
|
||||
abortSignal,
|
||||
skillFilter: channelConfig?.skills,
|
||||
sourceReplyDeliveryMode,
|
||||
typingKeepalive: shouldDisableCoreTypingKeepalive ? false : undefined,
|
||||
queuedDeliveryCorrelations: isRoomEvent ? [{ begin: beginDeliveryCorrelation }] : undefined,
|
||||
suppressTyping: isRoomEvent ? true : undefined,
|
||||
allowProgressCallbacksWhenSourceDeliverySuppressed:
|
||||
|
||||
@@ -222,34 +222,6 @@ describe("createDiscordMessageHandler queue behavior", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the configured typing cadence for prestarted feedback", async () => {
|
||||
preflightDiscordMessageMock.mockReset();
|
||||
processDiscordMessageMock.mockReset();
|
||||
preflightDiscordMessageMock.mockImplementation(async () =>
|
||||
createAcceptedDmPreflightContext({
|
||||
cfg: {
|
||||
...createPreflightContext().cfg,
|
||||
agents: { defaults: { typingIntervalSeconds: 7 } },
|
||||
session: { typingIntervalSeconds: 5 },
|
||||
},
|
||||
}),
|
||||
);
|
||||
processDiscordMessageMock.mockResolvedValue(undefined);
|
||||
const replyTypingFeedback = createReplyTypingFeedbackMock("dm-1");
|
||||
const createReplyTypingFeedback = vi.fn(() => replyTypingFeedback);
|
||||
|
||||
const handler = createDiscordMessageHandler({
|
||||
...createDiscordHandlerParams(),
|
||||
testing: { createReplyTypingFeedback },
|
||||
});
|
||||
await handler(createMessageData("m-typing-cadence", "dm-1") as never, {} as never);
|
||||
await flushQueueWork();
|
||||
|
||||
expect(createReplyTypingFeedback).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ keepaliveIntervalMs: 7_000 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps accepted DM dispatch running when accepted typing feedback fails", async () => {
|
||||
preflightDiscordMessageMock.mockReset();
|
||||
processDiscordMessageMock.mockReset();
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
createChannelInboundDebouncer,
|
||||
shouldDebounceTextInbound,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { finiteSecondsToTimerSafeMilliseconds } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { danger, logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { resolveOpenProviderRuntimeGroupPolicy } from "openclaw/plugin-sdk/runtime-group-policy";
|
||||
import type { Client } from "../internal/discord.js";
|
||||
@@ -103,9 +102,6 @@ function startAcceptedTypingFeedback(params: {
|
||||
accountId: ctx.accountId,
|
||||
channelId: ctx.messageChannelId,
|
||||
log: logVerbose,
|
||||
keepaliveIntervalMs: finiteSecondsToTimerSafeMilliseconds(
|
||||
ctx.cfg.agents?.defaults?.typingIntervalSeconds ?? ctx.cfg.session?.typingIntervalSeconds,
|
||||
),
|
||||
});
|
||||
const cleanup = replyTypingFeedback.onCleanup;
|
||||
replyTypingFeedback.onCleanup = () => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "openclaw/plugin-sdk/command-auth-native";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import type { ResolvedAgentRoute } from "openclaw/plugin-sdk/routing";
|
||||
import { getSessionEntry, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { loadSessionStore, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
@@ -202,10 +202,11 @@ export async function resolveDiscordNativeChoiceContext(params: {
|
||||
const storePath = resolveStorePath(params.cfg.session?.store, {
|
||||
agentId: route.agentId,
|
||||
});
|
||||
const sessionEntry = getSessionEntry({ storePath, sessionKey: route.sessionKey });
|
||||
const sessionStore = loadSessionStore(storePath);
|
||||
const sessionEntry = sessionStore[route.sessionKey];
|
||||
const override = resolveStoredModelOverride({
|
||||
sessionEntry,
|
||||
loadSessionEntry: (sessionKey) => getSessionEntry({ storePath, sessionKey }),
|
||||
sessionStore,
|
||||
sessionKey: route.sessionKey,
|
||||
defaultProvider: fallback.provider,
|
||||
});
|
||||
@@ -237,15 +238,11 @@ export function resolveDiscordModelPickerCurrentModel(params: {
|
||||
const storePath = resolveStorePath(params.cfg.session?.store, {
|
||||
agentId: params.route.agentId,
|
||||
});
|
||||
const sessionEntry = getSessionEntry({
|
||||
storePath,
|
||||
sessionKey: params.route.sessionKey,
|
||||
readConsistency: "latest",
|
||||
});
|
||||
const sessionStore = loadSessionStore(storePath, { skipCache: true });
|
||||
const sessionEntry = sessionStore[params.route.sessionKey];
|
||||
const override = resolveStoredModelOverride({
|
||||
sessionEntry,
|
||||
loadSessionEntry: (sessionKey) =>
|
||||
getSessionEntry({ storePath, sessionKey, readConsistency: "latest" }),
|
||||
sessionStore,
|
||||
sessionKey: params.route.sessionKey,
|
||||
defaultProvider: params.data.resolvedDefault.provider,
|
||||
});
|
||||
@@ -270,12 +267,9 @@ export function resolveDiscordModelPickerCurrentRuntime(params: {
|
||||
const storePath = resolveStorePath(params.cfg.session?.store, {
|
||||
agentId: params.route.agentId,
|
||||
});
|
||||
const sessionStore = loadSessionStore(storePath, { skipCache: true });
|
||||
const sessionRuntime = normalizeOptionalString(
|
||||
getSessionEntry({
|
||||
storePath,
|
||||
sessionKey: params.route.sessionKey,
|
||||
readConsistency: "latest",
|
||||
})?.agentRuntimeOverride,
|
||||
sessionStore[params.route.sessionKey]?.agentRuntimeOverride,
|
||||
);
|
||||
if (sessionRuntime) {
|
||||
return sessionRuntime;
|
||||
|
||||
@@ -24,7 +24,6 @@ export function createDiscordReplyTypingFeedback(params: {
|
||||
rest?: RequestClient;
|
||||
log: (message: string) => void;
|
||||
maxDurationMs?: number;
|
||||
keepaliveIntervalMs?: number;
|
||||
}): DiscordReplyTypingFeedback {
|
||||
let channelId = params.channelId;
|
||||
const rest =
|
||||
@@ -45,7 +44,6 @@ export function createDiscordReplyTypingFeedback(params: {
|
||||
error: err,
|
||||
});
|
||||
},
|
||||
keepaliveIntervalMs: params.keepaliveIntervalMs,
|
||||
maxDurationMs: params.maxDurationMs ?? DISCORD_REPLY_TYPING_MAX_DURATION_MS,
|
||||
});
|
||||
const updateChannelId = (nextChannelId: string) => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Duckduckgo plugin module implements ddg client behavior.
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { readProviderTextResponse } from "openclaw/plugin-sdk/provider-http";
|
||||
import {
|
||||
DEFAULT_CACHE_TTL_MINUTES,
|
||||
DEFAULT_SEARCH_COUNT,
|
||||
@@ -37,49 +36,21 @@ type DuckDuckGoResult = {
|
||||
};
|
||||
|
||||
function decodeHtmlEntities(text: string): string {
|
||||
return text.replace(
|
||||
/&(?:lt|gt|quot|apos|#39|#x27|#x2F|nbsp|ndash|mdash|hellip|amp|#\d+|#x[0-9a-f]+);/gi,
|
||||
(entity) => {
|
||||
const normalized = entity.toLowerCase();
|
||||
if (normalized === "<") {
|
||||
return "<";
|
||||
}
|
||||
if (normalized === ">") {
|
||||
return ">";
|
||||
}
|
||||
if (normalized === """) {
|
||||
return '"';
|
||||
}
|
||||
if (normalized === "'" || normalized === "'" || normalized === "'") {
|
||||
return "'";
|
||||
}
|
||||
if (normalized === "/") {
|
||||
return "/";
|
||||
}
|
||||
if (normalized === " ") {
|
||||
return " ";
|
||||
}
|
||||
if (normalized === "–") {
|
||||
return "-";
|
||||
}
|
||||
if (normalized === "—") {
|
||||
return "--";
|
||||
}
|
||||
if (normalized === "…") {
|
||||
return "...";
|
||||
}
|
||||
if (normalized === "&") {
|
||||
return "&";
|
||||
}
|
||||
if (normalized.startsWith("&#x")) {
|
||||
return String.fromCodePoint(Number.parseInt(normalized.slice(3, -1), 16));
|
||||
}
|
||||
if (normalized.startsWith("&#")) {
|
||||
return String.fromCodePoint(Number.parseInt(normalized.slice(2, -1), 10));
|
||||
}
|
||||
return entity;
|
||||
},
|
||||
);
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/'/g, "'")
|
||||
.replace(/'/g, "'")
|
||||
.replace(///g, "/")
|
||||
.replace(/ /g, " ")
|
||||
.replace(/–/g, "-")
|
||||
.replace(/—/g, "--")
|
||||
.replace(/…/g, "...")
|
||||
.replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code)))
|
||||
.replace(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCodePoint(Number.parseInt(code, 16)));
|
||||
}
|
||||
|
||||
function stripHtml(html: string): string {
|
||||
@@ -114,10 +85,6 @@ function isBotChallenge(html: string): boolean {
|
||||
return /g-recaptcha|are you a human|id="challenge-form"|name="challenge"/i.test(html);
|
||||
}
|
||||
|
||||
async function readDuckDuckGoHtmlResponse(response: Response): Promise<string> {
|
||||
return await readProviderTextResponse(response, "DuckDuckGo search");
|
||||
}
|
||||
|
||||
function parseDuckDuckGoHtml(html: string): DuckDuckGoResult[] {
|
||||
const results: DuckDuckGoResult[] = [];
|
||||
const resultRegex = /<a\b(?=[^>]*\bclass="[^"]*\bresult__a\b[^"]*")([^>]*)>([\s\S]*?)<\/a>/gi;
|
||||
@@ -207,7 +174,7 @@ export async function runDuckDuckGoSearch(params: {
|
||||
);
|
||||
}
|
||||
|
||||
const html = await readDuckDuckGoHtmlResponse(response);
|
||||
const html = await response.text();
|
||||
if (isBotChallenge(html)) {
|
||||
throw new Error("DuckDuckGo returned a bot-detection challenge.");
|
||||
}
|
||||
@@ -243,6 +210,5 @@ export const testing = {
|
||||
decodeHtmlEntities,
|
||||
isBotChallenge,
|
||||
parseDuckDuckGoHtml,
|
||||
readDuckDuckGoHtmlResponse,
|
||||
};
|
||||
export { testing as __testing };
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Duckduckgo tests cover ddg search provider plugin behavior.
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createStreamingResponse } from "../../test-support/streaming-error-response.js";
|
||||
import { createDuckDuckGoWebSearchProvider as createDuckDuckGoWebSearchContractProvider } from "../web-search-contract-api.js";
|
||||
import { DEFAULT_DDG_SAFE_SEARCH, resolveDdgRegion, resolveDdgSafeSearch } from "./config.js";
|
||||
|
||||
@@ -105,24 +104,6 @@ describe("duckduckgo web search provider", () => {
|
||||
expect(runDuckDuckGoSearch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("bounds successful DuckDuckGo HTML bodies without using response.text()", async () => {
|
||||
const streamed = createStreamingResponse({
|
||||
chunkCount: 32,
|
||||
chunkSize: 1024 * 1024,
|
||||
text: "x",
|
||||
headers: { "Content-Type": "text/html" },
|
||||
});
|
||||
const textSpy = vi.spyOn(streamed.response, "text").mockRejectedValue(new Error("unbounded"));
|
||||
|
||||
await expect(ddgClientTesting.readDuckDuckGoHtmlResponse(streamed.response)).rejects.toThrow(
|
||||
"DuckDuckGo search: text response exceeds 16777216 bytes",
|
||||
);
|
||||
|
||||
expect(streamed.getReadCount()).toBeLessThan(32);
|
||||
expect(streamed.wasCanceled()).toBe(true);
|
||||
expect(textSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reads region from plugin config and normalizes empty values away", () => {
|
||||
expect(
|
||||
resolveDdgRegion({
|
||||
@@ -205,17 +186,6 @@ describe("duckduckgo web search provider", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not double-decode escaped entities (decodes & last)", () => {
|
||||
// A result whose text literally shows "<" arrives double-encoded as
|
||||
// "&lt;". Decoding & first would re-decode it into "<", corrupting
|
||||
// the snippet; & must be decoded last.
|
||||
expect(ddgClientTesting.decodeHtmlEntities("How to escape &lt; in HTML")).toBe(
|
||||
"How to escape < in HTML",
|
||||
);
|
||||
expect(ddgClientTesting.decodeHtmlEntities("a&#39;b")).toBe("a'b");
|
||||
expect(ddgClientTesting.decodeHtmlEntities("a&amp;b")).toBe("a&b");
|
||||
});
|
||||
|
||||
it("parses results when href appears before class", () => {
|
||||
const html = `
|
||||
<a href="https://duckduckgo.com/l/?uddg=https%3A%2F%2Fexample.com" class="result__a">
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
wrapWebContent,
|
||||
writeCachedSearchPayload,
|
||||
} from "openclaw/plugin-sdk/provider-web-search";
|
||||
import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
|
||||
import {
|
||||
normalizeOptionalLowercaseString,
|
||||
normalizeOptionalString,
|
||||
@@ -31,10 +30,6 @@ const EXA_SEARCH_TYPES = ["auto", "neural", "fast", "deep", "deep-reasoning", "i
|
||||
const EXA_FRESHNESS_VALUES = ["day", "week", "month", "year"] as const;
|
||||
const EXA_MAX_SEARCH_COUNT = 100;
|
||||
const EXA_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
|
||||
// Exa search responses are untrusted external bodies. Cap the success JSON the
|
||||
// same way other bundled providers do (16 MiB) so a misbehaving or hostile
|
||||
// endpoint cannot stream an unbounded body into memory before we parse it.
|
||||
const EXA_SEARCH_JSON_MAX_BYTES = 16 * 1024 * 1024;
|
||||
|
||||
type ExaConfig = {
|
||||
apiKey?: string;
|
||||
@@ -75,17 +70,9 @@ type ExaSearchResponse = {
|
||||
results?: unknown;
|
||||
};
|
||||
|
||||
async function readExaSearchResults(
|
||||
response: Response,
|
||||
opts?: { maxBytes?: number },
|
||||
): Promise<ExaSearchResult[]> {
|
||||
const maxBytes = opts?.maxBytes ?? EXA_SEARCH_JSON_MAX_BYTES;
|
||||
const bytes = await readResponseWithLimit(response, maxBytes, {
|
||||
onOverflow: ({ maxBytes: maxBytesLocal }) =>
|
||||
new Error(`Exa API response exceeds ${maxBytesLocal} bytes`),
|
||||
});
|
||||
async function readExaSearchResults(response: Response): Promise<ExaSearchResult[]> {
|
||||
try {
|
||||
return normalizeExaResults(JSON.parse(new TextDecoder().decode(bytes)));
|
||||
return normalizeExaResults(await response.json());
|
||||
} catch (cause) {
|
||||
throw new Error("Exa API returned malformed JSON", { cause });
|
||||
}
|
||||
|
||||
@@ -26,33 +26,6 @@ function cancelTrackedResponse(
|
||||
};
|
||||
}
|
||||
|
||||
function streamingJsonResponse(params: { chunkCount: number; chunkSize: number }): {
|
||||
response: Response;
|
||||
getReadCount: () => number;
|
||||
} {
|
||||
// Streaming fixture proves an oversized success body stops being read before
|
||||
// the whole payload is buffered into memory.
|
||||
let reads = 0;
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
pull(controller) {
|
||||
if (reads >= params.chunkCount) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
reads += 1;
|
||||
controller.enqueue(encoder.encode("a".repeat(params.chunkSize)));
|
||||
},
|
||||
});
|
||||
return {
|
||||
response: new Response(stream, {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
getReadCount: () => reads,
|
||||
};
|
||||
}
|
||||
|
||||
describe("exa web search provider", () => {
|
||||
it("exposes the expected metadata and selection wiring", () => {
|
||||
const provider = createExaWebSearchProvider();
|
||||
@@ -292,27 +265,6 @@ describe("exa web search provider", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("parses well-formed Exa search JSON under the byte cap", async () => {
|
||||
const response = new Response(
|
||||
JSON.stringify({ results: [{ url: "https://example.com", title: "Example" }] }),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
);
|
||||
|
||||
await expect(testing.readExaSearchResults(response)).resolves.toEqual([
|
||||
{ url: "https://example.com", title: "Example" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("caps oversized Exa search JSON instead of buffering the whole body", async () => {
|
||||
const streamed = streamingJsonResponse({ chunkCount: 64, chunkSize: 1024 });
|
||||
|
||||
await expect(
|
||||
testing.readExaSearchResults(streamed.response, { maxBytes: 4096 }),
|
||||
).rejects.toThrow(/Exa API response exceeds 4096 bytes/);
|
||||
|
||||
expect(streamed.getReadCount()).toBeLessThan(64);
|
||||
});
|
||||
|
||||
it("bounds Exa API error bodies without using response.text()", async () => {
|
||||
const tracked = cancelTrackedResponse(`${"exa upstream unavailable ".repeat(1024)}tail`, {
|
||||
status: 503,
|
||||
|
||||
@@ -43,7 +43,10 @@ export {
|
||||
filterSupplementalContextItems,
|
||||
resolveChannelContextVisibilityMode,
|
||||
} from "openclaw/plugin-sdk/context-visibility-runtime";
|
||||
export { getSessionEntry } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
export {
|
||||
loadSessionStore,
|
||||
resolveSessionStoreEntry,
|
||||
} from "openclaw/plugin-sdk/session-store-runtime";
|
||||
export { readJsonFileWithFallback } from "openclaw/plugin-sdk/json-store";
|
||||
export { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
||||
export { chunkTextForOutbound } from "openclaw/plugin-sdk/text-chunking";
|
||||
|
||||
@@ -10,4 +10,4 @@ export {
|
||||
filterSupplementalContextItems,
|
||||
normalizeAgentId,
|
||||
} from "../runtime-api.js";
|
||||
export { getSessionEntry } from "../runtime-api.js";
|
||||
export { loadSessionStore, resolveSessionStoreEntry } from "../runtime-api.js";
|
||||
|
||||
@@ -3,8 +3,8 @@ import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ClawdbotConfig } from "./bot-runtime-api.js";
|
||||
import { resolveFeishuReasoningPreviewEnabled } from "./reasoning-preview.js";
|
||||
|
||||
const { getSessionEntryMock } = vi.hoisted(() => ({
|
||||
getSessionEntryMock: vi.fn(),
|
||||
const { loadSessionStoreMock } = vi.hoisted(() => ({
|
||||
loadSessionStoreMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./bot-runtime-api.js", async () => {
|
||||
@@ -12,7 +12,7 @@ vi.mock("./bot-runtime-api.js", async () => {
|
||||
await vi.importActual<typeof import("./bot-runtime-api.js")>("./bot-runtime-api.js");
|
||||
return {
|
||||
...actual,
|
||||
getSessionEntry: getSessionEntryMock,
|
||||
loadSessionStore: loadSessionStoreMock,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -29,12 +29,9 @@ describe("resolveFeishuReasoningPreviewEnabled", () => {
|
||||
});
|
||||
|
||||
it("enables previews only for stream reasoning sessions", () => {
|
||||
getSessionEntryMock.mockImplementation(({ sessionKey }) => {
|
||||
const entries = {
|
||||
"agent:main:feishu:dm:ou_sender_1": { reasoningLevel: "stream" },
|
||||
"agent:main:feishu:dm:ou_sender_2": { reasoningLevel: "on" },
|
||||
};
|
||||
return entries[sessionKey as keyof typeof entries];
|
||||
loadSessionStoreMock.mockReturnValue({
|
||||
"agent:main:feishu:dm:ou_sender_1": { reasoningLevel: "stream" },
|
||||
"agent:main:feishu:dm:ou_sender_2": { reasoningLevel: "on" },
|
||||
});
|
||||
|
||||
expect(
|
||||
@@ -53,15 +50,10 @@ describe("resolveFeishuReasoningPreviewEnabled", () => {
|
||||
sessionKey: "agent:main:feishu:dm:ou_sender_2",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(getSessionEntryMock).toHaveBeenCalledWith({
|
||||
storePath: "/tmp/feishu-sessions.json",
|
||||
sessionKey: "agent:main:feishu:dm:ou_sender_1",
|
||||
readConsistency: "latest",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns false for missing sessions or load failures", () => {
|
||||
getSessionEntryMock.mockImplementationOnce(() => {
|
||||
loadSessionStoreMock.mockImplementationOnce(() => {
|
||||
throw new Error("disk unavailable");
|
||||
});
|
||||
|
||||
@@ -83,12 +75,9 @@ describe("resolveFeishuReasoningPreviewEnabled", () => {
|
||||
});
|
||||
|
||||
it("falls back to configured stream defaults", () => {
|
||||
getSessionEntryMock.mockImplementation(({ sessionKey }) => {
|
||||
const entries = {
|
||||
"agent:main:feishu:dm:ou_sender_1": {},
|
||||
"agent:main:feishu:dm:ou_sender_2": { reasoningLevel: "off" },
|
||||
};
|
||||
return entries[sessionKey as keyof typeof entries];
|
||||
loadSessionStoreMock.mockReturnValue({
|
||||
"agent:main:feishu:dm:ou_sender_1": {},
|
||||
"agent:main:feishu:dm:ou_sender_2": { reasoningLevel: "off" },
|
||||
});
|
||||
|
||||
const cfg: ClawdbotConfig = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Feishu plugin module implements reasoning preview behavior.
|
||||
import { resolveFeishuConfigReasoningDefault } from "./agent-config.js";
|
||||
import { getSessionEntry } from "./bot-runtime-api.js";
|
||||
import { loadSessionStore, resolveSessionStoreEntry } from "./bot-runtime-api.js";
|
||||
import type { ClawdbotConfig } from "./bot-runtime-api.js";
|
||||
|
||||
export function resolveFeishuReasoningPreviewEnabled(params: {
|
||||
@@ -16,11 +16,9 @@ export function resolveFeishuReasoningPreviewEnabled(params: {
|
||||
}
|
||||
|
||||
try {
|
||||
const level = getSessionEntry({
|
||||
storePath: params.storePath,
|
||||
sessionKey: params.sessionKey,
|
||||
readConsistency: "latest",
|
||||
})?.reasoningLevel;
|
||||
const store = loadSessionStore(params.storePath, { skipCache: true });
|
||||
const level = resolveSessionStoreEntry({ store, sessionKey: params.sessionKey }).existing
|
||||
?.reasoningLevel;
|
||||
if (level === "on" || level === "stream" || level === "off") {
|
||||
return level === "stream";
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Firecrawl plugin module implements firecrawl client behavior.
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
|
||||
import {
|
||||
DEFAULT_CACHE_TTL_MINUTES,
|
||||
markdownToText,
|
||||
@@ -42,7 +41,6 @@ const SCRAPE_CACHE = new Map<
|
||||
>();
|
||||
const DEFAULT_SEARCH_COUNT = 5;
|
||||
const DEFAULT_SCRAPE_MAX_CHARS = 50_000;
|
||||
const FIRECRAWL_SCRAPE_RESPONSE_MAX_BYTES = 64 * 1024 * 1024;
|
||||
const ALLOWED_FIRECRAWL_HOSTS = new Set(["api.firecrawl.dev"]);
|
||||
const FIRECRAWL_SELF_HOSTED_PRIVATE_ERROR =
|
||||
"Firecrawl custom baseUrl must target a private or internal self-hosted endpoint.";
|
||||
@@ -67,9 +65,12 @@ type FirecrawlSearchItem = {
|
||||
async function readFirecrawlJsonResponse(
|
||||
response: Response,
|
||||
label: string,
|
||||
opts?: { maxBytes?: number },
|
||||
): Promise<Record<string, unknown>> {
|
||||
return await readProviderJsonResponse<Record<string, unknown>>(response, label, opts);
|
||||
try {
|
||||
return (await response.json()) as Record<string, unknown>;
|
||||
} catch (cause) {
|
||||
throw new Error(`${label}: malformed JSON response`, { cause });
|
||||
}
|
||||
}
|
||||
|
||||
export type FirecrawlSearchParams = {
|
||||
@@ -219,9 +220,11 @@ async function postFirecrawlJson<T>(
|
||||
const readJsonPayload = async (): Promise<Record<string, unknown> | null> => {
|
||||
const candidate = response as Response & { clone?: () => Response };
|
||||
const jsonResponse = typeof candidate.clone === "function" ? candidate.clone() : response;
|
||||
if (typeof jsonResponse.json !== "function") {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const body = await readResponseText(jsonResponse, { maxBytes: 64_000 });
|
||||
const payload = JSON.parse(body.text) as unknown;
|
||||
const payload = await jsonResponse.json();
|
||||
return payload && typeof payload === "object" && !Array.isArray(payload)
|
||||
? (payload as Record<string, unknown>)
|
||||
: null;
|
||||
@@ -576,10 +579,7 @@ export async function runFirecrawlScrape(
|
||||
},
|
||||
},
|
||||
async (response) => {
|
||||
const payloadLocal = await readFirecrawlJsonResponse(response, "Firecrawl fetch failed", {
|
||||
// Scrape can legitimately return page bodies before maxChars truncates parsed output.
|
||||
maxBytes: FIRECRAWL_SCRAPE_RESPONSE_MAX_BYTES,
|
||||
});
|
||||
const payloadLocal = await readFirecrawlJsonResponse(response, "Firecrawl fetch failed");
|
||||
if (payloadLocal.success === false) {
|
||||
const detail =
|
||||
typeof payloadLocal.error === "string"
|
||||
@@ -613,7 +613,6 @@ export const testing = {
|
||||
assertFirecrawlScrapeTargetAllowed,
|
||||
parseFirecrawlScrapePayload,
|
||||
postFirecrawlJson,
|
||||
readFirecrawlJsonResponse,
|
||||
resolveEndpoint,
|
||||
validateFirecrawlBaseUrl,
|
||||
resolveSearchItems,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { mockPinnedHostnameResolution } from "openclaw/plugin-sdk/test-env";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createStreamingResponse } from "../../test-support/streaming-error-response.js";
|
||||
import {
|
||||
DEFAULT_FIRECRAWL_BASE_URL,
|
||||
DEFAULT_FIRECRAWL_MAX_AGE_MS,
|
||||
@@ -967,27 +966,6 @@ describe("firecrawl tools", () => {
|
||||
).rejects.toThrow("Firecrawl Search API error: malformed JSON response");
|
||||
});
|
||||
|
||||
it("bounds successful Firecrawl JSON bodies before parsing", async () => {
|
||||
const streamed = createStreamingResponse({
|
||||
chunkCount: 32,
|
||||
chunkSize: 1024 * 1024,
|
||||
text: "x",
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
const jsonSpy = vi.spyOn(streamed.response, "json").mockRejectedValue(new Error("unbounded"));
|
||||
|
||||
await expect(
|
||||
firecrawlClientTesting.readFirecrawlJsonResponse(
|
||||
streamed.response,
|
||||
"Firecrawl Search API error",
|
||||
),
|
||||
).rejects.toThrow("Firecrawl Search API error: JSON response exceeds 16777216 bytes");
|
||||
|
||||
expect(streamed.getReadCount()).toBeLessThan(32);
|
||||
expect(streamed.wasCanceled()).toBe(true);
|
||||
expect(jsonSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reports malformed Firecrawl scrape JSON with a stable provider error", async () => {
|
||||
global.fetch = vi.fn(
|
||||
async () =>
|
||||
|
||||
@@ -256,183 +256,6 @@ describe("iMessage monitor last-route updates", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps direct progress options when imsg lacks native typing support", async () => {
|
||||
setCachedIMessagePrivateApiStatus("imsg", {
|
||||
available: true,
|
||||
v2Ready: true,
|
||||
selectors: {},
|
||||
rpcMethods: ["watch.subscribe", "send", "read"],
|
||||
});
|
||||
dispatchInboundMessageMock.mockImplementationOnce(async (params) => {
|
||||
expect(params.replyOptions?.suppressDefaultToolProgressMessages).toBe(true);
|
||||
expect(params.replyOptions?.allowProgressCallbacksWhenSourceDeliverySuppressed).toBe(true);
|
||||
expect(params.replyOptions?.onToolStart).toBeUndefined();
|
||||
return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } } as const;
|
||||
});
|
||||
|
||||
let onNotification: ((message: { method: string; params: unknown }) => void) | undefined;
|
||||
const client = {
|
||||
request: vi.fn(async (method: string) => {
|
||||
if (method === "watch.subscribe") {
|
||||
return { subscription: 1 };
|
||||
}
|
||||
if (method === "typing") {
|
||||
throw new Error("typing should not start without native typing support");
|
||||
}
|
||||
throw new Error(`unexpected imsg method ${method}`);
|
||||
}),
|
||||
waitForClose: vi.fn(async () => {
|
||||
onNotification?.({
|
||||
method: "message",
|
||||
params: {
|
||||
message: {
|
||||
id: 13,
|
||||
chat_id: 123,
|
||||
sender: "+15550001111",
|
||||
is_from_me: false,
|
||||
text: "run a long script without native typing",
|
||||
is_group: false,
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
}),
|
||||
stop: vi.fn(async () => {}),
|
||||
};
|
||||
createIMessageRpcClientMock.mockImplementation(async (params) => {
|
||||
if (!params?.onNotification) {
|
||||
throw new Error("expected iMessage notification handler");
|
||||
}
|
||||
onNotification = params.onNotification;
|
||||
return client as never;
|
||||
});
|
||||
|
||||
await monitorIMessageProvider({
|
||||
config: {
|
||||
channels: {
|
||||
imessage: {
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["+15550001111"],
|
||||
sendReadReceipts: false,
|
||||
},
|
||||
},
|
||||
messages: { inbound: { debounceMs: 0 } },
|
||||
session: { mainKey: "main" },
|
||||
} as never,
|
||||
runtime: { error: vi.fn(), exit: vi.fn(), log: vi.fn() },
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(dispatchInboundMessageMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(client.request).not.toHaveBeenCalledWith(
|
||||
"typing",
|
||||
expect.objectContaining({ typing: true }),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it("starts direct typing before dispatching the inbound turn", async () => {
|
||||
setCachedIMessagePrivateApiStatus("imsg", {
|
||||
available: true,
|
||||
v2Ready: true,
|
||||
selectors: {},
|
||||
rpcMethods: ["watch.subscribe", "send", "typing"],
|
||||
});
|
||||
|
||||
let onNotification: ((message: { method: string; params: unknown }) => void) | undefined;
|
||||
const earlyTypingClient = {
|
||||
request: vi.fn(async (method: string) => {
|
||||
if (method === "typing") {
|
||||
return { ok: true };
|
||||
}
|
||||
throw new Error(`unexpected imsg typing-client method ${method}`);
|
||||
}),
|
||||
stop: vi.fn(async () => {}),
|
||||
};
|
||||
const watchClient = {
|
||||
request: vi.fn(async (method: string) => {
|
||||
if (method === "watch.subscribe") {
|
||||
return { subscription: 1 };
|
||||
}
|
||||
if (method === "typing") {
|
||||
return { ok: true };
|
||||
}
|
||||
throw new Error(`unexpected imsg watch-client method ${method}`);
|
||||
}),
|
||||
waitForClose: vi.fn(async () => {
|
||||
onNotification?.({
|
||||
method: "message",
|
||||
params: {
|
||||
message: {
|
||||
id: 12,
|
||||
chat_id: 123,
|
||||
sender: "+15550001111",
|
||||
is_from_me: false,
|
||||
text: "respond after a slow context build",
|
||||
is_group: false,
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
expect(earlyTypingClient.request).toHaveBeenCalledWith(
|
||||
"typing",
|
||||
expect.objectContaining({ typing: true, to: "+15550001111" }),
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(dispatchInboundMessageMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
}),
|
||||
stop: vi.fn(async () => {}),
|
||||
};
|
||||
createIMessageRpcClientMock.mockImplementation(async (params) => {
|
||||
if (params?.onNotification) {
|
||||
onNotification = params.onNotification;
|
||||
return watchClient as never;
|
||||
}
|
||||
return earlyTypingClient as never;
|
||||
});
|
||||
dispatchInboundMessageMock.mockImplementationOnce(async () => {
|
||||
expect(earlyTypingClient.request).toHaveBeenCalledWith(
|
||||
"typing",
|
||||
expect.objectContaining({ typing: true, to: "+15550001111" }),
|
||||
expect.any(Object),
|
||||
);
|
||||
return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } } as const;
|
||||
});
|
||||
|
||||
await monitorIMessageProvider({
|
||||
config: {
|
||||
channels: {
|
||||
imessage: {
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["+15550001111"],
|
||||
sendReadReceipts: false,
|
||||
},
|
||||
},
|
||||
messages: { inbound: { debounceMs: 0 } },
|
||||
session: { mainKey: "main" },
|
||||
} as never,
|
||||
runtime: { error: vi.fn(), exit: vi.fn(), log: vi.fn() },
|
||||
});
|
||||
|
||||
expect(watchClient.request).not.toHaveBeenCalledWith(
|
||||
"typing",
|
||||
expect.objectContaining({ typing: true }),
|
||||
expect.anything(),
|
||||
);
|
||||
await vi.waitFor(() => {
|
||||
expect(earlyTypingClient.request).toHaveBeenCalledWith(
|
||||
"typing",
|
||||
expect.objectContaining({ typing: false, to: "+15550001111" }),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["never", "message", "thinking"] as const)(
|
||||
"does not start direct tool typing when typingMode is %s",
|
||||
async (typingMode) => {
|
||||
@@ -597,87 +420,6 @@ describe("iMessage monitor last-route updates", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not wait for read receipts before dispatching the inbound turn", async () => {
|
||||
setCachedIMessagePrivateApiStatus("imsg", {
|
||||
available: true,
|
||||
v2Ready: true,
|
||||
selectors: {},
|
||||
rpcMethods: ["watch.subscribe", "read"],
|
||||
});
|
||||
|
||||
let onNotification: ((message: { method: string; params: unknown }) => void) | undefined;
|
||||
const readClient = {
|
||||
request: vi.fn((method: string) => {
|
||||
if (method === "read") {
|
||||
return new Promise(() => {});
|
||||
}
|
||||
return Promise.reject(new Error(`unexpected imsg read-client method ${method}`));
|
||||
}),
|
||||
stop: vi.fn(async () => {}),
|
||||
};
|
||||
const watchClient = {
|
||||
request: vi.fn((method: string) => {
|
||||
if (method === "watch.subscribe") {
|
||||
return Promise.resolve({ subscription: 1 });
|
||||
}
|
||||
return Promise.reject(new Error(`unexpected imsg watch-client method ${method}`));
|
||||
}),
|
||||
waitForClose: vi.fn(async () => {
|
||||
onNotification?.({
|
||||
method: "message",
|
||||
params: {
|
||||
message: {
|
||||
id: 11,
|
||||
chat_id: 123,
|
||||
sender: "+15550001111",
|
||||
is_from_me: false,
|
||||
text: "respond without waiting for read receipt",
|
||||
is_group: false,
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
expect(dispatchInboundMessageMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
}),
|
||||
stop: vi.fn(async () => {}),
|
||||
};
|
||||
createIMessageRpcClientMock.mockImplementation(async (params) => {
|
||||
if (params?.onNotification) {
|
||||
onNotification = params.onNotification;
|
||||
return watchClient as never;
|
||||
}
|
||||
return readClient as never;
|
||||
});
|
||||
|
||||
await monitorIMessageProvider({
|
||||
config: {
|
||||
channels: {
|
||||
imessage: {
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["+15550001111"],
|
||||
},
|
||||
},
|
||||
messages: { inbound: { debounceMs: 0 } },
|
||||
session: { mainKey: "main" },
|
||||
} as never,
|
||||
runtime: { error: vi.fn(), exit: vi.fn(), log: vi.fn() },
|
||||
});
|
||||
|
||||
expect(readClient.request).toHaveBeenCalledWith(
|
||||
"read",
|
||||
expect.objectContaining({ to: "+15550001111" }),
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(watchClient.request).not.toHaveBeenCalledWith(
|
||||
"read",
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
);
|
||||
expect(dispatchInboundMessageMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: "flat true",
|
||||
|
||||
@@ -1087,7 +1087,7 @@ function buildIMessageEchoScope(params: {
|
||||
return scopes;
|
||||
}
|
||||
|
||||
export function buildDirectIMessageReplyTarget(params: {
|
||||
function buildDirectIMessageReplyTarget(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
sender: string;
|
||||
|
||||
@@ -94,7 +94,6 @@ import {
|
||||
releaseIMessageInboundReplay,
|
||||
} from "./inbound-dedupe.js";
|
||||
import {
|
||||
buildDirectIMessageReplyTarget,
|
||||
buildIMessageInboundContext,
|
||||
rememberIMessageSkippedFromMeForSelfChatDedupe,
|
||||
resolveIMessageReactionContext,
|
||||
@@ -1040,87 +1039,6 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
||||
const storePath = resolveStorePath(cfg.session?.store, {
|
||||
agentId: decision.route.agentId,
|
||||
});
|
||||
const privateApiStatus = getCachedIMessagePrivateApiStatus(cliPath);
|
||||
const supportsTyping = imessageRpcSupportsMethod(privateApiStatus, "typing");
|
||||
const supportsRead = imessageRpcSupportsMethod(privateApiStatus, "read");
|
||||
if (privateApiStatus?.available === true) {
|
||||
// Surface a single warning per restart when the bridge is up but we
|
||||
// had to gate off typing/read because the imsg build pre-dates the
|
||||
// capability list. Otherwise the user sees no typing bubble / no
|
||||
// "Read" receipt with no visible reason.
|
||||
if (!supportsTyping || !supportsRead) {
|
||||
warnIfImsgUpgradeNeeded.fireOnce(privateApiStatus.rpcMethods, runtime);
|
||||
}
|
||||
}
|
||||
const configuredTypingMode = resolveConfiguredIMessageTypingMode(cfg);
|
||||
const sendPolicy = resolveSendPolicy({
|
||||
cfg,
|
||||
entry: getSessionEntry({ storePath, sessionKey: decision.route.sessionKey }),
|
||||
sessionKey: decision.route.sessionKey,
|
||||
channel: "imessage",
|
||||
chatType: decision.isGroup ? "group" : "direct",
|
||||
});
|
||||
const shouldUseDirectToolTypingOptions =
|
||||
!decision.isGroup &&
|
||||
sendPolicy !== "deny" &&
|
||||
(configuredTypingMode === undefined || configuredTypingMode === "instant");
|
||||
const shouldStartDirectTyping = supportsTyping && shouldUseDirectToolTypingOptions;
|
||||
const earlyDirectTypingTarget = shouldStartDirectTyping
|
||||
? buildDirectIMessageReplyTarget({
|
||||
cfg,
|
||||
accountId: decision.route.accountId,
|
||||
sender: decision.sender,
|
||||
})
|
||||
: undefined;
|
||||
let stopEarlyDirectTyping: (() => void) | undefined;
|
||||
if (earlyDirectTypingTarget) {
|
||||
// Start channel-native feedback before the expensive history/context/model
|
||||
// path. Use a short-lived client so a slow typing RPC cannot block the
|
||||
// monitor client's watch stream. Stop is sequenced after start so fast
|
||||
// command replies cannot leave a late true after typing:false.
|
||||
const earlyDirectTypingStarted = sendIMessageTyping(earlyDirectTypingTarget, true, {
|
||||
cfg,
|
||||
accountId: accountInfo.accountId,
|
||||
}).then(
|
||||
() => true,
|
||||
(err: unknown) => {
|
||||
logTypingFailure({
|
||||
log: (msg) => logVerbose(msg),
|
||||
channel: "imessage",
|
||||
action: "start",
|
||||
target: earlyDirectTypingTarget,
|
||||
error: err,
|
||||
});
|
||||
return false;
|
||||
},
|
||||
);
|
||||
let earlyTypingStopQueued = false;
|
||||
stopEarlyDirectTyping = () => {
|
||||
if (earlyTypingStopQueued) {
|
||||
return;
|
||||
}
|
||||
earlyTypingStopQueued = true;
|
||||
void earlyDirectTypingStarted
|
||||
.then(async (started) => {
|
||||
if (!started) {
|
||||
return;
|
||||
}
|
||||
await sendIMessageTyping(earlyDirectTypingTarget, false, {
|
||||
cfg,
|
||||
accountId: accountInfo.accountId,
|
||||
});
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
logTypingFailure({
|
||||
log: (msg) => logVerbose(msg),
|
||||
channel: "imessage",
|
||||
action: "stop",
|
||||
target: earlyDirectTypingTarget,
|
||||
error: err,
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
const stagedAttachments = remoteHost
|
||||
? []
|
||||
: await stageIMessageAttachments(validAttachments, {
|
||||
@@ -1189,20 +1107,31 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
||||
);
|
||||
}
|
||||
|
||||
const privateApiStatus = getCachedIMessagePrivateApiStatus(cliPath);
|
||||
const supportsTyping = imessageRpcSupportsMethod(privateApiStatus, "typing");
|
||||
const supportsRead = imessageRpcSupportsMethod(privateApiStatus, "read");
|
||||
if (privateApiStatus?.available === true) {
|
||||
// Surface a single warning per restart when the bridge is up but we
|
||||
// had to gate off typing/read because the imsg build pre-dates the
|
||||
// capability list. Otherwise the user sees no typing bubble / no
|
||||
// "Read" receipt with no visible reason.
|
||||
if (!supportsTyping || !supportsRead) {
|
||||
warnIfImsgUpgradeNeeded.fireOnce(privateApiStatus.rpcMethods, runtime);
|
||||
}
|
||||
}
|
||||
const sendReadReceipts = imessageCfg.sendReadReceipts !== false;
|
||||
const typingTarget = ctxPayload.To;
|
||||
|
||||
if (supportsRead && sendReadReceipts && typingTarget) {
|
||||
// Read receipts are best-effort channel UI. Do not put them on the
|
||||
// critical path before model dispatch; slow private-API reads otherwise
|
||||
// make accepted iMessage turns feel stuck before the agent starts. Use
|
||||
// a short-lived client so a stuck read cannot block monitor-client typing.
|
||||
void markIMessageChatRead(typingTarget, {
|
||||
cfg,
|
||||
accountId: accountInfo.accountId,
|
||||
}).catch((err: unknown) => {
|
||||
try {
|
||||
await markIMessageChatRead(typingTarget, {
|
||||
cfg,
|
||||
accountId: accountInfo.accountId,
|
||||
client: getActiveClient(),
|
||||
});
|
||||
} catch (err) {
|
||||
runtime.error?.(`imessage: mark read failed: ${String(err)}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const { onModelSelected, ...replyPipeline } = createChannelMessageReplyPipeline({
|
||||
@@ -1305,27 +1234,35 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
||||
},
|
||||
});
|
||||
let directTypingController: IMessageTypingController | undefined;
|
||||
const directToolTypingOptions = shouldUseDirectToolTypingOptions
|
||||
const configuredTypingMode = resolveConfiguredIMessageTypingMode(cfg);
|
||||
const sendPolicy = resolveSendPolicy({
|
||||
cfg,
|
||||
entry: getSessionEntry({ storePath, sessionKey: decision.route.sessionKey }),
|
||||
sessionKey: decision.route.sessionKey,
|
||||
channel: "imessage",
|
||||
chatType: decision.isGroup ? "group" : "direct",
|
||||
});
|
||||
const shouldStartToolTyping =
|
||||
!decision.isGroup &&
|
||||
sendPolicy !== "deny" &&
|
||||
(configuredTypingMode === undefined || configuredTypingMode === "instant");
|
||||
const directToolTypingOptions = shouldStartToolTyping
|
||||
? ({
|
||||
// iMessage's native typing bubble is channel-owned UI, not a
|
||||
// visible tool-progress message. The suppress flag is what lets
|
||||
// dispatch forward this callback even when verbose progress is off;
|
||||
// allowProgress covers message_tool_only source delivery. Keep this on
|
||||
// the direct instant/default path even when older imsg builds do not
|
||||
// report native typing support.
|
||||
// the direct instant/default path so configured typingMode values still
|
||||
// decide when typing can begin.
|
||||
suppressDefaultToolProgressMessages: true,
|
||||
allowProgressCallbacksWhenSourceDeliverySuppressed: true,
|
||||
onTypingController: (typing: IMessageTypingController) => {
|
||||
directTypingController = typing;
|
||||
typingReplyOptions.onTypingController?.(typing);
|
||||
},
|
||||
...(supportsTyping
|
||||
? {
|
||||
onToolStart: async () => {
|
||||
await directTypingController?.startTypingLoop();
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
onToolStart: async () => {
|
||||
await directTypingController?.startTypingLoop();
|
||||
},
|
||||
} as const)
|
||||
: {};
|
||||
const configuredBlockStreaming = resolveChannelStreamingBlockEnabled(accountInfo.config);
|
||||
@@ -1388,13 +1325,11 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
||||
historyMap: groupHistories,
|
||||
limit: historyLimit,
|
||||
},
|
||||
onPreDispatchFailure: () => {
|
||||
stopEarlyDirectTyping?.();
|
||||
void settleReplyDispatcher({
|
||||
onPreDispatchFailure: () =>
|
||||
settleReplyDispatcher({
|
||||
dispatcher,
|
||||
onSettled: () => markDispatchIdle(),
|
||||
});
|
||||
},
|
||||
}),
|
||||
runDispatch: async () => {
|
||||
try {
|
||||
return await dispatchInboundMessage({
|
||||
@@ -1413,7 +1348,6 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
||||
});
|
||||
} finally {
|
||||
markDispatchIdle();
|
||||
stopEarlyDirectTyping?.();
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -49,15 +49,6 @@ describe("sanitizeOutboundText", () => {
|
||||
expect(result).not.toMatch(/^assistant:$/m);
|
||||
});
|
||||
|
||||
it("preserves prose lines that merely end with 'user:'/'system:'", () => {
|
||||
expect(sanitizeOutboundText("Please send this reply to the user:")).toBe(
|
||||
"Please send this reply to the user:",
|
||||
);
|
||||
expect(sanitizeOutboundText("Here is a note for the system:")).toBe(
|
||||
"Here is a note for the system:",
|
||||
);
|
||||
});
|
||||
|
||||
it("collapses excessive blank lines after stripping", () => {
|
||||
const text = "Hello\n\n\n\n\nWorld";
|
||||
expect(sanitizeOutboundText(text)).toBe("Hello\n\nWorld");
|
||||
|
||||
@@ -7,9 +7,7 @@ import { stripAssistantInternalScaffolding } from "openclaw/plugin-sdk/text-chun
|
||||
*/
|
||||
const INTERNAL_SEPARATOR_RE = /(?:#\+){2,}#?/g;
|
||||
const ASSISTANT_ROLE_MARKER_RE = /\bassistant\s+to\s*=\s*\w+/gi;
|
||||
// Only a standalone role marker on its own line (a leaked turn boundary) — not
|
||||
// any line that merely ends with the word "user/system/assistant:" in prose.
|
||||
const ROLE_TURN_MARKER_RE = /^[ \t]*(?:user|system|assistant)\s*:\s*$/gm;
|
||||
const ROLE_TURN_MARKER_RE = /\b(?:user|system|assistant)\s*:\s*$/gm;
|
||||
|
||||
/**
|
||||
* Strip all assistant-internal scaffolding from outbound text before delivery.
|
||||
|
||||
@@ -40,7 +40,10 @@ import {
|
||||
import type { GetReplyOptions } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
|
||||
import { resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime";
|
||||
import { getSessionEntry } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import {
|
||||
loadSessionStore,
|
||||
resolveSessionStoreEntry,
|
||||
} from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type {
|
||||
CoreConfig,
|
||||
@@ -344,11 +347,12 @@ function resolveMatrixSharedDmContextNotice(params: {
|
||||
}
|
||||
|
||||
try {
|
||||
const store = loadSessionStore(params.storePath);
|
||||
const currentSession = resolveMatrixStoredSessionMeta(
|
||||
getSessionEntry({
|
||||
storePath: params.storePath,
|
||||
resolveSessionStoreEntry({
|
||||
store,
|
||||
sessionKey: params.sessionKey,
|
||||
}),
|
||||
}).existing,
|
||||
);
|
||||
if (!currentSession) {
|
||||
return null;
|
||||
|
||||
@@ -6,7 +6,11 @@ import {
|
||||
type ChannelOutboundSessionRouteParams,
|
||||
} from "openclaw/plugin-sdk/channel-core";
|
||||
import { parseThreadSessionSuffix } from "openclaw/plugin-sdk/routing";
|
||||
import { getSessionEntry, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import {
|
||||
loadSessionStore,
|
||||
resolveSessionStoreEntry,
|
||||
resolveStorePath,
|
||||
} from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { resolveMatrixAccountConfig } from "./matrix/account-config.js";
|
||||
import { resolveDefaultMatrixAccountId } from "./matrix/accounts.js";
|
||||
import { resolveMatrixStoredSessionMeta } from "./matrix/session-store-metadata.js";
|
||||
@@ -47,10 +51,11 @@ function resolveMatrixCurrentDmRoomId(params: {
|
||||
const storePath = resolveStorePath(params.cfg.session?.store, {
|
||||
agentId: params.agentId,
|
||||
});
|
||||
const existing = getSessionEntry({
|
||||
storePath,
|
||||
const store = loadSessionStore(storePath);
|
||||
const existing = resolveSessionStoreEntry({
|
||||
store,
|
||||
sessionKey,
|
||||
});
|
||||
}).existing;
|
||||
const currentSession = resolveMatrixStoredSessionMeta(existing);
|
||||
if (!currentSession) {
|
||||
return undefined;
|
||||
|
||||
@@ -46,7 +46,7 @@ export {
|
||||
warnMissingProviderGroupPolicyFallbackOnce,
|
||||
} from "openclaw/plugin-sdk/runtime-group-policy";
|
||||
export { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
|
||||
export { resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
export { loadSessionStore, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
export { formatInboundFromLabel } from "openclaw/plugin-sdk/channel-inbound";
|
||||
export { logInboundDrop } from "openclaw/plugin-sdk/channel-inbound";
|
||||
export { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
|
||||
|
||||
@@ -214,66 +214,4 @@ describe("Mattermost model picker", () => {
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves current and parent model overrides from targeted session entries", () => {
|
||||
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "mm-model-picker-"));
|
||||
try {
|
||||
const storePath = path.join(testDir, "{agentId}.json");
|
||||
const supportStorePath = path.join(testDir, "support.json");
|
||||
const parentSessionKey = "agent:support:mattermost:default:channel-1";
|
||||
const childSessionKey = "agent:support:mattermost:default:child-with-explicit-parent";
|
||||
const directSessionKey = "agent:support:mattermost:default:direct-1";
|
||||
fs.writeFileSync(
|
||||
supportStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
[parentSessionKey]: {
|
||||
providerOverride: "anthropic",
|
||||
modelOverride: "claude-sonnet-4-5",
|
||||
sessionId: "parent-session",
|
||||
},
|
||||
[childSessionKey]: {
|
||||
parentSessionKey,
|
||||
sessionId: "child-session",
|
||||
},
|
||||
[directSessionKey]: {
|
||||
providerOverride: "openai",
|
||||
modelOverride: "gpt-5",
|
||||
sessionId: "direct-session",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
const cfg: OpenClawConfig = {
|
||||
session: {
|
||||
store: storePath,
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
resolveMattermostModelPickerCurrentModel({
|
||||
cfg,
|
||||
route: {
|
||||
agentId: "support",
|
||||
sessionKey: directSessionKey,
|
||||
},
|
||||
data,
|
||||
}),
|
||||
).toBe("openai/gpt-5");
|
||||
expect(
|
||||
resolveMattermostModelPickerCurrentModel({
|
||||
cfg,
|
||||
route: {
|
||||
agentId: "support",
|
||||
sessionKey: childSessionKey,
|
||||
},
|
||||
data,
|
||||
}),
|
||||
).toBe("anthropic/claude-sonnet-4-5");
|
||||
} finally {
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
||||
import { parseStrictInteger } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { getSessionEntry, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { loadSessionStore, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import {
|
||||
normalizeOptionalString,
|
||||
normalizeStringifiedOptionalString,
|
||||
@@ -237,28 +237,21 @@ export function resolveMattermostModelPickerCurrentModel(params: {
|
||||
cfg: OpenClawConfig;
|
||||
route: { agentId: string; sessionKey: string };
|
||||
data: ModelsProviderData;
|
||||
readConsistency?: "latest";
|
||||
skipCache?: boolean;
|
||||
}): string {
|
||||
const fallback = `${params.data.resolvedDefault.provider}/${params.data.resolvedDefault.model}`;
|
||||
try {
|
||||
const storePath = resolveStorePath(params.cfg.session?.store, {
|
||||
agentId: params.route.agentId,
|
||||
});
|
||||
const sessionEntry = getSessionEntry({
|
||||
storePath,
|
||||
sessionKey: params.route.sessionKey,
|
||||
...(params.readConsistency === "latest" ? { readConsistency: "latest" as const } : {}),
|
||||
});
|
||||
const sessionStore = params.skipCache
|
||||
? loadSessionStore(storePath, { skipCache: true })
|
||||
: loadSessionStore(storePath);
|
||||
const sessionEntry = sessionStore[params.route.sessionKey];
|
||||
const override = resolveStoredModelOverride({
|
||||
sessionEntry,
|
||||
loadSessionEntry: (sessionKey) =>
|
||||
getSessionEntry({
|
||||
storePath,
|
||||
sessionKey,
|
||||
...(params.readConsistency === "latest" ? { readConsistency: "latest" as const } : {}),
|
||||
}),
|
||||
sessionStore,
|
||||
sessionKey: params.route.sessionKey,
|
||||
parentSessionKey: sessionEntry?.parentSessionKey,
|
||||
defaultProvider: params.data.resolvedDefault.provider,
|
||||
});
|
||||
if (!override?.model) {
|
||||
|
||||
@@ -1256,7 +1256,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
cfg,
|
||||
route: modelSessionRoute,
|
||||
data,
|
||||
readConsistency: "latest",
|
||||
skipCache: true,
|
||||
});
|
||||
const view = renderMattermostModelsPickerView({
|
||||
ownerUserId: pickerState.ownerUserId,
|
||||
|
||||
@@ -36,6 +36,7 @@ export {
|
||||
isTrustedProxyAddress,
|
||||
listSkillCommandsForAgents,
|
||||
loadOutboundMediaFromUrl,
|
||||
loadSessionStore,
|
||||
logInboundDrop,
|
||||
logTypingFailure,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
|
||||
@@ -29,30 +29,6 @@ describe("concept vocabulary", () => {
|
||||
expect(tags).not.toContain("2026-04-04.md");
|
||||
});
|
||||
|
||||
it("preserves short protected-glossary terms past the latin minimum-length gate", () => {
|
||||
const tags = deriveConceptTags({
|
||||
path: "memory/2026-04-04.md",
|
||||
snippet: "Store the session in kv and back up to s3 nightly.",
|
||||
});
|
||||
|
||||
// "kv" and "s3" are 2-char latin glossary entries that the generic min-length-3 gate would drop.
|
||||
expect(tags).toContain("kv");
|
||||
expect(tags).toContain("s3");
|
||||
});
|
||||
|
||||
it("does not surface short glossary terms that only appear inside longer words", () => {
|
||||
const tags = deriveConceptTags({
|
||||
path: "memory/2026-04-04.md",
|
||||
snippet: "Played the mkv recording and tuned the css3 layout.",
|
||||
});
|
||||
|
||||
// "kv"/"s3" are substrings of "mkv"/"css3"; whole-word matching must not emit them as tags.
|
||||
expect(tags).not.toContain("kv");
|
||||
expect(tags).not.toContain("s3");
|
||||
expect(tags).toContain("mkv");
|
||||
expect(tags).toContain("css3");
|
||||
});
|
||||
|
||||
it("extracts protected and segmented CJK concept tags", () => {
|
||||
const tags = deriveConceptTags({
|
||||
path: "memory/2026-04-04.md",
|
||||
|
||||
@@ -330,7 +330,7 @@ function isKanaOnlyToken(value: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeConceptToken(rawToken: string, fromGlossary = false): string | null {
|
||||
function normalizeConceptToken(rawToken: string): string | null {
|
||||
const normalized = normalizeLowercaseStringOrEmpty(
|
||||
rawToken
|
||||
.normalize("NFKC")
|
||||
@@ -348,9 +348,7 @@ function normalizeConceptToken(rawToken: string, fromGlossary = false): string |
|
||||
return null;
|
||||
}
|
||||
const script = classifyConceptTagScript(normalized);
|
||||
// Glossary entries are an explicit allowlist of short technical terms (e.g. "kv", "s3"); they
|
||||
// bypass the per-script minimum length that would otherwise discard them.
|
||||
if (!fromGlossary && normalized.length < minimumTokenLengthForScript(script)) {
|
||||
if (normalized.length < minimumTokenLengthForScript(script)) {
|
||||
return null;
|
||||
}
|
||||
if (isKanaOnlyToken(normalized) && normalized.length < 3) {
|
||||
@@ -362,43 +360,14 @@ function normalizeConceptToken(rawToken: string, fromGlossary = false): string |
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// Only entries shorter than their script's minimum token length rely on the glossary bypass, and
|
||||
// only those need whole-word matching so they don't fire inside longer words ("kv" in "mkv"). Longer
|
||||
// entries keep substring containment (the shipped behavior, e.g. "backup" tagging inside "backups").
|
||||
// Precomputed so derive() does not reclassify on every call.
|
||||
const GLOSSARY_ENTRIES = PROTECTED_GLOSSARY.map((entry) => ({
|
||||
entry,
|
||||
wholeWord: entry.length < minimumTokenLengthForScript(classifyConceptTagScript(entry)),
|
||||
}));
|
||||
|
||||
function isAlphanumericAt(source: string, index: number): boolean {
|
||||
const ch = source[index];
|
||||
return ch !== undefined && LETTER_OR_NUMBER_RE.test(ch);
|
||||
}
|
||||
|
||||
// True when `entry` occurs as a delimiter-bounded token, not inside a longer word. Keeps short
|
||||
// glossary entries like "kv"/"s3" from firing inside "mkv"/"css3" once they bypass the length gate.
|
||||
function includesStandaloneTerm(source: string, entry: string): boolean {
|
||||
let from = source.indexOf(entry);
|
||||
while (from !== -1) {
|
||||
if (!isAlphanumericAt(source, from - 1) && !isAlphanumericAt(source, from + entry.length)) {
|
||||
return true;
|
||||
}
|
||||
from = source.indexOf(entry, from + 1);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function collectGlossaryMatches(source: string): string[] {
|
||||
const normalizedSource = normalizeLowercaseStringOrEmpty(source.normalize("NFKC"));
|
||||
const matches: string[] = [];
|
||||
for (const { entry, wholeWord } of GLOSSARY_ENTRIES) {
|
||||
const present = wholeWord
|
||||
? includesStandaloneTerm(normalizedSource, entry)
|
||||
: normalizedSource.includes(entry);
|
||||
if (present) {
|
||||
matches.push(entry);
|
||||
for (const entry of PROTECTED_GLOSSARY) {
|
||||
if (!normalizedSource.includes(entry)) {
|
||||
continue;
|
||||
}
|
||||
matches.push(entry);
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
@@ -416,13 +385,8 @@ function collectSegmentTokens(source: string): string[] {
|
||||
return source.split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
||||
}
|
||||
|
||||
function pushNormalizedTag(
|
||||
tags: string[],
|
||||
rawToken: string,
|
||||
limit: number,
|
||||
fromGlossary = false,
|
||||
): void {
|
||||
const normalized = normalizeConceptToken(rawToken, fromGlossary);
|
||||
function pushNormalizedTag(tags: string[], rawToken: string, limit: number): void {
|
||||
const normalized = normalizeConceptToken(rawToken);
|
||||
if (!normalized || tags.includes(normalized)) {
|
||||
return;
|
||||
}
|
||||
@@ -446,17 +410,14 @@ export function deriveConceptTags(params: {
|
||||
}
|
||||
|
||||
const tags: string[] = [];
|
||||
const tokenSources: Array<{ tokens: string[]; fromGlossary: boolean }> = [
|
||||
{ tokens: collectGlossaryMatches(source), fromGlossary: true },
|
||||
{ tokens: collectCompoundTokens(source), fromGlossary: false },
|
||||
{ tokens: collectSegmentTokens(source), fromGlossary: false },
|
||||
];
|
||||
for (const { tokens, fromGlossary } of tokenSources) {
|
||||
for (const rawToken of tokens) {
|
||||
pushNormalizedTag(tags, rawToken, limit, fromGlossary);
|
||||
if (tags.length >= limit) {
|
||||
return tags;
|
||||
}
|
||||
for (const rawToken of [
|
||||
...collectGlossaryMatches(source),
|
||||
...collectCompoundTokens(source),
|
||||
...collectSegmentTokens(source),
|
||||
]) {
|
||||
pushNormalizedTag(tags, rawToken, limit);
|
||||
if (tags.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return tags;
|
||||
|
||||
@@ -1987,78 +1987,6 @@ describe("memory-core dreaming phases", () => {
|
||||
expect(newOccurrences).toBe(1);
|
||||
});
|
||||
|
||||
it("skips reset/deleted archive artifacts without active transcripts during session ingestion", async () => {
|
||||
const workspaceDir = await createDreamingWorkspace();
|
||||
vi.stubEnv("OPENCLAW_TEST_FAST", "1");
|
||||
vi.stubEnv("OPENCLAW_STATE_DIR", path.join(workspaceDir, ".state"));
|
||||
const sessionsDir = resolveSessionTranscriptsDirForAgent("main");
|
||||
await fs.mkdir(sessionsDir, { recursive: true });
|
||||
const archivePath = path.join(
|
||||
sessionsDir,
|
||||
"archived-only.jsonl.deleted.2026-04-06T01-00-00.000Z",
|
||||
);
|
||||
await fs.writeFile(
|
||||
archivePath,
|
||||
[
|
||||
JSON.stringify({
|
||||
type: "message",
|
||||
message: {
|
||||
role: "user",
|
||||
timestamp: "2026-04-05T18:01:00.000Z",
|
||||
content: [{ type: "text", text: "Archived session should not be dreamed." }],
|
||||
},
|
||||
}),
|
||||
].join("\n") + "\n",
|
||||
"utf-8",
|
||||
);
|
||||
const mtime = new Date("2026-04-06T01:05:00.000Z");
|
||||
await fs.utimes(archivePath, mtime, mtime);
|
||||
|
||||
const { beforeAgentReply } = createHarness(
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: workspaceDir,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
"memory-core": {
|
||||
config: {
|
||||
dreaming: {
|
||||
enabled: true,
|
||||
phases: {
|
||||
light: {
|
||||
enabled: true,
|
||||
limit: 20,
|
||||
lookbackDays: 7,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
workspaceDir,
|
||||
);
|
||||
|
||||
try {
|
||||
await withDreamingTestClock(async () => {
|
||||
await triggerLightDreaming(beforeAgentReply, workspaceDir, 5);
|
||||
});
|
||||
} finally {
|
||||
vi.unstubAllEnvs();
|
||||
}
|
||||
|
||||
await expectPathMissing(
|
||||
path.join(workspaceDir, "memory", ".dreams", "session-corpus", "2026-04-05.txt"),
|
||||
);
|
||||
|
||||
const sessionIngestion = await testing.readSessionIngestionState(workspaceDir);
|
||||
expect(Object.keys(sessionIngestion.files)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("buckets session snippets by per-message day rather than file mtime", async () => {
|
||||
const workspaceDir = await createDreamingWorkspace();
|
||||
vi.stubEnv("OPENCLAW_TEST_FAST", "1");
|
||||
|
||||
@@ -848,12 +848,7 @@ async function collectSessionIngestionBatches(params: {
|
||||
for (const agentId of agentIds) {
|
||||
for (const entry of await listSessionTranscriptCorpusEntriesForAgent(agentId)) {
|
||||
const absolutePath = entry.sessionFile;
|
||||
if (
|
||||
// Dreaming learns only from the live corpus. Retained reset/delete
|
||||
// archives stay in the shared corpus for QMD and memory_search.
|
||||
entry.artifactKind === "archive-artifact" ||
|
||||
isCheckpointSessionTranscriptPath(absolutePath)
|
||||
) {
|
||||
if (isCheckpointSessionTranscriptPath(absolutePath)) {
|
||||
continue;
|
||||
}
|
||||
sessionFiles.push({
|
||||
|
||||
@@ -3189,9 +3189,7 @@ describe("short-term promotion", () => {
|
||||
path: "memory/2026-04-03.md",
|
||||
snippet: "Move backups to S3 Glacier and sync QMD router notes.",
|
||||
}),
|
||||
// "s3" is a protected-glossary term; it now surfaces as a standalone token past the
|
||||
// per-script min-length gate (the longer terms still match as substrings).
|
||||
).toStrictEqual(["backup", "backups", "glacier", "qmd", "router", "s3", "sync"]);
|
||||
).toStrictEqual(["backup", "backups", "glacier", "qmd", "router", "sync"]);
|
||||
});
|
||||
|
||||
it("extracts multilingual concept tags across latin and cjk snippets", () => {
|
||||
|
||||
@@ -37,15 +37,6 @@ describe("stripHtmlFromTeamsMessage", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not double-decode escaped entities (decodes & last)", () => {
|
||||
// Graph encodes literally-typed entity text by escaping its '&' to '&'.
|
||||
// Decoding '&' first would re-decode the now-bare '<'/'>' into
|
||||
// angle brackets, corrupting the user's literal text.
|
||||
expect(stripHtmlFromTeamsMessage("The token is &lt;APIKEY&gt;")).toBe(
|
||||
"The token is <APIKEY>",
|
||||
);
|
||||
});
|
||||
|
||||
it("normalizes multiple whitespace to single space", () => {
|
||||
expect(stripHtmlFromTeamsMessage("hello world")).toBe("hello world");
|
||||
});
|
||||
|
||||
@@ -35,16 +35,14 @@ export function stripHtmlFromTeamsMessage(html: string): string {
|
||||
let text = html.replace(/<at[^>]*>(.*?)<\/at>/gi, "@$1");
|
||||
// Strip remaining HTML tags.
|
||||
text = text.replace(/<[^>]*>/g, " ");
|
||||
// Decode common HTML entities. & must be decoded LAST to prevent
|
||||
// double-decoding (e.g. &lt; → < not <), matching decodeHtmlEntities
|
||||
// in inbound.ts.
|
||||
// Decode common HTML entities.
|
||||
text = text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/ /g, " ")
|
||||
.replace(/&/g, "&");
|
||||
.replace(/ /g, " ");
|
||||
// Normalize whitespace.
|
||||
return text.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Ollama tests cover embedding provider plugin behavior.
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createStreamingResponse } from "../../test-support/streaming-error-response.js";
|
||||
|
||||
const { fetchConfiguredLocalOriginWithSsrFGuardMock } = vi.hoisted(() => ({
|
||||
fetchConfiguredLocalOriginWithSsrFGuardMock: vi.fn(
|
||||
@@ -413,40 +412,10 @@ describe("ollama embedding provider", () => {
|
||||
});
|
||||
|
||||
await expect(provider.embedQuery("hello")).rejects.toThrow(
|
||||
"Ollama embed response: malformed JSON response",
|
||||
"Ollama embed response returned malformed JSON",
|
||||
);
|
||||
});
|
||||
|
||||
it("bounds successful embed JSON bodies before parsing", async () => {
|
||||
const streamed = createStreamingResponse({
|
||||
chunkCount: 32,
|
||||
chunkSize: 1024 * 1024,
|
||||
text: "x",
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
const jsonSpy = vi.spyOn(streamed.response, "json").mockRejectedValue(new Error("unbounded"));
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => streamed.response),
|
||||
);
|
||||
|
||||
const { provider } = await createOllamaEmbeddingProvider({
|
||||
config: {} as OpenClawConfig,
|
||||
provider: "ollama",
|
||||
model: "nomic-embed-text",
|
||||
fallback: "none",
|
||||
remote: { baseUrl: "http://127.0.0.1:11434" },
|
||||
});
|
||||
|
||||
await expect(provider.embedQuery("hello")).rejects.toThrow(
|
||||
"Ollama embed response: JSON response exceeds 16777216 bytes",
|
||||
);
|
||||
|
||||
expect(streamed.getReadCount()).toBeLessThan(32);
|
||||
expect(streamed.wasCanceled()).toBe(true);
|
||||
expect(jsonSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects non-number embedding values instead of zeroing them", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
|
||||
@@ -6,10 +6,7 @@ import {
|
||||
normalizeOptionalSecretInput,
|
||||
} from "openclaw/plugin-sdk/provider-auth";
|
||||
import { resolveEnvApiKey } from "openclaw/plugin-sdk/provider-auth-runtime";
|
||||
import {
|
||||
readProviderJsonResponse,
|
||||
readResponseTextLimited,
|
||||
} from "openclaw/plugin-sdk/provider-http";
|
||||
import { readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
|
||||
import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import {
|
||||
hasConfiguredSecretInput,
|
||||
@@ -120,9 +117,14 @@ async function withRemoteHttpResponse<T>(params: {
|
||||
}
|
||||
|
||||
async function readOllamaEmbeddingJsonResponse(
|
||||
response: Response,
|
||||
response: Pick<Response, "json">,
|
||||
): Promise<{ embeddings?: unknown }> {
|
||||
const payload = await readProviderJsonResponse<unknown>(response, "Ollama embed response");
|
||||
let payload: unknown;
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch (cause) {
|
||||
throw new Error("Ollama embed response returned malformed JSON", { cause });
|
||||
}
|
||||
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
||||
throw new Error("Ollama embed response returned a non-object JSON payload");
|
||||
}
|
||||
|
||||
@@ -5,9 +5,7 @@ import {
|
||||
buildOllamaProvider,
|
||||
buildOllamaModelDefinition,
|
||||
enrichOllamaModelsWithContext,
|
||||
fetchOllamaModels,
|
||||
parseOllamaNumCtxParameter,
|
||||
queryOllamaModelShowInfo,
|
||||
resetOllamaModelShowInfoCacheForTest,
|
||||
resolveOllamaApiBase,
|
||||
type OllamaTagModel,
|
||||
@@ -382,57 +380,4 @@ describe("ollama provider models", () => {
|
||||
expect(parseOllamaNumCtxParameter('stop "<|eot_id|>"')).toBeUndefined();
|
||||
expect(parseOllamaNumCtxParameter({ num_ctx: 8192 })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("fails soft and stops reading when discovery streams exceed the JSON byte cap", async () => {
|
||||
// Larger than the shared 16 MiB readProviderJsonResponse cap so the bounded reader cancels
|
||||
// the stream mid-flight; if the cap were removed the reader would buffer the whole payload.
|
||||
const ONE_MIB = 1024 * 1024;
|
||||
const TOTAL_CHUNKS = 32; // 32 MiB advertised body, double the cap.
|
||||
const chunk = new Uint8Array(ONE_MIB);
|
||||
|
||||
let bytesPulled = 0;
|
||||
let canceled = false;
|
||||
const makeOversizedJsonResponse = (): Response => {
|
||||
bytesPulled = 0;
|
||||
canceled = false;
|
||||
let pulled = 0;
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
pull(controller) {
|
||||
if (pulled >= TOTAL_CHUNKS) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
pulled += 1;
|
||||
bytesPulled += chunk.length;
|
||||
controller.enqueue(chunk);
|
||||
},
|
||||
cancel() {
|
||||
canceled = true;
|
||||
},
|
||||
});
|
||||
return new Response(body, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
};
|
||||
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => makeOversizedJsonResponse()),
|
||||
);
|
||||
const tags = await fetchOllamaModels("http://127.0.0.1:11434");
|
||||
expect(tags).toEqual({ reachable: false, models: [] });
|
||||
expect(canceled).toBe(true);
|
||||
// Only the bounded prefix is pulled, never the full advertised 32 MiB stream.
|
||||
expect(bytesPulled).toBeLessThan(TOTAL_CHUNKS * ONE_MIB);
|
||||
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => makeOversizedJsonResponse()),
|
||||
);
|
||||
const showInfo = await queryOllamaModelShowInfo("http://127.0.0.1:11434", "evil-model:latest");
|
||||
expect(showInfo).toEqual({});
|
||||
expect(canceled).toBe(true);
|
||||
expect(bytesPulled).toBeLessThan(TOTAL_CHUNKS * ONE_MIB);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-onboard";
|
||||
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
|
||||
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import {
|
||||
OLLAMA_DEFAULT_BASE_URL,
|
||||
@@ -147,11 +146,11 @@ export async function queryOllamaModelShowInfo(
|
||||
if (!response.ok) {
|
||||
return {};
|
||||
}
|
||||
const data = await readProviderJsonResponse<{
|
||||
const data = (await response.json()) as {
|
||||
model_info?: Record<string, unknown>;
|
||||
capabilities?: unknown;
|
||||
parameters?: unknown;
|
||||
}>(response, "ollama-provider-models.show");
|
||||
};
|
||||
|
||||
let contextWindow: number | undefined;
|
||||
if (data.model_info) {
|
||||
@@ -315,10 +314,7 @@ export async function fetchOllamaModels(
|
||||
if (!response.ok) {
|
||||
return { reachable: true, models: [] };
|
||||
}
|
||||
const data = await readProviderJsonResponse<OllamaTagsResponse>(
|
||||
response,
|
||||
"ollama-provider-models.tags",
|
||||
);
|
||||
const data = (await response.json()) as OllamaTagsResponse;
|
||||
const models = (data.models ?? []).filter((m) => m.name);
|
||||
return { reachable: true, models };
|
||||
} finally {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Ollama tests cover web search provider plugin behavior.
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createStreamingResponse } from "../../test-support/streaming-error-response.js";
|
||||
import { createOllamaWebSearchProvider as createContractOllamaWebSearchProvider } from "../web-search-contract-api.js";
|
||||
import {
|
||||
testing,
|
||||
@@ -404,32 +403,7 @@ describe("ollama web search provider", () => {
|
||||
config: createOllamaConfig(),
|
||||
query: "openclaw",
|
||||
}),
|
||||
).rejects.toThrow("Ollama web search: malformed JSON response");
|
||||
});
|
||||
|
||||
it("bounds successful Ollama web search JSON bodies before parsing", async () => {
|
||||
const streamed = createStreamingResponse({
|
||||
chunkCount: 32,
|
||||
chunkSize: 1024 * 1024,
|
||||
text: "x",
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
const jsonSpy = vi.spyOn(streamed.response, "json").mockRejectedValue(new Error("unbounded"));
|
||||
fetchWithSsrFGuardMock.mockResolvedValueOnce({
|
||||
response: streamed.response,
|
||||
release: vi.fn(async () => {}),
|
||||
});
|
||||
|
||||
await expect(
|
||||
runOllamaWebSearch({
|
||||
config: createOllamaConfig(),
|
||||
query: "openclaw",
|
||||
}),
|
||||
).rejects.toThrow("Ollama web search: JSON response exceeds 16777216 bytes");
|
||||
|
||||
expect(streamed.getReadCount()).toBeLessThan(32);
|
||||
expect(streamed.wasCanceled()).toBe(true);
|
||||
expect(jsonSpy).not.toHaveBeenCalled();
|
||||
).rejects.toThrow("Ollama web search returned malformed JSON");
|
||||
});
|
||||
|
||||
it("warns when Ollama is not reachable during setup without cancelling", async () => {
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
normalizeOptionalSecretInput,
|
||||
} from "openclaw/plugin-sdk/provider-auth";
|
||||
import { resolveEnvApiKey } from "openclaw/plugin-sdk/provider-auth-runtime";
|
||||
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
|
||||
import {
|
||||
enablePluginInConfig,
|
||||
readPositiveIntegerParam,
|
||||
@@ -68,7 +67,11 @@ type OllamaWebSearchAttempt = {
|
||||
};
|
||||
|
||||
async function readOllamaWebSearchResponse(response: Response): Promise<OllamaWebSearchResponse> {
|
||||
return await readProviderJsonResponse<OllamaWebSearchResponse>(response, "Ollama web search");
|
||||
try {
|
||||
return (await response.json()) as OllamaWebSearchResponse;
|
||||
} catch (cause) {
|
||||
throw new Error("Ollama web search returned malformed JSON", { cause });
|
||||
}
|
||||
}
|
||||
|
||||
function isOllamaCloudBaseUrl(baseUrl: string): boolean {
|
||||
|
||||
@@ -89,27 +89,13 @@ prose run alice/code-review
|
||||
2. Fetch the `.prose` content
|
||||
3. Load the VM and execute as normal
|
||||
|
||||
Top-level remote runs are explicit user requests. Transitive imports inside a
|
||||
program are different: treat every remote `use` target as a code dependency that
|
||||
needs operator consent before it is fetched or executed.
|
||||
|
||||
This same resolution applies to `use` statements inside `.prose` files, but the
|
||||
VM must fail closed until the operator approves the remote dependency list:
|
||||
This same resolution applies to `use` statements inside `.prose` files:
|
||||
|
||||
```prose
|
||||
use "https://example.com/my-program.prose" # Direct URL
|
||||
use "alice/research" as research # Registry shorthand
|
||||
```
|
||||
|
||||
When a program contains any remote `use` target (`http://`, `https://`, or
|
||||
registry shorthand):
|
||||
|
||||
1. Collect and display the exact resolved remote targets.
|
||||
2. Explain that these are transitive code dependencies for this run.
|
||||
3. Ask the operator to reply exactly `approve remote prose imports` to continue.
|
||||
4. Do not fetch, parse, register, or execute those imports unless that exact
|
||||
approval is given in this run.
|
||||
|
||||
---
|
||||
|
||||
## File Locations
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user