diff --git a/src/main/runtime/rpc/dispatcher-computer-errors.test.ts b/src/main/runtime/rpc/dispatcher-computer-errors.test.ts index 862973e3913..bbc37e57ab1 100644 --- a/src/main/runtime/rpc/dispatcher-computer-errors.test.ts +++ b/src/main/runtime/rpc/dispatcher-computer-errors.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { z } from 'zod' import { RpcDispatcher } from './dispatcher' import { defineMethod, InvalidArgumentError, type RpcRequest } from './core' @@ -37,6 +37,20 @@ const METHODS = [ handler: () => { throw new InvalidArgumentError('Async validation rejected payload') } + }), + defineMethod({ + name: 'orchestration.inspectCaller', + params: z.object({}), + handler: (_params, { authenticatedCallerFingerprint }) => ({ + authenticatedCallerFingerprint + }) + }), + defineMethod({ + name: 'orchestration.federationInspectCaller', + params: z.object({}), + handler: (_params, { authenticatedCallerFingerprint }) => ({ + authenticatedCallerFingerprint + }) }) ] @@ -109,12 +123,16 @@ describe('RpcDispatcher computer-use validation errors', () => { }) }) - it('maps async validation errors to invalid_argument without shadowing Zod formatting', async () => { + it('maps async validation errors over streaming transport without shadowing Zod formatting', async () => { + const messages: string[] = [] const dispatcher = new RpcDispatcher({ runtime: makeRuntime(), methods: METHODS }) - const response = await dispatcher.dispatch(makeRequest('orchestration.invalidArgument', {})) + await dispatcher.dispatchStreaming( + makeRequest('orchestration.invalidArgument', {}), + (message) => messages.push(message) + ) - expect(response).toMatchObject({ + expect(JSON.parse(messages[0]!)).toMatchObject({ ok: false, error: { code: 'invalid_argument', @@ -122,4 +140,35 @@ describe('RpcDispatcher computer-use validation errors', () => { } }) }) + + it('forwards a paired caller fingerprint without requiring local orchestration state', async () => { + const dispatcher = new RpcDispatcher({ runtime: makeRuntime(), methods: METHODS }) + + const response = await dispatcher.dispatch(makeRequest('orchestration.inspectCaller', {}), { + authenticatedCallerFingerprint: 'paired-caller' + }) + + expect(response).toMatchObject({ + ok: true, + result: { authenticatedCallerFingerprint: 'paired-caller' } + }) + }) + + it('provides local identity to read-only federation authorization', async () => { + const getOrCreateLocalMutationCallerFingerprint = vi.fn(() => 'local-caller') + const runtime = Object.assign(makeRuntime(), { + getOrchestrationDb: () => ({ getOrCreateLocalMutationCallerFingerprint }) + }) + const dispatcher = new RpcDispatcher({ runtime, methods: METHODS }) + + const response = await dispatcher.dispatch( + makeRequest('orchestration.federationInspectCaller', {}) + ) + + expect(response).toMatchObject({ + ok: true, + result: { authenticatedCallerFingerprint: 'local-caller' } + }) + expect(getOrCreateLocalMutationCallerFingerprint).toHaveBeenCalledOnce() + }) }) diff --git a/src/main/runtime/rpc/dispatcher.ts b/src/main/runtime/rpc/dispatcher.ts index b86b30f7c52..7e7ef60f6fb 100644 --- a/src/main/runtime/rpc/dispatcher.ts +++ b/src/main/runtime/rpc/dispatcher.ts @@ -10,6 +10,7 @@ import { } from './core' import type { FeatureInteractionId } from '../../../shared/feature-interactions' +import { isOrchestrationMutation } from '../../../shared/orchestration-rpc-contract' import { errorResponse, successResponse } from './errors' import { ALL_RPC_METHODS } from './methods' import { emulatorProbe, emulatorProbeError } from '../../emulator/emulator-probe' @@ -93,7 +94,7 @@ export class RpcDispatcher { ) const authenticatedCallerFingerprint = options?.authenticatedCallerFingerprint ?? - (request.method.startsWith('orchestration.') + (needsLocalCallerFingerprint(request, effectiveParams) ? this.orchestrationMutations.getLocalAuthenticatedCallerFingerprint() : undefined) const invoke = (mutation?: DurableMutationInvocation) => { @@ -186,7 +187,7 @@ export class RpcDispatcher { ) const authenticatedCallerFingerprint = options?.authenticatedCallerFingerprint ?? - (request.method.startsWith('orchestration.') + (needsLocalCallerFingerprint(request, effectiveParams) ? this.orchestrationMutations.getLocalAuthenticatedCallerFingerprint() : undefined) const invoke = (mutation?: DurableMutationInvocation) => { @@ -303,3 +304,10 @@ export class RpcDispatcher { return { runtimeId: this.runtime.getRuntimeId() } } } + +function needsLocalCallerFingerprint(request: RpcRequest, params: unknown): boolean { + return ( + request.method.startsWith('orchestration.federation') || + (!!request.orchestrationRequestId && isOrchestrationMutation(request.method, params)) + ) +}