Skip to content

Policy

Policy answers one question, before anything external happens:

May this actor perform this proposed operation on these resources?

It is trusted configuration. The application writes it; the agent proposes work against it and can neither choose it, weaken it, nor name itself as someone else.

policy = gantry.Policy(
    name="data-agents",
    rules=[
        gantry.allow.query(sources=["analytics.*"]),
        gantry.allow.materialize(sources=["raw.*"], destinations=["agent_scratch.*"]),
        gantry.deny.materialize(destinations=["prod.*"]),
    ],
)

db = gantry.sql.connect("postgres", url=DATABASE_URL, policy=policy)

with gantry.actor.context(actor=gantry.actor.actor("agent", "etl-agent"), environment="prod"):
    run = await db.query(schemas=["analytics"])(sql)

run.admission.policy  # "data-agents"
run.admission.policy_hash  # "sha256:…"
run.admission.matched_rules  # ("allow-query-0",)

Policy decides before the engine is asked

A refused proposal is never submitted. The run exists — it is created before admission — and carries the decision, with no execution attached to it.

Defining a policy

gantry.Policy dataclass

A named set of rules, and the hash of exactly those rules.

The version is derived from the rules rather than declared beside them, so a run that records sha256:… records the configuration that actually decided it. Two policies that differ anywhere hash differently; the same rules written in the same order hash the same in any process.

Source code in gantry/policy/model.py
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
@dataclass(frozen=True, slots=True)
class Policy:
    """A named set of rules, and the hash of exactly those rules.

    The version is derived from the rules rather than declared beside them, so
    a run that records `sha256:…` records the configuration that actually
    decided it. Two policies that differ anywhere hash differently; the same
    rules written in the same order hash the same in any process.
    """

    name: str
    rules: tuple[PolicyRule, ...] = ()

    def __init__(self, name: str, rules: Sequence[PolicyRule] = ()) -> None:
        if not isinstance(name, str) or not name.strip():
            raise PolicyConfigurationError("policy name must be a non-empty string")
        if isinstance(rules, PolicyRule):
            raise PolicyConfigurationError("rules must be a list of rules, not one rule")
        materialized = tuple(rules)
        for rule in materialized:
            if not isinstance(rule, PolicyRule):
                raise PolicyConfigurationError(f"rules must be PolicyRule objects, got {rule!r}")
        object.__setattr__(self, "name", name.strip())
        object.__setattr__(self, "rules", tuple(_named(materialized)))

    @property
    def version(self) -> str:
        """`sha256:…` over the canonical form of the name and rules."""
        canonical = json.dumps(
            {"name": self.name, "rules": [rule.as_dict() for rule in self.rules]},
            sort_keys=True,
            separators=(",", ":"),
        )
        return f"sha256:{sha256(canonical.encode()).hexdigest()}"

    def as_dict(self) -> dict[str, object]:
        return {
            "name": self.name,
            "version": self.version,
            "rules": [rule.as_dict() for rule in self.rules],
        }

version property

version: str

sha256:… over the canonical form of the name and rules.

gantry.PolicyRule dataclass

One rule. Every dimension left as None is unconstrained, except one.

destinations is the exception, and the asymmetry is deliberate: an allow rule that does not name destinations authorizes no writes at all. Reading the wrong table is a leak; writing the wrong table destroys something, so write authority is never granted by omission. deny rules keep the plain reading — a dimension left out simply does not narrow the rule.

constraints are ceilings the request must already be under. A rule cannot raise a limit the operation configured, so trusted constraints only ever compose toward less authority.

require_confirmation does not change the answer — the rule still allows — it asks the host to tell the user before the work happens. Authority and "should someone be told" are separate questions, and collapsing them would turn every sensitive operation into a refusal.

