mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
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
@@ -243,7 +243,7 @@ export interface Config {
|
|||||||
## `@deepseek-ai/dsh-api-session-controller`
|
## `@deepseek-ai/dsh-api-session-controller`
|
||||||
|
|
||||||
- `inject`: `agentDefaultModel` · `agents` · `attachments` · `fileUploads` · `fs` · `llm` · `remoteAccess` · `sessions` · `sessionProjections` · `sessionQuery` · `typert` · `workspaceRegistry`
|
- `inject`: `agentDefaultModel` · `agents` · `attachments` · `fileUploads` · `fs` · `llm` · `remoteAccess` · `sessions` · `sessionProjections` · `sessionQuery` · `typert` · `workspaceRegistry`
|
||||||
- `source`: [`packages/api/session-controller/src/index.ts:92`](../packages/api/session-controller/src/index.ts)
|
- `source`: [`packages/api/session-controller/src/index.ts:100`](../packages/api/session-controller/src/index.ts)
|
||||||
|
|
||||||
```ts config-catalog
|
```ts config-catalog
|
||||||
/** Session Controller deployment policy. */
|
/** Session Controller deployment policy. */
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ export interface Config {
|
|||||||
## `@deepseek-ai/dsh-api-session-controller`
|
## `@deepseek-ai/dsh-api-session-controller`
|
||||||
|
|
||||||
- `inject`: `agentDefaultModel` · `agents` · `attachments` · `fileUploads` · `fs` · `llm` · `remoteAccess` · `sessions` · `sessionProjections` · `sessionQuery` · `typert` · `workspaceRegistry`
|
- `inject`: `agentDefaultModel` · `agents` · `attachments` · `fileUploads` · `fs` · `llm` · `remoteAccess` · `sessions` · `sessionProjections` · `sessionQuery` · `typert` · `workspaceRegistry`
|
||||||
- `source`: [`packages/api/session-controller/src/index.ts:92`](../packages/api/session-controller/src/index.ts)
|
- `source`: [`packages/api/session-controller/src/index.ts:100`](../packages/api/session-controller/src/index.ts)
|
||||||
|
|
||||||
```ts config-catalog
|
```ts config-catalog
|
||||||
/** Session Controller deployment policy. */
|
/** Session Controller deployment policy. */
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ type ResourceIds = readonly unknown[] | 'invalid'
|
|||||||
* `resources` reader of the method wins; otherwise every lookup parameter
|
* `resources` reader of the method wins; otherwise every lookup parameter
|
||||||
* whose provider declares the kind contributes its wire resource id, and so
|
* whose provider declares the kind contributes its wire resource id, and so
|
||||||
* does the Context identity of a Context method whose Host adapter declares
|
* does the Context identity of a Context method whose Host adapter declares
|
||||||
* the kind; otherwise the kind's default field registered in
|
* the kind; otherwise the kind's registered default resolver receives named
|
||||||
* `typert.resources` is read from a top-level argument or, failing that,
|
* wire values and ordered parameter descriptions. A default field instead
|
||||||
* from the first object argument.
|
* reads a top-level argument or, failing that, the first object argument.
|
||||||
* @param typert - registry holding default fields, lookup providers, and Host Context adapters.
|
* @param typert - registry holding default fields, lookup providers, and Host Context adapters.
|
||||||
* @param readers - the method's `resources` readers, or `undefined` when it declares none.
|
* @param readers - the method's `resources` readers, or `undefined` when it declares none.
|
||||||
* @param descriptor - receiver, parameter order, lookups, and cancellation slot of the method.
|
* @param descriptor - receiver, parameter order, lookups, and cancellation slot of the method.
|
||||||
@@ -53,7 +53,7 @@ export function callResources(
|
|||||||
for (const declaration of typert.resources.entries()) {
|
for (const declaration of typert.resources.entries()) {
|
||||||
sources.set(declaration.kind, () => {
|
sources.set(declaration.kind, () => {
|
||||||
if ('resolve' in declaration) {
|
if ('resolve' in declaration) {
|
||||||
return read(() => declaration.resolve(...positionalArgs), [])
|
return read(() => declaration.resolve({ args, parameters: descriptor.parameters }), [])
|
||||||
}
|
}
|
||||||
const id = defaultFieldId(descriptor, args, declaration.field)
|
const id = defaultFieldId(descriptor, args, declaration.field)
|
||||||
return Array.isArray(id) ? 'invalid' : id === undefined ? [] : [id]
|
return Array.isArray(id) ? 'invalid' : id === undefined ? [] : [id]
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
type TypertContext,
|
type TypertContext,
|
||||||
type TypertLookup,
|
type TypertLookup,
|
||||||
type TypertResource,
|
type TypertResource,
|
||||||
|
type TypertResourceResolveInput,
|
||||||
} from '@deepseek-ai/dsh-typert-protocol'
|
} from '@deepseek-ai/dsh-typert-protocol'
|
||||||
import TypertRegistry from '@deepseek-ai/dsh-typert-registry'
|
import TypertRegistry from '@deepseek-ai/dsh-typert-registry'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
@@ -872,8 +873,12 @@ describe('resource resolution', () => {
|
|||||||
const ctx = new Context()
|
const ctx = new Context()
|
||||||
roots.push(ctx)
|
roots.push(ctx)
|
||||||
await ctx.plugin(TypertRegistry)
|
await ctx.plugin(TypertRegistry)
|
||||||
const resolve = vi.fn((request?: string | { address?: { id: string } }) => {
|
const resolve = vi.fn(({ args, parameters }: TypertResourceResolveInput) => {
|
||||||
return typeof request === 'string' ? request : request?.address?.id
|
const named = parameters.find(parameter => parameter.wire === 'roomId')
|
||||||
|
if (named !== undefined) return args[named.wire] as string | undefined
|
||||||
|
const first = parameters[0]
|
||||||
|
const request = first === undefined ? undefined : args[first.wire] as { address?: { id: string } } | undefined
|
||||||
|
return request?.address?.id
|
||||||
})
|
})
|
||||||
const dispose = ctx.typert.resources.register('test-shelf', { resolve })
|
const dispose = ctx.typert.resources.register('test-shelf', { resolve })
|
||||||
expect(callResources(ctx.typert, undefined, method('roomId'), { roomId: 'r1' }, NEVER)).toEqual([ROOM])
|
expect(callResources(ctx.typert, undefined, method('roomId'), { roomId: 'r1' }, NEVER)).toEqual([ROOM])
|
||||||
@@ -882,12 +887,14 @@ describe('resource resolution', () => {
|
|||||||
resolve.mockClear()
|
resolve.mockClear()
|
||||||
const args = { other: {}, later: { address: { id: 'r1' } } }
|
const args = { other: {}, later: { address: { id: 'r1' } } }
|
||||||
expect(callResources(ctx.typert, undefined, method('other', 'later'), args, NEVER)).toEqual([GLOBAL])
|
expect(callResources(ctx.typert, undefined, method('other', 'later'), args, NEVER)).toEqual([GLOBAL])
|
||||||
expect(resolve).toHaveBeenCalledExactlyOnceWith(args.other, args.later)
|
expect(resolve).toHaveBeenCalledExactlyOnceWith({ args, parameters: method('other', 'later').parameters })
|
||||||
resolve.mockClear()
|
resolve.mockClear()
|
||||||
expect(callResources(ctx.typert, undefined, {
|
expect(callResources(ctx.typert, undefined, {
|
||||||
...method('renamed'), cancellation: { parameter: 'signal' },
|
...method('renamed'), cancellation: { parameter: 'signal' },
|
||||||
}, { renamed: { address: { id: 'r1' } } }, NEVER)).toEqual([ROOM])
|
}, { renamed: { address: { id: 'r1' } } }, NEVER)).toEqual([ROOM])
|
||||||
expect(resolve).toHaveBeenCalledExactlyOnceWith({ address: { id: 'r1' } }, NEVER)
|
expect(resolve).toHaveBeenCalledExactlyOnceWith({
|
||||||
|
args: { renamed: { address: { id: 'r1' } } }, parameters: method('renamed').parameters,
|
||||||
|
})
|
||||||
expect(callResources(ctx.typert, { 'test-shelf': () => 'override' }, method('request'), {
|
expect(callResources(ctx.typert, { 'test-shelf': () => 'override' }, method('request'), {
|
||||||
request: { address: { id: 'r1' } },
|
request: { address: { id: 'r1' } },
|
||||||
}, NEVER)).toEqual([shelf('override')])
|
}, NEVER)).toEqual([shelf('override')])
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { canOpenNativePath, nativeFileManager, nativeFileApplications, openNativ
|
|||||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||||
import type {} from '@deepseek-ai/dsh-session-projection-cache'
|
import type {} from '@deepseek-ai/dsh-session-projection-cache'
|
||||||
import { SessionQueryError, type SessionObservation } from '@deepseek-ai/dsh-session-query'
|
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 { canonicalClientTimeZone } from '@deepseek-ai/dsh-util-time'
|
||||||
import { assertNever } from '@deepseek-ai/dsh-util-values'
|
import { assertNever } from '@deepseek-ai/dsh-util-values'
|
||||||
import type {} from '@deepseek-ai/dsh-api-remote-access'
|
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.
|
* Extract the Session resource from an ID or a history address.
|
||||||
* @param request - first wire argument carrying a Session ID or address.
|
* @param input - validated wire arguments and their ordered parameter descriptions.
|
||||||
* @returns the target Session identity when present.
|
* @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 {
|
function resolveSessionResource({ args, parameters }: TypertResourceResolveInput): SessionId | undefined {
|
||||||
const address = request?.address
|
const sessionParameter = parameters.find(parameter => parameter.name === 'sessionId' || parameter.wire === 'sessionId')
|
||||||
return request?.sessionId ?? (address === undefined ? undefined
|
if (sessionParameter !== undefined) return args[sessionParameter.wire] as SessionId | undefined
|
||||||
: address.kind === 'session' ? address.sessionId : address.childSessionId)
|
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. */
|
/** 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')
|
if (resource === undefined || !('resolve' in resource)) throw new Error('Session resource resolver is missing')
|
||||||
for (const method of ['page', 'follow']) {
|
for (const method of ['page', 'follow']) {
|
||||||
const reader = remoteMethods(controller).find(marker => marker.method === method)?.resources?.['session']
|
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)
|
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.
|
// 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)
|
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 () => {
|
it('lets a global role call session/list and lists only the Sessions a Session role admits', async () => {
|
||||||
const { ctx, as } = await harness()
|
const { ctx, as } = await harness()
|
||||||
await ctx.plugin(TypertGatewayService)
|
await ctx.plugin(TypertGatewayService)
|
||||||
|
|||||||
@@ -21,8 +21,8 @@
|
|||||||
en: 63968de5fc817d06
|
en: 63968de5fc817d06
|
||||||
zh: fbe2804b2a2d70d3
|
zh: fbe2804b2a2d70d3
|
||||||
/deepseek-ai-dsh-typert-protocol/use-this-package/identifying-the-caller-and-admitting-non-owner-callers:
|
/deepseek-ai-dsh-typert-protocol/use-this-package/identifying-the-caller-and-admitting-non-owner-callers:
|
||||||
en: 78e08ae732a8f6f4
|
en: 668e1adf84ba4c50
|
||||||
zh: be15fb7599994fb9
|
zh: 56561e73ec79ee38
|
||||||
/deepseek-ai-dsh-typert-protocol/use-this-package/associating-host-objects-and-contexts-with-wire-identities:
|
/deepseek-ai-dsh-typert-protocol/use-this-package/associating-host-objects-and-contexts-with-wire-identities:
|
||||||
en: 3db384ae6f215632
|
en: 3db384ae6f215632
|
||||||
zh: 56207184b09d4018
|
zh: 56207184b09d4018
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ The invoker takes a caller-owned, mounted, unscoped Client Remote and resolves i
|
|||||||
|
|
||||||
`PeerScope.principal` is the caller identity that in-process Host code fixes when it creates the Peer; no wire field can set it, and `PeerScope.signal` aborts when the Peer scope ends. `PrincipalMap` is merge-extensible: each driver declares one JSON entry with a unique literal `kind` and no credential, because a principal can be persisted in a Session log; the built-in entry is `owner: { kind: 'owner' }`. A Remote method that records its caller on durable output calls `principalOf(this.ctx.invocation)`, which returns the principal of a non-owner caller and `undefined` for the owner and outside a Remote call, and writes the field only when it is defined.
|
`PeerScope.principal` is the caller identity that in-process Host code fixes when it creates the Peer; no wire field can set it, and `PeerScope.signal` aborts when the Peer scope ends. `PrincipalMap` is merge-extensible: each driver declares one JSON entry with a unique literal `kind` and no credential, because a principal can be persisted in a Session log; the built-in entry is `owner: { kind: 'owner' }`. A Remote method that records its caller on durable output calls `principalOf(this.ctx.invocation)`, which returns the principal of a non-owner caller and `undefined` for the owner and outside a Remote call, and writes the field only when it is defined.
|
||||||
|
|
||||||
Every Remote method is under access control: [`dsh-api-remote-access`](../../api/remote-access/README.md) decides whether a non-owner caller may reach it on the resources the call touches. `TypertResourceMap` is merge-extensible beside `TypertLookupMap`: the package that owns an identity type declares its resource kind as `TypertResource<Id>` (`dsh-session` declares `session: TypertResource<SessionId>`, `dsh-workspace` declares `workspace: TypertResource<WorkspaceId>`). A `RemoteAccessResource` is either the built-in identity-less `{ kind: 'global' }` or one declared kind with its identity; resources are independent, so access to `global` implies nothing about any session. At runtime the domain that owns a kind registers its default wire field through `typert.resources.register(kind, { field })`, which rejects `global`; a lookup provider and a Host Context adapter name the kind their wire identity carries through `resource`, so a method with an Agent lookup parameter or an Agent-scoped receiver carries a `session` resource without options. The `resources` option of `@Remote` and `@RemoteScope` maps a kind to a reader that receives the method's wire arguments as its own parameters; their types are inferred from the decorated method, with lookup parameters mapped to their wire types, so `@Remote({ name: 'page', resources: { session: request => request.address.sessionId } })` reads a field the defaults cannot. A call whose readers and defaults name no resource touches `global`; they must name every resource the method accesses, because access control never checks one they omit. A forwarded `emit` entry of `TypertForwardableEventEntry` declares `resources` readers typed by its own event's parameters, and an entry without them touches `global`. Resource resolution belongs to the [API Gateway](../../api/gateway/README.md); decisions and refusal reasons belong to [`dsh-api-remote-access`](../../api/remote-access/README.md).
|
Every Remote method is under access control: [`dsh-api-remote-access`](../../api/remote-access/README.md) decides whether a non-owner caller may reach it on the resources the call touches. `TypertResourceMap` is merge-extensible beside `TypertLookupMap`: the package that owns an identity type declares its resource kind as `TypertResource<Id>` (`dsh-session` declares `session: TypertResource<SessionId>`, `dsh-workspace` declares `workspace: TypertResource<WorkspaceId>`). A `RemoteAccessResource` is either the built-in identity-less `{ kind: 'global' }` or one declared kind with its identity; resources are independent, so access to `global` implies nothing about any session. At runtime the domain that owns a kind registers its default extraction through `typert.resources.register(kind, { field })` or `{ resolve }`, which rejects `global`. A default resolver receives `{ args, parameters }`: validated named wire values and ordered parameter descriptions containing `name` and `wire`, before lookup resolution; it returns an identity or `undefined` when absent. This lets a domain distinguish a named identity parameter from an unrelated string; a lookup provider and a Host Context adapter name the kind their wire identity carries through `resource`, so a method with an Agent lookup parameter or an Agent-scoped receiver carries a `session` resource without options. The `resources` option of `@Remote` and `@RemoteScope` maps a kind to a reader that receives the method's wire arguments as its own parameters; their types are inferred from the decorated method, with lookup parameters mapped to their wire types, so `@Remote({ name: 'page', resources: { session: request => request.address.sessionId } })` reads a field the defaults cannot. A call whose readers and defaults name no resource touches `global`; they must name every resource the method accesses, because access control never checks one they omit. A forwarded `emit` entry of `TypertForwardableEventEntry` declares `resources` readers typed by its own event's parameters, and an entry without them touches `global`. Resource resolution belongs to the [API Gateway](../../api/gateway/README.md); decisions and refusal reasons belong to [`dsh-api-remote-access`](../../api/remote-access/README.md).
|
||||||
|
|
||||||
### Associating Host objects and Contexts with wire identities
|
### Associating Host objects and Contexts with wire identities
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export class GoalService extends TypertRemoteService {
|
|||||||
|
|
||||||
`PeerScope.principal` 是进程内 Host 代码创建 Peer 时固定的调用方身份;任何 wire 字段都无法设置它,`PeerScope.signal` 则在 Peer scope 结束时中止。`PrincipalMap` 可合并扩展:每个驱动方声明一个 JSON 条目,带唯一的字面量 `kind` 且不含凭据,因为 principal 可能持久化到会话日志中;内置条目是 `owner: { kind: 'owner' }`。在持久输出上记录调用方的 Remote 方法调用 `principalOf(this.ctx.invocation)`:调用方不是 owner 时它返回该 principal,对 owner 以及在 Remote 调用之外返回 `undefined`;方法只在其有定义时写入该字段。
|
`PeerScope.principal` 是进程内 Host 代码创建 Peer 时固定的调用方身份;任何 wire 字段都无法设置它,`PeerScope.signal` 则在 Peer scope 结束时中止。`PrincipalMap` 可合并扩展:每个驱动方声明一个 JSON 条目,带唯一的字面量 `kind` 且不含凭据,因为 principal 可能持久化到会话日志中;内置条目是 `owner: { kind: 'owner' }`。在持久输出上记录调用方的 Remote 方法调用 `principalOf(this.ctx.invocation)`:调用方不是 owner 时它返回该 principal,对 owner 以及在 Remote 调用之外返回 `undefined`;方法只在其有定义时写入该字段。
|
||||||
|
|
||||||
每个 Remote 方法都受访问控制:由 [`dsh-api-remote-access`](../../api/remote-access/README.zh.md) 判定非 owner 调用方能否在该调用触及的资源上调用它。`TypertResourceMap` 与 `TypertLookupMap` 并列、可合并扩展:拥有身份类型的包以 `TypertResource<Id>` 声明自己的资源种类(`dsh-session` 声明 `session: TypertResource<SessionId>`,`dsh-workspace` 声明 `workspace: TypertResource<WorkspaceId>`)。`RemoteAccessResource` 要么是内置、无身份的 `{ kind: 'global' }`,要么是某个已声明种类加其身份;资源之间相互独立,`global` 上的访问不蕴含任何会话上的访问。运行时由拥有该种类的领域经 `typert.resources.register(kind, { field })` 登记默认 wire 字段,该登记拒绝 `global`;lookup provider 与 Host Context adapter 经 `resource` 声明其 wire 身份所属的种类,因此带 Agent 查找参数或 Agent 作用域接收器的方法无需选项即携带 `session` 资源。`@Remote` 与 `@RemoteScope` 的 `resources` 选项把种类映射到一个以方法 wire 参数为自身位置参数的读取函数;这些参数的类型从被装饰的方法推断,查找参数映射为其 wire 类型,因此 `@Remote({ name: 'page', resources: { session: request => request.address.sessionId } })` 可以读取默认规则读不到的字段。读取函数与默认规则都未给出资源的调用触及 `global`;它们必须给出方法访问的全部资源,因为访问控制从不检查被遗漏的资源。`TypertForwardableEventEntry` 中的转发 `emit` 条目可以声明 `resources` 读取函数,其类型由该事件自身的参数决定;未声明的条目触及 `global`。资源解析归 [API Gateway](../../api/gateway/README.zh.md);判定与拒绝原因归 [`dsh-api-remote-access`](../../api/remote-access/README.zh.md)。
|
每个 Remote 方法都受访问控制:由 [`dsh-api-remote-access`](../../api/remote-access/README.zh.md) 判定非 owner 调用方能否在该调用触及的资源上调用它。`TypertResourceMap` 与 `TypertLookupMap` 并列、可合并扩展:拥有身份类型的包以 `TypertResource<Id>` 声明自己的资源种类(`dsh-session` 声明 `session: TypertResource<SessionId>`,`dsh-workspace` 声明 `workspace: TypertResource<WorkspaceId>`)。`RemoteAccessResource` 要么是内置、无身份的 `{ kind: 'global' }`,要么是某个已声明种类加其身份;资源之间相互独立,`global` 上的访问不蕴含任何会话上的访问。运行时由拥有该种类的领域经 `typert.resources.register(kind, { field })` 或 `{ resolve }` 登记默认提取规则,该登记拒绝 `global`。默认 resolver 在 lookup 解析前接收 `{ args, parameters }`:已校验的命名 wire 值,以及包含 `name`、`wire` 的有序参数描述;返回身份,无匹配时返回 `undefined`。领域因此可以区分具名身份参数与无关字符串;lookup provider 与 Host Context adapter 经 `resource` 声明其 wire 身份所属的种类,因此带 Agent 查找参数或 Agent 作用域接收器的方法无需选项即携带 `session` 资源。`@Remote` 与 `@RemoteScope` 的 `resources` 选项把种类映射到一个以方法 wire 参数为自身位置参数的读取函数;这些参数的类型从被装饰的方法推断,查找参数映射为其 wire 类型,因此 `@Remote({ name: 'page', resources: { session: request => request.address.sessionId } })` 可以读取默认规则读不到的字段。读取函数与默认规则都未给出资源的调用触及 `global`;它们必须给出方法访问的全部资源,因为访问控制从不检查被遗漏的资源。`TypertForwardableEventEntry` 中的转发 `emit` 条目可以声明 `resources` 读取函数,其类型由该事件自身的参数决定;未声明的条目触及 `global`。资源解析归 [API Gateway](../../api/gateway/README.zh.md);判定与拒绝原因归 [`dsh-api-remote-access`](../../api/remote-access/README.zh.md)。
|
||||||
|
|
||||||
### 把 Host 对象与 Context 关联到 wire identity
|
### 把 Host 对象与 Context 关联到 wire identity
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export type {
|
|||||||
TypertResourceReader,
|
TypertResourceReader,
|
||||||
TypertResourceReaders,
|
TypertResourceReaders,
|
||||||
TypertResourceRegistry,
|
TypertResourceRegistry,
|
||||||
|
TypertResourceResolveInput,
|
||||||
TypertLocalRegistry,
|
TypertLocalRegistry,
|
||||||
TypertLookup,
|
TypertLookup,
|
||||||
TypertLookupDefinition,
|
TypertLookupDefinition,
|
||||||
|
|||||||
@@ -878,12 +878,18 @@ export interface TypertContextRegistry {
|
|||||||
subscribe(listener: TypertRegistryListener): TypertDisposer
|
subscribe(listener: TypertRegistryListener): TypertDisposer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Validated wire values and their ordered business parameters for resource extraction. */
|
||||||
|
export interface TypertResourceResolveInput {
|
||||||
|
readonly args: Readonly<Record<string, unknown>>
|
||||||
|
readonly parameters: readonly Pick<InvocationParameterDescriptor, 'name' | 'wire'>[]
|
||||||
|
}
|
||||||
|
|
||||||
/** Default identity extraction for one resource kind. */
|
/** Default identity extraction for one resource kind. */
|
||||||
export type TypertResourceDeclaration =
|
export type TypertResourceDeclaration =
|
||||||
| { readonly field: string }
|
| { readonly field: string }
|
||||||
| {
|
| {
|
||||||
/** Called once with spread positional wire arguments. Undefined means no matching resource. */
|
/** Called once before lookups resolve. Undefined means no matching resource. */
|
||||||
resolve(...args: readonly unknown[]): string | undefined
|
resolve(input: TypertResourceResolveInput): string | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
/** One registered resource kind with its default identity extraction. */
|
/** One registered resource kind with its default identity extraction. */
|
||||||
|
|||||||
Reference in new issue
Block a user