Skip to content

Commit f0ff3ed

Browse files
committed
Auto-merge upstream openclaw/openclaw
2 parents 42728fd + 0c0170b commit f0ff3ed

37 files changed

Lines changed: 1092 additions & 823 deletions

docs/AGENTS.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Docs Guide
2+
3+
This directory owns docs authoring, Mintlify link rules, and docs i18n policy.
4+
5+
## Mintlify Rules
6+
7+
- Docs are hosted on Mintlify (`https://docs.openclaw.ai`).
8+
- Internal doc links in `docs/**/*.md` must stay root-relative with no `.md` or `.mdx` suffix (example: `[Config](/configuration)`).
9+
- Section cross-references should use anchors on root-relative paths (example: `[Hooks](/configuration#hooks)`).
10+
- Doc headings should avoid em dashes and apostrophes because Mintlify anchor generation is brittle there.
11+
- README and other GitHub-rendered docs should keep absolute docs URLs so links work outside Mintlify.
12+
- Docs content must stay generic: no personal device names, hostnames, or local paths; use placeholders like `user@gateway-host`.
13+
14+
## Docs Content Rules
15+
16+
- For docs, UI copy, and picker lists, order services/providers alphabetically unless the section is explicitly describing runtime order or auto-detection order.
17+
- Keep bundled plugin naming consistent with the repo-wide plugin terminology rules in the root `AGENTS.md`.
18+
19+
## Docs i18n
20+
21+
- Foreign-language docs are not maintained in this repo. The generated publish output lives in the separate `openclaw/docs` repo (often cloned locally as `../openclaw-docs`).
22+
- Do not add or edit localized docs under `docs/<locale>/**` here.
23+
- Treat English docs in this repo plus glossary files as the source of truth.
24+
- Pipeline: update English docs here, update `docs/.i18n/glossary.<locale>.json` as needed, then let the publish-repo sync and `scripts/docs-i18n` run in `openclaw/docs`.
25+
- Before rerunning `scripts/docs-i18n`, add glossary entries for any new technical terms, page titles, or short nav labels that must stay in English or use a fixed translation.
26+
- `pnpm docs:check-i18n-glossary` is the guard for changed English doc titles and short internal doc labels.
27+
- Translation memory lives in generated `docs/.i18n/*.tm.jsonl` files in the publish repo.
28+
- See `docs/.i18n/README.md`.

docs/CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
AGENTS.md

docs/plugins/architecture.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,9 @@ actual behavior such as hooks, tools, commands, or provider flows.
527527
Optional manifest `activation` and `setup` blocks stay on the control plane.
528528
They are metadata-only descriptors for activation planning and setup discovery;
529529
they do not replace runtime registration, `register(...)`, or `setupEntry`.
530+
The first activation consumer now uses manifest command hints to narrow CLI
531+
plugin loading when a primary command is known, instead of always loading every
532+
CLI-capable plugin up front.
530533

531534
Setup discovery now prefers descriptor-owned ids such as `setup.providers` and
532535
`setup.cliBackends` to narrow candidate plugins before it falls back to

docs/plugins/manifest.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,9 @@ should activate it later.
221221

222222
This block is metadata only. It does not register runtime behavior, and it does
223223
not replace `register(...)`, `setupEntry`, or other runtime/plugin entrypoints.
224+
Current consumers use it as a narrowing hint before broader plugin loading, so
225+
missing activation metadata only costs performance; it should not change
226+
correctness.
224227