Source code in gantry/policy/model.py
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
@dataclass(frozen=True, slots=True)
class PolicyRule:
    """One rule. Every dimension left as `None` is unconstrained, except one.

    `destinations` is the exception, and the asymmetry is deliberate: an allow
    rule that does not name destinations authorizes no writes at all. Reading
    the wrong table is a leak; writing the wrong table destroys something, so
    write authority is never granted by omission. `deny` rules keep the plain
    reading — a dimension left out simply does not narrow the rule.

    `constraints` are ceilings the request must already be under. A rule cannot
    raise a limit the operation configured, so trusted constraints only ever
    compose toward less authority.

    `require_confirmation` does not change the answer — the rule still allows —
    it asks the host to tell the user before the work happens. Authority and
    "should someone be told" are separate questions, and collapsing them would
    turn every sensitive operation into a refusal.
    """

    effect: Effect
    name: str | None = None
    require_confirmation: bool = False
    confirmation_code: ConfirmationReasonCode | str | None = None
    confirmation_message: str | None = None
    actors: tuple[str, ...] | None = None
    operations: tuple[OperationKind, ...] | None = None
    engines: tuple[str, ...] | None = None
    sources: tuple[str, ...] | None = None
    destinations: tuple[str, ...] | None = None
    environments: tuple[str, ...] | None = None
    constraints: Mapping[str, float] = field(default_factory=dict)

    def __post_init__(self) -> None:
        object.__setattr__(self, "effect", _effect(self.effect))
        if self.name is not None and not self.name.strip():
            raise PolicyConfigurationError("rule name must not be empty when given")
        object.__setattr__(self, "actors", _names(self.actors, "actors"))
        object.__setattr__(self, "engines", _names(self.engines, "engines"))
        object.__setattr__(self, "environments", _names(self.environments, "environments"))
        object.__setattr__(self, "sources", _patterns(self.sources, "sources"))
        object.__setattr__(self, "destinations", _patterns(self.destinations, "destinations"))
        object.__setattr__(self, "operations", _operations(self.operations))
        object.__setattr__(self, "constraints", _constraints(self.constraints))
        self._check_confirmation()

    def _check_confirmation(self) -> None:
        """A confirmation prompt nobody will ever see is a mistake, not a preference."""
        if self.confirmation_code is not None:
            try:
                object.__setattr__(
                    self, "confirmation_code", ConfirmationReasonCode(self.confirmation_code)
                )
            except ValueError as error:
                known = ", ".join(code.value for code in ConfirmationReasonCode)
                raise PolicyConfigurationError(
                    f"unknown confirmation code: {self.confirmation_code!r}; v0 has {known}"
                ) from error
        if self.confirmation_message is not None and not self.confirmation_message.strip():
            raise PolicyConfigurationError("confirmation message must not be empty when given")
        if self.require_confirmation and self.effect is Effect.DENY:
            raise PolicyConfigurationError(
                "a deny rule cannot require confirmation: nothing it matches will run"
            )
        if not self.require_confirmation and (
            self.confirmation_code is not None or self.confirmation_message is not None
        ):
            raise PolicyConfigurationError(
                "confirmation_code and confirmation_message need require_confirmation=True, "
                "or nothing will ever show them"
            )

    def as_dict(self) -> dict[str, object]:
        payload: dict[str, object] = {"effect": self.effect.value}
        if self.name is not None:
            payload["name"] = self.name
        if self.require_confirmation:
            payload["require_confirmation"] = True
        if self.confirmation_code is not None:
            payload["confirmation_code"] = str(self.confirmation_code)
        if self.confirmation_message is not None:
            payload["confirmation_message"] = self.confirmation_message
        for key in ("actors", "engines", "sources", "destinations", "environments"):
            value = getattr(self, key)
            if value is not None:
                payload[key] = list(value)
        if self.operations is not None:
            payload["operations"] = [operation.value for operation in self.operations]
        if self.constraints:
            payload["constraints"] = dict(sorted(self.constraints.items()))
        return payload

Allow rules

Rules that grant authority. Omitting sources allows any source to be read; omitting destinations grants no write authority at all, so the writing operations require them.

gantry.policy.allow.query

query(
    *,
    name: str | None = None,
    require_confirmation: bool = False,
    confirmation_code: ConfirmationReasonCode
    | str
    | None = None,
    confirmation_message: str | None = None,
    actors: Sequence[str] | None = None,
    engines: Sequence[str] | None = None,
    sources: Sequence[str] | None = None,
    destinations: Sequence[str] | None = None,
    environments: Sequence[str] | None = None,
    constraints: Mapping[str, float] | None = None,
) -> PolicyRule

