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
+6
-6
@@ -82,7 +82,7 @@ test('facetContextText carries the binding that does change', () => {
|
||||
deny,
|
||||
'facet: /w/src/room\n' +
|
||||
'granted outside it: src/shared\n' +
|
||||
'outside the facet: refused — `facet action=add` asks the user to grant a path, and is the only way to widen the scope',
|
||||
'outside the facet: refused — ask the user to widen the facet from its context panel, or with `facet action=add` where that can be granted',
|
||||
)
|
||||
|
||||
// The binding is the only half that can follow a live edit, so the two modes
|
||||
@@ -97,14 +97,14 @@ test('facetContextText carries the binding that does change', () => {
|
||||
test('a session with no approval channel says so instead of promising a question', () => {
|
||||
// `approval: never` refuses every ask before an answerer is consulted, so the
|
||||
// mode stops mattering: `ask` and `deny` both end in a refusal, and
|
||||
// `facet action=add` cannot be approved either. `canAsk: false` is the one
|
||||
// `facet action=add` cannot be granted either. `canAsk: false` is the one
|
||||
// input that collapses them, and it has to name the mechanism that still
|
||||
// works — `extraRoots` is configuration, not a per-conversation grant.
|
||||
// works — the context panel, which the user owns.
|
||||
for (const mode of ['ask', 'deny']) {
|
||||
const text = facetContextText({ cwd: '/w', root: '/w/src/room', extras: [], mode, canAsk: false })
|
||||
assert.match(text, /outside the facet: refused — approval prompts are off in this session/)
|
||||
assert.match(text, /`facet action=add` cannot be approved either/)
|
||||
assert.match(text, /`extraRoots`/)
|
||||
assert.match(text, /outside the facet: refused — this session has no approval channel/)
|
||||
assert.match(text, /`facet action=add` cannot be granted/)
|
||||
assert.match(text, /context panel/)
|
||||
assert.doesNotMatch(text, /put to the user as a question/)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user