mirror of
https://github.com/luxfi/aml.git
synced 2026-09-24 00:25:20 +00:00
main
5
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]> |
||
|
|
90fef65897 |
planes: a bound in bytes, and a tenant that only ever spends its own
Two shapes, both on the ingest path, both proven by measurement. A bound over a COUNT of caller-sized things is not a bound. A velocity key is not a pointer to what the caller sent: it is one ring per window, 444 buckets allocated whole on the first transaction, a measured 22.7 KB. So MaxKeys=100000 meant 2,163 MiB against a 1 GiB pod — the honest reading was that the process dies at about 46,000 keys, from one tenant, with ordinary traffic. The ceiling is now in BYTES and the key count is derived from it in one place (keyCost), so the two cannot disagree; Store.Ceiling() is an upper bound a test WEIGHS a full store against. One store shared by every tenant under one cap is a way for one institution to switch another's monitoring off. Measured: a victim's 24h window went count=1 to count=0, and a victim's model went learned=40 to learned=0, purely from two other tenants' traffic — no error, no alert, and an aggregate that reads zero is what a clean account also reads. Per-tenant state now lives in pkg/roster, which ADMITS and has no removal at all: a tenant keeps what it holds, a tenant arriving with no room is refused by name (anomaly's ReasonCrowded) and counted, and the only key a tenant can drop is its own least recently used. internal/source reads both packages to keep the shape out. Reaching a bound is allowed; reaching it quietly is not. velocity.Load grades a tenant clear/crowded/full and counts what it dropped, anomaly.State carries Crowded and Pressure counts it, and roster.Default is ONE answer to how many institutions this process holds state for, so the per-tenant bounds multiply to a figure an operator can hold against a pod. Closing R5 (cross-tenant model eviction) and R5b (the adopted reload was read once per TRANSACTION under thrash, now once per admission) from the review, plus the same two shapes in pkg/velocity, which nothing had reported. 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]> |
||
|
|
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]> |