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
@@ -93,3 +93,97 @@ test('prompt placements are carried, so the preset row needs no config', () => {
|
||||
assert.equal(createFacetPolicy({ mode: 'ask', sectionOrder: 7, contextOrder: 8 }).sectionOrder, 7)
|
||||
assert.equal(createFacetPolicy({ mode: 'ask', sectionOrder: 7, contextOrder: 8 }).contextOrder, 8)
|
||||
})
|
||||
|
||||
// ── per-session scope ────────────────────────────────────────────────────────
|
||||
//
|
||||
// The declaration is the user's, written from the browser panel; the narrowing
|
||||
// is the agent's, held in memory. Everything below is about which of the two
|
||||
// wins, and the answer is always "the narrower one, and never a stale agent
|
||||
// focus that the user has since narrowed past".
|
||||
|
||||
test('a declared focus narrows the root, and the session directory stays the base', () => {
|
||||
const policy = createFacetPolicy({
|
||||
mode: 'deny',
|
||||
scopes: { s1: { focus: 'src/controllers/room' } },
|
||||
})
|
||||
const scope = policy.scopeFor(agentAt('/w', 's1'))
|
||||
|
||||
assert.equal(scope.root, '/w/src/controllers/room')
|
||||
// `cwd` is still where relative paths resolve from — a declaration written
|
||||
// against the session keeps its meaning whatever the session focuses on.
|
||||
assert.equal(scope.cwd, '/w')
|
||||
})
|
||||
|
||||
test('a declaration is read live, so the panel needs no remount', () => {
|
||||
const scopes = {}
|
||||
const policy = createFacetPolicy({ mode: 'deny', scopes })
|
||||
assert.equal(policy.scopeFor(agentAt('/w', 's1')).root, '/w')
|
||||
|
||||
// What a panel write does to the running reference.
|
||||
scopes.s1 = { focus: 'src/module' }
|
||||
assert.equal(policy.scopeFor(agentAt('/w', 's1')).root, '/w/src/module')
|
||||
})
|
||||
|
||||
test('declared extras join the deployment seed and the session grants', () => {
|
||||
const policy = createFacetPolicy({
|
||||
mode: 'deny',
|
||||
extraRoots: ['seed'],
|
||||
scopes: { s1: { focus: 'src/room', extras: ['shared', '/abs/common'] } },
|
||||
})
|
||||
policy.grant('s1', ['granted'], '/w')
|
||||
|
||||
assert.deepEqual(policy.scopeFor(agentAt('/w', 's1')).extras, [
|
||||
'/w/seed',
|
||||
'/w/shared',
|
||||
'/abs/common',
|
||||
'/w/granted',
|
||||
])
|
||||
})
|
||||
|
||||
test('an agent narrowing is honoured while it stays inside the declared root', () => {
|
||||
const policy = createFacetPolicy({ mode: 'deny', scopes: { s1: { focus: 'src' } } })
|
||||
policy.narrowTo('s1', '/w/src/room')
|
||||
|
||||
assert.equal(policy.scopeFor(agentAt('/w', 's1')).root, '/w/src/room')
|
||||
assert.equal(policy.narrowFor('s1'), '/w/src/room')
|
||||
})
|
||||
|
||||
test('a narrowing the user has since narrowed past is dropped, not honoured', () => {
|
||||
const scopes = { s1: { focus: 'src' } }
|
||||
const policy = createFacetPolicy({ mode: 'deny', scopes })
|
||||
policy.narrowTo('s1', '/w/src/room')
|
||||
assert.equal(policy.scopeFor(agentAt('/w', 's1')).root, '/w/src/room')
|
||||
|
||||
// The user narrows to a sibling: the agent's focus is now outside the scope
|
||||
// they declared. Honouring it would put the session back where the user had
|
||||
// just decided it should not be.
|
||||
scopes.s1 = { focus: 'src/other' }
|
||||
assert.equal(policy.scopeFor(agentAt('/w', 's1')).root, '/w/src/other')
|
||||
})
|
||||
|
||||
test('reset and forget drop the agent side and leave the declaration alone', () => {
|
||||
const policy = createFacetPolicy({ mode: 'deny', scopes: { s1: { focus: 'src', extras: ['shared'] } } })
|
||||
policy.narrowTo('s1', '/w/src/room')
|
||||
policy.grant('s1', ['granted'], '/w')
|
||||
|
||||
policy.reset('s1')
|
||||
// Back to what the user declared — narrower than the session, never wider.
|
||||
assert.equal(policy.scopeFor(agentAt('/w', 's1')).root, '/w/src')
|
||||
assert.deepEqual(policy.grantsFor('s1'), [])
|
||||
assert.deepEqual(policy.scopeFor(agentAt('/w', 's1')).extras, ['/w/shared'])
|
||||
|
||||
policy.narrowTo('s1', '/w/src/room')
|
||||
policy.forget('s1')
|
||||
assert.equal(policy.narrowFor('s1'), undefined)
|
||||
assert.equal(policy.scopeFor(agentAt('/w', 's1')).root, '/w/src')
|
||||
})
|
||||
|
||||
test('junk in a declaration is ignored rather than thrown on', () => {
|
||||
const policy = createFacetPolicy({
|
||||
mode: 'deny',
|
||||
scopes: { s1: { focus: ' ', extras: ['ok', '', 42, null] }, s2: 'not an object' },
|
||||
})
|
||||
assert.equal(policy.scopeFor(agentAt('/w', 's1')).root, '/w')
|
||||
assert.deepEqual(policy.scopeFor(agentAt('/w', 's1')).extras, ['/w/ok'])
|
||||
assert.equal(policy.scopeFor(agentAt('/w', 's2')).root, '/w')
|
||||
})
|
||||
Reference in new issue
Block a user