Allow reading. Omitting sources allows any source to be read.

destinations is for the one case where a query writes — a pipeline ending in $out, an INSERT submitted through a query operation. Omitting it grants no write authority, so a read policy stays a read policy.

Source code in gantry/policy/allow.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def query(
    *,
    name: str | None = None,
    require_confirmation: bool = False,
    confirmation_code: ConfirmationReasonCode | str | None = None,
    confirmation_message: str | None = None,
    actors: Sequence[str] | None = None,
    engines: Sequence[str] | None = None,
    sources: Sequence[str] | None = None,
    destinations: Sequence[str] | None = None,
    environments: Sequence[str] | None = None,
    constraints: Mapping[str, float] | None = None,
) -> PolicyRule:
    """Allow reading. Omitting `sources` allows any source to be read.

    `destinations` is for the one case where a query writes — a pipeline ending
    in `$out`, an `INSERT` submitted through a query operation. Omitting it
    grants no write authority, so a read policy stays a read policy.
    """
    return _allow(
        OperationKind.QUERY,
        name=name,
        require_confirmation=require_confirmation,
        confirmation_code=confirmation_code,
        confirmation_message=confirmation_message,
        actors=actors,
        engines=engines,
        sources=sources,
        destinations=destinations,
        environments=environments,
        constraints=constraints,
    )

gantry.policy.allow.materialize

materialize(
    *,
    destinations: Sequence[str],
    name: str | None = None,
    require_confirmation: bool = False,
    confirmation_code: ConfirmationReasonCode
    | str
    | None = None,
    confirmation_message: str | None = None,
    actors: Sequence[str] | None = None,
    engines: Sequence[str] | None = None,
    sources: Sequence[str] | None = None,
    environments: Sequence[str] | None = None,
    constraints: Mapping[str, float] | None = None,
) -> PolicyRule

Allow a materialization that lands in destinations.

Source code in gantry/policy/allow.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def materialize(
    *,
    destinations: Sequence[str],
    name: str | None = None,
    require_confirmation: bool = False,
    confirmation_code: ConfirmationReasonCode | str | None = None,
    confirmation_message: str | None = None,
    actors: Sequence[str] | None = None,
    engines: Sequence[str] | None = None,
    sources: Sequence[str] | None = None,
    environments: Sequence[str] | None = None,
    constraints: Mapping[str, float] | None = None,
) -> PolicyRule:
    """Allow a materialization that lands in `destinations`."""
    return _allow(
        OperationKind.MATERIALIZE,
        name=name,
        require_confirmation=require_confirmation,
        confirmation_code=confirmation_code,
        confirmation_message=confirmation_message,
        actors=actors,
        engines=engines,
        sources=sources,
        destinations=destinations,
        environments=environments,
        constraints=constraints,
    )

gantry.policy.allow.batch

batch(
    *,
    destinations: Sequence[str],
    name: str | None = None,
    require_confirmation: bool = False,
    confirmation_code: ConfirmationReasonCode
    | str
    | None = None,
    confirmation_message: str | None = None,
    actors: Sequence[str] | None = None,
    engines: Sequence[str] | None = None,
    sources: Sequence[str] | None = None,
    environments: Sequence[str] | None = None,
    constraints: Mapping[str, float] | None = None,
) -> PolicyRule

Allow a batch job that writes into destinations.

Source code in gantry/policy/allow.py
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def batch(
    *,
    destinations: Sequence[str],
    name: str | None = None,
    require_confirmation: bool = False,
    confirmation_code: ConfirmationReasonCode | str | None = None,
    confirmation_message: str | None = None,
    actors: Sequence[str] | None = None,
    engines: Sequence[str] | None = None,
    sources: Sequence[str] | None = None,
    environments: Sequence[str] | None = None,
    constraints: Mapping[str, float] | None = None,
) -> PolicyRule:
    """Allow a batch job that writes into `destinations`."""
    return _allow(
        OperationKind.BATCH,
        name=name,
        require_confirmation=require_confirmation,
        confirmation_code=confirmation_code,
        confirmation_message=confirmation_message,
        actors=actors,
        engines=engines,
        sources=sources,
        destinations=destinations,
        environments=environments,
        constraints=constraints,
    )

