Accept daemon settings in the published editor schema (#6621)

* fix(config): accept daemon settings in the published editor schema

The website schema omitted accepted configuration fields, so editors
rejected valid browser, profile, plugin and metadata settings. Regenerate
from the daemon schema and guard the published artifact against drift.

Co-authored-by: Okan YILDIRIM <63511519+ragokan@users.noreply.github.com>

* fix(config): preserve proxy help when regenerating the schema

Keep the existing editor descriptions in the source schema so regeneration retains them. The new regression fails when the proxy descriptions disappear.

* test(usage): create SQLite store fixtures in one transaction

The Windows plugin suite exceeded its five-second test deadline while creating and exercising the first OMP store fixture. Commit the fixture as a unit rather than flushing each inserted row separately; preserve the read-only discovery assertions.

* test(config): show structured schema validation errors

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Co-authored-by: Okan YILDIRIM <63511519+ragokan@users.noreply.github.com>
This commit is contained in:
paseo-bot[bot]
2026-10-11 18:50:03 +02:00
committed by GitHub
co-authored by Okan YILDIRIM
parent 8d9e7d3ad3
commit 1939ee79ed
5 changed files with 794 additions and 433 deletions
@@ -0,0 +1,78 @@
import { readFileSync } from "node:fs";
import Ajv from "ajv";
import { expect, test } from "vitest";
import { z } from "zod";
import { PersistedConfigSchema } from "./persisted-config.js";
const publishedSchema = JSON.parse(
readFileSync(
new URL("../../../website/public/schemas/paseo.config.v1.json", import.meta.url),
"utf8",
),
);
test("editors accept the daemon's browser, profile, plugin and metadata configuration", () => {
const config = {
$schema: "https://paseo.sh/schemas/paseo.config.v1.json",
version: 1,
daemon: {
browserTools: { enabled: true },
terminalProfiles: [],
agentProfiles: [],
},
pluginsEnabled: true,
plugins: {},
agents: {
metadataGeneration: { providers: [{ provider: "claude", model: "claude-opus-5" }] },
},
};
expect(PersistedConfigSchema.parse(config)).toEqual(config);
const validate = new Ajv({ allErrors: true, strict: false }).compile(publishedSchema);
const valid = validate(config);
expect(validate.errors).toBeNull();
expect(valid).toBe(true);
});
test("the published schema stays current with the daemon's configuration schema", () => {
const generated = z.toJSONSchema(PersistedConfigSchema, {
target: "draft-07",
unrepresentable: "any",
io: "input",
});
generated.title = "PaseoConfigV1";
expect(
publishedSchema,
"Run npm run generate:config-schema --workspace=@getpaseo/server",
).toEqual(generated);
});
test("editors retain the existing proxy configuration help", () => {
const daemon = publishedSchema.properties.daemon.properties;
expect(daemon.trustedProxies.description).toBe(
'Express trusted proxy setting for X-Forwarded-* headers. Defaults to ["loopback"]. Use true only when the final trusted proxy overwrites client-supplied forwarded headers.',
);
expect(daemon.serviceProxy.properties.enabled.description).toBe(
"Compatibility shim. false suppresses optional service proxy listen/public layers only; localhost proxying remains enabled.",
);
expect(daemon.serviceProxy.properties.listen.description).toBe(
"Optional service-only listener address. Presence enables the standalone service proxy listener.",
);
expect(daemon.serviceProxy.properties.publicBaseUrl.description).toBe(
"Optional public base URL for service host aliases.",
);
});
test.each([
{ daemon: { browserTools: { enabled: "yes" } } },
{ daemon: { terminalProfiles: "default" } },
{ daemon: { agentProfiles: "default" } },
{ pluginsEnabled: "yes" },
{ plugins: [] },
{ agents: { metadataGeneration: { providers: [{ provider: "" }] } } },
{ unknownSetting: true },
])("editors still reject invalid configuration: %j", (config) => {
expect(PersistedConfigSchema.safeParse(config).success).toBe(false);
const validate = new Ajv({ allErrors: true, strict: false }).compile(publishedSchema);
expect(validate(config)).toBe(false);
});
+22 -4
View File
@@ -237,7 +237,12 @@ export const PersistedConfigSchema = z
listen: z.string().optional(), listen: z.string().optional(),
hostnames: z.union([z.literal(true), z.array(z.string())]).optional(), hostnames: z.union([z.literal(true), z.array(z.string())]).optional(),
allowedHosts: z.union([z.literal(true), z.array(z.string())]).optional(), allowedHosts: z.union([z.literal(true), z.array(z.string())]).optional(),
trustedProxies: z.union([z.literal(true), z.array(z.string())]).optional(), trustedProxies: z
.union([z.literal(true), z.array(z.string())])
.optional()
.describe(
'Express trusted proxy setting for X-Forwarded-* headers. Defaults to ["loopback"]. Use true only when the final trusted proxy overwrites client-supplied forwarded headers.',
),
mcp: z mcp: z
.object({ .object({
enabled: z.boolean().optional(), enabled: z.boolean().optional(),
@@ -284,9 +289,22 @@ export const PersistedConfigSchema = z
// COMPAT(serviceProxyEnabled): added 2026-06-02, remove after 2026-12-02. // COMPAT(serviceProxyEnabled): added 2026-06-02, remove after 2026-12-02.
// Parsed only to suppress optional public/listen layers for old configs; // Parsed only to suppress optional public/listen layers for old configs;
// localhost service proxying remains always enabled. // localhost service proxying remains always enabled.
enabled: z.boolean().optional(), enabled: z
listen: z.string().optional(), .boolean()
publicBaseUrl: z.url().optional(), .optional()
.describe(
"Compatibility shim. false suppresses optional service proxy listen/public layers only; localhost proxying remains enabled.",
),
listen: z
.string()
.optional()
.describe(
"Optional service-only listener address. Presence enables the standalone service proxy listener.",
),
publicBaseUrl: z
.url()
.optional()
.describe("Optional public base URL for service host aliases."),
}) })
.strict() .strict()
.optional(), .optional(),
File diff suppressed because it is too large Load Diff
@@ -45,6 +45,8 @@ async function logins(options: Parameters<typeof discover>[1]) {
async function database(path: string) { async function database(path: string) {
await mkdir((await import("node:path")).dirname(path), { recursive: true }); await mkdir((await import("node:path")).dirname(path), { recursive: true });
const db = new DatabaseSync(path); const db = new DatabaseSync(path);
// Create the fixture with one durable commit instead of one per row.
db.exec("BEGIN");
db.exec( db.exec(
"CREATE TABLE auth_credentials (id INTEGER PRIMARY KEY, provider TEXT, credential_type TEXT, data TEXT, disabled_cause TEXT)", "CREATE TABLE auth_credentials (id INTEGER PRIMARY KEY, provider TEXT, credential_type TEXT, data TEXT, disabled_cause TEXT)",
); );
@@ -78,6 +80,7 @@ async function database(path: string) {
null, null,
); );
insert.run(5, "anthropic", "oauth", "invalid", null); insert.run(5, "anthropic", "oauth", "invalid", null);
db.exec("COMMIT");
db.close(); db.close();
} }
@@ -39,6 +39,8 @@ async function database(path: string) {
await mkdir((await import("node:path")).dirname(path), { recursive: true }); await mkdir((await import("node:path")).dirname(path), { recursive: true });
const { DatabaseSync } = await import("node:sqlite"); const { DatabaseSync } = await import("node:sqlite");
const db = new DatabaseSync(path); const db = new DatabaseSync(path);
// Create the fixture with one durable commit instead of one per row.
db.exec("BEGIN");
db.exec( db.exec(
"CREATE TABLE auth_credentials (id INTEGER PRIMARY KEY, provider TEXT, credential_type TEXT, data TEXT, disabled_cause TEXT)", "CREATE TABLE auth_credentials (id INTEGER PRIMARY KEY, provider TEXT, credential_type TEXT, data TEXT, disabled_cause TEXT)",
); );
@@ -72,6 +74,7 @@ async function database(path: string) {
null, null,
); );
insert.run(5, "openai-codex", "oauth", "invalid", null); insert.run(5, "openai-codex", "oauth", "invalid", null);
db.exec("COMMIT");
db.close(); db.close();
} }