File size: 6,435 Bytes
69f9a4b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import { spawn } from "node:child_process"
import { createServer } from "node:http"
import { fileURLToPath } from "node:url"
import path from "node:path"
import { FIXTURES, freePort, isolatedEnv } from "./predeploy-smoke"

const dockerEnv = () => {
  const env = isolatedEnv()
  for (const key of ["DOCKER_HOST", "DOCKER_CONTEXT", "DOCKER_TLS", "DOCKER_TLS_VERIFY", "DOCKER_CERT_PATH"]) delete env[key]
  return env
}
async function command(args: string[], allowFailure = false) {
  const child = spawn("docker", ["--host=unix:///var/run/docker.sock", ...args], { env: dockerEnv(), stdio: ["ignore", "pipe", "pipe"] })
  let output = ""
  child.stdout.on("data", data => { output += data }); child.stderr.on("data", data => { output += data })
  const code = await new Promise<number>((resolve, reject) => { child.once("error", reject); child.once("exit", code => resolve(code ?? 1)) })
  if (code && !allowFailure) {
    const safe = Object.values(FIXTURES).reduce((text, secret) => text.split(secret).join("[FIXTURE_REDACTED]"), output)
    throw new Error(`Docker ${args[0]} failed:\n${safe.slice(-6000)}`)
  }
  return { code, output }
}
export async function dockerGate() {
  try {
    if ((await command(["info"], true)).code !== 0) { console.log("SKIPPED_ENV_DOCKER: managed local daemon unavailable"); return { result: "SKIPPED_ENV_DOCKER", passed: 0, failed: 0, skipped: 3 } }
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error
    console.log("SKIPPED_ENV_DOCKER: docker executable absent"); return { result: "SKIPPED_ENV_DOCKER", passed: 0, failed: 0, skipped: 3 }
  }
  const args = ["build", "-t", "nodepad-predeploy"]
  // Optional CA bundle is only a BuildKit secret; never copied into the image.
  if (process.env.CODEX_PROXY_CERT) args.push("--secret", "id=proxy_ca,src=/etc/ssl/certs/ca-certificates.crt")
  args.push(".")
  await command(args)
  const server = createServer(async (req, res) => {
    if (req.headers.authorization !== `Bearer ${FIXTURES.OPENAI_API}`) { res.writeHead(401).end(); return }
    res.setHeader("Content-Type", "application/json")
    if (req.url === "/v1/models") res.end(JSON.stringify({ data: [{ id: "qwythos-9b" }] }))
    else if (req.url === "/v1/chat/completions") res.end(JSON.stringify({ choices: [{ message: { content: "NODEPAD_LLM_OK" } }] }))
    else res.writeHead(404).end()
  })
  const providerPort = await new Promise<number>((resolve, reject) => { server.once("error", reject); server.listen(0, "0.0.0.0", () => resolve((server.address() as { port: number }).port)) })
  const port = await freePort()
  const name = `nodepad-predeploy-${process.pid}`
  let passed = 1
  const check = (condition: unknown, message: string) => { if (!condition) throw new Error(message); passed++ }
  const env = { ...FIXTURES, NODEPAD_GITHUB_SYNC: "false", NODEPAD_MAX_RESEARCH_TASKS_PER_HEARTBEAT: "0", NODEPAD_RESEARCH_ENABLED: "false", NODEPAD_DATA_DIR: "/app/data", OPENAI_URL: `http://host.docker.internal:${providerPort}/v1`, OPENAI_MODEL: "qwythos-9b" }
  try {
    // Loopback-only publication; no host networking. Docker's configured proxies stay intact.
    await command(["run", "-d", "--name", name, "--add-host", "host.docker.internal:host-gateway", "-p", `127.0.0.1:${port}:7860`, ...Object.entries(env).flatMap(([key, value]) => ["-e", `${key}=${value}`]), "nodepad-predeploy"])
    const base = `http://127.0.0.1:${port}`
    async function request(url: string, options: RequestInit = {}) {
      return fetch(base + url, { ...options, redirect: "manual", signal: AbortSignal.timeout(15_000) })
    }
    let ready = false
    for (let n = 0; n < 100; n++) {
      try { if ((await request("/health")).ok) { ready = true; break } } catch { /* startup poll */ }
      await new Promise(resolve => setTimeout(resolve, 100))
    }
    check(ready, "Docker readiness failed")
    check((await request("/login")).ok, "Docker login page")
    check((await request("/api/v1/workspaces")).status === 401, "Docker anonymous protection")
    const uid = (await command(["exec", name, "id", "-u"])).output.trim()
    check(uid === "1001", "Docker must run as non-root nextjs UID 1001")
    const login = await request("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ accessKey: FIXTURES.NODEPAD_LOGIN_KEY }) })
    check(login.ok, "Docker writable session store")
    const cookie = (login.headers.get("set-cookie") || "").split(";")[0]
    const headers = { Authorization: `Bearer ${FIXTURES.NODEPAD_API_KEY}`, "Content-Type": "application/json" }
    check((await request("/api/v1/workspaces/docker/evidence", { method: "POST", headers, body: JSON.stringify({ claim: "Docker persistent fixture" }) })).status === 201, "Docker non-root data write")
    check((await request("/api/v1/llm/models", { headers: { Cookie: cookie } })).ok, "Docker local mock discovery")
    const completion = await request("/api/v1/llm/test", { method: "POST", headers, body: "{}" })
    check(completion.ok && (await completion.json()).content === "NODEPAD_LLM_OK", "Docker local mock inference")
    await command(["restart", name])
    for (let n = 0; n < 100; n++) { try { if ((await request("/health")).ok) break } catch {} await new Promise(resolve => setTimeout(resolve, 100)) }
    const evidence = await request("/api/v1/workspaces/docker/evidence", { headers })
    check(evidence.ok && (await evidence.json()).length === 1, "Docker data survives restart")
    const sessionStatus = await request("/api/auth/status", { headers: { Cookie: cookie } })
    check(sessionStatus.ok && (await sessionStatus.json()).authenticated === true, "Docker session survives restart")
    const logs = (await command(["logs", name])).output
    check(Object.values(FIXTURES).every(secret => !logs.includes(secret)), "Docker logs must not contain fixture credentials")
    console.log(`PASS Docker build/runtime/non-root/persistence: ${passed} assertions`)
    return { result: "PASS", passed, failed: 0, skipped: 0 }
  } finally {
    await command(["rm", "-f", name], true)
    server.closeAllConnections(); await new Promise<void>(resolve => server.close(() => resolve()))
  }
}
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) dockerGate().catch(error => {
  console.error(error instanceof Error ? error.message : "Docker validation failed"); process.exitCode = 1
})