feat: set a conversation's scope from the composer, where the agent cannot reach
Release / release (push) Successful in 6s
Release / release (push) Successful in 6s
The per-session scope the host half now reads had no writer. This is it: a crosshair on the composer tool row, present only in sessions running the facet preset, opening a panel that sets the focus and the paths allowed outside it. The gate is the session's own `agentPreset` projection rather than anything inferred from the URL or from the plugin being installed, so a control for a plugin that is not running never appears. It writes through the settings document — the same one the plugin card edits — because `settings.mutate` accepts a path under a volatile node and refuses anything else. That is what makes the split real rather than aspirational: the agent has no write path to `scopes`, and the panel's write is the only one that can widen a session. The panel also carries the mode details and the usage notes, which the preset picker's guide dialog cannot show for a preset no core package ships — its guide table holds four shipped ids and is looked up only for built-ins. Also adds `@deepseek-ai/dsh-client-ui-conversation` to the client half's inject list: registering into `conversation.input.left` is a dependency on the package that declares it, and `slots.inject` waits for a slot rather than creating one, so naming another package's slot without loading it first registers nothing — silently, because nothing errors.
This commit is contained in:
1 parent
d9caf4eeec
commit
266e90aa2d
4 files changed
+589
-37
No files matched your search
@@ -103,23 +103,22 @@ first turn has run, so choose it when you start the conversation.
|
||||
|
||||
### Configure it in your profile, not in the package
|
||||
|
||||
The host row's config is where this deployment's facts belong — the profile's own
|
||||
`cordis.patch.yml`:
|
||||
The host row's config is where this *deployment's* facts belong — the profile's
|
||||
own `cordis.patch.yml`:
|
||||
|
||||
```yaml
|
||||
- id: facet-policy
|
||||
name: dsh-facet
|
||||
config:
|
||||
mode: ask # ask | deny | off
|
||||
extraRoots: # always in scope, in every session
|
||||
- ../../shared # relative to each session's own directory
|
||||
extraRoots: [] # optional: a seed of paths always in scope, every session
|
||||
```
|
||||
|
||||
Both fields are also on the plugin's card under **Plugins** in the Web UI, which
|
||||
is the same layer reached a different way: a settings write rewrites this entry in
|
||||
the profile patch rather than storing anything new. An edit there is live — the
|
||||
guard reads `mode` and `extraRoots` on every call, so a change decides the next
|
||||
file call without a restart.
|
||||
Both are also on the plugin's card under **Plugins** in the Web UI, which is the
|
||||
same layer reached a different way: a settings write rewrites this entry in the
|
||||
profile patch rather than storing anything new. An edit there is live — the guard
|
||||
reads `mode` and `extraRoots` on every call, so a change decides the next file
|
||||
call without a restart.
|
||||
|
||||
The one exception is `off`. Choosing it while a session is running stops the guard
|
||||
immediately and empties the binding on the next request, because the binding is a
|
||||
@@ -132,11 +131,37 @@ patch. `ask` and `deny` swap freely in both directions.
|
||||
place the rule and the binding among the other prompt sections, which is decided
|
||||
once at load, and no section is re-ordered without a remount.
|
||||
|
||||
`extraRoots` is for code a module legitimately always depends on. Anything you
|
||||
grant by hand is per-conversation and is never persisted — "this session may
|
||||
also see `src/shared`" is a statement about one conversation, and writing it down
|
||||
would quietly widen every future one, which is the failure this plugin exists to
|
||||
prevent.
|
||||
### Per-session scope, and who gets to decide it
|
||||
|
||||
Scope has two layers, and the split is the point:
|
||||
|
||||
| layer | what it is | who can write it |
|
||||
|---|---|---|
|
||||
| **the user's declaration** | `scopes[<session id>]` in this row's config — a `focus` and a list of `extras` | only the context panel, through the settings service |
|
||||
| **the agent's** | a narrowing `focus` and any session grants, held in memory | the agent, and only ever to *reduce* |
|
||||
|
||||
The effective root is the agent's narrowing while it still sits inside your root,
|
||||
and your root otherwise. So an agent that goes wrong cannot widen its own scope,
|
||||
and `facet action=reset` returns it to what you declared rather than to the
|
||||
session directory — landing on the directory would have made `reset` a widening.
|
||||
|
||||
You set the declaration from the **crosshair button on the composer tool row**,
|
||||
which appears only in sessions running the `facet` preset. It shows the focus and
|
||||
the allowed paths, adds and removes them, and carries the mode details and usage
|
||||
notes that the preset picker's guide dialog cannot show for a preset no core
|
||||
package ships. Writes go to the profile patch, so reopening a chat gives back the
|
||||
scope it had.
|
||||
|
||||
Because the panel owns widening, `facet action=add` is the *other* path and the
|
||||
narrower one: it is gated on an approval channel, and a deployment with none — the
|
||||
`danger-full-access` permission preset sets `approval: never` — refuses it before
|
||||
anyone is asked. In that deployment the panel is the only way to widen, which is
|
||||
exactly what the binding tells the agent.
|
||||
|
||||
`extraRoots` is only a seed now. Before per-session scope it was the *only* way to
|
||||
widen anything, which meant "this conversation needs the entity folder" had to be
|
||||
written as a fact about every conversation in every workspace. It is for paths a
|
||||
deployment genuinely always wants, and it is empty by default.
|
||||
|
||||
## The compaction row
|
||||
|
||||
@@ -188,13 +213,15 @@ Honest boundaries, because each one is a decision rather than an oversight:
|
||||
|
||||
Verified:
|
||||
|
||||
- 53 tests (`npm test`) over the scope algebra, the policy state, the guard, and
|
||||
- 73 tests (`npm test`) over the scope algebra, the policy state, the guard, and
|
||||
the browser half — including that containment is decided on canonical targets
|
||||
(a symlink inside the facet pointing outside it is refused), that a prefix is
|
||||
not a parent (`/a/room` does not contain `/a/roommate`), that a malformed tool
|
||||
call never makes the guard throw, that a live `mode` edit decides the next call
|
||||
without the guard being remounted, and that every `facet` result satisfies the
|
||||
tool's own declared output schema.
|
||||
without the guard being remounted, that an agent narrowing the user has since
|
||||
narrowed past is dropped rather than honoured, that `reset` lands on the
|
||||
declared scope rather than the session directory, and that every `facet` result
|
||||
satisfies the tool's own declared output schema.
|
||||
- The bundle's patch layers parse, merge and land correctly against the real
|
||||
profile: `dsh --profile web --patch ./cordis.patch.yml --patch
|
||||
./presets/facet.patch.yml --dump-config` shows `facet-policy`, `preset-facet`,
|
||||
@@ -264,13 +291,13 @@ it.
|
||||
|
||||
```
|
||||
index.js host row: the policy, published as ctx.facetPolicy
|
||||
client/client.js browser half: the settings card on the Plugins page
|
||||
client/client.js browser half: the settings card, and the context panel
|
||||
lib/policy.js the stateful half — scope resolution, grants
|
||||
lib/scope.js the pure half — argument extraction, text, reasons
|
||||
lib/guard.js preset row: rule, binding, tool, pre-execute guard
|
||||
presets/facet.patch.yml the `facet` agent preset
|
||||
presets/standard-compaction.patch.yml optional: the same budget for `standard`
|
||||
test/ 53 tests
|
||||
test/ 73 tests
|
||||
```
|
||||
|
||||
`client/client.js` is a browser half rather than a module the host imports, and
|
||||
@@ -287,6 +314,14 @@ entry id (`facet-policy`, the row's `id` in `cordis.patch.yml`) while the
|
||||
happens to name them identically, so binding the form to the wrong one produces
|
||||
a card that renders, saves, and writes nowhere. `test/client.test.js` pins both.
|
||||
|
||||
The same file carries the context panel, registered into
|
||||
`conversation.input.left` — the composer tool row — and gated on the session's own
|
||||
`agentPreset` projection, so it appears only where the facet is actually in force.
|
||||
That gate is why `scopes` is `volatile()` in the host schema: `settings.mutate`
|
||||
refuses a path that is not under a volatile node, and refusing is exactly right —
|
||||
the panel is the only writer, and a plugin that could not serve the namespace
|
||||
would be a control that silently stored nothing.
|
||||
|
||||
## Also in this repository
|
||||
|
||||
- [`facet-preset.md`](../facet-preset.md) — the design note this was built from,
|
||||
|
||||
Reference in new issue
Block a user