feat(server): expose the Core pipeline through a synchronous HTTP API#67
Closed
sigilmakes wants to merge 1 commit into
Closed
feat(server): expose the Core pipeline through a synchronous HTTP API#67sigilmakes wants to merge 1 commit into
sigilmakes wants to merge 1 commit into
Conversation
sigilmakes
changed the base branch from
stack/server-transports
to
runtime/51-model-lifecycle
July 16, 2026 18:12
sigilmakes
force-pushed
the
server/44-http-api
branch
from
July 16, 2026 18:12
625dca8 to
6720448
Compare
sigilmakes
force-pushed
the
server/44-http-api
branch
from
July 16, 2026 18:37
6720448 to
701659a
Compare
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
force-pushed
the
runtime/51-model-lifecycle
branch
from
July 17, 2026 11:30
f75f257 to
f2fe8eb
Compare
sigilmakes
force-pushed
the
server/44-http-api
branch
from
July 17, 2026 11:30
701659a to
4734c58
Compare
Collaborator
Author
Temporarily closing this stacked PRThis 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 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. |
This was referenced Jul 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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, andCoreResultused 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:
It binds to loopback by default. FastAPI and Uvicorn are available through the optional
httpextra, 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:
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:
.., absolute-path substitution, or symlinks;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
CoreRuntimefor its process lifetime:/healthdoes not trigger initialization;Stable responses and errors
Successful responses use strict
CoreResultJSON, 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:
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:
Within this PR, start with
server/request.py, thenserver/http.py, then the CLI/docs and endpoint tests.Verification
uv run pytest— 687 passeduv run ruff check src tests— passeduv run ruff format --check src tests— passeduv run pre-commit run --all-files— passedCloses #44.
Written by an agent on behalf of Willow.