"""Production application launcher for multi-namespace MCP fleets (#978 B1). One real LLM application launch mints exactly one trusted ``client_instance_id`` and propagates it to every Gitea MCP namespace worker started for that launch. Workers never invent a trusted instance identity from PID proximity, timestamps, or ordinary untrusted environment values. This module is the production serve-path authority for instance identity. Tests and fixtures may call the same functions, but production registration receives the identity from the env this launcher builds — not from a hand-set test-only helper that bypasses it. """ from __future__ import annotations import os import secrets from typing import Any, Mapping, Sequence import mcp_fleet_snapshot as fleet import mcp_worker_identity as mwi #: Canonical MCP server names for the five role namespaces. NAMESPACE_SERVER_NAMES: tuple[str, ...] = ( "gitea-author", "gitea-reviewer", "gitea-merger", "gitea-controller", "gitea-reconciler", ) #: Map MCP server name → role/namespace kind. SERVER_TO_NAMESPACE: dict[str, str] = { "gitea-author": "author", "gitea-reviewer": "reviewer", "gitea-merger": "merger", "gitea-controller": "controller", "gitea-reconciler": "reconciler", } SANCTIONED_NAMESPACES: tuple[str, ...] = ( "author", "reviewer", "merger", "controller", "reconciler", ) # Env the launcher may set on every worker of one application launch. CLIENT_NAME_ENV = "GITEA_MCP_CLIENT" CLIENT_INSTANCE_ENV = fleet.CLIENT_INSTANCE_ENV FLEET_RUN_ENV = fleet.FLEET_RUN_ENV CLIENT_SESSION_ENV = "GITEA_MCP_CLIENT_SESSION" CLIENT_MANAGED_ENV = "GITEA_CLIENT_MANAGED" PROFILE_ENV = "GITEA_MCP_PROFILE" CONFIG_ENV = "GITEA_MCP_CONFIG" INSTANCE_PROVENANCE_ENV = "GITEA_MCP_INSTANCE_PROVENANCE" WORKER_IDENTITY_ENV = "GITEA_MCP_WORKER_IDENTITY" GENERATION_ID_ENV = "GITEA_MCP_GENERATION_ID" # Marker the trusted launcher alone writes; ordinary user env without this # marker is never classified as launcher-trusted provenance. LAUNCHER_PROVENANCE_VALUE = fleet.INSTANCE_ID_PROVENANCE_TRUSTED def mint_application_launch( client_type: str | None, *, launch_nonce: str | None = None, fleet_run_id: str | None = None, session_id: str | None = None, now=None, ) -> dict[str, Any]: """Mint one trusted application-instance identity for a production launch. Called exactly once per real application launch. The returned ``client_instance_id`` is injected into every namespace worker environment for that launch. A second call (separate launch) yields a different ID. """ client = mwi.normalize_client_name(client_type) instance_id = fleet.generate_client_instance_id( client, launch_nonce=launch_nonce, now=now ) assessment = fleet.assess_instance_identity(instance_id) if not assessment["trusted"]: # generate_client_instance_id always produces a trusted format; fail # closed if that invariant ever breaks rather than shipping untrusted. raise RuntimeError( f"launcher produced untrusted client_instance_id {instance_id!r}: " f"{assessment.get('reasons')}" ) session = (session_id or "").strip() or f"launch-{secrets.token_hex(12)}" return { "client_type": client, "client_instance_id": instance_id, "instance_id_provenance": LAUNCHER_PROVENANCE_VALUE, "instance_identity_trusted": True, "fleet_run_id": (fleet_run_id or "").strip() or None, "session_id": session, "namespaces": list(SANCTIONED_NAMESPACES), "namespace_server_names": list(NAMESPACE_SERVER_NAMES), } def namespace_worker_env( *, profile_name: str, client_type: str | None, client_instance_id: str, config_path: str | None = None, fleet_run_id: str | None = None, session_id: str | None = None, extra_env: Mapping[str, str] | None = None, ) -> dict[str, str]: """Build the environment for one namespace worker of a trusted launch. Never invents a client_instance_id. The caller must supply the launch-minted identity so all five workers receive the same value. """ assessment = fleet.assess_instance_identity(client_instance_id) if not assessment["trusted"]: raise ValueError( "namespace_worker_env refuses untrusted client_instance_id " f"{client_instance_id!r}: {assessment.get('reasons')}" ) client = mwi.normalize_client_name(client_type) env: dict[str, str] = { PROFILE_ENV: str(profile_name), CLIENT_MANAGED_ENV: "1", "GITEA_MCP_CLIENT": client, CLIENT_INSTANCE_ENV: assessment["client_instance_id"], INSTANCE_PROVENANCE_ENV: LAUNCHER_PROVENANCE_VALUE, } if config_path: env[CONFIG_ENV] = str(config_path) if fleet_run_id: env[FLEET_RUN_ENV] = str(fleet_run_id) if session_id: env[CLIENT_SESSION_ENV] = str(session_id) if extra_env: # Never let untrusted callers override the trusted instance keys. protected = { CLIENT_INSTANCE_ENV, INSTANCE_PROVENANCE_ENV, "GITEA_MCP_CLIENT", CLIENT_MANAGED_ENV, } for key, value in extra_env.items(): if key in protected: continue env[str(key)] = str(value) return env def resolve_launch_namespaces( namespaces: "Sequence[str] | None" = None, ) -> tuple[str, ...]: """Validate an explicit project-scoped namespace subset (#985). ``None`` keeps the historical whole-fleet behaviour and resolves to all five sanctioned namespaces. An explicit sequence is validated against :data:`SANCTIONED_NAMESPACES` and returned in canonical fleet order, so a project-scoped configuration (for example author/reviewer/merger only) can launch without inventing controller or reconciler profiles. This is an allow-list, not a filter: anything outside the sanctioned set is refused rather than silently dropped, so a typo can never quietly shrink a launch to fewer workers than the operator intended. """ if namespaces is None: return tuple(SANCTIONED_NAMESPACES) requested = [str(ns).strip().lower() for ns in namespaces] if not requested or any(not ns for ns in requested): raise ValueError( "namespaces must be a non-empty sequence of sanctioned namespace " f"names; choose from {list(SANCTIONED_NAMESPACES)}" ) unknown = sorted({ns for ns in requested if ns not in SANCTIONED_NAMESPACES}) if unknown: raise ValueError( f"unsanctioned namespace(s) {unknown} requested; sanctioned " f"namespaces are {list(SANCTIONED_NAMESPACES)}" ) duplicates = sorted({ns for ns in requested if requested.count(ns) > 1}) if duplicates: raise ValueError( f"duplicate namespace(s) {duplicates} requested; each sanctioned " "namespace may appear at most once in one launch" ) selected = set(requested) return tuple(ns for ns in SANCTIONED_NAMESPACES if ns in selected) def build_application_mcp_servers( profile_by_namespace: Mapping[str, str], *, client_type: str | None, namespaces: "Sequence[str] | None" = None, config_path: str | None = None, client_instance_id: str | None = None, fleet_run_id: str | None = None, session_id: str | None = None, launch_nonce: str | None = None, command: str | None = None, args: list[str] | None = None, now=None, ) -> dict[str, Any]: """Build a production ``mcpServers`` map for one application launch. Mints one ``client_instance_id`` when *client_instance_id* is omitted (fresh launch). When the caller supplies a previously minted trusted ID (resume of the same launch / config rewrite), that ID is reused so reconnect keeps attribution. A full new application restart omits the ID and receives a fresh mint. Every namespace server entry receives the **same** instance ID. Separate calls with no supplied ID receive distinct IDs. *namespaces* selects an explicitly validated project-scoped subset (#985). Omitting it preserves the original whole-fleet behaviour, so existing five-namespace callers are unaffected. Only the resolved namespaces need a profile, and only they are started; excluded namespaces are reported so a caller can prove a controller/reconciler worker was never launched. """ resolved_namespaces = resolve_launch_namespaces(namespaces) missing = [ ns for ns in resolved_namespaces if not profile_by_namespace.get(ns) ] if missing: raise ValueError( "build_application_mcp_servers requires a profile for every " f"requested namespace; missing: {missing}" ) if client_instance_id is None: launch = mint_application_launch( client_type, launch_nonce=launch_nonce, fleet_run_id=fleet_run_id, session_id=session_id, now=now, ) else: assessment = fleet.assess_instance_identity(client_instance_id) if not assessment["trusted"]: raise ValueError( "refusing to propagate untrusted client_instance_id " f"{client_instance_id!r} into production launch envs: " f"{assessment.get('reasons')}" ) launch = { "client_type": mwi.normalize_client_name(client_type), "client_instance_id": assessment["client_instance_id"], "instance_id_provenance": LAUNCHER_PROVENANCE_VALUE, "instance_identity_trusted": True, "fleet_run_id": (fleet_run_id or "").strip() or None, "session_id": (session_id or "").strip() or f"launch-{secrets.token_hex(12)}", "namespaces": list(SANCTIONED_NAMESPACES), "namespace_server_names": list(NAMESPACE_SERVER_NAMES), } # The launch record describes only the namespaces this launch actually # starts, so downstream attribution never implies an unstarted worker. launch["namespaces"] = list(resolved_namespaces) launch["namespace_server_names"] = [ f"gitea-{ns}" for ns in resolved_namespaces ] # Resolve command/args from the production server entry when not provided. if command is None or args is None: import gitea_config cmd, cmd_args = gitea_config.server_command() command = command or cmd args = args if args is not None else list(cmd_args) servers: dict[str, Any] = {} shared_id = launch["client_instance_id"] for namespace in resolved_namespaces: server_name = f"gitea-{namespace}" profile = profile_by_namespace[namespace] env = namespace_worker_env( profile_name=profile, client_type=launch["client_type"], client_instance_id=shared_id, config_path=config_path, fleet_run_id=launch.get("fleet_run_id"), session_id=launch.get("session_id"), ) servers[server_name] = { "command": command, "args": list(args), "env": env, } excluded = [ns for ns in SANCTIONED_NAMESPACES if ns not in resolved_namespaces] return { "mcpServers": servers, "launch": launch, "client_instance_id": shared_id, "client_type": launch["client_type"], "namespaces": list(resolved_namespaces), "excluded_namespaces": excluded, "project_scoped": bool(excluded), "shared_instance_id_across_namespaces": True, "namespace_count": len(resolved_namespaces), } def launcher_entry_for_profile( profile_name: str, *, client_type: str | None = None, config_path: str | None = None, client_instance_id: str | None = None, fleet_run_id: str | None = None, session_id: str | None = None, server_key: str = "gitea-tools", launch_nonce: str | None = None, now=None, ) -> dict[str, Any]: """Thin single-server production launcher entry with trusted instance ID. Used when only one namespace is being configured. Still mints (or reuses) a trusted ``client_instance_id`` so production never relies on the legacy placeholder identity for normal launches. """ import gitea_config if client_instance_id is None: launch = mint_application_launch( client_type or "unknown", launch_nonce=launch_nonce, fleet_run_id=fleet_run_id, session_id=session_id, now=now, ) client_instance_id = launch["client_instance_id"] client = launch["client_type"] fleet_run = launch.get("fleet_run_id") session = launch.get("session_id") else: assessment = fleet.assess_instance_identity(client_instance_id) if not assessment["trusted"]: raise ValueError( f"untrusted client_instance_id {client_instance_id!r}" ) client = mwi.normalize_client_name(client_type) fleet_run = (fleet_run_id or "").strip() or None session = (session_id or "").strip() or None client_instance_id = assessment["client_instance_id"] command, args = gitea_config.server_command() env = namespace_worker_env( profile_name=profile_name, client_type=client, client_instance_id=client_instance_id, config_path=config_path or gitea_config.DEFAULT_CONFIG_PATH, fleet_run_id=fleet_run, session_id=session, ) return { server_key: { "command": command, "args": args, "env": env, }, "client_instance_id": client_instance_id, "client_type": client, } def collect_instance_ids_from_mcp_servers( mcp_servers: Mapping[str, Any], *, namespaces: "Sequence[str] | None" = None, ) -> dict[str, Any]: """Inspect a production mcpServers map for shared instance attribution. Returns the unique set of client_instance_id values across the gitea-* servers of one launch and whether every worker of that launch shares exactly one trusted ID. The inspected set is the launch's own namespaces, not an assumed five (#985): a project-scoped author/reviewer/merger launch must be able to prove shared attribution without a controller or reconciler entry counting as a missing worker. Pass *namespaces* to assert an exact expected subset; omit it to inspect whichever sanctioned namespace servers are present. """ if namespaces is None: expected_servers = [ name for name in NAMESPACE_SERVER_NAMES if name in mcp_servers ] else: expected_servers = [ f"gitea-{ns}" for ns in resolve_launch_namespaces(namespaces) ] ids: list[str] = [] by_server: dict[str, str | None] = {} for name in expected_servers: entry = mcp_servers.get(name) or {} env = entry.get("env") or {} raw = (env.get(CLIENT_INSTANCE_ENV) or "").strip() or None by_server[name] = raw if raw: ids.append(raw) unique = sorted(set(ids)) trusted = [ i for i in unique if fleet.assess_instance_identity(i)["trusted"] ] missing_servers = [n for n in expected_servers if n not in mcp_servers] return { "server_instance_ids": by_server, "unique_instance_ids": unique, "trusted_instance_ids": trusted, "inspected_servers": list(expected_servers), "missing_servers": missing_servers, "shared_single_trusted_id": ( bool(expected_servers) and not missing_servers and len(unique) == 1 and len(trusted) == 1 and all(by_server.get(n) == unique[0] for n in expected_servers) ), "namespace_server_count": sum( 1 for n in NAMESPACE_SERVER_NAMES if n in mcp_servers ), } def inherit_or_refuse_client_instance( env: Mapping[str, str] | None = None, ) -> dict[str, Any]: """Resolve instance identity for a worker process at serve time. Production workers inherit the launcher-issued ID. They never mint a trusted ID themselves. Missing / legacy / malformed values fail soft into an untrusted assessment so registration can still record a diagnostic row without authorizing multi-instance fleet mutation. """ source = dict(env if env is not None else os.environ) raw = (source.get(CLIENT_INSTANCE_ENV) or "").strip() or None provenance_marker = (source.get(INSTANCE_PROVENANCE_ENV) or "").strip() assessment = dict(fleet.assess_instance_identity(raw)) # The ``inst-…`` format is public and reproducible, so format alone cannot # establish trust: anyone able to set one env var could otherwise hand-write # a well-formed ID and be believed. Trust therefore requires the launcher's # own seal as well, and a well-formed but unsealed identity fails closed # rather than degrading to a mere annotation (#985). if assessment["trusted"] and provenance_marker != LAUNCHER_PROVENANCE_VALUE: assessment["launcher_sealed"] = False assessment["trusted"] = False assessment["complete"] = False assessment["provenance"] = fleet.INSTANCE_ID_PROVENANCE_UNSEALED assessment["reasons"] = list(assessment.get("reasons") or []) + [ f"{INSTANCE_PROVENANCE_ENV} is not {LAUNCHER_PROVENANCE_VALUE!r}; " "identity format is valid but not sealed by the production " "launcher, so trusted attribution is refused" ] else: assessment["launcher_sealed"] = bool( assessment["trusted"] and provenance_marker == LAUNCHER_PROVENANCE_VALUE ) assessment["fleet_run_id"] = (source.get(FLEET_RUN_ENV) or "").strip() or None assessment["session_id"] = ( (source.get(CLIENT_SESSION_ENV) or "").strip() or None ) assessment["client_type"] = mwi.normalize_client_name( (source.get("GITEA_MCP_CLIENT") or "").strip() or None ) return assessment # --------------------------------------------------------------------------- # Runnable launch path (#985) # --------------------------------------------------------------------------- #: How each supported LLM client is started with a per-launch MCP config. #: #: The launch interface is deliberately a data-driven registry rather than a #: Claude-specific code path: adding another supported client is one entry, and #: every client necessarily goes through the same mint-once/propagate-to-all #: identity mechanism because the argv builder only ever receives a config file #: this module wrote. CLIENT_LAUNCH_SPECS: dict[str, dict[str, Any]] = { "claude_code": { "command": "claude", "config_args": lambda path: ["--mcp-config", path, "--strict-mcp-config"], "docs": "claude --mcp-config --strict-mcp-config", }, } def supported_launch_clients() -> tuple[str, ...]: """Client types with a sanctioned runnable launch path.""" return tuple(sorted(CLIENT_LAUNCH_SPECS)) def build_client_launch_argv( client_type: str, config_path: str, *, extra_args: "Sequence[str] | None" = None, ) -> list[str]: """Build the argv that starts *client_type* against a per-launch config.""" client = mwi.normalize_client_name(client_type) spec = CLIENT_LAUNCH_SPECS.get(client) if spec is None: raise ValueError( f"no sanctioned runnable launch path for client {client!r}; " f"supported clients: {list(supported_launch_clients())}" ) argv = [str(spec["command"])] + list(spec["config_args"](str(config_path))) if extra_args: argv.extend(str(a) for a in extra_args) return argv def write_launch_config( mcp_servers: Mapping[str, Any], *, directory: str | None = None, ) -> str: """Write one launch's ``mcpServers`` config to a fresh per-launch file. The file is per-launch and owner-readable only. #985 explicitly rules out persisting an instance ID into a shared, reused ``.mcp.json``: two concurrent sessions reading one static trusted ID is precisely the reuse case the duplicate guard must reject. A distinct file per launch keeps the minted identity bound to the launch that minted it. """ import json import tempfile fd, path = tempfile.mkstemp( prefix="gitea-mcp-launch-", suffix=".json", dir=directory ) try: os.fchmod(fd, 0o600) with os.fdopen(fd, "w", encoding="utf-8") as handle: json.dump({"mcpServers": dict(mcp_servers)}, handle, indent=2) handle.write("\n") except Exception: try: os.unlink(path) except OSError: pass raise return path def prepare_application_launch( profile_by_namespace: Mapping[str, str], *, client_type: str, namespaces: "Sequence[str] | None" = None, config_path: str | None = None, fleet_run_id: str | None = None, session_id: str | None = None, launch_nonce: str | None = None, extra_args: "Sequence[str] | None" = None, config_directory: str | None = None, now=None, ) -> dict[str, Any]: """Mint one launch and produce everything needed to exec the client. Returns the built servers, the per-launch config file path, the argv to exec, and the shared trusted identity — without starting anything, so the same preparation is testable and inspectable via ``--dry-run``. """ built = build_application_mcp_servers( profile_by_namespace, client_type=client_type, namespaces=namespaces, config_path=config_path, fleet_run_id=fleet_run_id, session_id=session_id, launch_nonce=launch_nonce, now=now, ) # Fail before writing anything if this client has no runnable path. argv_probe = build_client_launch_argv( client_type, "", extra_args=extra_args ) launch_config_path = write_launch_config( built["mcpServers"], directory=config_directory ) argv = build_client_launch_argv( client_type, launch_config_path, extra_args=extra_args ) proof = collect_instance_ids_from_mcp_servers( built["mcpServers"], namespaces=built["namespaces"] ) return { "client_type": built["client_type"], "client_instance_id": built["client_instance_id"], "namespaces": built["namespaces"], "excluded_namespaces": built["excluded_namespaces"], "project_scoped": built["project_scoped"], "launch_config_path": launch_config_path, "argv": argv, "argv_template": argv_probe, "mcpServers": built["mcpServers"], "launch": built["launch"], "shared_single_trusted_id": proof["shared_single_trusted_id"], "attribution_proof": proof, } def _parse_profile_assignments(values: "Sequence[str]") -> dict[str, str]: """Parse ``namespace=profile`` CLI pairs into a mapping.""" profiles: dict[str, str] = {} for raw in values or (): text = str(raw) if "=" not in text: raise ValueError( f"invalid --profile {text!r}; expected namespace=profile-name" ) namespace, _, profile = text.partition("=") namespace = namespace.strip().lower() profile = profile.strip() if not namespace or not profile: raise ValueError( f"invalid --profile {text!r}; expected namespace=profile-name" ) if namespace in profiles: raise ValueError(f"duplicate --profile entry for namespace {namespace!r}") profiles[namespace] = profile return profiles def main(argv: "Sequence[str] | None" = None) -> int: """Runnable entry point: start a supported LLM client for one launch. Example (project-scoped, three namespaces):: python3 -m mcp_application_launcher \\ --client claude_code \\ --namespaces author,reviewer,merger \\ --profile author=prgs-author \\ --profile reviewer=prgs-reviewer \\ --profile merger=prgs-merger """ import argparse import json parser = argparse.ArgumentParser( prog="mcp_application_launcher", description=( "Start a supported LLM client with one trusted, per-launch " "GITEA_MCP_CLIENT_INSTANCE shared by every MCP namespace worker." ), ) parser.add_argument( "--client", default="claude_code", help=f"client to launch; supported: {list(supported_launch_clients())}", ) parser.add_argument( "--namespaces", default=None, help=( "comma-separated sanctioned namespace subset (e.g. " "'author,reviewer,merger'); omit to launch all five" ), ) parser.add_argument( "--profile", action="append", default=[], metavar="NAMESPACE=PROFILE", help="profile for one namespace; repeat once per launched namespace", ) parser.add_argument("--config-path", default=None, help="profiles.json path") parser.add_argument("--fleet-run-id", default=None) parser.add_argument("--session-id", default=None) parser.add_argument( "--dry-run", action="store_true", help="print the launch plan as JSON and exit without starting anything", ) parser.add_argument( "client_args", nargs=argparse.REMAINDER, help="arguments forwarded to the client after '--'", ) ns = parser.parse_args(list(argv) if argv is not None else None) try: profiles = _parse_profile_assignments(ns.profile) requested = ( [part.strip() for part in ns.namespaces.split(",") if part.strip()] if ns.namespaces else None ) resolved = resolve_launch_namespaces(requested) # Profiles for namespaces that are not being launched are almost always # a mistake (a stale five-namespace invocation), so refuse rather than # silently ignore them. stray = sorted(set(profiles) - set(resolved)) if stray: raise ValueError( f"--profile given for namespace(s) {stray} that this launch " f"does not start; launched namespaces are {list(resolved)}" ) forwarded = [a for a in (ns.client_args or []) if a != "--"] prepared = prepare_application_launch( profiles, client_type=ns.client, namespaces=requested, config_path=ns.config_path, fleet_run_id=ns.fleet_run_id, session_id=ns.session_id, extra_args=forwarded, ) except ValueError as exc: parser.error(str(exc)) return 2 # pragma: no cover - argparse.error raises SystemExit if ns.dry_run: plan = { key: prepared[key] for key in ( "client_type", "client_instance_id", "namespaces", "excluded_namespaces", "project_scoped", "launch_config_path", "argv", "shared_single_trusted_id", ) } print(json.dumps(plan, indent=2)) return 0 argv_to_exec = prepared["argv"] os.execvp(argv_to_exec[0], argv_to_exec) return 0 # pragma: no cover - execvp does not return if __name__ == "__main__": # pragma: no cover - process entry point raise SystemExit(main())