mirror of
https://github.com/luxfi/aml.git
synced 2026-09-24 00:25:20 +00:00
main
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bae63692f0 |
the corporate name is Lux Industries Inc
Lux Partners Limited is the Isle of Man company and does not appear on a public surface. Lux Industries Inc, a Delaware C corporation, is the one these copyright lines, citations and terms name. A bare "Lux Partners" is left alone unless the line says who owns something — elsewhere it is the name of a site, not a party. |
||
|
|
1b7a28d31f |
planes: one assembly, one identity for an offer, and a bound nobody can spend twice
The defining failure of the last review was not in a plane. It was that there were TWO wirings: cmd/amld assembled a Handler by hand and the tests assembled a different one, so the record fingerprint could be a struct field the suite exercised and no column stored — every retry a permanent 409 in production while every test passed. api.Wire is now the only assembly. It creates every collection, opens every shelf, makes every join and registers what has to run on a cadence; cmd/amld supplies only what an installation answers differently, the tests call the same function, and internal/source.NoLiteral reads cmd/amld to refuse a second copy. Handler completeness is checked by reflection, because a hand-written field list would be the third copy. A retry is not a second transaction. pkg/receipt resolves the identity of an OFFER once, at the top of ingest, before anything is read or written, and returns the prior answer byte for byte — so the ledger, the aggregates, the history, the alerts and the case plane all agree on one transaction instead of the ledger holding one record while the aggregates hold three. Two different facts under one reference is 409 and never 503. Because the answer is kept AFTER the work, a process that dies in between leaves no receipt, so every plane past it also recognises the transaction for itself: history does not append a second row, alerts keep the first judgement, and a judged transaction opens no second case. A bound over a COUNT of caller-sized values is not a bound. types.MaxIdent bounds every identifier at the door, which is what turns the counts downstream into figures in bytes; dictionary.MaxName does the same for a payload key, and dictionary.Ceiling is the product a test WEIGHS a full accumulator against. The field catalog's accumulator moves into roster, which admits and never removes, so per-tenant bounds multiply by ONE number an operator can hold against a pod. engine.Evaluator now keeps no compiled rules at all. A candidate's text arrives on the wire at /v1/aml/rules/test, so a compiled-rule table there was keyed by a string a caller wrote, sized by how many distinct ones a caller sends, with no cap and no removal, in the memory every institution's ingest runs in — an OOM from one authenticated caller, which on this deployment is every control off at once. A rule's compiled form is a value with an owner: the installed library, or one replay. Nothing shared, nothing to grow. Found here, not reported. Handler.history is the one function that materialises a whole retained history, so it now REQUIRES a *topology.Grant, and a Grant comes from Budget.Admit and nowhere else. An ungated whole-history read is something a caller has to write on purpose rather than something a new door can forget. The rule replay and the activation rate fold take the same one budget the model studies take. Reaching a bound is allowed; reaching it quietly is not. GET /v1/aml/load is the one door for "is anything of mine quietly degraded": the tenant's aggregates against its own ceiling in bytes with a grade, whether it has a behavioural model at all, the field catalog's pressure, and the process's counts of institutions held and refused. It names no other institution. Closing R1-R8 from the review plus R11, the two recurring classes, and the silent degradation the load door answers. Twenty-four mutations, twenty-four killed. Co-authored-by: Hanzo Dev <[email protected]> |
||
|
|
7c314d0b30 |
planes: the five surfaces a monitoring programme is operated through
Lists, suppressions, activations, the field catalog and the model plane. Each is its own package, each is Base-backed, and none of them can destroy anything — disposal is pkg/retention's decision alone, per tenant, and every one of them has a test that reads its own source for a call to Delete. lists holds the institution's own allow and deny lists, by class, normalised once so an address written two ways is one address. It reaches a rule as one term, Listed(name, value), and it refuses both a list nobody declared and an empty value: either would be a rule that is catalogued, reported as coverage, and incapable of firing. Removing a value stops it matching and keeps the row, because a list entry is the reason a payment was refused. suppress holds the decisions to keep a detection out of a queue. Reason and decider are required and neither is defaulted; a suppression naming neither a rule nor a subject is refused, because that is a kill switch. Lifting keeps the row. The narrowest cover wins, so a specific decision about one account is the one named on the activation rather than the rule-wide one behind it. watch writes every activation before anything happens to it, and marks the ones a suppression covered or a fold matched rather than dropping them: a system that discards what it decided not to show cannot say how much it is not showing. Action is what the rule asked for, Response is what the plane concluded, and exactly two declared mechanisms move it. A rung may only raise — lowering is a suppression by another name — and elevation beats folding, because an activation that reached an escalation rung is the escalation. The live feed is a convenience over the durable rows; it drops rather than blocking ingest, and the drops are counted per tenant and published on every poll. dictionary reports what a tenant's payloads carry: declared fields discovered by reflection over the transaction rather than from a second list, the tenant's own Raw keys, fill, blindness, and a distinct count kept as a bitmap sketch — hashed with the tenant, so two tenants' bitmaps cannot be compared, and saturating into "no longer a count" rather than drifting down. No value is ever stored. It is write-behind and says so: Pending is exactly what a restart would lose. topology searches the space of model shapes over a tenant's own history, through the detector's own learning path rather than a copy of it, and reports the learning curve, the realised alert share against the stated appetite, where each candidate went blind, and which features carried the signal. It has no store and imports nothing that has one. It names NO winner where nothing has been judged: ranking needs an outcome, and a ranking invented from unlabelled data would look like a recommendation and be a preference. Ties go to the smaller model. models keeps every search and every fitted state, because both are governed acts, and adopts a fit into the live model with its decider recorded. The learned state never leaves the store — mass counters describe where a tenant's activity is dense. A fit installs only into a model of the same shape, which is what stops a tenant's model being swapped underneath it by restoring a file. Around them: anomaly.Learn exposes the assessment Assess was already computing and throwing away, so a study of the model reads the same scores production does; engine gains one provider and one vocabulary term; and the tenant key moves to pkg/brand, where the brand registry is, so the five new planes hold the same shape at their own write boundary instead of each growing an opinion about what a tenant is. api/tenant.go's qualify and qualified are now aliases of it, not a second copy. Every operation is func(ctx, org, *In) (*Out, error): the tenant is an argument and never something the operation resolves for itself. api/typed.go is the whole of what turns one into a route — query into the input for a GET, body for a POST, the path bound last and winning — and api/planes.go is a routing table with no handler bodies in it, which a test reads as source. The contract is the Go types and nothing else, so a zip face over the same functions restates none of it. Co-authored-by: Hanzo Dev <[email protected]> |
||
|
|
3c041ed331 |
merge the detection library and the screening rewrite into one engine
Two branches had independently rebuilt the same two packages, which is one
implementation too many. This resolves them into one.
Their side is the base for detection and screening, because it is better:
- Rules name evidence, and SetRules refuses a set whose evidence this
deployment cannot supply — so the eleven stub helpers that silently disabled
thirteen rules cannot exist. A rule that cannot fire is worse than an absent
one, because it still appears in the catalog a regulator reads.
- One screening store, filled by the refresh and read by the endpoints. Two of
them is why sanctions search answered "no match" for every name while
designations were being loaded somewhere else.
- Parsers for all four publishers against their real files, recovering what the
single-parser path could not read at all: OFAC's digital-currency addresses,
weak aliases held apart from screening candidates, and OFSI's row-per-name
layout collapsed to subjects.
- Name matching over runes, not bytes. Two distinct Han names scored 0.867
byte-wise, above the 0.85 threshold, and 0.700 correctly.
- Deviation from the median and MAD rather than the mean, because a monitoring
baseline contains the outliers it is used to hunt.
- A rule vocabulary with providers behind every term, in place of expressions
reaching into a struct.
Kept from this side, because their branch did not have it:
- The tenant comes from an authenticated identity, not from a request header.
- A rule that could not run carries no weight. Their action() already capped a
failing rule at review, which is right, but the hit still entered the score
at full weight — so a broken rule saturated every transaction's score and the
queue stopped ranking. Faults are now excluded from the aggregate while still
raising their own alert and still forcing review.
- Per-source readiness: which list, how many designations, how long ago, and
the reason if the last attempt failed. Their store answers whether screening
can be relied on at all; this answers what is behind it, which is what an
operator can act on.
- Behavioural scoring, retention, tokenisation, replay and reference data.
Conversion now refuses an unknown currency instead of passing the amount
through. Passing it through understates every currency worth more than a dollar,
so a payment three times over a reporting limit tested as sitting under it.
One ordering table for actions (types.ActionRank), one EvalAll signature, one
screening store, one readiness surface. 15 packages, tests and race clean.
Co-authored-by: Hanzo Dev <[email protected]>
|
||
|
|
d8812d66f1 |
engine: a rule that could not run is a fault, not a match
An ordinary $100 payment came back with six alerts and a saturated score, so the effective false-positive rate was 100% and the score ranked nothing. The mechanism was one line of policy. A rule whose evaluation errored was recorded as a hit with Match=true, on the reasoning that a broken rule should not silently let a transaction through. The safety instinct is right; the encoding conflated two different findings. "This transaction exhibits this typology" is evidence about the transaction. "This rule did not run" is a fault in the system, which says nothing about the transaction — and because a rule that fails once fails on every transaction, recording it as a match alerts on all of them. Evaluate now separates them. A matching rule produces a typology alert that carries its score. Rules that could not be evaluated produce one alert naming them, with no score, because a rule that did not run is not evidence of risk. The transaction is still not cleared: the fault forces review, since a transaction assessed by only part of the rule set has not been assessed. Tests cover the reported case directly — six unevaluable rules against a benign payment, expecting one fault alert, zero score, and a refusal to allow — plus a benign transaction under a working rule set alerting not at all, and a fault alongside a real match where only the match scores. Five mutants, including restoring the original Match=true, all killed. Co-authored-by: Hanzo Dev <[email protected]> |
||
|
|
c1372cb0cc |
aml: anomaly scoring, retention, tokenisation, replay, reference data
Five gaps that a monitoring product cannot be sold without. anomaly scores whether a transaction is unusual for the entity that made it, which is the statutory question — consistency with the customer's own profile — and one no fixed threshold can express. It complements the rule library rather than replacing it: rules encode typologies someone has already named, and are blind to anything nobody has written down. The design is deliberately attributable: a score can name the features that drove it, which is what a supervisor asks for and what a neural scorer cannot answer. retention keeps records for their statutory period and destroys them when it runs out, including the retained assessment whether or not it produced a report. token protects direct identifiers at rest while keeping the engine able to correlate: one key schedule, a deterministic org-scoped pseudonym to index on and a sealed record for reads that are entitled to it, so a record can be found by party without holding the party's name in the clear. replay runs a rule over real history and reports what it would have done before anyone activates it — the same question as "how many alerts would this raise", which is what makes a rule change reviewable rather than a live experiment. reference holds the published data a rule needs but cannot derive: currency conversion and jurisdiction risk listings, each carrying the date it was current at, each refusing to answer rather than guess. Passing an unknown currency through unchanged moves every threshold by the exchange rate — the structuring rule compared raw notional, so 9,500 units of a minor currency read as a near-threshold cash amount. A jurisdiction list that answers "not listed" because it was never loaded is indistinguishable from a clean world. Co-authored-by: Hanzo Dev <[email protected]> |
||
|
|
04accf64e0 |
engine, history: one definition of a subject that names nobody
Subject.Valid is the single place that decides what an unusable subject is — unknown kind, no tenant, no identifier — and the scope that builds subjects and both stores that consume them all defer to it. Each of those failures has the same consequence and it is the worst one available: a subject naming nobody yields an empty window, and a rule over an empty window sees no activity, which is indistinguishable from a customer who did nothing. A rule that reaches no verdict now records review regardless of the action it was configured with. A failing flag-only rule escalates, because nobody looks at a flag; a failing block rule does not block, because an evidence outage would otherwise decline every payment. Reference data refuses rather than guesses: an unloaded or undated jurisdiction listing fails loudly instead of answering not-listed for every country on earth. Dropped the empty-expression check, which the parser already makes. 22 of 22 mutations to the engine, history and reference guards are killed by the suite; 58 of 58 across measure, sanctions and engine together. Co-authored-by: Hanzo Dev <[email protected]> |
||
|
|
039fa7e3d6 |
engine: a rule is admitted only if its evidence exists
The previous evaluator registered a stub for every term the language offered. Absent a real implementation, sanctions screening answered false, geographic matching answered true, the local hour answered noon, travel-rule completeness answered complete, and chain risk answered zero. Six of the terms were later replaced by store-backed versions; eleven never were, which left thirteen of the twenty-three shipped rules structurally unable to fire. Measured on the shipped code: a 50,000 dollar crypto payment at 3 a.m. from a politically exposed customer in a listed jurisdiction, to a fresh address, naming a designated person, raised four alerts out of twenty-three. There are now no stubs. Terms are methods on the evaluation scope, each term is mapped to the provider that answers it, and a rule naming a term whose provider is absent is refused when the rule set is installed. The refusal names the term. A rule set containing one such rule installs nothing, because a partially installed set is a control surface nobody can describe. Rules must yield a boolean. The previous evaluator accepted a number and read it as truthy, so a velocity rule written as a bare sum fired on any activity at all. Evidence failures at evaluation time — an unknown currency, an absent subject, an unreachable store — are recorded on the alert and routed to review rather than cleared, so an outage cannot become an approval. Windows are fetched once per subject and lookback for the whole rule set rather than once per rule. Aggregation is over the transaction's own timestamp, not the row's insertion time, and history queries are parameterised and scoped to the tenant. The previous store interpolated the customer identifier into the filter string and filtered by neither organisation nor transaction time. Co-authored-by: Hanzo Dev <[email protected]> |
||
|
|
e6f58397ab |
sanctions: parse all four published list families
Each parser was written against the published file, not against a description of it. Verified counts: OFAC 19,175 (matching the Record_Count the file declares), UN 1,011, EU 6,017, OFSI 5,135. What the previous single parser lost, and why each matters: OFAC publishes 963 digital currency addresses across 19 networks in idList. Dropping idList meant a payment to a designated blockchain address could not be detected by any means. OFAC marks 4,383 of its 24,546 aliases weak and the UN and OFSI mark theirs Low; flattening them to plain strings matched fragments at full confidence. OFAC flags which of several dates of birth is the main one, and for 37 subjects it is not the first — taking the first dropped the tie-break for exactly the subjects that need one. UN, EU and OFSI were parsed with the OFAC parser on the stated grounds that the formats are similar. They are not: the roots are CONSOLIDATED_LIST, export and a CSV. All three failed every night, logged a line, and continued. OFSI publishes one row per name and joins them by group; 19,761 rows are 5,135 subjects. It also puts the header on line two behind a preamble, orders the name columns Name 6 first, and writes partial dates as 00/00/1994. A parse that yields fewer records than the publisher declares is refused rather than loaded, because a partial sanctions list clears designated subjects. Co-authored-by: Hanzo Dev <[email protected]> |
||
|
|
07e69e3ee2 |
luxfi/aml — real-time AML/CFT transaction monitoring
A Go AML engine: a configurable rules DSL over expr-lang with weight-of-evidence scoring, sanctions screening against OFAC/UN/EU/HMT, case management, and scored alerting. Single binary, embedded SQLite per org, embedded UI. Surface (all /v1, org-scoped): /v1/aml/transactions ingest and score /v1/aml/rules /rules/test the rules DSL and its dry-run /v1/aml/sanctions/search list screening /v1/aml/cases investigation workflow /v1/aml/health Build: make build produces one static binary with the UI embedded (embedui tag); bare Go tooling needs no Node. Third-party license and notice texts are generated from the module graph the linker actually walks and ship beside every artifact. Co-authored-by: Hanzo Dev <[email protected]> |