provider_filter_set_index_decimal
The guardrail flagship shape in one atom: a set argument
(["VIOLENCE", "HATE"]), an index-then-field projection
(["VIOLENCE"].severityScore), and a decimal extension-method comparison
(.lessThan(decimal("0.5"))), all via the Content::Filter information
provider. Permit Read only when the document’s VIOLENCE severity is below 0.5.
The Content::Filter(string, set<string>) provider returns a per-category
record { VIOLENCE: { severityScore: decimal }, HATE: { severityScore: decimal } },
so the policy can index into ["VIOLENCE"] and compare .severityScore.
The Rhai implementation is inlined into providers.json (the CLI does not
resolve a scriptFile path); filter.rhai is kept alongside for reference.
The trace shows all three cases:
@0—"violent"scores VIOLENCE 0.90 (>= 0.5) -> DENY.@10—"safe"scores VIOLENCE 0.10 (< 0.5) -> ALLOW.@20—"hateful"scores VIOLENCE 0.10 (only HATE is high) -> ALLOW.
Referenced by guide/05-information-providers.md.
Policy
// Permit Read only when the document's VIOLENCE severity is below 0.5, via the
// Content::Filter provider. Shows a SET argument (["VIOLENCE", "HATE"]), an
// INDEX projection (["VIOLENCE"]) chained with a field access (.severityScore),
// and a decimal extension-method comparison.
@id("read_low_violence")
permit (
principal,
action == Drupe::Action::"Read",
resource
)
when {
Content::Filter(context.input.document, ["VIOLENCE", "HATE"])["VIOLENCE"].severityScore.lessThan(decimal("0.5"))
};
Schema
namespace Drupe {
type ReadInput = {
document: String
};
entity Gateway;
entity OAuthUser = {
id: String
} tags String;
action "Read" appliesTo {
principal: [OAuthUser],
resource: [Gateway],
context: {
input: ReadInput
}
};
}
Trace
@0 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { document: "violent" }) Drupe::Action::"Read"::request(input: { document: "violent" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u1")
@10 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { document: "safe" }) Drupe::Action::"Read"::request(input: { document: "safe" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u2")
@20 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { document: "hateful" }) Drupe::Action::"Read"::request(input: { document: "hateful" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u3")
Expected Output
@0 (time point 0): DENY
@10 (time point 1): ALLOW [rules: 0]
@20 (time point 2): ALLOW [rules: 0]
Provider Declarations
{
"availableProviders": {
"Content::Filter": {
"argumentTypes": [
{
"paramType": "string"
},
{
"paramType": "set",
"items": {
"paramType": "string"
}
}
],
"outputType": {
"paramType": "record",
"fields": {
"VIOLENCE": {
"paramType": "record",
"fields": {
"severityScore": {
"paramType": "decimal"
}
},
"required": [
"severityScore"
]
},
"HATE": {
"paramType": "record",
"fields": {
"severityScore": {
"paramType": "decimal"
}
},
"required": [
"severityScore"
]
}
},
"required": [
"VIOLENCE",
"HATE"
]
},
"implementation": {
"kind": "rhai",
"script": "// `Content::Filter(text, categories) -> { <CATEGORY>: { severityScore } }`.\n//\n// Takes a document and a SET of category names (the second declared\n// argument, `paramType: set`, arrives as a Rhai array). Returns a record\n// keyed by category, each with a decimal `severityScore` \u2014 so the policy\n// can project `[\"VIOLENCE\"].severityScore` and compare it. Scores here are\n// a fixed lookup per keyword; a real provider would call a classifier.\nfn score_for(text, category) {\n if text == \"violent\" && category == \"VIOLENCE\" {\n parse_decimal(\"0.90\")\n } else if text == \"hateful\" && category == \"HATE\" {\n parse_decimal(\"0.90\")\n } else {\n parse_decimal(\"0.10\")\n }\n}\n\nfn evaluate(text, categories) {\n // Defensive per the provider contract: a provider may be evaluated\n // for ANY decision event, so any argument may be absent (unit).\n // Return a conforming sentinel instead of erroring (errors are UB).\n if type_of(text) == \"()\" || type_of(categories) == \"()\" {\n return #{ VIOLENCE: #{ severityScore: parse_decimal(\"-1.0\") }, HATE: #{ severityScore: parse_decimal(\"-1.0\") } };\n }\n\n let out = #{};\n for category in categories {\n out[category] = #{ severityScore: score_for(text, category) };\n }\n out\n}\n"
}
}
}
}