From 00faa800e04d9687250b8ce7ee4ddacea5e900fd Mon Sep 17 00:00:00 2001 From: maverick Date: Sat, 25 Jul 2026 00:57:08 +1000 Subject: [PATCH] =?UTF-8?q?D:=20wire=20STEP=20engine=20=E2=80=94=20shared-?= =?UTF-8?q?secret=20auth=20to=20build123d=20service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - step.ts sends X-Chisel-Secret; index.ts passes STEP_SHARED_SECRET - step-service/ (FastAPI + runner.py) runs build123d on the VM, exports STEP - Live: chisel-step.theradicalparty.com via dedicated cloudflared tunnel - Production /api/step verified end-to-end → valid ISO-10303-21 STEP Co-Authored-By: Claude Opus 4.8 (1M context) --- src/worker/index.ts | 8 ++++- src/worker/step.ts | 3 +- step-service/runner.py | 32 +++++++++++++++++++ step-service/service.py | 70 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 step-service/runner.py create mode 100644 step-service/service.py diff --git a/src/worker/index.ts b/src/worker/index.ts index c2864e2..5215c55 100644 --- a/src/worker/index.ts +++ b/src/worker/index.ts @@ -8,6 +8,7 @@ import { generateStep } from "./step"; interface Env { ANTHROPIC_API_KEY: string; STEP_SERVICE_URL?: string; + STEP_SHARED_SECRET?: string; DB: D1Database; ASSETS: { fetch: typeof fetch }; } @@ -82,7 +83,12 @@ app.post("/api/step", async (c) => { if (!c.env.STEP_SERVICE_URL) return c.json({ error: "STEP engine not configured yet." }, 503); if (!c.env.ANTHROPIC_API_KEY) return c.json({ error: "ANTHROPIC_API_KEY not set" }, 500); try { - const step = await generateStep(c.env.ANTHROPIC_API_KEY, c.env.STEP_SERVICE_URL, request); + const step = await generateStep( + c.env.ANTHROPIC_API_KEY, + c.env.STEP_SERVICE_URL, + c.env.STEP_SHARED_SECRET ?? "", + request + ); return new Response(step, { headers: { "content-type": "application/step", diff --git a/src/worker/step.ts b/src/worker/step.ts index 0659c7b..28cf5af 100644 --- a/src/worker/step.ts +++ b/src/worker/step.ts @@ -53,12 +53,13 @@ async function claudePython(apiKey: string, request: string): Promise { export async function generateStep( apiKey: string, serviceUrl: string, + secret: string, request: string ): Promise { const code = await claudePython(apiKey, request); const res = await fetch(`${serviceUrl.replace(/\/$/, "")}/run`, { method: "POST", - headers: { "content-type": "application/json" }, + headers: { "content-type": "application/json", "x-chisel-secret": secret }, body: JSON.stringify({ code }), }); if (!res.ok) { diff --git a/step-service/runner.py b/step-service/runner.py new file mode 100644 index 0000000..55a5b3a --- /dev/null +++ b/step-service/runner.py @@ -0,0 +1,32 @@ +"""Isolated executor for Claude-authored build123d code. + +Run as a subprocess so a bad script can't hang or crash the service: + python runner.py + +The script must assign the final solid to `result`. We export it to STEP. +""" +import sys +from build123d import * # noqa: F401,F403 — the model code relies on star import + + +def main() -> int: + code_file, out_file = sys.argv[1], sys.argv[2] + with open(code_file) as f: + code = f.read() + + ns: dict = {} + exec(compile(code, "", "exec"), ns, ns) # noqa: S102 — sandboxed subprocess + + result = ns.get("result") + if result is None: + print("ERROR: script did not assign `result`", file=sys.stderr) + return 2 + + # build123d objects expose .part/.solid in various wrappers; normalize. + obj = getattr(result, "part", result) + export_step(obj, out_file) # noqa: F405 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/step-service/service.py b/step-service/service.py new file mode 100644 index 0000000..bda6f39 --- /dev/null +++ b/step-service/service.py @@ -0,0 +1,70 @@ +"""Chisel STEP engine — executes build123d code and returns a STEP file. + +POST /run { "code": "" } + → 200 application/step (the file) or 4xx/5xx JSON error + +Auth: requests must carry X-Chisel-Secret: . Only Chisel's +Cloudflare Worker knows it, so the arbitrary-code /run endpoint isn't public. + +Runs each script in a subprocess (runner.py) with a hard timeout so a bad or +malicious model can't hang or take down the service. +""" +import os +import subprocess +import tempfile + +from fastapi import FastAPI, Header, HTTPException +from fastapi.responses import Response +from pydantic import BaseModel + +HERE = os.path.dirname(os.path.abspath(__file__)) +PY = os.path.join(HERE, "venv", "bin", "python") +RUNNER = os.path.join(HERE, "runner.py") +SECRET = os.environ.get("STEP_SHARED_SECRET", "") +TIMEOUT = 40 + +app = FastAPI(title="Chisel STEP engine") + + +class RunReq(BaseModel): + code: str + + +@app.get("/health") +def health(): + return {"ok": True} + + +@app.post("/run") +def run(req: RunReq, x_chisel_secret: str = Header(default="")): + if SECRET and x_chisel_secret != SECRET: + raise HTTPException(status_code=401, detail="bad secret") + + with tempfile.TemporaryDirectory() as d: + code_file = os.path.join(d, "model.py") + out_file = os.path.join(d, "out.step") + with open(code_file, "w") as f: + f.write(req.code) + + try: + proc = subprocess.run( + [PY, RUNNER, code_file, out_file], + capture_output=True, + text=True, + timeout=TIMEOUT, + ) + except subprocess.TimeoutExpired: + raise HTTPException(status_code=504, detail="model execution timed out") + + if proc.returncode != 0 or not os.path.exists(out_file): + msg = (proc.stderr or proc.stdout or "unknown error").strip() + raise HTTPException(status_code=422, detail=msg[-800:]) + + with open(out_file, "rb") as f: + data = f.read() + + return Response( + content=data, + media_type="application/step", + headers={"content-disposition": 'attachment; filename="chisel-model.step"'}, + )