Skip to content

feat: add backward-compatible Container support for Agent/Protocol arhitecture#47

Merged
joamag merged 11 commits intomasterfrom
feat/container-backwards
Feb 13, 2026
Merged

feat: add backward-compatible Container support for Agent/Protocol arhitecture#47
joamag merged 11 commits intomasterfrom
feat/container-backwards

Conversation

@joamag
Copy link
Contributor

@joamag joamag commented Feb 13, 2026

Enables the Container to multiplex both old Base-derived and new Agent/Protocol-derived objects on a shared poll, fixing AttributeError when running ConsulProxyServer. Adds _container_loop bridging, event relay, proxy throttle fixes, and comprehensive test coverage for container setup and proxy data flow.

Summary by CodeRabbit

  • New Features

    • Agents and clients can use a container-level event loop and relay protocol events to their owning client.
    • Protocols now expose connection state controls and pending limits for better backpressure handling.
    • HTTP/raw clients propagate protocol events to client-level observers.
  • Bug Fixes

    • Fixed AttributeError in proxy server lifecycle handling.
  • Documentation

    • Added detailed architecture and asyncio-compatibility guides.
  • Tests

    • Added extensive container, agent, and reverse-proxy test coverage.

…chitecture

Enables the Container to multiplex both old Base-derived and new
Agent/Protocol-derived objects on a shared poll, fixing AttributeError
when running ConsulProxyServer. Adds _container_loop bridging, event
relay, proxy throttle fixes, and comprehensive test coverage for
container setup and proxy data flow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 13, 2026 12:26
@joamag joamag assigned joamag and unassigned joamag Feb 13, 2026
@joamag joamag added the enhancement New feature or request label Feb 13, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 2026

Warning

Rate limit exceeded

@joamag has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 23 minutes and 19 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Add container-loop wiring and Base-compatible lifecycle hooks to agents; extend StreamProtocol to proxy connection/socket and flow-control properties; implement client-level protocol event relay; change proxy read-delegation paths; add comprehensive container and reverse-proxy tests; expand architecture/compat docs and update CHANGELOG.

Changes

Cohort / File(s) Summary
Agent & Container
src/netius/base/agent.py, src/netius/base/container.py
Introduce _container_loop and Base-compatible lifecycle/introspection stubs on Agent; add ClientAgent.connect() and protocol event relay; container binds _container_loop to non-Base bases in start/apply.
Protocol API
src/netius/base/protocol.py
Expose connection/socket, add read-state and throttle accessors (renable, disable_read/enable_read, is_throttleable/is_exhausted/is_restored), and proxyable max_pending/min_pending; propagate pre-set limits in connection_made.
Clients — Event Relay
src/netius/clients/http.py, src/netius/clients/raw.py
Clients use container loop when present; add _relay_protocol_events() to relay protocol events (headers/message/partial/data/open/close) up to the client.
Proxy & Throttle Delegation
src/netius/servers/proxy.py, src/netius/servers/socks.py, src/netius/extra/proxy_r.py
Route read-enable/disable calls through server reads() instead of per-client reads; unpack (loop, connection) from http_client.method(...) where returned.
Tests
src/netius/test/base/container.py, src/netius/test/extra/proxy_r.py
Add extensive container and reverse-proxy tests covering lifecycle, container-loop wiring, event relays, balancers, rules, request flows, and x-forwarded handling.
Docs & Changelog
doc/architecture.md, doc/compat.md, CHANGELOG.md
Add architecture and compat docs describing execution modes/dispatch and container wiring; update CHANGELOG Unreleased with compatibility, client-agent protocol integration, container-loop, proxy delegation, tests, and a ConsulProxyServer AttributeError fix.

Sequence Diagram(s)

sequenceDiagram
    actor Observer
    participant HTTPClient as HTTP Client
    participant ClientAgent as Client Agent
    participant Container as Container/_container_loop
    participant StreamProtocol as StreamProtocol
    participant Connection as Connection/Transport

    Observer->>HTTPClient: attach listener
    HTTPClient->>ClientAgent: connect(host,port)
    ClientAgent->>Container: use _container_loop / compat.connect_stream
    Container->>StreamProtocol: create protocol (native/compat)
    StreamProtocol->>Connection: bind transport / connection_made
    Connection->>StreamProtocol: propagate connection state
    StreamProtocol->>ClientAgent: protocol ready
    ClientAgent->>HTTPClient: _relay_protocol_events(protocol)
    HTTPClient->>Observer: re-emit headers/message/partial/data/open/close
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 I hopped through loops and wires tonight,

