返回 CodeWhale
agent-presets.ts
根目录 / crates / tui / extension-host / src / dsh / agent-presets.ts
1 import { PERSONA, normalizePersonaConfig } from './persona.ts'
2 /** Raw DSH roster preparation, over the existing reviewed file/entry receipt.
3 * Discovery borrows the pinned package's algorithms. Mounts use the one Loader;
4 * Rust alone selects caller/AgentProfile ancestry and writes session/settings.
5 */
6 import { Service } from '@deepseek-ai/cordis'
7 import { createHash } from 'node:crypto'
8 import { resolve, relative, dirname, sep } from 'node:path'
9 import { fileURLToPath, pathToFileURL } from 'node:url'
10 import { discoverPresets, type DiscoveryIO } from './upstream/agent-presets/discovery.ts'
11 import { PRESET_ID, type AgentPreset, type PresetRoot } from './upstream/agent-presets/preset.ts'
12 import { classifyRowSpecifier } from './upstream/agent-presets/specifier.ts'
13 import { fileComposition, mountedCompositionRows, type AgentPresetCompositionRow } from './upstream/agent-presets/composition-inventory.ts'
14 import {isJsExpr} from './upstream/loader/src/index.ts'
15 import type { ReviewedComposition, ReviewedModule, CompositionMount } from './composition.ts'
16
17 export const AGENT_PRESETS = '@deepseek-ai/dsh-agent-presets'
18 const MAX_PRESETS = 64
19 const hash = (bytes: string) => createHash('sha256').update(bytes).digest('hex')
20 const SHA = /^[a-f0-9]{64}$/
21 function plain(path: string): boolean {
22 return typeof path === 'string' && path.length > 0 && path.length <= 4096 && !/[\\:\u0000-\u001f\u007f]/u.test(path)
23 && !path.startsWith('/') && path.split('/').every(p => p !== '' && p !== '.' && p !== '..')
24 }
25 function fail(message: string): never { throw new Error(`agent-presets: ${message}`) }
26
27 /** Exact package export lookup inside the admitted inventory, no upward walk. */
28 export function containedPackageModule(name: string, documents: ReadonlyMap<string, string>, files: Readonly<Record<string, string>>): ReviewedModule | undefined {
29 if (!name || /[\\:\u0000-\u0020\u007f]/u.test(name)) return
30 const parts = name.split('/')
31 const n = name.startsWith('@') ? 2 : 1
32 if (parts.length < n || parts.some(p => !p || p === '.' || p === '..')) return
33 const pkg = parts.slice(0, n).join('/')
34 const packageRoot = `node_modules/${pkg}`
35 const source = documents.get(`${packageRoot}/package.json`)
36 if (source === undefined) return
37 let data: any
38 try { data = JSON.parse(source) } catch { return }
39 if (data.name !== pkg || data.type !== 'module') return
40 const subpath = parts.length === n ? '.' : `./${parts.slice(n).join('/')}`
41 const refused=Symbol('unsupported package target')
42 function target(value: any): string | undefined | typeof refused {
43 if (typeof value === 'string') return value
44 if (!value || Array.isArray(value) || typeof value !== 'object') return refused
45 for (const [condition, child] of Object.entries(value)) {
46 if (['node', 'import', 'default'].includes(condition)) { const picked = target(child); if (picked !== undefined) return picked }
47 }
48 }
49 let entry: string | undefined | typeof refused
50 if (data.exports !== undefined) {
51 const value = data.exports
52 entry = value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).some(k => k.startsWith('.'))
53 ? target(value[subpath]) : subpath === '.' ? target(value) : undefined
54 } else if (subpath === '.') entry = typeof data.main === 'string' ? data.main : './index.js'
55 else entry = subpath
56 if (typeof entry!=='string' || !entry.startsWith('./') || !plain(entry.slice(2))) return
57 const path = `${packageRoot}/${entry.slice(2)}`
58 if (!/\.(mjs|js|mts)$/.test(path) || !SHA.test(files[path] ?? '')) return
59 return { name, path, sha256: files[path] }
60 }
61
62 export interface PresetCatalogRow {
63 id: string
64 trust: 'system' | 'user'
65 name?: string
66 description?: string
67 order?: number
68 broken?: string
69 is_default: boolean
70 entry?: { path: string; sha256: string }
71 }
72 export interface PresetCatalog {
73 version: 1
74 default?: string
75 mode_selection_enabled: boolean
76 presets: PresetCatalogRow[]
77 /** Pure file projection; conditional gates stay conditional until mounted. */
78 inventory: Record<string, {path: string; rows: AgentPresetCompositionRow[]} | {path: string; broken: string}>
79 }
80 export interface PreparedPreset {
81 metadata: PresetCatalogRow
82 composition: ReviewedComposition
83 /** Original source for read/inventory; never written by the host. */
84 source: string
85 }
86 export interface PresetReviewInput {
87 kind: 'agent-presets'
88 composition: ReviewedComposition
89 /** Exact documents only; other module bytes remain in composition.files. */
90 documents: { path: string; sha256: string; source: string }[]
91 directories: string[]
92 }
93 export interface PresetReview {
94 catalog: PresetCatalog
95 presets: PreparedPreset[]
96 }
97
98 /** Nonexecuting discovery: no FS calls, home expansion or ambient package lookup. */
99 export async function reviewAgentPresets(input: PresetReviewInput): Promise<PresetReview> {
100 const { composition } = input
101 const files = composition.files
102 if (!files || Object.keys(files).length > 4096 || !Array.isArray(input.directories) || input.directories.length > 4096) fail('invalid source inventory')
103 const base = resolve('codewhale-reviewed-agent-presets')
104 const baseUrl = pathToFileURL(`${base}${sep}`).href
105 function key(path: string): string {
106 const inside = relative(base, path).split(sep).join('/')
107 if (!plain(inside)) fail('path escapes the admitted source root')
108 return inside
109 }
110 const documents = new Map<string, string>()
111 let bytes = 0
112 for (const doc of input.documents) {
113 if (!plain(doc.path) || !SHA.test(doc.sha256) || files[doc.path] !== doc.sha256 || hash(doc.source) !== doc.sha256 || documents.has(doc.path)) fail('document changed or is outside the source receipt')
114 bytes += Buffer.byteLength(doc.source)
115 if (Buffer.byteLength(doc.source) > 1024 * 1024 || bytes > 4 * 1024 * 1024) fail('documents exceed their bound')
116 documents.set(doc.path, doc.source)
117 }
118 const directories = new Set(input.directories)
119 for (const path of directories) if (!plain(path)) fail('invalid source directory')
120 for (const path of Object.keys(files)) if (!plain(path) || !SHA.test(files[path])) fail('invalid source file receipt')
121 const invalid = new Set<string>()
122 const parsed = new Map<string, any[]>()
123 const io: DiscoveryIO = {
124 async readFile(path) { const relative = key(path); if (invalid.has(relative)) throw new Error('composition exceeds its data/shape bound'); const text = documents.get(relative); if (text === undefined) throw new Error('document not admitted'); return text },
125 async stat(path) { return { isFile: () => Object.hasOwn(files, key(path)) } },
126 async readdir(path) {
127 const prefix = `${key(path)}/`
128 if (!directories.has(prefix.slice(0, -1))) { const e = new Error('missing root'); Object.assign(e, { code: 'ENOENT' }); throw e }
129 return [...directories].filter(p => p.startsWith(prefix) && !p.slice(prefix.length).includes('/'))
130 .map(p => ({ name: p.slice(prefix.length), isDirectory: () => true }))
131 },
132 }
133 // Import here avoids an initialization cycle; reviewer never imports a row.
134 const { reviewComposition, parsePresetComposition, compositionRowProblem, hasReviewedRowBridge } = await import('./composition.ts')
135 for (const [path, source] of documents) {
136 if (!path.endsWith('/agent.cordis.yml')) continue
137 try { parsed.set(path, parsePresetComposition(source)) } catch { invalid.add(path) }
138 }
139 const top = reviewComposition(composition)
140 const rosterRows: any[] = []
141 function visit(rows: any[]) {
142 for (const row of rows) {
143 if (row.group === true || row.name === 'cordis:group' || row.name === '@deepseek-ai/cordis-plugin-group') visit(row.config ?? [])
144 else if (row.name === AGENT_PRESETS) rosterRows.push(row)
145 }
146 }
147 visit(top.entries)
148 if (rosterRows.length !== 1) fail('exactly one raw roster row is required')
149 const config = rosterRows[0].config ?? {}
150 if (!config || Array.isArray(config) || typeof config !== 'object' || Object.keys(config).some(k => !['default', 'roots', 'includeShippedRoot', 'includeUserRoot'].includes(k))) fail('roster configuration must be literal upstream fields')
151 for (const k of ['includeShippedRoot', 'includeUserRoot']) if (config[k] !== undefined && typeof config[k] !== 'boolean') fail('root gates must be literal booleans')
152 if (config.default !== undefined && (typeof config.default !== 'string' || !PRESET_ID.test(config.default) || config.default.length > 64)) fail('invalid default id')
153 const roots: PresetRoot[] = []
154 if (config.includeShippedRoot !== false) {
155 const candidates = [...documents.entries()].filter(([p, text]) => {
156 if (p !== 'package.json' && !p.endsWith('/package.json')) return false
157 try { return JSON.parse(text).name === AGENT_PRESETS } catch { return false }
158 })
159 if (candidates.length !== 1) fail('shipped root requires exactly one admitted agent-presets package')
160 const folder = dirname(candidates[0][0]).split(sep).join('/')
161 const root = folder === '.' ? 'presets' : `${folder}/presets`
162 if (!directories.has(root)) fail('shipped preset root was not admitted')
163 roots.push({ path: resolve(base, root), trust: 'system' })
164 }
165 if (config.roots !== undefined && !Array.isArray(config.roots)) fail('roots must be a literal list')
166 for (const root of config.roots ?? []) {
167 if (!root || Object.keys(root).some(k => !['path', 'trust'].includes(k)) || !plain(root.path) || !['system', 'user'].includes(root.trust)) fail('configured roots must stay inside the admitted package')
168 if (!directories.has(root.path)) fail('configured preset root was not admitted')
169 roots.push({ path: resolve(base, root.path), trust: root.trust })
170 }
171 if (config.includeUserRoot !== false) {
172 if (!directories.has('.agent-presets')) fail('user root requires an admitted .agent-presets tree; ambient home discovery is refused')
173 roots.push({ path: resolve(base, '.agent-presets'), trust: 'user' })
174 }
175 if (roots.length > 64) fail('too many roots')
176 const declared = new Map(composition.modules.map(m => [m.name, m]))
177 if (declared.size !== composition.modules.length) fail('duplicate module name receipt')
178 const found = await discoverPresets(roots, baseUrl, name => hasReviewedRowBridge(name) || declared.has(name) || containedPackageModule(name, documents, files) !== undefined, io)
179 if (found.length > MAX_PRESETS) fail('too many presets')
180 const prepared: PreparedPreset[] = []
181 const rows: PresetCatalogRow[] = []
182 const inventory: PresetCatalog['inventory'] = Object.create(null)
183 for (const preset of found) {
184 if (preset.id.length > 64 || (preset.name?.length ?? 0) > 1024 || (preset.description?.length ?? 0) > 4096) fail('preset metadata exceeds its bound')
185 const metadata: PresetCatalogRow = { id: preset.id, trust: preset.trust, is_default: preset.id === config.default,
186 ...(preset.name === undefined ? {} : { name: preset.name }), ...(preset.description === undefined ? {} : { description: preset.description }),
187 ...(preset.order === undefined ? {} : { order: preset.order }), ...(preset.broken === undefined ? {} : { broken: preset.broken }) }
188 rows.push(metadata)
189 const path = key(preset.path)
190 inventory[preset.id] = {path, ...(preset.broken !== undefined ? {broken:preset.broken} : await fileComposition(preset.path, () => { throw new Error('conditional until mounted') }, io.readFile))}
191 if (preset.broken !== undefined) continue
192 const source = await io.readFile(preset.path, 'utf8')
193 const entries = parsed.get(key(preset.path))!
194 const modules = new Map<string, ReviewedModule>()
195 function resolveRows(children: any[]) {
196 for (const row of children) {
197 if (Boolean(row.disabled) && !isJsExpr(row.disabled)) continue
198 if (row.group === true || row.name === 'cordis:group' || row.name === '@deepseek-ai/cordis-plugin-group') { resolveRows(row.config ?? []); continue }
199 const specifier = classifyRowSpecifier(row.name)
200 if (row.name === PERSONA) normalizePersonaConfig(row.config)
201 if (specifier.kind === 'builtin' || hasReviewedRowBridge(row.name)) continue
202 if (row.name === AGENT_PRESETS) fail('nested roster cannot create a second selection authority')
203 const problem=compositionRowProblem(row.name);if(problem) fail(problem)
204 let receipt: ReviewedModule | undefined
205 if (specifier.kind === 'package') receipt = declared.get(row.name) ?? containedPackageModule(row.name, documents, files)
206 else {
207 const url = specifier.kind === 'file' ? new URL(specifier.specifier) : new URL(specifier.specifier, pathToFileURL(preset.path).href)
208 const path = key(fileURLToPath(url))
209 if (files[path] !== undefined) receipt = { name: row.name, path, sha256: files[path] }
210 }
211 if (!receipt || files[receipt.path] !== receipt.sha256) { if (isJsExpr(row.disabled)) continue; fail('row module has no exact admitted receipt') }
212 modules.set(row.name, receipt)
213 }
214 }
215 try { resolveRows(entries) } catch (error) { metadata.broken = error instanceof Error ? error.message : 'preset preparation refused'; continue }
216 const layer = JSON.stringify([{ insert: entries }])
217 const spec: ReviewedComposition = { version: 1, layers: [{ path: key(preset.path), sha256: hash(layer), source: layer }], modules: [...modules.values()], files }
218 reviewComposition(spec)
219 prepared.push({ metadata, composition: spec, source })
220 }
221 if (config.default !== undefined && !prepared.some(p => p.metadata.id === config.default)) fail('default preset is missing or broken')
222 if (!prepared.length) fail('no admitted usable preset')
223 if (Buffer.byteLength(JSON.stringify(inventory)) > 4 * 1024 * 1024) fail('composition inventory exceeds its bound')
224 return { catalog: { version: 1, ...(config.default === undefined ? {} : {default:config.default}), mode_selection_enabled: true, presets: rows, inventory }, presets: prepared }
225 }
226
227 export interface PresetMountConfig {
228 catalog: PresetCatalog
229 selected: PreparedPreset
230 base_url: string
231 }
232 /** The raw roster row is a scope-local view of Core's captured selected entry.
233 * Its subtree owns the real five contributions. It cannot select/reparent an
234 * Agent, start a turn, write a session, or mutate roots/settings.
235 */
236 export function defineReviewedAgentPresets(mount: (ctx: any, base: string, spec: ReviewedComposition) => Promise<CompositionMount>) {
237 return class ReviewedAgentPresets extends Service {
238 static inject = ['loader']
239 private readonly snapshot: PresetMountConfig
240 private readonly selfCtx: any
241 constructor(ctx: any, config: { codewhale_reviewed?: PresetMountConfig }) {
242 super(ctx, 'agentPresets')
243 this.selfCtx = ctx
244 const snapshot = config?.codewhale_reviewed
245 if (!snapshot || snapshot.catalog?.version !== 1 || !snapshot.catalog.presets.some(p => p.id === snapshot.selected?.metadata.id && !p.broken)) fail('raw roster needs an installer-minted preset receipt')
246 this.snapshot = structuredClone(snapshot)
247 }
248 private mounting?: Promise<CompositionMount>
249 private mounted?: CompositionMount
250 mountSelected() { return this.mounting ??= mount(this.selfCtx, this.snapshot.base_url, this.snapshot.selected.composition).then(result => this.mounted=result) }
251 async list() { return this.snapshot.catalog.presets.map(row => ({...structuredClone(row),path:fileURLToPath(new URL(this.snapshot.catalog.inventory[row.id].path,this.snapshot.base_url))})) }
252 async remoteExportList() { return {presets:this.snapshot.catalog.presets.map(({is_default,entry,...row})=>({...structuredClone(row),isDefault:is_default})),authorable:false,defaultId:this.defaultId,modeSelectionEnabled:this.snapshot.catalog.mode_selection_enabled} }
253 get defaultId() { return this.snapshot.catalog.default }
254 get authorable() { return false }
255 async resolve(id = this.defaultId) {
256 const row = (await this.list()).find(p => p.id === id)
257 if (!row) fail('preset not found')
258 return row
259 }
260 composedPreset() { return this.snapshot.selected.metadata.id }
261 async compositionInventory() {
262 return this.snapshot.catalog.presets.map(row => {
263 const identity={id:row.id,trust:row.trust,...(row.name===undefined?{}:{name:row.name}),isDefault:row.id===this.defaultId}
264 if (row.id===this.composedPreset() && this.mounted) return {...identity,rows:mountedCompositionRows(this.mounted.tree)}
265 const read=this.snapshot.catalog.inventory[row.id]
266 if (row.broken || 'broken' in read) return {...identity,broken:row.broken ?? ('broken' in read?read.broken:undefined),rows:[]}
267 return {...identity,rows:structuredClone(read.rows)}
268 })
269 }
270 async readDocument(id: string) {
271 if (id !== this.composedPreset()) fail('read another preset through Core file authority')
272 return { agentPreset: id, trust: this.snapshot.selected.metadata.trust, content: this.snapshot.selected.source }
273 }
274 select() { fail('select through Core AgentProfile/native preset selection before a turn') }
275 recompose() { fail('Core owns the captured agent selection and blank-session check') }
276 copy() { fail('copy through Core file/install/review/reload authority') }
277 remove() { fail('delete through Core file/install/review/reload authority') }
278 }
279 }
280
280 lines TYPESCRIPT