ARCH: добавить policy gate ответа MCP discovery
This commit is contained in:
@@ -5,6 +5,8 @@ import {
|
||||
resolveAssistantLivingChatMemoryContext
|
||||
} from "./assistantMemoryRecapPolicy";
|
||||
import { resolveAssistantOrganizationAuthority } from "./assistantContinuityPolicy";
|
||||
import { attachAssistantMcpDiscoveryDebug } from "./assistantMcpDiscoveryDebugAttachment";
|
||||
import { applyAssistantMcpDiscoveryResponsePolicy } from "./assistantMcpDiscoveryResponsePolicy";
|
||||
|
||||
export interface AssistantLivingChatSessionScopeInput {
|
||||
knownOrganizations?: unknown[];
|
||||
@@ -305,6 +307,18 @@ export async function runAssistantLivingChatRuntime(
|
||||
};
|
||||
}
|
||||
|
||||
const mcpDiscoveryResponsePolicy = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: chatText,
|
||||
currentReplySource: livingChatSource,
|
||||
livingChatSource,
|
||||
modeDecisionReason: input.modeDecision?.reason ?? null,
|
||||
addressRuntimeMeta
|
||||
});
|
||||
if (mcpDiscoveryResponsePolicy.applied) {
|
||||
chatText = mcpDiscoveryResponsePolicy.reply_text;
|
||||
livingChatSource = mcpDiscoveryResponsePolicy.reply_source;
|
||||
}
|
||||
|
||||
const predecomposeContract =
|
||||
addressRuntimeMeta.predecomposeContract && typeof addressRuntimeMeta.predecomposeContract === "object"
|
||||
? (addressRuntimeMeta.predecomposeContract as Record<string, unknown>)
|
||||
@@ -325,6 +339,9 @@ export async function runAssistantLivingChatRuntime(
|
||||
living_router_mode: input.modeDecision?.mode ?? "chat",
|
||||
living_router_reason: input.modeDecision?.reason ?? "living_chat_signal_detected",
|
||||
living_chat_response_source: livingChatSource,
|
||||
mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy,
|
||||
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
|
||||
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied,
|
||||
living_chat_script_guard_applied: livingChatScriptGuardApplied,
|
||||
living_chat_script_guard_reason: livingChatScriptGuardReason,
|
||||
living_chat_grounding_guard_applied: livingChatGroundingGuardApplied,
|
||||
@@ -359,6 +376,6 @@ export async function runAssistantLivingChatRuntime(
|
||||
return {
|
||||
handled: true,
|
||||
chatText,
|
||||
debug
|
||||
debug: attachAssistantMcpDiscoveryDebug(debug, { addressRuntimeMeta })
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import {
|
||||
buildAssistantMcpDiscoveryResponseCandidate,
|
||||
type AssistantMcpDiscoveryResponseCandidateContract,
|
||||
type AssistantMcpDiscoveryResponseCandidateStatus
|
||||
} from "./assistantMcpDiscoveryResponseCandidate";
|
||||
import type { AssistantMcpDiscoveryRuntimeEntryPointContract } from "./assistantMcpDiscoveryRuntimeEntryPoint";
|
||||
|
||||
export const ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION =
|
||||
"assistant_mcp_discovery_response_policy_v1" as const;
|
||||
|
||||
export type AssistantMcpDiscoveryResponsePolicyDecision = "apply_candidate" | "keep_current_reply";
|
||||
|
||||
export interface ApplyAssistantMcpDiscoveryResponsePolicyInput {
|
||||
currentReply: string;
|
||||
currentReplySource?: string | null;
|
||||
livingChatSource?: string | null;
|
||||
modeDecisionReason?: string | null;
|
||||
addressRuntimeMeta?: Record<string, unknown> | null;
|
||||
entryPoint?: unknown;
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryResponsePolicyResult {
|
||||
schema_version: typeof ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION;
|
||||
policy_owner: "assistantMcpDiscoveryResponsePolicy";
|
||||
decision: AssistantMcpDiscoveryResponsePolicyDecision;
|
||||
applied: boolean;
|
||||
reply_text: string;
|
||||
reply_source: string;
|
||||
candidate: AssistantMcpDiscoveryResponseCandidateContract;
|
||||
reason_codes: string[];
|
||||
}
|
||||
|
||||
const ALLOWED_CANDIDATE_STATUSES = new Set<AssistantMcpDiscoveryResponseCandidateStatus>([
|
||||
"ready_for_guarded_use",
|
||||
"checked_sources_only_candidate",
|
||||
"clarification_candidate"
|
||||
]);
|
||||
|
||||
function toRecordObject(value: unknown): Record<string, unknown> | null {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function toNonEmptyString(value: unknown): string | null {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
const text = String(value).trim();
|
||||
return text.length > 0 ? text : null;
|
||||
}
|
||||
|
||||
function normalizeReasonCode(value: string): string | null {
|
||||
const normalized = value
|
||||
.trim()
|
||||
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toLowerCase();
|
||||
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||
}
|
||||
|
||||
function pushReason(target: string[], value: string): void {
|
||||
const normalized = normalizeReasonCode(value);
|
||||
if (normalized && !target.includes(normalized)) {
|
||||
target.push(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
function hasInternalMechanics(value: string): boolean {
|
||||
const text = value.toLowerCase();
|
||||
return (
|
||||
text.includes("query_documents") ||
|
||||
text.includes("query_movements") ||
|
||||
text.includes("primitive") ||
|
||||
text.includes("runtime_") ||
|
||||
text.includes("planner_") ||
|
||||
text.includes("catalog_") ||
|
||||
text.includes("select ")
|
||||
);
|
||||
}
|
||||
|
||||
function isMcpDiscoveryEntryPointContract(value: unknown): value is AssistantMcpDiscoveryRuntimeEntryPointContract {
|
||||
const record = toRecordObject(value);
|
||||
return (
|
||||
record?.schema_version === "assistant_mcp_discovery_runtime_entry_point_v1" &&
|
||||
record?.policy_owner === "assistantMcpDiscoveryRuntimeEntryPoint"
|
||||
);
|
||||
}
|
||||
|
||||
function resolveEntryPoint(
|
||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput
|
||||
): AssistantMcpDiscoveryRuntimeEntryPointContract | null {
|
||||
if (isMcpDiscoveryEntryPointContract(input.entryPoint)) {
|
||||
return input.entryPoint;
|
||||
}
|
||||
const runtimeMetaEntryPoint =
|
||||
input.addressRuntimeMeta?.mcpDiscoveryRuntimeEntryPoint ??
|
||||
input.addressRuntimeMeta?.assistantMcpDiscoveryRuntimeEntryPoint ??
|
||||
input.addressRuntimeMeta?.assistant_mcp_discovery_entry_point_v1;
|
||||
return isMcpDiscoveryEntryPointContract(runtimeMetaEntryPoint) ? runtimeMetaEntryPoint : null;
|
||||
}
|
||||
|
||||
function isUnsupportedCurrentTurnBoundary(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): boolean {
|
||||
return (
|
||||
input.modeDecisionReason === "unsupported_current_turn_meaning_boundary" ||
|
||||
input.livingChatSource === "deterministic_unsupported_current_turn_boundary" ||
|
||||
input.currentReplySource === "deterministic_unsupported_current_turn_boundary"
|
||||
);
|
||||
}
|
||||
|
||||
export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput
|
||||
): AssistantMcpDiscoveryResponsePolicyResult {
|
||||
const currentReply = String(input.currentReply ?? "");
|
||||
const currentReplySource =
|
||||
toNonEmptyString(input.currentReplySource) ?? toNonEmptyString(input.livingChatSource) ?? "unknown";
|
||||
const entryPoint = resolveEntryPoint(input);
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(entryPoint);
|
||||
const reasonCodes = [...candidate.reason_codes];
|
||||
|
||||
if (!entryPoint) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
|
||||
}
|
||||
if (!isUnsupportedCurrentTurnBoundary(input)) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_not_unsupported_boundary");
|
||||
}
|
||||
if (!ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status)) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed");
|
||||
}
|
||||
if (!candidate.eligible_for_future_hot_runtime) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_not_eligible");
|
||||
}
|
||||
if (!toNonEmptyString(candidate.reply_text)) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_missing_reply_text");
|
||||
}
|
||||
if (candidate.reply_text && hasInternalMechanics(candidate.reply_text)) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_contains_internal_mechanics");
|
||||
}
|
||||
|
||||
const canApply =
|
||||
Boolean(entryPoint) &&
|
||||
isUnsupportedCurrentTurnBoundary(input) &&
|
||||
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
||||
candidate.eligible_for_future_hot_runtime &&
|
||||
Boolean(toNonEmptyString(candidate.reply_text)) &&
|
||||
!hasInternalMechanics(String(candidate.reply_text ?? ""));
|
||||
|
||||
if (!canApply) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_kept_current_reply");
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryResponsePolicy",
|
||||
decision: "keep_current_reply",
|
||||
applied: false,
|
||||
reply_text: currentReply,
|
||||
reply_source: currentReplySource,
|
||||
candidate,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_applied");
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryResponsePolicy",
|
||||
decision: "apply_candidate",
|
||||
applied: true,
|
||||
reply_text: String(candidate.reply_text),
|
||||
reply_source: "mcp_discovery_response_candidate_guarded",
|
||||
candidate,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user