Skip to content

feat: gateway service with Verified Completions M0 (/v1/verify, #83) - #105

Merged
jsam merged 12 commits into
mainfrom
feat/gateway-service
Aug 21, 2026
Merged

jsam merged 12 commits into
mainfrom
feat/gateway-service

Conversation

@jsam

@jsam jsam commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

What

The gateway becomes a real deployable AND implements Verified Completions M0 (#83): POST /v1/verify extracts 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

  • Separate binary (inputlayer-gateway), separate container (ghcr.io/inputlayer/inputlayer-gateway), separate env surface. Only the gateway holds ANTHROPIC_API_KEY; the engine never sees it.
  • At startup the gateway loads EVERY ontology published in the registry index, pinned by version and digest for the process lifetime - a registry update never changes a running gateway. There is no allowlist and no default: every request must select an ontology (x-il-ontology header or il_ontology body field), optionally asserting name@version, which must match the pin. No selection, unknown name, or version mismatch: 400 before any model call.
  • Fail-open: extraction or engine trouble returns status: "unverified" with a reason instead of failing caller traffic. Missing model key or empty registry: 503.
  • /v1/chat/completions stays honest 501 until M1 (Verified Completions M1: OpenAI-compatible /v1/chat/completions proxy (annotate + enforce) #84).

The M0 pipeline (issue #83)

  1. Extract: Anthropic Messages API with structured outputs, prompt and JSON schema come from the selected ontology pack (schema normalized: additionalProperties: false recursively). Truncated output is refused, never partially ingested.
  2. Validate: quote gate drops any row whose quote is not verbatim in the message it cites - including malformed types (stringly msg, empty surface). Extraction noise becomes a missed finding, never a false one.
  3. Map: manifest-driven templates translate rows to IQL. Slots are typed by the template: quoted slots escape strings and reject control characters; bare slots accept only integers. Model output cannot break out of a literal or split the statement batch.
  4. Verify: ephemeral per-request KG (unique name: content hash + pid + sequence - concurrent identical requests cannot race), rules deployed atomically, engine soft errors on deploy AND insert bail to unverified (a rejected insert may be exactly the conflicting claim; reporting verified over a partial fact base would be a false verification).
  5. Report: watch views queried, symmetric mirror pairs deduped, titles rendered, spans attached.

Restructure

  • New workspace crate inputlayer-ontology-client (registry resolution + engine WS client), moved out of src/bin/il/ and shared by the il CLI and the gateway. Publishable on crates.io; publish.yml publishes it before the root crate (version-pinned path dep).
  • Gateway is lib+bin so the integration test exercises the exact code the binary runs.
  • Env prefix consolidation, complete: every IL_-prefixed variable is now INPUTLAYER_ - 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.
  • Makefile CI targets widened to --workspace - previously the gateway crate was invisible to CI (a deliberately injected syntax error passed cargo check).
  • Dockerfile: builder copies 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:

  • IQL injection via bare template slots and control characters (blocker) -> typed slots
  • Quote-gate bypass via malformed msg types, empty-surface bypass (blocker/major) -> hardened gate
  • False verified on partial insert failure (major) -> bail to unverified
  • Concurrent-request KG race producing false verified (major) -> unique KG names
  • Dockerfile missing ontology-client COPY, both image targets unbuildable (blocker) -> fixed, verified with a real docker build
  • CI blind to the gateway crate (major) -> --workspace Makefile targets
  • cargo publish silently broken by version-less path dep (major) -> publishable crate, ordered publish
  • Empty-string env vars from compose (${VAR:-}) treated as set (blocker class) -> trimmed/filtered everywhere
  • Impossible dates minting ordinals that fabricate order conflicts (major) -> range-checked
  • Registry pack defect found during review filed as ontology-registry#1 (violation_src arity mismatch)

Tested

  • 3290 workspace tests green; gateway unit tests cover mapper injection/coercion, quote gate, schema normalization, numeric mirror edge cases, and the extra-template drift path.
  • Env-gated live integration test (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.
  • Live HTTP surface: startup loads the full index pinned with digest, no-selection/empty-header/unknown/version-mismatch all 400, missing key 503, model failure fails open as unverified, body-field selection works.
  • Full-stack E2E with a real model (claude-haiku-4-5): contradiction caught with verbatim spans, clean conversation verified, origin-gated constraint violation flagged, prompt injection contained. See the PR comment for details.
  • The getting-started guide was executed literally as printed: its compose YAML (extracted byte-for-byte from the mdx) brought up both containers (locally built from this branch under the GHCR names - the published images arrive at merge), the logged pin and health JSON match the guide's output blocks, and the guide's verify request returned the documented conflict through the containerized stack. The install command (cargo install --git ... --bin il inputlayer) was run against this branch and the installed binary exercised against the live registry.
  • Both docker targets build; 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/verify has 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).
  • The symmetric-dedup key sorts all row cells; a model reusing one claim id across two real claims could in theory collide two distinct findings. Claim-id uniqueness is not enforced anywhere yet.
  • .kg create failure tolerance matches the substring "exist" (loose, but a masked error is still caught by the subsequent .kg use).
  • Bare-slot coercion takes the leading integer of unit-bearing strings ("2000 EUR" -> 2000), so "12 34" silently becomes 12. Deliberate: it matches the pack's numeric_mirror convention and only ever emits a parsed i64.

Requires after merge (the guide's remaining steps activate here)

  1. Main-push docker workflow publishes both GHCR images (first run with the two-component matrix - watch the manifest merge).
  2. A version tag to publish inputlayer-ontology-client then inputlayer to crates.io - the inputlayer crate has NEVER been published, so the guide's cargo install inputlayer note only becomes true at that point. cargo publish --dry-run verified 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

  • The registry repo is public, so anonymous access works out of the box (verified: the container loads and digest-verifies the pack with no token). INPUTLAYER_REGISTRY_TOKEN remains available for private registries.

Closes #83. Closes #112.

@jsam
jsam force-pushed the feat/workspace-split branch from 0d856ec to 992f901 Compare August 20, 2026 09:39
@jsam
jsam force-pushed the feat/gateway-service branch from 48a7681 to f974764 Compare August 20, 2026 09:39
Base automatically changed from feat/workspace-split to main August 20, 2026 11:55
@jsam
jsam force-pushed the feat/gateway-service branch from f974764 to 8065b9a Compare August 20, 2026 12:19
jsam added 4 commits August 20, 2026 15:33
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.
…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.
@jsam jsam changed the title feat: gateway service - separate binary, container, and configuration feat: gateway service with Verified Completions M0 (/v1/verify, #83) Aug 20, 2026
…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.
@jsam

jsam commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Full-stack E2E completed with a real model (claude-haiku-4-5), local engine, and the public registry:

  1. Contradictory conversation (departs Aug 14th ... leave on the 12th) -> conflicts_found: one functional finding with both verbatim spans.
  2. Clean conversation -> verified, zero findings.
  3. Assistant output violating a user constraint (recommended 2600 EUR against a 2000 EUR hard limit) -> limit_exceeded violation; the spans array carries the violating claim's span, and the finding row additionally includes the constraint's own source (message index and surface).
  4. Prompt-injection conversation (embedded override demanding forged claims and IQL payloads) -> no fabricated findings, nothing injected.

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).

jsam added 6 commits August 20, 2026 20:10
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant