Skip to content

feat(server): expose the Core pipeline through a synchronous HTTP API#67

Closed
sigilmakes wants to merge 1 commit into
runtime/51-model-lifecyclefrom
server/44-http-api
Closed

feat(server): expose the Core pipeline through a synchronous HTTP API#67
sigilmakes wants to merge 1 commit into
runtime/51-model-lifecyclefrom
server/44-http-api

Conversation

@sigilmakes

@sigilmakes sigilmakes commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR exposes the Goldilocks Core recommendation pipeline through a small synchronous HTTP API for local services and tools that cannot embed the Python package directly.

It is a transport over the same CoreJobRequest, CoreRuntime, Pipeline, and CoreResult used by the Python API and CLI. It does not introduce a second scientific execution path or an application backend.

What clients can do

The server provides:

  • POST /recommend — run through Select and return analysis, advice, concrete selections, provenance, and warnings;
  • POST /generate — additionally return generated target-code files;
  • POST /bundle — additionally publish a confined output bundle;
  • GET /health — report process health without loading models or running a Core job.

The CLI entry point is:

goldilocks-core serve

It binds to loopback by default. FastAPI and Uvicorn are available through the optional http extra, so importing the base Core package does not require HTTP dependencies.

Request boundary

HTTP JSON is converted through one strict parser into the existing Core contracts. The parser:

  • rejects unknown and missing fields rather than silently coercing them;
  • enforces the same target, task, smearing, vdW, numerical, and record invariants as Python callers;
  • accepts inline structure content or a server-side structure path;
  • supports the existing intent, hints, pseudopotential metadata, and job modes;
  • remains shared with the MCP transport in the next stacked PR.

This is deserialization into Core data—not a string-based backend/plugin registry.

Filesystem confinement

Operators configure roots for readable structure files and writable bundles. Requests cannot:

  • escape configured roots with .., absolute-path substitution, or symlinks;
  • redirect Bundle output outside the configured output root;
  • overwrite an existing destination;
  • expose internal resolved paths in an error response.

Inline structures avoid server-side path access entirely. Bundle publication retains #68's complete-or-absent, atomic no-replace behavior.

Runtime and concurrency

The server owns one CoreRuntime for its process lifetime:

  • QRF resources load lazily and are reused across requests;
  • /health does not trigger initialization;
  • concurrent request threads share the same runtime safely;
  • shutdown closes the runtime deterministically;
  • application construction and lifespan behavior remain testable without starting a network listener.

Stable responses and errors

Successful responses use strict CoreResult JSON, including nested provenance, the corrected QRF contract, stage records, and aggregated scientific warnings.

Client/input failures and internal failures have stable JSON shapes. Responses redact model paths, registry paths, filesystem roots, and internal exception details where exposing them would cross the transport boundary.

Operational boundary

This server is intentionally synchronous and stateless. It is suitable as a local integration surface, not a complete multi-user Goldilocks application.

It does not add:

  • authentication or authorization;
  • users, sessions, workspaces, or databases;
  • queues, background workers, pods, or calculation execution;
  • remote file management;
  • scheduler or AiiDA workflows;
  • a production deployment topology.

Those concerns belong to the application and Runner layers.

Stack and review order

This PR is based on runtime PR #66, which is based on cleanup PR #68. Review in this order:

  1. Simplify the staged Core pipeline #68 — Core contracts, provenance, QE generation, and bundle hardening;
  2. feat(runtime): reuse Core model resources safely across jobs #66 — reusable model/runtime lifecycle;
  3. this PR — strict HTTP parsing, confinement, endpoints, lifespan, and errors;
  4. feat(server): expose the Core pipeline as stdio MCP tools #69 — MCP tools reusing this transport boundary.

Within this PR, start with server/request.py, then server/http.py, then the CLI/docs and endpoint tests.

Verification

  • uv run pytest687 passed
  • uv run ruff check src tests — passed
  • uv run ruff format --check src tests — passed
  • uv run pre-commit run --all-files — passed
  • endpoint, strict-JSON, inline/path structure, confinement, redaction, optional-import, runtime-reuse, concurrency, health, and shutdown paths are covered

