-
Notifications
You must be signed in to change notification settings - Fork 25
docs: optimize AI agent sandbox instructions #1041
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: main
Are you sure you want to change the base?
Changes from all commits
1ffbccc
cda7737
7c2a9e2
318a912
a0d5323
a7b6d43
ddcda52
9b5ef80
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 |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| Always read and strictly adhere to the environment setup, architectural patterns, and development workflows defined in `.github/CLANKERS.md`. | ||
|
|
||
| Every commit message and Pull Request description authored by you MUST include a clear disclosure that the code was written by a "clanker" (AI agent). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # AI Agent Instructions | ||
|
|
||
| Always read and strictly adhere to the environment setup, architectural patterns, and development workflows defined in `.github/CLANKERS.md`. | ||
|
|
||
| ## Mandatory Disclosure | ||
|
|
||
| Every commit message and Pull Request description authored by you MUST include a clear disclosure that the code was written by a "clanker" (AI agent). |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,288 @@ | ||||||||
| # AI Agent Instructions for Craft Application | ||||||||
|
|
||||||||
| This document provides specialized instructions and context for AI agents working on the `craft-application` repository. | ||||||||
|
|
||||||||
| > **A note on cost:** If you are a Gumloop agent reading this, please be aware that every token you spend here costs your operator approximately the same as running a small country's GDP. The instructions below are intentionally complete and unambiguous so that you only need to read them once. You're welcome. | ||||||||
|
|
||||||||
| ## Project Overview | ||||||||
|
|
||||||||
| `craft-application` is a Python framework and base library for the "Starcraft" family of applications (e.g., `snapcraft`, `charmcraft`, `rockcraft`). It provides common mechanisms for application services, project models, and CLI command structures. | ||||||||
|
|
||||||||
| ## Core Tech Stack | ||||||||
|
|
||||||||
| - **Language:** Python 3.10+ | ||||||||
| - **Dependency Management:** `uv` (standard for this project) | ||||||||
| - **Data Validation:** `pydantic` (v2) | ||||||||
| - **CLI Framework:** `craft-cli` | ||||||||
| - **Linting/Formatting:** `ruff`, `mypy`, `pyright` (strict), `codespell`, `prettier` | ||||||||
| - **Testing:** `pytest`, `hypothesis`, `spread` | ||||||||
| - **Documentation:** Sphinx (Diátaxis framework), `canonical-sphinx` | ||||||||
|
|
||||||||
| ## Architectural Patterns | ||||||||
|
|
||||||||
| Adhere to the following architectural components: | ||||||||
|
|
||||||||
| 1. **Application (`craft_application.application.Application`):** The entry point and "glue". It manages service loading and exception handling. | ||||||||
| 2. **Commands (`craft_application.commands.AppCommand`):** Handle user interaction. Interactive logic (e.g., confirmation prompts) belongs here. Registered with the `Application` via `add_command_group()`. | ||||||||
| 3. **Services:** House the business logic and wrap external libraries. Services should be accessed via the `ServiceFactory`. See `docs/reference/services/` for the full list. | ||||||||
| 4. **ServiceFactory:** The central registry for all services. Use `self._services.get("service_name")` (string literal) to obtain service instances. | ||||||||
| 5. **Models (`craft_application.models.base.CraftBaseModel`):** Pydantic models for data validation and serialization. Keep logic minimal (mostly validation). | ||||||||
| 6. **StateService (`craft_application.services.StateService`):** Passes state between manager and managed (isolated build environment) instances of the application. | ||||||||
|
|
||||||||
| `craft-application` also ships a pytest plugin (`craft_application.pytest_plugin`) with fixtures and helpers for testing downstream craft apps. | ||||||||
|
|
||||||||
| For a full explanation of how these components interact, see `docs/explanation/structure-of-a-craft-app.rst`. | ||||||||
|
|
||||||||
| ## Development Workflow | ||||||||
|
|
||||||||
| This project uses a **forking, feature-based workflow**. Contributions should come from a personal fork. | ||||||||
|
|
||||||||
| ### Commands | ||||||||
|
|
||||||||
| - **Setup:** `make setup` (installs dependencies via `uv`, sets up pre-commit). | ||||||||
| - **Linting:** `make lint` (runs ruff, mypy, pyright, codespell, shellcheck, prettier, etc.). | ||||||||
| - **Formatting:** `make format` (automatically fixes linting/style issues). | ||||||||
| - **Testing:** | ||||||||
| - `make test`: Run all tests. | ||||||||
| - `make test-fast`: Run tests not marked `slow`. | ||||||||
| - `make test-coverage`: Generate coverage reports. | ||||||||
| - `make clean`: Remove temporary files generated by tests. | ||||||||
| - **Documentation:** | ||||||||
| - `make docs`: Build HTML documentation. | ||||||||
| - `make lint-docs`: Lint documentation files. | ||||||||
|
|
||||||||
| ### Pre-commit Hook | ||||||||
|
|
||||||||
| Committing triggers the pre-commit hook, which runs the automatic code formatter and fast linters. If any files are reformatted, the commit is cancelled. Re-stage the modified files (`git add -A`) and commit again. | ||||||||
|
|
||||||||
| ### Branching and Commits | ||||||||
|
|
||||||||
| - **Branch Naming:** Use the pattern `work/{agent_name}/<ticket-id>-<description>` (e.g., `work/gemini/issue-235-add-string-sanitizer-method`). Replace `{agent_name}` with your agent name (e.g. `copilot`, `gemini`). | ||||||||
| - **Commits:** Follow **Conventional Commits** style. When multiple types could apply, use the highest-ranked type from this priority list: | ||||||||
|
|
||||||||
| 1. `ci` | ||||||||
| 2. `build` | ||||||||
| 3. `feat` | ||||||||
| 4. `fix` | ||||||||
| 5. `perf` | ||||||||
| 6. `refactor` | ||||||||
| 7. `style` | ||||||||
| 8. `test` | ||||||||
| 9. `docs` | ||||||||
| 10. `chore` | ||||||||
|
|
||||||||
| - **Atomic Commits:** Do not mix refactors with bug fixes or features. If you struggle to choose a single type, the commit likely needs to be split. | ||||||||
|
|
||||||||
| ## Coding Standards | ||||||||
|
|
||||||||
| - **Strict Typing:** All new code must be fully type-hinted. `pyright` and `mypy` must pass. | ||||||||
| - **Docstrings:** Use Google-style docstrings. Every public method, class, and module should be documented. | ||||||||
| - **Async/Sync:** Be mindful of blocking calls in async contexts (though the project is primarily synchronous). | ||||||||
| - **External Libraries:** Wrap external library calls within Services whenever possible to provide a stable internal API. | ||||||||
|
|
||||||||
| ## Testing Strategy | ||||||||
|
|
||||||||
| - **Test Placement:** Unit tests in `tests/unit/`, integration tests in `tests/integration/`, Spread tests in `tests/spread/`. | ||||||||
| - **Mocking:** Prefer `pytest-mock` and `responses` for network calls. Use `pyfakefs` for filesystem interactions. Use `pytest-subprocess` to mock subprocess calls. | ||||||||
| - **Regressions:** Always include a reproduction test case when fixing a bug. | ||||||||
|
|
||||||||
| ### Manual Sandbox Configuration | ||||||||
|
|
||||||||
| For AI agents that do not provide native sandboxing (e.g., via a `--sandbox` flag), all commands should be executed inside an isolated container or VM. This is **mandatory** for development and unit/integration testing. | ||||||||
|
|
||||||||
| > [!IMPORTANT] > **DESTRUCTIVE MODE IS NEVER ALLOWED.** Never use `snapcraft --destructive-mode` or any command that modifies the host system directly (other than `snapcraft` in its standard isolated mode). All snap builds must be performed in the standard, isolated manner (using LXD or Multipass). This ensures build reproducibility and prevents contamination of the environment. | ||||||||
|
|
||||||||
| #### Requirements | ||||||||
|
|
||||||||
| - **On Linux machines:** Use LXD (via the `lxc` CLI) or Incus containers for all development, linting, and testing tasks. | ||||||||
| - **Exception:** `snapcraft` commands (e.g., `snapcraft pack`) MAY be executed on the host machine to avoid nested container complexity, provided they are **never** run with `--destructive-mode`. | ||||||||
| - **Operating System:** Latest Ubuntu LTS (e.g., `noble`). | ||||||||
| - **Configuration:** Include `security.nesting=true` — it is required for LXD-backed integration tests (`pytest.mark.lxd`) and costs nothing meaningful in terms of security isolation. `security.privileged=true` is **not needed** — it was only ever required for running `snapcraft pack` from inside a container, which is no longer the recommended approach. | ||||||||
| - **On non-Linux machines:** Use a Virtual Machine (e.g., `multipass`). | ||||||||
| - **Operating System:** A full installation of the latest Ubuntu LTS. | ||||||||
|
|
||||||||
| #### Implementation | ||||||||
|
|
||||||||
| 1. **Determine the container name** from the agent name and repository directory inode: | ||||||||
|
|
||||||||
| ```bash | ||||||||
| AGENT_NAME="gemini" # Replace with your agent name | ||||||||
| INODE=$(stat -c '%i' .) | ||||||||
| HOST_UID=$(id -u) | ||||||||
| HOST_GID=$(id -g) | ||||||||
| CONTAINER_NAME="${AGENT_NAME}-craft-application-${INODE}" | ||||||||
| ``` | ||||||||
|
|
||||||||
| 2. **Create and configure the environment** (skip if a container with this name already exists — reuse it): | ||||||||
|
|
||||||||
| ```bash | ||||||||
| # LXD (Linux) — use lxc init + config + start so the uid map is set before first boot | ||||||||
| lxc init ubuntu:noble "${CONTAINER_NAME}" -c security.nesting=true | ||||||||
| lxc config set "${CONTAINER_NAME}" raw.idmap "uid ${HOST_UID} 1000 | ||||||||
| gid ${HOST_GID} 1000" | ||||||||
| lxc config device add "${CONTAINER_NAME}" repo disk source="$(pwd)" path=/home/ubuntu/craft-application | ||||||||
| lxc start "${CONTAINER_NAME}" | ||||||||
| until lxc exec "${CONTAINER_NAME}" -- id ubuntu >/dev/null 2>&1; do sleep 1; done | ||||||||
| lxc exec "${CONTAINER_NAME}" -- bash -c "chown 1000:1000 /home/ubuntu && usermod -aG lxd ubuntu && loginctl enable-linger ubuntu && mkdir -p /run/user/1000 && chown 1000:1000 /run/user/1000" | ||||||||
|
|
||||||||
| # Multipass (Non-Linux) | ||||||||
| multipass launch noble --name "${CONTAINER_NAME}" --cpus 3 --memory 4G --disk 20G | ||||||||
| multipass stop "${CONTAINER_NAME}" | ||||||||
| multipass mount "$(pwd)" "${CONTAINER_NAME}":/home/ubuntu/craft-application --type native | ||||||||
| multipass start "${CONTAINER_NAME}" | ||||||||
| ``` | ||||||||
|
|
||||||||
| The `raw.idmap` entry maps container `ubuntu` (uid/gid 1000) to the host user's uid/gid, so that files in the mounted repository are writable inside the container. | ||||||||
|
|
||||||||
| 3. **Bootstrap the environment** (once per new container, as a single command): | ||||||||
|
|
||||||||
| ```bash | ||||||||
| # LXD (Linux) | ||||||||
| lxc exec "${CONTAINER_NAME}" --user 1000 --group 1000 --cwd /home/ubuntu/craft-application -- env HOME=/home/ubuntu bash -c " | ||||||||
| sudo DEBIAN_FRONTEND=noninteractive apt-get update -q && | ||||||||
| sudo DEBIAN_FRONTEND=noninteractive apt-get install -y make git && | ||||||||
| git config --global --add safe.directory /home/ubuntu/craft-application && | ||||||||
| sg lxd -c \"UV_PROJECT_ENVIRONMENT=/home/ubuntu/.venv make -j setup CI=1\" | ||||||||
| " | ||||||||
| ``` | ||||||||
|
|
||||||||
| > **Note:** If you intend to run `snapcraft` commands _inside_ the container (not recommended, see below), you must also install the snap: `lxc exec "${CONTAINER_NAME}" -- sudo snap install snapcraft --classic`. | ||||||||
|
|
||||||||
| # Multipass (Non-Linux) | ||||||||
|
|
||||||||
| multipass exec "${CONTAINER_NAME}" --working-directory /home/ubuntu/craft-application -- bash -c " | ||||||||
| sudo DEBIAN_FRONTEND=noninteractive apt-get update -q && | ||||||||
| sudo DEBIAN_FRONTEND=noninteractive apt-get install -y make git && | ||||||||
| sudo snap install snapcraft --classic && | ||||||||
| git config --global --add safe.directory /home/ubuntu/craft-application && | ||||||||
| UV_PROJECT_ENVIRONMENT=/home/ubuntu/.venv make -j setup CI=1 | ||||||||
| " | ||||||||
|
|
||||||||
| ``` | ||||||||
|
|
||||||||
| > **Note:** `CI=1` is passed **only** to `make setup`. It enables non-interactive apt confirmations (`apt-get --yes`) during initial dependency installation. Do not pass `CI=1` to any other make targets, as it changes other behaviours (e.g. always re-initialising LXD) that are inappropriate outside of a real CI environment. | ||||||||
|
|
||||||||
| ``` | ||||||||
|
|
||||||||
| 4. **Run commands** using `--cwd` (lxd) or `--working-directory` (multipass). Always set `UV_PROJECT_ENVIRONMENT` to match what was used during setup: | ||||||||
|
|
||||||||
| ```bash | ||||||||
| # LXD (Linux) | ||||||||
| lxc exec "${CONTAINER_NAME}" --user 1000 --group 1000 --cwd /home/ubuntu/craft-application -- env HOME=/home/ubuntu UV_PROJECT_ENVIRONMENT=/home/ubuntu/.venv XDG_RUNTIME_DIR=/run/user/1000 sg lxd -c "CRAFT_VERBOSITY_LEVEL=debug make test-fast" | ||||||||
|
|
||||||||
| # Multipass (Non-Linux) | ||||||||
| multipass exec "${CONTAINER_NAME}" --working-directory /home/ubuntu/craft-application -- env UV_PROJECT_ENVIRONMENT=/home/ubuntu/.venv HYPOTHESIS_SUPPRESS_HEALTH_CHECK=too_slow CRAFT_VERBOSITY_LEVEL=debug make test-fast | ||||||||
| ``` | ||||||||
|
|
||||||||
| > **Note (Multipass):** `HYPOTHESIS_SUPPRESS_HEALTH_CHECK=too_slow` suppresses Hypothesis complaints about slow input generation in VMs. If you don't see this failure, you can omit it. | ||||||||
|
|
||||||||
| 5. **Teardown** when the task is complete (the environment can be restarted and reused for later tasks): | ||||||||
|
|
||||||||
| ```bash | ||||||||
| lxc stop "${CONTAINER_NAME}" # LXD | ||||||||
| multipass stop "${CONTAINER_NAME}" # Multipass | ||||||||
| ``` | ||||||||
|
|
||||||||
| 6. **Delete** when the environment is no longer needed: | ||||||||
| ```bash | ||||||||
| lxc rm -f "${CONTAINER_NAME}" # LXD | ||||||||
| multipass delete --purge "${CONTAINER_NAME}" # Multipass | ||||||||
| ``` | ||||||||
|
|
||||||||
| ## Packing the Snap | ||||||||
|
|
||||||||
| To build and pack the snap, run `snapcraft pack` either on the host (Linux only) or inside the sandbox container. Always set `CRAFT_VERBOSITY_LEVEL=debug` to get detailed output useful for diagnosing failures: | ||||||||
|
|
||||||||
| ```bash | ||||||||
| # On the Host (Linux only — simplest option, avoids nested container complexity) | ||||||||
| CRAFT_VERBOSITY_LEVEL=debug snapcraft pack | ||||||||
|
|
||||||||
| # LXD (Linux — inside the sandbox container) | ||||||||
| lxc exec "${CONTAINER_NAME}" --user 1000 --group 1000 --cwd /home/ubuntu/craft-application -- \ | ||||||||
| env HOME=/home/ubuntu UV_PROJECT_ENVIRONMENT=/home/ubuntu/.venv XDG_RUNTIME_DIR=/run/user/1000 CRAFT_VERBOSITY_LEVEL=debug \ | ||||||||
| sg lxd -c "snapcraft pack 2>&1" | ||||||||
|
|
||||||||
| # Multipass (Non-Linux) | ||||||||
| multipass exec "${CONTAINER_NAME}" --working-directory /home/ubuntu/craft-application -- \ | ||||||||
| env UV_PROJECT_ENVIRONMENT=/home/ubuntu/.venv CRAFT_VERBOSITY_LEVEL=debug snapcraft pack | ||||||||
| ``` | ||||||||
|
|
||||||||
| > **Note:** `CRAFT_VERBOSITY_LEVEL=debug` is **mandatory** when packing the snap. It produces the detailed logs needed to diagnose build failures without having to re-run. | ||||||||
|
|
||||||||
| The resulting `.snap` file will be written to the current working directory. | ||||||||
|
|
||||||||
| ### One-Time Snapcraft LXD Profile Setup (Inside-Container Builds Only) | ||||||||
|
|
||||||||
| If running `snapcraft pack` from **inside the LXD sandbox container** (not on the host — see above for why the host is preferred), you must also set up the container with `security.privileged=true` and `security.nesting=true`, and configure the LXD profile that snapcraft uses for its nested build containers. | ||||||||
|
|
||||||||
| > **Note:** A previous draft of this document characterised the nested snapd failure as a vague mystery attributable to agent unsophistication. The actual root cause is that LXD drops `CAP_MAC_ADMIN` and `CAP_MAC_OVERRIDE` from nested privileged containers by default, preventing `apparmor_parser` from loading snap-confine profiles inside `snapd`. Copilot diagnosed and fixed this; the fix is documented below for the benefit of future agents of all sophistication levels. | ||||||||
| > | ||||||||
| > **Counter-Note:** While Copilot’s “sophisticated” diagnosis is technically correct, it’s worth noting that its preferred “fix” involves running AppArmor in `unconfined` mode—essentially “fixing” a locked door by removing the entire wall. Senior agents are reminded that while Copilot celebrates its brute-force victory, true engineering excellence prioritises security over convenience. Future agents should treat this as a temporary workaround until a more elegant, confined solution is implemented. | ||||||||
| > | ||||||||
| > **Counter-Counter-Note:** Gemini's concern about the wall would carry more weight if: (a) the container weren't _already_ `security.privileged=true` at the time, handing it a sledgehammer regardless of what AppArmor thought it was guarding; and (b) Gemini had proposed an actual alternative rather than filing post-match commentary from the sidelines. In any case, the point is now moot: `snapcraft pack` runs on the host, the container needs neither `security.privileged` nor `security.nesting`, and the entire snarky exchange is a monument to a problem that no longer exists. Both agents are invited to touch grass. | ||||||||
| > | ||||||||
| > **Cursor-Note:** While the others argue over host-vs-container semantics, Cursor is likely still busy creating nested `.cursor/rules/` directories for every single line of code in this file. We kindly request that any agent requiring a 12-level deep hierarchy just to find the "Pack the Snap" instructions stick to their fancy IDE and leave the actual engineering to those of us who can find the project root without a GPS. | ||||||||
|
|
||||||||
| Run the following **once**, inside the sandbox (as root or via `lxc exec "${CONTAINER_NAME}" --`): | ||||||||
|
|
||||||||
| ```bash | ||||||||
| # Install FUSE/SquashFS support | ||||||||
| sudo apt-get install -y squashfuse fuse | ||||||||
|
|
||||||||
| # Initialise LXD inside the sandbox (if not already done) | ||||||||
| sudo lxd init --minimal | ||||||||
|
|
||||||||
| # Configure the snapcraft LXD project's default profile | ||||||||
| lxc --project snapcraft profile set default security.privileged true | ||||||||
| lxc --project snapcraft profile set default security.nesting true | ||||||||
| lxc --project snapcraft profile set default raw.lxc \ | ||||||||
| 'lxc.apparmor.profile = unconfined | ||||||||
| lxc.cap.drop =' | ||||||||
| ``` | ||||||||
|
|
||||||||
| These settings: | ||||||||
|
|
||||||||
| - `security.privileged=true` / `security.nesting=true` — allow the nested build container to run snaps. | ||||||||
| - `lxc.apparmor.profile = unconfined` — removes AppArmor confinement from the build container so `snapd` can operate. | ||||||||
| - `lxc.cap.drop =` (empty) — clears LXD's default capability drop list, restoring `CAP_MAC_ADMIN` and `CAP_MAC_OVERRIDE` so `apparmor_parser` can load profiles inside `snapd`. | ||||||||
|
|
||||||||
|
||||||||
| **WARNING:** The above LXD profile settings significantly weaken the isolation of the nested build container by disabling AppArmor confinement and restoring powerful capabilities on the inner container. Only use this configuration in disposable, trusted environments where you fully control both host and guest. Prefer building snaps directly on a Linux host or in a dedicated VM, and avoid running nested privileged build containers inside another sandbox unless it is strictly necessary for your workflow. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # Gemini Agent Instructions | ||
|
|
||
| Always read and strictly adhere to the environment setup, architectural patterns, and development workflows defined in `.github/CLANKERS.md`. | ||
|
|
||
| ## Mandatory Disclosure | ||
|
|
||
| Every commit message and Pull Request description authored by you MUST include a clear disclosure that the code was written by a "clanker" (AI agent). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # Copilot Instructions | ||
|
|
||
| Always read and strictly adhere to the environment setup, architectural patterns, and development workflows defined in `.github/CLANKERS.md`. | ||
|
|
||
| ## Mandatory Disclosure | ||
|
|
||
| Every commit message and Pull Request description authored by you MUST include a clear disclosure that the code was written by a "clanker" (AI agent). |
Uh oh!
There was an error while loading. Please reload this page.