subagent: seed inherited policy events at creation
The parent implementation introduced sandboxMode and approvalPolicy as generic SessionHeader fields, then propagated those fields through both persistence backends, session-query indexes, collision checks, policy-specific seed-boundary folds, catalogs, and a broad test matrix. That storage plane is unnecessary: Session already accepts a validated constructor seed, and persistence captures that seed when the session is announced before committing its first batch. Capture each parent override synchronously at delegation, append source-tagged sandbox/mode and approval/policy records after the optional fork prefix, and create the child with that combined seed. Keeping header.seedLength at the original fork-prefix length preserves lineage while ordinary last-event-wins folds make the inherited records outrank stale parent history and remain subordinate to later child switches. Unswitched parents still stamp nothing, so children continue to follow deployment defaults. Remove the generic header fields and every persistence/query/schema branch built around them. Collapse the inheritance suite from ten leaking scenarios to four owned-context cases covering real filesystem confinement, stale fork precedence, delegation-time capture, and the no-override path. The assembled headless snapshot now asserts the persisted inheritance event directly. This keeps the security behavior while restoring policy ownership to the existing event log and deleting the speculative durability machinery that the original tests did not exercise.
This commit is contained in:
@@ -146,8 +146,6 @@ interface SessionHeaderRow {
|
||||
parent_session: string | null
|
||||
seed_length: number | null
|
||||
delegation_depth: number | null
|
||||
sandbox_mode: string | null
|
||||
approval_policy: string | null
|
||||
}
|
||||
|
||||
interface SearchRow extends SessionHeaderRow {
|
||||
@@ -526,23 +524,6 @@ export class SessionQuerySqlite extends SessionQueryService {
|
||||
}
|
||||
}
|
||||
|
||||
/** The shared header column bindings both session tables lead with. */
|
||||
private static _headerBindings(
|
||||
header: SessionHeader,
|
||||
): [string, number, number, string | null, string | null, number | null, number | null, string | null, string | null] {
|
||||
return [
|
||||
header.id,
|
||||
header.version,
|
||||
header.createdAt,
|
||||
header.cwd ?? null,
|
||||
header.parentSession ?? null,
|
||||
header.seedLength ?? null,
|
||||
header.delegationDepth ?? null,
|
||||
header.sandboxMode ?? null,
|
||||
header.approvalPolicy ?? null,
|
||||
]
|
||||
}
|
||||
|
||||
private _replacePersistedSession(
|
||||
entry: ObservedSession,
|
||||
revision: SessionPersistenceRevision,
|
||||
@@ -552,9 +533,19 @@ export class SessionQuerySqlite extends SessionQueryService {
|
||||
const db = this._requireDb()
|
||||
db.prepare(`
|
||||
INSERT INTO persisted_sessions
|
||||
(id, version, created_at, cwd, parent_session, seed_length, delegation_depth, sandbox_mode, approval_policy, revision, generation)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(...SessionQuerySqlite._headerBindings(entry.header), revision, generation)
|
||||
(id, version, created_at, cwd, parent_session, seed_length, delegation_depth, revision, generation)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
entry.header.id,
|
||||
entry.header.version,
|
||||
entry.header.createdAt,
|
||||
entry.header.cwd ?? null,
|
||||
entry.header.parentSession ?? null,
|
||||
entry.header.seedLength ?? null,
|
||||
entry.header.delegationDepth ?? null,
|
||||
revision,
|
||||
generation,
|
||||
)
|
||||
const insert = db.prepare(`
|
||||
INSERT INTO persisted_docs (text, session_id, seq, type, time, surface, codepoint_length)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
@@ -578,9 +569,20 @@ export class SessionQuerySqlite extends SessionQueryService {
|
||||
const db = this._requireDb()
|
||||
db.prepare(`
|
||||
INSERT INTO temp.live_sessions
|
||||
(id, version, created_at, cwd, parent_session, seed_length, delegation_depth, sandbox_mode, approval_policy, fingerprint, persisted, generation)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(...SessionQuerySqlite._headerBindings(entry.header), entry.fingerprint, persisted ? 1 : 0, generation)
|
||||
(id, version, created_at, cwd, parent_session, seed_length, delegation_depth, fingerprint, persisted, generation)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
entry.header.id,
|
||||
entry.header.version,
|
||||
entry.header.createdAt,
|
||||
entry.header.cwd ?? null,
|
||||
entry.header.parentSession ?? null,
|
||||
entry.header.seedLength ?? null,
|
||||
entry.header.delegationDepth ?? null,
|
||||
entry.fingerprint,
|
||||
persisted ? 1 : 0,
|
||||
generation,
|
||||
)
|
||||
const insert = db.prepare(`
|
||||
INSERT INTO temp.live_docs (text, session_id, seq, type, time, surface, codepoint_length)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
@@ -669,7 +671,7 @@ export class SessionQuerySqlite extends SessionQueryService {
|
||||
const db = this._requireDb()
|
||||
const live = db.prepare(
|
||||
`SELECT
|
||||
id AS session_id, version, created_at, cwd, parent_session, seed_length, delegation_depth, sandbox_mode, approval_policy, generation
|
||||
id AS session_id, version, created_at, cwd, parent_session, seed_length, delegation_depth, generation
|
||||
FROM temp.live_sessions
|
||||
WHERE id = ?`,
|
||||
).get(sessionId) as (SessionHeaderRow & { generation: number }) | undefined
|
||||
@@ -679,7 +681,7 @@ export class SessionQuerySqlite extends SessionQueryService {
|
||||
if (persistenceBinding.service !== undefined) {
|
||||
const persisted = db.prepare(
|
||||
`SELECT
|
||||
id AS session_id, version, created_at, cwd, parent_session, seed_length, delegation_depth, sandbox_mode, approval_policy, generation
|
||||
id AS session_id, version, created_at, cwd, parent_session, seed_length, delegation_depth, generation
|
||||
FROM persisted_sessions
|
||||
WHERE id = ?`,
|
||||
).get(sessionId) as (SessionHeaderRow & { generation: number }) | undefined
|
||||
@@ -738,8 +740,6 @@ function selectedDocumentsSql(): { sql: string } {
|
||||
ps.parent_session AS parent_session,
|
||||
ps.seed_length AS seed_length,
|
||||
ps.delegation_depth AS delegation_depth,
|
||||
ps.sandbox_mode AS sandbox_mode,
|
||||
ps.approval_policy AS approval_policy,
|
||||
0 AS live,
|
||||
1 AS persisted,
|
||||
CAST(pd.seq AS INTEGER) AS seq,
|
||||
@@ -762,8 +762,6 @@ function selectedDocumentsSql(): { sql: string } {
|
||||
ls.parent_session AS parent_session,
|
||||
ls.seed_length AS seed_length,
|
||||
ls.delegation_depth AS delegation_depth,
|
||||
ls.sandbox_mode AS sandbox_mode,
|
||||
ls.approval_policy AS approval_policy,
|
||||
1 AS live,
|
||||
CASE WHEN ? = 1 THEN ls.persisted ELSE 0 END AS persisted,
|
||||
CAST(ld.seq AS INTEGER) AS seq,
|
||||
@@ -872,8 +870,6 @@ function sameHeader(a: SessionHeader, b: SessionHeader): boolean {
|
||||
&& a.parentSession === b.parentSession
|
||||
&& a.seedLength === b.seedLength
|
||||
&& (a.delegationDepth ?? 0) === (b.delegationDepth ?? 0)
|
||||
&& a.sandboxMode === b.sandboxMode
|
||||
&& a.approvalPolicy === b.approvalPolicy
|
||||
}
|
||||
|
||||
function rowHeader(row: SessionHeaderRow): SessionHeader {
|
||||
@@ -885,8 +881,6 @@ function rowHeader(row: SessionHeaderRow): SessionHeader {
|
||||
...row.parent_session === null ? {} : { parentSession: row.parent_session as SessionId },
|
||||
...row.seed_length === null ? {} : { seedLength: row.seed_length },
|
||||
...row.delegation_depth === null ? {} : { delegationDepth: row.delegation_depth },
|
||||
...row.sandbox_mode === null ? {} : { sandboxMode: row.sandbox_mode },
|
||||
...row.approval_policy === null ? {} : { approvalPolicy: row.approval_policy },
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { mkdir, open } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
|
||||
/** Current derived-index schema version. Incompatible versions reset in place. */
|
||||
export const SESSION_QUERY_SQLITE_SCHEMA_VERSION = 6
|
||||
export const SESSION_QUERY_SQLITE_SCHEMA_VERSION = 5
|
||||
|
||||
/** SQLite application id protecting unrelated databases from derived resets. */
|
||||
export const SESSION_QUERY_SQLITE_APPLICATION_ID = 0x44534851
|
||||
@@ -117,8 +117,6 @@ function ensurePersistentSchema(db: DatabaseSync): void {
|
||||
parent_session TEXT,
|
||||
seed_length INTEGER,
|
||||
delegation_depth INTEGER,
|
||||
sandbox_mode TEXT,
|
||||
approval_policy TEXT,
|
||||
revision TEXT NOT NULL,
|
||||
generation INTEGER NOT NULL
|
||||
) STRICT
|
||||
@@ -148,8 +146,6 @@ function ensureTemporarySchema(db: DatabaseSync): void {
|
||||
parent_session TEXT,
|
||||
seed_length INTEGER,
|
||||
delegation_depth INTEGER,
|
||||
sandbox_mode TEXT,
|
||||
approval_policy TEXT,
|
||||
fingerprint TEXT NOT NULL,
|
||||
persisted INTEGER NOT NULL CHECK (persisted IN (0, 1)),
|
||||
generation INTEGER NOT NULL
|
||||
|
||||
@@ -225,40 +225,6 @@ 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',
|
||||
createUserMessage({ 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')
|
||||
|
||||
Reference in New Issue
Block a user