diff --git a/apps/web/src/app/config.json/extras.ts b/apps/web/src/app/config.json/extras.ts index aeb098728d..5eec0a7c71 100644 --- a/apps/web/src/app/config.json/extras.ts +++ b/apps/web/src/app/config.json/extras.ts @@ -15,6 +15,7 @@ * 3. Paste that shape into the right bucket below: * - Top-level: `top` * - New primary agent: `agents` + * - Permission key: `permissions` * - Under `experimental`: `experimental` * - Anywhere else nested: add a new bucket here AND extend the merge * logic in `./route.ts` to overlay that section. @@ -92,6 +93,11 @@ export const kiloExtras = { debug: agentConfig, orchestrator: agentConfig, }, + permissions: { + notebook_read: { $ref: '#/$defs/PermissionRuleConfig' }, + notebook_edit: { $ref: '#/$defs/PermissionRuleConfig' }, + notebook_execute: { $ref: '#/$defs/PermissionRuleConfig' }, + }, experimental: { codebase_search: { description: 'Enable AI-powered codebase search', diff --git a/apps/web/src/app/config.json/route.ts b/apps/web/src/app/config.json/route.ts index 8651577275..3630945b6b 100644 --- a/apps/web/src/app/config.json/route.ts +++ b/apps/web/src/app/config.json/route.ts @@ -30,6 +30,21 @@ export function merge(schema: Schema): Schema { }; properties.agent = agent; + const defs = isObject(schema.$defs) ? { ...schema.$defs } : undefined; + if (defs && isObject(defs.PermissionConfig)) { + const permissionConfig = { ...defs.PermissionConfig }; + if (Array.isArray(permissionConfig.anyOf)) { + permissionConfig.anyOf = permissionConfig.anyOf.map(variant => { + if (!isObject(variant) || !isObject(variant.properties)) return variant; + return { + ...variant, + properties: { ...variant.properties, ...kiloExtras.permissions }, + }; + }); + defs.PermissionConfig = permissionConfig; + } + } + const experimental = isObject(properties.experimental) ? { ...properties.experimental } : {}; experimental.properties = { ...(isObject(experimental.properties) ? experimental.properties : {}), @@ -37,7 +52,7 @@ export function merge(schema: Schema): Schema { }; properties.experimental = experimental; - return { ...schema, properties }; + return { ...schema, ...(defs ? { $defs: defs } : {}), properties }; } export async function GET() { diff --git a/apps/web/src/tests/cli-config-schema.test.ts b/apps/web/src/tests/cli-config-schema.test.ts index 10ac0be9ee..2ecfa79061 100644 --- a/apps/web/src/tests/cli-config-schema.test.ts +++ b/apps/web/src/tests/cli-config-schema.test.ts @@ -4,6 +4,20 @@ const upstream: Schema = { $schema: 'https://json-schema.org/draft/2020-12/schema', ref: 'Config', type: 'object', + $defs: { + PermissionConfig: { + anyOf: [ + { $ref: '#/$defs/PermissionActionConfig' }, + { + type: 'object', + properties: { + read: { $ref: '#/$defs/PermissionRuleConfig' }, + }, + additionalProperties: { $ref: '#/$defs/PermissionRuleConfig' }, + }, + ], + }, + }, properties: { agent: { type: 'object', @@ -75,6 +89,25 @@ describe('kilo config.json schema merge', () => { expect(agent.properties.build).toBeDefined(); // upstream key preserved }); + test('adds notebook permission keys without dropping upstream', () => { + const defs = out.$defs as Record; + const permissionConfig = defs.PermissionConfig as { anyOf: Array> }; + const permissionObject = permissionConfig.anyOf.find(variant => variant.type === 'object') as { + properties: Record; + }; + + expect(permissionObject.properties.notebook_read).toEqual({ + $ref: '#/$defs/PermissionRuleConfig', + }); + expect(permissionObject.properties.notebook_edit).toEqual({ + $ref: '#/$defs/PermissionRuleConfig', + }); + expect(permissionObject.properties.notebook_execute).toEqual({ + $ref: '#/$defs/PermissionRuleConfig', + }); + expect(permissionObject.properties.read).toBeDefined(); + }); + test('adds kilo experimental keys without dropping upstream', () => { const exp = props.experimental as { properties: Record }; expect(exp.properties.codebase_search).toBeDefined();