Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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"
      }
    }
  }
}