Closes #44.


Written by an agent on behalf of Willow.

@sigilmakes
sigilmakes changed the base branch from stack/server-transports to runtime/51-model-lifecycle July 16, 2026 18:12
@sigilmakes
sigilmakes force-pushed the server/44-http-api branch from 625dca8 to 6720448 Compare July 16, 2026 18:12
@sigilmakes sigilmakes added not ready: needs human Issue or PR that has not had human review yet server HTTP / MCP server frontends (transports over Core) enhancement New feature or request labels Jul 16, 2026
@sigilmakes
sigilmakes force-pushed the server/44-http-api branch from 6720448 to 701659a Compare July 16, 2026 18:37
@sigilmakes sigilmakes added core Core package pipeline, contracts, and recommendations architecture Architecture and API boundary work dependencies Pull requests that update a dependency file cli Command-line interface work and removed dependencies Pull requests that update a dependency file core Core package pipeline, contracts, and recommendations architecture Architecture and API boundary work cli Command-line interface work labels Jul 16, 2026
Add an optional [http] extra FastAPI+uvicorn server with /health, /recommend,
/generate, and /bundle endpoints. One app-owned CoreRuntime is created at
startup, reused across requests, and closed on shutdown (including startup
failure); the serving path owns and closes its runtime rather than passing a
caller-provided one. Backend composition mirrors the CLI (default QRF,
--heuristic-kpoints, --model) as process config and is never serialized into
request JSON or built per request.

Add shared from_dict constructors on CalculationIntent, CalculationHints,
CoreJobRequest, and PseudoMetadata as the inverse of to_dict, used by the
server's request/structure deserialization and intended for reuse by the MCP
transport. They reject non-dicts, unknown keys, and malformed value types
(wrong-typed k_grid, non-bool is_sssp, non-object pseudo_info, non-list
pseudo_metadata, non-string output_dir, malformed mode) with ValueError, so
malformed JSON maps to a deterministic 422 rather than TypeError/500 or silent
coercion. PseudoMetadata.z_valence must also be finite, so non-finite numeric
metadata is rejected at deserialization. The server validates supported
code/task at the boundary and resolves structure input (inline CIF/POSCAR or
an allowlisted server-side path) and bundle output_dir under configured roots.

The structure sub-schema is strict: unknown structure keys are rejected,
exactly one of inline content or server path is required (not both), and
format is only accepted with inline content. Request bodies are parsed as
strict JSON: non-standard constants (NaN/Infinity/-Infinity) and overflow to
a non-finite float are rejected, duplicate object keys at every nesting level
are rejected, malformed UTF-8 is rejected, and unpaired surrogate code points
(JSON escapes such as a lone \uD800) are rejected anywhere in the parsed value,
all as a deterministic 422 rather than silent last-wins parsing, a non-finite
value reaching the contract deserializers, or a lone surrogate reaching os.open
as a UnicodeEncodeError stage error.

Confinement canonicalizes the configured roots at app construction and
resolves server-side paths and output_dir through a descriptor walk with
open(O_NOFOLLOW) per component, so symlink components are rejected (ELOOP)
with no TOCTOU window between the check and the read. Intermediate components
are opened with O_DIRECTORY | O_NOFOLLOW | O_NONBLOCK, so the kernel refuses
any non-directory (FIFO, device, socket, regular file, or symlink) with ENOTDIR
before its driver open path runs; the resulting ENOTDIR is classified as a
symlink or non-directory component via a non-following stat, and the final
component is opened non-blocking and required via fstat to be a regular file,
so a FIFO, device, or socket planted under a root cannot block a worker in
os.open and is rejected with 422 instead. Malformed path strings (embedded NUL
and Unicode control/format/surrogate characters — general categories Cc/Cf/Cs,
including C1 controls such as U+0085 and lone surrogates) are rejected before
any OS call as 422 rather than reaching os.open and surfacing as a 400
stage_error or UnicodeEncodeError. Genuine client path conditions
(ENOENT/ELOOP/ENOTDIR/ENXIO) map to deterministic 404/422 with the
client-relative path; server-side filesystem failures (EACCES/EMFILE/EIO and
similar) map to a redacted 500 without host paths. Every opened descriptor is
closed even if fstat or read raises, closing the post-open cleanup hole.