gantry.policy.allow.stream

stream(
    *,
    destinations: Sequence[str],
    name: str | None = None,
    require_confirmation: bool = False,
    confirmation_code: ConfirmationReasonCode
    | str
    | None = None,
    confirmation_message: str | None = None,
    actors: Sequence[str] | None = None,
    engines: Sequence[str] | None = None,
    sources: Sequence[str] | None = None,
    environments: Sequence[str] | None = None,
    constraints: Mapping[str, float] | None = None,
) -> PolicyRule

Allow submitting a streaming job that writes into destinations.

Source code in gantry/policy/allow.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
def stream(
    *,
    destinations: Sequence[str],
    name: str | None = None,
    require_confirmation: bool = False,
    confirmation_code: ConfirmationReasonCode | str | None = None,
    confirmation_message: str | None = None,
    actors: Sequence[str] | None = None,
    engines: Sequence[str] | None = None,
    sources: Sequence[str] | None = None,
    environments: Sequence[str] | None = None,
    constraints: Mapping[str, float] | None = None,
) -> PolicyRule:
    """Allow submitting a streaming job that writes into `destinations`."""
    return _allow(
        OperationKind.STREAM,
        name=name,
        require_confirmation=require_confirmation,
        confirmation_code=confirmation_code,
        confirmation_message=confirmation_message,
        actors=actors,
        engines=engines,
        sources=sources,
        destinations=destinations,
        environments=environments,
        constraints=constraints,
    )

Deny rules

A matching deny wins over every allow.

gantry.policy.deny.query

query(
    *,
    name: str | None = None,
    actors: Sequence[str] | None = None,
    engines: Sequence[str] | None = None,
    sources: Sequence[str] | None = None,
    destinations: Sequence[str] | None = None,
    environments: Sequence[str] | None = None,
    constraints: Mapping[str, float] | None = None,
) -> PolicyRule

Deny queries matching this rule.

Source code in gantry/policy/deny.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def query(
    *,
    name: str | None = None,
    actors: Sequence[str] | None = None,
    engines: Sequence[str] | None = None,
    sources: Sequence[str] | None = None,
    destinations: Sequence[str] | None = None,
    environments: Sequence[str] | None = None,
    constraints: Mapping[str, float] | None = None,
) -> PolicyRule:
    """Deny queries matching this rule."""
    return _deny(
        OperationKind.QUERY,
        name=name,
        actors=actors,
        engines=engines,
        sources=sources,
        destinations=destinations,
        environments=environments,
        constraints=constraints,
    )

gantry.policy.deny.materialize

materialize(
    *,
    destinations: Sequence[str] | None = None,
    name: str | None = None,
    actors: Sequence[str] | None = None,
    engines: Sequence[str] | None = None,
    sources: Sequence[str] | None = None,
    environments: Sequence[str] | None = None,
    constraints: Mapping[str, float] | None = None,
) -> PolicyRule

Deny materializations matching this rule.

Source code in gantry/policy/deny.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def materialize(
    *,
    destinations: Sequence[str] | None = None,
    name: str | None = None,
    actors: Sequence[str] | None = None,
    engines: Sequence[str] | None = None,
    sources: Sequence[str] | None = None,
    environments: Sequence[str] | None = None,
    constraints: Mapping[str, float] | None = None,
) -> PolicyRule:
    """Deny materializations matching this rule."""
    return _deny(
        OperationKind.MATERIALIZE,
        name=name,
        actors=actors,
        engines=engines,
        sources=sources,
        destinations=destinations,
        environments=environments,
        constraints=constraints,
    )

gantry.policy.deny.batch

batch(
    *,
    destinations: Sequence[str] | None = None,
    name: str | None = None,
    actors: Sequence[str] | None = None,
    engines: Sequence[str] | None = None,
    sources: Sequence[str] | None = None,
    environments: Sequence[str] | None = None,
    constraints: Mapping[str, float] | None = None,
) -> PolicyRule

Deny batch jobs matching this rule.

