Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ What this does:

## Public API

- `init_assembly(gateway_url, api_key, agent_id=None, mode="auto", *, control_plane_url=None) -> AssemblyContext`
- `init_assembly(gateway_url, api_key, agent_id=None, mode="auto", *, control_plane_url=None, allow_insecure=False) -> AssemblyContext`
- Exceptions: `AssemblyError`, `AgentError`, `PolicyError`, `GatewayError`, `ConfigurationError`
- Data models: `AgentConfig`, `AgentState`, `PolicyEvaluation`

Expand Down Expand Up @@ -206,6 +206,25 @@ Resolution order is **explicit kwarg > env-var > unset**:
| `gateway_url` | `AA_GATEWAY_URL` |
| `control_plane_url` | `AA_CONTROL_PLANE_URL` |

### Insecure transport opt-in

Agent registration dials the gateway's gRPC port over the native `register`
call. By default a **plaintext `http://` register channel to a non-loopback
host is refused** — the `Register` call carries the agent identity, so it must
not travel unencrypted to a remote host. Loopback (local dev) and `https://`
targets always pass.

Pass `allow_insecure=True` to opt into plaintext to a non-loopback host, on a
**trusted network only** (e.g. a private link where TLS terminates elsewhere).
This mirrors the operator-control `connect(allow_insecure=...)` opt-in:

```python
init_assembly(
gateway_url="http://gateway.internal:7391",
allow_insecure=True, # trusted-network only; leave False in production
)
```

## Error Handling

```python
Expand Down
12 changes: 11 additions & 1 deletion agent_assembly/core/assembly.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ def init_assembly(
spawned_by_tool: str | None = None,
depth: int | None = None,
enforcement_mode: EnforcementMode | None = None,
allow_insecure: bool = False,
) -> AssemblyContext:
"""Initialize the Agent Assembly SDK runtime for this process.

Expand Down Expand Up @@ -204,6 +205,15 @@ def init_assembly(
the agent in dry-run / sandbox mode: every action proceeds (local checks
fail open) and the gateway records would-be violations as shadow audit
events.
:param allow_insecure: Opt into a plaintext (non-TLS) native ``register``
channel when the resolved gateway host is non-loopback (AAASM-4664).
Defaults to ``False`` — secure-by-default (AAASM-4655): a derived
plaintext ``http://`` register endpoint to a non-loopback host is
refused, since the ``Register`` call carries the agent identity and must
not travel unencrypted to a remote host. Set to ``True`` only on a
trusted network (loopback dev / private link), mirroring op-control's
``connect(allow_insecure=...)`` semantics. Loopback and ``https://``
targets always pass regardless of this flag.
"""
gateway_url = resolve_gateway_url(gateway_url)
api_key = resolve_api_key(api_key)
Expand Down Expand Up @@ -267,7 +277,7 @@ def init_assembly(
runtime_client=runtime_client,
agent_id=resolved_agent_id,
enforcement_mode=enforcement_mode,
gateway_endpoint=resolve_gateway_grpc_endpoint(gateway_url),
gateway_endpoint=resolve_gateway_grpc_endpoint(gateway_url, allow_insecure=allow_insecure),
native_available=native_available,
team_id=team_id,
parent_agent_id=parent_agent_id,
Expand Down
55 changes: 55 additions & 0 deletions test/unit/core/test_init_registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,61 @@ def test_enforce_mode_propagates_register_failure(monkeypatch: pytest.MonkeyPatc
)


# A non-loopback gateway host reached over plaintext http:// — the register
# endpoint TLS guard (AAASM-4655) fail-closes this by default, and the
# allow_insecure opt-in (AAASM-4664) is what lets it through.
_INSECURE_GW_URL = "http://gateway.test"
_INSECURE_GRPC_ENDPOINT = "http://gateway.test:50051"


def test_init_refuses_plaintext_nonloopback_register_by_default(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Omitting ``allow_insecure`` keeps init fail-closed: a plaintext http://
register channel to a non-loopback host is refused (AAASM-4664 preserves the
secure-by-default guard from AAASM-4655)."""
runtime_client = FakeRuntimeClient(decision="allow")
install_fake_core(monkeypatch, runtime_client)
_no_network(monkeypatch)
monkeypatch.setattr(core_assembly, "_register_adapters", lambda **_kwargs: [])

with pytest.raises(ConfigurationError, match="Failed to initialize assembly runtime"):
init_assembly(
gateway_url=_INSECURE_GW_URL,
api_key=_API_KEY,
agent_id="agent-insecure-default",
mode="sdk-only",
)
# The guard fires before register is dialed, so no registration happened.
assert runtime_client.register_calls == []


def test_init_allow_insecure_permits_plaintext_nonloopback_register(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``allow_insecure=True`` threads through to
``resolve_gateway_grpc_endpoint`` and opts into a plaintext http:// register
channel to a non-loopback host (trusted-network only, AAASM-4664)."""
runtime_client = FakeRuntimeClient(decision="allow")
install_fake_core(monkeypatch, runtime_client)
_no_network(monkeypatch)
monkeypatch.setattr(core_assembly, "_register_adapters", lambda **_kwargs: [])

context = init_assembly(
gateway_url=_INSECURE_GW_URL,
api_key=_API_KEY,
agent_id="agent-insecure-optin",
mode="sdk-only",
allow_insecure=True,
)
try:
assert runtime_client.register_calls == [
("agent-insecure-optin", "agent-insecure-optin", "python", _INSECURE_GRPC_ENDPOINT, None, None)
]
finally:
context.shutdown()


def _patched_register_adapters(adapter: _CapturingAdapter) -> object:
"""Build a stand-in for ``_register_adapters`` that drives the real
interceptor builder and registers the capturing adapter with it."""
Expand Down