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:
@@ -47,9 +47,9 @@ export interface CreateAgentOptions {
|
||||
readonly sessionId: SessionId
|
||||
/**
|
||||
* Session creation metadata: validated absolute `cwd`, `parentSession`
|
||||
* fork lineage, the `seedLength` seed boundary, the `delegationDepth`
|
||||
* recursion budget, and the inherited `sandboxMode`/`approvalPolicy`
|
||||
* delegation baselines. Mirrors the corresponding fields of
|
||||
* fork lineage, the `seedLength` seed boundary, and the `delegationDepth`
|
||||
* recursion budget. Mirrors the
|
||||
* `cwd`/`parentSession`/`seedLength`/`delegationDepth` fields of
|
||||
* {@link CreateSessionOptions.meta} in dsh-session (the internal-only
|
||||
* `createdAt`, used when reconstructing a persisted session, is deliberately
|
||||
* excluded — a factory caller never sets it). This is durable session data,
|
||||
@@ -61,20 +61,14 @@ export interface CreateAgentOptions {
|
||||
readonly parentSession?: SessionId
|
||||
readonly seedLength?: number
|
||||
readonly delegationDepth?: number
|
||||
readonly sandboxMode?: string
|
||||
readonly approvalPolicy?: string
|
||||
}
|
||||
/**
|
||||
* Seed events to reconstruct the child session's log from (the fork lineage
|
||||
* primitive). When present, the factory creates the session with this event
|
||||
* prefix so `deriveMessages()`/`lastTurnNumber` continue from it — used by the
|
||||
* in-process FORK subagent backend to seed a child with a balanced
|
||||
* completed-turn prefix of the parent's log. The prefix MUST be contiguous
|
||||
* from seq 0, carry only lossless-JSON data, and be balanced (no open
|
||||
* turn/step, no dangling tool-call), or the session constructor (and the
|
||||
* dev-mode invariants replay) reject it. The factory passes the raw seed to
|
||||
* the session's durable validator/snapshot boundary. Absent for a fresh
|
||||
* (spawn) child.
|
||||
* Initial session events. A fork starts with a balanced completed-turn
|
||||
* prefix of the parent's log; creation-time log facts may follow that
|
||||
* prefix. The complete seed must be contiguous from seq 0, carry only
|
||||
* lossless-JSON data, and contain no open turn/step or dangling tool call.
|
||||
* The factory passes it to the session's durable validator/snapshot
|
||||
* boundary before publication.
|
||||
*/
|
||||
readonly seed?: readonly SessionEvent[]
|
||||
/** Per-agent options (model, …). */
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/core/session/README.md
|
||||
README.md: 5168a08d293b71a60f23ed65c434ad6d33a7cdfe
|
||||
README.zh.md: 236ee2a66efc5742765c062c20a4eadde34dffdb
|
||||
README.md: 40516d12180de9c30efd40fdffa873da20ddacb3
|
||||
README.zh.md: 43842643a3434c741f219f7b6c26622cddfae8e7
|
||||
|
||||
@@ -12,7 +12,7 @@ Creates and holds event-sourced `Session` instances. Persistence is intentionall
|
||||
|
||||
### Public API
|
||||
|
||||
- `ctx.sessions.create(id?, { seed?, meta? }?)` validates and detaches durable seed/header data, fills the version and id, defaults `createdAt` to now, publishes the session, and binds it to the calling fiber. Persisted reconstruction supplies its original `createdAt`, `seedLength`, `delegationDepth`, and the inherited `sandboxMode`/`approvalPolicy` delegation baselines.
|
||||
- `ctx.sessions.create(id?, { seed?, meta? }?)` validates and detaches durable seed/header data, fills the version and id, defaults `createdAt` to now, publishes the session, and binds it to the calling fiber. Persisted reconstruction supplies its original `createdAt`, `seedLength`, and `delegationDepth`.
|
||||
- `ctx.sessions.flush(session)` dispatches the awaited parallel durability checkpoint through the session's captured scope. Every listener starts and the call waits for all to settle before reporting failure; unpublished, detached, and stale objects reject.
|
||||
- `findLastMessageTurnEnd(events)` pairs message-triggered starts with their ends and returns the latest matched `turn/end`. Outcome consumers use this fold instead of the raw latest log event because between-turn records and non-message turns have no prompt outcome.
|
||||
- `ctx.sessions.fork(source, boundary?, childSessionId?): Session` — Resolve a live session object or id, select a seed through the inclusive `boundary` event seq (default: current last event), require that prefix to end outside an open turn, and create a live child session with lineage metadata.
|
||||
@@ -43,7 +43,7 @@ Plain class (not a Cordis Service). Create via `ctx.sessions.create()`.
|
||||
- `session.surface` exposes the readonly `SessionSurface` view owned by the session's single incremental surface manager; `replaceGeneration` changes on every committed rewrite.
|
||||
- `session.events` is a cached frozen snapshot invalidated by append; accepted events remain deeply frozen.
|
||||
- `session.seq`, `session.id` — current sequence and readonly typed identity.
|
||||
- `session.header: SessionHeader` — detached, deep-frozen creation metadata (`version`, `id`, `createdAt`, optional `cwd`/`parentSession`/`seedLength`/`delegationDepth`/`sandboxMode`/`approvalPolicy`). Construction validates the durable record and requires its id to match `session.id`.
|
||||
- `session.header: SessionHeader` — detached, deep-frozen creation metadata (`version`, `id`, `createdAt`, optional `cwd`/`parentSession`/`seedLength`/`delegationDepth`). Construction validates the durable record and requires its id to match `session.id`.
|
||||
|
||||
### Lossless JSON utilities
|
||||
|
||||
@@ -86,7 +86,7 @@ Every `SessionEvent` carries two optional top-level fields (structural metadata)
|
||||
|
||||
### Metadata types (`types.ts`)
|
||||
|
||||
- `SessionHeader` — session metadata written once when published as `Session.header`, where detachment and deep-freezing enforce immutability at runtime: `{ version, id, createdAt, cwd?, parentSession?, seedLength?, delegationDepth?, sandboxMode?, approvalPolicy? }`. Persistence loaders may return mutable detached copies of the same data type. Owned here (beside `SessionId`) because `Session.header` is typed by it; persistence backends re-export it rather than own it (which would force a package cycle).
|
||||
- `SessionHeader` — session metadata written once when published as `Session.header`, where detachment and deep-freezing enforce immutability at runtime: `{ version, id, createdAt, cwd?, parentSession?, seedLength?, delegationDepth? }`. Persistence loaders may return mutable detached copies of the same data type. Owned here (beside `SessionId`) because `Session.header` is typed by it; persistence backends re-export it rather than own it (which would force a package cycle).
|
||||
|
||||
### Extension points
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
### 公共 API
|
||||
|
||||
- `ctx.sessions.create(id?, { seed?, meta? }?)` 校验持久种子/头部数据并生成脱离副本,补齐版本和 id,在未提供 `createdAt` 时使用当前时间,发布会话并将其绑定到调用方 fiber。持久化重建会提供原始的 `createdAt`、`seedLength`、`delegationDepth`,以及继承的 `sandboxMode`/`approvalPolicy` 委派基线。
|
||||
- `ctx.sessions.create(id?, { seed?, meta? }?)` 校验持久种子/头部数据并生成脱离副本,补齐版本和 id,在未提供 `createdAt` 时使用当前时间,发布会话并将其绑定到调用方 fiber。持久化重建会提供原始的 `createdAt`、`seedLength` 和 `delegationDepth`。
|
||||
- `ctx.sessions.flush(session)` 通过会话捕获的作用域分发受等待的并行持久性检查点。每个监听器都会启动;调用会等待全部结算后才报告失败。未发布、已脱离和陈旧的对象会被拒绝。
|
||||
- `findLastMessageTurnEnd(events)` 将由消息触发的开始与结束配对,并返回最近匹配的 `turn/end`。结果消费方使用该折叠逻辑,而不直接取日志中最近的事件,因为轮次间记录和非消息轮次没有提示词结果。
|
||||
- `ctx.sessions.fork(source, boundary?, childSessionId?): Session`:解析实时会话对象或 id,选取截至 `boundary` 事件序号(含该事件)的种子(默认为当前最后一个事件),要求所选前缀结束时没有开放轮次,再创建带谱系元数据的实时子会话。
|
||||
@@ -43,7 +43,7 @@
|
||||
- `session.surface` 暴露只读 `SessionSurface` 视图,由会话唯一的增量 surface 管理器所有;每次提交重写,`replaceGeneration` 都会变化。
|
||||
- `session.events` 是按追加失效的缓存冻结快照;已接受事件保持深度冻结。
|
||||
- `session.seq`、`session.id`:当前序号和只读类型化身份。
|
||||
- `session.header: SessionHeader`:脱离、深冻结的创建元数据(`version`、`id`、`createdAt`,以及可选的 `cwd`/`parentSession`/`seedLength`/`delegationDepth`/`sandboxMode`/`approvalPolicy`)。构造时会校验持久记录,并要求其中的 id 与 `session.id` 一致。
|
||||
- `session.header: SessionHeader`:脱离、深冻结的创建元数据(`version`、`id`、`createdAt`,以及可选的 `cwd`/`parentSession`/`seedLength`/`delegationDepth`)。构造时会校验持久记录,并要求其中的 id 与 `session.id` 一致。
|
||||
|
||||
### 无损 JSON 工具
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
|
||||
### 元数据类型(`types.ts`)
|
||||
|
||||
- `SessionHeader`:会话元数据,在发布为 `Session.header` 时写入一次;脱离和深冻结保证运行时不可变:`{ version, id, createdAt, cwd?, parentSession?, seedLength?, delegationDepth?, sandboxMode?, approvalPolicy? }`。持久化 loader 可返回相同数据类型的可变脱离副本。该类型由此包与 `SessionId` 一同所有,因为 `Session.header` 以它为类型;持久化后端只是重新导出而不拥有它,否则会形成包循环依赖。
|
||||
- `SessionHeader`:会话元数据,在发布为 `Session.header` 时写入一次;脱离和深冻结保证运行时不可变:`{ version, id, createdAt, cwd?, parentSession?, seedLength?, delegationDepth? }`。持久化 loader 可返回相同数据类型的可变脱离副本。该类型由此包与 `SessionId` 一同所有,因为 `Session.header` 以它为类型;持久化后端只是重新导出而不拥有它,否则会形成包循环依赖。
|
||||
|
||||
### 扩展点
|
||||
|
||||
|
||||
@@ -143,14 +143,6 @@ function snapshotSessionHeader(id: SessionId, source?: SessionHeader): SessionHe
|
||||
&& (typeof record.delegationDepth !== 'number' || !Number.isSafeInteger(record.delegationDepth) || record.delegationDepth < 0)) {
|
||||
throw new Error('session header delegationDepth must be a non-negative safe integer')
|
||||
}
|
||||
// Neutral strings only: the owning policy packages validate the values
|
||||
// against their closed vocabularies on read (durable boundary).
|
||||
if (record.sandboxMode !== undefined && typeof record.sandboxMode !== 'string') {
|
||||
throw new Error('session header sandboxMode must be a string')
|
||||
}
|
||||
if (record.approvalPolicy !== undefined && typeof record.approvalPolicy !== 'string') {
|
||||
throw new Error('session header approvalPolicy must be a string')
|
||||
}
|
||||
return deepFreeze(record as unknown as SessionHeader)
|
||||
}
|
||||
|
||||
@@ -776,8 +768,6 @@ export class SessionStore extends Service {
|
||||
...meta?.parentSession === undefined ? {} : { parentSession: meta.parentSession },
|
||||
...meta?.seedLength === undefined ? {} : { seedLength: meta.seedLength },
|
||||
...meta?.delegationDepth === undefined ? {} : { delegationDepth: meta.delegationDepth },
|
||||
...meta?.sandboxMode === undefined ? {} : { sandboxMode: meta.sandboxMode },
|
||||
...meta?.approvalPolicy === undefined ? {} : { approvalPolicy: meta.approvalPolicy },
|
||||
}
|
||||
return new Session(sessionId, seed, header)
|
||||
}
|
||||
|
||||
@@ -64,24 +64,6 @@ export interface SessionHeader {
|
||||
* resume — a runtime-only depth would reset a resumed child to top-level.
|
||||
*/
|
||||
readonly delegationDepth?: number
|
||||
/**
|
||||
* The sandbox-mode override inherited from the delegating parent at
|
||||
* creation (the delegation-inheritance baseline). A neutral string here:
|
||||
* the policy owner (`dsh-sandbox-policy`) validates it against its closed
|
||||
* vocabulary on every read, this being a durable boundary. Absent for
|
||||
* top-level sessions and for children of unswitched parents, which keep
|
||||
* following the LIVE deployment default. Header-carried (the
|
||||
* `delegationDepth` precedent) so the baseline is durable from the creation
|
||||
* moment — no first-turn event survives every crash window, because an
|
||||
* idle injection can persist a complete turn before any prompt turn opens.
|
||||
*/
|
||||
readonly sandboxMode?: string
|
||||
/**
|
||||
* The approval-policy override inherited from the delegating parent at
|
||||
* creation. Same contract as {@link SessionHeader.sandboxMode}; validated
|
||||
* by `dsh-user-approval` on read.
|
||||
*/
|
||||
readonly approvalPolicy?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,7 +72,7 @@ export interface SessionHeader {
|
||||
* store folds into a {@link SessionHeader}.
|
||||
*/
|
||||
export interface CreateSessionOptions {
|
||||
/** Events to seed the new session with (replay/fork). */
|
||||
/** Initial log events supplied at construction (replay, fork, or creation-time facts). */
|
||||
readonly seed?: readonly SessionEvent[]
|
||||
/**
|
||||
* Storage metadata read once before publication. `seedLength` is explicit
|
||||
@@ -102,8 +84,6 @@ export interface CreateSessionOptions {
|
||||
readonly createdAt?: number
|
||||
readonly seedLength?: number
|
||||
readonly delegationDepth?: number
|
||||
readonly sandboxMode?: string
|
||||
readonly approvalPolicy?: string
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -977,8 +977,6 @@ describe('Session', () => {
|
||||
{ header: { ...base, seedLength: '1' }, error: /seedLength must be a non-negative safe integer/ },
|
||||
{ header: { ...base, seedLength: 0.5 }, error: /seedLength must be a non-negative safe integer/ },
|
||||
{ header: { ...base, seedLength: -1 }, error: /seedLength must be a non-negative safe integer/ },
|
||||
{ header: { ...base, sandboxMode: 1 }, error: /header sandboxMode must be a string/ },
|
||||
{ header: { ...base, approvalPolicy: 1 }, error: /header approvalPolicy must be a string/ },
|
||||
]
|
||||
|
||||
for (const { header, error } of cases) {
|
||||
@@ -1230,8 +1228,6 @@ describe('SessionStore', () => {
|
||||
{ meta: { delegationDepth: '1' }, error: /delegationDepth must be a non-negative safe integer/ },
|
||||
{ meta: { delegationDepth: 0.5 }, error: /delegationDepth must be a non-negative safe integer/ },
|
||||
{ meta: { delegationDepth: -1 }, error: /delegationDepth must be a non-negative safe integer/ },
|
||||
{ meta: { sandboxMode: 1 }, error: /header sandboxMode must be a string/ },
|
||||
{ meta: { approvalPolicy: 1 }, error: /header approvalPolicy must be a string/ },
|
||||
]
|
||||
|
||||
for (const [index, { meta, error }] of cases.entries()) {
|
||||
|
||||
@@ -725,7 +725,7 @@ describe('ToolRegistry', () => {
|
||||
*/
|
||||
function fakeAgent(): Agent {
|
||||
return {
|
||||
session: { events: [{ type: 'turn/start' }], header: { version: 0, id: 'fake-ask-session', createdAt: 0 }, append: () => ({}) },
|
||||
session: { events: [{ type: 'turn/start' }], append: () => ({}) },
|
||||
} as unknown as Agent
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user