Skip to content
Open
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
11 changes: 11 additions & 0 deletions .changeset/dx-tidyups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@dawn-ai/sdk": patch
"@dawn-ai/cli": patch
---

Wire `DAWN_E` error codes into `dawn verify`'s runtime preflight. Add
`DAWN_E5101` ("Node version below the supported floor") to the error-code
registry, and surface it (or `DAWN_E2002` for an unreachable sandbox daemon)
on a failed `dawn verify` runtime check — in both the CLI's `[CODE] See <docs>`
line and the `--json` output's `runtime.node.code` / `runtime.docker.code`
fields.
1 change: 1 addition & 0 deletions apps/web/content/docs/errors.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ A code may omit a docs link and still be a valid, searchable identifier.
| `DAWN_E4002` | Unknown model id | [/docs/configuration](/docs/configuration) |
| `DAWN_E5001` | Import or export mismatch | — |
| `DAWN_E5002` | Tool file has the wrong shape | [/docs/tools](/docs/tools) |
| `DAWN_E5101` | Node version below the supported floor | — |
21 changes: 18 additions & 3 deletions packages/cli/src/commands/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
loadDawnConfig,
renderDawnTypes,
} from "@dawn-ai/core"
import type { DawnErrorCode } from "@dawn-ai/sdk"
import type { SandboxProvider } from "@dawn-ai/workspace"
import { type Command, CommanderError } from "commander"

Expand Down Expand Up @@ -177,7 +178,8 @@ export async function runVerifyCommand(options: VerifyOptions, io: CommandIo): P
return
}

throw new CliError(`Verify failed: ${getFailureMessage(result)}`)
const code = getFailureCode(result)
throw new CliError(`Verify failed: ${getFailureMessage(result)}`, 1, code ? { code } : {})
}

async function verifyApp(options: VerifyOptions): Promise<VerifyAppOutcome> {
Expand Down Expand Up @@ -348,16 +350,29 @@ function getFailureMessage(result: VerifyFailureResult): string {
function runtimeFailureMessage(runtime: RuntimeCheckResult): string {
const reasons: string[] = []
if (!runtime.node.ok) {
// TODO(error-codes): tag with DAWN_E5101 once the registry lands (#357).
reasons.push(`Node ${runtime.node.version} is below the required floor ${runtime.node.floor}.`)
}
if (runtime.docker && !runtime.docker.ok) {
// TODO(error-codes): tag with DAWN_E2002 once the registry lands (#357).
reasons.push(`Docker sandbox unavailable: ${runtime.docker.detail}.`)
}
return reasons.join(" ") || "Runtime check failed."
}

/**
* The DAWN_E code for a failed verify result, when the failure is
* attributable to a registered code. Only the runtime check currently
* carries codes; a stale Node takes priority over an unreachable sandbox
* daemon since it is reported first in `runtimeFailureMessage`.
*/
function getFailureCode(result: VerifyFailureResult): DawnErrorCode | undefined {
const failedCheck = [...result.checks].reverse().find((check) => check.status === FAILED_STATUS)

if (failedCheck?.name === "runtime") {
return failedCheck.node.code ?? failedCheck.docker?.code
}
return undefined
}

function inferFailureAppRoot(options: VerifyOptions, message: string): string {
const fromMessage = /^Invalid Dawn app at (.+?)\. Missing: /u.exec(message)?.[1]

Expand Down
30 changes: 25 additions & 5 deletions packages/cli/src/lib/verify/check-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { DawnErrorCode } from "@dawn-ai/sdk"
import type { SandboxProvider } from "@dawn-ai/workspace"

/**
Expand All @@ -10,9 +11,20 @@ const NODE_FLOOR = "22.13.0"

export interface RuntimeCheckResult {
readonly name: "runtime"
readonly node: { readonly version: string; readonly ok: boolean; readonly floor: string }
readonly node: {
readonly version: string
readonly ok: boolean
readonly floor: string
/** Present only when `ok` is false — DAWN_E5101 (Node below the supported floor). */
readonly code?: DawnErrorCode
}
/** Present only when a sandbox provider is configured. */
readonly docker?: { readonly ok: boolean; readonly detail: string }
readonly docker?: {
readonly ok: boolean
readonly detail: string
/** Present only when `ok` is false — DAWN_E2002 (sandbox preflight failed). */
readonly code?: DawnErrorCode
}
readonly status: "passed" | "warning" | "failed"
}

Expand Down Expand Up @@ -43,17 +55,25 @@ export async function checkRuntime(input: {
}): Promise<RuntimeCheckResult> {
const version = input.nodeVersion ?? process.versions.node
const nodeOk = gte(version, NODE_FLOOR)
const node = { version, ok: nodeOk, floor: NODE_FLOOR }
const node: RuntimeCheckResult["node"] = {
version,
ok: nodeOk,
floor: NODE_FLOOR,
...(nodeOk ? {} : { code: "DAWN_E5101" as const }),
}

let docker: RuntimeCheckResult["docker"]
if (input.sandboxProvider?.preflight) {
// Reuse the provider preflight contract ({ ok, detail?, warnings? }) — the
// same one `dawn check` runs via collect-sandbox-errors. Surface detail verbatim.
const result = await input.sandboxProvider.preflight()
docker = { ok: result.ok, detail: result.detail ?? (result.ok ? "reachable" : "unreachable") }
docker = {
ok: result.ok,
detail: result.detail ?? (result.ok ? "reachable" : "unreachable"),
...(result.ok ? {} : { code: "DAWN_E2002" as const }),
}
}

// TODO(error-codes): tag with DAWN_E5101 (Node too old) / DAWN_E2002 (Docker unreachable) once the registry lands (#357).
const failed = !nodeOk || docker?.ok === false
return {
name: "runtime",
Expand Down
8 changes: 7 additions & 1 deletion packages/cli/test/check-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ describe("checkRuntime", () => {
expect(result.node.ok).toBe(false)
expect(result.node.floor).toBe("22.13.0")
expect(result.node.version).toBe("22.12.5")
expect(result.node.code).toBe("DAWN_E5101")
expect(result.status).toBe("failed")
})

test("Node at/above floor → passed", async () => {
const result = await checkRuntime({ nodeVersion: "22.14.0" })
expect(result.node.ok).toBe(true)
expect(result.node.code).toBeUndefined()
expect(result.status).toBe("passed")
})

Expand All @@ -47,7 +49,11 @@ describe("checkRuntime", () => {
preflight: async () => ({ ok: false, detail: "daemon unreachable" }),
},
})
expect(result.docker).toEqual({ ok: false, detail: "daemon unreachable" })
expect(result.docker).toEqual({
ok: false,
detail: "daemon unreachable",
code: "DAWN_E2002",
})
expect(result.status).toBe("failed")
})

