feat: scope a session from a declaration the agent cannot widen
The facet root was the session's working directory and nothing else, so the only way to scope a conversation to a module was to make the module a workspace — and the only way to reach anything outside it was `extraRoots`, which is a fact about the deployment rather than about one conversation. Scope now has two layers, and the split is the point. The **user layer** is `config.scopes[sessionId]` — a `focus` and a list of `extras`, written only from the browser context panel through the settings service, which is what `settings.mutate` requiring a volatile path buys us. The **agent layer** is in memory: a narrowing `focus` and its grants. Both can only ever reduce what the user layer allows, so an agent that goes wrong cannot widen its own scope. `facet action=focus` is therefore the one scope change the agent may make unasked, bounded by the scope already in force and decided on canonical targets. `reset` clears the agent's side only, landing on the user's declaration rather than on the session directory — landing on the directory would have made `reset` a widening. `extraRoots` demotes to an optional deployment-wide seed. The binding now names the context panel as the way to widen, because it is: in a deployment with no approval channel, `facet action=add` cannot be granted at all.
This commit is contained in:
1 parent
f36d46efa1
commit
d9caf4eeec
8 files changed
+431
-52
No files matched your search
@@ -33,20 +33,25 @@ export const name = 'dsh-facet'
|
||||
/**
|
||||
* Every configuration field this plugin exposes.
|
||||
*
|
||||
* `mode` and `extraRoots` are marked `volatile()`, and that marker is doing two
|
||||
* jobs rather than one. It is what puts them on the settings page at all — the
|
||||
* framework's form projection keeps only fields under a volatile ancestor, and
|
||||
* drops an entry with no such field from `settings.describe` entirely, so
|
||||
* without it this row has no card. It is also what makes an edit *live*: a
|
||||
* volatile field is handed to {@link apply} as a reference rather than a value,
|
||||
* and the framework commits an edit into that reference without re-applying the
|
||||
* plugin.
|
||||
* `mode`, `extraRoots` and `scopes` are marked `volatile()`, and that marker is
|
||||
* doing two jobs rather than one. It is what puts them on the settings page at
|
||||
* all — the framework's form projection keeps only fields under a volatile
|
||||
* ancestor, and drops an entry with no such field from `settings.describe`
|
||||
* entirely, so without it this row has no card. It is also what makes an edit
|
||||
* *live*: a volatile field is handed to {@link apply} as a reference rather than
|
||||
* a value, and the framework commits an edit into that reference without
|
||||
* re-applying the plugin.
|
||||
*
|
||||
* The second job is why every reader of these two fields goes through
|
||||
* The second job is why every reader of these fields goes through
|
||||
* `configValue()` in `./lib/policy.js` and reads at the point of use. A plugin
|
||||
* that takes the reference for a string gets a settings card that appears to
|
||||
* work and changes nothing.
|
||||
*
|
||||
* `scopes` is volatile for a third reason: it is the only field the browser
|
||||
* context panel writes, and `settings.mutate` refuses a path that is not under a
|
||||
* volatile node. That is the whole mechanism by which the panel — and not the
|
||||
* agent — decides what one conversation may see.
|
||||
*
|
||||
* `sectionOrder` and `contextOrder` are deliberately NOT volatile. They place
|
||||
* the rule and the binding among the other prompt sections, which is settled
|
||||
* once at load; making them volatile would put two knobs nobody should touch on
|
||||
@@ -61,12 +66,46 @@ export const Config = Schema.object({
|
||||
mode: Schema.union(['ask', 'deny', 'off']).default('ask').volatile(),
|
||||
|
||||
/**
|
||||
* Paths always in scope, for code a module legitimately depends on — shared
|
||||
* types, a common test harness. Relative paths resolve against each session's
|
||||
* own directory, so the same list is meaningful in every workspace.
|
||||
* A deployment-wide seed of paths always in scope, for code a module
|
||||
* legitimately depends on — shared types, a common test harness.
|
||||
*
|
||||
* Deliberately only a seed. Before per-session scope existed this was the only
|
||||
* way to widen anything, which meant "this one conversation needs the entity
|
||||
* folder" had to be written as a fact about every conversation in every
|
||||
* workspace. Per-session scope belongs in {@link Config.scopes}; this is for
|
||||
* the paths a deployment genuinely always wants, and it is empty by default.
|
||||
*
|
||||
* Relative paths resolve against each session's own directory, so the same
|
||||
* list is meaningful in every workspace.
|
||||
*/
|
||||
extraRoots: Schema.array(String).default([]).volatile(),
|
||||
|
||||
/**
|
||||
* Per-session scope — the user's declaration for one conversation, keyed by
|
||||
* session id.
|
||||
*
|
||||
* Written only by the browser context panel, through the settings service, and
|
||||
* never by the agent: this plugin exposes no write path to it, and a facet
|
||||
* session carries no settings tool. `focus` narrows the session to a folder;
|
||||
* `extras` are paths the conversation may read outside it. Both are relative
|
||||
* to the session's own directory unless absolute, so a declaration keeps its
|
||||
* meaning wherever the workspace lives.
|
||||
*
|
||||
* Entries outlive their sessions, which is deliberate — reopening a chat
|
||||
* should give back the scope it had — and costs a little growth in the
|
||||
* profile patch that the panel can tidy.
|
||||
*/
|
||||
scopes: Schema.dict(
|
||||
Schema.object({
|
||||
/** The session's facet root, relative to the session directory or absolute. Empty means the session directory itself. */
|
||||
focus: Schema.string(),
|
||||
/** Paths this session may also read, outside the root. */
|
||||
extras: Schema.array(String).default([]),
|
||||
}),
|
||||
)
|
||||
.default({})
|
||||
.volatile(),
|
||||
|
||||
/** Placement of the invariant rule in the system prompt. */
|
||||
sectionOrder: Schema.number().default(100),
|
||||
|
||||
|
||||
Reference in new issue
Block a user