refactor(session-query): split model-facing tool modules
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* Caller identity, workspace authorization, and visible lineage projection.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-tool-session-query/workspace-access
|
||||
*/
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import { HarnessError } from '@deepseek-ai/dsh-llm'
|
||||
import {
|
||||
SessionId,
|
||||
type SessionEvent,
|
||||
type SessionHeader,
|
||||
type SessionId as SessionIdValue,
|
||||
} from '@deepseek-ai/dsh-session'
|
||||
import type {
|
||||
SessionLineageNode,
|
||||
SessionRecord,
|
||||
} from '@deepseek-ai/dsh-session-query'
|
||||
import type { ToolRunContext } from '@deepseek-ai/dsh-tools'
|
||||
import { serviceBoundary } from './service-boundary.ts'
|
||||
|
||||
interface Caller {
|
||||
readonly id: SessionIdValue
|
||||
readonly header: SessionHeader
|
||||
readonly events: readonly SessionEvent[]
|
||||
}
|
||||
|
||||
interface TitleView {
|
||||
readonly text: string
|
||||
readonly unavailableCode?: string
|
||||
}
|
||||
|
||||
interface CompleteTitleMap extends ReadonlyMap<SessionIdValue, TitleView> {
|
||||
get(id: SessionIdValue): TitleView
|
||||
}
|
||||
|
||||
interface AuthorizedDescendant {
|
||||
readonly record: SessionRecord
|
||||
readonly descendants: Array<AuthorizedDescendant | null>
|
||||
}
|
||||
|
||||
interface DescendantProjectionFrame {
|
||||
readonly node: SessionLineageNode
|
||||
readonly target: Array<AuthorizedDescendant | null>
|
||||
readonly next: DescendantProjectionFrame | undefined
|
||||
}
|
||||
|
||||
interface DescendantVisit {
|
||||
readonly node: AuthorizedDescendant | null
|
||||
readonly depth: number
|
||||
readonly next: DescendantVisit | undefined
|
||||
}
|
||||
|
||||
function callerOf(exec: ToolRunContext): Caller {
|
||||
const agent = exec.agent
|
||||
if (agent === undefined) {
|
||||
throw new HarnessError(
|
||||
'session query tools require an agent-bound caller',
|
||||
'SESSION_QUERY_TOOL_MISSING_AGENT',
|
||||
)
|
||||
}
|
||||
return {
|
||||
id: agent.session.id,
|
||||
header: agent.session.header,
|
||||
events: agent.session.events,
|
||||
}
|
||||
}
|
||||
|
||||
function targetId(args: { readonly session_id?: string }, caller: Caller): SessionIdValue {
|
||||
return args.session_id === undefined ? caller.id : SessionId(args.session_id)
|
||||
}
|
||||
|
||||
async function authorizeTarget(
|
||||
ctx: Context,
|
||||
caller: Caller,
|
||||
target: SessionIdValue,
|
||||
signal: AbortSignal,
|
||||
): Promise<void> {
|
||||
if (target === caller.id) return
|
||||
const cwd = caller.header.cwd
|
||||
if (cwd === undefined) throw serviceBoundary.unauthorizedTarget()
|
||||
const records = await serviceBoundary.call(ctx, signal, 'target authorization', () =>
|
||||
ctx.sessionQuery.filterSessions([
|
||||
{ kind: 'id', values: [target] },
|
||||
{ kind: 'cwd', values: [cwd] },
|
||||
], signal))
|
||||
if (records.length !== 1) throw serviceBoundary.unauthorizedTarget()
|
||||
}
|
||||
|
||||
function recordAuthorized(record: SessionRecord, caller: Caller): boolean {
|
||||
return headerAuthorized(record.header, caller)
|
||||
}
|
||||
|
||||
function headerAuthorized(header: SessionHeader, caller: Caller): boolean {
|
||||
if (header.id === caller.id) return header.cwd === caller.header.cwd
|
||||
return caller.header.cwd !== undefined && header.cwd === caller.header.cwd
|
||||
}
|
||||
|
||||
function assertObservedTargetAuthorized(
|
||||
caller: Caller,
|
||||
target: SessionIdValue,
|
||||
observed: SessionHeader,
|
||||
): void {
|
||||
if (observed.id !== target || !headerAuthorized(observed, caller)) {
|
||||
throw serviceBoundary.unauthorizedTarget()
|
||||
}
|
||||
}
|
||||
|
||||
async function authorizeSessionIds(
|
||||
ctx: Context,
|
||||
caller: Caller,
|
||||
ids: readonly SessionIdValue[],
|
||||
signal: AbortSignal,
|
||||
): Promise<ReadonlySet<SessionIdValue>> {
|
||||
const unique = [...new Set(ids)]
|
||||
const authorized = new Set<SessionIdValue>()
|
||||
if (unique.includes(caller.id)) authorized.add(caller.id)
|
||||
const cwd = caller.header.cwd
|
||||
const other = unique.filter(id => id !== caller.id)
|
||||
if (cwd === undefined || other.length === 0) return authorized
|
||||
const records = await serviceBoundary.call(ctx, signal, 'session-id authorization', () =>
|
||||
ctx.sessionQuery.filterSessions([
|
||||
{ kind: 'id', values: other },
|
||||
{ kind: 'cwd', values: [cwd] },
|
||||
], signal))
|
||||
const requested = new Set(other)
|
||||
for (const record of records) {
|
||||
if (requested.has(record.header.id) && recordAuthorized(record, caller)) {
|
||||
authorized.add(record.header.id)
|
||||
}
|
||||
}
|
||||
return authorized
|
||||
}
|
||||
|
||||
async function readTitles(
|
||||
ctx: Context,
|
||||
caller: Caller,
|
||||
ids: readonly SessionIdValue[],
|
||||
signal: AbortSignal,
|
||||
): Promise<CompleteTitleMap> {
|
||||
const result = new Map<SessionIdValue, TitleView>()
|
||||
const observations = await serviceBoundary.call(ctx, signal, 'title observation', () =>
|
||||
ctx.sessionQuery.readTitleSnapshots(ids, signal))
|
||||
for (const observation of observations) {
|
||||
if (observation.status === 'rejected') {
|
||||
result.set(observation.sessionId, unavailableTitle(ctx, observation.reason))
|
||||
continue
|
||||
}
|
||||
assertObservedTargetAuthorized(caller, observation.sessionId, observation.value.session)
|
||||
result.set(observation.sessionId, { text: observation.value.title?.title ?? 'untitled' })
|
||||
}
|
||||
return result as CompleteTitleMap
|
||||
}
|
||||
|
||||
async function readTitle(
|
||||
ctx: Context,
|
||||
caller: Caller,
|
||||
id: SessionIdValue,
|
||||
signal: AbortSignal,
|
||||
): Promise<TitleView> {
|
||||
return (await readTitles(ctx, caller, [id], signal)).get(id)
|
||||
}
|
||||
|
||||
function unavailableTitle(
|
||||
ctx: Context,
|
||||
error: unknown,
|
||||
): TitleView {
|
||||
const sanitized = serviceBoundary.sanitizeError(ctx, 'title observation item', error)
|
||||
if (sanitized.code === 'SESSION_QUERY_TOOL_UNAUTHORIZED') throw sanitized
|
||||
return { text: 'untitled', unavailableCode: sanitized.code }
|
||||
}
|
||||
|
||||
function authorizeDescendants(
|
||||
nodes: readonly SessionLineageNode[],
|
||||
caller: Caller,
|
||||
): Array<AuthorizedDescendant | null> {
|
||||
const result: Array<AuthorizedDescendant | null> = []
|
||||
let pending: DescendantProjectionFrame | undefined
|
||||
for (const node of [...nodes].reverse()) {
|
||||
pending = { node, target: result, next: pending }
|
||||
}
|
||||
while (pending !== undefined) {
|
||||
const current = pending
|
||||
pending = current.next
|
||||
if (!recordAuthorized(current.node.session, caller)) {
|
||||
current.target.push(null)
|
||||
continue
|
||||
}
|
||||
const projected: AuthorizedDescendant = {
|
||||
record: current.node.session,
|
||||
descendants: [],
|
||||
}
|
||||
current.target.push(projected)
|
||||
for (const child of [...current.node.descendants].reverse()) {
|
||||
pending = {
|
||||
node: child,
|
||||
target: projected.descendants,
|
||||
next: pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function * visitDescendants(
|
||||
nodes: readonly (AuthorizedDescendant | null)[],
|
||||
): Generator<DescendantVisit> {
|
||||
let pending: DescendantVisit | undefined
|
||||
for (const node of [...nodes].reverse()) {
|
||||
pending = { node, depth: 0, next: pending }
|
||||
}
|
||||
while (pending !== undefined) {
|
||||
const current = pending
|
||||
pending = current.next
|
||||
yield current
|
||||
if (current.node === null) continue
|
||||
for (const child of [...current.node.descendants].reverse()) {
|
||||
pending = {
|
||||
node: child,
|
||||
depth: current.depth + 1,
|
||||
next: pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function descendantIds(nodes: readonly (AuthorizedDescendant | null)[]): SessionIdValue[] {
|
||||
const ids: SessionIdValue[] = []
|
||||
for (const { node } of visitDescendants(nodes)) {
|
||||
if (node !== null) ids.push(node.record.header.id)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
function titleText(view: TitleView): string {
|
||||
return view.unavailableCode === undefined
|
||||
? view.text
|
||||
: `${view.text} (title unavailable: ${view.unavailableCode})`
|
||||
}
|
||||
|
||||
/** Workspace-scoped caller authorization, title access, and lineage projection. */
|
||||
export const workspaceAccess = {
|
||||
callerOf,
|
||||
targetId,
|
||||
authorizeTarget,
|
||||
recordAuthorized,
|
||||
assertObservedTargetAuthorized,
|
||||
authorizeSessionIds,
|
||||
readTitles,
|
||||
readTitle,
|
||||
authorizeDescendants,
|
||||
visitDescendants,
|
||||
descendantIds,
|
||||
titleText,
|
||||
}
|
||||
Reference in New Issue
Block a user