Skip to content

Commit be38a6c

Browse files
committed
Gateway: add path-scoped config schema lookup
1 parent 8c85ad5 commit be38a6c

18 files changed

+424
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Docs: https://docs.openclaw.ai
3939
- TUI/final-error rendering fallback: when a chat `final` event has no renderable assistant content but includes envelope `errorMessage`, render the formatted error text instead of collapsing to `"(no output)"`, preserving actionable failure context in-session. (#14687) Thanks @Mquarmoc.
4040
- OpenAI Codex OAuth/login hardening: fail OAuth completion early when the returned token is missing `api.responses.write`, and allow `openclaw models auth login --provider openai-codex` to use the built-in OAuth path even when no provider plugins are installed. (#36660) Thanks @driesvints.
4141
- OpenAI Codex OAuth/scope request parity: augment the OAuth authorize URL with required API scopes (`api.responses.write`, `model.request`, `api.model.read`) before browser handoff so OAuth tokens include runtime model/request permissions expected by OpenAI API calls. (#24720) Thanks @Skippy-Gunboat.
42+
- Agents/config schema lookup: add `gateway` tool action `config.schema.lookup` so agents can inspect one config path at a time before edits without loading the full schema into prompt context.
4243
- Onboarding/API key input hardening: strip non-Latin1 Unicode artifacts from normalized secret input (while preserving Latin-1 content and internal spaces) so malformed copied API keys cannot trigger HTTP header `ByteString` construction crashes; adds regression coverage for shared normalization and MiniMax auth header usage. (#24496) Thanks @fa6maalassaf.
4344
- Kimi Coding/Anthropic tools compatibility: normalize `anthropic-messages` tool payloads to OpenAI-style `tools[].function` + compatible `tool_choice` when targeting Kimi Coding endpoints, restoring tool-call workflows that regressed after v2026.3.2. (#37038) Thanks @mochimochimochi-hub.
4445
- Heartbeat/workspace-path guardrails: append explicit workspace `HEARTBEAT.md` path guidance (and `docs/heartbeat.md` avoidance) to heartbeat prompts so heartbeat runs target workspace checklists reliably across packaged install layouts. (#37037) Thanks @stofancy.

docs/experiments/onboarding-config-protocol.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ Purpose: shared onboarding + config surfaces across CLI, macOS app, and Web UI.
2323
- `wizard.cancel` params: `{ sessionId }`
2424
- `wizard.status` params: `{ sessionId }`
2525
- `config.schema` params: `{}`
26+
- `config.schema.lookup` params: `{ path }`
2627

2728
Responses (shape)
2829

2930
- Wizard: `{ sessionId, done, step?, status?, error? }`
3031
- Config schema: `{ schema, uiHints, version, generatedAt }`
32+
- Config schema lookup: `{ path, schema, hint?, hintPath?, children[] }`
3133

3234
## UI Hints
3335

docs/tools/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,13 +453,15 @@ Restart or apply updates to the running Gateway process (in-place).
453453
Core actions:
454454

455455
- `restart` (authorizes + sends `SIGUSR1` for in-process restart; `openclaw gateway` restart in-place)
456+
- `config.schema.lookup` (inspect one config path at a time without loading the full schema into prompt context)
456457
- `config.get`
457458
- `config.apply` (validate + write config + restart + wake)
458459
- `config.patch` (merge partial update + restart + wake)
459460
- `update.run` (run update + restart + wake)
460461

461462
Notes:
462463

464+
- `config.schema.lookup` expects a targeted dot path such as `gateway.auth` or `agents.list.*.heartbeat`.
463465
- Use `delayMs` (defaults to 2000) to avoid interrupting an in-flight reply.
464466
- `config.schema` remains available to internal Control UI flows and is not exposed through the agent `gateway` tool.
465467
- `restart` is enabled by default; set `commands.restart: false` to disable it.

src/agents/openclaw-gateway-tool.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,27 @@ vi.mock("./tools/gateway.js", () => ({
1111
if (method === "config.get") {
1212
return { hash: "hash-1" };
1313
}
14+
if (method === "config.schema.lookup") {
15+
return {
16+
path: "gateway.auth",
17+
schema: {
18+
type: "object",
19+
},
20+
hint: { label: "Gateway Auth" },
21+
hintPath: "gateway.auth",
22+
children: [
23+
{
24+
key: "token",
25+
path: "gateway.auth.token",
26+
type: "string",
27+
required: true,
28+
hasChildren: false,
29+
hint: { label: "Token", sensitive: true },
30+
hintPath: "gateway.auth.token",
31+
},
32+
],
33+
};
34+
}
1435
return { ok: true };
1536
}),
1637
readGatewayCallOptions: vi.fn(() => ({})),
@@ -166,4 +187,36 @@ describe("gateway tool", () => {
166187
expect(params).toMatchObject({ timeoutMs: 20 * 60_000 });
167188
}
168189
});
190+
191+
it("returns a path-scoped schema lookup result", async () => {
192+
const { callGatewayTool } = await import("./tools/gateway.js");
193+
const tool = requireGatewayTool();
194+
195+
const result = await tool.execute("call5", {
196+
action: "config.schema.lookup",
197+
path: "gateway.auth",
198+
});
199+
200+
expect(callGatewayTool).toHaveBeenCalledWith("config.schema.lookup", expect.any(Object), {
201+
path: "gateway.auth",
202+
});
203+
expect(result.details).toMatchObject({
204+
ok: true,
205+
result: {
206+
path: "gateway.auth",
207+
hintPath: "gateway.auth",
208+
children: [
209+
expect.objectContaining({
210+
key: "token",
211+
path: "gateway.auth.token",
212+
required: true,
213+
hintPath: "gateway.auth.token",
214+
}),
215+
],
216+
},
217+
});
218+
const schema = (result.details as { result?: { schema?: { properties?: unknown } } }).result
219+
?.schema;
220+
expect(schema?.properties).toBeUndefined();
221+
});
169222
});

src/agents/system-prompt.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,10 +443,12 @@ describe("buildAgentSystemPrompt", () => {
443443
});
444444

445445
expect(prompt).toContain("## OpenClaw Self-Update");
446+
expect(prompt).toContain("config.schema.lookup");
446447
expect(prompt).toContain("config.apply");
447448
expect(prompt).toContain("config.patch");
448449
expect(prompt).toContain("update.run");
449-
expect(prompt).not.toContain("config.schema");
450+
expect(prompt).not.toContain("Use config.schema to");
451+
expect(prompt).not.toContain("config.schema, config.apply");
450452
});
451453

452454
it("includes skills guidance when skills prompt is present", () => {

src/agents/system-prompt.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,8 @@ export function buildAgentSystemPrompt(params: {
482482
? [
483483
"Get Updates (self-update) is ONLY allowed when the user explicitly asks for it.",
484484
"Do not run config.apply or update.run unless the user explicitly requests an update or config change; if it's not explicit, ask first.",
485-
"Actions: config.get, config.apply (validate + write full config, then restart), config.patch (partial update, merges with existing), update.run (update deps or git, then restart).",
485+
"Use config.schema.lookup with a specific dot path to inspect only the relevant config subtree before making config changes or answering config-field questions; avoid guessing field names/types.",
486+
"Actions: config.schema.lookup, config.get, config.apply (validate + write full config, then restart), config.patch (partial update, merges with existing), update.run (update deps or git, then restart).",
486487
"After restart, OpenClaw pings the last active session automatically.",
487488
].join("\n")
488489
: "",

src/agents/tools/gateway-tool.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ function resolveBaseHashFromSnapshot(snapshot: unknown): string | undefined {
3434
const GATEWAY_ACTIONS = [
3535
"restart",
3636
"config.get",
37+
"config.schema.lookup",
3738
"config.apply",
3839
"config.patch",
3940
"update.run",
@@ -47,10 +48,12 @@ const GatewayToolSchema = Type.Object({
4748
// restart
4849
delayMs: Type.Optional(Type.Number()),
4950
reason: Type.Optional(Type.String()),
50-
// config.get, config.apply, update.run
51+
// config.get, config.schema.lookup, config.apply, update.run
5152
gatewayUrl: Type.Optional(Type.String()),
5253
gatewayToken: Type.Optional(Type.String()),
5354
timeoutMs: Type.Optional(Type.Number()),
55+
// config.schema.lookup
56+
path: Type.Optional(Type.String()),
5457
// config.apply, config.patch
5558
raw: Type.Optional(Type.String()),
5659
baseHash: Type.Optional(Type.String()),
@@ -73,7 +76,7 @@ export function createGatewayTool(opts?: {
7376
name: "gateway",
7477
ownerOnly: true,
7578
description:
76-
"Restart, apply config, or update the gateway in-place (SIGUSR1). Use config.patch for safe partial config updates (merges with existing). Use config.apply only when replacing entire config. Both trigger restart after writing. Always pass a human-readable completion message via the `note` parameter so the system can deliver it to the user after restart.",
79+
"Restart, inspect a specific config schema path, apply config, or update the gateway in-place (SIGUSR1). Use config.schema.lookup with a targeted dot path before config edits. Use config.patch for safe partial config updates (merges with existing). Use config.apply only when replacing entire config. Both trigger restart after writing. Always pass a human-readable completion message via the `note` parameter so the system can deliver it to the user after restart.",
7780
parameters: GatewayToolSchema,
7881
execute: async (_toolCallId, args) => {
7982
const params = args as Record<string, unknown>;
@@ -171,6 +174,14 @@ export function createGatewayTool(opts?: {
171174
const result = await callGatewayTool("config.get", gatewayOpts, {});
172175
return jsonResult({ ok: true, result });
173176
}
177+
if (action === "config.schema.lookup") {
178+
const path = readStringParam(params, "path", {
179+
required: true,
180+
label: "path",
181+
});
182+
const result = await callGatewayTool("config.schema.lookup", gatewayOpts, { path });
183+
return jsonResult({ ok: true, result });
184+
}
174185
if (action === "config.apply") {
175186
const { raw, baseHash, sessionKey, note, restartDelayMs } =
176187
await resolveConfigWriteParams();

src/config/schema.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { beforeAll, describe, expect, it } from "vitest";
2-
import { buildConfigSchema } from "./schema.js";
2+
import { buildConfigSchema, lookupConfigSchema } from "./schema.js";
33
import { applyDerivedTags, CONFIG_TAGS, deriveTagsForPath } from "./schema.tags.js";
44

55
describe("config schema", () => {
@@ -202,4 +202,34 @@ describe("config schema", () => {
202202
}
203203
}
204204
});
205+
206+
it("looks up a config schema path with immediate child summaries", () => {
207+
const lookup = lookupConfigSchema(baseSchema, "gateway.auth");
208+
expect(lookup?.path).toBe("gateway.auth");
209+
expect(lookup?.hintPath).toBe("gateway.auth");
210+
expect(lookup?.children.some((child) => child.key === "token")).toBe(true);
211+
const tokenChild = lookup?.children.find((child) => child.key === "token");
212+
expect(tokenChild?.path).toBe("gateway.auth.token");
213+
expect(tokenChild?.hint?.sensitive).toBe(true);
214+
expect(tokenChild?.hintPath).toBe("gateway.auth.token");
215+
const schema = lookup?.schema as { properties?: unknown } | undefined;
216+
expect(schema?.properties).toBeUndefined();
217+
});
218+
219+
it("matches wildcard ui hints for concrete lookup paths", () => {
220+
const lookup = lookupConfigSchema(baseSchema, "agents.list.0.identity.avatar");
221+
expect(lookup?.path).toBe("agents.list.0.identity.avatar");
222+
expect(lookup?.hintPath).toBe("agents.list.*.identity.avatar");
223+
expect(lookup?.hint?.help).toContain("workspace-relative path");
224+
});
225+
226+
it("normalizes bracketed lookup paths", () => {
227+
const lookup = lookupConfigSchema(baseSchema, "agents.list[0].identity.avatar");
228+
expect(lookup?.path).toBe("agents.list.0.identity.avatar");
229+
expect(lookup?.hintPath).toBe("agents.list.*.identity.avatar");
230+
});
231+
232+
it("returns null for missing config schema paths", () => {
233+
expect(lookupConfigSchema(baseSchema, "gateway.notReal.path")).toBeNull();
234+
});
205235
});

0 commit comments

Comments
 (0)