From 97d3bfa34e461e6b4dfd868bf25e77812919f2de Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Mon, 24 Aug 2026 01:05:11 +0530 Subject: [PATCH 1/3] refactor(config): decode persisted documents --- src/local-agent-config.ts | 13 +++++++---- src/user-config.ts | 48 +++++++++++++++++++++------------------ 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/src/local-agent-config.ts b/src/local-agent-config.ts index 3f1de5aa..538355d9 100644 --- a/src/local-agent-config.ts +++ b/src/local-agent-config.ts @@ -11,7 +11,7 @@ const providerSchema = z.object({ effort: z.string().trim().min(1).optional(), }).strict(); -const subagentsSchema = z.object({ +export const subagentsConfigSchema = z.object({ enabled: z.boolean(), providers: z.array(providerSchema), }).strict().superRefine((value, context) => { @@ -28,9 +28,14 @@ const subagentsSchema = z.object({ } }); +export const storedSubagentsConfigSchema = z.union([ + z.boolean(), + subagentsConfigSchema, +]); + export type SubagentProviderConfig = z.infer; -export type SubagentsConfig = z.infer; -export type StoredSubagentsConfig = boolean | SubagentsConfig; +export type SubagentsConfig = z.infer; +export type StoredSubagentsConfig = z.infer; export function resolveSubagentsConfig( value: unknown, @@ -40,7 +45,7 @@ export function resolveSubagentsConfig( ? { enabled: false, providers: [] } : typeof value === "boolean" ? legacySubagentsConfig(value) - : subagentsSchema.parse(value); + : subagentsConfigSchema.parse(value); return { ...stored, enabled: env.DEVSPACE_SUBAGENTS === undefined diff --git a/src/user-config.ts b/src/user-config.ts index 98d05ac6..4d3e33ed 100644 --- a/src/user-config.ts +++ b/src/user-config.ts @@ -7,26 +7,30 @@ import { } from "node:fs"; import { homedir } from "node:os"; import { join, resolve } from "node:path"; +import * as z from "zod/v4"; import { expandHomePath } from "./roots.js"; -import type { StoredSubagentsConfig } from "./local-agent-config.js"; - -export interface DevspaceUserConfig { - host?: string; - port?: number; - allowedRoots?: string[]; - publicBaseUrl?: string | null; - allowedHosts?: string[]; - stateDir?: string; - worktreeRoot?: string; - artifactsEnabled?: boolean; - artifactMaxFileBytes?: number; - agentDir?: string; - subagents?: StoredSubagentsConfig; -} +import { storedSubagentsConfigSchema } from "./local-agent-config.js"; -export interface DevspaceAuthConfig { - ownerToken?: string; -} +const devspaceUserConfigSchema = z.object({ + host: z.string().optional(), + port: z.number().optional(), + allowedRoots: z.array(z.string()).optional(), + publicBaseUrl: z.string().nullable().optional(), + allowedHosts: z.array(z.string()).optional(), + stateDir: z.string().optional(), + worktreeRoot: z.string().optional(), + artifactsEnabled: z.boolean().optional(), + artifactMaxFileBytes: z.number().optional(), + agentDir: z.string().optional(), + subagents: storedSubagentsConfigSchema.optional(), +}).strict(); + +const devspaceAuthConfigSchema = z.object({ + ownerToken: z.string().optional(), +}).strict(); + +export type DevspaceUserConfig = z.infer; +export type DevspaceAuthConfig = z.infer; export interface DevspaceFiles { dir: string; @@ -71,8 +75,8 @@ export function loadDevspaceFiles(env: NodeJS.ProcessEnv = process.env): Devspac authPath, configExists, authExists, - config: configExists ? readJsonFile(configPath) : {}, - auth: authExists ? readJsonFile(authPath) : {}, + config: configExists ? readJsonFile(configPath, devspaceUserConfigSchema) : {}, + auth: authExists ? readJsonFile(authPath, devspaceAuthConfigSchema) : {}, }; } @@ -100,9 +104,9 @@ export function generateOwnerToken(): string { return randomBytes(32).toString("base64url"); } -function readJsonFile(filePath: string): T { +function readJsonFile(filePath: string, schema: z.ZodType): T { try { - return JSON.parse(readFileSync(filePath, "utf8")) as T; + return schema.parse(JSON.parse(readFileSync(filePath, "utf8")) as unknown); } catch (error) { const reason = error instanceof Error ? error.message : String(error); throw new Error(`Unable to read ${filePath}: ${reason}`); From ea7fea2f5397a7746d02a4c06a41c67b0810b6e4 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Mon, 24 Aug 2026 01:05:11 +0530 Subject: [PATCH 2/3] test(config): reject invalid persisted documents --- package.json | 2 +- src/user-config.test.ts | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 src/user-config.test.ts diff --git a/package.json b/package.json index 388f99e2..723881ae 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "dev": "node scripts/dev-server.mjs", "postinstall": "node scripts/fix-node-pty-permissions.mjs", "start": "node dist/cli.js serve", - "test": "tsx src/config.test.ts && tsx src/onboarding.test.ts && tsx src/cli-workspace.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-config.test.ts && tsx src/local-agent-catalog.test.ts && tsx src/local-agent-presentation.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-daemon-lifecycle.test.ts && tsx src/local-agent-daemon-protocol.test.ts && tsx src/local-agent-daemon.test.ts && tsx src/local-agent-codex.test.ts && tsx src/local-agent-opencode.test.ts && tsx src/local-agent-acp.test.ts && tsx src/local-agent-grok.test.ts && tsx src/local-agent-pi-sandbox.test.ts && tsx src/local-agent-pi.test.ts && tsx src/local-agent-claude.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/local-agent-manager.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts", + "test": "tsx src/user-config.test.ts && tsx src/config.test.ts && tsx src/onboarding.test.ts && tsx src/cli-workspace.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-config.test.ts && tsx src/local-agent-catalog.test.ts && tsx src/local-agent-presentation.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-daemon-lifecycle.test.ts && tsx src/local-agent-daemon-protocol.test.ts && tsx src/local-agent-daemon.test.ts && tsx src/local-agent-codex.test.ts && tsx src/local-agent-opencode.test.ts && tsx src/local-agent-acp.test.ts && tsx src/local-agent-grok.test.ts && tsx src/local-agent-pi-sandbox.test.ts && tsx src/local-agent-pi.test.ts && tsx src/local-agent-claude.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/local-agent-manager.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts", "typecheck": "tsc -p tsconfig.json --noEmit" }, "keywords": [], diff --git a/src/user-config.test.ts b/src/user-config.test.ts new file mode 100644 index 00000000..c058e387 --- /dev/null +++ b/src/user-config.test.ts @@ -0,0 +1,41 @@ +import assert from "node:assert/strict"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { loadDevspaceFiles } from "./user-config.js"; + +const configDir = mkdtempSync(join(tmpdir(), "devspace-user-config-test-")); +const env = { DEVSPACE_CONFIG_DIR: configDir }; + +try { + writeFileSync(join(configDir, "config.json"), JSON.stringify({ + port: 8787, + subagents: { + enabled: true, + providers: [{ id: "codex", enabled: true }], + }, + })); + writeFileSync(join(configDir, "auth.json"), JSON.stringify({ + ownerToken: "test-owner-token", + })); + + assert.deepEqual(loadDevspaceFiles(env).config, { + port: 8787, + subagents: { + enabled: true, + providers: [{ id: "codex", enabled: true }], + }, + }); + assert.equal(loadDevspaceFiles(env).auth.ownerToken, "test-owner-token"); + + writeFileSync(join(configDir, "config.json"), JSON.stringify({ port: "8787" })); + assert.throws(() => loadDevspaceFiles(env), /expected number/i); + + writeFileSync(join(configDir, "config.json"), JSON.stringify({ unknownSetting: true })); + assert.throws(() => loadDevspaceFiles(env), /unrecognized key/i); + + writeFileSync(join(configDir, "config.json"), "{"); + assert.throws(() => loadDevspaceFiles(env), /Unable to read .*config\.json/); +} finally { + rmSync(configDir, { recursive: true, force: true }); +} From 2bfa1512221df8eb119a104a4f0e17b7681ca84b Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Mon, 24 Aug 2026 01:06:11 +0530 Subject: [PATCH 3/3] fix(config): preserve legacy extension fields --- src/user-config.test.ts | 2 +- src/user-config.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/user-config.test.ts b/src/user-config.test.ts index c058e387..f13ca46f 100644 --- a/src/user-config.test.ts +++ b/src/user-config.test.ts @@ -32,7 +32,7 @@ try { assert.throws(() => loadDevspaceFiles(env), /expected number/i); writeFileSync(join(configDir, "config.json"), JSON.stringify({ unknownSetting: true })); - assert.throws(() => loadDevspaceFiles(env), /unrecognized key/i); + assert.equal(loadDevspaceFiles(env).config.unknownSetting, true); writeFileSync(join(configDir, "config.json"), "{"); assert.throws(() => loadDevspaceFiles(env), /Unable to read .*config\.json/); diff --git a/src/user-config.ts b/src/user-config.ts index 4d3e33ed..506b468c 100644 --- a/src/user-config.ts +++ b/src/user-config.ts @@ -23,11 +23,11 @@ const devspaceUserConfigSchema = z.object({ artifactMaxFileBytes: z.number().optional(), agentDir: z.string().optional(), subagents: storedSubagentsConfigSchema.optional(), -}).strict(); +}).passthrough(); const devspaceAuthConfigSchema = z.object({ ownerToken: z.string().optional(), -}).strict(); +}).passthrough(); export type DevspaceUserConfig = z.infer; export type DevspaceAuthConfig = z.infer;