Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/main/kun-process.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1160,6 +1160,7 @@ describe('syncGuiManagedKunConfig', () => {
}
},
serve: {
runtimeToken: 'keep-this-token',
legacyServeFlag: true,
tokenEconomy: {
customTokenEconomyFlag: 'keep',
Expand Down Expand Up @@ -1248,6 +1249,7 @@ describe('syncGuiManagedKunConfig', () => {
expect(KunConfigSchema.safeParse(parsed).success).toBe(true)
expect(parsed.legacyTopLevelFlag).toBeUndefined()
expect(parsed.serve.legacyServeFlag).toBeUndefined()
expect(parsed.serve.runtimeToken).toBe('keep-this-token')
expect(parsed.serve.storage).toMatchObject({
backend: 'hybrid',
sqlitePath: '/tmp/kun-index.sqlite3'
Expand Down
44 changes: 42 additions & 2 deletions src/main/runtime/kun-runtime-config-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,8 +323,48 @@ function sanitizeCapabilities(value: unknown): Record<string, unknown> {
}

function parseKunConfigSection(schema: SafeParseSchema, value: unknown): Record<string, unknown> {
const parsed = schema.safeParse(objectValue(value))
return parsed.success ? objectValue(parsed.data) : {}
const raw = objectValue(value)
const parsed = schema.safeParse(raw)
if (parsed.success) return objectValue(parsed.data)

// Older GUI/runtime versions may leave a newly introduced top-level key in
// the config. A strict schema should reject that key, but dropping the
// entire section also discards valid credentials and endpoint settings.
// Retry with only keys known by the object schema; invalid known values are
// still rejected by the normal schema and therefore fail closed.
const shape = schemaShape(schema)
if (!shape) return {}
const known = Object.fromEntries(Object.keys(shape).flatMap((key) =>
key in raw ? [[key, raw[key]] as const] : []
))
const sanitized = schema.safeParse(known)
if (sanitized.success) return objectValue(sanitized.data)

// A known nested object may itself contain an obsolete key. Validate each
// top-level value independently so one stale nested option cannot discard
// unrelated credentials or endpoint fields from the section.
const validEntries = Object.entries(known).flatMap(([key, entry]) => {
const parsedEntry = schema.safeParse({ [key]: entry })
return parsedEntry.success ? [[key, objectValue(parsedEntry.data)[key]] as const] : []
})
return Object.fromEntries(validEntries)
}

function schemaShape(schema: SafeParseSchema): Record<string, unknown> | undefined {
const candidate = schema as SafeParseSchema & {
shape?: unknown
def?: unknown
_def?: unknown
}
for (const container of [candidate, candidate.def, candidate._def]) {
if (!container || typeof container !== 'object') continue
const rawShape = 'shape' in container ? (container as { shape?: unknown }).shape : undefined
const shape = typeof rawShape === 'function' ? rawShape() : rawShape
if (shape && typeof shape === 'object' && !Array.isArray(shape)) {
return shape as Record<string, unknown>
}
}
return undefined
}

function parseKunHooksSection(value: unknown): unknown[] {
Expand Down
5 changes: 4 additions & 1 deletion src/main/services/skill-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import {
COMMON_WORKSPACE_SKILL_DIRS,
type CommonSkillDir
} from '../../shared/skill-dirs'
import { expandHomePath } from './workspace-service'
// Keep the skill catalog usable from Node-only migration/config tests. The
// workspace barrel also exports Electron-backed editors and files, which
// unnecessarily requires the Electron binary during CI.
import { expandHomePath } from './workspace-paths'

export type GuiSkillScope = 'project' | 'global'

Expand Down
Loading