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
+10
-2
@@ -160,14 +160,22 @@ test('nothing is registered while the Host does not serve the namespace', async
|
||||
assert.equal(seen.entry, undefined, 'but no card is registered for a namespace nobody serves')
|
||||
})
|
||||
|
||||
test('the card stages exactly the fields the host marks volatile', async () => {
|
||||
test('the card stages exactly the deployment fields the host marks volatile', async () => {
|
||||
const { module: mod, staged } = await loadClient()
|
||||
drive(mod)
|
||||
|
||||
const declared = Object.entries(Config.dict)
|
||||
const volatileFields = Object.entries(Config.dict)
|
||||
.filter(([, schema]) => schema.meta.volatile === true)
|
||||
.map(([key]) => key)
|
||||
|
||||
// `scopes` is volatile too, but it is not a deployment field and must not be a
|
||||
// control on the card: it is per-session state, written by the context panel
|
||||
// and keyed by session id. Staging it here would render a dictionary blob a
|
||||
// user could not sensibly edit, and would put the panel's store on the one
|
||||
// surface that edits the deployment.
|
||||
assert.ok(volatileFields.includes('scopes'), 'scopes must be volatile or the panel cannot write it')
|
||||
const declared = volatileFields.filter((key) => key !== 'scopes')
|
||||
|
||||
// A field served but never staged is invisible in the UI; a field staged but
|
||||
// never served makes `whileServed` withhold the whole card.
|
||||
assert.deepEqual(
|
||||
|
||||
Reference in new issue
Block a user