Source code in gantry/policy/deny.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def batch(
    *,
    destinations: Sequence[str] | None = None,
    name: str | None = None,
    actors: Sequence[str] | None = None,
    engines: Sequence[str] | None = None,
    sources: Sequence[str] | None = None,
    environments: Sequence[str] | None = None,
    constraints: Mapping[str, float] | None = None,
) -> PolicyRule:
    """Deny batch jobs matching this rule."""
    return _deny(
        OperationKind.BATCH,
        name=name,
        actors=actors,
        engines=engines,
        sources=sources,
        destinations=destinations,
        environments=environments,
        constraints=constraints,
    )

gantry.policy.deny.stream

stream(
    *,
    destinations: Sequence[str] | None = None,
    name: str | None = None,
    actors: Sequence[str] | None = None,
    engines: Sequence[str] | None = None,
    sources: Sequence[str] | None = None,
    environments: Sequence[str] | None = None,
    constraints: Mapping[str, float] | None = None,
) -> PolicyRule

Deny streaming submissions matching this rule.

Source code in gantry/policy/deny.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def stream(
    *,
    destinations: Sequence[str] | None = None,
    name: str | None = None,
    actors: Sequence[str] | None = None,
    engines: Sequence[str] | None = None,
    sources: Sequence[str] | None = None,
    environments: Sequence[str] | None = None,
    constraints: Mapping[str, float] | None = None,
) -> PolicyRule:
    """Deny streaming submissions matching this rule."""
    return _deny(
        OperationKind.STREAM,
        name=name,
        actors=actors,
        engines=engines,
        sources=sources,
        destinations=destinations,
        environments=environments,
        constraints=constraints,
    )

gantry.policy.deny.anything

anything(
    *,
    name: str | None = None,
    actors: Sequence[str] | None = None,
    engines: Sequence[str] | None = None,
    sources: Sequence[str] | None = None,
    destinations: Sequence[str] | None = None,
    environments: Sequence[str] | None = None,
    constraints: Mapping[str, float] | None = None,
) -> PolicyRule

Deny across every operation — gantry.deny.anything(environments=["prod"]).

Source code in gantry/policy/deny.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
def anything(
    *,
    name: str | None = None,
    actors: Sequence[str] | None = None,
    engines: Sequence[str] | None = None,
    sources: Sequence[str] | None = None,
    destinations: Sequence[str] | None = None,
    environments: Sequence[str] | None = None,
    constraints: Mapping[str, float] | None = None,
) -> PolicyRule:
    """Deny across every operation — `gantry.deny.anything(environments=["prod"])`."""
    return _deny(
        None,
        name=name,
        actors=actors,
        engines=engines,
        sources=sources,
        destinations=destinations,
        environments=environments,
        constraints=constraints,
    )

What policy decides on

gantry.PolicyRequest dataclass

One proposed operation, normalized away from any engine's syntax.

Built by provider inspection, never by the agent: inputs and outputs are what Gantry determined the proposal touches, not what it claimed to touch. constraints are the bounds the operation was configured with, so a rule can require that a request already sits under a ceiling.

unresolved is how inspection says it could not tell. A request carrying any is denied — an effect nobody can name is not one anybody authorized.

Source code in gantry/policy/request.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@dataclass(frozen=True, slots=True)
class PolicyRequest:
    """One proposed operation, normalized away from any engine's syntax.

    Built by provider inspection, never by the agent: `inputs` and `outputs`
    are what Gantry determined the proposal touches, not what it claimed to
    touch. `constraints` are the bounds the operation was configured with, so a
    rule can require that a request already sits under a ceiling.

    `unresolved` is how inspection says it could not tell. A request carrying
    any is denied — an effect nobody can name is not one anybody authorized.
    """

    actor: ActorRef
    operation: OperationKind
    engine: str
    inputs: tuple[ResourceRef, ...] = ()
    outputs: tuple[ResourceRef, ...] = ()
    environment: str | None = None
    constraints: Mapping[str, float] = field(default_factory=dict)
    unresolved: tuple[str, ...] = ()

    def __post_init__(self) -> None:
        object.__setattr__(self, "operation", OperationKind(self.operation))
        object.__setattr__(self, "inputs", tuple(self.inputs))
        object.__setattr__(self, "outputs", tuple(self.outputs))
        object.__setattr__(self, "unresolved", tuple(self.unresolved))
        environment = self.environment
        if environment is not None:
            object.__setattr__(self, "environment", environment.strip().lower() or None)

    def as_dict(self) -> dict[str, object]:
        payload: dict[str, object] = {
            "actor": self.actor.label,
            "operation": self.operation.value,
            "engine": self.engine,
            "inputs": [ref.resource for ref in self.inputs],
            "outputs": [ref.resource for ref in self.outputs],
            "environment": self.environment,
        }
        if self.constraints:
            payload["constraints"] = dict(sorted(self.constraints.items()))
        if self.unresolved:
            payload["unresolved"] = list(self.unresolved)
        return payload

