Skip to content
Draft
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
13 changes: 1 addition & 12 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,17 @@ LABEL org.opencontainers.image.source="https://github.com/firstdraft/drawing-boa
LABEL org.opencontainers.image.description="First Draft Drawing Board development environment"

USER root
RUN apt-get update && \
export DEBIAN_FRONTEND=noninteractive && \
apt-get install --yes --no-install-recommends lsof openssh-client openssh-server && \
rm -f /etc/ssh/ssh_host_* && \
rm -rf /var/lib/apt/lists/* && \
install -d -m 0755 /etc/ssh/sshd_config.d /run/sshd && \
RUN install -d -m 0755 /etc/ssh/sshd_config.d && \
printf '%s\n' \
'Port 2222' \
'AuthenticationMethods publickey' \
'PermitRootLogin no' \
'PasswordAuthentication no' \
'KbdInteractiveAuthentication no' \
'PubkeyAuthentication yes' \
'UsePAM yes' \
> /etc/ssh/sshd_config.d/99-foundation.conf

COPY .devcontainer/workspace-init /usr/local/share/firstdraft-workspace-init.sh
RUN chmod 0755 /usr/local/share/firstdraft-workspace-init.sh

USER vscode

ENTRYPOINT ["/usr/local/share/firstdraft-workspace-init.sh"]
CMD ["sleep", "infinity"]

# The Rails image installs Ruby through mise, whose activation normally happens
Expand Down
6 changes: 3 additions & 3 deletions .devcontainer/agent-versions.env
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Last reviewed together on 2026-08-28. Re-review every pin before each template release.
# Last reviewed together on 2026-08-30. Re-review every pin before each template release.
CLAUDE_CODE_VERSION=2.1.226
CODEX_VERSION=0.147.0
FIRSTDRAFT_CLI_VERSION=0.2.1
FIRSTDRAFT_CLI_VERSION=0.2.2
# CLI 0.2.x requires staging to serve First Draft API 0.3.x before template promotion.
FIRSTDRAFT_CLI_DEFAULT_API_URL=https://firstdraft.com
FIRSTDRAFT_STAGING_API_URL=https://staging.firstdraft.com
FIRSTDRAFT_SKILLS_REVISION=160d33a5a7d9f9b2282729ecfd3b2e24a1123143
FIRSTDRAFT_SKILLS_REVISION=0a765f88d1cd500168e18ce1adda03802773f35e
FIRSTDRAFT_CLAUDE_SKILL_NAME=create-full-stack-app
# Codex namespaces the canonical source checkout with its root plugin manifest.
FIRSTDRAFT_CODEX_SKILL_NAME=firstdraft:create-full-stack-app
Expand Down
2 changes: 1 addition & 1 deletion .devcontainer/compose.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
rails-app:
image: ghcr.io/firstdraft/drawing-board-workspace@sha256:7e19db9fba85a96a157dc7fc3fb7b22138d52a52d29b7c29a8bccf76e67be94f
image: ghcr.io/firstdraft/drawing-board-workspace@sha256:06602be5cc829d5142c12b06c505dbf8353a3ade6751ca4bf01a785ea2c3e6e3
volumes:
- ..:/workspaces/drawing-board:cached
command: sleep infinity
Expand Down
5 changes: 5 additions & 0 deletions .devcontainer/image/devcontainer-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
"resolved": "ghcr.io/devcontainers/features/node@sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6",
"integrity": "sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6"
},
"ghcr.io/devcontainers/features/sshd:1": {
"version": "1.1.0",
"resolved": "ghcr.io/devcontainers/features/sshd@sha256:f5251b8e4325f68f7280973c6cd65daff414449c66f240621502d4e8e74eb7ee",
"integrity": "sha256:f5251b8e4325f68f7280973c6cd65daff414449c66f240621502d4e8e74eb7ee"
},
"ghcr.io/rails/devcontainer/features/activestorage:1": {
"version": "1.1.1",
"resolved": "ghcr.io/rails/devcontainer/features/activestorage@sha256:7fa8fff898ac33076ebf65631d3c6c902dc9bad87de3e5dfa645f3e2d7a35c07",
Expand Down
1 change: 1 addition & 0 deletions .devcontainer/image/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/rails/devcontainer/features/activestorage:1": {},
"ghcr.io/devcontainers/features/node:1": { "version": "24.18.0" },
"ghcr.io/devcontainers/features/sshd:1": {},
"ghcr.io/rails/devcontainer/features/postgres-client:1": { "version": "18" }
},
"remoteUser": "vscode"
Expand Down
50 changes: 27 additions & 23 deletions .devcontainer/image/receipt.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,60 @@
"format": "firstdraft.drawing-board-development-image/1",
"source": {
"repository": "firstdraft/drawing-board",
"commit": "ac9e56438896424e49c4d4d3dd16eda0523fa301",
"tree": "617af7255cb697ef22a85e580316cc7b39fd8248",
"tag": "devcontainer-image-candidate-safe-ac9e564"
"commit": "1488d6337847a0dfd7da9b7c11de895e81160421",
"tree": "4af88c915789dda2fa6d5e366e35f672981a5030",
"tag": "devcontainer-image-candidate-safe-1488d63"
},
"inputs": {
".devcontainer/Dockerfile": "254f6d24cc242edff62b7cb927182600daf287844af252ad31b61e86016a7574",
".devcontainer/image/devcontainer.json": "45d55afb832a2ff8803da94541ef9c6a9e72b49d028fab807bb7c5f4771b0f8c",
".devcontainer/image/devcontainer-lock.json": "acf4120fe5260b1579df0285db7c86c7377aed59df8a75f3ea8c3c0e46b45aa1",
".devcontainer/workspace-init": "2a76af1205092a381c755a78b161e9971538dc07d3c0bd260b785a56c8f2dc6c",
".github/workflows/devcontainer-image.yml": "f59269fb6bbdf50eb86681ba7a09da5697ed510f2636436e54c639fa0ba9adf6",
"script/devcontainer-image-smoke": "45dfee61bb59c1106ca230741e86d77690f1abe6bf6b653d55e528e16982eda8"
".devcontainer/Dockerfile": "ff81e7bf2b56191d20d592429d29c1be30e0fabaffd9ae9f3c4f0f52e73cd480",
".devcontainer/image/devcontainer.json": "5143f0ed66fd97b986b86f8be171516d194796ca3137f0a14d7580e0bb9bbe75",
".devcontainer/image/devcontainer-lock.json": "9efc5a704e887e1c66679f574cfcb6d5eb9d0ff6b80e3fe3dffd9cbdc7db894b",
".github/workflows/devcontainer-image.yml": "d442d1a1383fb470e5d3e6fa14f2a29b7b09b5dd33a44cfa93575c7467f806a7",
"script/devcontainer-image-smoke": "272260b7bbdbe61613e43ee3e73df2eaf3941442aaa6dc7a27a848d2da1e2134"
},
"publication": {
"workflow_run": 33267389991,
"build_job": 99139776455,
"verify_job": 99141426403,
"workflow_run": 33320822128,
"build_job": 99282366552,
"verify_job": 99284135796,
"package": "ghcr.io/firstdraft/drawing-board-workspace",
"manifest": "sha256:7e19db9fba85a96a157dc7fc3fb7b22138d52a52d29b7c29a8bccf76e67be94f",
"manifest": "sha256:06602be5cc829d5142c12b06c505dbf8353a3ade6751ca4bf01a785ea2c3e6e3",
"platforms": {
"linux/amd64": "sha256:9325cc36d84884a2d79234c50805bddb4ab05ae33816c3920e6077b7ab20f470",
"linux/arm64": "sha256:58fa619ec460a34b411d0eab1eb790881e833875dc4c88b6324eb3bcfbac505a"
"linux/amd64": "sha256:1c8a08347080623ce27daf13651327142eee2ac163907430614869435db2fdae",
"linux/arm64": "sha256:0d09a17e7618d69bc2fa1c57754cbb0453363a19f061686f517efceee59bde10"
},
"visibility": "private",
"anonymous_pull": "not_yet_observed",
"visibility": "public",
"anonymous_pull": "passed",
"comparison_codespace": "not_yet_observed"
},
"verification": {
"platforms": {
"linux/amd64": {
"layers_contain_no_ssh_host_keys": true,
"locked_feature_ids_present_once_in_metadata": true,
"image_rest_contains_no_ssh_host_keys": true,
"no_command_stays_running": true,
"two_started_containers_have_distinct_ed25519_host_keys": true,
"official_sshd_feature_starts_key_only_listener": true,
"postgresql_client": "18.6",
"psql_major": 18,
"pg_dump_major": 18
},
"linux/arm64": {
"layers_contain_no_ssh_host_keys": true,
"locked_feature_ids_present_once_in_metadata": true,
"runtime": "not_observed"
}
},
"workflow_log_sha256": "2774dcd347a32b8313c65969ce28f35eef87a42edd2f248f1ae548c3db2cf447"
"workflow_log_sha256": "8c538bf40f5fa2fc6ead4b4f96afcfb779a0ec429e22c49b74d0df3fb0eb224d"
},
"policy": {
"ssh": {
"lifecycle": "official_devcontainers_sshd_feature",
"image_layer_host_keys": "accepted_for_disposable_github_tunneled_development",
"client_authentication": "public_key_only",
"root_login": "denied"
}
},
"rejected_predecessor": {
"package": "ghcr.io/firstdraft/drawing-board-devcontainer",
"manifest": "sha256:27f652c012ff5684a034612300bee43d0c12b72f0e63ee6527ad28e9a403ccf5",
"required_visibility": "private_forever",
"reason": "Build-time SSH host private keys are recoverable from OCI layers."
"reason": "Quarantined under the superseded per-container-host-key policy; it remains unapproved for consumption."
}
}
14 changes: 0 additions & 14 deletions .devcontainer/workspace-init

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/devcontainer-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@ jobs:
docker pull --platform linux/amd64 "${IMAGE%:*}@$amd64"
script/devcontainer-image-smoke "${IMAGE%:*}@$amd64"
docker pull --platform linux/arm64 "${IMAGE%:*}@$arm64"
script/devcontainer-image-smoke --layers-only "${IMAGE%:*}@$arm64"
script/devcontainer-image-smoke --metadata-only "${IMAGE%:*}@$arm64"
9 changes: 6 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,12 @@ name the Skill or translate the request into a command.
approved remote when that repository exists, or preserve it under the Drawing Board's ignored, bind-mounted `tmp/`
before starting a different Compile. Do not use `/tmp`, the container home, or another path that a Codespace
rebuild discards.
- If a Codespaces forwarded-port URL reaches Rails' **Blocked hosts** page, report the known target-owned defect and
stop. Do not edit or clear `config.hosts`, or add host-admission environment variables to the Drawing Board or the
generated application.
- The pinned CLI also supports explicit POSIX current-root adoption with `--output .`, but that is not this
template's default. Select it only when the user asks to turn this exact root into the application and has approved
moving the existing Drawing Board material under `design/`; never infer it from a generic request to Compile.
- If a Codespaces forwarded-port URL reaches Rails' **Blocked hosts** page, preserve the exact generated revision,
report the observed response, and stop. Do not edit or clear `config.hosts`, or add host-admission environment
variables to the Drawing Board or the generated application.
- Zero-flag `bin/firstdraft plan compile` is a separate mode for a user who explicitly asks for a private GitHub
repository. Never switch modes to recover from an ambiguous start. Neither mode deploys the application.
- Use `bin/firstdraft` for every First Draft command. When the installed Skill shows `firstdraft ...`, pass those
Expand Down
39 changes: 23 additions & 16 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ cross-repository sequence and its safety boundaries live in
| `script/check` | Fast source, pin, wrapper, and credential contracts |
| `script/check-depth-one` | Receipt validation in a real one-commit checkout without image-source history |
| `script/check-image-receipt.mjs` | Exact source, publication, platform, and rejected-package receipt contract |
| `script/devcontainer-image-smoke` | Non-vacuous image-layer, locked Feature-ID, no-command, host-key, and PostgreSQL checks |
| `script/devcontainer-image-smoke` | Default command, locked Feature-ID, maintained SSH lifecycle, and PostgreSQL checks |
| `script/devcontainer-smoke` | Runtime smoke executed inside the built Dev Container |
| `script/initialize-application` | Parentless nested Git initialization for direct-download output |
| `script/selenium` | On-demand Selenium start, status, and stop inside the Dev Container |
Expand Down Expand Up @@ -80,29 +80,36 @@ runs the template-root runtime smoke twice for every pull request. The generated
a separate qualification input because `./application` is absent from the template checkout. A change to an exact
pin should name the compatible upstream revision or package and preserve the same version in every checked consumer.

The current candidate consumes a private development image by immutable manifest digest. CI authenticates with its
job token so the private candidate can be reviewed, but ordinary template-derived Codespaces are not qualified to
pull it yet. The public/anonymous boundary is a later explicit and irreversible package-visibility action. To update
the image:
The current template consumes a public development image by immutable manifest digest. A credential-free manifest
request reproduced that exact multi-platform index, so ordinary template-derived Codespaces can pull it without
access to the First Draft organization. CI still authenticates with its job token, but that is not an availability
requirement. To update the image:

1. change `.devcontainer/Dockerfile` or `.devcontainer/image/devcontainer.json`;
2. let the current Dev Container CLI regenerate `.devcontainer/image/devcontainer-lock.json`, then review every
resolved Feature version and digest rather than editing the lock by hand;
3. push one `devcontainer-image-candidate-safe-<short-sha>` tag to run the candidate-only image workflow;
4. verify both private image platforms, then record that private receipt and immutable digest for review;
5. after separate visibility approval, make only the corrected package public, prove a credential-free pull by
immutable digest, and update the receipt's observation; and
6. run the contracts, the built-container smoke twice, and one fresh non-prebuilt Codespace comparison before the
public image enters the ordinary template.
4. verify both image platforms, then record the reviewed receipt and immutable digest;
5. prove a credential-free manifest read by immutable digest and update the receipt's observation; and
6. run the contracts, the built-container smoke twice, and one fresh non-prebuilt Codespace comparison before
calling the successor digest qualified for the ordinary template.

The image uses the maintained `ghcr.io/devcontainers/features/sshd:1` Feature for the SSH server lifecycle expected
by Codespaces and keeps only the key-only, non-root policy in the Dockerfile. Do not replace the Feature entrypoint
with a custom OpenSSH startup script; the ordinary local image smoke is not proof that a different entrypoint will
be started by Codespaces. Image-layer host keys supplied by the maintained Feature are accepted for this disposable,
GitHub-tunneled development environment; client authentication remains key-only and root login remains denied. This
supersedes the per-container-host-key experiment, but does not approve consumption of its quarantined package.

The candidate workflow does not move a stable or `latest` tag. The image receipt binds the source revision, source
tree, workflow run, platforms, and manifest digest consumed by the template, while naming anonymous pull and the
comparison Codespace as unobserved until they actually run. The Docker-outside-of-Docker Feature reaches the host
daemon: that host is a disposable VM in Codespaces, but it is the developer's own machine on the supported local
path. Do not run an untrusted workspace or agent with that socket mounted. The workspace starts only its exact
Compose-owned Selenium service when `script/application-smoke` or `script/selenium start` requests browser proof.
tree, workflow run, platforms, and manifest digest consumed by the template. The current receipt records anonymous
access as passed and leaves the comparison Codespace unobserved until it actually runs. The Docker-outside-of-Docker
Feature reaches the host daemon: that host is a disposable VM in Codespaces, but it is the developer's own machine
on the supported local path. Do not run an untrusted workspace or agent with that socket mounted. The workspace
starts only its exact Compose-owned Selenium service when `script/application-smoke` or `script/selenium start`
requests browser proof.
The comparison Codespace must also prove that `script/selenium` can resolve the Compose project from that runtime's
container identity; the current private-image receipt does not claim that observation or a speculative fallback.
container identity; the current public-image receipt does not claim that observation or a speculative fallback.

## Credentials and external systems

Expand Down
Loading