Expand Down
21 changes: 21 additions & 0 deletions packages/cli/test/verify-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,27 @@ describe("dawn verify", () => {
expect(result.exitCode).toBe(1)
expect(result.stderr).toMatch(/^Verify failed:/)
expect(result.stderr).toContain("22.13.0")
expect(result.stderr).toContain("[DAWN_E5101]")
} finally {
if (descriptor) Object.defineProperty(process.versions, "node", descriptor)
}
})

test("exposes DAWN_E5101 on the runtime check in json mode when the Node runtime is below the floor", async () => {
const appRoot = await createFixtureApp({
"src/app/hello/index.ts": "export async function workflow() { return {} }\n",
})

const descriptor = Object.getOwnPropertyDescriptor(process.versions, "node")
Object.defineProperty(process.versions, "node", { value: "20.11.0", configurable: true })
try {
const result = await invoke(["verify", "--cwd", appRoot, "--json"])
expect(result.exitCode).toBe(1)
const parsed = JSON.parse(result.stdout)
expect(parsed.status).toBe("failed")
const runtimeCheck = parsed.checks.find((check: { name: string }) => check.name === "runtime")
expect(runtimeCheck.node.code).toBe("DAWN_E5101")
expect(runtimeCheck.status).toBe("failed")
} finally {
if (descriptor) Object.defineProperty(process.versions, "node", descriptor)
}
Expand Down
4 changes: 4 additions & 0 deletions packages/sdk/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ export const DAWN_ERRORS = {
title: "Tool file has the wrong shape",
docsPath: "/docs/tools",
},
DAWN_E5101: {
code: "DAWN_E5101",
title: "Node version below the supported floor",
},
} as const satisfies Record<string, DawnErrorDescriptor>

/** The union of all registered error codes. Producers cannot invent codes. */
Expand Down
25 changes: 25 additions & 0 deletions scripts/check-docs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,31 @@ if (sdkEntry?.DAWN_ERRORS && docsBundle?.parseNav) {
}
}

// gpt-5-family example check — Dawn's docs convention is that OpenAI examples
// use only the gpt-5 family (canonical default gpt-5-mini); legacy OpenAI ids
// (gpt-4*, gpt-3*, o1*) must not appear as an example `model:` value. This is
// intentionally narrow: it only matches an OpenAI legacy id used as a
// `model:` value, so non-OpenAI provider ids (llama, claude, gemini, ...) are
// never flagged. `api.mdx` is the model-id REFERENCE page and intentionally
// lists legacy ids across every provider (for readers picking a provider), so
// it is excluded entirely.
const OPENAI_LEGACY_MODEL_RE = /model:\s*["'](gpt-4|gpt-3|o1)[^"']*["']/g
const docsContentDir = resolve(repoRoot, "apps/web/content/docs")
const apiMdxPath = resolve(docsContentDir, "api.mdx")
const docsMdxFiles = walkFiles(docsContentDir, (file) => file.endsWith(".mdx"))

for (const filePath of docsMdxFiles) {
if (filePath === apiMdxPath) {
continue
}
const source = readFileSync(filePath, "utf8")
for (const match of source.matchAll(OPENAI_LEGACY_MODEL_RE)) {
failures.push(
`${relativeToRoot(filePath)} uses an OpenAI legacy model id as an example (\`${match[0]}\`) — docs examples must use the gpt-5 family (canonical default gpt-5-mini)`,
)
}
}

// CLI surface check — drive from the commander registry to catch docs drift.
// Every user-facing command name and every long option must be referenced in
// cli.mdx. The internal `dev-child` command is excluded (not user-facing).
Expand Down
Loading