5.5 KiB
AGENTS.md
Guidance for agents working inside core/.
Core Architecture Rules
The core traffic path is layered. Keep these responsibilities separate:
ingressadapts the entry mechanism and emitsmodel.AcceptedSession.sessionowns the current connection lifecycle and short-livedSessionState.statestores and aggregates observed facts intoTargetProfileand read-onlyProfileSnapshot.policyreadsSessionStateandProfileSnapshot, then emitsPolicyDecisionorOffloadSignal.offloadexecutes explicit offload signals and reports factual success or failure events.
Do not move long-lived target profile state into session. session may track only current-connection facts such as protocol, consecutive rewrite/no-rewrite counters, and flow offload status.
Event And Signal Boundaries
SessionEvent is a fact: something observed in the current connection or by an executor.
Examples:
SessionOpenedProtocolClassifiedHTTPRequestObservedRewriteAppliedRewriteSkippedNonHTTPObservedSessionClosedFlowOffloadSucceededFlowOffloadFailed
SessionEvent must not mean "please do this". Avoid event names or payload fields such as RequestTargetOffload, TryBypass, or ShouldPromote.
OffloadSignal is an explicit action candidate produced by policy:
TryFlowOffloadTryTargetOffloadCancelTargetOffloadNoAction
Target-level offload must follow this data flow:
SessionEvent -> StateStore -> ProfileSnapshot -> policy.TargetEvaluator -> OffloadSignal -> offload.Coordinator
Flow-level offload may be decided in the synchronous session path, but it must still enter offload through an explicit OffloadSignal.
Synchronous Vs Async Work
The current connection path must remain synchronous and bounded:
AcceptedSession -> SessionState -> classify -> ProfileSnapshot -> policy -> forward/rewrite/drop
Async fan-out is for observations and projections:
SessionEvent -> StateStore aggregation
SessionEvent -> stats projection
SessionEvent -> offload observation
Do not make the current connection wait for target profile aggregation unless a specific synchronous snapshot read is required for policy evaluation.
Package Notes
internal/model: shared vocabulary only. Keep behavior out of this package.internal/ingress: entry adapters. Current implementation is redirect; keep the interface open for future tproxy-style adapters.internal/session: connection orchestration, protocol classification, forwarding, and event emission.internal/state: fact aggregation only. It must not decide rewrite or offload actions.internal/policy: decisions only. It should not mutate profile state or execute offload.internal/offload: explicit action execution and factual result events.internal/rewrite: User-Agent rewrite rules only. It must not directly enqueue firewall or offload actions.internal/firewall: system execution boundary for batching ipset/nfset writes. It must not own target profiles or offload policy.
There is one production traffic path. Do not reintroduce a parallel processor or decision engine beside session -> events/state/policy/offload.
Runtime Lifecycle
app.Runowns signal handling and passes cancellation into the runtime.proxy.Servermust stop ingress first, reject queued sessions, propagate context cancellation to active handlers, and wait for them.session.DefaultManagermust use context-aware dialing and close both client and upstream connections on cancellation.- Shutdown then proceeds through
offload.Stop -> Executor.Flush/Stop -> Dispatcher.WaitIdle/Stop -> stats.Stop -> state.Stop. - Do not stop the dispatcher before executor completions have emitted and projected their result events.
- Lifecycle tests must synchronize with channels/barriers; real timeouts are hang guards only.
Testing Expectations
Before finishing a core change, run:
make ci
For changes touching architecture boundaries, add or update focused tests:
state: profiles are derived only from factual events.policy: decisions are derived from session input and profile snapshots.offload: actions happen only from explicitOffloadSignal.session: connection-local state does not own target profile data.
Async and time-dependent tests must be deterministic:
- Inject
internal/clock.Clock; useinternal/testkit/manualclockto advance business time. - Use
Dispatcher.WaitIdleto wait for causal event fan-out andExecutor.Flushto wait for queued firewall work. - Do not use
time.Sleep, polling, or real timers to advance business state or prove that a decision delay or cooldown elapsed. - Real context deadlines and timeout channels are allowed only as hang protection for concurrency and lifecycle tests.
- Prefer
internal/testkit/scenariofor cross-package target decision flows, preserving the fact -> snapshot -> signal -> action -> result fact boundaries.
Run the deterministic core stress suite with:
make stress
Use:
make cover
when changing cross-package behavior or preparing a PR that needs coverage output.
Compatibility
Keep existing CLI flags and user-visible behavior compatible unless the task explicitly asks for a breaking change.
Existing target-level firewall bypass behavior should remain expressible as target offload. Future eBPF flow offload should integrate through offload.FlowOffloader without making session, state, or rewrite depend on eBPF implementation details.