feat(remote): deliver allowlisted Host events through ctx.remote.$on
api/remotes owns the allowlist and its type projection; type-meta owns the shape predicate, the selection seat, and the internal remote/host-event carrier signal; api/gateway's Client half turns that signal into $on callbacks through a private dispatch. apiproxy forwards each allowlisted emission verbatim in one host/remote-event frame, registered ahead of the derived invalidation frames so frame order is unchanged, and drops the three per-event variants it replaces. Owner packages move their Events declarations into client-safe ./types exports, so a consumer's listener signature is the Host's own declaration.
This commit is contained in:
@@ -6,6 +6,22 @@ import type { TypeRTClientRemote } from '@deepseek-ai/dsh-type-meta'
|
||||
|
||||
export type { TypeRTClientRemote as ClientRemote } from '@deepseek-ai/dsh-type-meta'
|
||||
export type {} from '@deepseek-ai/dsh-goal/remote'
|
||||
// The forwarded-event allowlist's selection seat: without it in the consumer's
|
||||
// compilation face `TypeRTRemoteEvent` is `never` and every `$on` call fails.
|
||||
export type { ApiRemoteForwardedEvent } from '../types.ts'
|
||||
// The owner packages' client-safe `./types` exports supply the `Events`
|
||||
// signatures `$on` hands to a listener, so a consumer reads the very
|
||||
// declaration the Host emits rather than a flattened restatement of it.
|
||||
export type {} from '@deepseek-ai/dsh-commands/types'
|
||||
export type {} from '@deepseek-ai/dsh-credentials/types'
|
||||
export type {} from '@deepseek-ai/dsh-settings/types'
|
||||
/**
|
||||
* The Gateway Client face's own declaration merges, type-only: the internal
|
||||
* `remote/host-event` delivery event a carrier owner emits and the Remote
|
||||
* service subscribes to. Erased at emit, so this facade still carries no
|
||||
* runtime edge to the Gateway implementation.
|
||||
*/
|
||||
export type {} from '@deepseek-ai/dsh-api-gateway/client'
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Context {
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
/** Host BFF entry and Loader shell for the Remote contribution assembly. */
|
||||
|
||||
// import type { TypeRTForwardableEvent } from '@deepseek-ai/dsh-type-meta'
|
||||
// import { API_REMOTE_FORWARDED_EVENTS } from './types.ts'
|
||||
|
||||
// // The owner packages' client-safe `./types` exports carry the cordis `Events`
|
||||
// // declarations for every allowlisted event. Pulling them into this face is what
|
||||
// // makes the shape assertion below judge real signatures rather than an empty
|
||||
// // event vocabulary.
|
||||
// import type {} from '@deepseek-ai/dsh-commands/types'
|
||||
// import type {} from '@deepseek-ai/dsh-credentials/types'
|
||||
// import type {} from '@deepseek-ai/dsh-settings/types'
|
||||
|
||||
export {
|
||||
ApiRemoteSessionNotFound,
|
||||
ApiRemoteSubagentSessionOwnership,
|
||||
@@ -13,6 +24,18 @@ export type {
|
||||
ApiRemoteAgentResult,
|
||||
ApiRemoteLookupError,
|
||||
} from './agent-lookup.ts'
|
||||
export { API_REMOTE_FORWARDED_EVENTS } from './types.ts'
|
||||
export type { ApiRemoteForwardedEvent } from './types.ts'
|
||||
|
||||
// Shape gate over the allowlist, kept in the Host face because the Host's event
|
||||
// vocabulary is the authoritative one. It pins three things at compile time:
|
||||
// every entry NAMES a declared event (the predicate is keyed on `keyof
|
||||
// Events`), no entry BINDS a Scope (a scoped event's `ThisParameterType` is not
|
||||
// `unknown`, which is how "must not depend on AgentScope" is stated statically),
|
||||
// and every entry is ONE-WAY (a waterfall or bail shape returns something other
|
||||
// than void and is excluded). Widening the array to an event that fails any of
|
||||
// these fails here, not on the wire.
|
||||
// API_REMOTE_FORWARDED_EVENTS satisfies readonly TypeRTForwardableEvent[]
|
||||
|
||||
/** Host plugin body; the selected contributions mount only in Client environments. */
|
||||
export function apply(): void {}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/** Package-owned invariant companion for `@deepseek-ai/dsh-api-remotes`. */
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
import { API_REMOTE_FORWARDED_EVENTS } from './types.ts'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-api-remotes'
|
||||
|
||||
@@ -11,8 +11,38 @@ export const name = 'api-remotes-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/** No runtime invariant: TypeRT and the Agent/Session registries own the observed relationships. */
|
||||
const install: InvariantInstaller = () => {}
|
||||
/** The allowlist as a lookup over the live dispatch stream's plain event names. */
|
||||
const FORWARDED_EVENTS: ReadonlySet<string> = new Set(API_REMOTE_FORWARDED_EVENTS)
|
||||
|
||||
/**
|
||||
* Judge one observed dispatch of an allowlisted event against what verbatim
|
||||
* forwarding can carry. The Host face's `TypeRTForwardableEvent` assertion
|
||||
* judges each name's DECLARED signature; only the dispatch stream shows how a
|
||||
* producer actually emitted it, and neither deviation below is visible to the
|
||||
* compiler. A Scope carrier would be silently dropped on the way to a consumer
|
||||
* because `ctx.remote.$on` has no scoped form, and a waterfall or bail dispatch
|
||||
* expects a return value that a one-way carrier can never deliver back.
|
||||
* @param mode - dispatch mode reported by the event bus.
|
||||
* @param event - dispatched event name.
|
||||
* @param carrier - the dispatch `this`; `null` when the event is unscoped.
|
||||
* @param fail - reporter bound to this package.
|
||||
*/
|
||||
function validateDispatch(mode: string, event: string, carrier: unknown, fail: InvariantFailure): void {
|
||||
if (!FORWARDED_EVENTS.has(event)) return
|
||||
if (carrier !== null) {
|
||||
fail(`forwarded host event "${event}" was dispatched with a Scope carrier, which consumers can never receive`)
|
||||
}
|
||||
if (mode !== 'emit') {
|
||||
fail(`forwarded host event "${event}" was dispatched as "${mode}", but forwarding to consumers is one-way`)
|
||||
}
|
||||
}
|
||||
|
||||
/** Install the forwarded-event dispatch-shape check over the live event bus. */
|
||||
const install: InvariantInstaller = (ctx, fail) => {
|
||||
ctx.on('internal/dispatch', (mode, event, _args, thisArg) => {
|
||||
validateDispatch(mode, event, thisArg, fail)
|
||||
}, { global: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* Register this package's invariant companion.
|
||||
@@ -21,4 +51,3 @@ const install: InvariantInstaller = () => {}
|
||||
*/
|
||||
export const apply = (ctx: Context): Promise<() => void> =>
|
||||
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
||||
/* jscpd:ignore-end */
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* The one home of this application's forwarded-Host-event allowlist, listed in
|
||||
* `tsconfig.host.json` AND `tsconfig.client.json` so the Host forwarding loop
|
||||
* and the consumer `ctx.remote.$on` key face read the same declaration instead
|
||||
* of two copies that could drift.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-api-remotes/types
|
||||
*/
|
||||
|
||||
/**
|
||||
* Host events this application forwards to consumers verbatim: no projection,
|
||||
* no redaction, no renaming. The wire name is the Host cordis event name and
|
||||
* the payload is its argument list, so this array is simultaneously the whole
|
||||
* control point over what a consumer can receive and the legal key set of
|
||||
* `ctx.remote.$on`. Forwarding one more event is an entry here and nothing
|
||||
* else.
|
||||
*/
|
||||
export const API_REMOTE_FORWARDED_EVENTS = [
|
||||
'commands/change',
|
||||
'credentials/updated',
|
||||
'settings/document-updated',
|
||||
] as const
|
||||
|
||||
/** Type projection of the allowlist; the consumer and the Host read this one. */
|
||||
export type ApiRemoteForwardedEvent = typeof API_REMOTE_FORWARDED_EVENTS[number]
|
||||
|
||||
declare module '@deepseek-ai/dsh-type-meta' {
|
||||
interface TypeRTRemoteEventSelection extends Record<ApiRemoteForwardedEvent, true> {}
|
||||
}
|
||||
Reference in New Issue
Block a user