fix: make docs anchor audit use Mintlify CLI

This commit is contained in:
Peter Steinberger
2026-04-10 21:39:15 +01:00
parent b0a39f4112
commit 972ed139a7
11 changed files with 120 additions and 39 deletions

View File

@@ -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.
<a id="maintenance"></a>
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

View File

@@ -164,10 +164,14 @@ Enable any bundled hook:
openclaw hooks enable <hook-name>
```
<a id="session-memory"></a>
### session-memory details
Extracts the last 15 user/assistant messages, generates a descriptive filename slug via LLM, and saves to `<workspace>/memory/YYYY-MM-DD-slug.md`. Requires `workspace.dir` to be configured.
<a id="bootstrap-extra-files"></a>
### 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`).
<a id="command-logger"></a>
### command-logger details
Logs every slash command to `~/.openclaw/logs/commands.log`.
<a id="boot-md"></a>
### 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.

View File

@@ -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:

View File

@@ -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).
</Accordion>

View File

@@ -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).
</Warning>
**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)
<a id="dm-access-model-pairing-allowlist-open-disabled"></a>
## 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:

View File

@@ -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

View File

@@ -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)
</Accordion>
@@ -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)
</Accordion>
@@ -376,6 +377,7 @@ flowchart TD
- [/tools/browser-wsl2-windows-remote-cdp-troubleshooting](/tools/browser-wsl2-windows-remote-cdp-troubleshooting)
</Accordion>
</AccordionGroup>
## Related

View File

@@ -256,7 +256,7 @@ should use `resolveInboundMentionDecision({ facts, policy })`.
<Step title="Package and manifest">
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):
<CodeGroup>
```json package.json

View File

@@ -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.

View File

@@ -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);

View File

@@ -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);