fix(typert): preserve parameter metadata in resource resolvers
This commit is contained in:
1 parent
94464e2214
commit
05f94de1f9
11 files changed
+67
-24
No files matched your search
@@ -12,7 +12,7 @@ import { canOpenNativePath, nativeFileManager, nativeFileApplications, openNativ
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type {} from '@deepseek-ai/dsh-session-projection-cache'
|
||||
import { SessionQueryError, type SessionObservation } from '@deepseek-ai/dsh-session-query'
|
||||
import { Remote, RemoteError, TypertRemoteService, principalOf } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { Remote, RemoteError, TypertRemoteService, principalOf, type TypertResourceResolveInput } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { canonicalClientTimeZone } from '@deepseek-ai/dsh-util-time'
|
||||
import { assertNever } from '@deepseek-ai/dsh-util-values'
|
||||
import type {} from '@deepseek-ai/dsh-api-remote-access'
|
||||
@@ -79,13 +79,21 @@ declare module '@deepseek-ai/cordis' {
|
||||
|
||||
/**
|
||||
* Extract the Session resource from an ID or a history address.
|
||||
* @param request - first wire argument carrying a Session ID or address.
|
||||
* @returns the target Session identity when present.
|
||||
* @param input - validated wire arguments and their ordered parameter descriptions.
|
||||
* @returns the named Session identity, otherwise the first object's Session identity or history address.
|
||||
*/
|
||||
function resolveSessionResource(request?: { readonly sessionId?: SessionId; readonly address?: SessionAddress }): SessionId | undefined {
|
||||
const address = request?.address
|
||||
return request?.sessionId ?? (address === undefined ? undefined
|
||||
: address.kind === 'session' ? address.sessionId : address.childSessionId)
|
||||
function resolveSessionResource({ args, parameters }: TypertResourceResolveInput): SessionId | undefined {
|
||||
const sessionParameter = parameters.find(parameter => parameter.name === 'sessionId' || parameter.wire === 'sessionId')
|
||||
if (sessionParameter !== undefined) return args[sessionParameter.wire] as SessionId | undefined
|
||||
for (const parameter of parameters) {
|
||||
const value = args[parameter.wire]
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) continue
|
||||
const request = value as { readonly sessionId?: SessionId; readonly address?: SessionAddress }
|
||||
const address = request.address
|
||||
return request.sessionId ?? (address === undefined ? undefined
|
||||
: address.kind === 'session' ? address.sessionId : address.childSessionId)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** Session Controller deployment policy. */
|
||||
|
||||
@@ -118,13 +118,34 @@ describe('Session Controller access', () => {
|
||||
if (resource === undefined || !('resolve' in resource)) throw new Error('Session resource resolver is missing')
|
||||
for (const method of ['page', 'follow']) {
|
||||
const reader = remoteMethods(controller).find(marker => marker.method === method)?.resources?.['session']
|
||||
?? ((...args: readonly unknown[]) => resource.resolve(...args))
|
||||
?? ((request: unknown) => resource.resolve({ args: { request }, parameters: [{ name: 'request', wire: 'request' }] }))
|
||||
expect(reader?.({ address: { kind: 'session', sessionId: SHARED } })).toBe(SHARED)
|
||||
// A subagent address reads the child transcript; whether the parent's access covers it is the policy's rule.
|
||||
expect(reader?.({ address: { kind: 'subagent', parentSessionId: SHARED, childSessionId: PRIVATE } })).toBe(PRIVATE)
|
||||
}
|
||||
})
|
||||
|
||||
it('resolves named Session IDs and first-object requests without treating unrelated strings as identities', async () => {
|
||||
const { ctx } = await harness()
|
||||
const resource = ctx.typert.resources.get('session')
|
||||
if (resource === undefined || !('resolve' in resource)) throw new Error('Session resource resolver is missing')
|
||||
const resolve = (args: Readonly<Record<string, unknown>>, ...names: string[]) => resource.resolve({
|
||||
args, parameters: names.map(name => ({ name, wire: name })),
|
||||
})
|
||||
expect(resolve({ sessionId: SHARED, zzz: 'other' }, 'sessionId', 'zzz')).toBe(SHARED)
|
||||
expect(resolve({ options: { sessionId: SHARED }, yyy: 'other' }, 'options', 'yyy')).toBe(SHARED)
|
||||
expect(resolve({ options: { address: { kind: 'session', sessionId: SHARED } }, xxx: 'other' }, 'options', 'xxx')).toBe(SHARED)
|
||||
expect(resolve({ options: { address: { kind: 'subagent', parentSessionId: SHARED, childSessionId: PRIVATE } } }, 'options')).toBe(PRIVATE)
|
||||
expect(resolve({ ns: SHARED, value: 'other' }, 'ns', 'value')).toBeUndefined()
|
||||
expect(resolve({ ref: SHARED, value: 'other' }, 'ref', 'value')).toBeUndefined()
|
||||
expect(resolve({ zzz: 'other', sessionId: SHARED }, 'zzz', 'sessionId')).toBe(SHARED)
|
||||
expect(resolve({ first: {}, later: { sessionId: SHARED } }, 'first', 'later')).toBeUndefined()
|
||||
expect(resolve({ count: 1, tags: [], empty: null, options: { sessionId: SHARED } }, 'count', 'tags', 'empty', 'options')).toBe(SHARED)
|
||||
expect(resolve({}, 'sessionId')).toBeUndefined()
|
||||
expect(resolve({})).toBeUndefined()
|
||||
expect(resource.resolve({ args: { target: SHARED }, parameters: [{ name: 'sessionId', wire: 'target' }] })).toBe(SHARED)
|
||||
})
|
||||
|
||||
it('lets a global role call session/list and lists only the Sessions a Session role admits', async () => {
|
||||
const { ctx, as } = await harness()
|
||||
await ctx.plugin(TypertGatewayService)
|
||||
|
||||
Reference in new issue
Block a user