zozo123 commented on code in PR #71725: URL: https://github.com/apache/airflow/pull/71725#discussion_r4211656119
########## providers/common/ai/tests/system/common/ai/example_sandbox_toolset_boat.py: ########## @@ -0,0 +1,167 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""End-to-end system test for SandboxToolset with Boat.""" + +from __future__ import annotations + +import os +from datetime import UTC, datetime + +from airflow.providers.common.compat.sdk import dag as airflow_dag, task + +ENV_ID = os.environ.get("SYSTEM_TESTS_ENV_ID") +DAG_ID = f"common_ai_sandbox_toolset_boat_{ENV_ID}" if ENV_ID else "common_ai_sandbox_toolset_boat" + +MARKER = "boundary-ok" +ENV_MARKER = "env-ok" +STATE_PATH = "/tmp/airflow_sandbox_e2e" + + +@airflow_dag( + dag_id=DAG_ID, + schedule="@once", + start_date=datetime(2024, 1, 1, tzinfo=UTC), + catchup=False, + tags=["common.ai", "sandbox", "boat", "system_test"], +) +def example_sandbox_toolset_boat(): + @task + def run_sandbox_agent() -> str: + from pydantic_ai import Agent + from pydantic_ai.messages import ModelMessage, ModelResponse, TextPart, ToolCallPart + from pydantic_ai.models.function import AgentInfo, FunctionModel + + from airflow.providers.common.ai.sandbox import BoatSandboxBackend, SandboxSpec + from airflow.providers.common.ai.toolsets import SandboxToolset + + def model_function(messages: list[ModelMessage], _info: AgentInfo) -> ModelResponse: + returns = [ + part.content + for message in messages + for part in message.parts + if part.part_kind == "tool-return" + ] + + if not returns: + return ModelResponse( + parts=[ + ToolCallPart( + tool_name="write_file", + args={"path": STATE_PATH, "content": MARKER}, + tool_call_id="write", + ) + ] + ) + if "Wrote" not in str(returns[0]): + raise RuntimeError(f"Unexpected write_file result: {returns[0]!r}") + + if len(returns) == 1: + return ModelResponse( + parts=[ + ToolCallPart( + tool_name="run_command", + args={ + "command": ( + f"printf '%s|%s|%s\\n' \"$(cat {STATE_PATH})\" " + '"$AIRFLOW_BOAT_E2E_ENV" "$((6 * 7))"' + ) + }, + tool_call_id="shell", + ) + ] + ) + shell_out = str(returns[1]) + if f"{MARKER}|{ENV_MARKER}|42" not in shell_out: + raise RuntimeError(f"Unexpected run_command result: {shell_out!r}") + + if len(returns) == 2: + return ModelResponse( + parts=[ + ToolCallPart(tool_name="read_file", args={"path": STATE_PATH}, tool_call_id="read") + ] + ) + if MARKER not in str(returns[2]): + raise RuntimeError(f"Unexpected read_file result: {returns[2]!r}") + + if len(returns) == 3: + return ModelResponse( + parts=[ + ToolCallPart( + tool_name="run_command", + args={"command": "echo to-stderr >&2; exit 3"}, + tool_call_id="fail", + ) + ] + ) + failed = str(returns[3]) + if "[exit code: 3]" not in failed or "to-stderr" not in failed: + raise RuntimeError(f"Unexpected failure result: {failed!r}") + + command_cases = [ + ("echo comment-ok # trailing comment", "comment-ok"), + ("cat <<'EOF'\nheredoc-ok\nEOF", "heredoc-ok"), + ("sleep 20 & echo background-ok", "background-ok"), Review Comment: The system test now runs `sleep 30` with a 2 s budget and checks that the sandbox and an earlier file survive (fb80b92a08). It passed live against boat.dev. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_ ########## providers/common/ai/docs/sandbox/backends.rst: ########## @@ -301,24 +387,28 @@ do not change. Four behaviours do, so read them before assuming the same Dag behaves identically everywhere: - **CPU.** ``sbx`` gives a sandbox every host CPU; Modal defaults to a request of - 0.125 of one, so set ``cpu``; OpenSandbox takes ``cpu`` as a limit the server enforces. + 0.125 of one, so set ``cpu``; OpenSandbox takes ``cpu`` as a limit the server enforces; + Boat sizes by ``machine_type``. - **Egress allowlists.** ``sbx`` enforces ``allow_egress_to`` at the host policy layer; Modal matches TLS handshake names, which is weaker and has to be opted into; OpenSandbox enforces it in an egress sidecar, and the backend reads the enforced policy back rather than trusting the create request. ``allow_egress_to_cidrs`` is enforced at the address layer on Modal, refused on ``sbx``, and refused by OpenSandbox because its SDK cannot prove that the sidecar is running in the - ``dns+nft`` mode required for CIDR enforcement. + ``dns+nft`` mode required for CIDR enforcement. Boat has no form of egress control + at all and refuses every network restriction rather than provisioning something + weaker. - **Command timeouts.** A timeout destroys an ``sbx`` sandbox and its files; - Modal and a server-enforced OpenSandbox timeout preserve the sandbox and files. - OpenSandbox destroys it only if the command event stream itself stalls past the - client-side grace period. + Modal, Boat and a server-enforced OpenSandbox timeout preserve the sandbox and + files. OpenSandbox destroys it only if the command event stream itself stalls + past the client-side grace period, and Boat only if its own deadline, set past + the one in the guest, ends the call. - **Symlinks.** ``write_file`` through a symlink follows the link on ``sbx`` and replaces it on Modal and OpenSandbox. Review Comment: Boat follows the link: measured live, the symlink stays and its target receives the content. The bullet now names Boat (5e6559afe3). _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_ -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