relayed a header, nudged a light;
agents bound and protocols sing,
containers hum—what joy they bring!
A tiny rabbit cheers this bright.

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title describes the main architectural change—adding backward-compatible Container support for Agent/Protocol—which aligns with the primary objectives of enabling Container multiplexing of both legacy Base-derived and new Agent/Protocol objects.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into master

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/container-backwards

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/netius/base/agent.py`:
- Around line 132-164: The call in Agent.connect unpacks *args after keyword
args into compat.connect_stream which triggers Ruff B026 and can raise TypeError
when positional args are provided; fix by either removing *args from the
compat.connect_stream call (since current usage only passes host, port, ssl) or
by moving *args before the explicit keyword args so positional args are
forwarded correctly, and update the connect signature accordingly; additionally
validate that cls.protocol exists and is callable (or explicitly call a
documented classmethod) by adding a guard that raises NotImplementedError if
cls.protocol is missing/not callable to make the subclass contract explicit
(referencing connect, cls.protocol, and compat.connect_stream).

In `@src/netius/servers/socks.py`:
- Line 231: The change calling self.reads((tunnel_c.socket,), state=False) will
raise AttributeError because SOCKSServer (and its bases ServerAgent/Agent) don't
implement reads()/writes()/errors(); revert to using the original raw_client
interface or add the missing stub methods. Fix option A: change the call back to
self.raw_client.reads((tunnel_c.socket,), state=False) and ensure raw_client is
correctly assigned in SOCKSServer.__init__ (or wherever the client is created).
Fix option B: add no-op stub methods reads(), writes(), and errors() to
Agent/ServerAgent that forward to raw_client if present (or raise a clear
error), so Container can safely call them; update SOCKSServer to use those stubs
consistently.

In `@src/netius/test/extra/proxy_r.py`:
- Line 821: The test sets frontend.ctx_request.return_value =
contextlib.nullcontext(), which fails on Python versions lacking nullcontext;
add a small polyfill before its use (e.g., at module top or before the test)
that detects whether contextlib.nullcontext exists and, if not, defines a
compatible nullcontext context manager (matching the standard library signature)
and assigns it into the contextlib namespace or imports it into the module
namespace; update the test to use the fallback name if needed so
frontend.ctx_request.return_value always gets a working nullcontext across
Python 2.7+ and 3.x.
🧹 Nitpick comments (5)
src/netius/base/container.py (1)

181-186: Duplicate _container_loop assignment across start_base and apply_base — intentional but worth a note.

Both start_base (line 163) and apply_base (line 185) set _container_loop for non-Base objects. This redundancy is safe but could become a maintenance concern if the logic diverges. Consider extracting into a small helper if more attributes are added in the future.

src/netius/test/extra/proxy_r.py (1)

806-827: Consider making _make_frontend more robust for attribute deletion.

The del frontend.prefix / del frontend.state etc. pattern works with MagicMock to simulate hasattr returning False. This is a valid technique but somewhat fragile — if the production code changes which attributes it checks, these tests could silently pass with stale mock configuration.

src/netius/base/agent.py (1)

166-181: Event relay wiring is clean; minor note on unbinding.

The relay correctly maps protocol "open" → agent "connect" and protocol "close" → agent "close", matching the observer trigger signatures. One thing to be aware of: the lambdas are anonymous, so there's no way to unbind them if a protocol is discarded before it fires events. If protocols are always short-lived and cleaned up, this is fine; otherwise leaked bindings could accumulate.

src/netius/test/base/container.py (2)

36-44: Good test structure with setUp/tearDown lifecycle management.

The fixture properly creates and cleans up the container. Note that a few tests (e.g. test_cleanup_agent at line 183) call self.container.cleanup() explicitly, and then tearDown calls it again. Verify that double-cleanup on Container is idempotent to avoid spurious test failures.

#!/bin/bash
# Check if Container.cleanup() is safe to call twice
ast-grep --pattern $'class Container($$$) {
  $$$
  def cleanup(self) {
    $$$
  }
  $$$
}'

# Also check Base.cleanup for idempotency
rg -n "def cleanup" --type=py -B 1 -A 15 -g '*/base/*.py'

212-260: Event relay tests validate the protocol → client → observer pipeline effectively.

Both the HTTP and Raw relay tests cover the full event chain: protocol events are triggered, relayed through the client agent, and received by bound observers. The HTTP test also correctly distinguishes "headers", "message", and "partial" bindings (lines 216-218) even though those events aren't triggered in this particular test — consider adding trigger calls for those protocol-specific events to fully exercise the HTTP relay, or add a TODO.

Comment on lines +132 to +164
def connect(self, host, port, ssl=False, *args, **kwargs):
"""
Creates a new protocol based connection to the provided
host and port, using the container's shared event loop when
available (dual architecture support).

:type host: String
:param host: The hostname or IP address of the remote
host to which the connection should be made.
:type port: int
:param port: The port number of the remote host to
which the connection should be made.
:type ssl: bool
:param ssl: If the connection should be established using
a secure SSL/TLS channel.
:rtype: Protocol
:return: The protocol instance that represents the newly
created connection.
"""

cls = self.__class__
protocol = cls.protocol()
compat.connect_stream(
lambda: protocol,
host=host,
port=port,
ssl=ssl,
loop=self._container_loop,
*args,
**kwargs
)
self._relay_protocol_events(protocol)
return protocol
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the compat module and check connect_stream signature
find . -type f -name "*.py" | head -20
git ls-files | grep -E "(compat|agent)" | head -20

Repository: hivesolutions/netius

Length of output: 719


🏁 Script executed:

# Look at the file being reviewed
cat -n src/netius/base/agent.py | sed -n '132,164p'

Repository: hivesolutions/netius

Length of output: 1456


🏁 Script executed:

# Search for connect_stream definition
rg -n "def connect_stream" --type=py -A 10

Repository: hivesolutions/netius

Length of output: 700


🏁 Script executed:

# Search for _connect_stream implementations
rg -n "_connect_stream" --type=py -A 5 | head -50

Repository: hivesolutions/netius

Length of output: 1126


🏁 Script executed:

# Check for ClientAgent subclasses and protocol definitions
rg -n "class.*Agent\(" --type=py -B 1 -A 5 | head -100

Repository: hivesolutions/netius

Length of output: 1069


🏁 Script executed:

# Search for protocol classmethod/staticmethod definitions
rg -n "def protocol" --type=py -B 2 -A 5

Repository: hivesolutions/netius

Length of output: 46


🏁 Script executed:

# Try to parse the file as Python to see if it's syntactically valid
python3 -m py_compile src/netius/base/agent.py 2>&1 || echo "Parse error found"

Repository: hivesolutions/netius

Length of output: 46


🏁 Script executed:

# Get full signatures of the implementation functions
cat -n src/netius/base/compat.py | sed -n '604,625p'
cat -n src/netius/base/compat.py | sed -n '680,701p'

Repository: hivesolutions/netius

Length of output: 1349


🏁 Script executed:

# Search for calls to connect() method
rg -n "\.connect\(" --type=py -B 2 -A 2 | head -60

Repository: hivesolutions/netius

Length of output: 3391


🏁 Script executed:

# Check if any subclass defines protocol classmethod or method
rg -n "class.*ClientAgent" --type=py -A 50 | grep -E "(class|def protocol)" | head -30

Repository: hivesolutions/netius

Length of output: 1218


🏁 Script executed:

# Look for protocol classmethod definitions in subclasses
rg -n "def protocol\|protocol =" --type=py -B 3 -A 5 | head -80

Repository: hivesolutions/netius

Length of output: 46


🏁 Script executed:

# Check ClientAgent more carefully for protocol definition
cat -n src/netius/base/agent.py | sed -n '106,180p'

Repository: hivesolutions/netius

Length of output: 3238


🏁 Script executed:

# Verify the actual syntax is valid and check what the review is referring to
python3 << 'EOF'
# Simulate the current pattern
def connect_stream(protocol_factory, host, port, ssl=False, loop=None, *args, **kwargs):
    print(f"protocol_factory: {protocol_factory}")
    print(f"host: {host}")
    print(f"port: {port}")
    print(f"args: {args}")
    print(f"kwargs: {kwargs}")

def test_current_call():
    """Test the current call pattern from agent.py"""
    def protocol():
        return "protocol"
    
    extra_args = ("extra1", "extra2")
    extra_kwargs = {"extra_key": "extra_value"}
    
    # This is what agent.py currently does
    connect_stream(
        lambda: protocol,
        host="localhost",
        port=8080,
        ssl=False,
        loop=None,
        *extra_args,
        **extra_kwargs
    )

test_current_call()
EOF

Repository: hivesolutions/netius

Length of output: 253


🏁 Script executed:

# Search specifically for "protocol" classmethod/staticmethod in all client files
rg -n "def protocol" --type=py -B 2 -A 3

Repository: hivesolutions/netius

Length of output: 46


🏁 Script executed:

# Check a few specific subclass files for protocol() definition
grep -n "def protocol" src/netius/clients/http.py src/netius/clients/dns.py src/netius/clients/raw.py src/netius/clients/ws.py 2>/dev/null

Repository: hivesolutions/netius

Length of output: 46


🏁 Script executed:

# Try broader search in all Python files for protocol as method
rg -n "^\s*def protocol" --type=py

Repository: hivesolutions/netius

Length of output: 46


Move *args before keyword arguments or remove it if positional arguments aren't needed.

Ruff B026 is valid. When *args is unpacked after keyword arguments in the call to compat.connect_stream() (line 160), it causes a mismatch: any positional arguments supplied to connect() would collide with the explicitly named parameters, raising TypeError: got multiple values for argument. Current usage patterns pass only host, port, and ssl, so removing *args aligns with actual usage. If positional arguments are genuinely required for connect_stream, place *args before keyword arguments instead.

Additionally, the cls.protocol() call at line 153 assumes subclasses define a protocol classmethod, but this contract is undocumented. Consider documenting this requirement or raising NotImplementedError if not defined to surface misuse early.

🧰 Tools
🪛 Pylint (4.0.4)

[error] 153-153: Class 'ClientAgent' has no 'protocol' member

(E1101)

🪛 Ruff (0.15.0)

[warning] 160-160: Star-arg unpacking after a keyword argument is strongly discouraged

(B026)

🤖 Prompt for AI Agents
In `@src/netius/base/agent.py` around lines 132 - 164, The call in Agent.connect
unpacks *args after keyword args into compat.connect_stream which triggers Ruff
B026 and can raise TypeError when positional args are provided; fix by either
removing *args from the compat.connect_stream call (since current usage only
passes host, port, ssl) or by moving *args before the explicit keyword args so
positional args are forwarded correctly, and update the connect signature
accordingly; additionally validate that cls.protocol exists and is callable (or
explicitly call a documented classmethod) by adding a guard that raises
NotImplementedError if cls.protocol is missing/not callable to make the subclass
contract explicit (referencing connect, cls.protocol, and
compat.connect_stream).


tunnel_c.enable_read()
self.raw_client.reads((tunnel_c.socket,), state=False)
self.reads((tunnel_c.socket,), state=False)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check ServerAgent definition and reads() method availability
echo "=== Checking ServerAgent definition ==="
rg -n 'class ServerAgent' --type py -A 5

echo "=== Checking Agent class definition ==="
rg -n 'class Agent' --type py -A 5

echo "=== Checking reads method in the codebase ==="
rg -n 'def reads' --type py

echo "=== Checking how ServerAgent is imported ==="
rg -n 'ServerAgent' src/netius/__init__.py -B 2 -A 2

echo "=== Check agent.py for method definitions ==="
cat -n src/netius/agent.py | head -100

echo "=== Check inheritance of SOCKSServer in detail ==="
rg -n 'class SOCKSServer' src/netius/servers/socks.py -A 15

Repository: hivesolutions/netius

Length of output: 1831


🏁 Script executed:

#!/bin/bash
# Need to check if Agent has reads() method and understand the full picture
echo "=== Full Agent class definition ==="
cat -n src/netius/base/agent.py

echo "=== Check what ContainerServer is ==="
rg -n 'class ContainerServer' --type py -A 10

echo "=== Check ProxyServer to understand analogous pattern ==="
rg -n 'class ProxyServer' src/netius/servers/proxy.py -A 20

echo "=== Check what HTTP2Server inherits from ==="
rg -n 'class HTTP2Server' --type py

echo "=== Look for any method stubs or monkey-patching in agent.py ==="
sed -n '180,210p' src/netius/base/agent.py

Repository: hivesolutions/netius

Length of output: 9430


🏁 Script executed:

#!/bin/bash
# Check if reads() is added to Agent class anywhere (might be further down)
echo "=== Check full Agent class for reads method ==="
rg -n 'def reads' src/netius/base/agent.py

# Check if Agent has any other Base-compatible stubs we missed
echo "=== Check all method definitions in Agent class ==="
sed -n '38,185p' src/netius/base/agent.py | grep -n 'def '

# Check if reads is added via assignment/setattr
echo "=== Check for dynamic method assignment in Agent ==="
rg -n 'reads.*=' src/netius/base/agent.py

# Let's also check the actual SOCKSServer usage - where does raw_client come from?
echo "=== Check raw_client in SOCKSServer ==="
rg -n 'raw_client' src/netius/servers/socks.py -B 2 -A 2

# Check if there's a reads method that was supposed to be added but wasn't
echo "=== Check Server class reads method signature ==="
sed -n '417,430p' src/netius/base/server.py

Repository: hivesolutions/netius

Length of output: 1867


🏁 Script executed:

#!/bin/bash
# Find raw_client definition in SOCKSServer
echo "=== Find raw_client initialization in SOCKSServer ==="
rg -n 'self.raw_client' src/netius/servers/socks.py -B 2 | head -40

# Look at the full __init__ method
echo "=== Full SOCKSServer __init__ method ==="
sed -n '120,150p' src/netius/servers/socks.py

# Check what RawClient is
echo "=== Find RawClient class ==="
rg -n 'class RawClient' --type py

# Check line 231 in context
echo "=== Context around line 231 ==="
sed -n '225,240p' src/netius/servers/socks.py

Repository: hivesolutions/netius

Length of output: 2340


🏁 Script executed:

#!/bin/bash
# Find where raw_client is defined - check if it's a property or in parent class
echo "=== Search for raw_client property or assignment ==="
rg -n 'raw_client' src/netius/servers/socks.py

echo "=== Check if Container or ContainerServer defines raw_client ==="
rg -n 'raw_client' src/netius/base/container.py

echo "=== Check what add_base does ==="
rg -n 'def add_base' src/netius/base/container.py -A 10

Repository: hivesolutions/netius

Length of output: 772


🏁 Script executed:

#!/bin/bash
# Check if raw_client is a property defined in SOCKSServer or parent
echo "=== Search for raw_client as property or descriptor ==="
rg -n '@property' src/netius/servers/socks.py -A 3

# Check Container class to see if it provides raw_client
echo "=== Check Container class full definition ==="
sed -n '1,150p' src/netius/base/container.py | tail -60

# Check if raw_client is created via add_base or elsewhere
echo "=== Check if raw_client is created as attribute ==="
sed -n '130,145p' src/netius/servers/socks.py

# Let's see what RawClient looks like
echo "=== Check RawClient class ==="
sed -n '45,80p' src/netius/clients/raw.py

Repository: hivesolutions/netius

Length of output: 3962


🏁 Script executed:

#!/bin/bash
# The issue is clear: raw_client is referenced but never assigned. Let me check if this exists elsewhere
echo "=== Check if raw_client is dynamically created ==="
rg -n 'raw_client\s*=' src/netius/servers/socks.py

# Check git history or another version to understand raw_client
echo "=== Check all files that reference raw_client ==="
rg -n 'raw_client' --type py

# Let me check if Agent is supposed to have reads() stub but is missing it
echo "=== Look for PR changes or TODOs about Agent stubs ==="
rg -n 'stub' src/netius/base/agent.py -B 2 -A 2

# Check if reads is supposed to be in Agent but just isn't shown
echo "=== Full remaining Agent code after line 104 ==="
tail -n +104 src/netius/base/agent.py

Repository: hivesolutions/netius

Length of output: 6761


🏁 Script executed:

#!/bin/bash
# Critical finding: raw_client is never assigned in SOCKSServer but is referenced
# Let's check if this is a pre-existing bug or part of this change

echo "=== Check ProxyServer's raw_client assignment ==="
sed -n '170,195p' src/netius/servers/proxy.py

echo "=== Compare: SOCKSServer missing raw_client assignment ==="
echo "Lines 120-145 of socks.py show raw_client is used but never assigned:"
sed -n '120,150p' src/netius/servers/socks.py

echo "=== Check if Agent actually has reads() stub that should exist ==="
rg -n 'def reads\|def writes\|def errors' src/netius/base/agent.py

echo "=== What Agent methods are supposed to be Base-compatible stubs? ==="
grep -A 30 'Base-compatible stub' src/netius/base/agent.py

Repository: hivesolutions/netius

Length of output: 3633


🏁 Script executed:

#!/bin/bash
# Final verification: Check if reads/writes/errors stubs should be in Agent
echo "=== Check if there are any recent commits that added stubs to Agent ==="
git log --oneline -20 src/netius/base/agent.py 2>/dev/null || echo "Git log not available"

echo "=== Check Container's poll.poll_owner() to see what it calls ==="
sed -n '110,135p' src/netius/base/container.py

echo "=== Verify: Agent docstring says Base-compatible stubs but shows none ==="
sed -n '38,105p' src/netius/base/agent.py | grep -E '(def |Provides Base)'

echo "=== Let's check if this is part of the current PR changes ==="
echo "Looking for any Agent modifications in the repo..."
git diff HEAD src/netius/base/agent.py 2>/dev/null | head -100 || echo "No git diff available"

Repository: hivesolutions/netius

Length of output: 1771


🏁 Script executed:

#!/bin/bash
# The commit message suggests reads/writes/errors stubs should exist
# Let's check the commit to see what was supposed to be added

echo "=== Check that commit for what was added ==="
git show 8f10356 --stat 2>/dev/null | head -50 || echo "Commit details not available"

echo "=== Check if reads/writes/errors are supposed to be in Agent ==="
git show 8f10356 -- src/netius/base/agent.py 2>/dev/null | grep -A 5 -B 5 'def reads\|def writes\|def errors' || echo "No reads/writes/errors found in commit"

echo "=== Current state: Agent missing reads/writes/errors stubs needed by Container ==="
echo "Container calls base.reads(), base.writes(), base.errors() on all bases (line ~127)"
echo "But Agent class (which ServerAgent inherits from) doesn't have these methods"
echo "This is a critical incompleteness in the stub implementation"

Repository: hivesolutions/netius

Length of output: 3039


🏁 Script executed:

#!/bin/bash
# Let me verify the exact issue by checking what the reads() signature expects
echo "=== Server.reads() full signature and implementation ==="
sed -n '417,435p' src/netius/base/server.py

echo "=== Confirm: Agent has NO reads/writes/errors stubs ==="
echo "Agent stubs that DO exist:"
sed -n '38,185p' src/netius/base/agent.py | grep 'def ' | head -20

echo "=== Agent stubs that are MISSING (required by Container.poll) ==="
echo "Container calls: base.reads(reads), base.writes(writes), base.errors(errors)"
echo "But Agent has no: def reads, def writes, def errors"

echo "=== Conclusion: raw_client attribute is never assigned in SOCKSServer ==="
echo "Line 143: self.add_base(self.raw_client)  <- raw_client doesn't exist!"
echo "Line 147: self.raw_client.destroy()        <- raw_client doesn't exist!"
echo "Line 177: self.raw_client.connect()        <- raw_client doesn't exist!"
echo "Line 231: self.reads(...)                  <- reads() doesn't exist on ServerAgent!"

Repository: hivesolutions/netius

Length of output: 1916


self.reads() is not available on SOCKSServer—this change will raise AttributeError at runtime.

SOCKSServer inherits from netius.ServerAgentAgent, neither of which defines reads(). The commit adding backward-compatible Container support (8f10356) promised Base-compatible stubs but never added the reads(), writes(), or errors() methods that Container calls on all bases. The original code used self.raw_client.reads() (though raw_client itself is never assigned in __init__). Either add the missing stub methods to Agent or revert this change to the original approach.

🧰 Tools
🪛 Pylint (4.0.4)

[error] 231-231: Instance of 'SOCKSServer' has no 'reads' member

(E1101)

🤖 Prompt for AI Agents
In `@src/netius/servers/socks.py` at line 231, The change calling
self.reads((tunnel_c.socket,), state=False) will raise AttributeError because
SOCKSServer (and its bases ServerAgent/Agent) don't implement
reads()/writes()/errors(); revert to using the original raw_client interface or
add the missing stub methods. Fix option A: change the call back to
self.raw_client.reads((tunnel_c.socket,), state=False) and ensure raw_client is
correctly assigned in SOCKSServer.__init__ (or wherever the client is created).
Fix option B: add no-op stub methods reads(), writes(), and errors() to
Agent/ServerAgent that forward to raw_client if present (or raise a clear
error), so Container can safely call them; update SOCKSServer to use those stubs
consistently.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates Netius’ Container ecosystem to support multiplexing both legacy Base-derived components and newer Agent/Protocol-based components on the same poll/event loop (targeting issues like the AttributeError seen when running ConsulProxyServer).

Changes:

  • Add Base-compatible lifecycle/introspection stubs to Agent and introduce _container_loop propagation from Container to Agent instances.
  • Add protocol→client event relaying for HTTPClient/RawClient, and adjust proxy throttle paths to use the proxy server’s poll registration (self.reads(...)).
  • Add/expand tests for container setup + reverse proxy request/response flow, and document the change in CHANGELOG.md.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 21 comments.

Show a summary per file
File Description
src/netius/base/agent.py Adds Base-like stub methods to Agent; adds _container_loop; introduces ClientAgent.connect() and protocol event relaying hooks.
src/netius/base/container.py Sets _container_loop on non-Base objects to force protocol connections onto the container owner’s loop.
src/netius/base/protocol.py Adds backward-compat properties/methods on StreamProtocol to look like legacy Connection objects (socket/throttling/pending limits).
src/netius/clients/http.py Uses _container_loop as default loop when unset; relays protocol events through the client to maintain client-first observer patterns.
src/netius/clients/raw.py Adds protocol event relaying for data via the client.
src/netius/extra/proxy_r.py Updates reverse proxy to handle HTTPClient.method() returning (loop, protocol) in async mode.
src/netius/servers/proxy.py Fixes proxy throttling to register reads on the proxy server (owner of the sockets), not on the internal clients.
src/netius/servers/socks.py Same throttling fix as proxy server (server owns the sockets).
src/netius/test/base/container.py New tests covering container behavior with Agent-based clients and mixed Base+Agent setups.
src/netius/test/extra/proxy_r.py Large expansion of reverse proxy tests for routing, relaying, error handling, and container wiring.
CHANGELOG.md Documents the new dual-architecture container support, event relays, throttle changes, and fixes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +823 to +827
del frontend.prefix
del frontend.state
del frontend.proxy_c
del frontend.tunnel_c
return frontend
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

del frontend.prefix/state/proxy_c/tunnel_c on a fresh MagicMock will typically raise AttributeError because those attributes were never set. This makes the test helper brittle and likely to fail before reaching the code under test. Prefer a lightweight real object for frontend (e.g. a custom class / types.SimpleNamespace) or create the mock with spec_set so hasattr() behaves correctly without deleting attributes.

Copilot uses AI. Check for mistakes.
joamag and others added 6 commits February 13, 2026 12:36
Adds architecture.md covering the event loop, Protocol/Transport,
Agent, and Container design. Refocuses compat.md on execution modes,
the CompatLoop adapter, and native vs compat performance tradeoffs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@joamag joamag added p-high High priority issue labels Feb 13, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@doc/architecture.md`:
- Around line 7-10: Replace the hyphenated phrase "Bi-directional asyncio
Compatibility" (and any occurrences of "bi-directional compatibility") with the
unhyphenated "Bidirectional asyncio Compatibility" everywhere in the document;
update the section header "Bi-directional asyncio Compatibility" and any inline
uses of "bi-directional compatibility" to "Bidirectional" to match standard
technical usage and asyncio wording.
- Around line 129-131: Replace the informal "eg" with the standard abbreviation
"e.g." in the sentence "Connections are owned by the right Base (eg
ProxyServer)" so the line reads "Connections are owned by the right Base (e.g.
ProxyServer)"; update any other occurrences of "eg" in the same document for
consistency and run a quick spell/grammar check on doc/architecture.md to ensure
punctuation spacing matches the style.

