fix(config): reject schema-only keys (#106031)

* fix(config): reject schema-only keys

* chore(config): refresh plugin SDK baseline

* test(config): remove stale remote enabled fixtures
This commit is contained in:
Peter Steinberger
2026-07-12 23:10:12 -07:00
committed by GitHub
parent 6410e33cb0
commit 0633afe6a5
18 changed files with 51 additions and 51 deletions
+4 -4
View File
@@ -1,4 +1,4 @@
d2eded996abf00045c50ad400648f6e36565a7f158fc2000e15b805052436004 config-baseline.json
2179eacdae1e28fccf05beb4f89c24d3822e93897dd343a0ca2373f20e55ed85 config-baseline.core.json
82596aeb4c8d4cd18fefae554ff4d0c9e2cd82895b53d460db8f8ceb819f0ef6 config-baseline.channel.json
168e3c52da484c235fbc451dead71af16c3bee0c0186c460e5b4b4af70a496df config-baseline.plugin.json
3ad1269afb060076125275c04db3ce432020d6a90d969982806cc6019d57b78d config-baseline.json
0883f41da0e064a58ac557e244a97d6052c3d262225d309cffbbef9e7300f48e config-baseline.core.json
12ce7ba45d75c1753ce090fca8359a15708343cb2aeabb3dd99e8f6915062628 config-baseline.channel.json
eff5bf88593d81104b23578ba5255fa4cb9dd6ca3cb0566e07942190da68a589 config-baseline.plugin.json
@@ -1,2 +1,2 @@
5ddfbeaef2941a299f3677c7f8dd951f112f80ceefef7c6cbd8e4b1d5ec0670f plugin-sdk-api-baseline.json
df6272053970ab3c15f586f7a19c2abe0c9709b8938187e1ec0a85368e5ccd6c plugin-sdk-api-baseline.jsonl
ae6151f3cd57676e269374d916691e838e284b66404ee8d7a905654f444d528d plugin-sdk-api-baseline.json
7d7eafc52679b982474e2616878bbaf601ba3f1682ce5f3dcd4a0fdcaf4c83d6 plugin-sdk-api-baseline.jsonl
+1 -1
View File
@@ -1327,7 +1327,7 @@ See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for preceden
- `pruneAfter`: age cutoff for stale entries (default `30d`).
- `maxEntries`: maximum number of SQLite session entries (default `500`). Runtime writes batch cleanup with a small high-water buffer for production-sized caps; `openclaw sessions cleanup --enforce` applies the cap immediately.
- Short-lived gateway model-run probe sessions use fixed `24h` retention, but cleanup is pressure-gated: it only removes stale strict model-run probe rows when session-entry maintenance/cap pressure is reached. Only strict explicit probe keys matching `agent:*:explicit:model-run-<uuid>` are eligible; normal direct, group, thread, cron, hook, heartbeat, ACP, and sub-agent sessions do not inherit this 24h retention. When model-run cleanup runs, it runs before the broader `pruneAfter` stale-entry cleanup and `maxEntries` cap.
- `rotateBytes`: deprecated and ignored; `openclaw doctor --fix` removes it from older configs.
- Legacy `rotateBytes` is rejected by the current schema; `openclaw doctor --fix` removes it from older configs.
- `resetArchiveRetention`: retention for `*.reset.<timestamp>` transcript archives. Defaults to `pruneAfter`; set `false` to disable.
- `maxDiskBytes`: optional sessions-directory disk budget. In `warn` mode it logs warnings; in `enforce` mode it removes oldest artifacts/sessions first.
- `highWaterBytes`: optional target after budget cleanup. Defaults to `80%` of `maxDiskBytes`.
@@ -78,7 +78,7 @@ Maintenance keeps durable external conversation pointers such as group sessions
Normal Gateway writes flow through the session accessor, which serializes per-agent SQLite mutations through the runtime writer path. Runtime code should prefer the accessor helpers in `src/config/sessions/session-accessor.ts`; legacy `sessions.json` helpers are migration and offline-maintenance tools. When a Gateway is reachable, non-dry-run `openclaw sessions cleanup` and `openclaw agents delete` delegate store mutations to the Gateway so cleanup joins the same writer queue; `--store <path>` is the explicit offline repair path for a selected legacy store and always stays local (as does `--dry-run`). `maxEntries` cleanup is batched for production-sized stores, so a store may briefly exceed the configured cap before the next high-water cleanup rewrites it down. Reads never prune or cap entries during Gateway startup - only writes or `openclaw sessions cleanup --enforce` do, and the latter also applies the cap immediately and prunes old unreferenced legacy transcript, checkpoint, and trajectory artifacts even with no disk budget configured.
OpenClaw no longer creates automatic `sessions.json.bak.*` rotation backups during Gateway writes. The legacy `session.maintenance.rotateBytes` key is ignored and `openclaw doctor --fix` removes it from older configs.
OpenClaw no longer creates automatic `sessions.json.bak.*` rotation backups during Gateway writes. The current schema rejects the legacy `session.maintenance.rotateBytes` key, and `openclaw doctor --fix` removes it from older configs.
Transcript mutations use the session write queue for the SQLite transcript target:
@@ -1657,7 +1657,7 @@ describe("registerPolicyDoctorChecks", () => {
tools: { elevated: { enabled: true } },
gateway: {
mode: "remote",
remote: { enabled: true, url: "wss://remote.example.test:18789" },
remote: { url: "wss://remote.example.test:18789" },
controlUi: {
allowInsecureAuth: true,
dangerouslyDisableDeviceAuth: true,
@@ -1725,9 +1725,7 @@ describe("registerPolicyDoctorChecks", () => {
tools: { elevated: { enabled: false } },
gateway: {
mode: "local",
remote: {
enabled: true,
},
remote: {},
controlUi: {
allowInsecureAuth: false,
dangerouslyDisableDeviceAuth: false,
@@ -7943,7 +7941,6 @@ describe("registerPolicyDoctorChecks", () => {
...cfgWithPolicy(),
gateway: {
remote: {
enabled: true,
url: "wss://remote.example.test:18789",
},
},
-8
View File
@@ -27,7 +27,6 @@
"gateway.http.endpoints.*.enabled",
"gateway.mode",
"gateway.nodes.denyCommands.*",
"gateway.remote.enabled",
"gateway.tailscale.mode",
"gateway.tailscale.preserveFunnel",
"logging.redactSensitive",
@@ -380,13 +379,6 @@
"policy": "gateway.remote.allow",
"reason": "Policy observes whether Gateway remote mode is enabled.",
},
{
"pattern": "gateway.remote.enabled",
"status": "observed",
"area": "gateway",
"policy": "gateway.remote.allow",
"reason": "Policy observes explicit remote Gateway enablement.",
},
{
"pattern": "gateway.http.endpoints.*.enabled",
"status": "observed",
+4 -4
View File
@@ -817,8 +817,8 @@
"src/config/mutate.ts": 1240,
"src/config/plugin-auto-enable.shared.ts": 1158,
"src/config/redact-snapshot.ts": 890,
"src/config/schema.help.ts": 2089,
"src/config/schema.labels.ts": 1133,
"src/config/schema.help.ts": 2087,
"src/config/schema.labels.ts": 1132,
"src/config/schema.ts": 847,
"src/config/sessions/cleanup-service.ts": 694,
"src/config/sessions/disk-budget.ts": 848,
@@ -831,13 +831,13 @@
"src/config/sessions/transcript.ts": 868,
"src/config/sessions/types.ts": 821,
"src/config/types.agent-defaults.ts": 606,
"src/config/types.gateway.ts": 604,
"src/config/types.gateway.ts": 602,
"src/config/types.tools.ts": 784,
"src/config/validation.ts": 2130,
"src/config/zod-schema.agent-runtime.ts": 1165,
"src/config/zod-schema.core.ts": 1157,
"src/config/zod-schema.providers-core.ts": 1748,
"src/config/zod-schema.ts": 1649,
"src/config/zod-schema.ts": 1648,
"src/context-engine/registry.ts": 1075,
"src/context-engine/types.ts": 523,
"src/crestodian/chat-engine.ts": 1104,
-1
View File
@@ -846,7 +846,6 @@ describe("gateway.remote.transport", () => {
const res = validateConfigObject({
gateway: {
remote: {
enabled: true,
transport: "direct",
url: "wss://gateway.example.ts.net",
},
+37
View File
@@ -0,0 +1,37 @@
// Verifies schema-only config keys stay outside the canonical config contract.
import { describe, expect, it } from "vitest";
import { validateConfigObjectRaw } from "./validation.js";
function expectUnknownKey(params: { config: Record<string, unknown>; path: string; key: string }) {
const result = validateConfigObjectRaw(params.config);
expect(result.ok).toBe(false);
if (result.ok) {
return;
}
const issue = result.issues.find(
(candidate) =>
candidate.path === params.path &&
candidate.message.includes(`Unrecognized key: "${params.key}"`),
);
if (!issue) {
throw new Error(`Expected unknown ${params.path}.${params.key} validation issue`);
}
}
describe("dead config keys", () => {
it("rejects legacy session.maintenance.rotateBytes", () => {
expectUnknownKey({
config: { session: { maintenance: { rotateBytes: "10mb" } } },
path: "session.maintenance",
key: "rotateBytes",
});
});
it("rejects unused gateway.remote.enabled", () => {
expectUnknownKey({
config: { gateway: { remote: { enabled: false } } },
path: "gateway.remote",
key: "enabled",
});
});
});
-8
View File
@@ -188,7 +188,6 @@ const TARGET_KEYS = [
"session.maintenance.pruneAfter",
"session.maintenance.pruneDays",
"session.maintenance.maxEntries",
"session.maintenance.rotateBytes",
"session.maintenance.resetArchiveRetention",
"session.maintenance.maxDiskBytes",
"session.maintenance.highWaterBytes",
@@ -840,13 +839,6 @@ describe("config help copy quality", () => {
expect(pruneAfter.includes("30d")).toBe(true);
expect(pruneAfter.includes("12h")).toBe(true);
const rotate = expectDefined(
FIELD_HELP["session.maintenance.rotateBytes"],
'FIELD_HELP["session.maintenance.rotateBytes"] test invariant',
);
expect(/deprecated/i.test(rotate)).toBe(true);
expect(rotate.includes("doctor --fix")).toBe(true);
const deprecated = expectDefined(
FIELD_HELP["session.maintenance.pruneDays"],
'FIELD_HELP["session.maintenance.pruneDays"] test invariant',
-2
View File
@@ -1792,8 +1792,6 @@ export const FIELD_HELP: Record<string, string> = {
"Deprecated age-retention field kept for compatibility with legacy configs using day counts. Use session.maintenance.pruneAfter instead so duration syntax and behavior are consistent.",
"session.maintenance.maxEntries":
"Caps total session entry count retained in the store to prevent unbounded growth over time. Use lower limits for constrained environments, or higher limits when longer history is required.",
"session.maintenance.rotateBytes":
'Deprecated and ignored. Do not use for `sessions.json` growth control; OpenClaw no longer creates automatic rotation backups, and "openclaw doctor --fix" removes this key.',
"session.maintenance.resetArchiveRetention":
"Age-based retention for archived transcripts (`*.reset.<timestamp>` and `*.deleted.<timestamp>`). Defaults to keeping archives until the disk budget evicts them oldest-first; set a duration (for example `30d`) to opt into wall-clock deletion, or `false` to disable it explicitly.",
"session.maintenance.maxDiskBytes":
-1
View File
@@ -897,7 +897,6 @@ export const FIELD_LABELS: Record<string, string> = {
"session.maintenance.pruneAfter": "Session Prune After",
"session.maintenance.pruneDays": "Session Prune Days (Deprecated)",
"session.maintenance.maxEntries": "Session Max Entries",
"session.maintenance.rotateBytes": "Deprecated Session Rotate Size",
"session.maintenance.resetArchiveRetention": "Session Reset Archive Retention",
"session.maintenance.maxDiskBytes": "Session Max Disk Budget",
"session.maintenance.highWaterBytes": "Session Disk High-water Target",
@@ -1657,7 +1657,6 @@ describe("Integration: saveSessionStore with pruning", () => {
mode: "enforce",
pruneAfter: "365d",
maxEntries: 100,
rotateBytes: 200,
},
},
});
@@ -1696,7 +1695,6 @@ describe("Integration: saveSessionStore with pruning", () => {
mode: "enforce",
pruneAfter: "365d",
maxEntries: 1,
rotateBytes: 200,
},
},
});
-2
View File
@@ -265,8 +265,6 @@ export type SessionMaintenanceConfig = {
pruneDays?: number;
/** Maximum number of session entries to keep. Default: 500. */
maxEntries?: number;
/** @deprecated Ignored. Run `openclaw doctor --fix` to remove. */
rotateBytes?: number | string;
/**
* Age-based retention for archived transcripts (`*.reset.<timestamp>` and
* `*.deleted.<timestamp>`). Default and `false`: keep archives until the
-2
View File
@@ -251,8 +251,6 @@ export type GatewayTailscaleConfig = {
};
export type GatewayRemoteConfig = {
/** Whether remote gateway surfaces are enabled. Default: true when absent. */
enabled?: boolean;
/** Remote Gateway WebSocket URL (ws:// or wss://). */
url?: string;
/** Transport for macOS remote connections (ssh tunnel or direct WS). */
-1
View File
@@ -88,7 +88,6 @@ export const SessionSchema = z
/** @deprecated Use pruneAfter instead. */
pruneDays: z.number().int().positive().optional(),
maxEntries: z.number().int().positive().optional(),
rotateBytes: z.union([z.string(), z.number()]).optional(),
resetArchiveRetention: z.union([z.string(), z.number(), z.literal(false)]).optional(),
maxDiskBytes: z.union([z.string(), z.number()]).optional(),
highWaterBytes: z.union([z.string(), z.number()]).optional(),
-1
View File
@@ -58,7 +58,6 @@ type ConfigSchemaShape<T extends object> = {
};
const GatewayRemoteSchemaShape = {
enabled: z.boolean().optional(),
url: z.string().optional(),
transport: z.union([z.literal("ssh"), z.literal("direct")]).optional(),
remotePort: z.number().int().min(1).max(65_535).optional(),
@@ -56,7 +56,6 @@ describe("secrets runtime gateway local surfaces", () => {
password: "local-password",
},
remote: {
enabled: true,
token: { source: "env", provider: "default", id: "MISSING_REMOTE_TOKEN" },
password: { source: "env", provider: "default", id: "MISSING_REMOTE_PASSWORD" },
},
@@ -181,7 +180,6 @@ describe("secrets runtime gateway local surfaces", () => {
password: { source: "env", provider: "default", id: "GATEWAY_PASSWORD_REF" },
},
remote: {
enabled: true,
token: "remote-token",
},
},
@@ -199,7 +197,6 @@ describe("secrets runtime gateway local surfaces", () => {
mode,
},
remote: {
enabled: true,
token: { source: "env", provider: "default", id: "REMOTE_GATEWAY_TOKEN_REF" },
password: {
source: "env",
@@ -234,7 +231,6 @@ describe("secrets runtime gateway local surfaces", () => {
gateway: {
mode: "local",
remote: {
enabled: true,
token: { source: "env", provider: "default", id: "REMOTE_GATEWAY_TOKEN_REF" },
},
},
@@ -256,7 +252,6 @@ describe("secrets runtime gateway local surfaces", () => {
gateway: {
mode: "local",
remote: {
enabled: true,
password: { source: "env", provider: "default", id: "REMOTE_GATEWAY_PASSWORD_REF" },
},
},
@@ -281,7 +276,6 @@ describe("secrets runtime gateway local surfaces", () => {
mode: "local",
tailscale: { mode: "serve" },
remote: {
enabled: true,
token: { source: "env", provider: "default", id: "REMOTE_GATEWAY_TOKEN" },
password: { source: "env", provider: "default", id: "REMOTE_GATEWAY_PASSWORD" },
},