225228
```json
226229
{
@@ -242,6 +245,10 @@ not replace `register(...)`, `setupEntry`, or other runtime/plugin entrypoints.
242245
| `onRoutes` | No | `string[]` | Route kinds that should activate this plugin. |
243246
| `onCapabilities` | No | `Array<"provider" \| "channel" \| "tool" \| "hook">` | Broad capability hints used by control-plane activation planning. |
244247

248+
For command-triggered planning specifically, OpenClaw still falls back to
249+
legacy `commandAliases[].cliCommand` or `commandAliases[].name` when a plugin
250+
has not added explicit `activation.onCommands` metadata yet.
251+
245252
## setup reference
246253

247254
Use `setup` when setup and onboarding surfaces need cheap plugin-owned metadata

extensions/AGENTS.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ third-party plugins see.
5151
provider needs OpenAI-style Anthropic tool payload compat, Gemini schema
5252
cleanup, or an XAI compat patch, use a named shared helper instead of
5353
inlining the policy knobs again.
54+
- Keep control-plane metadata separate from runtime logic. Discovery, config
55+
validation, setup hints, onboarding hints, and activation planning should be
56+
expressible from manifest/descriptors whenever possible.
57+
- If setup truly requires runtime execution, make that explicit in the plugin's
58+
declared setup/runtime surface instead of letting metadata flows import
59+
runtime code accidentally.
60+
- Do not rely on eager global registry seeding or import-time side effects to
61+
make a plugin “available”. Plugin availability should come from manifest
62+
ownership plus targeted activation.
5463

5564
## Expanding The Boundary
5665

extensions/browser/src/browser/routes/agent.act.existing-session-navigation-guard.test.ts

Lines changed: 50 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,6 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
import { createExistingSessionAgentSharedModule } from "./existing-session.test-support.js";
23
import { createBrowserRouteApp, createBrowserRouteResponse } from "./test-helpers.js";
3-
import type { BrowserRequest } from "./types.js";
4-
5-
const routeState = vi.hoisted(() => ({
6-
profileCtx: {
7-
profile: {
8-
driver: "existing-session" as const,
9-
name: "chrome-live",
10-
},
11-
ensureTabAvailable: vi.fn(async () => ({
12-
targetId: "7",
13-
url: "https://example.com",
14-
})),
15-
},
16-
tab: {
17-
targetId: "7",
18-
url: "https://example.com",
19-
},
20-
}));
214

225
const chromeMcpMocks = vi.hoisted(() => ({
236
clickChromeMcpElement: vi.fn(async () => {}),
@@ -49,26 +32,7 @@ vi.mock("../chrome-mcp.js", () => ({
4932

5033
vi.mock("../navigation-guard.js", () => navigationGuardMocks);
5134

52-
vi.mock("./agent.shared.js", () => ({
53-
getPwAiModule: vi.fn(async () => null),
54-
handleRouteError: vi.fn(),
55-
readBody: vi.fn((req: BrowserRequest) => req.body ?? {}),
56-
requirePwAi: vi.fn(async () => {
57-
throw new Error("Playwright should not be used for existing-session tests");
58-
}),
59-
resolveProfileContext: vi.fn(() => routeState.profileCtx),
60-
resolveTargetIdFromBody: vi.fn((body: Record<string, unknown>) =>
61-
typeof body.targetId === "string" ? body.targetId : undefined,
62-
),
63-
withPlaywrightRouteContext: vi.fn(),
64-
withRouteTabContext: vi.fn(async ({ run }: { run: (args: unknown) => Promise<void> }) => {
65-
await run({
66-
profileCtx: routeState.profileCtx,
67-
cdpUrl: "http://127.0.0.1:18800",
68-
tab: routeState.tab,
69-
});
70-
}),
71-
}));
35+
vi.mock("./agent.shared.js", () => createExistingSessionAgentSharedModule());
7236

7337
const DEFAULT_SSRF_POLICY = { allowPrivateNetwork: false } as const;
7438

@@ -119,6 +83,31 @@ describe("existing-session interaction navigation guard", () => {
11983
return response;
12084
}
12185

86+
async function expectActionToReject(body: Record<string, unknown>) {
87+
const handler = getActPostHandler();
88+
const response = createBrowserRouteResponse();
89+
const pending = handler?.({ params: {}, query: {}, body }, response.res) ?? Promise.resolve();
90+
void pending.catch(() => {});
91+
const completion = (async () => {
92+
await vi.runAllTimersAsync();
93+
await pending;
94+
})();
95+
96+
await expect(completion).rejects.toThrow("Unable to verify stable post-interaction navigation");
97+
}
98+
99+
function expectNavigationProbeUrls(urls: string[]) {
100+
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenCalledTimes(
101+
urls.length,
102+
);
103+
for (const [index, url] of urls.entries()) {
104+
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
105+
index + 1,
106+
expect.objectContaining({ url }),
107+
);
108+
}
109+
}
110+
122111
it("checks navigation after click and key-driven submit paths", async () => {
123112
const clickResponse = await runAction({ kind: "click", ref: "btn-1" });
124113
const typeResponse = await runAction({
@@ -134,31 +123,7 @@ describe("existing-session interaction navigation guard", () => {
134123
expect(chromeMcpMocks.pressChromeMcpKey).toHaveBeenCalledWith(
135124
expect.objectContaining({ key: "Enter" }),
136125
);
137-
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenCalledTimes(6);
138-
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
139-
1,
140-
expect.objectContaining({ url: "https://example.com" }),
141-
);
142-
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
143-
2,
144-
expect.objectContaining({ url: "https://example.com" }),
145-
);
146-
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
147-
3,
148-
expect.objectContaining({ url: "https://example.com" }),
149-
);
150-
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
151-
4,
152-
expect.objectContaining({ url: "https://example.com" }),
153-
);
154-
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
155-
5,
156-
expect.objectContaining({ url: "https://example.com" }),
157-
);
158-
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
159-
6,
160-
expect.objectContaining({ url: "https://example.com" }),
161-
);
126+
expectNavigationProbeUrls(Array.from({ length: 6 }, () => "https://example.com"));
162127
});
163128

164129
it("rechecks the page url after delayed navigation-triggering interactions", async () => {
@@ -172,18 +137,11 @@ describe("existing-session interaction navigation guard", () => {
172137

173138
expect(response.statusCode).toBe(200);
174139
expect(chromeMcpMocks.evaluateChromeMcpScript).toHaveBeenCalledTimes(4);
175-
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
176-
1,
177-
expect.objectContaining({ url: "https://example.com" }),
178-
);
179-
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
180-
2,
181-
expect.objectContaining({ url: "http://169.254.169.254/latest/meta-data/" }),
182-
);
183-
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
184-
3,
185-
expect.objectContaining({ url: "http://169.254.169.254/latest/meta-data/" }),
186-
);
140+
expectNavigationProbeUrls([
141+
"https://example.com",
142+
"http://169.254.169.254/latest/meta-data/",
143+
"http://169.254.169.254/latest/meta-data/",
144+
]);
187145
});
188146

189147
it("fails closed when location probes never return a usable url", async () => {
@@ -193,20 +151,7 @@ describe("existing-session interaction navigation guard", () => {
193151
.mockResolvedValueOnce(null as never)
194152
.mockResolvedValueOnce(" " as never);
195153

196-
const handler = getActPostHandler();
197-
const response = createBrowserRouteResponse();
198-
const pending =
199-
handler?.(
200-
{ params: {}, query: {}, body: { kind: "evaluate", fn: "() => 1" } },
201-
response.res,
202-
) ?? Promise.resolve();
203-
void pending.catch(() => {});
204-
const completion = (async () => {
205-
await vi.runAllTimersAsync();
206-
await pending;
207-
})();
208-
209-
await expect(completion).rejects.toThrow("Unable to verify stable post-interaction navigation");
154+
await expectActionToReject({ kind: "evaluate", fn: "() => 1" });
210155
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).not.toHaveBeenCalled();
211156
});
212157

@@ -218,24 +163,8 @@ describe("existing-session interaction navigation guard", () => {
218163
.mockResolvedValueOnce(undefined as never) // location probe 3 - unreadable
219164
.mockResolvedValueOnce(undefined as never); // follow-up probe - still unreadable
220165

221-
const handler = getActPostHandler();
222-
const response = createBrowserRouteResponse();
223-
const pending =
224-
handler?.(
225-
{ params: {}, query: {}, body: { kind: "evaluate", fn: "() => 1" } },
226-
response.res,
227-
) ?? Promise.resolve();
228-
void pending.catch(() => {});
229-
const completion = (async () => {
230-
await vi.runAllTimersAsync();
231-
await pending;
232-
})();
233-
234-
await expect(completion).rejects.toThrow("Unable to verify stable post-interaction navigation");
235-
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenCalledOnce();
236-
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenCalledWith(
237-
expect.objectContaining({ url: "https://example.com" }),
238-
);
166+
await expectActionToReject({ kind: "evaluate", fn: "() => 1" });
167+
expectNavigationProbeUrls(["https://example.com"]);
239168
});
240169

241170
it("confirms stability via follow-up probe when URL changes on the last loop iteration", async () => {
@@ -256,19 +185,11 @@ describe("existing-session interaction navigation guard", () => {
256185
expect(response.statusCode).toBe(200);
257186
// 1 action call + 5 location probes (3 in loop + 1 failed + 1 follow-up)
258187
expect(chromeMcpMocks.evaluateChromeMcpScript).toHaveBeenCalledTimes(5);
259-
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenCalledTimes(3);
260-
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
261-
1,
262-
expect.objectContaining({ url: "https://example.com" }),
263-
);
264-
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
265-
2,
266-
expect.objectContaining({ url: "https://safe-redirect.com" }),
267-
);
268-
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
269-
3,
270-
expect.objectContaining({ url: "https://safe-redirect.com" }),
271-
);
188+
expectNavigationProbeUrls([
189+
"https://example.com",
190+
"https://safe-redirect.com",
191+
"https://safe-redirect.com",
192+
]);
272193
});
273194

274195
it("keeps probing through the full window before declaring navigation stable", async () => {
@@ -283,23 +204,12 @@ describe("existing-session interaction navigation guard", () => {
283204

284205
expect(response.statusCode).toBe(200);
285206
expect(chromeMcpMocks.evaluateChromeMcpScript).toHaveBeenCalledTimes(5);
286-
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenCalledTimes(4);
287-
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
288-
1,
289-
expect.objectContaining({ url: "https://example.com" }),
290-
);
291-
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
292-
2,
293-
expect.objectContaining({ url: "https://example.com" }),
294-
);
295-
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
296-
3,
297-
expect.objectContaining({ url: "https://safe-redirect.com" }),
298-
);
299-
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
300-
4,
301-
expect.objectContaining({ url: "https://safe-redirect.com" }),
302-
);
207+
expectNavigationProbeUrls([
208+
"https://example.com",
209+
"https://example.com",
210+
"https://safe-redirect.com",
211+
"https://safe-redirect.com",
212+
]);
303213
});
304214

305215
it("fails closed when follow-up probe sees yet another URL change", async () => {
@@ -310,20 +220,7 @@ describe("existing-session interaction navigation guard", () => {
310220
.mockResolvedValueOnce("https://c.com" as never) // location probe 3: changed again
311221
.mockResolvedValueOnce("https://d.com" as never); // follow-up: still changing
312222

313-
const handler = getActPostHandler();
314-
const response = createBrowserRouteResponse();
315-
const pending =
316-
handler?.(
317-
{ params: {}, query: {}, body: { kind: "evaluate", fn: "() => 1" } },
318-
response.res,
319-
) ?? Promise.resolve();
320-
void pending.catch(() => {});
321-
const completion = (async () => {
322-
await vi.runAllTimersAsync();
323-
await pending;
324-
})();
325-
326-
await expect(completion).rejects.toThrow("Unable to verify stable post-interaction navigation");
223+
await expectActionToReject({ kind: "evaluate", fn: "() => 1" });
327224
});
328225

329226
it("fails closed when a probe error follows two stable reads", async () => {
@@ -336,21 +233,8 @@ describe("existing-session interaction navigation guard", () => {
336233
.mockRejectedValueOnce(new Error("context destroyed") as never) // location probe 3 → error
337234
.mockRejectedValueOnce(new Error("context destroyed") as never); // follow-up → still errored
338235

339-
const handler = getActPostHandler();
340-
const response = createBrowserRouteResponse();
341-
const pending =
342-
handler?.(
343-
{ params: {}, query: {}, body: { kind: "evaluate", fn: "() => 1" } },
344-
response.res,
345-
) ?? Promise.resolve();
346-
void pending.catch(() => {});
347-
const completion = (async () => {
348-
await vi.runAllTimersAsync();
349-
await pending;
350-
})();
351-
352-
await expect(completion).rejects.toThrow("Unable to verify stable post-interaction navigation");
353-
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenCalledTimes(2);
236+
await expectActionToReject({ kind: "evaluate", fn: "() => 1" });
237+
expectNavigationProbeUrls(["https://example.com", "https://example.com"]);
354238
});
355239

356240
it("skips the guard when no SSRF policy is configured", async () => {

0 commit comments

Comments
 (0)