What it returns

gantry.PolicyDecision dataclass

Whether the proposal is authorized, under which policy, and why.

Carried onto the run whether it allowed or refused. An allow that records nothing is indistinguishable later from an operation nobody checked.

confirmation is a second, independent answer: the operation is allowed, and the host should tell the user before it happens. A denial never carries one — there is nothing to confirm about work that will not run.

Source code in gantry/policy/decision.py
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
@dataclass(frozen=True, slots=True)
class PolicyDecision:
    """Whether the proposal is authorized, under which policy, and why.

    Carried onto the run whether it allowed or refused. An allow that records
    nothing is indistinguishable later from an operation nobody checked.

    `confirmation` is a second, independent answer: the operation is allowed,
    and the host should tell the user before it happens. A denial never carries
    one — there is nothing to confirm about work that will not run.
    """

    allowed: bool
    policy: str
    policy_version: str
    matched_rules: tuple[str, ...] = ()
    reasons: tuple[PolicyReason, ...] = ()
    confirmation: ConfirmationRequirement = NOT_REQUIRED
    evaluated_at: datetime = field(default_factory=lambda: datetime.now(UTC))

    @property
    def codes(self) -> tuple[str, ...]:
        return tuple(dict.fromkeys(reason.code.value for reason in self.reasons))

    def messages(self) -> tuple[str, ...]:
        return tuple(dict.fromkeys(str(reason) for reason in self.reasons))

    def as_dict(self) -> dict[str, object]:
        return {
            "allowed": self.allowed,
            "policy": self.policy,
            "policy_version": self.policy_version,
            "matched_rules": list(self.matched_rules),
            "reasons": [reason.as_dict() for reason in self.reasons],
            "confirmation": self.confirmation.as_dict(),
            "evaluated_at": self.evaluated_at.isoformat(),
        }

gantry.PolicyReason dataclass

One reason, tied to the resource and rule it came from.

Source code in gantry/policy/decision.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@dataclass(frozen=True, slots=True)
class PolicyReason:
    """One reason, tied to the resource and rule it came from."""

    code: PolicyReasonCode
    resource: str | None = None
    rule: str | None = None
    message: str | None = None

    def __str__(self) -> str:
        if self.message:
            return self.message
        parts = [self.code.value]
        if self.resource:
            parts.append(self.resource)
        if self.rule:
            parts.append(f"rule {self.rule}")
        return ": ".join(parts)

    def as_dict(self) -> dict[str, object]:
        return {
            "code": self.code.value,
            "resource": self.resource,
            "rule": self.rule,
            "message": str(self),
        }

gantry.PolicyReasonCode

Bases: StrEnum

Why a decision came out the way it did, in machine-readable form.

A denial that says only "permission denied" cannot be acted on by the agent that hit it, triaged by the operator who reads it, or counted by anyone asking which rule keeps firing.

Source code in gantry/policy/decision.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class PolicyReasonCode(StrEnum):
    """Why a decision came out the way it did, in machine-readable form.

    A denial that says only "permission denied" cannot be acted on by the agent
    that hit it, triaged by the operator who reads it, or counted by anyone
    asking which rule keeps firing.
    """

    NO_MATCHING_ALLOW = "NO_MATCHING_ALLOW"
    EXPLICIT_DENY = "EXPLICIT_DENY"
    ACTOR_DENIED = "ACTOR_DENIED"
    OPERATION_DENIED = "OPERATION_DENIED"
    SOURCE_DENIED = "SOURCE_DENIED"
    DESTINATION_DENIED = "DESTINATION_DENIED"
    ENVIRONMENT_DENIED = "ENVIRONMENT_DENIED"
    CONSTRAINT_EXCEEDED = "CONSTRAINT_EXCEEDED"
    RESOURCE_UNRESOLVED = "RESOURCE_UNRESOLVED"
    POLICY_INVALID = "POLICY_INVALID"

