mirror of
https://github.com/luxfi/aml.git
synced 2026-09-24 00:25:20 +00:00
planes: the statistical plane is shadow until somebody arms it
api.Deployment carried `Shadow bool`, whose zero value is false — so an assembly that said nothing about the behavioural model got one contributing to verdicts. cmd/amld set it correctly, which is exactly why nothing would have caught it: the default only bites whoever writes the next Deployment. Stated from the LIVE side, the zero value is shadow. Detection has to be testable before it is activated — a new deployment scores, learns and publishes at GET /v1/aml/anomaly what it WOULD have alerted on, and contributes nothing until somebody has read that. A default that arms a statistical control for anyone who did not think about it is the one direction a default must not fail in. Found in my own work in this cycle, not reported by the review. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -810,7 +810,7 @@ processed.
|
||||
| `AML_TOKEN_KEY` | KMS-held tokenisation root, hex, ≥32 bytes. No default |
|
||||
| `AML_DEFAULT_ORG` | Label carried by the rule catalog. The catalog is the deployment's, not a tenant's |
|
||||
| `AML_BUSINESS_ZONE` | Zone business-day and business-hour rules are answered in. Default UTC |
|
||||
| `AML_ANOMALY` | `live` leaves shadow mode. Anything else scores without contributing |
|
||||
| `AML_ANOMALY` | `live` leaves shadow mode. Anything else scores without contributing. The assembly states it from the LIVE side (`api.Deployment.Live`) so the zero value is shadow — a default that armed the statistical plane for anyone who did not think about it is the one direction a default must not fail in |
|
||||
|
||||
The five planes take no configuration. A list, a suppression and a rung are all
|
||||
tenant DECLARATIONS carrying a reason and a decider, so none of them is a switch an
|
||||
|
||||
+5
-6
@@ -146,12 +146,11 @@ func main() {
|
||||
Zone: zone,
|
||||
Rate: reference.RatesFromEnv(),
|
||||
Jurisdictions: reference.JurisdictionsFromEnv(),
|
||||
// Shadow until someone has read what the model would have done.
|
||||
// Detection has to be testable before it is activated, so a new
|
||||
// deployment scores, learns and publishes at GET /v1/aml/anomaly what
|
||||
// it WOULD have alerted on, contributing nothing to any transaction's
|
||||
// outcome until AML_ANOMALY=live. The rules are unaffected.
|
||||
Shadow: os.Getenv("AML_ANOMALY") != "live",
|
||||
// The behavioural model contributes to a verdict only once somebody
|
||||
// has read what it would have done. The rules are unaffected either
|
||||
// way. See api.Deployment.Live, which is stated from the live side so
|
||||
// that the zero value is shadow.
|
||||
Live: os.Getenv("AML_ANOMALY") == "live",
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("refusing to start: %w", err)
|
||||
|
||||
+11
-6
@@ -81,11 +81,16 @@ type Deployment struct {
|
||||
Rate reference.Rates
|
||||
// Jurisdictions is the sanctioned-jurisdiction reference the rules read.
|
||||
Jurisdictions reference.Jurisdictions
|
||||
// Shadow keeps the behavioural model out of every verdict while it learns and
|
||||
// reports what it would have done. It is the safe default and it is true for
|
||||
// the zero value, because a detection that acts before anyone has read what it
|
||||
// would have done is a detection nobody chose.
|
||||
Shadow bool
|
||||
// Live lets the behavioural model contribute to a verdict.
|
||||
//
|
||||
// It is stated from the LIVE side so that the zero value is shadow. A
|
||||
// detection has to be testable before it is activated: a new deployment
|
||||
// scores, learns and publishes at GET /v1/aml/anomaly what it WOULD have
|
||||
// alerted on, and contributes nothing to any transaction's outcome until
|
||||
// somebody has read that. Written as `Shadow bool` the same field would arm
|
||||
// the model for anyone who did not think about it, which is the one direction
|
||||
// a default must never fail in.
|
||||
Live bool
|
||||
// Limit is the reporting limit transactions are judged against. Zero takes
|
||||
// the fallback.
|
||||
Limit float64
|
||||
@@ -126,7 +131,7 @@ func Wire(app core.App, d Deployment) (*Handler, error) {
|
||||
// behavioural measure reads; the model reads them to score whether a
|
||||
// transaction is unusual for the entity that made it.
|
||||
windows := velocity.New(velocity.Config{})
|
||||
model, err := anomaly.New(anomaly.Config{Shadow: d.Shadow}, windows)
|
||||
model, err := anomaly.New(anomaly.Config{Shadow: !d.Live}, windows)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the behavioural plane cannot be built: %w", err)
|
||||
}
|
||||
|
||||
@@ -163,3 +163,28 @@ func TestTheDegradationReportNamesEveryBoundedStore(t *testing.T) {
|
||||
t.Error("a tenant that has been scored reports no model")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTheBehaviouralModelIsShadowUntilSomebodyArmsIt.
|
||||
//
|
||||
// A default that fails towards "the statistical plane is now deciding" is the one
|
||||
// direction a default must not fail in: detection has to be testable before it is
|
||||
// activated, and a model that acts before anyone has read what it would have done
|
||||
// is a model nobody chose. So the field is stated from the LIVE side and the zero
|
||||
// value is shadow.
|
||||
func TestTheBehaviouralModelIsShadowUntilSomebodyArmsIt(t *testing.T) {
|
||||
app := shelves(t)
|
||||
d := deployment()
|
||||
d.Live = false
|
||||
h, err := Wire(app, d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !h.Anomaly.Config().Shadow {
|
||||
t.Error("a deployment that said nothing about the behavioural model got one that contributes to verdicts")
|
||||
}
|
||||
if armed, err := Wire(shelves(t), func() Deployment { d := deployment(); d.Live = true; return d }()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if armed.Anomaly.Config().Shadow {
|
||||
t.Error("a deployment that armed the model still got shadow")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user