Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .agents/skills/add-mission/references/mission-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Every mission supplies:
- An effective learning `track` and `environment`; omitted values preserve the legacy `linux` and `simulated` defaults.
- Teaching content: `story`, `objective`, one or more unique base names in `suggested_commands`, `hints`, and `explanation`.
- Simulated labs: an absolute clean `start_dir` plus declarative `setup` containing `directories`, `files`, `processes`, `environment`, and `archives` as needed.
- Docker labs: `track: docker`, `environment: docker`, and a declarative `docker` setup containing digest-pinned image references and logical `running` or `stopped` container fixtures. They do not define simulated setup or a start directory.
- Docker labs: `track: docker`, `environment: docker`, and a declarative `docker` setup containing digest-pinned image references and logical `running` or `stopped` container fixtures. Fixtures may select fixed Go-implemented behaviors: an optional startup `log`, a `health` probe (`healthy` or `unhealthy`), a stopped one-shot job (`log` + `exit_code`), or a running bounded crash loop (`log` + non-zero `exit_code` + `restart: on-failure`). They do not define simulated setup or a start directory.
- Declarative `validation.all`: one or more observable conditions.
- `rewards`: positive `xp` and non-negative `hint_penalty`.

Expand All @@ -26,7 +26,7 @@ Use the smallest combination that proves the outcome without encoding a command
- Virtual file state: `file_content_equals`, `file_content_contains`, `file_lines_equal`, `file_mode_equals`, `file_owner_equals`.
- Virtual processes: `process_stopped`, `process_running`.
- Virtual environment: `env_equals` with `NAME=value`.
- Docker state: `docker_container_running` with a declared logical container alias and `docker_container_count_equals` with a non-negative count.
- Docker state: `docker_container_running`, `docker_container_stopped`, or `docker_container_absent` with a declared logical container alias, and `docker_container_count_equals` with a non-negative count. Running and stopped both require the container to still exist.

Output-only validation is appropriate when producing the exact useful output is the lesson. Prefer filesystem, process, or environment state when the operational result should survive the final command. Combine positive and negative conditions when distractors could otherwise produce a false completion.

Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# OpsQuest agent guide

OpsQuest is a Go 1.26 CLI game with 23 in-memory Linux missions and 6 optional Docker Foundations labs. Linux player commands stay inside the teaching shell. Docker input is parsed into a deliberately small command subset that can affect only disposable, OpsQuest-labeled resources. Kubernetes remains future scope unless a task explicitly adds it.
OpsQuest is a Go 1.26 CLI game with 23 in-memory Linux missions and 11 optional Docker labs across two worlds. Linux player commands stay inside the teaching shell. Docker input is parsed into a deliberately small command subset that can affect only disposable, OpsQuest-labeled resources. Kubernetes remains future scope unless a task explicitly adds it.

## Repository map

Expand Down
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ OpsQuest validates the result, not a prescribed command. Equivalent supported so
## What is included