When a policy cannot mean anything

gantry.PolicyConfigurationError

Bases: ValueError

A policy that cannot mean anything, raised where it is written.

A policy is trusted configuration, so the useful time to reject it is at construction — while the author is looking at it — rather than at admission, where the agent sees a refusal it cannot act on and the operator sees a denial that was really a typo.

Source code in gantry/policy/errors.py
 7
 8
 9
10
11
12
13
14
class PolicyConfigurationError(ValueError):
    """A policy that cannot mean anything, raised where it is written.

    A policy is trusted configuration, so the useful time to reject it is at
    construction — while the author is looking at it — rather than at admission,
    where the agent sees a refusal it cannot act on and the operator sees a
    denial that was really a typo.
    """

Resource patterns

gantry.policy.normalize_pattern

normalize_pattern(pattern: str) -> str

Validate one pattern and fold it to its matching form.

Accepted: events, analytics.*, project.dataset.*, scratch_* and a bare *. Everything else — a star inside a name, an empty segment, a non-final star — is a configuration error rather than a pattern that silently matches nothing.

Source code in gantry/policy/patterns.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
def normalize_pattern(pattern: str) -> str:
    """Validate one pattern and fold it to its matching form.

    Accepted: `events`, `analytics.*`, `project.dataset.*`, `scratch_*` and a
    bare `*`. Everything else — a star inside a name, an empty segment, a
    non-final star — is a configuration error rather than a pattern that
    silently matches nothing.
    """
    if not isinstance(pattern, str):
        raise PolicyConfigurationError(f"resource pattern must be a string, not {type(pattern)}")
    cleaned = pattern.strip()
    if not cleaned:
        raise PolicyConfigurationError("resource pattern must not be empty")
    segments = cleaned.split(".")
    for index, segment in enumerate(segments):
        if not segment:
            raise PolicyConfigurationError(f"resource pattern has an empty segment: {pattern!r}")
        last = index == len(segments) - 1
        if _WILDCARD not in segment:
            continue
        if not last:
            raise PolicyConfigurationError(f"'*' is only allowed in the last segment: {pattern!r}")
        if segment.count(_WILDCARD) > 1 or not segment.endswith(_WILDCARD):
            raise PolicyConfigurationError(f"'*' is only allowed at the end of a name: {pattern!r}")
    return cleaned.lower()

gantry.policy.matches

matches(pattern: str, resource: str) -> bool

Does one normalized pattern cover this resource name?

A whole * segment covers one or more remaining segments, so analytics.* covers analytics.customers and analytics.public.customers but not analytics itself — the pattern names things in a namespace, and a namespace is not one of its own members.

A prefix* segment covers that one segment only: scratch_* matches scratch_orders and not scratch_orders.v2, because a prefix is a name, not a namespace.

Source code in gantry/policy/patterns.py
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
def matches(pattern: str, resource: str) -> bool:
    """Does one normalized pattern cover this resource name?

    A whole `*` segment covers one or more remaining segments, so `analytics.*`
    covers `analytics.customers` and `analytics.public.customers` but not
    `analytics` itself — the pattern names things *in* a namespace, and a
    namespace is not one of its own members.

    A `prefix*` segment covers that one segment only: `scratch_*` matches
    `scratch_orders` and not `scratch_orders.v2`, because a prefix is a name, not
    a namespace.
    """
    name = resource.strip().lower()
    if not name:
        return False
    if pattern == _WILDCARD:
        return True
    expected = pattern.split(".")
    actual = name.split(".")
    tail = expected[-1]
    if tail == _WILDCARD:
        prefix = expected[:-1]
        return len(actual) > len(prefix) and actual[: len(prefix)] == prefix
    if not tail.endswith(_WILDCARD):
        return expected == actual
    if len(actual) != len(expected) or actual[:-1] != expected[:-1]:
        return False
    return actual[-1].startswith(tail[:-1])