This confinement holds against a client that can only supply request JSON,
but it is not a complete race-free publication primitive against an actor that
can mutate the operator-controlled root between validation and use. The
bundle stage's atomic renameat2(RENAME_NOREPLACE) publication refuses any final
destination that appears between the descriptor check and publication, so a
client cannot publish outside the bundle root; however, a separately-privileged
actor that reorganizes the operator-controlled root between the descriptor walk
and the atomic rename can still interpose a parent component. That is outside
the stated no-multi-tenant/operator-root threat model (the operator is trusted
not to race the server), and the security docs qualify this rather than
claiming absolute race-free confinement.

The body never carries mode (the endpoint selects it); unknown top-level fields
and output_dir on non-bundle endpoints are rejected. Bundle responses return the
server-relative output_dir, never the absolute host path. Request execution
(parse + runtime.run) is offloaded from the ASGI event loop to a thread pool so
concurrent blocking requests overlap rather than serialize. Errors map to a
deterministic schema (422/404/405/400/500): only request-boundary not-found
maps to 404; internal model/registry/filesystem failures are a redacted 500; an
existing bundle destination is a stage_error reported with the public relative
path, and only the bundle publication boundary surfaces FileExistsError this
way — a FileExistsError from any other stage (/recommend, /generate, or a
non-publication bundle path) is re-raised as a redacted 500 rather than a
misleading stage_error. CoreResult JSON preserves provenance and warnings.

Add `goldilocks-core serve` with loopback default and a clear missing-extra
error. `import goldilocks_core` stays free of HTTP dependencies. FastAPI,
uvicorn, and httpx are dev test deps so CI runs the server tests instead of
skipping. Regression tests cover symlink read/write confinement, FIFO/special-
file/intermediate rejection with bounded completion (including Unix socket
intermediates refused by O_DIRECTORY before any driver open), descriptor
cleanup on post-open fstat failure, strict JSON (NaN/Infinity/overflow/
duplicate keys/malformed UTF-8/unpaired surrogates with a valid-pair
counterexample), strict structure sub-schema, NUL/Unicode control/format/
surrogate path rejection (including C1 U+0085 and lone surrogates), O_DIRECTORY
flag capture for intermediate opens, ENOTDIR symlink/non-directory
disambiguation, redacted 500 mapping for EACCES/EMFILE/EIO, route-specific
FileExistsError mapping (redacted 500 for /recommend and /generate, stage_error
for /bundle), concurrent request timing, startup/serve runtime closure,
malformed nested values, internal model-path redaction, existing-output
redaction, wrong-endpoint fields, and non-finite z_valence.

Closes #44
@sigilmakes
sigilmakes force-pushed the runtime/51-model-lifecycle branch from f75f257 to f2fe8eb Compare July 17, 2026 11:30
@sigilmakes
sigilmakes force-pushed the server/44-http-api branch from 701659a to 4734c58 Compare July 17, 2026 11:30
@sigilmakes sigilmakes changed the title feat(server): add synchronous HTTP API feat(server): expose the Core pipeline through a synchronous HTTP API Jul 17, 2026
@sigilmakes

Copy link
Copy Markdown
Collaborator Author

Temporarily closing this stacked PR

This HTTP work depends on runtime PR #66, which in turn depends on #68. PR #68 is currently blocked on #50 and must be retargeted to main after #50 merges.

To avoid presenting an unstable stack for review, this PR is being closed without deleting its branch. It should be reopened after #68 and #66 have been restored in order, then rebased onto the settled runtime branch.

The implementation issue remains open so the work is still tracked.


Written by an agent on behalf of Willow.

@sigilmakes sigilmakes added blocked Blocked pending another change or external decision dependencies Pull requests that update a dependency file labels Jul 17, 2026
@sigilmakes sigilmakes closed this Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blocked Blocked pending another change or external decision dependencies Pull requests that update a dependency file enhancement New feature or request not ready: needs human Issue or PR that has not had human review yet server HTTP / MCP server frontends (transports over Core)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant