feat(ql3): harden copilot mcp host deployment

This commit is contained in:
whyour
2026-08-16 03:41:38 +08:00
parent 58025ede55
commit da4e7cf688
25 changed files with 734 additions and 48 deletions
@@ -80,7 +80,7 @@ FROM node:24.18.0-bookworm-slim@sha256:6f7b03f7c2c8e2e784dcf9295400527b9b1270fd3
ARG SOURCE_REVISION=uncommitted
LABEL org.opencontainers.image.title="QingLong 3.0 Cluster Admin" \
org.opencontainers.image.description="QingLong 3.0 short-lived cluster administration jobs" \
org.opencontainers.image.description="QingLong 3.0 cluster operations and bounded stdio MCP" \
org.opencontainers.image.source="https://github.com/whyour/qinglong" \
org.opencontainers.image.revision="${SOURCE_REVISION}" \
org.opencontainers.image.licenses="Apache-2.0"
@@ -2,7 +2,7 @@
"name": "@qinglong/cluster-admin-image-dependencies",
"version": "3.0.0-alpha.0",
"private": true,
"description": "Locked external dependencies for the QingLong 3.0 short-lived cluster admin image",
"description": "Locked external dependencies for QingLong 3.0 cluster operations and bounded stdio MCP",
"license": "Apache-2.0",
"engines": {
"node": ">=24.18.0 <25"
@@ -2,7 +2,7 @@
"name": "@qinglong/cluster-admin-image-dependencies",
"version": "3.0.0-alpha.0",
"private": true,
"description": "Production-only external dependency root for the QingLong 3.0 short-lived cluster admin image",
"description": "Production-only external dependency root for QingLong 3.0 cluster operations and bounded stdio MCP",
"license": "Apache-2.0",
"engines": {
"node": ">=24.18.0 <25"
+19 -9
View File
@@ -34,16 +34,18 @@ docker build \
```
The admin image has its own builder and production locks. Its five production
roots resolve to 84 external packages and it adds only `runtime-core`,
`cluster-postgres` and `cluster-admin`. Kubernetes client code and the admin
database role therefore exist only in an ephemeral Job image, never in
`cluster-control`.
roots resolve to 87 external packages and it adds only `runtime-core`, `ai`,
`cluster-postgres` and `cluster-admin`. Kubernetes client code and admin
database roles remain outside `cluster-control`; explicit operation commands
stay short-lived, while the Copilot MCP subpath loads only its remote API
client and stdio transport.
Its default entrypoint is the bounded `ql3-cluster-admin` product facade. The
facade exposes only seven operator-facing remote clients: `package`,
facade exposes eight operator-facing remote clients: `copilot`, `package`,
`package-kubernetes`, `worker-credential`, `approval`, `run`, `automation` and
`model-credential`. It delegates with the current Node executable, an exact
same-image target and `shell=false`; arguments remain opaque. Server
`model-credential`, plus the bounded `copilot-mcp` stdio process. It delegates
with the current Node executable, an exact same-image target and `shell=false`;
arguments and MCP stdio remain opaque. Server
`*-manage` processes, migration, recovery, executors, Prompt output key custody
and garbage collection remain separate explicit binaries and Kubernetes
operations. Existing Jobs continue to name their exact binary and do not
@@ -69,6 +71,14 @@ contents are deliberately forbidden. There is no home-directory, environment
or ambient Kubernetes context discovery. Existing explicit `--config` calls
remain supported when `--context` is absent.
Cluster Copilot MCP is deployed by its external MCP host, not as a Kubernetes
Deployment or Service. The reviewed digest-pinned Docker launcher, compact /
standard / dense resource ceilings, private projection examples and one-shot
readiness check live in
[`deploy/mcp/ql3-cluster-copilot`](../../mcp/ql3-cluster-copilot). This host
adapter is never composed into Edge/Standalone, Cluster Control, Cluster AI or
the shared operations Kustomization.
Validate the complete local operator context before a maintenance window:
```sh
@@ -78,7 +88,7 @@ ql3-cluster-admin context validate \
This command reuses the production client parsers to validate every selected
endpoint shape, CA, matching client certificate/private key and the bounded
Kubernetes config. It also verifies that all seven facade targets exist in the
Kubernetes config. It also verifies that all nine facade targets exist in the
same installation. It does not read a command or assertion, open a network
connection, query Kubernetes or mutate the cluster. Its JSON summary contains
only command names and reviewed transport/authentication classes; paths,
@@ -99,7 +109,7 @@ pnpm sbom:cluster-image:ql3 --image=admin
pnpm audit:image-release:ql3
```
The admin image resolves 84 external plus 3 internal components. Its production
The admin image resolves 87 external plus 4 internal components. Its production
closure legitimately contains the upstream runtime dependency
`@types/js-yaml` from `@kubernetes/client-node`; the exact production lock, not
the package-name prefix, is authoritative. TypeScript, root devDependencies and
@@ -1,6 +1,9 @@
{
"schemaVersion": 1,
"commands": {
"copilot": {
"configFile": "/secure/qinglong3/copilot-client.json"
},
"package": {
"configFile": "/secure/qinglong3/plugin-package-client.json"
},
+67
View File
@@ -0,0 +1,67 @@
# Cluster Copilot MCP stdio container
This is an explicit host-side deployment for the bounded Cluster Copilot MCP.
It is not a Kubernetes Deployment or Service: stdio must be owned by the MCP
host that launches the process. A resident Pod without that parent session
would be unreachable while still retaining a credential and image attack
surface.
The launcher uses the existing independently released Cluster Admin image and
its default `ql3-cluster-admin` entrypoint. It selects the reviewed
`copilot-mcp` subcommand, never overrides the entrypoint and never mounts a
Kubernetes token, database credential, Docker socket or writable directory.
## Prepare the private projection
Create an absolute canonical directory owned by UID/GID `10001:10001`, mode
`0700`. Copy `mcp-config.example.json` to `mcp.json` and
`client-config.example.json` to `client.json`; install the reviewed API CA as
`ca.pem` and the separately issued `ql3c_` Project API credential as
`credential`. All four files must be regular, non-symlink, UID 10001-owned,
canonical files with mode `0600`. Never put the credential value in the MCP
host config, argv, environment or image.
Replace the endpoint, DNS server name and CA. The client uses TLS 1.3, does not
use a client certificate, proxy, redirect or ambient CA, and rereads
`credential` for every Tool call.
## Select resources and egress
Create a dedicated Docker network whose host firewall permits only DNS and the
reviewed Cluster API destination. The launcher rejects `bridge`, `default`,
`host`, `none` and an implicit network, but Docker network naming alone is not
an egress allowlist.
| Resource class | Memory | CPU | PIDs | Maximum configured concurrency |
| --- | ---: | ---: | ---: | ---: |
| `compact` | 192 MiB | 0.25 | 32 | 1 |
| `standard` | 512 MiB | 1 | 64 | 4 |
| `dense` | 1 GiB | 2 | 96 | 16 |
The launcher passes a second concurrency ceiling to the process. Startup and
preflight fail closed if `mcp.json` requests more concurrency than its resource
class. There is no queue, retry, poller, watcher or resident health timer.
Export only the immutable image digest, private directory path, dedicated
network name and resource class, then validate before registering the host:
```sh
deploy/mcp/ql3-cluster-copilot/docker-stdio.sh check
```
The check validates all mounted path/credential authority and makes one
unauthenticated `GET /readyz`. Its JSON contains no endpoint, path, credential
or cluster identity. A not-ready response exits 69; invalid local authority or
transport failure emits only a low-sensitive failure fact.
Copy `mcp-host.example.json` into the external MCP host's private
configuration, replace its launcher path, image digest, private root and
network, and map its `command`/`args`/`env` fields to the host's equivalent
stdio process adapter. `serve` runs Docker attached to stdin/stdout with a
read-only root filesystem, no capabilities, no-new-privileges, a fixed
non-root UID, bounded memory/CPU/PIDs and `--pull never`.
Do not compose this directory into Edge/Standalone, `cluster-control`, the
Cluster AI Pod or any shared Kubernetes operations Kustomization. Small router
profiles continue to use the separately bounded Local MCP artifact only when
explicitly selected; otherwise they carry no MCP dependency at all.
@@ -0,0 +1,7 @@
{
"schema": "qinglong/cluster-copilot-client-config@v1",
"endpoint": "https://replace-cluster-api.example.com:5800/",
"servername": "replace-cluster-api.example.com",
"caFile": "/var/run/secrets/qinglong3/copilot-mcp/ca.pem",
"requestTimeoutMs": 30000
}
+83
View File
@@ -0,0 +1,83 @@
#!/bin/sh
set -eu
usage() {
printf '%s\n' 'Usage: docker-stdio.sh check|serve' >&2
exit 64
}
fail() {
printf '%s\n' '{"schemaVersion":1,"component":"qinglong3-cluster-copilot-mcp-launcher","event":"launch_failed"}' >&2
exit 78
}
[ "$#" -eq 1 ] || usage
mode=$1
case "$mode" in
check|serve) ;;
*) usage ;;
esac
image=${QL3_COPILOT_MCP_IMAGE-}
private_root=${QL3_COPILOT_MCP_PRIVATE_ROOT-}
network=${QL3_COPILOT_MCP_NETWORK-}
resource_class=${QL3_COPILOT_MCP_RESOURCE_CLASS-compact}
printf '%s' "$image" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9._/-]{0,191}@sha256:[0-9a-f]{64}$' || fail
printf '%s' "$network" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$' || fail
case "$network" in
bridge|default|host|none) fail ;;
esac
case "$private_root" in
/*) ;;
*) fail ;;
esac
case "$private_root" in
*','*|*':'*) fail ;;
esac
[ -d "$private_root" ] || fail
canonical_root=$(CDPATH= cd -- "$private_root" 2>/dev/null && pwd -P) || fail
[ "$canonical_root" = "$private_root" ] || fail
case "$resource_class" in
compact)
memory=192m
cpus=0.25
pids=32
concurrency_ceiling=1
;;
standard)
memory=512m
cpus=1
pids=64
concurrency_ceiling=4
;;
dense)
memory=1g
cpus=2
pids=96
concurrency_ceiling=16
;;
*) fail ;;
esac
set -- docker run --rm -i --pull never --init --read-only \
--network "$network" \
--cap-drop ALL \
--security-opt no-new-privileges \
--user 10001:10001 \
--pids-limit "$pids" \
--memory "$memory" \
--cpus "$cpus" \
--mount "type=bind,src=$private_root,dst=/var/run/secrets/qinglong3/copilot-mcp,readonly" \
"$image" \
copilot-mcp
if [ "$mode" = check ]; then
exec "$@" --check --config /var/run/secrets/qinglong3/copilot-mcp/mcp.json \
"--concurrency-ceiling=$concurrency_ceiling"
fi
exec "$@" --config /var/run/secrets/qinglong3/copilot-mcp/mcp.json \
"--concurrency-ceiling=$concurrency_ceiling"
@@ -0,0 +1,6 @@
{
"schema": "qinglong/cluster-copilot-mcp-server@v1",
"clientConfigFile": "/var/run/secrets/qinglong3/copilot-mcp/client.json",
"credentialFile": "/var/run/secrets/qinglong3/copilot-mcp/credential",
"maxConcurrentRequests": 1
}
@@ -0,0 +1,14 @@
{
"mcpServers": {
"qinglong-cluster-copilot": {
"command": "/absolute/path/to/docker-stdio.sh",
"args": ["serve"],
"env": {
"QL3_COPILOT_MCP_IMAGE": "ghcr.io/replace-owner/qinglong3-cluster-admin@sha256:0000000000000000000000000000000000000000000000000000000000000000",
"QL3_COPILOT_MCP_PRIVATE_ROOT": "/absolute/private/qinglong3-cluster-copilot",
"QL3_COPILOT_MCP_NETWORK": "qinglong3-copilot-egress",
"QL3_COPILOT_MCP_RESOURCE_CLASS": "compact"
}
}
}
}