-
Notifications
You must be signed in to change notification settings - Fork 1.1k
examples: agent-host tool-policy BYOI template (#645) #1219
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 41 commits
8f7e2ff
64d0fe5
19e549a
f817387
c6dd509
ca731d2
76ee7fa
b4b04d5
d237405
f841f37
68d047a
0541764
0551642
b776fac
e763e59
af55c07
fa9b32b
ea18327
2802bc9
0348de6
bb5732f
1f5f44f
8f0424b
7ee3be8
67357c5
57f656e
e0d7fee
0bcdff5
4782905
e6cfef6
5a94fac
220a5d0
e0ca75c
2d18b1b
43a9377
fe2f7e3
19a75e4
aeb3c49
8a305cc
8987c47
f842558
85ba1c5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |
| | 示例 | 说明 | | ||
| |------|------| | ||
| | [代码沙箱快速入门](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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 建议:表格排序位置 与英文版一样,由于"Agent"(A)在字母序上排在"代码沙箱"(代)和"浏览器沙箱"(浏)之前,建议将新行移到表格最上方("代码沙箱快速入门"之前),以便与表格的近似字母序惯例保持一致。 |
||
| | [浏览器沙箱(Playwright)](https://github.com/tencentcloud/CubeSandbox/tree/master/examples/browser-sandbox) | 在 MicroVM 中运行无头 Chromium,通过 CDP 协议使用 Playwright 远程控制浏览器。 | | ||
| | [OpenClaw 集成](https://github.com/tencentcloud/CubeSandbox/tree/master/examples/openclaw-integration) | 部署 Cube Sandbox 并配置 OpenClaw Skill,让 AI Agent 能够在隔离的虚拟机环境中执行代码。 | | ||
| | [SWE-bench + mini-swe-agent](https://github.com/tencentcloud/CubeSandbox/tree/master/examples/mini-rl-training) | 使用 cube-sandbox + mini-swe-agent 在隔离沙箱中自动化 SWE-bench 编码任务,支持多模型切换和 RL 训练愿景。 | | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| # Required: Cube API server address | ||
| export E2B_API_URL="http://<your-node-ip>:3000" | ||
|
|
||
| # Required: any non-empty value satisfies the SDK check | ||
| export E2B_API_KEY="e2b_000000" | ||
|
|
||
| # Required: template ID from cubemastercli tpl create-from-image (this Dockerfile) | ||
| export CUBE_TEMPLATE_ID="<your-template-id>" | ||
|
|
||
| # Optional: mkcert CA when talking to cube.app HTTPS | ||
| # export SSL_CERT_FILE="/root/.local/share/mkcert/rootCA.pem" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| guest/cube-tool text eol=lf | ||
| tool-profile.txt text eol=lf |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| .env | ||
| __pycache__/ | ||
| *.pyc | ||
| .venv/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| # syntax=docker/dockerfile:1.7 | ||
| # | ||
| # Agent toolbox BYOI: cubesandbox-base + guest cube-tool runner + tool-profile. | ||
| # Host demos allowlist "cube-tool" (and/or bare toolbox names); the wrapper | ||
| # re-checks the real tool name inside the MicroVM. | ||
| # | ||
| # Inherited cube-entrypoint keeps envd on :49983 (/health). This Dockerfile | ||
| # only EXPOSEs that port — it does not add a separate health server. | ||
| # | ||
| # docker build -t agent-tool-allowlist-sandbox:latest . | ||
| # | ||
| # cubesandbox-base already ships coreutils and curl. INSTALL_CURL=1 is only a | ||
| # defensive reinstall if a custom CUBE_BASE_IMAGE omits curl. | ||
|
|
||
| ARG CUBE_BASE_IMAGE=ghcr.io/tencentcloud/cubesandbox-base:2026.16 | ||
| FROM ${CUBE_BASE_IMAGE} | ||
|
|
||
| ARG DEBIAN_FRONTEND=noninteractive | ||
| ARG INSTALL_CURL=0 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor accuracy nit: the base image |
||
|
|
||
| RUN mkdir -p /etc/cube-sandbox /workspace /usr/local/bin \ | ||
| && if [ "$INSTALL_CURL" = "1" ]; then \ | ||
| apt-get update \ | ||
| && apt-get install -y --no-install-recommends curl \ | ||
| && rm -rf /var/lib/apt/lists/*; \ | ||
| fi | ||
|
|
||
| COPY tool-profile.txt /etc/cube-sandbox/tool-profile.txt | ||
| COPY guest/cube-tool /usr/local/bin/cube-tool | ||
| COPY workspace/ /workspace/ | ||
|
|
||
| # Base image default user is ``user`` (uid 1000). Make /workspace writable for | ||
| # demos that choose WORKDIR paths (most demos still write under /tmp). | ||
| RUN sed -i 's/\r$//' /usr/local/bin/cube-tool /etc/cube-sandbox/tool-profile.txt \ | ||
| && chmod 0755 /usr/local/bin/cube-tool \ | ||
| && chown -R 1000:1000 /workspace \ | ||
| && /usr/local/bin/cube-tool echo "cube-tool-ok" \ | ||
| && if /usr/local/bin/cube-tool bash -c id; then echo "cube-tool must deny bash" >&2; exit 1; fi \ | ||
| && test -f /workspace/README.txt | ||
|
|
||
| WORKDIR /workspace | ||
| ENV CUBE_TOOL_PROFILE=/etc/cube-sandbox/tool-profile.txt | ||
|
|
||
| EXPOSE 49983 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| # Agent Tool Allowlist Sandbox | ||
|
|
||
| [中文文档](README_zh.md) | ||
|
|
||
| [#645](https://github.com/TencentCloud/CubeSandbox/issues/645) **scenario template**: | ||
| agent-host **tool argv policy** + a BYOI image with a real guest runner | ||
| (`/usr/local/bin/cube-tool`). Same shelf as `network-policy` (egress) — a | ||
| platform control-plane pattern users copy — not another language runtime. | ||
|
|
||
| **Why a dedicated example (answer to [#1062](https://github.com/TencentCloud/CubeSandbox/pull/1062) close notes)** | ||
| - Not a thin script on `sandbox-code`: this tree builds its **own** OCI image, | ||
| installs `cube-tool`, embeds `tool-profile.txt`, and verifies with | ||
| `verify_template.py` on a live cluster. | ||
| - Not a second code-interpreter tutorial: the product is **host+guest tool | ||
| policy**, stacked with airgap / CIDR / pause / fan-out. | ||
|
|
||
| **What you get** | ||
| - Host refuses illegal tools before `Sandbox.create` / `commands.run` | ||
| - Image installs `/usr/local/bin/cube-tool` + `/etc/cube-sandbox/tool-profile.txt` | ||
| + `/workspace` — guest re-checks the profile (not just a text file on disk) | ||
| - Stacks with airgap, CIDR `allow_out`, pause/resume, and parallel sandboxes | ||
|
|
||
| **What this is not:** kernel jail, bash-free base, language runtime, or an LLM agent. | ||
|
|
||
| ## Use cases | ||
|
|
||
| - Agent hosts that should prefer `cube-tool <name> …` over raw `bash`/`curl` | ||
| - Defense-in-depth: host allowlists `cube-tool`; guest refuses off-profile names | ||
| - Differentiated stacks: checkpoint / egress / multi-sandbox fan-out | ||
|
|
||
| ## Resources | ||
|
|
||
| | Item | Suggestion | | ||
| |------|------------| | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two nits here: (1) the demos actually write to |
||
| | Writable layer | `--writable-layer-size 1G` (demos write under `/tmp`; `/workspace` is chowned to uid 1000) | | ||
| | Ports | expose/probe `49983` (envd from `cubesandbox-base`) | | ||
| | Fan-out | keep `N≤2` on shared nodes | | ||
| | CPU/mem | default template quotas are enough for echo/artifact demos | | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - Cube Sandbox cluster + `cubemastercli` + Docker + Python 3.10+ | ||
|
|
||
| ```bash | ||
| pip install -r requirements.txt | ||
| cp .env.example .env | ||
| ``` | ||
|
|
||
| ## Quick start | ||
|
|
||
| ### 1 — Build & register | ||
|
|
||
| ```bash | ||
| docker build -t agent-tool-allowlist-sandbox:latest . | ||
| # optional: --build-arg INSTALL_CURL=1 | ||
|
|
||
| # local image smoke (no cluster) | ||
| docker run --rm agent-tool-allowlist-sandbox:latest \ | ||
| 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clarify: nothing listens on port 49983 / The template registration command passes 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. |
||
| --writable-layer-size 1G \ | ||
| --expose-port 49983 \ | ||
| --probe 49983 \ | ||
| --probe-path /health | ||
| ``` | ||
|
|
||
| `--probe 49983 --probe-path /health` targets **envd** from `cubesandbox-base` | ||
| (inherited entrypoint); this Dockerfile only `EXPOSE`s that port and does not | ||
| add its own health server. | ||
|
|
||
| Put READY template id into `.env` as `CUBE_TEMPLATE_ID`. | ||
|
|
||
| ### 2 — Run demos | ||
|
|
||
| | Step | Command | Expect | Cluster | | ||
| |------|---------|--------|---------| | ||
| | Limits | `python tool_allowlist_limits.py` | `LIMITS_DEMO_OK` | no | | ||
| | Unit tests | `python -m unittest test_tool_allowlist.py -v` | OK | no | | ||
| | Deny | `python tool_allowlist_deny.py` | denied | no | | ||
| | Template smoke | `python verify_template.py` | `TEMPLATE_VERIFY_OK` | yes | | ||
| | Guest runner | `python tool_allowlist_guest_runner.py` | `GUEST_RUNNER_OK` | yes | | ||
| | Allow + airgap | `python tool_allowlist_allow.py` | echo + artifact | yes | | ||
| | Loop | `python tool_agent_loop.py` | `AGENT_LOOP_OK` | yes | | ||
| | Checkpoint | `python tool_allowlist_checkpoint.py` | `CHECKPOINT_OK` | yes | | ||
| | Egress stack | `python tool_allowlist_egress.py` | `EGRESS_STACK_OK` | yes | | ||
| | Fan-out | `python tool_allowlist_fanout.py` | `FANOUT_OK` | yes | | ||
|
|
||
| ## How it works | ||
|
|
||
| ``` | ||
| propose: cube-tool echo hi | ||
| │ | ||
| ▼ | ||
| host assert_allowlisted (argv0 must be allowlisted; prefer cube-tool) | ||
| │ | ||
| ▼ | ||
| Sandbox.create(this BYOI) | ||
| │ | ||
| ▼ | ||
| /usr/local/bin/cube-tool → checks tool-profile.txt → exec echo | ||
| ``` | ||
|
|
||
| Bare allowlisted tools (`echo`, `cat`, …) still pass the host gate for demos; | ||
| prefer `cube-tool` so the guest profile is enforced. | ||
|
|
||
| ## Directory | ||
|
|
||
| ``` | ||
| ├── Dockerfile # installs cube-tool + profile + /workspace | ||
| ├── tool-profile.txt # guest allowlist (copied into the image) | ||
| ├── guest/cube-tool # in-guest runner | ||
| ├── workspace/ # default WORKDIR in the image | ||
| ├── tool_allowlist.py # host argv gate | ||
| ├── tool_allowlist_*.py # demos | ||
| ├── verify_template.py | ||
| └── … | ||
| ``` | ||
|
|
||
| ## Limits | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Documentation gap: Shell globbing ( The documented residuals cover
These are consistent with the gate's documented scope (no shell-expansion or path confinement), but listing them explicitly here would help readers understand the full residual surface. Same gap in |
||
|
|
||
| - Base image still has a shell; callers that bypass `cube-tool` are out of scope | ||
| for the guest wrapper. | ||
| - Allowlisting bare `cat` still permits `cat /etc/passwd` through the host gate. | ||
| - Documented residuals (host gate is not a full shell parser): simple redirects | ||
| (`echo … > file`, `cat < /etc/passwd`) and glob chars `*` / `?` (guest shell | ||
| may expand them before the binary runs). | ||
| - Host gate **does** refuse bash process substitution (`<(…)`, `>(…)`) and | ||
| `/dev/tcp` / `/dev/udp` (argv0 would otherwise stay allowlisted while the | ||
| guest shell runs other programs or opens sockets). | ||
| - Growing the allowlist needs `extra_binaries` + `allow_unsafe_allowlist_extension=True`. | ||
| - `cubesandbox-base` already includes `curl`; `INSTALL_CURL=1` is only for custom | ||
| bases that omit it. Demo “curl missing” branches are defensive fallbacks. | ||
| - Fan-out creates real VMs — keep `N` small. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| # Agent 工具白名单沙箱 | ||
|
|
||
| [English](README.md) | ||
|
|
||
| [#645](https://github.com/TencentCloud/CubeSandbox/issues/645) **场景模板**:Agent | ||
| 宿主侧 **工具 argv 策略** + 带真实 guest runner(`/usr/local/bin/cube-tool`)的 | ||
| BYOI 镜像。与 `network-policy`(出口策略)同层——给用户可复制的平台控制面样例, | ||
| 而不是又一个语言运行时。 | ||
|
|
||
| **为何单独成例(回应 [#1062](https://github.com/TencentCloud/CubeSandbox/pull/1062) 关闭意见)** | ||
| - 不是在 `sandbox-code` 上叠薄脚本:本目录自建 OCI 镜像、安装 `cube-tool`、嵌入 | ||
| `tool-profile.txt`,并用 `verify_template.py` 在真集群冒烟。 | ||
| - 不是第二份代码解释器教程:交付物是 **宿主+guest 工具策略**,并可叠加断网 / | ||
| CIDR / pause / 扇出。 | ||
|
|
||
| **你会得到** | ||
| - 宿主机在 `Sandbox.create` / `commands.run` 前拒绝非法工具 | ||
| - 镜像安装 `/usr/local/bin/cube-tool`、`tool-profile.txt`、`/workspace`——guest | ||
| 会再校验工具名(不是只放一个文本文件) | ||
| - 可叠加断网、CIDR `allow_out`、pause/resume、多沙箱扇出 | ||
|
|
||
| **这不是:** 内核级 jail、无 bash 基础镜像、语言运行时,或 LLM Agent。 | ||
|
|
||
| ## 资源建议 | ||
|
|
||
| | 项 | 建议 | | ||
| |----|------| | ||
| | 可写层 | `--writable-layer-size 1G`(演示脚本写 `/tmp`;镜像内 `/workspace` 已 chown 给 uid 1000) | | ||
| | 端口 | expose/probe `49983`(`cubesandbox-base` 的 envd) | | ||
| | 扇出 | 共享节点上保持 `N≤2` | | ||
| | CPU/内存 | 默认模板配额即可跑 echo/artifact 演示 | | ||
|
|
||
| ## 快速开始 | ||
|
|
||
| ```bash | ||
| pip install -r requirements.txt | ||
| cp .env.example .env | ||
|
|
||
| docker build -t agent-tool-allowlist-sandbox:latest . | ||
| docker run --rm agent-tool-allowlist-sandbox:latest cube-tool echo build-ok | ||
|
|
||
| cubemastercli tpl create-from-image \ | ||
| --image <仓库或本地>/agent-tool-allowlist-sandbox:latest \ | ||
| --writable-layer-size 1G \ | ||
| --expose-port 49983 \ | ||
| --probe 49983 \ | ||
| --probe-path /health | ||
| ``` | ||
|
|
||
| `--probe 49983 --probe-path /health` 指向基础镜像自带的 **envd**(继承 | ||
| entrypoint);本 Dockerfile 只 `EXPOSE` 该端口,不另起健康检查服务。 | ||
|
|
||
| 把 READY 模板 id 写入 `.env` 的 `CUBE_TEMPLATE_ID`。 | ||
|
|
||
| | 步骤 | 命令 | 期望 | | ||
| |------|------|------| | ||
| | 边界/单测/拒绝 | `tool_allowlist_limits.py` / unittest / `deny` | OK | | ||
| | 模板冒烟 | `verify_template.py` | `TEMPLATE_VERIFY_OK` | | ||
| | Guest runner | `tool_allowlist_guest_runner.py` | `GUEST_RUNNER_OK` | | ||
| | allow / loop / checkpoint / egress / fanout | 见英文 README 表 | 对应 `*_OK` | | ||
|
|
||
| ## 原理 | ||
|
|
||
| 推荐路径:宿主白名单放行 `cube-tool` → 镜像内 `cube-tool` 对照 | ||
| `/etc/cube-sandbox/tool-profile.txt` 再 `exec`。裸 `echo`/`cat` 仍可过宿主门控 | ||
| (演示用);生产更应只放行 `cube-tool`。 | ||
|
|
||
| ## 限制 | ||
|
|
||
| - 基础镜像仍有 shell;绕过 `cube-tool` 直接调 bash/路径二进制,不在 guest wrapper 范围内。 | ||
| - 宿主白名单含裸 `cat` 时,`cat /etc/passwd` 仍过宿主门控。 | ||
| - 文档化残差(本门控不是完整 shell 解析器):简单重定向(`echo … > file`、`cat < /etc/passwd`) | ||
| 以及 `*` / `?` 通配(可能由 guest shell 先展开)。 | ||
| - 宿主门控**会拒绝** bash 进程替换(`<(…)` / `>(…)`)以及 `/dev/tcp` / `/dev/udp` | ||
| (否则 argv0 仍在白名单内,guest shell 却能拉起其他程序或开套接字)。 | ||
| - 扩白名单需 `extra_binaries` + `allow_unsafe_allowlist_extension=True`。 | ||
| - `cubesandbox-base` 已含 `curl`;`INSTALL_CURL=1` 仅用于自定义缺 curl 的 base。 | ||
| - Fan-out 会创建真实 VM,共享集群请保持小 `N`。 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| from pathlib import Path | ||
|
|
||
| from dotenv import load_dotenv | ||
|
|
||
|
|
||
| def load_local_dotenv() -> None: | ||
| """Best-effort load of a nearby .env file without overriding real env vars.""" | ||
| for path in (Path(__file__).with_name(".env"), Path.cwd() / ".env"): | ||
| if path.is_file(): | ||
| # str() keeps older python-dotenv working; requirements pin >=0.17. | ||
| load_dotenv(dotenv_path=str(path), override=False) | ||
| return |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| #!/bin/sh | ||
| # Copyright (c) 2024 Tencent Inc. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # | ||
| # In-guest tool runner: only exec binaries listed in tool-profile.txt. | ||
| # Complements the host argv gate — host should allow "cube-tool" and let | ||
| # this wrapper re-check the real tool name inside the MicroVM. | ||
| # | ||
| # Not a kernel jail: callers that bypass cube-tool (raw bash, path binaries) | ||
| # are out of scope for this wrapper. | ||
|
|
||
| set -eu | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hardening nit: Consider resetting The script quotes variables correctly and uses the |
||
|
|
||
| PROFILE="${CUBE_TOOL_PROFILE:-/etc/cube-sandbox/tool-profile.txt}" | ||
|
|
||
| usage() { | ||
| echo "usage: cube-tool <tool> [args...]" >&2 | ||
| echo "profile: ${PROFILE}" >&2 | ||
| exit 2 | ||
| } | ||
|
|
||
| [ "$#" -ge 1 ] || usage | ||
|
|
||
| tool=$1 | ||
| shift | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: the case pattern already rejects The |
||
|
|
||
| case "$tool" in | ||
| ''|*[!A-Za-z0-9._-]*|.*|*/*|*\\*) | ||
| echo "cube-tool: refusing unsafe tool name: ${tool}" >&2 | ||
| exit 1 | ||
| ;; | ||
| esac | ||
|
|
||
| if [ ! -f "$PROFILE" ]; then | ||
| echo "cube-tool: missing profile ${PROFILE}" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| # 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: use The Since |
||
| fi | ||
|
|
||
| bin=$(command -v "$tool" 2>/dev/null || true) | ||
| if [ -z "$bin" ]; then | ||
| echo "cube-tool: tool not installed: ${tool}" >&2 | ||
| exit 127 | ||
| fi | ||
|
|
||
| # Refuse path hijacks if command -v returned something unexpected. | ||
| base=$(basename "$bin") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This 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 |
||
| if [ "$base" != "$tool" ]; then | ||
| echo "cube-tool: resolved binary basename mismatch: ${bin}" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| exec "$bin" "$@" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| e2b-code-interpreter>=2.4.1 | ||
| python-dotenv>=0.17.0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 theA-prefixed entry fits best at the start.