fix(agent-presets,connection): reclaim a deleted default, unpin a fence beside an open gate

Deleting the preset a user default names left the setting pointed at an id
nothing will ever supply again, and every session created without an explicit
pick then failed to start — the delete dialog called it 'new sessions cannot
select it', which understates a hard creation error. `remove` now clears the
user layer when it named the preset just deleted, exposing the deployment's own
default underneath. Storing a default that does not exist YET stays deliberate:
the roster is a live directory, so a name absent now may exist by the time a
session asks, and `resolve` still reports that case.

`agentPreset.select` also leaves the loopback set. It was pinned as a real
escalation — one preset mounts the toolset that edits the live runtime — but
`session.create` already takes an `agentPreset`, so pinning only the switch left
the same capability one method over. The deeper reason is that the capability is
not the preset's to grant: the deployment's own default already carries `bash`
and the filesystem tools, so any caller that may start a session at all can
already run commands as this process. `read`/`write`/`remove` stay pinned on
their own footing — those touch files, not sessions.
This commit is contained in:
Yichen Jiang
2026-08-08 11:41:47 +08:00
parent 68389b2a89
commit 5cf5e0cb76
17 changed files with 102 additions and 34 deletions
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/preset/agent-presets/README.md
README.md: c61a84730a273052eeab31dc24696a7340621d4f
README.zh.md: 1caacec4f93990865e82c4498e64578eb149a3d6
README.md: cc019e79673c8441feaeb0509e0f333fcce17252
README.zh.md: 48950cc001811abd7cfcb2eac34e923f64886514
+1 -1
View File
@@ -18,7 +18,7 @@ Discovery is unmemoized: `list()` and `resolve()` re-read the roots on every cal
- `ctx.agentPresets.authorable: boolean` Whether any configured root has `user` trust, and therefore whether a preset can be written at all.
- `ctx.agentPresets.read(id): Promise<string>` One preset's composition text, exactly as stored.
- `ctx.agentPresets.write(id, content): Promise<void>` Create or replace a locally authored preset.
- `ctx.agentPresets.remove(id): Promise<void>` Delete a locally authored preset.
- `ctx.agentPresets.remove(id): Promise<void>` Delete a locally authored preset. Clears the user default when it named the preset just deleted: storing a default that does not exist yet is deliberate, but one this call removed will never be supplied again and would fail every session created without an explicit pick.
`AgentPreset` carries `id` (the directory name), `trust` (`system` or `user`, from the root it was found under), and `path` (the absolute composition file).
+1 -1
View File
@@ -18,7 +18,7 @@
- `ctx.agentPresets.authorable: boolean` 是否存在 `user` 信任级别的根目录,也即是否可能写入 preset。
- `ctx.agentPresets.read(id): Promise<string>` 某个 preset 的组装文本,与存储内容完全一致。
- `ctx.agentPresets.write(id, content): Promise<void>` 创建或替换一个本地创作的 preset。
- `ctx.agentPresets.remove(id): Promise<void>` 删除一个本地创作的 preset。
- `ctx.agentPresets.remove(id): Promise<void>` 删除一个本地创作的 preset。若用户默认值正指向刚被删除的这一个,则清除它:存下一个尚不存在的默认值是有意为之,但本次调用删掉的那个再也不会有人提供,留着它会让每个未显式指名的会话都无法开启。
`AgentPreset` 携带 `id`(目录名)、`trust`(`system` 或 `user`,取自它所在的根目录)以及 `path`(组装文件的绝对路径)。
+23 -2
View File
@@ -13,7 +13,7 @@
import { Context, Service } from 'cordis'
import { scopeOf } from '@deepseek-ai/dsh-scope'
import z from 'schemastery'
import { settingsNamespace, type SettingsScope } from '@deepseek-ai/dsh-settings'
import { settingsNamespace, type SettingsScope, type default as SettingsService } from '@deepseek-ai/dsh-settings'
import { discoverPresets } from './discovery.ts'
import { deleteComposition, readComposition, writeComposition } from './authoring.ts'
import type { PresetMetadata } from './metadata.ts'
@@ -83,6 +83,12 @@ export class AgentPresets extends Service {
*/
private settings: SettingsScope<AgentPresetSettings> | undefined
/**
* The settings service behind {@link settings}, held for the one write this
* service makes: clearing a user default it has just deleted.
*/
private settingsService: SettingsService | undefined
constructor(ctx: Context, public config: Config) {
super(ctx, 'agentPresets')
// Deliberately not `installSettingsSection`: that helper exists to re-judge
@@ -96,7 +102,11 @@ export class AgentPresets extends Service {
AgentPresetSettingsSchema,
{ base: { default: config.default } },
)
settingsCtx.effect(() => () => { this.settings = undefined }, 'agentPresets.settings()')
this.settingsService = settingsCtx.settings
settingsCtx.effect(() => () => {
this.settings = undefined
this.settingsService = undefined
}, 'agentPresets.settings()')
})
}
@@ -196,6 +206,17 @@ export class AgentPresets extends Service {
*/
async remove(id: string): Promise<void> {
await deleteComposition(this.config.roots, await this.resolve(id))
// Storing a default that does not exist YET is deliberate — the roster is a
// live directory, so a name absent now may exist by the time a session asks
// for it, and `resolve` reports it then. A default this call just deleted is
// not that case: nothing will ever supply it again, and left in place every
// session created without an explicit pick would fail to start. Clearing it
// exposes the deployment's own default underneath, which is the layering.
if (this.settings?.get().default !== id) return
await this.settingsService?.mutate(
settingsNamespace(SETTINGS_NAMESPACE),
[{ op: 'unset', path: ['default'] }],
)
}
/**
@@ -4,7 +4,7 @@
* so a person can change which preset new sessions get without a restart.
*/
import { mkdtemp, writeFile } from 'node:fs/promises'
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
@@ -20,7 +20,7 @@ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import SettingsLocal from '@deepseek-ai/dsh-settings-local'
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
import { describe, expect, it } from 'vitest'
import AgentPresets, { SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-presets'
import AgentPresets, { COMPOSITION_FILE, SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-presets'
const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), 'fixtures')
const ROOTS = [{ path: join(FIXTURES, 'system'), trust: 'system' as const }]
@@ -30,7 +30,9 @@ const NS = settingsNamespace(SETTINGS_NAMESPACE)
* A composition with a real file-backed settings provider. `settingsFiber` is
* the provider's own handle, so a test can take it away the way a reload does.
*/
async function harness(): Promise<{ ctx: Context; settingsFile: string; settingsFiber: { dispose: () => unknown } }> {
async function harness(
extraRoots: readonly { path: string; trust: 'system' | 'user' }[] = [],
): Promise<{ ctx: Context; settingsFile: string; settingsFiber: { dispose: () => unknown } }> {
const home = await mkdtemp(join(tmpdir(), 'dsh-preset-settings-'))
const settingsFile = join(home, 'settings.yaml')
await writeFile(settingsFile, '{}\n')
@@ -47,7 +49,7 @@ async function harness(): Promise<{ ctx: Context; settingsFile: string; settings
await ctx.plugin(AgentLoop, { agents: [] })
const settingsFiber = ctx.plugin(SettingsLocal, { path: settingsFile, watch: false })
await settingsFiber
await ctx.plugin(AgentPresets, { default: 'standard', roots: ROOTS })
await ctx.plugin(AgentPresets, { default: 'standard', roots: [...ROOTS, ...extraRoots] })
return { ctx, settingsFile, settingsFiber }
}
@@ -114,6 +116,26 @@ describe('the default preset as a user setting', () => {
expect(ctx.agentPresets.defaultId).toBe('standard')
})
it('clears a user default it has just deleted', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-preset-authored-'))
await mkdir(join(root, 'mine'))
await writeFile(
join(root, 'mine', COMPOSITION_FILE),
`- id: only\n name: ${join(FIXTURES, 'plugins', 'contribute.js')}\n config:\n tool: only\n`,
)
const { ctx } = await harness([{ path: root, trust: 'user' as const }])
await ctx.settings.update(NS, { default: 'mine' })
expect(ctx.agentPresets.defaultId).toBe('mine')
await ctx.agentPresets.remove('mine')
// Nothing will ever supply that id again, so leaving the setting pointed at
// it would fail every session created without an explicit pick. Clearing it
// exposes the deployment's own default underneath.
expect(ctx.agentPresets.defaultId).toBe('standard')
expect((await ctx.agentPresets.resolve()).id).toBe('standard')
})
it('reports an unknown user default only when a session tries to use it', async () => {
const { ctx } = await harness()