Files
dsh-facet/test/policy.test.js
NotExpectedYet 8ab3299694
Release / release (push) Successful in 6s
fix: a delegated session inherits the facet it was spawned under
The user's declaration is keyed by session id, and a session spawned or forked
from a scoped one has none of its own — so its facet was its own working
directory, one level above its parent's. Delegation was therefore a way around
the scope rather than work inside it: the parent could not read the file itself
and could have a subagent read it.

A session now inherits the root resolved for the session named by its
`parentSession` header, which the harness already records durably for both a
spawned child and a fork. The inherited root is the parent's *effective* root, so
it already includes any narrowing the parent made for itself: delegating is not
a way back out of a self-imposed focus either. Because each session records its
own root as it resolves a scope, and a scope is resolved for every model request
and every tool call, inheritance holds at any depth of delegation rather than for
one hop.

Two boundaries are deliberate. A declaration the user made *for the child* is a
more specific statement about that session and stands — inheritance only answers
the case where they made none. And a parent's recorded root outlives its session
while any child that inherited it is still running, because disposing a parent
must not quietly widen a subagent that is still working.
2026-09-27 07:43:03 +01:00

248 lines
11 KiB
JavaScript

import assert from 'node:assert/strict'
import { test } from 'node:test'
import { createFacetPolicy } from '../lib/policy.js'
/** A stand-in for the agent shape the policy reads: session id plus working directory. */
const agentAt = (cwd, id = 's1') => ({ session: { id, header: { cwd } } })
test('the scope is the session directory, and nothing else', () => {
const policy = createFacetPolicy({ mode: 'ask' })
const scope = policy.scopeFor(agentAt('/w/src/controllers/room'))
assert.equal(scope.root, '/w/src/controllers/room')
assert.equal(scope.cwd, scope.root)
assert.deepEqual(scope.extras, [])
})
test('an agent with no working directory has no facet', () => {
const policy = createFacetPolicy({ mode: 'ask' })
assert.equal(policy.scopeFor(undefined), undefined)
assert.equal(policy.scopeFor({}), undefined)
assert.equal(policy.scopeFor({ session: { header: {} } }), undefined)
assert.equal(policy.scopeFor({ session: { header: { cwd: ' ' } } }), undefined)
assert.equal(policy.scopeFor({ session: { header: { cwd: 7 } } }), undefined)
})
test('a grant widens one session and leaves every other alone', () => {
const policy = createFacetPolicy({ mode: 'ask' })
policy.grant('s1', ['../shared'], '/w/src/room')
assert.deepEqual(policy.scopeFor(agentAt('/w/src/room', 's1')).extras, ['/w/src/shared'])
assert.deepEqual(policy.scopeFor(agentAt('/w/src/room', 's2')).extras, [])
assert.deepEqual(policy.grantsFor('s1'), ['/w/src/shared'])
assert.deepEqual(policy.grantsFor('s2'), [])
})
test('grants are absolute, deduplicated and order-preserving', () => {
const policy = createFacetPolicy({ mode: 'ask' })
// `shared` is relative to the session directory, `/w/src/shared` is not, and
// both are kept: they are different places that happen to share a name.
policy.grant('s1', ['shared', '/w/src/shared', 'shared', 'other'], '/w/src/room')
assert.deepEqual(policy.grantsFor('s1'), ['/w/src/room/shared', '/w/src/shared', '/w/src/room/other'])
})
test('revoke and reset narrow without touching anyone else', () => {
const policy = createFacetPolicy({ mode: 'ask' })
policy.grant('s1', ['a', 'b'], '/w')
policy.grant('s2', ['c'], '/w')
assert.deepEqual(policy.revoke('s1', ['a'], '/w'), ['/w/b'])
assert.deepEqual(policy.revoke('s1', ['b'], '/w'), [])
assert.equal(policy.sessions.includes('s1'), false)
assert.deepEqual(policy.grantsFor('s2'), ['/w/c'])
policy.grant('s3', ['d'], '/w')
assert.deepEqual(policy.reset('s3'), [])
assert.deepEqual(policy.grantsFor('s3'), [])
})
test('a disposed session does not leave its grant behind', () => {
const policy = createFacetPolicy({ mode: 'ask' })
policy.grant('s1', ['shared'], '/w')
assert.deepEqual(policy.sessions, ['s1'])
policy.forget('s1')
assert.deepEqual(policy.sessions, [])
// A resumed session id must not inherit a grant nobody remembers making.
assert.deepEqual(policy.scopeFor(agentAt('/w', 's1')).extras, [])
})
test('standing grants resolve against each session, not the process', () => {
const policy = createFacetPolicy({ mode: 'ask', extraRoots: ['../shared', '/abs/common'] })
assert.deepEqual(policy.scopeFor(agentAt('/w/a/one')).extras, ['/w/a/shared', '/abs/common'])
assert.deepEqual(policy.scopeFor(agentAt('/w/b/two')).extras, ['/w/b/shared', '/abs/common'])
})
test('junk in the configuration and in a grant is dropped, not thrown', () => {
const policy = createFacetPolicy({ mode: 'ask', extraRoots: ['ok', '', ' ', 42, null] })
assert.deepEqual(policy.standing, ['ok'])
policy.grant('s1', ['', ' ', 42, null, 'real'], '/w')
assert.deepEqual(policy.grantsFor('s1'), ['/w/real'])
})
test('standing grants and session grants are merged without repetition', () => {
const policy = createFacetPolicy({ mode: 'ask', extraRoots: ['shared'] })
policy.grant('s1', ['shared', 'other'], '/w')
assert.deepEqual(policy.scopeFor(agentAt('/w', 's1')).extras, ['/w/shared', '/w/other'])
})
test('prompt placements are carried, so the preset row needs no config', () => {
assert.equal(createFacetPolicy({ mode: 'ask' }).sectionOrder, 100)
assert.equal(createFacetPolicy({ mode: 'ask' }).contextOrder, 105)
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')
})
// ── delegated sessions ───────────────────────────────────────────────────────
//
// A session spawned or forked from this one has no declaration of its own, and
// the user's declaration is keyed by session id. What stops it starting wider
// than its parent is the parent's own resolved root, recorded as the parent
// resolves a scope — which it does for every model request and every tool call,
// so the record is always there before it can spawn anything.
/** An agent whose header carries the lineage the harness writes for a child. */
const spawned = (id, parentSession, cwd = '/w') => ({
session: { id, header: { cwd, parentSession } },
})
test('a session spawned from a scoped one inherits its root, at any depth', () => {
const policy = createFacetPolicy({ mode: 'deny', scopes: { s1: { focus: 'src/room' } } })
assert.equal(policy.scopeFor(agentAt('/w', 's1')).root, '/w/src/room')
const child = spawned('c1', 's1')
assert.equal(policy.scopeFor(child).root, '/w/src/room')
// Relative paths still resolve from the child's own directory, as for anyone.
assert.equal(policy.scopeFor(child).cwd, '/w')
assert.equal(policy.scopeFor(spawned('g1', 'c1')).root, '/w/src/room')
})
test('a child is never wider than its parent is now, narrowing included', () => {
const policy = createFacetPolicy({ mode: 'deny' })
policy.narrowTo('s1', '/w/src/room')
// The narrowing is the agent's own decision; spawning a child is not a way to
// undo it, because the root the child inherits already includes it.
assert.equal(policy.scopeFor(agentAt('/w', 's1')).root, '/w/src/room')
assert.equal(policy.scopeFor(spawned('c1', 's1')).root, '/w/src/room')
})
test('a declaration for the child itself stands over what it would inherit', () => {
const policy = createFacetPolicy({ mode: 'deny', scopes: { s1: { focus: 'src/room' }, c1: { focus: '.' } } })
policy.scopeFor(agentAt('/w', 's1'))
// Inheritance answers the case where the user made no statement about this
// session; a statement they did make is the more specific one and stands.
assert.equal(policy.scopeFor(spawned('c1', 's1')).root, '/w')
})
test('a root is kept while a session that inherited it is still alive', () => {
const policy = createFacetPolicy({ mode: 'deny', scopes: { s1: { focus: 'src/room' } } })
const child = spawned('c1', 's1')
policy.scopeFor(agentAt('/w', 's1'))
policy.scopeFor(child)
// Disposing the parent must not quietly widen the child that is still running.
policy.forget('s1')
assert.equal(policy.scopeFor(child).root, '/w/src/room')
// With nothing left to inherit it, the record goes too.
policy.forget('c1')
assert.equal(policy.scopeFor(agentAt('/w', 's1')).root, '/w/src/room')
})