feat: gateway service with Verified Completions M0 (/v1/verify, #83) - #105
Merged
Merged
Conversation
jsam
force-pushed
the
feat/workspace-split
branch
from
August 20, 2026 09:39
0d856ec to
992f901
Compare
jsam
force-pushed
the
feat/gateway-service
branch
from
August 20, 2026 09:39
48a7681 to
f974764
Compare
jsam
force-pushed
the
feat/gateway-service
branch
from
August 20, 2026 12:19
f974764 to
8065b9a
Compare
The gateway is the stack's model gateway and the second deployable next to the engine. Decided 2026-08-19: separate binary and container so the stateful engine never shares a process with the component that talks to the internet - a gateway crash or hung model-provider call cannot touch the engine, and only the gateway holds ANTHROPIC_API_KEY. - gateway binary: axum service with /health, /ready (engine reachability), and honest 501s on /v1/verify and /v1/chat/completions until #83/#84 land. Env-based configuration (GATEWAY_HOST/PORT, INPUTLAYER_URL, INPUTLAYER_API_KEY, ANTHROPIC_API_KEY). - Dockerfile: shared Rust builder, two named runtime targets (engine, gateway); engine stage last so the default build target is unchanged. - docker-compose: gateway ships as its own service in both compose files; Caddy routes /v1/verify and /v1/chat/completions to it. - docker.yml: component matrix builds and publishes both images to GHCR (inputlayer, inputlayer-gateway) and mirrors both to Docker Hub when DOCKERHUB_USERNAME/DOCKERHUB_TOKEN secrets are set.
…obe timeout
Adversarial review of this PR found two blockers plus hardening:
1. The merge job's artifact download pattern digests-inputlayer-* also
matched digests-inputlayer-gateway-*, feeding gateway digests into
the engine manifest merge - MANIFEST_UNKNOWN, engine tags silently
frozen while the workflow looks half-green. Artifact names now key
on the Dockerfile target (engine/gateway), which are not prefixes of
each other, and cache scopes are per-component.
2. The dep-cache dummy build could ship the dummy fn main() {} gateway
binary: COPY preserves context mtimes that predate the dummy build,
so cargo's mtime freshness skips the rebuild (verified empirically
by the reviewer). Sources are now touched before the real build.
3. The readiness probe's reqwest client had no timeout - a wedged
engine turned every /ready probe into a hung socket. 2s timeout.
4. GATEWAY_PORT now parses as u16 with a clear error naming the
variable; the gateway healthcheck respects a GATEWAY_PORT override.
CI's first full run of the two-target build failed: cargo build --bin
inputlayer-gateway at the workspace root resolves bins in the root
package only ('no bin target named inputlayer-gateway in default-run
packages'). Both builder invocations now select packages explicitly;
the dep-cache stage's || true had silently hidden the same failure,
which also means gateway deps were never being cached. Both exact
commands verified locally in release mode.
Decision: publishing to ghcr.io is enough; the conditional Docker Hub mirror (and its secrets requirement) is removed. Both images (inputlayer, inputlayer-gateway) publish to GHCR on push to main.
jsam
force-pushed
the
feat/gateway-service
branch
from
August 20, 2026 13:34
ec040f0 to
b424a35
Compare
This was referenced Aug 20, 2026
…ologies (#83) The gateway now implements the M0 verify pipeline: extraction via the Anthropic Messages API with structured outputs (prompt and schema come from the selected ontology pack - never open-domain), quote validation (non-verbatim, malformed, or empty quotes drop the row), manifest-driven mapping to IQL with typed template slots (quoted slots escape strings and reject control characters; bare slots accept integers only), ephemeral per-request knowledge graphs with race-free unique names, and findings reported from the pack's watch views with symmetric dedup and quoted spans. Failures fail open as "unverified"; a partial insert bails rather than reporting a false "verified". Every ontology published in the registry index is loaded at startup and pinned by version and digest. Every request must select one via the x-il-ontology header (or il_ontology body field); no selection, an unknown name, or a version mismatch is rejected before any model call. There is no allowlist and no default-ontology configuration. Restructure: registry resolution and the engine WS client move from src/bin/il/ into a shared workspace crate, inputlayer-ontology-client, used by both the il CLI and the gateway (published to crates.io ahead of the root crate). The gateway is lib+bin so the env-gated live integration test exercises the exact code the binary runs. Env prefixes consolidate on INPUTLAYER_ (IL_REGISTRY -> INPUTLAYER_REGISTRY, IL_REGISTRY_TOKEN -> INPUTLAYER_REGISTRY_TOKEN; internal IL_DEBUG and IL_TRACE_* tracked in #112). Hardening from three adversarial reviews, each fix unit-tested where applicable: IQL injection through bare slots and control characters; quote-gate bypass via malformed msg types; false "verified" on partial inserts and on concurrent-request KG races; impossible dates minting ordinals that fabricate ordering conflicts; empty-string env vars from compose treated as configured; Dockerfile missing the ontology-client COPY (both image targets were unbuildable); Makefile CI targets widened to --workspace (the gateway crate was previously invisible to CI); cargo publish repaired with a version-pinned, publishable dependency. Registry pack defect found during review filed as ontology-registry#1. Verified live: full-index startup load with digest pinning, mandatory selection (400 without it), 503 without a model key, fail-open unverified on model errors, and the Geneva-conversation conflict found end-to-end against a real engine through the real registry entry. docker build --target gateway succeeds from scratch; 3288 workspace tests, snapshot suite, il delegation tests, fmt/clippy/doc all clean.
…earing bounds
Live full-stack E2E with a real model exposed two defects the canned
extraction could not: the pack schema omitted the claims 'origin' field
its manifest mapped (structured outputs made it unemittable, so every
claim was silently skipped and the response was a false 'verified'), and
constraint bounds arrive with units ('2000 EUR') that the strict bare-slot
integer parse rejected.
The gateway now bails to 'unverified' whenever any extraction row fails
to map - schema/manifest drift must be loud, never a silent 'verified'
over partially mapped facts. The vestigial 'notes' response field is
gone. Bare template slots coerce strings via the same leading-integer
rule as numeric_mirror ('2000 EUR' -> 2000); output remains a parsed
i64, so the injection guarantees hold. The schema fixes shipped as
consistency-core 1.0.2 (with the violation_src 7-column arity fix and an
ASCII-only pass).
Verified end to end with claude-haiku-4-5: contradictory departure dates
-> conflicts_found with both verbatim spans; clean conversation ->
verified; assistant output exceeding a user max_value constraint ->
limit_exceeded violation with claim and constraint spans; prompt
injection attempting forged claims -> no fabricated findings.
Contributor
Author
|
Full-stack E2E completed with a real model (claude-haiku-4-5), local engine, and the public registry:
The live run exposed two pack/gateway defects the canned extraction could not, both fixed in 2ad552f + ontology-registry consistency-core 1.0.2: the schema omitted the mapped 'origin' field (all claims silently skipped -> false 'verified'; the gateway now bails to 'unverified' on any mapping drift), and unit-bearing bounds ('2000 EUR') are now coerced by the same leading-integer rule as numeric_mirror. Model spend for the verification: roughly $0.05-0.10 (estimate from token counts, not billed figures). |
Zero-to-catching-a-contradiction in ten minutes: compose stack (engine + gateway) with the model key, il CLI installation and registry provisioning, then live /v1/verify walkthroughs - the functional date conflict, the origin-gated constraint violation, and what a hostile conversation gets. Every command and every response in the guide is the literal output of the live E2E runs against consistency-core@1.0.2.
Rust 1.98's clippy (drain_collect, deny-by-warnings in CI) flags the old form; mem::take is the same semantics without the extra allocation. 91 persist tests green.
The second and last new Rust 1.98 clippy lint in the workspace; the whole workspace is now clean under 1.98 with -D warnings, verified locally on the same toolchain CI runs.
The IL_ prefix is gone everywhere: INPUTLAYER_DEBUG, INPUTLAYER_DEBUG_SESSION, INPUTLAYER_TRACE, INPUTLAYER_TRACE_JSON, INPUTLAYER_TRACE_LEVEL, INPUTLAYER_TRACE_FILE, INPUTLAYER_SERVER_LOG replace their IL_ forms across the engine, Makefile, snapshot runner, docs, and the generated docs bundles; the migrations guide's CI example secrets follow suit. Pre-1.0: no aliases, old names simply stop working. Workspace tests (3289), snapshot suite, fmt, and clippy 1.98 all green.
The install step now prints the verified from-source command (the inputlayer crate has never been published to crates.io, so the previously printed 'cargo install inputlayer' has never worked; it is now a parenthetical about the future release). The hostile-conversation section separates the mechanically enforced guarantees (quote gate, typed slots, drift bail - unit tested) from the single live injection run, reported as a test result rather than a universal claim. The rest of the tutorial is now verified literally as printed: the compose YAML was extracted byte-for-byte from this file, brought up both containers, and the guide's verify request returned the documented conflict with both spans through the containerized stack.
An adversarial fact-check of the PR body, guide, and commit messages against the actual code found four inaccuracies; two are fixed in code so the stated guarantees hold, two in the artifacts: - The drift-bail guarantee had a hole: a failed fill() inside an [[map.*.extra]] rule was silently dropped instead of surfacing as a skip, so the promised bail to 'unverified' never fired on that path (unreachable with consistency-core 1.0.2, but the guarantee now matches the code; unit-tested). - ANTHROPIC_API_KEY was emptiness-checked but not trimmed; a whitespace-only key counted as configured. - The guide's constraint-violation response block silently omitted the ontology and digest keys the gateway always emits; now the literal response. The il list block truncated the digest without an elision marker; now the full value. 3290 workspace tests green (count from an actual run of this tree).
This was referenced Aug 21, 2026
Closed
Open
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.
What
The gateway becomes a real deployable AND implements Verified Completions M0 (#83):
POST /v1/verifyextracts a conversation into typed claims - always bound to a registry ontology, never open-domain - runs them through the ontology's rules in an ephemeral knowledge graph, and returns findings with quoted spans.The service
inputlayer-gateway), separate container (ghcr.io/inputlayer/inputlayer-gateway), separate env surface. Only the gateway holdsANTHROPIC_API_KEY; the engine never sees it.x-il-ontologyheader oril_ontologybody field), optionally assertingname@version, which must match the pin. No selection, unknown name, or version mismatch: 400 before any model call.status: "unverified"with a reason instead of failing caller traffic. Missing model key or empty registry: 503./v1/chat/completionsstays honest 501 until M1 (Verified Completions M1: OpenAI-compatible /v1/chat/completions proxy (annotate + enforce) #84).The M0 pipeline (issue #83)
additionalProperties: falserecursively). Truncated output is refused, never partially ingested.msg, empty surface). Extraction noise becomes a missed finding, never a false one.unverified(a rejected insert may be exactly the conflicting claim; reportingverifiedover a partial fact base would be a false verification).Restructure
inputlayer-ontology-client(registry resolution + engine WS client), moved out ofsrc/bin/il/and shared by theilCLI and the gateway. Publishable on crates.io;publish.ymlpublishes it before the root crate (version-pinned path dep).IL_-prefixed variable is nowINPUTLAYER_- the user-facing registry vars plus all internal knobs (INPUTLAYER_DEBUG,INPUTLAYER_DEBUG_SESSION,INPUTLAYER_TRACE,INPUTLAYER_TRACE_JSON,INPUTLAYER_TRACE_LEVEL,INPUTLAYER_TRACE_FILE,INPUTLAYER_SERVER_LOG). No aliases (pre-1.0). Closes Consolidate internal debug env vars from IL_ to INPUTLAYER_ prefix #112.--workspace- previously the gateway crate was invisible to CI (a deliberately injected syntax error passedcargo check).ontology-client/, dep-cache stage covers it, mtime touch extended. Compose: registry env passthrough (empty values treated as unset) and a named volume for the ontology cache.Adversarially reviewed
Three independent reviews (request path, pipeline correctness, build/infra) on the working tree; every confirmed finding fixed and covered by a unit test where applicable:
msgtypes, empty-surface bypass (blocker/major) -> hardened gateverifiedon partial insert failure (major) -> bail tounverifiedverified(major) -> unique KG namesontology-clientCOPY, both image targets unbuildable (blocker) -> fixed, verified with a realdocker build--workspaceMakefile targetscargo publishsilently broken by version-less path dep (major) -> publishable crate, ordered publish${VAR:-}) treated as set (blocker class) -> trimmed/filtered everywhereTested
gateway/tests/live_pipeline.rs): real engine + real digest-verified registry entry + canned extraction; asserts the Geneva conflict is found, deduped to one finding with both spans, and the non-verbatim quote dropped.unverified, body-field selection works.cargo install --git ... --bin il inputlayer) was run against this branch and the installed binary exercised against the live registry.fmt/clippy --workspace(Rust 1.98)/doc --workspace/snapshot suite/il delegation tests all clean.Known limitations (reviewed, consciously not fixed in this PR)
/v1/verifyhas no request auth or rate limit; fine behind Caddy on a private network, must get auth before any public deploy (M1 territory, Verified Completions M1: OpenAI-compatible /v1/chat/completions proxy (annotate + enforce) #84)..kg createfailure tolerance matches the substring "exist" (loose, but a masked error is still caught by the subsequent.kg use).Requires after merge (the guide's remaining steps activate here)
inputlayer-ontology-clienttheninputlayerto crates.io - theinputlayercrate has NEVER been published, so the guide'scargo install inputlayernote only becomes true at that point.cargo publish --dry-runverified for ontology-client; the root crate's dry-run fails only on the not-yet-published dep, which the workflow's publish ordering handles.Notes
INPUTLAYER_REGISTRY_TOKENremains available for private registries.Closes #83. Closes #112.