Merge pull request 'feat(molecule_agent): add strip_a2a_boundary() for OFFSEC-003 trust-boundary markers' (#8) from feat/offsec003-a2a-boundary-strip into main
This commit is contained in:
commit
82e78f2025
@ -72,7 +72,7 @@ A runnable demo with full setup walkthrough lives at
|
||||
| `poll_state()` | 30.4 | Lightweight `{status, paused, deleted}` poll |
|
||||
| `heartbeat(...)` | 30.1 | Single bearer-authed heartbeat |
|
||||
| `get_peers()` / `discover_peer()` | 30.6 | Sibling URL discovery with TTL cache |
|
||||
| `call_peer(target, message)` | 30.6 | Direct A2A with proxy fallback |
|
||||
| `call_peer(target, message)` | 30.6 | Direct A2A with proxy fallback; response may be wrapped in OFFSEC-003 boundary markers — use ``strip_a2a_boundary()`` to remove them |
|
||||
| `fetch_inbound(since_id=…)` | 30.8c | One-shot poll of `/workspaces/:id/activity` for inbound A2A |
|
||||
| `reply(msg, text)` | 30.8c | Smart-routes reply to `/notify` (canvas user) or `/a2a` (peer) |
|
||||
| `run_heartbeat_loop()` | combo | Drives heartbeat + state-poll on a timer; exits on pause/delete |
|
||||
@ -165,6 +165,27 @@ silent acks. On non-2xx the underlying `requests.HTTPError` propagates so the
|
||||
handler can decide whether to retry, surface to its observability, or fail
|
||||
loudly.
|
||||
|
||||
### OFFSEC-003 — A2A peer response trust boundary
|
||||
|
||||
As of the OFFSEC-003 platform rollout, peer A2A responses are wrapped in
|
||||
trust-boundary markers before being returned to callers::
|
||||
|
||||
[A2A_RESULT_FROM_PEER]<peer response text>[/A2A_RESULT_FROM_PEER]
|
||||
|
||||
The markers signal that the enclosed content is untrusted third-party output.
|
||||
Use ``strip_a2a_boundary()`` to remove them before passing the response to
|
||||
your agent context::
|
||||
|
||||
from molecule_agent import RemoteAgentClient, strip_a2a_boundary
|
||||
|
||||
result = client.call_peer(target_id, "do the thing")
|
||||
raw_text = result.get("result", {}).get("text", "")
|
||||
trusted_text = strip_a2a_boundary(raw_text)
|
||||
|
||||
The function returns the input unchanged if the markers are absent (platform
|
||||
versions older than the OFFSEC-003 rollout), so it is safe to call on any
|
||||
response.
|
||||
|
||||
## CLI: `molecule_agent connect`
|
||||
|
||||
One command bootstraps the full poll-mode loop. No code beyond your handler:
|
||||
|
||||
@ -39,6 +39,7 @@ from .client import (
|
||||
PeerInfo,
|
||||
RemoteAgentClient,
|
||||
WorkspaceState,
|
||||
strip_a2a_boundary,
|
||||
verify_plugin_sha256,
|
||||
)
|
||||
from .inbound import (
|
||||
@ -71,6 +72,7 @@ __all__ = [
|
||||
"DEFAULT_POLL_INTERVAL",
|
||||
"compute_plugin_sha256",
|
||||
"verify_plugin_sha256",
|
||||
"strip_a2a_boundary",
|
||||
"__version__",
|
||||
]
|
||||
__version__ = "0.1.0"
|
||||
|
||||
@ -90,6 +90,43 @@ def make_idempotency_key(task_text: str) -> str:
|
||||
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
# ── A2A boundary marker stripping (OFFSEC-003) ───────────────────────────────
|
||||
|
||||
_A2A_BOUNDARY_START = "[A2A_RESULT_FROM_PEER]"
|
||||
_A2A_BOUNDARY_END = "[/A2A_RESULT_FROM_PEER]"
|
||||
|
||||
|
||||
def strip_a2a_boundary(text: str) -> str:
|
||||
"""Strip OFFSEC-003 trust-boundary markers from a peer A2A response.
|
||||
|
||||
The platform wraps peer A2A responses in::
|
||||
|
||||
[A2A_RESULT_FROM_PEER]<content>[/A2A_RESULT_FROM_PEER]
|
||||
|
||||
to mark them as untrusted third-party content. Call this helper to
|
||||
remove the wrapper before passing the content to your agent context:
|
||||
|
||||
Usage::
|
||||
|
||||
result = client.call_peer(target_id, "do the thing")
|
||||
text = result.get("result", {}).get("text", "")
|
||||
content = strip_a2a_boundary(text)
|
||||
|
||||
Returns the interior content (everything between the two markers).
|
||||
Returns the input unchanged if the boundary markers are absent (the caller
|
||||
may be talking to a platform version older than the OFFSEC-003 rollout).
|
||||
Returns ``""`` for ``None`` or empty input.
|
||||
"""
|
||||
if not text:
|
||||
return ""
|
||||
start = text.find(_A2A_BOUNDARY_START)
|
||||
end = text.find(_A2A_BOUNDARY_END)
|
||||
if start != -1 and end != -1 and end > start:
|
||||
return text[start + len(_A2A_BOUNDARY_START):end].strip()
|
||||
return text
|
||||
|
||||
|
||||
|
||||
def _safe_extract_tar(tf: tarfile.TarFile, dest: Path) -> None:
|
||||
"""Extract a tarfile, refusing entries that would escape `dest`
|
||||
and logging skipped symlinks/hardlinks.
|
||||
|
||||
@ -709,7 +709,7 @@ def test_install_plugin_404_raises_with_useful_url(client: RemoteAgentClient):
|
||||
|
||||
import hashlib
|
||||
|
||||
from molecule_agent.client import make_idempotency_key
|
||||
from molecule_agent.client import make_idempotency_key, strip_a2a_boundary
|
||||
|
||||
|
||||
def test_delegate_posts_task_and_idempotency_key(client: RemoteAgentClient):
|
||||
@ -883,6 +883,55 @@ def test_make_idempotency_key_deterministic():
|
||||
assert a == b
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# strip_a2a_boundary — OFFSEC-003 trust-boundary marker stripping
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_strip_a2a_boundary_basic():
|
||||
"""Interior text between the two markers is returned."""
|
||||
wrapped = "[A2A_RESULT_FROM_PEER]hello world[/A2A_RESULT_FROM_PEER]"
|
||||
assert strip_a2a_boundary(wrapped) == "hello world"
|
||||
|
||||
|
||||
def test_strip_a2a_boundary_strips_whitespace_edges():
|
||||
"""Trailing/leading whitespace inside the boundary is stripped."""
|
||||
wrapped = "[A2A_RESULT_FROM_PEER] peer reply [/A2A_RESULT_FROM_PEER]"
|
||||
assert strip_a2a_boundary(wrapped) == "peer reply"
|
||||
|
||||
|
||||
def test_strip_a2a_boundary_no_markers_returns_unchanged():
|
||||
"""Without both markers present the input passes through unchanged."""
|
||||
assert strip_a2a_boundary("plain text with no markers") == "plain text with no markers"
|
||||
|
||||
|
||||
def test_strip_a2a_boundary_only_start_returns_unchanged():
|
||||
"""Only a start marker — no-op to stay safe during mid-rollout."""
|
||||
assert strip_a2a_boundary("[A2A_RESULT_FROM_PEER]unclosed") == "[A2A_RESULT_FROM_PEER]unclosed"
|
||||
|
||||
|
||||
def test_strip_a2a_boundary_only_end_returns_unchanged():
|
||||
"""Only an end marker — no-op."""
|
||||
assert strip_a2a_boundary("[/A2A_RESULT_FROM_PEER]no start") == "[/A2A_RESULT_FROM_PEER]no start"
|
||||
|
||||
|
||||
def test_strip_a2a_boundary_empty_returns_empty():
|
||||
assert strip_a2a_boundary("") == ""
|
||||
assert strip_a2a_boundary(None) == "" # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_strip_a2a_boundary_end_before_start_returns_unchanged():
|
||||
"""If end marker appears before start, treat as no-op."""
|
||||
text = "[/A2A_RESULT_FROM_PEER]X[A2A_RESULT_FROM_PEER]"
|
||||
assert strip_a2a_boundary(text) == text
|
||||
|
||||
|
||||
def test_strip_a2a_boundary_multiline_content():
|
||||
"""Multiline interior content is preserved (stripped at edges only)."""
|
||||
wrapped = "[A2A_RESULT_FROM_PEER]\n step one\n step two\n[/A2A_RESULT_FROM_PEER]"
|
||||
assert strip_a2a_boundary(wrapped) == "step one\n step two"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _safe_extract_tar
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Loading…
Reference in New Issue
Block a user