fix(typert): harden remote reflection boundaries
This commit is contained in:
@@ -284,6 +284,7 @@ class ScopedRemoteNamespace extends Service {
|
||||
|
||||
install(descriptor: InvocationDescriptor, projection: ScopedProjection, token: MountToken): void {
|
||||
this.assertMethodAvailable(descriptor.method)
|
||||
if (this.methods.size === 0) this.ownerCtx.set(this.name, this)
|
||||
const method = descriptor.method
|
||||
Object.defineProperty(this, method, {
|
||||
configurable: true,
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
type InvocationParameterDescriptor,
|
||||
type TypeRTCodec,
|
||||
type TypeRTGatewayBinding,
|
||||
type TypeRTLookupProvider,
|
||||
} from '@deepseek-ai/dsh-type-meta'
|
||||
import type {
|
||||
InvokeRemoteRequest,
|
||||
@@ -149,6 +148,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
payload: unknown,
|
||||
_signal: AbortSignal,
|
||||
): Promise<ConnectionRpcResult> {
|
||||
// Remote methods have no cancellation parameter yet, so disconnects do not cancel business work.
|
||||
return this.invokeRpc(endpoint, payload)
|
||||
}
|
||||
|
||||
@@ -229,10 +229,8 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
const parameters: InvocationParameterDescriptor[] = []
|
||||
const wires = new Set<string>()
|
||||
for (const name of names) {
|
||||
const matches = this.ctx.typert.lookups.keys()
|
||||
.map(key => ({ key, provider: this.ctx.typert.lookups.get(key) }))
|
||||
.filter((entry): entry is { key: string; provider: TypeRTLookupProvider } =>
|
||||
entry.provider?.parameter === name)
|
||||
const matches = this.ctx.typert.lookups.definitions()
|
||||
.filter(definition => definition.parameter === name)
|
||||
if (matches.length > 1) {
|
||||
throw new TypertGatewayError(
|
||||
'signature-invalid',
|
||||
@@ -246,7 +244,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
? { name, wire: name, source: 'json', codec: { mode: 'src-json' } }
|
||||
: {
|
||||
name,
|
||||
wire: match.provider.wire,
|
||||
wire: match.wire,
|
||||
source: 'lookup',
|
||||
lookup: match.key,
|
||||
codec: { mode: 'src-json' },
|
||||
@@ -540,7 +538,7 @@ function decode(
|
||||
field: string,
|
||||
): unknown {
|
||||
try {
|
||||
if (codec.mode === 'strict') return codec.schema.parse(value)
|
||||
if (codec.mode === 'strict') value = codec.schema.parse(value)
|
||||
assertJsonValue(value, new Set())
|
||||
return value
|
||||
} catch (cause) {
|
||||
|
||||
@@ -204,7 +204,13 @@ describe('Client TypeRT API', () => {
|
||||
})
|
||||
|
||||
it('rejects duplicate, live, scoped-service, and Context namespace collisions', async () => {
|
||||
const ctx = await bench(vi.fn<ConnectionHandle['rpc']['call']>())
|
||||
const call = vi.fn<ConnectionHandle['rpc']['call']>()
|
||||
.mockResolvedValue({ ok: true, value: { renamed: true } })
|
||||
const ctx = await bench(call)
|
||||
const agentCtx = ctx.extend({ fixtureId: 'agent-remounted' }) as FixtureContext
|
||||
ctx.typert.contexts.registerClient('fixture', {
|
||||
identity: candidate => (candidate as Context & { fixtureId?: string }).fixtureId,
|
||||
})
|
||||
const direct = directDescriptor()
|
||||
const context = contextDescriptor()
|
||||
|
||||
@@ -242,6 +248,13 @@ describe('Client TypeRT API', () => {
|
||||
package: '@fixture/multiple-scoped',
|
||||
descriptors: [directDescriptor(), contextDescriptor()],
|
||||
})
|
||||
await expect(agentCtx.goals.rename({ objective: 'remounted' })).resolves.toEqual({ renamed: true })
|
||||
expect(call).toHaveBeenLastCalledWith(
|
||||
'/api',
|
||||
'goals/rename',
|
||||
{ args: { agentId: 'agent-remounted', request: { objective: 'remounted' } } },
|
||||
expect.any(AbortSignal),
|
||||
)
|
||||
await disposeMultipleScoped()
|
||||
})
|
||||
|
||||
|
||||
@@ -370,6 +370,19 @@ describe('TypertGatewayService', () => {
|
||||
})).resolves.toEqual({ agentId: 'agent-1', title: 'ship', scope: 'direct-src' })
|
||||
})
|
||||
|
||||
it('does not downgrade an observed SRC lookup after its provider unloads', async () => {
|
||||
const { ctx, service } = await setup()
|
||||
const dispose = registerAgentLookup(ctx, { id: 'agent-1' })
|
||||
await dispose()
|
||||
|
||||
await expectCode(ctx.typertGateway.invoke({
|
||||
namespace: 'goals',
|
||||
method: 'create',
|
||||
args: { agentId: 'agent-1', request: { title: 'ship' } },
|
||||
}), 'lookup-unavailable')
|
||||
expect(service.calls).toEqual([])
|
||||
})
|
||||
|
||||
it('derives SRC Remote Context identity and preserves the scoped Proxy receiver', async () => {
|
||||
const { ctx } = await setup()
|
||||
const scoped = ctx.extend({ fixtureScope: 'agent-src' })
|
||||
@@ -657,6 +670,22 @@ describe('TypertGatewayService', () => {
|
||||
}), 'result-invalid')
|
||||
})
|
||||
|
||||
it('rejects non-JSON values after strict codec validation', async () => {
|
||||
const { ctx, service } = await setup()
|
||||
const descriptor = strictOnlyDescriptor()
|
||||
registerStrict(ctx, [{
|
||||
...descriptor,
|
||||
result: strictCodec('@fixture/gateway#UnknownResult', z.unknown()),
|
||||
}])
|
||||
service.nextResult = 1n
|
||||
|
||||
await expectCode(ctx.typertGateway.invoke({
|
||||
namespace: 'goals',
|
||||
method: 'strictOnly',
|
||||
args: { request: { title: 'ship' } },
|
||||
}), 'result-invalid')
|
||||
})
|
||||
|
||||
it.each([
|
||||
undefined,
|
||||
Number.NaN,
|
||||
|
||||
Reference in New Issue
Block a user