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
@@ -139,9 +139,8 @@ describe('connection node half', () => {
'credentials.describe', 'credentials.set', 'credentials.unset',
'llm.discoverModels',
// A composition names the plugins a session runs: reading one is
// reconnaissance, writing one is arbitrary capability, and selecting one
// can move a session onto a preset that edits the live runtime.
'agentPreset.select', 'agentPreset.read', 'agentPreset.write', 'agentPreset.remove',
// reconnaissance and writing one is arbitrary capability.
'agentPreset.read', 'agentPreset.write', 'agentPreset.remove',
]) {
const denied = fakeResponse()
await routes[0]!.handler(
@@ -230,7 +229,7 @@ describe('connection node half over a real HTTP server', () => {
// Carries a draft credential and turns the host into a fetcher for a
// URL the caller picked: an anonymous LAN caller must not reach it.
'llm.discoverModels',
'agentPreset.select', 'agentPreset.read', 'agentPreset.write', 'agentPreset.remove',
'agentPreset.read', 'agentPreset.write', 'agentPreset.remove',
]) {
expect([method, await call(port, method, 'harness.example')]).toEqual([method, 403])
}
@@ -238,8 +237,11 @@ describe('connection node half over a real HTTP server', () => {
// client's model picker needs it, and it carries no key or endpoint
// state (404 is the empty proxy's carrier answer — the fence passed).
// `agentPreset.list` joins the model catalog for the same reason: ids and
// trust only, and a LAN client's preset picker needs it.
for (const method of ['llm.providers', 'llm.models', 'agentPreset.list']) {
// trust only, and a LAN client's preset picker needs it. `select` is
// reachable too: `session.create` already takes an `agentPreset`, and the
// deployment's own default already carries bash, so pinning the switch
// would be a fence beside an open gate.
for (const method of ['llm.providers', 'llm.models', 'agentPreset.list', 'agentPreset.select']) {
expect([method, await call(port, method, 'harness.example')]).toEqual([method, 404])
}
// Loopback reaches everything, configuration included.