Commit Graph
5 Commits
Author SHA1 Message Date
kevinveenbirkenbach 88be326eb5 fix(svc-ai-mcp-adapter): ship sse.py to the container that imports it
The module was added while nothing selected it, so neither the Dockerfile
COPY nor the build-context staging listed it and nothing noticed. The
first provider to declare upstream_transport: sse would have crashed the
sidecar at import instead of serving.

Two places have to agree, because the context is assembled before it is
built: instance.yml puts the file next to the Dockerfile, the COPY takes
it into the image, and either one alone is inert.

The lint parses server.py, resolves every import that names a sibling
module, and requires each in both places, plus the reverse for a module
nobody imports.
2026-08-29 10:49:16 +02:00
kevinveenbirkenbachandClaude Opus 5 700d537855 refactor(mcp): pin the adapter base by tag where the mirror looks
The adapter was the only image in the tree pinned by a hand-maintained digest,
and the pin was never recomputed by anything. It drifted twice: run 31374959721
lost seven of eight jobs to mirror/docker.io/python@sha256:6771159c... not
found, and the renewal to the newest upstream digest earlier today would have
reproduced it, because builds resolve FROM against the GHCR mirror while the
mirror is copied by tag and lags upstream by design.

The base is now FROM <image>:<version>, taken from the sidecar service in
meta/services.yml - the only file iter_role_images scans, so the tag the build
asks for is by construction the tag the mirror was told to copy. The version
moves from the moving 3.13-slim to 3.13.15-slim, which upstream stops rewriting
once published, and which the existing version drift job renews like every other
image.

With the build reading services.yml, the adapter block's digest, image and
version had no consumer left. They are removed together with the schema check
for the digest form, the warn-only drift test, and the manifest_digest helper
that existed only for it. What remains in the block describes the adapter rather
than the image it is built from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 23:38:53 +02:00
kevinveenbirkenbach 5fa47cff67 feat(mcp): proxy native upstream sessions 2026-08-12 01:55:48 +02:00
kevinveenbirkenbach 09d4e496cc fix(mcp): build the adapter against the base image digest it declares
The metadata named infinito/mcp-adapter:1.0.0 with an all-zero digest,
an image that does not exist, while the build actually used
python:3.13-slim. Declaration and artifact disagreed, so the declaration
proved nothing. Both now name the real base image and pin it by digest,
which a tag cannot guarantee across a rebuild.

The adapter base moves from the provider's service version to
mcp.adapter.digest, alongside the tool specification and its checksum.
2026-08-09 23:20:58 +02:00
kevinveenbirkenbachandClaude Opus 5 683de0ab9a feat(svc-ai-mcp-adapter): add the reusable per-provider MCP adapter
One audited implementation, instantiated once per provider application rather
than deployed as a single process holding every application's credential. Each
instance joins only its provider's network, carries only that provider's
least-privileged credential and serves a distinct endpoint.

Every decision lives in the policy layer, which validates the contract at
startup rather than at first call, so an instance missing a limit, an
allowlist or the schema hash never reaches a client. The server itself only
moves bytes over three JSON-RPC methods, with no stream state to get wrong.
Its upstream header is configurable because only half the providers accept an
RFC 6750 bearer.

The contract probe runs at deploy time rather than in Playwright, because it
needs the provider's real credential and a Playwright trace would capture it.
It proves, over both transports, that an unauthenticated caller is refused,
that a wrongly authenticated one is refused, that the handshake completes,
that the served tool list is exactly the contracted one, and that one
deterministic read answers without an error. Steps one and two are what
separate a guarded endpoint from one that merely happens to be unreachable.

The isolation probe counts the sidecar's network interfaces from inside it.
Reachability cannot be used here: an unknown name does not fail to resolve in
this environment, it falls through to a wildcard resolver that answers every
name with one reachable address.

The role is inert until a provider includes it; those inclusions follow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:04:08 +02:00