mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): harden copilot mcp host deployment
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user