version: 1 # Architecture rules for the modular-monolith bounded contexts. # See docs/adr/0004-modular-monolith-bounded-contexts.md for the rationale. # # Strict mode: bootstrap is for production wiring only. Per-context # internal code may NOT import its own context's bootstrap; tests reach # for the per-context testkit instead. Cross-context tests import only # **..api or **..testkit, never **..bootstrap and never # **..internal.**. testkit packages are pinned to their own context # so cross-context test allowances cannot be exploited as transitive # paths to other bounded contexts. dependenciesRules: # identity context internals may import only identity's own api + # internals + test-fixture surface, plus platform packages. Other # contexts that need identity functionality go through identity/api. - package: "**.identity.internal.**" shouldOnlyDependsOn: internal: - "**.identity.api" - "**.identity.api.**" - "**.identity.internal.**" - "**.identity.testkit" - "**.attrkeys" - "**.httpserver" - "**.secretseal" # shared AES-256-GCM sealer: ssoconfig seals the OIDC client secret with it (issue #496 extracted it) - "**.hmactoken" # shared v1 HMAC token envelope: satoken builds its service-account access token on it (ADR-0013 generalizes the host-token machinery; shared via internal/ so identity does not import endpoint) - "**.revocationset" # shared per-replica revocation snapshot: service-account tokens check it as host tokens do (issue #1226) - "**.testdb" # endpoint context internals. - package: "**.endpoint.internal.**" shouldOnlyDependsOn: internal: - "**.endpoint.api" - "**.endpoint.api.**" - "**.endpoint.internal.**" - "**.endpoint.testkit" - "**.attrkeys" - "**.httpserver" - "**.hmactoken" # shared v1 HMAC token envelope: signedtoken builds its host bearer token on it (extracted so satoken can reuse the same primitive without a cross-context import) - "**.revocationset" # shared per-replica revocation snapshot, which service-account tokens also use (issue #1226) - "**.identity.api" # identity.api.AuditEvent, encoded into the enrollment.revoke audit entry # auditoutbox is the shared transactional audit outbox: a revoke commits its audit entry in its own transaction (issue #1225). - "**.auditoutbox" - "**.testdb" # rulecontent context internals (ADR-0021). The eighth context, carved out of rules at issue #766 because rule content acquired # its own storage and stopped being a projection of the catalog. # # Deliberately the NARROWEST allow-list of any context: rulecontent produces rule definitions and evaluates nothing, so it needs # no other context's api at all. If an entry ever has to be added here, that is worth reading as a signal that evaluation is # leaking back into content rather than as a missing permission. - package: "**.rulecontent.internal.**" shouldOnlyDependsOn: internal: - "**.rulecontent.api" - "**.rulecontent.api.**" - "**.rulecontent.internal.**" - "**.sqlhelpers" - "**.testdb" # rules context internals. Cross-context test allowance is # **.detection.testkit (and only testkit); a transitional # **.detection.bootstrap + **.detection.testharness pair existed in # earlier revisions and is no longer permitted. - package: "**.rules.internal.**" shouldOnlyDependsOn: internal: # versionedset is the shared versioned single-row JSON set (issue #1109): one row holding a whole set, replaced under a # row lock with a conditional version check and a strictly increasing database-clock stamp. The watched-path set and the # reachable-address set are the same shape in two contexts, and the ordering guarantees are too subtle to keep in two # copies that can drift. - "**.versionedset" # catchup is the shared catch-up policy (issue #1071): one decision about when a host is owed a state again. - "**.catchup" # installerscript is the one definition of a package installer script that the agent (which attaches the package's # signature) and suspicious_exec (which trusts it) must share exactly, or a signature could be attributed to one path # while the alert names another (issue #1161). - "**.installerscript" # auditoutbox is shared server platform code (issue #1070 moved it out of this context so response could use it too); # detection-config changes and watched-path replacements commit their audit entries through it. - "**.auditoutbox" - "**.rules.api" - "**.rules.api.**" - "**.rules.internal.**" - "**.rules.testkit" - "**.attrkeys" - "**.httpserver" # sqlhelpers is shared DB platform code, the same category as httpserver above, and is already allow-listed for the # detection, response and visibility internals. Rules is simply the context that had not needed it until the # monitor-match counters wanted the common deadlock retry (#813) rather than a fourth copy of it. - "**.sqlhelpers" - "**.endpoint.api" - "**.identity.api" # identity.api.AuditRecorder for policy.{create,update,delete} audit - "**.detection.api" - "**.detection.api.**" - "**.detection.testkit" # rulecontent.api is the supplier seam ADR-0021 defines: rulecontent produces rule definitions, rules consumes and # evaluates them, exactly as rules already consumes detection.api's GraphReader. - "**.rulecontent.api" - "**.rulecontent.api.**" - "**.testdb" # response context internals. - package: "**.response.internal.**" shouldOnlyDependsOn: internal: # versionedset is the shared versioned single-row JSON set (issue #1109): one row holding a whole set, replaced under a # row lock with a conditional version check and a strictly increasing database-clock stamp. The watched-path set and the # reachable-address set are the same shape in two contexts, and the ordering guarantees are too subtle to keep in two # copies that can drift. - "**.versionedset" # catchup is the shared catch-up policy (issue #1071): one decision about when a host is owed a state again. - "**.catchup" # auditoutbox is the shared transactional audit outbox (issue #1070): a containment change commits its audit entry in the # transaction that records the change and queues its command, rather than recording the row afterwards. - "**.auditoutbox" - "**.response.api" - "**.response.api.**" - "**.response.internal.**" - "**.response.testkit" - "**.attrkeys" - "**.httpserver" - "**.endpoint.api" - "**.identity.api" - "**.sqlhelpers" - "**.testdb" # control is the shared gRPC wire contract for the agent control channel (a platform package like httpserver/sqlhelpers, # generated from internal/control/control.proto). The response gateway implements its server side. - "**.control" # netaddr parses the address notation an operator writes wherever configuration names hosts or ranges (issue #1059). A # platform package like httpserver: the trusted-proxy list and the reachable-address set are written by the same operator in # the same notation, and a second parser would eventually disagree with this one about a bare IPv6 literal. - "**.netaddr" # detection context internals. - package: "**.detection.internal.**" shouldOnlyDependsOn: internal: - "**.detection.api" - "**.detection.api.**" - "**.detection.internal.**" - "**.detection.testkit" - "**.attrkeys" - "**.httpserver" - "**.endpoint.api" - "**.identity.api" - "**.rules.api" - "**.visibility.api" # ADR-0015 cutover: intake fans out to EventLog/EventArchive, the processor claims from EventLog, and correlation/evidence read the EventArchive - "**.sqlhelpers" - "**.coordination.leader" # MySQL-advisory-lock leader coordinator: pipeline.Runner gates retention + process-TTL on it - "**.secretseal" # shared AES-256-GCM sealer: the store seals webhook signing secrets, the delivery worker opens them (issue #496) - "**.testdb" # observability context internals (issue #374). A sixth bounded context (an amendment to ADR-0004's # original five) that owns the runtime trace-sampler settings: its store + admin handler. It imports # the shared sampler infra (internal/observability/tracing) for the Settings domain type, and # identity.api for the authz chokepoint + audit recorder its admin endpoint funnels through. - package: "**.server.observability.internal.**" shouldOnlyDependsOn: internal: - "**.server.observability.internal.**" - "**.observability.tracing" - "**.identity.api" - "**.attrkeys" - "**.httpserver" - "**.testdb" # visibility context internals (ADR-0015): ingestion + the event store. The # MySQL event_queue (EventLog) store depends only on visibility.api; the # ClickHouse EventArchive lands later and will add httpserver (TimeRange). - package: "**.visibility.internal.**" shouldOnlyDependsOn: internal: - "**.visibility.api" - "**.visibility.api.**" - "**.visibility.internal.**" - "**.visibility.testkit" - "**.httpserver" # TimeRange on the EventArchive correlation read - "**.sqlhelpers" - "**.testdb" # The shared otelsql span policy, so the ClickHouse archive traces queries exactly as the MySQL pool does. - "**.observability.tracing" # ----- testkit packages ----------------------------------------------------- # # testkit is a curated test-fixture surface for each bounded context. # Pinning its dependencies to its own context (plus platform) prevents # cross-context tests from using testkit as a transitive sneak-in # path to a third context: even though rules.internal is allowed to # import detection.testkit, detection.testkit cannot pull in # identity/endpoint/response. arch-go inspects direct imports only, # so this defence-in-depth rule is what makes the layered allow-list # actually airtight. - package: "**.identity.testkit" shouldOnlyDependsOn: internal: - "**.identity.api" - "**.identity.api.**" - "**.identity.bootstrap" # testkit.ApplySchema wraps bootstrap.ApplySchema - "**.identity.internal.**" - "**.attrkeys" - "**.httpserver" - "**.sqlhelpers" - "**.testdb" - package: "**.endpoint.testkit" shouldOnlyDependsOn: internal: - "**.endpoint.api" - "**.endpoint.api.**" - "**.endpoint.bootstrap" - "**.endpoint.internal.**" - "**.attrkeys" - "**.httpserver" - "**.sqlhelpers" - "**.testdb" - package: "**.rules.testkit" shouldOnlyDependsOn: internal: - "**.rules.api" - "**.rules.api.**" - "**.rules.bootstrap" - "**.rules.internal.**" - "**.attrkeys" - "**.httpserver" - "**.sqlhelpers" - "**.testdb" - package: "**.response.testkit" shouldOnlyDependsOn: internal: - "**.response.api" - "**.response.api.**" - "**.response.bootstrap" - "**.response.internal.**" - "**.attrkeys" - "**.httpserver" - "**.sqlhelpers" - "**.testdb" - package: "**.detection.testkit" shouldOnlyDependsOn: internal: - "**.detection.api" - "**.detection.api.**" - "**.detection.bootstrap" - "**.detection.internal.**" - "**.rules.api" # Replay runs rulesapi.Rule against fixtures - "**.visibility.api" # ADR-0015: the detection store now requires an EventArchive (correlation + evidence reads) - "**.visibility.testkit" # MemArchive: the in-memory EventArchive the fixtures seed instead of the dropped MySQL events table - "**.attrkeys" - "**.httpserver" - "**.sqlhelpers" - "**.testdb" - package: "**.server.observability.testkit" shouldOnlyDependsOn: internal: - "**.server.observability.bootstrap" - "**.server.observability.internal.**" - "**.observability.tracing" - "**.attrkeys" - "**.httpserver" - "**.testdb" - package: "**.visibility.testkit" shouldOnlyDependsOn: internal: - "**.visibility.api" - "**.visibility.api.**" - "**.visibility.bootstrap" - "**.visibility.internal.**" - "**.httpserver" # MemArchive mirrors the EventArchive correlation read (TimeRange) - "**.testdb" # ----- api/ packages: pure-types, no cross-context internals ---------------- # # Each /api/ contains domain value types + interface declarations; any # executable cross-context wiring belongs in bootstrap/ or internal/. The # rule catches accidents like "rules.api gained a helper that imports # detection/internal/mysql for convenience" before they ship. # # rules/api carries an explicit cross-context exception: it re-exports # detection/api types (Event, Process, Finding, NullRawJSON, GraphReader, # TimeRange) as type aliases so the catalog rule files can implement # rulesapi.Rule.Evaluate without importing detection/api directly. The # alias is a deliberate design choice; this rule names it. - package: "**.identity.api" shouldOnlyDependsOn: internal: - "**.identity.api.**" - "**.attrkeys" - "**.httpserver" - "**.sqlhelpers" - package: "**.endpoint.api" shouldOnlyDependsOn: internal: - "**.endpoint.api.**" - "**.attrkeys" - "**.httpserver" - "**.sqlhelpers" - package: "**.rules.api" shouldOnlyDependsOn: internal: # catchup is the shared catch-up policy (issue #1071): the watched-path command carries its status already mapped # onto that vocabulary, by the context that owns the lifecycle, rather than cast from its spelling here. - "**.catchup" - "**.rules.api.**" - "**.detection.api" # alias re-export of detection/api types - "**.attrkeys" - "**.httpserver" - "**.sqlhelpers" - package: "**.response.api" shouldOnlyDependsOn: internal: # catchup is the shared catch-up policy (issue #1071): Status.Catchup maps this context's lifecycle onto it, here # rather than at each consumer, because this is the file a rename happens in. - "**.catchup" - "**.response.api.**" - "**.attrkeys" - "**.httpserver" - "**.sqlhelpers" - package: "**.detection.api" shouldOnlyDependsOn: internal: - "**.detection.api.**" - "**.visibility.api" # Event envelope alias: the canonical type lives in visibility (ADR-0015) - "**.attrkeys" - "**.httpserver" - "**.sqlhelpers" - package: "**.visibility.api" shouldOnlyDependsOn: internal: - "**.visibility.api.**" - "**.httpserver" # TimeRange on the EventArchive read; api is pure types/interfaces (no attrkeys/sqlhelpers) # ----- platform packages: allow-list of acceptable internal deps ------------ # # Plan §"Platform packages": "Platform packages MUST NOT import any context." # An earlier revision encoded this as a deny-list (shouldNotDependsOn each # bounded context); Gemini's review on PR #100 pointed out that the # deny-list is not future-proof: adding a sixth bounded context would # require updating every platform rule by hand. The allow-list form # (shouldOnlyDependsOn the explicit set of allowed internal-graph deps) # auto-excludes any new context: a future server// package # is implicitly forbidden because it isn't named in the allow. # # The allow-list enumerates: every other platform package, the repo-level # internal/ tree (envparse + observability today), and stdlib/third-party # imports (which arch-go's "internal:" key doesn't restrict; third-party # imports flow through automatically, no listing needed). # # Patterns use full module paths because `**.bootstrap` would otherwise # also match each context's bootstrap, and `**.testdb` would also match # testdb/full (the cross-context fixture composer that legitimately # imports each testkit). - package: "github.com/rawtrail/rawtrail/server/config" shouldOnlyDependsOn: internal: &platform_allow - "github.com/rawtrail/rawtrail/server/config" - "github.com/rawtrail/rawtrail/server/bootstrap" - "github.com/rawtrail/rawtrail/server/httpserver" - "github.com/rawtrail/rawtrail/server/metrics" - "github.com/rawtrail/rawtrail/server/attrkeys" - "github.com/rawtrail/rawtrail/server/apidocs" - "github.com/rawtrail/rawtrail/server/sqlhelpers" - "github.com/rawtrail/rawtrail/server/ui" - "**.internal.**" - package: "github.com/rawtrail/rawtrail/server/bootstrap" shouldOnlyDependsOn: internal: *platform_allow - package: "github.com/rawtrail/rawtrail/server/httpserver" shouldOnlyDependsOn: internal: *platform_allow - package: "github.com/rawtrail/rawtrail/server/metrics" shouldOnlyDependsOn: internal: *platform_allow - package: "github.com/rawtrail/rawtrail/server/attrkeys" shouldOnlyDependsOn: internal: *platform_allow - package: "github.com/rawtrail/rawtrail/server/apidocs" shouldOnlyDependsOn: internal: *platform_allow - package: "github.com/rawtrail/rawtrail/server/sqlhelpers" shouldOnlyDependsOn: internal: *platform_allow # auditoutbox sits with the platform packages but carries ONE context dependency, deliberately: the audit event it encodes and the # recorder a drain delivers to are identity's contract (ADR-0021), so no encoding of an audit row can be written without naming # them. It is listed apart from *platform_allow rather than folded into it so that exception stays visible: a second context # dependency here would mean the package had stopped being platform code. # catchup is the shared catch-up policy: when a host should be sent a state its latest command did not deliver, and the sweep that # asks (issue #1071). Pure platform code with no context dependency at all, since a context maps its own command onto catchup.Latest # rather than the package naming any context's types. - package: "github.com/rawtrail/rawtrail/server/catchup" shouldOnlyDependsOn: internal: *platform_allow - package: "github.com/rawtrail/rawtrail/server/auditoutbox" shouldOnlyDependsOn: internal: - "github.com/rawtrail/rawtrail/server/identity/api" - "github.com/rawtrail/rawtrail/server/sqlhelpers" - "github.com/rawtrail/rawtrail/server/testdb" # NOTE: server/testdb itself is a Go-leaf and imports no context, but its # sub-package server/testdb/full is the cross-context fixture composer and # imports each context's testkit. arch-go's package patterns are prefix- # matched (github.com/rawtrail/rawtrail/server/testdb matches both leaf + full), # so a single rule can't distinguish them. The leaf invariant is verified # by inspection (server/testdb/open.go imports nothing in server//) and # by the per-testkit rules below preventing transitive sneak-ins. - package: "github.com/rawtrail/rawtrail/server/ui" shouldOnlyDependsOn: internal: *platform_allow # ----- bootstrap/ access: enforced indirectly -------------------------------- # # Plan §"Architecture lint" prescribes "Allow only `cmd/**` and # `test/integration/**` to import any `bootstrap/` package." # arch-go v1.7.0 exposes only "depends-on" rule families # (shouldOnlyDependsOn / shouldNotDependsOn), not the inverse # "shouldOnlyBeAccessedBy" the plan sketched against v2. The # invariant is enforced indirectly by the importer-perspective # rules already in this file: # # - Platform packages cannot import any context (incl. bootstraps). # - /api packages cannot import any other context's bootstrap # (the api-purity allow-list excludes it). # - /internal/* packages cannot import another context's # bootstrap (each context's allow-list lists only that context's # own packages + cross-context **..api / **..testkit). # - /testkit may import its OWN context's bootstrap (per-testkit # rule); other contexts' testkits are not in any internal # allow-list, so cross-context testkit imports go through testkit # too, not bootstrap. # # The remaining production-reachable bootstrap importers are # cmd/rawtrail-{server,ingest} and each /testkit. New packages # outside those slots would have to be added explicitly; code review # is the gate. If arch-go ever ships a "shouldOnlyBeAccessedBy" rule # family in v2, this section becomes the single declarative # source of truth instead of a property derived from many other # rules.