From e2dfca018af9dd3da581a2161d4fc0787fe09792 Mon Sep 17 00:00:00 2001 From: Robin-fang611 Date: Mon, 10 Aug 2026 11:29:01 +0800 Subject: [PATCH 1/3] fix(inference): stop canonicalizeJson crash on undefined optional prompt block fields ResolvedAgentPromptBlock optional fields (ownerId/tenantId/provenance/metadata) were assigned directly even when undefined. canonicalizeJson rejects undefined values with RUNTIME_INVALID_INPUT, so any ReAct run with a prompt block failed at startup. Conditionally spread the fields only when defined. Also applies the same guard to promptResolution/toolRefs in resolveChatAgent (EventRuntime.ts), which had the identical crash class. --- apps/server/src/services/EventRuntime.ts | 6 ++++-- packages/inference/src/agent-prompts.ts | 8 ++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/server/src/services/EventRuntime.ts b/apps/server/src/services/EventRuntime.ts index 88ac8991..67895fde 100644 --- a/apps/server/src/services/EventRuntime.ts +++ b/apps/server/src/services/EventRuntime.ts @@ -1954,9 +1954,11 @@ class EventRuntimeService { input.options?.model ?? this.resolveChatModel().model, systemInstructions, - promptResolution, + ...(promptResolution === undefined ? {} : { promptResolution }), activeSkills, - toolRefs: spec.toolRefs ?? input.options?.tools?.map((tool) => tool.name), + ...(spec.toolRefs ?? input.options?.tools?.map((tool) => tool.name) + ? { toolRefs: spec.toolRefs ?? input.options?.tools?.map((tool) => tool.name) } + : {}), }; } diff --git a/packages/inference/src/agent-prompts.ts b/packages/inference/src/agent-prompts.ts index aa1597b0..2a9b546b 100644 --- a/packages/inference/src/agent-prompts.ts +++ b/packages/inference/src/agent-prompts.ts @@ -243,10 +243,10 @@ export class AgentPromptRegistry { templateContentHash: spec.contentHash!, scope: spec.scope ?? 'global', trustLevel: spec.trustLevel ?? 'reviewed', - ownerId: spec.ownerId, - tenantId: spec.tenantId, - provenance: spec.provenance, - metadata: spec.metadata, + ...(spec.ownerId === undefined ? {} : { ownerId: spec.ownerId }), + ...(spec.tenantId === undefined ? {} : { tenantId: spec.tenantId }), + ...(spec.provenance === undefined ? {} : { provenance: spec.provenance }), + ...(spec.metadata === undefined ? {} : { metadata: spec.metadata }), }); } return { From 89d7bc4e6e643a6ca0b63b277d031b286136814a Mon Sep 17 00:00:00 2001 From: Robin-fang611 Date: Mon, 10 Aug 2026 11:29:04 +0800 Subject: [PATCH 2/3] fix(runtime): inject tool schemas into ReAct reasoning requests createCanonicalReActAgentRuntime.reason() returned an InferenceRequest without the tools field. The model received no tool definitions and resorted to fabricating JSON tool calls that were never executed. Resolve tool descriptors via ToolManager.describeTool for each context.agent.toolRefs entry and attach them to the request. --- apps/server/src/services/EventRuntime.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/server/src/services/EventRuntime.ts b/apps/server/src/services/EventRuntime.ts index 67895fde..584ed3bb 100644 --- a/apps/server/src/services/EventRuntime.ts +++ b/apps/server/src/services/EventRuntime.ts @@ -72,6 +72,7 @@ import { type InferenceProvider, type InferenceRequest, type InferenceResponse, + type InferenceToolDescriptor, type LocalInferenceDriver, type KvCacheRef, type KvCacheScope, @@ -778,12 +779,26 @@ class EventRuntimeService { private createCanonicalReActAgentRuntime(): ReActAgentRuntime { return { async reason(context) { + const toolRefs = context.agent.toolRefs ?? []; + const tools: InferenceToolDescriptor[] = toolRefs + .map((toolRef) => { + const descriptor = getToolManager().describeTool(toolRef); + if (!descriptor) return null; + return { + id: descriptor.id ?? toolRef, + name: descriptor.name ?? toolRef, + description: descriptor.description ?? toolRef, + inputSchema: descriptor.inputSchema as Record, + } as InferenceToolDescriptor; + }) + .filter((tool): tool is InferenceToolDescriptor => tool !== null); return { runId: context.runId, stepId: context.stepId, sessionId: context.memoryScope?.sessionId, agentId: context.agent.id, modelAlias: context.agent.modelAlias, + ...(tools.length === 0 ? {} : { tools }), input: { instructions: context.agent.systemInstructions, messages: context.messages, From 5aa566cb1affed8ec6d9663d4d0cb5a7a065e024 Mon Sep 17 00:00:00 2001 From: Robin-fang611 Date: Mon, 10 Aug 2026 11:29:04 +0800 Subject: [PATCH 3/3] fix(runtime): derive MCP server allowlist from tool refs for capability scoping createEffectiveAgentCapabilitySnapshot ran capabilityConstraint with empty allowedMCPServerIds for both agent and domain scopes, so any MCP-backed tool was rejected with TOOL_CAPABILITY_SCOPE_DENIED. Derive the MCP server ids from the agent's tool refs (ToolManager.describeTool serverId/capabilityId) and pass them into both constraints. Also forward the effective capability snapshot ref to the tool runner when the contract snapshot is available. --- apps/server/src/services/EventRuntime.ts | 31 +++++++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/apps/server/src/services/EventRuntime.ts b/apps/server/src/services/EventRuntime.ts index 584ed3bb..fc34c876 100644 --- a/apps/server/src/services/EventRuntime.ts +++ b/apps/server/src/services/EventRuntime.ts @@ -907,6 +907,8 @@ class EventRuntimeService { const contractSnapshotRef = request.context.contractSnapshotRef ?? (await this.ensureRunToolSnapshot(request.context.runId)); + const contractSnapshot = await this.toolSnapshotStore.get(contractSnapshotRef); + const effectiveCapabilities = contractSnapshot?.effectiveCapabilities; return this.toolRunner.run({ ...request, toolId, @@ -915,6 +917,9 @@ class EventRuntimeService { userId, sessionId, contractSnapshotRef, + ...(effectiveCapabilities === undefined + ? {} + : { capabilitySnapshotRef: contractSnapshotRef }), principal: request.context.principal ?? { id: userId, principalId: userId, @@ -1931,14 +1936,28 @@ class EventRuntimeService { : []; const availableToolIds = spec.toolRefs ?? input.options?.tools?.map((tool) => tool.name) ?? []; const capabilityMetadata = asRecord(spec.metadata); + const allowedMCPServerIds = Array.from( + new Set( + availableToolIds + .map((toolRef) => { + const descriptor = getToolManager().describeTool(toolRef); + return descriptor?.serverId ?? descriptor?.capabilityId?.split('.')[0]; + }) + .filter((serverId): serverId is string => Boolean(serverId)), + ), + ); const effectiveCapabilities = createEffectiveAgentCapabilitySnapshot({ runId: input.runId, agentId: id, principalId: userId, tenantId: stringValue(asRecord(input.metadata)?.tenantId), domainId: runContext.domainPackId, - agent: capabilityConstraint(capabilityMetadata, availableToolIds, 'agent.policy'), - domain: capabilityConstraint(workflowState, availableToolIds, 'domain.policy'), + agent: capabilityConstraint(capabilityMetadata, availableToolIds, 'agent.policy', { + allowedMCPServerIds, + }), + domain: capabilityConstraint(workflowState, availableToolIds, 'domain.policy', { + allowedMCPServerIds, + }), activeSkills, }); this.runCapabilitySnapshots.set(input.runId, effectiveCapabilities); @@ -4892,7 +4911,8 @@ function stringList(input: unknown): string[] | undefined { function capabilityConstraint( source: Record | undefined, fallbackToolIds: string[], - defaultPolicyRef: string + defaultPolicyRef: string, + extras: { allowedMCPServerIds?: string[] } = {}, ): EffectiveAgentCapabilitySnapshotInput['agent'] { const memory = stringValue(source?.memoryAccess); const sideEffect = stringValue(source?.maximumSideEffectLevel); @@ -4911,7 +4931,10 @@ function capabilityConstraint( return { allowedToolIds: stringList(source?.allowedToolIds) ?? stringList(source?.allowedTools) ?? fallbackToolIds, - allowedMCPServerIds: stringList(source?.allowedMCPServerIds), + allowedMCPServerIds: + stringList(source?.allowedMCPServerIds)?.length + ? (stringList(source?.allowedMCPServerIds) as string[]) + : extras.allowedMCPServerIds ?? [], memoryAccess, allowedExecutionProfiles: stringList(source?.allowedExecutionProfiles) ?? [], maximumSideEffectLevel,