From 972ed139a778bcae902aae38ff6fc7de9cf8f96e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 10 Apr 2026 21:39:15 +0100 Subject: [PATCH] fix: make docs anchor audit use Mintlify CLI --- docs/automation/cron-jobs.md | 2 + docs/automation/hooks.md | 16 +++++++ docs/cli/agents.md | 2 +- docs/gateway/configuration.md | 2 +- docs/gateway/security/index.md | 6 ++- docs/gateway/troubleshooting.md | 2 +- docs/help/troubleshooting.md | 24 +++++----- docs/plugins/sdk-channel-plugins.md | 2 +- docs/tools/reactions.md | 2 +- scripts/docs-link-audit.mjs | 71 +++++++++++++++++++++++++---- src/scripts/docs-link-audit.test.ts | 30 +++++++----- 11 files changed, 120 insertions(+), 39 deletions(-) diff --git a/docs/automation/cron-jobs.md b/docs/automation/cron-jobs.md index 8106c21c94c..f71d568e5f7 100644 --- a/docs/automation/cron-jobs.md +++ b/docs/automation/cron-jobs.md @@ -43,6 +43,8 @@ together`, and similar hints) and no descendant subagent run is still responsible for the final answer, OpenClaw re-prompts once for the actual result before delivery. + + Task reconciliation for cron is runtime-owned: an active cron task stays live while the cron runtime still tracks that job as running, even if an old child session row still exists. Once the runtime stops owning the job and the 5-minute grace window expires, maintenance can diff --git a/docs/automation/hooks.md b/docs/automation/hooks.md index 71a239eb510..6220b25e348 100644 --- a/docs/automation/hooks.md +++ b/docs/automation/hooks.md @@ -164,10 +164,14 @@ Enable any bundled hook: openclaw hooks enable ``` + + ### session-memory details Extracts the last 15 user/assistant messages, generates a descriptive filename slug via LLM, and saves to `/memory/YYYY-MM-DD-slug.md`. Requires `workspace.dir` to be configured. + + ### bootstrap-extra-files config ```json @@ -187,6 +191,18 @@ Extracts the last 15 user/assistant messages, generates a descriptive filename s Paths resolve relative to workspace. Only recognized bootstrap basenames are loaded (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md`, `MEMORY.md`). + + +### command-logger details + +Logs every slash command to `~/.openclaw/logs/commands.log`. + + + +### boot-md details + +Runs `BOOT.md` from the active workspace when the gateway starts. + ## Plugin hooks Plugins can register hooks through the Plugin SDK for deeper integration: intercepting tool calls, modifying prompts, controlling message flow, and more. The Plugin SDK exposes 28 hooks covering model resolution, agent lifecycle, message flow, tool execution, subagent coordination, and gateway lifecycle. diff --git a/docs/cli/agents.md b/docs/cli/agents.md index 7110732338e..d43993628d8 100644 --- a/docs/cli/agents.md +++ b/docs/cli/agents.md @@ -37,7 +37,7 @@ Use routing bindings to pin inbound channel traffic to a specific agent. If you also want different visible skills per agent, configure `agents.defaults.skills` and `agents.list[].skills` in `openclaw.json`. See [Skills config](/tools/skills-config) and -[Configuration Reference](/gateway/configuration-reference#agentsdefaultsskills). +[Configuration Reference](/gateway/configuration-reference#agents-defaults-skills). List bindings: diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index fd53242fe7a..9f0ea090996 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -224,7 +224,7 @@ When validation fails: - Omit `agents.list[].skills` to inherit the defaults. - Set `agents.list[].skills: []` for no skills. - See [Skills](/tools/skills), [Skills config](/tools/skills-config), and - the [Configuration Reference](/gateway/configuration-reference#agentsdefaultsskills). + the [Configuration Reference](/gateway/configuration-reference#agents-defaults-skills). diff --git a/docs/gateway/security/index.md b/docs/gateway/security/index.md index 338ca9a7b3e..699ad3730a5 100644 --- a/docs/gateway/security/index.md +++ b/docs/gateway/security/index.md @@ -13,7 +13,7 @@ OpenClaw is **not** a hostile multi-tenant security boundary for multiple advers If you need mixed-trust or adversarial-user operation, split trust boundaries (separate gateway + credentials, ideally separate OS users/hosts). -**On this page:** [Trust model](#scope-first-personal-assistant-security-model) | [Quick audit](#quick-check-openclaw-security-audit) | [Hardened baseline](#hardened-baseline-in-60-seconds) | [DM access model](#dm-access-model-pairing--allowlist--open--disabled) | [Configuration hardening](#configuration-hardening-examples) | [Incident response](#incident-response) +**On this page:** [Trust model](#scope-first-personal-assistant-security-model) | [Quick audit](#quick-check-openclaw-security-audit) | [Hardened baseline](#hardened-baseline-in-60-seconds) | [DM access model](#dm-access-model-pairing-allowlist-open-disabled) | [Configuration hardening](#configuration-hardening-examples) | [Incident response](#incident-response) ## Scope first: personal assistant security model @@ -187,7 +187,7 @@ Allowlists gate triggers and command authorization. The `contextVisibility` sett - `contextVisibility: "allowlist"` filters supplemental context to senders allowed by the active allowlist checks. - `contextVisibility: "allowlist_quote"` behaves like `allowlist`, but still keeps one explicit quoted reply. -Set `contextVisibility` per channel or per room/conversation. See [Group Chats](/channels/groups#context-visibility) for setup details. +Set `contextVisibility` per channel or per room/conversation. See [Group Chats](/channels/groups#context-visibility-and-allowlists) for setup details. Advisory triage guidance: @@ -579,6 +579,8 @@ Plugins run **in-process** with the Gateway. Treat them as trusted code: Details: [Plugins](/tools/plugin) + + ## DM access model (pairing / allowlist / open / disabled) All current DM-capable channels support a DM policy (`dmPolicy` or `*.dm.policy`) that gates inbound DMs **before** the message is processed: diff --git a/docs/gateway/troubleshooting.md b/docs/gateway/troubleshooting.md index a910d774dcd..87e6bc92f67 100644 --- a/docs/gateway/troubleshooting.md +++ b/docs/gateway/troubleshooting.md @@ -111,7 +111,7 @@ Fix options: Related: - [/gateway/local-models](/gateway/local-models) -- [/gateway/configuration#models](/gateway/configuration#models) +- [/gateway/configuration](/gateway/configuration) - [/gateway/configuration-reference#openai-compatible-endpoints](/gateway/configuration-reference#openai-compatible-endpoints) ## No replies diff --git a/docs/help/troubleshooting.md b/docs/help/troubleshooting.md index ec1ede31311..49e02780a49 100644 --- a/docs/help/troubleshooting.md +++ b/docs/help/troubleshooting.md @@ -251,18 +251,19 @@ flowchart TD Common log signatures: -- `cron: scheduler disabled; jobs will not run automatically` → cron is disabled. -- `heartbeat skipped` with `reason=quiet-hours` → outside configured active hours. -- `heartbeat skipped` with `reason=empty-heartbeat-file` → `HEARTBEAT.md` exists but only contains blank/header-only scaffolding. -- `heartbeat skipped` with `reason=no-tasks-due` → `HEARTBEAT.md` task mode is active but none of the task intervals are due yet. -- `heartbeat skipped` with `reason=alerts-disabled` → all heartbeat visibility is disabled (`showOk`, `showAlerts`, and `useIndicator` are all off). -- `requests-in-flight` → main lane busy; heartbeat wake was deferred. - `unknown accountId` → heartbeat delivery target account does not exist. + - `cron: scheduler disabled; jobs will not run automatically` → cron is disabled. + - `heartbeat skipped` with `reason=quiet-hours` → outside configured active hours. + - `heartbeat skipped` with `reason=empty-heartbeat-file` → `HEARTBEAT.md` exists but only contains blank/header-only scaffolding. + - `heartbeat skipped` with `reason=no-tasks-due` → `HEARTBEAT.md` task mode is active but none of the task intervals are due yet. + - `heartbeat skipped` with `reason=alerts-disabled` → all heartbeat visibility is disabled (`showOk`, `showAlerts`, and `useIndicator` are all off). + - `requests-in-flight` → main lane busy; heartbeat wake was deferred. + - `unknown accountId` → heartbeat delivery target account does not exist. - Deep pages: + Deep pages: - - [/gateway/troubleshooting#cron-and-heartbeat-delivery](/gateway/troubleshooting#cron-and-heartbeat-delivery) - - [/automation/cron-jobs#troubleshooting](/automation/cron-jobs#troubleshooting) - - [/gateway/heartbeat](/gateway/heartbeat) + - [/gateway/troubleshooting#cron-and-heartbeat-delivery](/gateway/troubleshooting#cron-and-heartbeat-delivery) + - [/automation/cron-jobs#troubleshooting](/automation/cron-jobs#troubleshooting) + - [/gateway/heartbeat](/gateway/heartbeat) @@ -338,7 +339,7 @@ flowchart TD - [/tools/exec](/tools/exec) - [/tools/exec-approvals](/tools/exec-approvals) - - [/gateway/security#runtime-expectation-drift](/gateway/security#runtime-expectation-drift) + - [/gateway/security#what-the-audit-checks-high-level](/gateway/security#what-the-audit-checks-high-level) @@ -376,6 +377,7 @@ flowchart TD - [/tools/browser-wsl2-windows-remote-cdp-troubleshooting](/tools/browser-wsl2-windows-remote-cdp-troubleshooting) + ## Related diff --git a/docs/plugins/sdk-channel-plugins.md b/docs/plugins/sdk-channel-plugins.md index 3455f3cc422..04042a65306 100644 --- a/docs/plugins/sdk-channel-plugins.md +++ b/docs/plugins/sdk-channel-plugins.md @@ -256,7 +256,7 @@ should use `resolveInboundMentionDecision({ facts, policy })`. Create the standard plugin files. The `channel` field in `package.json` is what makes this a channel plugin. For the full package-metadata surface, - see [Plugin Setup and Config](/plugins/sdk-setup#openclawchannel): + see [Plugin Setup and Config](/plugins/sdk-setup#openclaw-channel): ```json package.json diff --git a/docs/tools/reactions.md b/docs/tools/reactions.md index 26456ecdd3e..98bd1d96cfd 100644 --- a/docs/tools/reactions.md +++ b/docs/tools/reactions.md @@ -68,7 +68,7 @@ tool with the `react` action. Reaction behavior varies by channel. Per-channel `reactionLevel` config controls how broadly the agent uses reactions. Values are typically `off`, `ack`, `minimal`, or `extensive`. - [Telegram reactionLevel](/channels/telegram#reaction-notifications) — `channels.telegram.reactionLevel` -- [WhatsApp reactionLevel](/channels/whatsapp#reactions) — `channels.whatsapp.reactionLevel` +- [WhatsApp reactionLevel](/channels/whatsapp#reaction-level) — `channels.whatsapp.reactionLevel` Set `reactionLevel` on individual channels to tune how actively the agent reacts to messages on each platform. diff --git a/scripts/docs-link-audit.mjs b/scripts/docs-link-audit.mjs index 1c1e899c1ba..d6e121dce18 100644 --- a/scripts/docs-link-audit.mjs +++ b/scripts/docs-link-audit.mjs @@ -9,6 +9,8 @@ import { pathToFileURL } from "node:url"; const ROOT = process.cwd(); const DOCS_DIR = path.join(ROOT, "docs"); const DOCS_JSON_PATH = path.join(DOCS_DIR, "docs.json"); +const MINTLIFY_BROKEN_LINKS_ARGS = ["dlx", "mint", "broken-links", "--check-anchors"]; +const NODE_25_UNSUPPORTED_BY_MINTLIFY = 25; if (!fs.existsSync(DOCS_DIR) || !fs.statSync(DOCS_DIR).isDirectory()) { console.error("docs:check-links: missing docs directory; run from repo root."); @@ -263,6 +265,56 @@ export function prepareAnchorAuditDocsDir(sourceDir = DOCS_DIR) { return tempDir; } +/** @param {string} version */ +function parseNodeMajor(version) { + const major = Number.parseInt(version.split(".")[0] ?? "", 10); + return Number.isFinite(major) ? major : 0; +} + +/** + * Mintlify currently rejects Node 25+. If the repo script itself is running + * under a too-new experimental Node, probe common local version managers and + * use their Node 22 wrapper for only the Mintlify child process. + * + * @param {{ + * cwd: string; + * nodeVersion?: string; + * spawnSyncImpl: typeof spawnSync; + * }} params + */ +export function resolveMintlifyAnchorAuditInvocation(params) { + const nodeVersion = params.nodeVersion ?? process.versions.node; + if (parseNodeMajor(nodeVersion) < NODE_25_UNSUPPORTED_BY_MINTLIFY) { + return { command: "pnpm", args: MINTLIFY_BROKEN_LINKS_ARGS }; + } + + const node22Probe = "process.exit(Number(process.versions.node.split('.')[0]) === 22 ? 0 : 1)"; + const candidates = [ + { + command: "fnm", + probeArgs: ["exec", "--using=22", "node", "-e", node22Probe], + args: ["exec", "--using=22", "pnpm", ...MINTLIFY_BROKEN_LINKS_ARGS], + }, + { + command: "mise", + probeArgs: ["exec", "node@22", "--", "node", "-e", node22Probe], + args: ["exec", "node@22", "--", "pnpm", ...MINTLIFY_BROKEN_LINKS_ARGS], + }, + ]; + + for (const candidate of candidates) { + const probe = params.spawnSyncImpl(candidate.command, candidate.probeArgs, { + cwd: params.cwd, + stdio: "ignore", + }); + if (probe.status === 0) { + return { command: candidate.command, args: candidate.args }; + } + } + + return { command: "pnpm", args: MINTLIFY_BROKEN_LINKS_ARGS }; +} + export function auditDocsLinks() { /** @type {{file: string; line: number; link: string; reason: string}[]} */ const broken = []; @@ -386,6 +438,7 @@ export function auditDocsLinks() { /** * @param {{ * args?: string[]; + * nodeVersion?: string; * spawnSyncImpl?: typeof spawnSync; * prepareAnchorAuditDocsDirImpl?: (sourceDir?: string) => string; * cleanupAnchorAuditDocsDirImpl?: (dir: string) => void; @@ -403,19 +456,19 @@ export function runDocsLinkAuditCli(options = {}) { const anchorDocsDir = prepareAnchorAuditDocsDirImpl(DOCS_DIR); try { - const result = spawnSyncImpl("mint", ["broken-links", "--check-anchors"], { + // Use the npm Mintlify package explicitly. Some developer machines also + // have the Swift Package Manager tool named `mint` on PATH, and that + // binary exits with "command 'broken-links' not found". + const invocation = resolveMintlifyAnchorAuditInvocation({ + cwd: anchorDocsDir, + nodeVersion: options.nodeVersion, + spawnSyncImpl, + }); + const result = spawnSyncImpl(invocation.command, invocation.args, { cwd: anchorDocsDir, stdio: "inherit", }); - if (result.error?.code === "ENOENT") { - const fallback = spawnSyncImpl("pnpm", ["dlx", "mint", "broken-links", "--check-anchors"], { - cwd: anchorDocsDir, - stdio: "inherit", - }); - return fallback.status ?? 1; - } - return result.status ?? 1; } finally { cleanupAnchorAuditDocsDirImpl(anchorDocsDir); diff --git a/src/scripts/docs-link-audit.test.ts b/src/scripts/docs-link-audit.test.ts index 7d9737cc893..a8615b0c0f8 100644 --- a/src/scripts/docs-link-audit.test.ts +++ b/src/scripts/docs-link-audit.test.ts @@ -19,6 +19,7 @@ const { ) => { ok: boolean; terminal: string; loop?: boolean }; runDocsLinkAuditCli: (options?: { args?: string[]; + nodeVersion?: string; spawnSyncImpl?: ( command: string, args: string[], @@ -146,7 +147,7 @@ describe("docs-link-audit", () => { } }); - it("prefers a local mint binary for anchor validation", () => { + it("uses Mintlify through pnpm dlx for anchor validation", () => { let invocation: | { command: string; @@ -159,6 +160,7 @@ describe("docs-link-audit", () => { const exitCode = runDocsLinkAuditCli({ args: ["--anchors"], + nodeVersion: "22.21.1", prepareAnchorAuditDocsDirImpl() { return anchorDocsDir; }, @@ -173,14 +175,14 @@ describe("docs-link-audit", () => { expect(exitCode).toBe(0); expect(invocation).toBeDefined(); - expect(invocation?.command).toBe("mint"); - expect(invocation?.args).toEqual(["broken-links", "--check-anchors"]); + expect(invocation?.command).toBe("pnpm"); + expect(invocation?.args).toEqual(["dlx", "mint", "broken-links", "--check-anchors"]); expect(invocation?.options.stdio).toBe("inherit"); expect(invocation?.options.cwd).toBe(anchorDocsDir); expect(cleanedDir).toBe(anchorDocsDir); }); - it("falls back to pnpm dlx when mint is not on PATH", () => { + it("wraps Mintlify with Node 22 when the current Node is too new", () => { const invocations: Array<{ command: string; args: string[]; @@ -191,6 +193,7 @@ describe("docs-link-audit", () => { const exitCode = runDocsLinkAuditCli({ args: ["--anchors"], + nodeVersion: "25.3.0", prepareAnchorAuditDocsDirImpl() { return anchorDocsDir; }, @@ -199,9 +202,6 @@ describe("docs-link-audit", () => { }, spawnSyncImpl(command, args, options) { invocations.push({ command, args, options }); - if (command === "mint") { - return { status: null, error: { code: "ENOENT" } }; - } return { status: 0 }; }, }); @@ -209,13 +209,19 @@ describe("docs-link-audit", () => { expect(exitCode).toBe(0); expect(invocations).toHaveLength(2); expect(invocations[0]).toMatchObject({ - command: "mint", - args: ["broken-links", "--check-anchors"], - options: { stdio: "inherit" }, + command: "fnm", + args: [ + "exec", + "--using=22", + "node", + "-e", + "process.exit(Number(process.versions.node.split('.')[0]) === 22 ? 0 : 1)", + ], + options: { stdio: "ignore" }, }); expect(invocations[1]).toMatchObject({ - command: "pnpm", - args: ["dlx", "mint", "broken-links", "--check-anchors"], + command: "fnm", + args: ["exec", "--using=22", "pnpm", "dlx", "mint", "broken-links", "--check-anchors"], options: { stdio: "inherit" }, }); expect(invocations[0]?.options.cwd).toBe(anchorDocsDir);