policy: reject out-of-range seed boundaries; carry baselines through session-query

Review fixes (ds-review-bot on #623):

- overrideOf (both knobs) rejects a seedLength past the log end before
  slicing: a malformed durable boundary would otherwise empty the
  own-switch slice until the log outgrew it, letting a wide baseline
  shadow a REAL later tightening. Malformed durable metadata fails loud,
  never open.
- The session-query derived index carries the two baseline fields end to
  end: schema columns on both session tables (SESSION_QUERY_SQLITE_SCHEMA
  _VERSION 6 — derived, rebuilds in place), inserts, header selects, the
  candidates CTE, rowHeader, sameHeader, and the cross-source
  assertSessionHeadersCompatible — so a search hit's header keeps the
  child's inherited confinement and conflicting live/persisted baselines
  reject.

Red-first: out-of-range seedLength tests in both policy suites;
baseline round-trip and live/persisted baseline-conflict tests in the
session-query sqlite suite.
This commit is contained in:
kingwl
2026-07-26 23:31:01 +08:00
parent 99f5fab7bc
commit 9aaa4a871f
10 changed files with 98 additions and 11 deletions

View File

@@ -220,6 +220,40 @@ describe('SQLite session search', () => {
.resolves.toMatchObject({ items: [{ header: { ...session.header, seedLength: 1 }, live: true, persisted: false }] })
})
it('round-trips the inherited policy baselines through search headers', async () => {
// A delegated child's header carries the sandbox/approval baselines; the
// derived index must return them — a consumer resuming from a search hit
// would otherwise rebuild a child without its inherited confinement.
const ctx = await liveContext({ path: ':memory:' })
const session = ctx.sessions.create(SessionId('live-baseline'), {
meta: { cwd: '/work', createdAt: 10, sandboxMode: 'read-only', approvalPolicy: 'never' },
})
session.append(
'user/message',
{ content: [{ type: 'text', text: 'baseline needle' }], source: { kind: 'user' } },
{ surfaceOp: 'append' },
)
const result = await ctx.sessionQuery.searchSessions({ query: 'needle' })
expect(result.items[0]?.header).toMatchObject({ sandboxMode: 'read-only', approvalPolicy: 'never' })
const events = await ctx.sessionQuery.searchEvents({ sessionId: session.id, query: 'needle' })
expect(events.session).toMatchObject({ sandboxMode: 'read-only', approvalPolicy: 'never' })
})
it('rejects live/persisted sources whose policy baselines conflict', async () => {
const shared = header('baseline-conflict', 10, { sandboxMode: 'read-only' })
TestPersistence.reset([{ meta: shared, events: messageEvents('persisted needle') }])
const ctx = await liveContext()
await ctx.plugin(TestPersistence)
ctx.sessions.create(shared.id, {
seed: messageEvents('live needle'),
meta: { createdAt: 10, sandboxMode: 'danger-full-access' },
})
await expect(ctx.sessionQuery.searchSessions({ query: 'needle' }))
.rejects.toThrow(expectCode('SESSION_QUERY_SOURCE_CONFLICT'))
})
it('searches all surfaces by default and applies metadata before ranking', async () => {
const ctx = await liveContext({ path: ':memory:', defaultLimit: 10, maxLimit: 20 })
const parent = SessionId('parent')