<!--
HiddenLayer APIM Policy — Interactions SaaS Integration
-->
<policies>
<inbound>
<base />
<!-- ========================================== -->
<!-- Timing -->
<!-- ========================================== -->
<set-variable name="request_start" value="@(DateTime.UtcNow)" />
<!-- ========================================== -->
<!-- OAuth 2.0 Client Credentials — with cache -->
<!-- ========================================== -->
<cache-lookup-value key="hl-access-token" variable-name="hl_cached_token" />
<choose>
<when condition="@(!context.Variables.ContainsKey("hl_cached_token"))">
<send-request mode="new" response-variable-name="hl_token_response" timeout="20" ignore-error="false">
<set-url>https://api.us.hiddenlayer.ai/oauth2/token</set-url>
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/x-www-form-urlencoded</value>
</set-header>
<set-body>@{
string enc(string s) => System.Net.WebUtility.UrlEncode(s);
var clientId = "{{hiddenlayer-client-id}}";
var clientSecret = "{{hiddenlayer-client-secret}}";
return $"grant_type=client_credentials&client_id={enc(clientId)}&client_secret={enc(clientSecret)}&audience=https://us.auth.hiddenlayer.ai/";
}</set-body>
</send-request>
<set-variable name="hl_token_json" value="@(((IResponse)context.Variables["hl_token_response"]).Body.As<JObject>())" />
<set-variable name="hl_access_token" value="@((string)((JObject)context.Variables["hl_token_json"])["access_token"])" />
<set-variable name="hl_expires_in" value="@((int?)((JObject)context.Variables["hl_token_json"])["expires_in"])" />
<cache-store-value key="hl-access-token" value="@((string)context.Variables["hl_access_token"])" duration="@((int)context.Variables["hl_expires_in"] - 60)" />
</when>
<otherwise>
<set-variable name="hl_access_token" value="@((string)context.Variables["hl_cached_token"])" />
</otherwise>
</choose>
<!-- ========================================== -->
<!-- Extract Request Metadata -->
<!-- ========================================== -->
<!-- correlation_id ties APIM trace, HL input event, and HL output event together -->
<set-variable name="correlation_id" value="@(((System.Guid)context.RequestId).ToString())" />
<!-- requester_id: caller-supplied grouping key, falls back to client IP -->
<set-variable name="requester_id" value="@{
string xff = context.Request.Headers.GetValueOrDefault("X-Forwarded-For", "");
if (!string.IsNullOrEmpty(xff)) { return xff.Split(',')[0].Trim(); }
return context.Request.IpAddress;
}" />
<!-- Preserve full request body for reuse in inbound redact and outbound scan -->
<set-variable name="original_body" value="@(context.Request.Body.As<JObject>(preserveContent: true))" />
<!-- Extract model name from request body for HL metadata -->
<set-variable name="model" value="@(((JObject)context.Variables["original_body"])["model"]?.ToString() ?? context.Api.Name)" />
<!--
Capture messages array with system role filtered out at source.
This ensures neither the input scan nor the output scan ever forwards
the system prompt to the HiddenLayer API.
-->
<set-variable name="messages" value="@{
var all = (JArray)((JObject)context.Variables["original_body"])["messages"];
if (all == null) { return null; }
var filtered = new JArray();
foreach (var msg in all) {
var m = msg as JObject;
if (m == null) { continue; }
var role = (string)m["role"];
if (!string.IsNullOrEmpty(role) && role.Equals("system", StringComparison.OrdinalIgnoreCase)) { continue; }
filtered.Add(msg);
}
return filtered;
}" />
<!-- ========================================== -->
<!-- INPUT SCAN -->
<!-- ========================================== -->
<set-variable name="hl_input_start" value="@(DateTime.UtcNow)" />
<send-request mode="new" response-variable-name="hl_input_response" timeout="10" ignore-error="true">
<set-url>https://api.hiddenlayer.ai/detection/v1/interactions</set-url>
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-header name="Authorization" exists-action="override">
<value>@("Bearer " + (string)context.Variables["hl_access_token"])</value>
</set-header>
<set-header name="HL-Project-Id" exists-action="override">
<value>{{hiddenlayer-project-id}}</value>
</set-header>
<set-header name="X-Correlation-ID" exists-action="override">
<value>@((string)context.Variables["correlation_id"])</value>
</set-header>
<set-header name="X-Requester-Id" exists-action="override">
<value>@((string)context.Variables["requester_id"])</value>
</set-header>
<set-body>@{
string inputText = null;
var original = (JObject)context.Variables["original_body"];
// /chat/completions: extract last user message from filtered messages array
// (system messages already excluded from context.Variables["messages"])
var messages = (JArray)context.Variables["messages"];
if (messages != null) {
for (int i = messages.Count - 1; i >= 0; i--) {
var m = messages[i] as JObject;
if (m == null) { continue; }
var role = (string)m["role"];
if (!string.IsNullOrEmpty(role) && role.Equals("user", StringComparison.OrdinalIgnoreCase)) {
var tok = m["content"];
if (tok != null) {
inputText = tok.Type == JTokenType.String
? (string)tok
: tok.ToString(Newtonsoft.Json.Formatting.None);
}
break;
}
}
}
// /responses endpoint: input field
else {
var input = original?["input"];
if (input?.Type == JTokenType.String) {
inputText = (string)input;
}
else if (input?.Type == JTokenType.Array) {
var arr = input as JArray;
for (int i = arr.Count - 1; i >= 0; i--) {
var item = arr[i] as JObject;
if ((string)item?["role"] == "user") {
inputText = (string)item?["content"];
break;
}
}
}
}
var payload = new JObject {
["metadata"] = new JObject {
["model"] = (string)context.Variables["model"],
["requester_id"] = (string)context.Variables["requester_id"],
["provider"] = "azure openai"
},
["input"] = new JObject {
["messages"] = new JArray()
}
};
if (!string.IsNullOrEmpty(inputText)) {
((JArray)payload["input"]["messages"]).Add(
new JObject { ["role"] = "user", ["content"] = inputText }
);
}
return payload.ToString(Newtonsoft.Json.Formatting.None);
}</set-body>
</send-request>
<set-variable name="hl_input_duration_ms" value="@((DateTime.UtcNow - (DateTime)context.Variables["hl_input_start"]).TotalMilliseconds)" />
<!-- Process input scan result -->
<choose>
<when condition="@(
context.Variables.ContainsKey("hl_input_response") &&
context.Variables["hl_input_response"] != null &&
((IResponse)context.Variables["hl_input_response"]).StatusCode >= 200 &&
((IResponse)context.Variables["hl_input_response"]).StatusCode < 300)">
<set-variable name="hl_input_result" value="@(((IResponse)context.Variables["hl_input_response"]).Body.As<JObject>())" />
<set-variable name="input_action" value="@(((JObject)context.Variables["hl_input_result"])?.SelectToken("evaluation.action")?.ToString() ?? "Allow")" />
<set-variable name="input_event_id" value="@(((JObject)context.Variables["hl_input_result"])?.SelectToken("metadata.event_id")?.ToString())" />
<choose>
<!-- BLOCK -->
<when condition="@(((string)context.Variables["input_action"]).Equals("Block", StringComparison.OrdinalIgnoreCase))">
<trace source="HiddenLayer-Inbound-SaaS" severity="error">
<message>HiddenLayer blocked inbound request</message>
<metadata name="correlation_id" value="@((string)context.Variables["correlation_id"])" />
<metadata name="event_id" value="@((string)context.Variables["input_event_id"])" />
<metadata name="threat_level" value="@(((JObject)context.Variables["hl_input_result"])?.SelectToken("evaluation.threat_level")?.ToString())" />
</trace>
<return-response>
<set-status code="403" reason="Blocked by HiddenLayer Policy" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>@{
var result = (JObject)context.Variables["hl_input_result"];
var err = new JObject {
["error"] = new JObject {
["message"] = "Request blocked by HiddenLayer security policy",
["type"] = "blocked_by_policy",
["code"] = "content_policy_violation",
["hiddenlayer"] = new JObject {
["correlation_id"] = (string)context.Variables["correlation_id"],
["event_id"] = (string)context.Variables["input_event_id"],
["phase"] = "input",
["threat_level"] = result?.SelectToken("evaluation.threat_level")?.ToString(),
["details"] = result?.SelectToken("analysis")
}
}
};
return err.ToString(Newtonsoft.Json.Formatting.None);
}</set-body>
</return-response>
</when>
<!-- REDACT -->
<when condition="@(((string)context.Variables["input_action"]).Equals("Redact", StringComparison.OrdinalIgnoreCase))">
<trace source="HiddenLayer-Inbound-SaaS" severity="information">
<message>HiddenLayer redacted inbound request</message>
<metadata name="correlation_id" value="@((string)context.Variables["correlation_id"])" />
<metadata name="event_id" value="@((string)context.Variables["input_event_id"])" />
</trace>
<set-body>@{
var body = (JObject)context.Variables["original_body"];
var result = (JObject)context.Variables["hl_input_result"];
var redacted = (string)result?.SelectToken("modified_data.input.messages[0].content") ?? "";
// Rewrite last user message in messages array
var msgs = body["messages"] as JArray;
if (msgs != null) {
for (int i = msgs.Count - 1; i >= 0; i--) {
var m = msgs[i] as JObject;
if (m != null && string.Equals((string)m["role"], "user", StringComparison.OrdinalIgnoreCase)) {
m["content"] = redacted;
break;
}
}
}
// /responses format fallback
else if (body["input"] != null) {
if (body["input"].Type == JTokenType.String) {
body["input"] = redacted;
}
else if (body["input"] is JArray arr) {
for (int i = arr.Count - 1; i >= 0; i--) {
var item = arr[i] as JObject;
if ((string)item?["role"] == "user") { item["content"] = redacted; break; }
}
}
}
return body.ToString(Newtonsoft.Json.Formatting.None);
}</set-body>
</when>
<!-- ALLOW: forward unchanged -->
<otherwise />
</choose>
</when>
<!-- HL error / timeout: fail-open -->
<otherwise>
<trace source="HiddenLayer-Inbound-SaaS" severity="error">
<message>HiddenLayer input scan failed or timed out — proceeding fail-open</message>
<metadata name="correlation_id" value="@((string)context.Variables["correlation_id"])" />
<metadata name="status_code" value="@(context.Variables.ContainsKey("hl_input_response") && context.Variables["hl_input_response"] != null ? ((IResponse)context.Variables["hl_input_response"]).StatusCode.ToString() : "null")" />
</trace>
</otherwise>
</choose>
<!-- Azure OpenAI auth — set after HL check to avoid sending to HiddenLayer -->
<set-header name="Authorization" exists-action="override">
<value>@("Bearer " + "{{aoai-api-key}}")</value>
</set-header>
<!-- Strip APIM/HL headers before forwarding to backend -->
<set-header name="Ocp-Apim-Subscription-Key" exists-action="delete" />
<set-header name="X-Requester-Id" exists-action="delete" />
<set-variable name="backend_start" value="@(DateTime.UtcNow)" />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
<set-variable name="backend_duration_ms" value="@((DateTime.UtcNow - (DateTime)context.Variables["backend_start"]).TotalMilliseconds)" />
<!-- Preserve full OpenAI response for redact rewrite if needed -->
<set-variable name="openai_response" value="@(context.Response.Body.As<JObject>(preserveContent: true))" />
<set-variable name="assistant_message" value="@(((JObject)context.Variables["openai_response"])?.SelectToken("choices[0].message"))" />
<!-- ========================================== -->
<!-- OUTPUT SCAN -->
<!-- Only the latest assistant message is sent -->
<!-- to HiddenLayer — no history, no system -->
<!-- prompt, no extra OpenAI fields. -->
<!-- ========================================== -->
<set-variable name="hl_output_start" value="@(DateTime.UtcNow)" />
<send-request mode="new" response-variable-name="hl_output_response" timeout="10" ignore-error="true">
<set-url>https://api.hiddenlayer.ai/detection/v1/interactions</set-url>
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-header name="Authorization" exists-action="override">
<value>@("Bearer " + (string)context.Variables["hl_access_token"])</value>
</set-header>
<set-header name="HL-Project-Id" exists-action="override">
<value>{{hiddenlayer-project-id}}</value>
</set-header>
<set-header name="X-Correlation-ID" exists-action="override">
<value>@((string)context.Variables["correlation_id"])</value>
</set-header>
<set-header name="X-Requester-Id" exists-action="override">
<value>@(context.Variables.ContainsKey("requester_id") ? (string)context.Variables["requester_id"] : context.Request.IpAddress)</value>
</set-header>
<set-body>@{
// Send only the latest assistant message, normalized to {role, content}.
// Strips extra OpenAI fields (tool_calls, refusal, function_call, etc.)
// and excludes all conversation history and system prompt.
var assistantMessage = context.Variables["assistant_message"] as JObject;
var outputMessages = new JArray();
if (assistantMessage != null) {
outputMessages.Add(new JObject {
["role"] = "assistant",
["content"] = assistantMessage["content"]?.ToString() ?? ""
});
}
var payload = new JObject {
["metadata"] = new JObject {
["model"] = (string)context.Variables["model"],
["requester_id"] = context.Variables.ContainsKey("requester_id")
? (string)context.Variables["requester_id"]
: context.Request.IpAddress,
["provider"] = "azure openai"
},
["output"] = new JObject {
["messages"] = outputMessages
}
};
return payload.ToString(Newtonsoft.Json.Formatting.None);
}</set-body>
</send-request>
<set-variable name="hl_output_duration_ms" value="@((DateTime.UtcNow - (DateTime)context.Variables["hl_output_start"]).TotalMilliseconds)" />
<!-- Process output scan result -->
<choose>
<when condition="@(
context.Variables.ContainsKey("hl_output_response") &&
context.Variables["hl_output_response"] != null &&
((IResponse)context.Variables["hl_output_response"]).StatusCode >= 200 &&
((IResponse)context.Variables["hl_output_response"]).StatusCode < 300)">
<set-variable name="hl_output_result" value="@(((IResponse)context.Variables["hl_output_response"]).Body.As<JObject>())" />
<set-variable name="output_action" value="@(((JObject)context.Variables["hl_output_result"])?.SelectToken("evaluation.action")?.ToString() ?? "Allow")" />
<set-variable name="output_event_id" value="@(((JObject)context.Variables["hl_output_result"])?.SelectToken("metadata.event_id")?.ToString())" />
<choose>
<!-- BLOCK -->
<when condition="@(((string)context.Variables["output_action"]).Equals("Block", StringComparison.OrdinalIgnoreCase))">
<trace source="HiddenLayer-Outbound-SaaS" severity="error">
<message>HiddenLayer blocked outbound response</message>
<metadata name="correlation_id" value="@((string)context.Variables["correlation_id"])" />
<metadata name="event_id" value="@((string)context.Variables["output_event_id"])" />
<metadata name="threat_level" value="@(((JObject)context.Variables["hl_output_result"])?.SelectToken("evaluation.threat_level")?.ToString())" />
</trace>
<return-response>
<set-status code="403" reason="Response Blocked by HiddenLayer Policy" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>@{
var result = (JObject)context.Variables["hl_output_result"];
var err = new JObject {
["error"] = new JObject {
["message"] = "Response blocked by HiddenLayer security policy",
["type"] = "response_blocked_by_policy",
["code"] = "content_policy_violation",
["hiddenlayer"] = new JObject {
["correlation_id"] = (string)context.Variables["correlation_id"],
["event_id"] = (string)context.Variables["output_event_id"],
["phase"] = "output",
["threat_level"] = result?.SelectToken("evaluation.threat_level")?.ToString(),
["details"] = result?.SelectToken("analysis")
}
}
};
return err.ToString(Newtonsoft.Json.Formatting.None);
}</set-body>
</return-response>
</when>
<!-- REDACT -->
<when condition="@(((string)context.Variables["output_action"]).Equals("Redact", StringComparison.OrdinalIgnoreCase))">
<trace source="HiddenLayer-Outbound-SaaS" severity="information">
<message>HiddenLayer redacted outbound response</message>
<metadata name="correlation_id" value="@((string)context.Variables["correlation_id"])" />
<metadata name="event_id" value="@((string)context.Variables["output_event_id"])" />
</trace>
<set-body>@{
var response = (JObject)context.Variables["openai_response"];
var result = (JObject)context.Variables["hl_output_result"];
// modified_data.output.messages contains the redacted assistant turn
var redactedMsgs = result?.SelectToken("modified_data.output.messages") as JArray;
if (redactedMsgs != null && redactedMsgs.Count > 0) {
var safeMsg = redactedMsgs[redactedMsgs.Count - 1] as JObject;
var choices = response?["choices"] as JArray;
if (choices != null && choices.Count > 0) {
var firstChoice = choices[0] as JObject;
if (firstChoice != null && safeMsg != null) {
firstChoice["message"] = safeMsg;
}
}
}
return response.ToString(Newtonsoft.Json.Formatting.None);
}</set-body>
</when>
<!-- ALLOW: return response unchanged -->
<otherwise />
</choose>
</when>
<!-- HL error / timeout: fail-open -->
<otherwise>
<trace source="HiddenLayer-Outbound-SaaS" severity="error">
<message>HiddenLayer output scan failed or timed out — returning original response fail-open</message>
<metadata name="correlation_id" value="@((string)context.Variables["correlation_id"])" />
<metadata name="status_code" value="@(context.Variables.ContainsKey("hl_output_response") && context.Variables["hl_output_response"] != null ? ((IResponse)context.Variables["hl_output_response"]).StatusCode.ToString() : "null")" />
</trace>
</otherwise>
</choose>
<!-- ========================================== -->
<!-- Timing Headers -->
<!-- ========================================== -->
<set-variable name="total_duration_ms" value="@(context.Elapsed.TotalMilliseconds)" />
<set-variable name="hl_total_ms" value="@((double)context.Variables["hl_input_duration_ms"] + (double)context.Variables["hl_output_duration_ms"])" />
<set-variable name="apim_overhead_ms" value="@((double)context.Variables["total_duration_ms"] - (double)context.Variables["hl_total_ms"] - (double)context.Variables["backend_duration_ms"])" />
<trace source="HiddenLayer-Timing" severity="information">
<message>Request timing summary</message>
<metadata name="correlation_id" value="@((string)context.Variables["correlation_id"])" />
<metadata name="total_ms" value="@(((double)context.Variables["total_duration_ms"]).ToString())" />
<metadata name="hl_input_ms" value="@(((double)context.Variables["hl_input_duration_ms"]).ToString())" />
<metadata name="hl_output_ms" value="@(((double)context.Variables["hl_output_duration_ms"]).ToString())" />
<metadata name="hl_total_ms" value="@(((double)context.Variables["hl_total_ms"]).ToString())" />
<metadata name="openai_ms" value="@(((double)context.Variables["backend_duration_ms"]).ToString())" />
<metadata name="apim_overhead_ms" value="@(((double)context.Variables["apim_overhead_ms"]).ToString())" />
<metadata name="status_code" value="@(context.Response.StatusCode.ToString())" />
</trace>
<set-header name="X-Correlation-ID" exists-action="override">
<value>@((string)context.Variables["correlation_id"])</value>
</set-header>
<set-header name="X-Total-Duration-Ms" exists-action="override">
<value>@(((double)context.Variables["total_duration_ms"]).ToString())</value>
</set-header>
<set-header name="X-HiddenLayer-Input-Ms" exists-action="override">
<value>@(((double)context.Variables["hl_input_duration_ms"]).ToString())</value>
</set-header>
<set-header name="X-HiddenLayer-Output-Ms" exists-action="override">
<value>@(((double)context.Variables["hl_output_duration_ms"]).ToString())</value>
</set-header>
<set-header name="X-OpenAI-Ms" exists-action="override">
<value>@(((double)context.Variables["backend_duration_ms"]).ToString())</value>
</set-header>
<set-header name="X-APIM-Overhead-Ms" exists-action="override">
<value>@(((double)context.Variables["apim_overhead_ms"]).ToString())</value>
</set-header>
</outbound>
<on-error>
<base />
<trace source="HiddenLayer-Policy-Error" severity="error">
<message>@("Unhandled policy error: " + (context.LastError?.Message ?? "unknown"))</message>
<metadata name="correlation_id" value="@(context.Variables.ContainsKey("correlation_id") ? (string)context.Variables["correlation_id"] : "not-set")" />
</trace>
</on-error>
</policies>