- 23 Linux missions across four ordered learning worlds
- 6 optional, disposable Docker Foundations missions
- 11 optional, disposable Docker missions across two worlds: Foundations and Container Triage
- An isolated in-memory filesystem, environment, process table, and archives
- Quote-aware globs and variables, pipelines, redirection, and command history
- A compact virtual `vi` and bounded virtual shell scripts
Expand Down Expand Up @@ -85,7 +85,10 @@ $ opsquest doctor
$ opsquest play 20
```

OpsQuest never pulls the image automatically. See
OpsQuest never pulls the image automatically. If an OpsQuest process is
killed before it can clean up, the next Docker mission removes its leftover lab
containers; `opsquest doctor` reports them and `opsquest doctor --cleanup`
removes them on demand. See
[Docker Foundations](docs/play/docker-foundations.md) for Docker Desktop,
Docker Engine, and OrbStack setup, plus the teaching subset and isolation
boundary.
Expand All @@ -94,7 +97,9 @@ boundary.

Player-entered Linux commands—including virtual scripts—are parsed and executed by OpsQuest. They never reach a host shell, host process, or host path. The teaching shell operates only on bounded in-memory state.

Docker input is parsed into a small set of typed teaching actions. OpsQuest constructs Docker arguments itself, uses only exact resources labeled for the current attempt, applies resource restrictions, and verifies ownership again during cleanup. It does not expose arbitrary Docker passthrough, privileged mode, host mounts, host networking, devices, or the Docker socket.
Docker input is parsed into a small set of typed teaching actions. OpsQuest constructs Docker arguments itself, uses only exact resources labeled for the current attempt, applies resource restrictions, and verifies ownership again during cleanup. Fixture processes live at most 24 hours, and leftovers from a crashed process are removed only when every ownership label matches and the recorded owner process is gone. It does not expose arbitrary Docker passthrough, privileged mode, host mounts, host networking, devices, or the Docker socket.

The optional web companion binds only to an ephemeral `127.0.0.1` port, uses a one-time pairing URL, and exposes a read-only mission projection. It cannot submit commands, mutate the profile or attempt, address Docker resources, or approve completion.

The optional web companion binds only to an ephemeral `127.0.0.1` port, uses a one-time pairing URL, and exposes a read-only mission projection. It cannot submit commands, mutate the profile or attempt, address Docker resources, or approve completion.

Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ The game describes an observable objective, gives you a disposable environment,
## Current scope

- 23 Linux missions across four ordered learning worlds
- 6 optional Docker Foundations missions
- 11 optional Docker missions across two worlds
- Outcome-based validation rather than one required command transcript
- An in-memory filesystem, process table, environment, archives, editor, and shell scripts
- Persistent XP, ranks, command practice, hints, completions, and achievements
Expand Down
4 changes: 2 additions & 2 deletions docs/play/diagrams/learning-journey.excalidraw
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
{"id":"docker-lane","type":"rectangle","x":40,"y":545,"width":1410,"height":215,"angle":0,"strokeColor":"#8b5cf6","backgroundColor":"#f5f3ff","fillStyle":"solid","strokeWidth":2,"strokeStyle":"solid","roughness":1,"opacity":100,"groupIds":[],"frameId":null,"roundness":{"type":3},"seed":214,"version":1,"versionNonce":2014,"isDeleted":false,"boundElements":null,"updated":1784505600000,"link":null,"locked":false},
{"id":"docker-label","type":"text","x":70,"y":565,"width":370,"height":20,"angle":0,"strokeColor":"#6d28d9","backgroundColor":"transparent","fillStyle":"solid","strokeWidth":1,"strokeStyle":"solid","roughness":1,"opacity":100,"groupIds":[],"frameId":null,"roundness":null,"seed":215,"version":1,"versionNonce":2015,"isDeleted":false,"boundElements":null,"updated":1784505600000,"link":null,"locked":false,"fontSize":16,"fontFamily":2,"text":"DOCKER TRACK · OPTIONAL · INDEPENDENT WORLD NUMBERING","textAlign":"left","verticalAlign":"top","containerId":null,"originalText":"DOCKER TRACK · OPTIONAL · INDEPENDENT WORLD NUMBERING","autoResize":true,"lineHeight":1.25},
{"id":"docker-world","type":"rectangle","x":85,"y":610,"width":390,"height":115,"angle":0,"strokeColor":"#7c3aed","backgroundColor":"#ffffff","fillStyle":"solid","strokeWidth":2,"strokeStyle":"solid","roughness":1,"opacity":100,"groupIds":[],"frameId":null,"roundness":{"type":3},"seed":216,"version":1,"versionNonce":2016,"isDeleted":false,"boundElements":null,"updated":1784505600000,"link":null,"locked":false},
{"id":"docker-world-text","type":"text","x":115,"y":630,"width":330,"height":75,"angle":0,"strokeColor":"#4c1d95","backgroundColor":"transparent","fillStyle":"solid","strokeWidth":1,"strokeStyle":"solid","roughness":1,"opacity":100,"groupIds":[],"frameId":null,"roundness":null,"seed":217,"version":1,"versionNonce":2017,"isDeleted":false,"boundElements":null,"updated":1784505600000,"link":null,"locked":false,"fontSize":18,"fontFamily":2,"text":"DOCKER WORLD 1\nIt Works on My Machine\nMissions 20–25 · lifecycle + diagnostics","textAlign":"left","verticalAlign":"top","containerId":null,"originalText":"DOCKER WORLD 1\nIt Works on My Machine\nMissions 20–25 · lifecycle + diagnostics","autoResize":true,"lineHeight":1.25},
{"id":"docker-world-text","type":"text","x":115,"y":630,"width":330,"height":75,"angle":0,"strokeColor":"#4c1d95","backgroundColor":"transparent","fillStyle":"solid","strokeWidth":1,"strokeStyle":"solid","roughness":1,"opacity":100,"groupIds":[],"frameId":null,"roundness":null,"seed":217,"version":1,"versionNonce":2017,"isDeleted":false,"boundElements":null,"updated":1784505600000,"link":null,"locked":false,"fontSize":18,"fontFamily":2,"text":"DOCKER WORLDS 1–2\nIt Works on My Machine → Container Triage\nMissions 20–34 · lifecycle, diagnostics, triage","textAlign":"left","verticalAlign":"top","containerId":null,"originalText":"DOCKER WORLDS 1–2\nIt Works on My Machine → Container Triage\nMissions 20–34 · lifecycle, diagnostics, triage","autoResize":true,"lineHeight":1.25},
{"id":"docker-note","type":"rectangle","x":650,"y":610,"width":700,"height":115,"angle":0,"strokeColor":"#a78bfa","backgroundColor":"#ede9fe","fillStyle":"solid","strokeWidth":2,"strokeStyle":"dashed","roughness":1,"opacity":100,"groupIds":[],"frameId":null,"roundness":{"type":3},"seed":218,"version":1,"versionNonce":2018,"isDeleted":false,"boundElements":null,"updated":1784505600000,"link":null,"locked":false},
{"id":"docker-note-text","type":"text","x":685,"y":630,"width":630,"height":75,"angle":0,"strokeColor":"#4c1d95","backgroundColor":"transparent","fillStyle":"solid","strokeWidth":1,"strokeStyle":"solid","roughness":1,"opacity":100,"groupIds":[],"frameId":null,"roundness":null,"seed":219,"version":1,"versionNonce":2019,"isDeleted":false,"boundElements":null,"updated":1784505600000,"link":null,"locked":false,"fontSize":18,"fontFamily":2,"text":"Separate readiness and progress\nPinned local fixture · no automatic pull · Linux never blocked\n6 bounded lifecycle and diagnostic missions","textAlign":"left","verticalAlign":"top","containerId":null,"originalText":"Separate readiness and progress\nPinned local fixture · no automatic pull · Linux never blocked\n6 bounded lifecycle and diagnostic missions","autoResize":true,"lineHeight":1.25},
{"id":"docker-note-text","type":"text","x":685,"y":630,"width":630,"height":75,"angle":0,"strokeColor":"#4c1d95","backgroundColor":"transparent","fillStyle":"solid","strokeWidth":1,"strokeStyle":"solid","roughness":1,"opacity":100,"groupIds":[],"frameId":null,"roundness":null,"seed":219,"version":1,"versionNonce":2019,"isDeleted":false,"boundElements":null,"updated":1784505600000,"link":null,"locked":false,"fontSize":18,"fontFamily":2,"text":"Separate readiness and progress\nPinned local fixture · no automatic pull · Linux never blocked\n11 bounded lifecycle and diagnostic missions","textAlign":"left","verticalAlign":"top","containerId":null,"originalText":"Separate readiness and progress\nPinned local fixture · no automatic pull · Linux never blocked\n11 bounded lifecycle and diagnostic missions","autoResize":true,"lineHeight":1.25},
{"id":"a-docker","type":"arrow","x":475,"y":668,"width":175,"height":0,"angle":0,"strokeColor":"#7c3aed","backgroundColor":"transparent","fillStyle":"solid","strokeWidth":3,"strokeStyle":"dashed","roughness":1,"opacity":100,"groupIds":[],"frameId":null,"roundness":{"type":2},"seed":220,"version":1,"versionNonce":2020,"isDeleted":false,"boundElements":null,"updated":1784505600000,"link":null,"locked":false,"points":[[0,0],[175,0]],"lastCommittedPoint":null,"startBinding":null,"endBinding":null,"startArrowhead":null,"endArrowhead":"arrow","elbowed":false}
],
"appState": {"gridSize": null, "viewBackgroundColor": "#f8fafc"},
Expand Down
10 changes: 5 additions & 5 deletions docs/play/diagrams/learning-journey.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 34 additions & 3 deletions docs/play/docker-foundations.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
description: Prepare and play the optional, disposable OpsQuest Docker Foundations missions.
description: Prepare and play the optional, disposable OpsQuest Docker missions.
audience: players
status: current
---
Expand Down Expand Up @@ -39,7 +39,7 @@ $ opsquest play --track docker

## Current lessons

The 6 beginner missions cover a deliberately narrow operational loop:
The track has 11 missions in two worlds. **World 1: It Works on My Machine** is 6 beginner missions covering a deliberately narrow lifecycle loop:

1. **Container Census** — list containers and start an existing stopped service.
2. **Last Broadcast** — read bounded logs from an exited one-shot job.
Expand All @@ -48,12 +48,43 @@ The 6 beginner missions cover a deliberately narrow operational loop:
5. **Recovery Pair** — restore two stopped services without replacing them.
6. **Shift Handoff** — combine `start` and `stop` while preserving supporting metrics.

The delivered teaching subset recognizes container listing plus `start`, `restart`, `stop`, `inspect`, `logs`, and `help` forms. It accepts exact logical aliases without forwarding arbitrary Docker CLI arguments or flags.
**World 2: Container Triage** adds health checks, restart policies, log tails, and removal:

1. **Janitor Duty** — remove finished job containers while services keep running.
2. **Tail End** — show only the last lines of a long job log.
3. **Running Isn't Healthy** — pull a running but unhealthy replica from service and start its standby.
4. **Crash Loop** — stop a container its restart policy keeps relaunching, then show why it fails.
5. **Postmortem Triage** — combine health, exit codes, stop, and remove, and keep the evidence the postmortem needs.

## Teaching subset

| Command | Supported forms |
| --- | --- |
| List | `docker ps` or `docker container ls`, with `-a`/`--all` and up to four `--filter`/`-f` values: `status=created\|running\|restarting\|exited` or `health=starting\|healthy\|unhealthy\|none` |
| Lifecycle | `start`, `restart`, and `stop` with one alias |
| Remove | `docker rm ALIAS` for a stopped container; `--force` is refused, so stop the container first |
| Inspect | `docker inspect ALIAS` shows logical state, exit code, health, restart count, and restart policy |
| Logs | `docker logs ALIAS` or `docker logs --tail N ALIAS` (also `-n N`, `--tail=N`, and `--tail all`) |

Every command also has a `docker container ...` form. OpsQuest parses these forms itself and accepts exact logical aliases. It never forwards other Docker CLI arguments or flags, and filters run inside OpsQuest rather than being passed to Docker.

## Isolation boundary

OpsQuest generates unique names and ownership labels, maps player-visible aliases to exact container IDs, applies resource restrictions, and removes only resources verified as belonging to the current attempt. Labs do not use privileged mode, host bind mounts, host networking, devices, or a mounted Docker socket.

Health probes and restart policies are fixed fixture behaviors chosen by the mission, never player input. A crash-loop fixture's restart policy allows at most 50 retries.

## Cleaning up after a crash

OpsQuest removes a lab's containers when the attempt ends. If the process is killed first, its containers stay behind. Their processes exit on their own within 24 hours, and the next Docker mission removes them. You can also check and clean up yourself:

```console
$ opsquest doctor # reports orphaned lab containers
$ opsquest doctor --cleanup # removes them
```

A container counts as orphaned only when it has every OpsQuest ownership label and its generated name, and either its recorded owner process on this machine has exited or it is older than 24 hours. Containers belonging to other running OpsQuest sessions on this machine are left alone for their 24-hour lifetime.

The selected Docker-compatible engine remains a powerful external dependency. OpsQuest constrains the lesson and cleanup scope; it does not present the engine itself as an untrusted-code security boundary.

See [Sandbox and safety](../technical/sandbox-and-safety.md#docker-teaching-boundary)
Expand Down
Loading
Loading