In `@doc/compat.md`:
- Around line 69-75: In _connect_stream_compat, the example incorrectly
instantiates a protocol and passes an undefined build_protocol to
loop.create_connection; remove the unused protocol = protocol_factory() or
replace it by assigning build_protocol = protocol_factory, or simply pass the
callable directly by changing the create_connection call to use protocol_factory
(i.e., ensure loop.create_connection receives the protocol factory callable
rather than a protocol instance), updating the code around protocol_factory,
protocol and build_protocol to be consistent.

In `@src/netius/test/base/container.py`:
- Around line 262-283: In test_mixed_base_and_agent, avoid double-cleanup by
removing the explicit per-agent cleanup calls (server.cleanup(),
http_client.cleanup(), raw_client.cleanup()) — rely on the container lifecycle
and its cleanup via self.container.cleanup()/tearDown; alternatively, if you
must clean them individually, call self.container.remove_base(...) for each (or
ensure agent.cleanup() is idempotent) so the container's cleanup() (which
iterates self.bases) won't invoke cleanup twice.
🧹 Nitpick comments (1)
src/netius/test/base/container.py (1)

149-149: Prefer assertIsInstance over assertEqual(isinstance(…), True).

assertIsInstance produces a clearer failure message ("X is not an instance of dict" vs "False != True").

