From edc1a308b1c491fc0d933541e556e5370649d345 Mon Sep 17 00:00:00 2001 From: Bryant Date: Wed, 15 Jul 2026 21:56:55 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20(init):=20Add=20allow=5Finsecur?= =?UTF-8?q?e=20opt-in=20to=20init=5Fassembly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thread an optional keyword-only allow_insecure (default False) from init_assembly through to resolve_gateway_grpc_endpoint, mirroring the op-control connect() opt-in. Default False preserves the secure-by-default register guard from AAASM-4655: a derived plaintext http:// register channel to a non-loopback host stays refused unless the operator explicitly opts in for a trusted network. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01HMnPRm9T3fdrS4uNYXkzg6 --- agent_assembly/core/assembly.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/agent_assembly/core/assembly.py b/agent_assembly/core/assembly.py index f69fdda..b0d71e6 100644 --- a/agent_assembly/core/assembly.py +++ b/agent_assembly/core/assembly.py @@ -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. @@ -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) @@ -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, From a194b338413c19c3eea75dfa9ad6c0a2ecc6a076 Mon Sep 17 00:00:00 2001 From: Bryant Date: Wed, 15 Jul 2026 21:57:22 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=93=9D=20(readme):=20Document=20allow?= =?UTF-8?q?=5Finsecure=20trusted-network=20opt-in?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the allow_insecure kwarg to the init_assembly public-API signature and a short "Insecure transport opt-in" subsection explaining the fail-closed default and the trusted-network-only opt-in for a plaintext non-loopback register. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01HMnPRm9T3fdrS4uNYXkzg6 --- README.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f15a222..1b16004 100644 --- a/README.md +++ b/README.md @@ -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` @@ -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 From 7caeb4dcc5ff3c78ef3b437bea26d39a8ca221ab Mon Sep 17 00:00:00 2001 From: Bryant Date: Wed, 15 Jul 2026 21:57:31 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E2=9C=85=20(test):=20Cover=20init=5Fassemb?= =?UTF-8?q?ly=20allow=5Finsecure=20register=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regression tests for AAASM-4664: with allow_insecure omitted, init refuses a plaintext http:// register channel to a non-loopback host (no register dialed); with allow_insecure=True it threads through and the native register is dialed against the derived non-loopback gRPC endpoint. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01HMnPRm9T3fdrS4uNYXkzg6 --- test/unit/core/test_init_registration.py | 55 ++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/test/unit/core/test_init_registration.py b/test/unit/core/test_init_registration.py index 7525f4a..b33325f 100644 --- a/test/unit/core/test_init_registration.py +++ b/test/unit/core/test_init_registration.py @@ -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."""