D: wire STEP engine — shared-secret auth to build123d service
- 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) <noreply@anthropic.com>
This commit is contained in:
parent
8db62d411f
commit
00faa800e0
4 changed files with 111 additions and 2 deletions
|
|
@ -8,6 +8,7 @@ import { generateStep } from "./step";
|
||||||
interface Env {
|
interface Env {
|
||||||
ANTHROPIC_API_KEY: string;
|
ANTHROPIC_API_KEY: string;
|
||||||
STEP_SERVICE_URL?: string;
|
STEP_SERVICE_URL?: string;
|
||||||
|
STEP_SHARED_SECRET?: string;
|
||||||
DB: D1Database;
|
DB: D1Database;
|
||||||
ASSETS: { fetch: typeof fetch };
|
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.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);
|
if (!c.env.ANTHROPIC_API_KEY) return c.json({ error: "ANTHROPIC_API_KEY not set" }, 500);
|
||||||
try {
|
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, {
|
return new Response(step, {
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "application/step",
|
"content-type": "application/step",
|
||||||
|
|
|
||||||
|
|
@ -53,12 +53,13 @@ async function claudePython(apiKey: string, request: string): Promise<string> {
|
||||||
export async function generateStep(
|
export async function generateStep(
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
serviceUrl: string,
|
serviceUrl: string,
|
||||||
|
secret: string,
|
||||||
request: string
|
request: string
|
||||||
): Promise<ArrayBuffer> {
|
): Promise<ArrayBuffer> {
|
||||||
const code = await claudePython(apiKey, request);
|
const code = await claudePython(apiKey, request);
|
||||||
const res = await fetch(`${serviceUrl.replace(/\/$/, "")}/run`, {
|
const res = await fetch(`${serviceUrl.replace(/\/$/, "")}/run`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "content-type": "application/json" },
|
headers: { "content-type": "application/json", "x-chisel-secret": secret },
|
||||||
body: JSON.stringify({ code }),
|
body: JSON.stringify({ code }),
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|
|
||||||
32
step-service/runner.py
Normal file
32
step-service/runner.py
Normal file
|
|
@ -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 <code_file> <out_step_file>
|
||||||
|
|
||||||
|
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, "<model>", "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())
|
||||||
70
step-service/service.py
Normal file
70
step-service/service.py
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
"""Chisel STEP engine — executes build123d code and returns a STEP file.
|
||||||
|
|
||||||
|
POST /run { "code": "<build123d python assigning `result`>" }
|
||||||
|
→ 200 application/step (the file) or 4xx/5xx JSON error
|
||||||
|
|
||||||
|
Auth: requests must carry X-Chisel-Secret: <STEP_SHARED_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"'},
|
||||||
|
)
|
||||||
Loading…
Add table
Reference in a new issue