mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-14 10:41:23 +00:00
fix: make docs anchor audit use Mintlify CLI
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user