Skip to content
Open
3 changes: 3 additions & 0 deletions .cursor/rules/clankers.md
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).
7 changes: 7 additions & 0 deletions .github/AGENTS.md
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).
288 changes: 288 additions & 0 deletions .github/CLANKERS.md
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.
Comment thread
bepri marked this conversation as resolved.

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`.

Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The instructions here explicitly disable AppArmor confinement (lxc.apparmor.profile = unconfined) and clear the capability drop list for nested build containers. That materially weakens sandbox isolation; the doc should add a clear WARNING about the security implications and steer readers toward safer alternatives (e.g., prefer host builds on Linux / avoid inside-container builds unless strictly necessary).

Suggested change
**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.

Copilot uses AI. Check for mistakes.
If a stale base instance exists from a previous failed attempt, delete it so it is recreated with the new profile:

```bash
lxc --project snapcraft list
lxc --project snapcraft delete --force <instance-name>
```

### Troubleshooting Snap Build Failures

If `snapcraft pack` fails with `Failed to enable snapd service` or `snapd.socket: Control process exited`:

1. **Inspect the nested container journal** — don't guess:

```bash
# Find the instance name first
lxc --project snapcraft list
lxc --project snapcraft exec local:<instance-name> -- journalctl -xeu snapd.service --no-pager
```

2. **AppArmor `Access denied. You need policy admin privileges`** — this means the one-time LXD profile setup above was not applied. Apply it, delete the stale base instance, and retry.

3. **FUSE/SquashFS not installed** — run `sudo apt-get install -y squashfuse fuse` inside the sandbox.

4. **LXD not initialised** — run `sudo lxd init --minimal` inside the sandbox.

5. **Nesting not enabled on the sandbox container** — verify and fix from the host:
```bash
lxc config set "${CONTAINER_NAME}" security.nesting true
lxc restart "${CONTAINER_NAME}"
```

## Documentation Guidelines

- Follow the **Diátaxis** framework:
- `tutorials/`: Learning-oriented.
- `how-to-guides/`: Problem-oriented.
- `explanation/`: Understanding-oriented.
- `reference/`: Information-oriented.
- Place app-agnostic documentation in `docs/common/` so it can be integrated into downstream craft tool docs.
- Update `docs/reference/changelog.rst` for all user-facing changes.
7 changes: 7 additions & 0 deletions .github/GEMINI.md
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).
7 changes: 7 additions & 0 deletions .github/copilot-instructions.md
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).
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Craft Application

[![Notice for AI Agents](https://img.shields.io/badge/AI-Instructions-blue)](.github/CLANKERS.md)

> [!NOTE] > **Notice for AI Agents:** Before contributing to this repository, you MUST read and follow the specialized instructions in [.github/CLANKERS.md](.github/CLANKERS.md). All AI-generated contributions must disclose that they were authored by a "clanker".

[![Documentation Status][rtd-badge]][rtd-latest]
[![Codecov Status][codecov-badge]][codecov-status]
[![Ruff status][ruff-badge]][ruff-site]
Expand Down
Loading