From 4f50953d354e67de6f067f764cf1101f25b7f3c3 Mon Sep 17 00:00:00 2001 From: Raghu Betina Date: Sat, 3 Oct 2026 15:30:00 -0500 Subject: [PATCH] Check the Drawing Board production contract First Draft is retiring its staging service, so Drawing Board now points Codespaces at production and passes the .env token to the CLI as FIRSTDRAFT_API_TOKEN (firstdraft/drawing-board#61). Expect that contract: a production .env.example and smoke .env, no staging URL pin, the token replacing any inherited value, and a stale staging URL refused with an error that names production. The default candidate moves to that pull request's head so this check runs it. --- .github/workflows/drawing-board.yml | 4 +- drawing-board/README.md | 14 +++-- drawing-board/script/check | 9 ++-- drawing-board/script/check-agent-setup.mjs | 2 +- .../script/check-firstdraft-wrapper.mjs | 52 ++++++++----------- drawing-board/script/devcontainer-smoke | 4 +- 6 files changed, 36 insertions(+), 49 deletions(-) diff --git a/.github/workflows/drawing-board.yml b/.github/workflows/drawing-board.yml index 5bc69f1..5508f8c 100644 --- a/.github/workflows/drawing-board.yml +++ b/.github/workflows/drawing-board.yml @@ -11,7 +11,7 @@ on: candidate: description: Exact firstdraft/drawing-board commit to qualify required: true - default: 058fb28aff15782b9ff764628f7ff77651a7e77e + default: 5c7c84c1c6fe5568829c1a50b030b57be7eb25e1 permissions: contents: read @@ -27,7 +27,7 @@ jobs: - name: Select the exact template candidate id: candidate env: - CANDIDATE: ${{ inputs.candidate || '058fb28aff15782b9ff764628f7ff77651a7e77e' }} + CANDIDATE: ${{ inputs.candidate || '5c7c84c1c6fe5568829c1a50b030b57be7eb25e1' }} run: | [[ "$CANDIDATE" =~ ^[0-9a-f]{40}$ ]] echo "sha=$CANDIDATE" >> "$GITHUB_OUTPUT" diff --git a/drawing-board/README.md b/drawing-board/README.md index c9c01aa..bff4f08 100644 --- a/drawing-board/README.md +++ b/drawing-board/README.md @@ -14,7 +14,7 @@ A repository created from this template must provide one ready-to-use workspace - the Dev Container installs the latest public Claude Code and Codex releases and the exact reviewed First Draft CLI; - every Skill declared by one exact source revision is linked into both agents; -- `.env` supplies the shared staging origin and token without entering Git; +- `.env` supplies the production origin and token without entering Git; - bare `firstdraft` on the Codespace PATH resolves to `bin/firstdraft`, and AGENTS.md routes Skill-issued commands through that wrapper; and - the same container carries the current generated Foundation's Ruby and Node toolchain plus healthy PostgreSQL; @@ -352,13 +352,11 @@ Never commit a First Draft API token, GitHub token, agent credential, or generat Board candidate for common credential shapes and verifies that `.env` remains ignored. The shared ignored `.env` is the credential path for both agents; do not add agent-specific token configuration. -The template wrapper intentionally selects staging. Its existing `.env` format keeps the staging token under -`FIRSTDRAFT_API_TOKEN`; the wrapper maps it to the CLI's `FIRSTDRAFT_STAGING_API_TOKEN`, removes the production token -and legacy plugin settings from the child environment, and overrides any inherited staging token. This applies to -the version probe as well as the requested command. A blank `.env` token never falls back to shell credentials. -The standalone CLI defaults to production and selects staging with `--staging`; Drawing Board's wrapper continues -to select staging through its required URL. Production defaults, GitHub Publication, and Service deployment -are owned by [firstdraft/firstdraft](https://github.com/firstdraft/firstdraft); Skill and plugin delivery are owned by +The template wrapper requires the production origin, `https://firstdraft.com`, which matches the pinned CLI's +default. It passes the `.env` token to the CLI as `FIRSTDRAFT_API_TOKEN`, replacing any inherited value, and removes +legacy plugin settings from the child environment. This applies to the version probe as well as the requested +command. A blank `.env` token never falls back to shell credentials. GitHub Publication and Service deployment are +owned by [firstdraft/firstdraft](https://github.com/firstdraft/firstdraft); Skill and plugin delivery are owned by [firstdraft/skills](https://github.com/firstdraft/skills). ## Documentation diff --git a/drawing-board/script/check b/drawing-board/script/check index e63cdd5..5b8d087 100755 --- a/drawing-board/script/check +++ b/drawing-board/script/check @@ -95,8 +95,8 @@ if [[ "$actual_bin_entries" != "$expected_bin_entries" ]]; then exit 1 fi -if [[ "$(cat .env.example)" != $'FIRSTDRAFT_API_URL=https://staging.firstdraft.com\nFIRSTDRAFT_API_TOKEN=' ]]; then - echo ".env.example must select staging and leave its token blank." >&2 +if [[ "$(cat .env.example)" != $'FIRSTDRAFT_API_URL=https://firstdraft.com\nFIRSTDRAFT_API_TOKEN=' ]]; then + echo ".env.example must select production and leave its token blank." >&2 exit 1 fi if ! git check-ignore --quiet .env || git ls-files --error-unmatch .env >/dev/null 2>&1; then @@ -170,9 +170,8 @@ fi "$checks/check-depth-one" -if [[ "$FIRSTDRAFT_CLI_DEFAULT_API_URL" != "https://firstdraft.com" || \ - "$FIRSTDRAFT_STAGING_API_URL" != "https://staging.firstdraft.com" ]]; then - echo "The pinned releases no longer match the documented staging-wrapper contract." >&2 +if [[ "$FIRSTDRAFT_CLI_DEFAULT_API_URL" != "https://firstdraft.com" ]]; then + echo "The pinned releases no longer match the documented production-wrapper contract." >&2 exit 1 fi diff --git a/drawing-board/script/check-agent-setup.mjs b/drawing-board/script/check-agent-setup.mjs index ea23843..6876bbf 100644 --- a/drawing-board/script/check-agent-setup.mjs +++ b/drawing-board/script/check-agent-setup.mjs @@ -148,7 +148,7 @@ writeFileSync(path.join(prefix, "bin/firstdraft"), "#!/usr/bin/env node\\nconsol [path.join(environment.CODEX_HOME, "sessions/session.jsonl"), '{"fixture":"existing Codex conversation"}\n'], [path.join(environment.CLAUDE_CONFIG_DIR, "skills/user-skill/SKILL.md"), "Claude user Skill\n"], [path.join(home, ".agents/skills/user-skill/SKILL.md"), "Codex user Skill\n"], - [path.join(workspace, ".env"), `FIRSTDRAFT_API_URL=https://staging.firstdraft.com\n${"FIRSTDRAFT_API_TOKEN"}=fixture-placeholder\n`], + [path.join(workspace, ".env"), `FIRSTDRAFT_API_URL=https://firstdraft.com\n${"FIRSTDRAFT_API_TOKEN"}=fixture-placeholder\n`], ]); for (const [file, content] of preserved) write(file, content); const verifyPreserved = () => { diff --git a/drawing-board/script/check-firstdraft-wrapper.mjs b/drawing-board/script/check-firstdraft-wrapper.mjs index 6be7383..9380b81 100644 --- a/drawing-board/script/check-firstdraft-wrapper.mjs +++ b/drawing-board/script/check-firstdraft-wrapper.mjs @@ -32,9 +32,8 @@ const arguments_ = process.argv.slice(2); const probe = { apiUrl: process.env.FIRSTDRAFT_API_URL, arguments_, - stagingTokenIsExpected: process.env.FIRSTDRAFT_STAGING_API_TOKEN === "test-token", - stagingTokenPresent: Boolean(process.env.FIRSTDRAFT_STAGING_API_TOKEN), - productionTokenPresent: Object.prototype.hasOwnProperty.call(process.env, "FIRSTDRAFT_API_TOKEN"), + tokenIsExpected: process.env.FIRSTDRAFT_API_TOKEN === "test-token", + tokenPresent: Boolean(process.env.FIRSTDRAFT_API_TOKEN), legacyUrlPresent: Object.prototype.hasOwnProperty.call(process.env, "FIRSTDRAFT_BASE_URL"), pluginOptionsPresent: [ "CLAUDE_PLUGIN_OPTION_API_TOKEN", @@ -58,7 +57,7 @@ fs.writeFileSync(process.env.FIRSTDRAFT_TEST_OUTPUT, JSON.stringify(probe)); const writeEnvironment = ({ apiToken = "", - apiUrl = "https://staging.firstdraft.com", + apiUrl = "https://firstdraft.com", extra = "", mode = 0o600, } = {}) => { @@ -72,7 +71,6 @@ fs.writeFileSync(process.env.FIRSTDRAFT_TEST_OUTPUT, JSON.stringify(probe)); const testEnvironment = { ...process.env, FIRSTDRAFT_API_TOKEN: "ambient-production-token", - FIRSTDRAFT_STAGING_API_TOKEN: "ambient-staging-token", FIRSTDRAFT_API_URL: "https://wrong.example.com", FIRSTDRAFT_BASE_URL: "https://legacy.example.com", CLAUDE_PLUGIN_OPTION_API_TOKEN: "uppercase-token", @@ -91,7 +89,7 @@ fs.writeFileSync(process.env.FIRSTDRAFT_TEST_OUTPUT, JSON.stringify(probe)); const symlinkTarget = path.join(testRepository, "environment-target"); fs.writeFileSync( symlinkTarget, - `FIRSTDRAFT_API_URL=https://staging.firstdraft.com\n${"FIRSTDRAFT_API_TOKEN"}=\n`, + `FIRSTDRAFT_API_URL=https://firstdraft.com\n${"FIRSTDRAFT_API_TOKEN"}=\n`, ); fs.chmodSync(symlinkTarget, 0o600); fs.symlinkSync(symlinkTarget, path.join(testRepository, ".env")); @@ -105,7 +103,7 @@ fs.writeFileSync(process.env.FIRSTDRAFT_TEST_OUTPUT, JSON.stringify(probe)); writeEnvironment(); assert.deepEqual(readConfiguration(testRepository), { apiToken: "", - apiUrl: "https://staging.firstdraft.com", + apiUrl: "https://firstdraft.com", }); assert.equal(requiresApiToken(["plan", "push"]), true); assert.equal(requiresApiToken(["plan", "push", "--help"]), false); @@ -113,10 +111,6 @@ fs.writeFileSync(process.env.FIRSTDRAFT_TEST_OUTPUT, JSON.stringify(probe)); assert.equal(requiresApiToken(["generate", "uuid"]), false); assert.equal(requiresApiToken(["future", "network-command"]), true); assert.equal(requiresApiToken(["--version"]), false); - assert.equal(requiresApiToken(["--staging", "--version"]), false); - assert.equal(requiresApiToken(["--staging", "plan", "init", "--name", "Test"]), false); - assert.equal(requiresApiToken(["--staging", "generate", "uuid"]), false); - assert.equal(requiresApiToken(["--staging", "plan", "push"]), true); await assert.rejects( run({ arguments_: ["plan", "push"], @@ -139,27 +133,25 @@ fs.writeFileSync(process.env.FIRSTDRAFT_TEST_OUTPUT, JSON.stringify(probe)); }); assert.deepEqual(result, {signal: null, status: 0}); assert.deepEqual(JSON.parse(fs.readFileSync(probeOutput, "utf8")), { - apiUrl: "https://staging.firstdraft.com", + apiUrl: "https://firstdraft.com", arguments_: ["plan", "push"], legacyUrlPresent: false, pluginOptionsPresent: false, - productionTokenPresent: false, - stagingTokenIsExpected: true, - stagingTokenPresent: true, + tokenIsExpected: true, + tokenPresent: true, }); assert.deepEqual(JSON.parse(fs.readFileSync(versionProbeOutput, "utf8")), { - apiUrl: "https://staging.firstdraft.com", + apiUrl: "https://firstdraft.com", arguments_: ["--version"], legacyUrlPresent: false, pluginOptionsPresent: false, - productionTokenPresent: false, - stagingTokenIsExpected: true, - stagingTokenPresent: true, + tokenIsExpected: true, + tokenPresent: true, }); writeEnvironment(); const localResult = await run({ - arguments_: ["--staging", "plan", "init", "--name", "Test"], + arguments_: ["plan", "init", "--name", "Test"], downstreamCli: fakeCli, environment: testEnvironment, root: testRepository, @@ -167,22 +159,20 @@ fs.writeFileSync(process.env.FIRSTDRAFT_TEST_OUTPUT, JSON.stringify(probe)); }); assert.deepEqual(localResult, {signal: null, status: 0}); assert.deepEqual(JSON.parse(fs.readFileSync(probeOutput, "utf8")), { - apiUrl: "https://staging.firstdraft.com", - arguments_: ["--staging", "plan", "init", "--name", "Test"], + apiUrl: "https://firstdraft.com", + arguments_: ["plan", "init", "--name", "Test"], legacyUrlPresent: false, pluginOptionsPresent: false, - productionTokenPresent: false, - stagingTokenIsExpected: false, - stagingTokenPresent: false, + tokenIsExpected: false, + tokenPresent: false, }); assert.deepEqual(JSON.parse(fs.readFileSync(versionProbeOutput, "utf8")), { - apiUrl: "https://staging.firstdraft.com", + apiUrl: "https://firstdraft.com", arguments_: ["--version"], legacyUrlPresent: false, pluginOptionsPresent: false, - productionTokenPresent: false, - stagingTokenIsExpected: false, - stagingTokenPresent: false, + tokenIsExpected: false, + tokenPresent: false, }); const injectionMarker = path.join(temporaryRoot, "injected"); @@ -196,7 +186,7 @@ fs.writeFileSync(process.env.FIRSTDRAFT_TEST_OUTPUT, JSON.stringify(probe)); writeEnvironment({apiToken: "test-token", mode: 0o644}); assert.throws(() => readConfiguration(testRepository), /mode 0600/); - writeEnvironment({apiToken: "test-token", apiUrl: "https://firstdraft.com"}); + writeEnvironment({apiToken: "test-token", apiUrl: "https://staging.firstdraft.com"}); await assert.rejects( run({ arguments_: ["plan", "compile"], @@ -205,7 +195,7 @@ fs.writeFileSync(process.env.FIRSTDRAFT_TEST_OUTPUT, JSON.stringify(probe)); root: testRepository, stdio: "ignore", }), - /FIRSTDRAFT_API_URL in .env must be https:\/\/staging\.firstdraft\.com/, + /FIRSTDRAFT_API_URL in .env must be https:\/\/firstdraft\.com\./, ); writeEnvironment({apiToken: "test-token"}); diff --git a/drawing-board/script/devcontainer-smoke b/drawing-board/script/devcontainer-smoke index 5a48f57..44ea828 100755 --- a/drawing-board/script/devcontainer-smoke +++ b/drawing-board/script/devcontainer-smoke @@ -66,12 +66,12 @@ test -f .env test -O .env test "$(stat -c '%a' .env)" = "600" -FIRSTDRAFT_STAGING_API_URL="$FIRSTDRAFT_STAGING_API_URL" node -e ' +FIRSTDRAFT_CLI_DEFAULT_API_URL="$FIRSTDRAFT_CLI_DEFAULT_API_URL" node -e ' const {parseEnv} = require("node:util"); const parsed = parseEnv(require("node:fs").readFileSync(".env", "utf8")); const valid = Object.keys(parsed).sort().join(",") === "FIRSTDRAFT_API_TOKEN,FIRSTDRAFT_API_URL" && - parsed.FIRSTDRAFT_API_URL === process.env.FIRSTDRAFT_STAGING_API_URL && + parsed.FIRSTDRAFT_API_URL === process.env.FIRSTDRAFT_CLI_DEFAULT_API_URL && parsed.FIRSTDRAFT_API_TOKEN === ""; process.exit(valid ? 0 : 1); '