♻️ Proposed fix
-        self.assertEqual(isinstance(result, dict), True)
+        self.assertIsInstance(result, dict)

Comment on lines +7 to +10
## Bi-directional asyncio Compatibility

A core design principle of Netius is **bi-directional compatibility** with Python's asyncio. Rather
than being a one-way wrapper, compatibility works in both directions across three layers:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Prefer “Bidirectional” (no hyphen) for standard technical usage.

This is a common style in technical documentation and aligns with asyncio’s wording.

✏️ Suggested wording
-## Bi-directional asyncio Compatibility
+## Bidirectional asyncio Compatibility
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## Bi-directional asyncio Compatibility
A core design principle of Netius is **bi-directional compatibility** with Python's asyncio. Rather
than being a one-way wrapper, compatibility works in both directions across three layers:
## Bidirectional asyncio Compatibility
A core design principle of Netius is **bi-directional compatibility** with Python's asyncio. Rather
than being a one-way wrapper, compatibility works in both directions across three layers:
🤖 Prompt for AI Agents
In `@doc/architecture.md` around lines 7 - 10, Replace the hyphenated phrase
"Bi-directional asyncio Compatibility" (and any occurrences of "bi-directional
compatibility") with the unhyphenated "Bidirectional asyncio Compatibility"
everywhere in the document; update the section header "Bi-directional asyncio
Compatibility" and any inline uses of "bi-directional compatibility" to
"Bidirectional" to match standard technical usage and asyncio wording.

Comment on lines +129 to +131
1. Connections are owned by the right Base (eg ProxyServer)
2. `poll_owner()` routes their events to that Base's `on_read()` -> `on_data_base()`
3. Data reaches the transport -> protocol layer
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use “e.g.” instead of “eg” in formal docs.

Minor readability/consistency improvement.

✏️ Suggested wording
-1. Connections are owned by the right Base (eg ProxyServer)
+1. Connections are owned by the right Base (e.g., ProxyServer)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
1. Connections are owned by the right Base (eg ProxyServer)
2. `poll_owner()` routes their events to that Base's `on_read()` -> `on_data_base()`
3. Data reaches the transport -> protocol layer
1. Connections are owned by the right Base (e.g., ProxyServer)
2. `poll_owner()` routes their events to that Base's `on_read()` -> `on_data_base()`
3. Data reaches the transport -> protocol layer
🤖 Prompt for AI Agents
In `@doc/architecture.md` around lines 129 - 131, Replace the informal "eg" with
the standard abbreviation "e.g." in the sentence "Connections are owned by the
right Base (eg ProxyServer)" so the line reads "Connections are owned by the
right Base (e.g. ProxyServer)"; update any other occurrences of "eg" in the same
document for consistency and run a quick spell/grammar check on
doc/architecture.md to ensure punctuation spacing matches the style.

Comment on lines +69 to +75
def _connect_stream_compat(..., loop=None):
loop = loop or common.get_loop()
protocol = protocol_factory()
connect = loop.create_connection(build_protocol, host=host, port=port, ssl=ssl)
future = loop.create_task(connect)
future.add_done_callback(on_connect)
```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clarify protocol factory variable naming.

The code example has inconsistent variable naming that may confuse readers:

  • Line 71 creates protocol = protocol_factory() but this variable is never used
  • Line 72 uses build_protocol (undefined) as the protocol factory argument to create_connection()
  • This differs from the Native Path example (line 56) which consistently uses protocol_factory()

Since create_connection() expects a protocol factory callable (not an instance), consider either:

  1. Removing line 71 and defining build_protocol as an alias: build_protocol = protocol_factory
  2. Or using protocol_factory directly: connect = loop.create_connection(protocol_factory, host=host, port=port, ssl=ssl)
📝 Suggested fix for consistency
 def _connect_stream_compat(..., loop=None):
     loop = loop or common.get_loop()
-    protocol = protocol_factory()
-    connect = loop.create_connection(build_protocol, host=host, port=port, ssl=ssl)
+    connect = loop.create_connection(protocol_factory, host=host, port=port, ssl=ssl)
     future = loop.create_task(connect)
     future.add_done_callback(on_connect)
🤖 Prompt for AI Agents
In `@doc/compat.md` around lines 69 - 75, In _connect_stream_compat, the example
incorrectly instantiates a protocol and passes an undefined build_protocol to
loop.create_connection; remove the unused protocol = protocol_factory() or
replace it by assigning build_protocol = protocol_factory, or simply pass the
callable directly by changing the create_connection call to use protocol_factory
(i.e., ensure loop.create_connection receives the protocol factory callable
rather than a protocol instance), updating the code around protocol_factory,
protocol and build_protocol to be consistent.

Comment on lines +262 to +283
def test_mixed_base_and_agent(self):
server = netius.StreamServer()
http_client = netius.clients.HTTPClient()
raw_client = netius.clients.RawClient()

self.container.add_base(server)
self.container.add_base(http_client)
self.container.add_base(raw_client)

self.assertEqual(len(self.container.bases), 3)

# runs all lifecycle operations to ensure no errors
self.container.start_all()
self.container.ticks()
self.container.call_all("on_start")
self.container.call_all("on_stop")
self.container.connections_dict()
self.container.connection_dict(1)

server.cleanup()
http_client.cleanup()
raw_client.cleanup()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same double-cleanup concern applies here — individual cleanup() calls followed by tearDown.

Lines 281-283 explicitly clean up server, http_client, and raw_client. Then tearDown calls self.container.cleanup(), which iterates self.bases (still containing all three) and calls .cleanup() on each again. If the agents' cleanup() is not idempotent this will fail. This pattern recurs in several tests in this file (e.g., test_add_base_agent, test_start_base_agent, test_ticks_agent, etc.).

🤖 Prompt for AI Agents
In `@src/netius/test/base/container.py` around lines 262 - 283, In
test_mixed_base_and_agent, avoid double-cleanup by removing the explicit
per-agent cleanup calls (server.cleanup(), http_client.cleanup(),
raw_client.cleanup()) — rely on the container lifecycle and its cleanup via
self.container.cleanup()/tearDown; alternatively, if you must clean them
individually, call self.container.remove_base(...) for each (or ensure
agent.cleanup() is idempotent) so the container's cleanup() (which iterates
self.bases) won't invoke cleanup twice.

Copy link
Contributor Author

@joamag joamag left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@joamag joamag merged commit 4ba1149 into master Feb 13, 2026
17 checks passed
@joamag joamag deleted the feat/container-backwards branch February 13, 2026 15:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request p-high High priority issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants