fix(controller): production client_instance_id launch path and instance-aware mutation gate
Remediate review 655 blockers on PR #979 (issue #978): B1 — Production launcher (mcp_application_launcher) mints one trusted client_instance_id per application launch and propagates it to all five namespace workers via GITEA_MCP_CLIENT_INSTANCE. launcher_entry and multi_namespace_launcher_entries use that path. Workers never invent a trusted ID; missing/malformed/user-supplied values fail closed. B2 — _check_mcp_runtimes_diagnostics is instance-aware: two legitimate instances sharing a profile are allowed when each has a distinct trusted client_instance_id; duplicate workers for the same (instance, profile) still fail closed. Worker identity/generation exported for peer scans. Tests cover shared ID across five namespaces, distinct launches, multi- instance same profile, same-instance duplicates, untrusted attribution, and the production launcher path. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
@@ -49,27 +49,54 @@ model (#949 assumption) as the operating rule for multi-instance fleets.
|
|||||||
## How five workers join one instance
|
## How five workers join one instance
|
||||||
|
|
||||||
1. The host starts one application instance (for example one Codex session).
|
1. The host starts one application instance (for example one Codex session).
|
||||||
2. The trusted launcher mints `client_instance_id` (see
|
2. The **production application launcher**
|
||||||
`mcp_fleet_snapshot.generate_client_instance_id`) and injects:
|
(`mcp_application_launcher.build_application_mcp_servers` /
|
||||||
|
`gitea_config.multi_namespace_launcher_entries`) mints exactly one
|
||||||
|
`client_instance_id` via `mcp_fleet_snapshot.generate_client_instance_id`
|
||||||
|
and injects the same env into every namespace worker:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
GITEA_MCP_CLIENT=<client_type>
|
GITEA_MCP_CLIENT=<client_type>
|
||||||
GITEA_MCP_CLIENT_INSTANCE=<client_instance_id>
|
GITEA_MCP_CLIENT_INSTANCE=<client_instance_id>
|
||||||
|
GITEA_MCP_INSTANCE_PROVENANCE=trusted_launcher
|
||||||
GITEA_MCP_CLIENT_SESSION=<session_id> # optional but recommended
|
GITEA_MCP_CLIENT_SESSION=<session_id> # optional but recommended
|
||||||
GITEA_MCP_FLEET_RUN_ID=<enrollment id> # when on an approved canary
|
GITEA_MCP_FLEET_RUN_ID=<enrollment id> # when on an approved canary
|
||||||
|
GITEA_CLIENT_MANAGED=1
|
||||||
|
GITEA_MCP_PROFILE=<role-profile>
|
||||||
```
|
```
|
||||||
|
|
||||||
3. The launcher starts the five MCP namespace processes (or attaches five
|
3. The MCP client starts the five namespace processes (`gitea-author`,
|
||||||
role profiles) with **that same environment**.
|
`gitea-reviewer`, `gitea-merger`, `gitea-controller`, `gitea-reconciler`)
|
||||||
|
from that generated `mcpServers` map — each entry carries the **same**
|
||||||
|
instance ID.
|
||||||
4. Each worker registers once into the worker registry with its own
|
4. Each worker registers once into the worker registry with its own
|
||||||
`worker_identity`, `namespace`, `generation_id`, and PID, but the shared
|
`worker_identity`, `namespace`, `generation_id`, and PID, but the shared
|
||||||
`client_instance_id`.
|
`client_instance_id`. Workers never mint a trusted instance ID themselves.
|
||||||
|
|
||||||
|
Only IDs matching the launcher format `inst-<client>-<timestamp>-<digest>`
|
||||||
|
are trusted. Missing, `legacy-pid-*` / `pid-*` placeholders, and ordinary
|
||||||
|
user-supplied strings fail closed as untrusted and cannot authorize
|
||||||
|
multi-instance fleet mutation safety.
|
||||||
|
|
||||||
Worker reconnect (same process, same registration) keeps the instance ID.
|
Worker reconnect (same process, same registration) keeps the instance ID.
|
||||||
Worker restart (new process) mints a new worker identity and generation but
|
Worker restart (new process) mints a new worker identity and generation but
|
||||||
must still receive the same `GITEA_MCP_CLIENT_INSTANCE` from the parent
|
must still receive the same `GITEA_MCP_CLIENT_INSTANCE` from the parent
|
||||||
application if it is the same launch. Full application restart mints a new
|
application if it is the same launch. Full application restart mints a new
|
||||||
|
`client_instance_id` (call the launcher without a prior ID).
|
||||||
|
|
||||||
|
### Mutation gate (instance-aware)
|
||||||
|
|
||||||
|
The capability / runtime diagnostic gate
|
||||||
|
(`_check_mcp_runtimes_diagnostics`) is **instance-aware**:
|
||||||
|
|
||||||
|
* Two legitimate application instances that share a profile (same
|
||||||
|
`GITEA_MCP_PROFILE`) are allowed when each has a distinct trusted
|
||||||
`client_instance_id`.
|
`client_instance_id`.
|
||||||
|
* More than one live worker for the same
|
||||||
|
`(client_instance_id, profile/namespace)` fails closed as a duplicate
|
||||||
|
namespace worker.
|
||||||
|
* Multiple processes sharing a profile **without** trusted instance
|
||||||
|
evidence still fail closed (indistinguishable from a duplicate).
|
||||||
|
|
||||||
## Approved fleet manifest
|
## Approved fleet manifest
|
||||||
|
|
||||||
|
|||||||
+67
-15
@@ -1206,6 +1206,12 @@ RECOGNIZED_GITEA_ENV_KEYS = frozenset({
|
|||||||
# #978: operator-approved fleet enrollment identity shared by one launch.
|
# #978: operator-approved fleet enrollment identity shared by one launch.
|
||||||
"GITEA_MCP_FLEET_RUN_ID",
|
"GITEA_MCP_FLEET_RUN_ID",
|
||||||
"GITEA_MCP_PROCESS_IDENTITY",
|
"GITEA_MCP_PROCESS_IDENTITY",
|
||||||
|
# #978 B1/B2: launcher-sealed provenance + peer-visible worker identity so
|
||||||
|
# the instance-aware mutation gate can distinguish two legitimate
|
||||||
|
# application instances that share a profile from a true duplicate worker.
|
||||||
|
"GITEA_MCP_INSTANCE_PROVENANCE",
|
||||||
|
"GITEA_MCP_WORKER_IDENTITY",
|
||||||
|
"GITEA_MCP_GENERATION_ID",
|
||||||
# #975 review 652 B1: production also consumes HEARTBEAT_INTERVAL_ENV from
|
# #975 review 652 B1: production also consumes HEARTBEAT_INTERVAL_ENV from
|
||||||
# mcp_worker_identity via gitea_mcp_server._start_worker_heartbeat. Omitting
|
# mcp_worker_identity via gitea_mcp_server._start_worker_heartbeat. Omitting
|
||||||
# it reproduced the same unsupported-env → runtime_reconnect_required
|
# it reproduced the same unsupported-env → runtime_reconnect_required
|
||||||
@@ -1239,24 +1245,70 @@ def get_unconsumed_gitea_env_overrides(env=None) -> dict[str, str]:
|
|||||||
return unconsumed
|
return unconsumed
|
||||||
|
|
||||||
|
|
||||||
def launcher_entry(profile_name, config_path=None):
|
def launcher_entry(
|
||||||
|
profile_name,
|
||||||
|
config_path=None,
|
||||||
|
*,
|
||||||
|
client_type=None,
|
||||||
|
client_instance_id=None,
|
||||||
|
fleet_run_id=None,
|
||||||
|
session_id=None,
|
||||||
|
launch_nonce=None,
|
||||||
|
):
|
||||||
"""Return a thin MCP launcher entry for *profile_name*.
|
"""Return a thin MCP launcher entry for *profile_name*.
|
||||||
|
|
||||||
Contains command/args and the GITEA_MCP_* / GITEA_CLIENT_MANAGED env vars — never a token
|
Contains command/args and the GITEA_MCP_* / GITEA_CLIENT_MANAGED env vars —
|
||||||
or password. Suitable for Claude / Gemini / Codex ``mcpServers`` blocks.
|
never a token or password. Suitable for Claude / Gemini / Codex
|
||||||
|
``mcpServers`` blocks.
|
||||||
|
|
||||||
|
#978 B1: production launches mint (or reuse) a trusted
|
||||||
|
``GITEA_MCP_CLIENT_INSTANCE`` so workers never fall back to the legacy
|
||||||
|
placeholder identity on the real serve path. Pass *client_instance_id* to
|
||||||
|
resume the same launch; omit it for a fresh launch (new ID).
|
||||||
"""
|
"""
|
||||||
command, args = server_command()
|
import mcp_application_launcher as app_launcher
|
||||||
return {
|
|
||||||
"gitea-tools": {
|
built = app_launcher.launcher_entry_for_profile(
|
||||||
"command": command,
|
profile_name,
|
||||||
"args": args,
|
client_type=client_type or "unknown",
|
||||||
"env": {
|
config_path=config_path or DEFAULT_CONFIG_PATH,
|
||||||
"GITEA_MCP_CONFIG": config_path or DEFAULT_CONFIG_PATH,
|
client_instance_id=client_instance_id,
|
||||||
"GITEA_MCP_PROFILE": profile_name,
|
fleet_run_id=fleet_run_id,
|
||||||
"GITEA_CLIENT_MANAGED": "1",
|
session_id=session_id,
|
||||||
},
|
launch_nonce=launch_nonce,
|
||||||
}
|
server_key="gitea-tools",
|
||||||
}
|
)
|
||||||
|
# Public shape stays {server_key: {command, args, env}} for existing callers.
|
||||||
|
return {"gitea-tools": built["gitea-tools"]}
|
||||||
|
|
||||||
|
|
||||||
|
def multi_namespace_launcher_entries(
|
||||||
|
profile_by_namespace,
|
||||||
|
*,
|
||||||
|
client_type,
|
||||||
|
config_path=None,
|
||||||
|
client_instance_id=None,
|
||||||
|
fleet_run_id=None,
|
||||||
|
session_id=None,
|
||||||
|
launch_nonce=None,
|
||||||
|
):
|
||||||
|
"""Build production mcpServers for all five namespaces of one application launch.
|
||||||
|
|
||||||
|
One shared trusted ``client_instance_id`` is minted (or reused) and
|
||||||
|
propagated to every namespace worker env. See
|
||||||
|
:func:`mcp_application_launcher.build_application_mcp_servers`.
|
||||||
|
"""
|
||||||
|
import mcp_application_launcher as app_launcher
|
||||||
|
|
||||||
|
return app_launcher.build_application_mcp_servers(
|
||||||
|
profile_by_namespace,
|
||||||
|
client_type=client_type,
|
||||||
|
config_path=config_path or DEFAULT_CONFIG_PATH,
|
||||||
|
client_instance_id=client_instance_id,
|
||||||
|
fleet_run_id=fleet_run_id,
|
||||||
|
session_id=session_id,
|
||||||
|
launch_nonce=launch_nonce,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+130
-33
@@ -15734,6 +15734,9 @@ CLIENT_NAME_ENV = "GITEA_MCP_CLIENT"
|
|||||||
CLIENT_INSTANCE_ENV = "GITEA_MCP_CLIENT_INSTANCE"
|
CLIENT_INSTANCE_ENV = "GITEA_MCP_CLIENT_INSTANCE"
|
||||||
CLIENT_SESSION_ENV = "GITEA_MCP_CLIENT_SESSION"
|
CLIENT_SESSION_ENV = "GITEA_MCP_CLIENT_SESSION"
|
||||||
FLEET_RUN_ENV = "GITEA_MCP_FLEET_RUN_ID"
|
FLEET_RUN_ENV = "GITEA_MCP_FLEET_RUN_ID"
|
||||||
|
INSTANCE_PROVENANCE_ENV = "GITEA_MCP_INSTANCE_PROVENANCE"
|
||||||
|
WORKER_IDENTITY_ENV = "GITEA_MCP_WORKER_IDENTITY"
|
||||||
|
GENERATION_ID_ENV = "GITEA_MCP_GENERATION_ID"
|
||||||
|
|
||||||
|
|
||||||
def _worker_registry():
|
def _worker_registry():
|
||||||
@@ -15760,28 +15763,51 @@ def _client_identity_hints() -> dict:
|
|||||||
"""What the launcher told us about itself (#948 / #975 / #978).
|
"""What the launcher told us about itself (#948 / #975 / #978).
|
||||||
|
|
||||||
``client_instance_id`` is established by the trusted application launcher
|
``client_instance_id`` is established by the trusted application launcher
|
||||||
once per launch and shared by all five namespace workers. When the launcher
|
once per launch and shared by all five namespace workers. Workers inherit
|
||||||
key is absent we still register under an explicit *legacy* placeholder so
|
that value from the production launcher env and never mint a trusted ID
|
||||||
diagnostic reads work, but the registration is marked incomplete and cannot
|
themselves. When the launcher key is absent or untrusted we still register
|
||||||
authorize multi-instance fleet mutation safety.
|
under an explicit *legacy* placeholder so diagnostic reads work, but the
|
||||||
|
registration is marked incomplete and cannot authorize multi-instance fleet
|
||||||
|
mutation safety.
|
||||||
"""
|
"""
|
||||||
|
import mcp_application_launcher as app_launcher
|
||||||
import mcp_fleet_snapshot
|
import mcp_fleet_snapshot
|
||||||
|
|
||||||
|
inherited = app_launcher.inherit_or_refuse_client_instance(os.environ)
|
||||||
client_name = (os.environ.get(CLIENT_NAME_ENV) or "").strip() or None
|
client_name = (os.environ.get(CLIENT_NAME_ENV) or "").strip() or None
|
||||||
raw_instance = (os.environ.get(CLIENT_INSTANCE_ENV) or "").strip() or None
|
instance = {
|
||||||
instance = mcp_fleet_snapshot.assess_instance_identity(raw_instance)
|
"client_instance_id": inherited.get("client_instance_id"),
|
||||||
# Legacy placeholder only when the launcher omitted the key — never invent a
|
"provenance": inherited.get("provenance"),
|
||||||
# trusted ID from PID proximity.
|
"trusted": bool(inherited.get("trusted")),
|
||||||
|
"complete": bool(inherited.get("complete")),
|
||||||
|
"launcher_sealed": bool(inherited.get("launcher_sealed")),
|
||||||
|
}
|
||||||
|
# Legacy placeholder only when the launcher omitted a usable key — never
|
||||||
|
# invent a trusted ID from PID proximity or ordinary user env.
|
||||||
|
if not instance["client_instance_id"] or not instance["trusted"]:
|
||||||
if not instance["client_instance_id"]:
|
if not instance["client_instance_id"]:
|
||||||
placeholder = f"legacy-pid-{os.getpid()}"
|
placeholder = f"legacy-pid-{os.getpid()}"
|
||||||
instance = mcp_fleet_snapshot.assess_instance_identity(
|
fallback = mcp_fleet_snapshot.assess_instance_identity(
|
||||||
placeholder, source="pid_fallback"
|
placeholder, source="pid_fallback"
|
||||||
)
|
)
|
||||||
|
instance = {
|
||||||
|
"client_instance_id": fallback["client_instance_id"],
|
||||||
|
"provenance": fallback["provenance"],
|
||||||
|
"trusted": False,
|
||||||
|
"complete": False,
|
||||||
|
"launcher_sealed": False,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Malformed / untrusted user-supplied value: keep it for diagnosis
|
||||||
|
# but never mark trusted.
|
||||||
|
instance["trusted"] = False
|
||||||
|
instance["launcher_sealed"] = False
|
||||||
return {
|
return {
|
||||||
"client_name": client_name,
|
"client_name": client_name,
|
||||||
"client_instance_id": instance["client_instance_id"],
|
"client_instance_id": instance["client_instance_id"],
|
||||||
"instance_id_provenance": instance["provenance"],
|
"instance_id_provenance": instance["provenance"],
|
||||||
"instance_identity_trusted": instance["trusted"],
|
"instance_identity_trusted": instance["trusted"],
|
||||||
|
"instance_launcher_sealed": instance.get("launcher_sealed", False),
|
||||||
"session_id": (os.environ.get(CLIENT_SESSION_ENV) or "").strip()
|
"session_id": (os.environ.get(CLIENT_SESSION_ENV) or "").strip()
|
||||||
or f"proc-{os.getpid()}-{_process_boot_head_sha or 'nohead'}",
|
or f"proc-{os.getpid()}-{_process_boot_head_sha or 'nohead'}",
|
||||||
"fleet_run_id": (os.environ.get(FLEET_RUN_ENV) or "").strip() or None,
|
"fleet_run_id": (os.environ.get(FLEET_RUN_ENV) or "").strip() or None,
|
||||||
@@ -15857,6 +15883,19 @@ def _active_worker_identity() -> str | None:
|
|||||||
if outcome.get("registered"):
|
if outcome.get("registered"):
|
||||||
_WORKER_IDENTITY = identity
|
_WORKER_IDENTITY = identity
|
||||||
_WORKER_GENERATION = generation
|
_WORKER_GENERATION = generation
|
||||||
|
# #978 B2: export worker/generation/instance into this process
|
||||||
|
# env so peer process scans (and the instance-aware mutation
|
||||||
|
# gate) can attribute live workers without treating shared
|
||||||
|
# profiles as duplicates. Never overwrite a launcher-sealed
|
||||||
|
# client_instance_id with a different value.
|
||||||
|
os.environ[WORKER_IDENTITY_ENV] = identity
|
||||||
|
os.environ[GENERATION_ID_ENV] = generation
|
||||||
|
if hints.get("client_instance_id") and not (
|
||||||
|
os.environ.get(CLIENT_INSTANCE_ENV) or ""
|
||||||
|
).strip():
|
||||||
|
os.environ[CLIENT_INSTANCE_ENV] = str(
|
||||||
|
hints["client_instance_id"]
|
||||||
|
)
|
||||||
# #975: registration is the only moment identity and fencing
|
# #975: registration is the only moment identity and fencing
|
||||||
# epoch are both known, so the heartbeat supervisor is started
|
# epoch are both known, so the heartbeat supervisor is started
|
||||||
# here. Without it ``last_heartbeat_at`` never left
|
# here. Without it ``last_heartbeat_at`` never left
|
||||||
@@ -22375,6 +22414,20 @@ def _check_mcp_runtimes_diagnostics(task: str, matching_profiles: list[str]) ->
|
|||||||
)
|
)
|
||||||
peer_worker_identity = peer_env.get("GITEA_MCP_WORKER_IDENTITY")
|
peer_worker_identity = peer_env.get("GITEA_MCP_WORKER_IDENTITY")
|
||||||
peer_generation = peer_env.get("GITEA_MCP_GENERATION_ID")
|
peer_generation = peer_env.get("GITEA_MCP_GENERATION_ID")
|
||||||
|
# #978 B2: instance attribution for the live fleet gate. Two legitimate
|
||||||
|
# application instances may share a profile when each has a distinct
|
||||||
|
# trusted client_instance_id; only same-(instance, profile/namespace)
|
||||||
|
# duplicates remain blocked.
|
||||||
|
import mcp_fleet_snapshot as _fleet
|
||||||
|
|
||||||
|
peer_instance_raw = peer_env.get("GITEA_MCP_CLIENT_INSTANCE")
|
||||||
|
peer_instance_assess = _fleet.assess_instance_identity(peer_instance_raw)
|
||||||
|
peer_client_instance = (
|
||||||
|
peer_instance_assess["client_instance_id"]
|
||||||
|
if peer_instance_assess.get("trusted")
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
peer_instance_trusted = bool(peer_instance_assess.get("trusted"))
|
||||||
|
|
||||||
for env_match in re.finditer(r'\b(GITEA_[A-Z0-9_]+)=([^\s]+)', env_out):
|
for env_match in re.finditer(r'\b(GITEA_[A-Z0-9_]+)=([^\s]+)', env_out):
|
||||||
k, v = env_match.group(1), env_match.group(2)
|
k, v = env_match.group(1), env_match.group(2)
|
||||||
@@ -22392,6 +22445,9 @@ def _check_mcp_runtimes_diagnostics(task: str, matching_profiles: list[str]) ->
|
|||||||
"is_client_managed": is_client_managed,
|
"is_client_managed": is_client_managed,
|
||||||
"worker_identity": peer_worker_identity,
|
"worker_identity": peer_worker_identity,
|
||||||
"generation_id": peer_generation,
|
"generation_id": peer_generation,
|
||||||
|
"client_instance_id": peer_client_instance,
|
||||||
|
"instance_identity_trusted": peer_instance_trusted,
|
||||||
|
"raw_client_instance_id": peer_instance_raw,
|
||||||
}
|
}
|
||||||
if profile not in all_profile_procs:
|
if profile not in all_profile_procs:
|
||||||
all_profile_procs[profile] = []
|
all_profile_procs[profile] = []
|
||||||
@@ -22399,44 +22455,85 @@ def _check_mcp_runtimes_diagnostics(task: str, matching_profiles: list[str]) ->
|
|||||||
|
|
||||||
running_profiles = {}
|
running_profiles = {}
|
||||||
for profile, procs in all_profile_procs.items():
|
for profile, procs in all_profile_procs.items():
|
||||||
# #948 AC40/AC43: sharing a profile is legitimate — profile is a
|
# #978 B2 / #948 AC40: sharing a profile is legitimate when each live
|
||||||
# reusable capability definition, not a worker identity. What is *not*
|
# process belongs to a distinct trusted application instance. The
|
||||||
# legitimate is reusing one worker identity, or two live sessions
|
# permanent model is instance-aware — not exactly_one_per_profile.
|
||||||
# claiming one generation. Distinctness has to be proven, though:
|
# Fail closed only when:
|
||||||
# processes carrying no identity evidence are indistinguishable, so
|
# * more than one live worker claims the same (client_instance_id,
|
||||||
# they stay classified as duplicates and keep the #686 wall intact.
|
# profile/namespace), or
|
||||||
identified = [p for p in procs if p.get("worker_identity")]
|
# * worker identity / generation is reused, or
|
||||||
|
# * multiple processes share a profile without trusted instance
|
||||||
|
# evidence (indistinguishable → treat as duplicate).
|
||||||
|
# Group by trusted client_instance_id (untrusted/missing → one bucket).
|
||||||
|
by_instance: dict[str, list[dict]] = {}
|
||||||
|
for p in procs:
|
||||||
|
if p.get("instance_identity_trusted") and p.get("client_instance_id"):
|
||||||
|
key = str(p["client_instance_id"])
|
||||||
|
else:
|
||||||
|
key = "__untrusted_or_missing__"
|
||||||
|
by_instance.setdefault(key, []).append(p)
|
||||||
|
|
||||||
|
for instance_key, group in by_instance.items():
|
||||||
|
if len(group) <= 1:
|
||||||
|
continue
|
||||||
|
identified = [p for p in group if p.get("worker_identity")]
|
||||||
distinct_identities = {p["worker_identity"] for p in identified}
|
distinct_identities = {p["worker_identity"] for p in identified}
|
||||||
all_identified = len(identified) == len(procs)
|
all_identified = len(identified) == len(group)
|
||||||
duplicate_identity = len(identified) != len(distinct_identities)
|
duplicate_identity = len(identified) != len(distinct_identities)
|
||||||
contested_generation = any(
|
contested_generation = any(
|
||||||
len({p["worker_identity"] for p in identified if p.get("generation_id") == gen}) > 1
|
len(
|
||||||
for gen in {p.get("generation_id") for p in identified if p.get("generation_id")}
|
{
|
||||||
|
p["worker_identity"]
|
||||||
|
for p in identified
|
||||||
|
if p.get("generation_id") == gen
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
> 1
|
||||||
if len(procs) > 1 and (
|
for gen in {
|
||||||
|
p.get("generation_id")
|
||||||
|
for p in identified
|
||||||
|
if p.get("generation_id")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# Same trusted (instance, profile) with >1 live process is always a
|
||||||
|
# duplicate-namespace-worker, even if worker identities differ —
|
||||||
|
# one application launch gets exactly one worker per namespace.
|
||||||
|
same_trusted_instance = instance_key != "__untrusted_or_missing__"
|
||||||
|
if same_trusted_instance or (
|
||||||
not all_identified or duplicate_identity or contested_generation
|
not all_identified or duplicate_identity or contested_generation
|
||||||
):
|
):
|
||||||
pids_str = ", ".join(str(p["pid"]) for p in procs)
|
pids_str = ", ".join(str(p["pid"]) for p in group)
|
||||||
if duplicate_identity or contested_generation:
|
if same_trusted_instance:
|
||||||
detail = (
|
detail = (
|
||||||
"The same worker identity or generation is claimed more than once, "
|
f"More than one live worker for profile '{profile}' under "
|
||||||
"so these are genuine duplicates rather than independent workers."
|
f"client_instance_id {instance_key!r} (duplicate namespace "
|
||||||
|
"worker). Two legitimate application instances with "
|
||||||
|
"distinct trusted client_instance_id values may share a "
|
||||||
|
"profile; this collision does not."
|
||||||
|
)
|
||||||
|
elif duplicate_identity or contested_generation:
|
||||||
|
detail = (
|
||||||
|
"The same worker identity or generation is claimed more "
|
||||||
|
"than once, so these are genuine duplicates rather than "
|
||||||
|
"independent workers."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
detail = (
|
detail = (
|
||||||
"Manual or duplicate launches defeat staleness detection and cannot "
|
"Multiple processes share this profile without distinct "
|
||||||
"receive client stdio."
|
"trusted client_instance_id evidence, so they cannot be "
|
||||||
|
"told apart from a duplicate MCP process. Relaunch via "
|
||||||
|
"the production application launcher so each instance "
|
||||||
|
"receives a trusted GITEA_MCP_CLIENT_INSTANCE."
|
||||||
)
|
)
|
||||||
reasons.append(
|
reasons.append(
|
||||||
f"stale-runtime: Duplicate MCP server process(es) detected for profile '{profile}' (PIDs: {pids_str}). "
|
f"stale-runtime: Duplicate MCP server process(es) detected "
|
||||||
|
f"for profile '{profile}' (PIDs: {pids_str}). "
|
||||||
+ detail
|
+ detail
|
||||||
)
|
)
|
||||||
# Otherwise: several independently identified workers share one profile.
|
# Multiple trusted instances sharing one profile intentionally produce
|
||||||
# No reason is appended, deliberately. Every reason this function
|
# no reason here. Callers raise every reason as a hard RuntimeError, so
|
||||||
# returns is raised as a hard RuntimeError by its callers, so recording
|
# treating legitimate multi-instance concurrency as "informational"
|
||||||
# legitimate concurrency here as "informational" would block exactly the
|
# would re-introduce the profile-only wall #978 removes.
|
||||||
# case #948 exists to permit.
|
|
||||||
client_procs = [p for p in procs if p["is_client_managed"]]
|
client_procs = [p for p in procs if p["is_client_managed"]]
|
||||||
if client_procs:
|
if client_procs:
|
||||||
client_procs.sort(key=lambda p: p["start_time"], reverse=True)
|
client_procs.sort(key=lambda p: p["start_time"], reverse=True)
|
||||||
|
|||||||
@@ -0,0 +1,393 @@
|
|||||||
|
"""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
|
||||||
|
|
||||||
|
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 build_application_mcp_servers(
|
||||||
|
profile_by_namespace: Mapping[str, str],
|
||||||
|
*,
|
||||||
|
client_type: str | 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.
|
||||||
|
"""
|
||||||
|
missing = [
|
||||||
|
ns for ns in SANCTIONED_NAMESPACES if not profile_by_namespace.get(ns)
|
||||||
|
]
|
||||||
|
if missing:
|
||||||
|
raise ValueError(
|
||||||
|
"build_application_mcp_servers requires a profile for every "
|
||||||
|
f"sanctioned 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),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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 SANCTIONED_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,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"mcpServers": servers,
|
||||||
|
"launch": launch,
|
||||||
|
"client_instance_id": shared_id,
|
||||||
|
"client_type": launch["client_type"],
|
||||||
|
"namespaces": list(SANCTIONED_NAMESPACES),
|
||||||
|
"shared_instance_id_across_namespaces": True,
|
||||||
|
"namespace_count": len(SANCTIONED_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],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Inspect a production mcpServers map for shared instance attribution.
|
||||||
|
|
||||||
|
Returns the unique set of client_instance_id values across gitea-* servers
|
||||||
|
and whether all five namespaces share exactly one trusted ID.
|
||||||
|
"""
|
||||||
|
ids: list[str] = []
|
||||||
|
by_server: dict[str, str | None] = {}
|
||||||
|
for name in NAMESPACE_SERVER_NAMES:
|
||||||
|
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"]
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
"server_instance_ids": by_server,
|
||||||
|
"unique_instance_ids": unique,
|
||||||
|
"trusted_instance_ids": trusted,
|
||||||
|
"shared_single_trusted_id": (
|
||||||
|
len(unique) == 1
|
||||||
|
and len(trusted) == 1
|
||||||
|
and all(by_server.get(n) == unique[0] for n in NAMESPACE_SERVER_NAMES)
|
||||||
|
),
|
||||||
|
"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 = fleet.assess_instance_identity(raw)
|
||||||
|
# Ordinary user-supplied values without launcher provenance marker are
|
||||||
|
# still format-checked by assess_instance_identity. When the format is
|
||||||
|
# trusted but the launcher marker is absent, keep the ID but note that
|
||||||
|
# provenance is not launcher-sealed (operator hand-set or legacy config).
|
||||||
|
if assessment["trusted"] and provenance_marker != LAUNCHER_PROVENANCE_VALUE:
|
||||||
|
assessment = dict(assessment)
|
||||||
|
assessment["launcher_sealed"] = False
|
||||||
|
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"
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
assessment = dict(assessment)
|
||||||
|
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
|
||||||
+23
-1
@@ -88,8 +88,13 @@ INSTANCE_ID_PROVENANCE_MISSING = "missing"
|
|||||||
CLIENT_INSTANCE_ENV = "GITEA_MCP_CLIENT_INSTANCE"
|
CLIENT_INSTANCE_ENV = "GITEA_MCP_CLIENT_INSTANCE"
|
||||||
FLEET_RUN_ENV = "GITEA_MCP_FLEET_RUN_ID"
|
FLEET_RUN_ENV = "GITEA_MCP_FLEET_RUN_ID"
|
||||||
PROCESS_IDENTITY_ENV = "GITEA_MCP_PROCESS_IDENTITY"
|
PROCESS_IDENTITY_ENV = "GITEA_MCP_PROCESS_IDENTITY"
|
||||||
|
INSTANCE_PROVENANCE_ENV = "GITEA_MCP_INSTANCE_PROVENANCE"
|
||||||
|
|
||||||
_LEGACY_INSTANCE_PREFIXES = ("pid-", "proc-", "legacy-")
|
_LEGACY_INSTANCE_PREFIXES = ("pid-", "proc-", "legacy-")
|
||||||
|
#: Trusted launcher-issued IDs use the reserved ``inst-`` prefix
|
||||||
|
#: (see :func:`generate_client_instance_id`). Ordinary user-supplied strings
|
||||||
|
#: without that prefix never count as trusted attribution.
|
||||||
|
_TRUSTED_INSTANCE_PREFIX = "inst-"
|
||||||
|
|
||||||
|
|
||||||
def _utc_now() -> datetime:
|
def _utc_now() -> datetime:
|
||||||
@@ -131,7 +136,10 @@ def assess_instance_identity(
|
|||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Classify whether a client_instance_id is trusted enough for mutation.
|
"""Classify whether a client_instance_id is trusted enough for mutation.
|
||||||
|
|
||||||
Trusted instance IDs are non-empty and not the pre-#978 PID/proc fallbacks.
|
Trusted instance IDs are non-empty, match the launcher-minted ``inst-…``
|
||||||
|
format from :func:`generate_client_instance_id`, and are not pre-#978
|
||||||
|
PID/proc/legacy placeholders. Ordinary user-supplied or malformed values
|
||||||
|
fail closed as untrusted so they cannot spoof multi-instance attribution.
|
||||||
Incomplete identities remain visible for diagnosis but cannot authorize
|
Incomplete identities remain visible for diagnosis but cannot authorize
|
||||||
unsafe mutation.
|
unsafe mutation.
|
||||||
"""
|
"""
|
||||||
@@ -159,6 +167,20 @@ def assess_instance_identity(
|
|||||||
"not a trusted launcher-issued instance identity"
|
"not a trusted launcher-issued instance identity"
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
if not text.startswith(_TRUSTED_INSTANCE_PREFIX) or len(text) <= len(
|
||||||
|
_TRUSTED_INSTANCE_PREFIX
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
"client_instance_id": text,
|
||||||
|
"complete": False,
|
||||||
|
"trusted": False,
|
||||||
|
"provenance": INSTANCE_ID_PROVENANCE_LEGACY,
|
||||||
|
"reasons": [
|
||||||
|
f"client_instance_id {text!r} is malformed or user-supplied and "
|
||||||
|
f"does not use the trusted launcher prefix "
|
||||||
|
f"{_TRUSTED_INSTANCE_PREFIX!r}; refusing trusted attribution"
|
||||||
|
],
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
"client_instance_id": text,
|
"client_instance_id": text,
|
||||||
"complete": True,
|
"complete": True,
|
||||||
|
|||||||
@@ -175,8 +175,21 @@ class TestLauncherSnippets(unittest.TestCase):
|
|||||||
def test_only_safe_keys_no_secrets(self):
|
def test_only_safe_keys_no_secrets(self):
|
||||||
entry = gitea_config.launcher_entry("prgs", "/cfg/profiles.json")["gitea-tools"]
|
entry = gitea_config.launcher_entry("prgs", "/cfg/profiles.json")["gitea-tools"]
|
||||||
self.assertEqual(set(entry), {"command", "args", "env"})
|
self.assertEqual(set(entry), {"command", "args", "env"})
|
||||||
self.assertEqual(set(entry["env"]), {"GITEA_MCP_CONFIG", "GITEA_MCP_PROFILE", "GITEA_CLIENT_MANAGED"})
|
# #978 B1: production launcher also injects trusted client identity.
|
||||||
|
required = {
|
||||||
|
"GITEA_MCP_CONFIG",
|
||||||
|
"GITEA_MCP_PROFILE",
|
||||||
|
"GITEA_CLIENT_MANAGED",
|
||||||
|
"GITEA_MCP_CLIENT",
|
||||||
|
"GITEA_MCP_CLIENT_INSTANCE",
|
||||||
|
"GITEA_MCP_INSTANCE_PROVENANCE",
|
||||||
|
}
|
||||||
|
self.assertTrue(required.issubset(set(entry["env"])), entry["env"])
|
||||||
self.assertEqual(entry["env"]["GITEA_MCP_PROFILE"], "prgs")
|
self.assertEqual(entry["env"]["GITEA_MCP_PROFILE"], "prgs")
|
||||||
|
self.assertTrue(
|
||||||
|
entry["env"]["GITEA_MCP_CLIENT_INSTANCE"].startswith("inst-"),
|
||||||
|
entry["env"]["GITEA_MCP_CLIENT_INSTANCE"],
|
||||||
|
)
|
||||||
blob = json.dumps(entry).lower()
|
blob = json.dumps(entry).lower()
|
||||||
for word in ("token", "password", "secret"):
|
for word in ("token", "password", "secret"):
|
||||||
self.assertNotIn(word, blob)
|
self.assertNotIn(word, blob)
|
||||||
|
|||||||
@@ -1049,6 +1049,386 @@ class ClientHintsTests(unittest.TestCase):
|
|||||||
self.assertEqual(hints["client_instance_id"], inst)
|
self.assertEqual(hints["client_instance_id"], inst)
|
||||||
self.assertEqual(hints["fleet_run_id"], "run-1")
|
self.assertEqual(hints["fleet_run_id"], "run-1")
|
||||||
|
|
||||||
|
def test_malformed_user_supplied_instance_not_trusted(self):
|
||||||
|
import gitea_mcp_server as server
|
||||||
|
|
||||||
|
with mock.patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"GITEA_MCP_CLIENT": "codex",
|
||||||
|
"GITEA_MCP_CLIENT_INSTANCE": "user-spoofed-not-launcher",
|
||||||
|
},
|
||||||
|
clear=False,
|
||||||
|
):
|
||||||
|
hints = server._client_identity_hints()
|
||||||
|
self.assertFalse(hints["instance_identity_trusted"])
|
||||||
|
self.assertEqual(hints["client_instance_id"], "user-spoofed-not-launcher")
|
||||||
|
|
||||||
|
|
||||||
|
class ProductionLauncherB1Tests(unittest.TestCase):
|
||||||
|
"""#978 review 655 B1: production launcher mints + propagates instance ID."""
|
||||||
|
|
||||||
|
def test_one_launch_shares_id_across_five_namespaces(self):
|
||||||
|
import mcp_application_launcher as launcher
|
||||||
|
|
||||||
|
profiles = {ns: f"prgs-{ns}" for ns in NAMESPACES}
|
||||||
|
built = launcher.build_application_mcp_servers(
|
||||||
|
profiles,
|
||||||
|
client_type="codex",
|
||||||
|
config_path="/cfg/profiles.json",
|
||||||
|
launch_nonce="launch-one",
|
||||||
|
now=NOW,
|
||||||
|
command="/venv/bin/python3",
|
||||||
|
args=["mcp_server.py"],
|
||||||
|
)
|
||||||
|
servers = built["mcpServers"]
|
||||||
|
self.assertEqual(len(servers), 5)
|
||||||
|
ids = {
|
||||||
|
servers[f"gitea-{ns}"]["env"]["GITEA_MCP_CLIENT_INSTANCE"]
|
||||||
|
for ns in NAMESPACES
|
||||||
|
}
|
||||||
|
self.assertEqual(len(ids), 1, ids)
|
||||||
|
shared = next(iter(ids))
|
||||||
|
self.assertTrue(fleet.assess_instance_identity(shared)["trusted"])
|
||||||
|
self.assertEqual(built["client_instance_id"], shared)
|
||||||
|
# No legacy placeholder on any production worker env.
|
||||||
|
for ns in NAMESPACES:
|
||||||
|
env = servers[f"gitea-{ns}"]["env"]
|
||||||
|
self.assertEqual(env["GITEA_MCP_CLIENT_INSTANCE"], shared)
|
||||||
|
self.assertEqual(
|
||||||
|
env["GITEA_MCP_INSTANCE_PROVENANCE"],
|
||||||
|
fleet.INSTANCE_ID_PROVENANCE_TRUSTED,
|
||||||
|
)
|
||||||
|
self.assertEqual(env["GITEA_MCP_CLIENT"], "codex")
|
||||||
|
self.assertEqual(env["GITEA_CLIENT_MANAGED"], "1")
|
||||||
|
self.assertFalse(env["GITEA_MCP_CLIENT_INSTANCE"].startswith("legacy-"))
|
||||||
|
|
||||||
|
proof = launcher.collect_instance_ids_from_mcp_servers(servers)
|
||||||
|
self.assertTrue(proof["shared_single_trusted_id"], proof)
|
||||||
|
|
||||||
|
def test_two_launches_receive_different_ids(self):
|
||||||
|
import mcp_application_launcher as launcher
|
||||||
|
|
||||||
|
profiles = {ns: f"prgs-{ns}" for ns in NAMESPACES}
|
||||||
|
a = launcher.build_application_mcp_servers(
|
||||||
|
profiles,
|
||||||
|
client_type="codex",
|
||||||
|
launch_nonce="L1",
|
||||||
|
now=NOW,
|
||||||
|
command="python3",
|
||||||
|
args=["mcp_server.py"],
|
||||||
|
)
|
||||||
|
b = launcher.build_application_mcp_servers(
|
||||||
|
profiles,
|
||||||
|
client_type="codex",
|
||||||
|
launch_nonce="L2",
|
||||||
|
now=NOW,
|
||||||
|
command="python3",
|
||||||
|
args=["mcp_server.py"],
|
||||||
|
)
|
||||||
|
self.assertNotEqual(a["client_instance_id"], b["client_instance_id"])
|
||||||
|
|
||||||
|
def test_resume_reuses_supplied_trusted_id(self):
|
||||||
|
import mcp_application_launcher as launcher
|
||||||
|
|
||||||
|
profiles = {ns: f"prgs-{ns}" for ns in NAMESPACES}
|
||||||
|
first = launcher.build_application_mcp_servers(
|
||||||
|
profiles,
|
||||||
|
client_type="claude_code",
|
||||||
|
launch_nonce="resume-src",
|
||||||
|
now=NOW,
|
||||||
|
command="python3",
|
||||||
|
args=["mcp_server.py"],
|
||||||
|
)
|
||||||
|
resumed = launcher.build_application_mcp_servers(
|
||||||
|
profiles,
|
||||||
|
client_type="claude_code",
|
||||||
|
client_instance_id=first["client_instance_id"],
|
||||||
|
command="python3",
|
||||||
|
args=["mcp_server.py"],
|
||||||
|
)
|
||||||
|
self.assertEqual(first["client_instance_id"], resumed["client_instance_id"])
|
||||||
|
|
||||||
|
def test_refuses_untrusted_instance_id_on_production_path(self):
|
||||||
|
import mcp_application_launcher as launcher
|
||||||
|
|
||||||
|
profiles = {ns: f"prgs-{ns}" for ns in NAMESPACES}
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
launcher.build_application_mcp_servers(
|
||||||
|
profiles,
|
||||||
|
client_type="codex",
|
||||||
|
client_instance_id="pid-999",
|
||||||
|
command="python3",
|
||||||
|
args=["mcp_server.py"],
|
||||||
|
)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
launcher.namespace_worker_env(
|
||||||
|
profile_name="prgs-author",
|
||||||
|
client_type="codex",
|
||||||
|
client_instance_id="user-supplied-garbage",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_extra_env_cannot_override_trusted_instance_keys(self):
|
||||||
|
import mcp_application_launcher as launcher
|
||||||
|
|
||||||
|
trusted = fleet.generate_client_instance_id(
|
||||||
|
"codex", launch_nonce="seal", now=NOW
|
||||||
|
)
|
||||||
|
env = launcher.namespace_worker_env(
|
||||||
|
profile_name="prgs-author",
|
||||||
|
client_type="codex",
|
||||||
|
client_instance_id=trusted,
|
||||||
|
extra_env={
|
||||||
|
"GITEA_MCP_CLIENT_INSTANCE": "inst-spoofed-20260730T000000Z-deadbeefdead",
|
||||||
|
"GITEA_MCP_INSTANCE_PROVENANCE": "trusted_launcher",
|
||||||
|
"GITEA_CLIENT_MANAGED": "0",
|
||||||
|
"GITEA_MCP_CLIENT": "gemini",
|
||||||
|
"GITEA_AUTHOR_WORKTREE": "/ok/extra",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(env["GITEA_MCP_CLIENT_INSTANCE"], trusted)
|
||||||
|
self.assertEqual(env["GITEA_CLIENT_MANAGED"], "1")
|
||||||
|
self.assertEqual(env["GITEA_MCP_CLIENT"], "codex")
|
||||||
|
self.assertEqual(env["GITEA_AUTHOR_WORKTREE"], "/ok/extra")
|
||||||
|
|
||||||
|
def test_gitea_config_launcher_entry_uses_production_path(self):
|
||||||
|
"""Production launcher_entry — not a test helper — mints trusted IDs."""
|
||||||
|
import gitea_config
|
||||||
|
|
||||||
|
entry_a = gitea_config.launcher_entry(
|
||||||
|
"prgs-author",
|
||||||
|
"/cfg/profiles.json",
|
||||||
|
client_type="codex",
|
||||||
|
launch_nonce="cfg-a",
|
||||||
|
)["gitea-tools"]
|
||||||
|
entry_b = gitea_config.launcher_entry(
|
||||||
|
"prgs-author",
|
||||||
|
"/cfg/profiles.json",
|
||||||
|
client_type="codex",
|
||||||
|
launch_nonce="cfg-b",
|
||||||
|
)["gitea-tools"]
|
||||||
|
id_a = entry_a["env"]["GITEA_MCP_CLIENT_INSTANCE"]
|
||||||
|
id_b = entry_b["env"]["GITEA_MCP_CLIENT_INSTANCE"]
|
||||||
|
self.assertNotEqual(id_a, id_b)
|
||||||
|
self.assertTrue(fleet.assess_instance_identity(id_a)["trusted"])
|
||||||
|
self.assertTrue(fleet.assess_instance_identity(id_b)["trusted"])
|
||||||
|
|
||||||
|
def test_multi_namespace_launcher_entries_api(self):
|
||||||
|
import gitea_config
|
||||||
|
|
||||||
|
profiles = {ns: f"prgs-{ns}" for ns in NAMESPACES}
|
||||||
|
built = gitea_config.multi_namespace_launcher_entries(
|
||||||
|
profiles,
|
||||||
|
client_type="grok",
|
||||||
|
config_path="/cfg/profiles.json",
|
||||||
|
launch_nonce="multi-api",
|
||||||
|
)
|
||||||
|
self.assertTrue(built["shared_instance_id_across_namespaces"])
|
||||||
|
self.assertEqual(built["namespace_count"], 5)
|
||||||
|
proof = __import__(
|
||||||
|
"mcp_application_launcher", fromlist=["*"]
|
||||||
|
).collect_instance_ids_from_mcp_servers(built["mcpServers"])
|
||||||
|
self.assertTrue(proof["shared_single_trusted_id"], proof)
|
||||||
|
|
||||||
|
def test_malformed_and_missing_fail_safely(self):
|
||||||
|
self.assertFalse(fleet.assess_instance_identity("")["trusted"])
|
||||||
|
self.assertFalse(fleet.assess_instance_identity(None)["trusted"])
|
||||||
|
self.assertFalse(fleet.assess_instance_identity("legacy-pid-1")["trusted"])
|
||||||
|
self.assertFalse(fleet.assess_instance_identity("not-an-inst-id")["trusted"])
|
||||||
|
self.assertFalse(fleet.assess_instance_identity("inst-")["trusted"])
|
||||||
|
self.assertFalse(fleet.assess_instance_identity("user-spoofed")["trusted"])
|
||||||
|
# Reserved inst- prefix from the launcher is trusted.
|
||||||
|
self.assertTrue(fleet.assess_instance_identity("inst-codex-1")["trusted"])
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceAwareMutationGateB2Tests(unittest.TestCase):
|
||||||
|
"""#978 review 655 B2: mutation gate is instance-aware, not profile-only."""
|
||||||
|
|
||||||
|
def _ps_lines(self, rows):
|
||||||
|
# rows: list of (pid, lstart, command) — command ignored beyond mcp_server.py
|
||||||
|
header = " PID LSTART COMMAND\n"
|
||||||
|
body = "".join(
|
||||||
|
f"{pid} {lstart} /path/to/python mcp_server.py\n"
|
||||||
|
for pid, lstart, _env in rows
|
||||||
|
)
|
||||||
|
return header + body
|
||||||
|
|
||||||
|
def _side_effect(self, rows, *, self_pid=1000):
|
||||||
|
ps_out = self._ps_lines(rows)
|
||||||
|
env_by_pid = {str(pid): env for pid, _ls, env in rows}
|
||||||
|
|
||||||
|
def side_effect(args, **kwargs):
|
||||||
|
mock_run = mock.MagicMock()
|
||||||
|
if args[0] == "ps" and "eww" in args:
|
||||||
|
mock_run.stdout = env_by_pid.get(str(args[2]), "")
|
||||||
|
return mock_run
|
||||||
|
if args[0] == "ps":
|
||||||
|
mock_run.stdout = ps_out
|
||||||
|
return mock_run
|
||||||
|
if args[0] == "git":
|
||||||
|
mock_run.stdout = "deadbeef"
|
||||||
|
return mock_run
|
||||||
|
raise ValueError(args)
|
||||||
|
|
||||||
|
return side_effect
|
||||||
|
|
||||||
|
def test_two_valid_instances_same_profile_allowed(self):
|
||||||
|
import gitea_mcp_server as server
|
||||||
|
|
||||||
|
inst_a = fleet.generate_client_instance_id(
|
||||||
|
"codex", launch_nonce="gate-a", now=NOW
|
||||||
|
)
|
||||||
|
inst_b = fleet.generate_client_instance_id(
|
||||||
|
"codex", launch_nonce="gate-b", now=NOW
|
||||||
|
)
|
||||||
|
lstart = "Wed Jul 8 15:00:00 2026"
|
||||||
|
rows = [
|
||||||
|
(
|
||||||
|
2001,
|
||||||
|
lstart,
|
||||||
|
(
|
||||||
|
f"GITEA_MCP_PROFILE=prgs-author GITEA_CLIENT_MANAGED=1 "
|
||||||
|
f"GITEA_MCP_CLIENT_INSTANCE={inst_a} "
|
||||||
|
f"GITEA_MCP_WORKER_IDENTITY=worker-a "
|
||||||
|
f"GITEA_MCP_GENERATION_ID=gen-a"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
2002,
|
||||||
|
lstart,
|
||||||
|
(
|
||||||
|
f"GITEA_MCP_PROFILE=prgs-author GITEA_CLIENT_MANAGED=1 "
|
||||||
|
f"GITEA_MCP_CLIENT_INSTANCE={inst_b} "
|
||||||
|
f"GITEA_MCP_WORKER_IDENTITY=worker-b "
|
||||||
|
f"GITEA_MCP_GENERATION_ID=gen-b"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
with mock.patch("subprocess.run") as mock_run, mock.patch(
|
||||||
|
"os.path.getmtime", return_value=datetime(2026, 7, 8, 11, 0, 0).timestamp()
|
||||||
|
), mock.patch("os.path.exists", return_value=True), mock.patch(
|
||||||
|
"os.getpid", return_value=9999
|
||||||
|
):
|
||||||
|
mock_run.side_effect = self._side_effect(rows)
|
||||||
|
reasons = server._check_mcp_runtimes_diagnostics(
|
||||||
|
"create_issue", ["prgs-author"]
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
any("Duplicate MCP server process(es) detected" in r for r in reasons),
|
||||||
|
reasons,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_duplicate_same_instance_and_namespace_blocked(self):
|
||||||
|
import gitea_mcp_server as server
|
||||||
|
|
||||||
|
inst = fleet.generate_client_instance_id(
|
||||||
|
"codex", launch_nonce="dup", now=NOW
|
||||||
|
)
|
||||||
|
lstart = "Wed Jul 8 15:00:00 2026"
|
||||||
|
rows = [
|
||||||
|
(
|
||||||
|
3001,
|
||||||
|
lstart,
|
||||||
|
(
|
||||||
|
f"GITEA_MCP_PROFILE=prgs-author GITEA_CLIENT_MANAGED=1 "
|
||||||
|
f"GITEA_MCP_CLIENT_INSTANCE={inst} "
|
||||||
|
f"GITEA_MCP_WORKER_IDENTITY=worker-1 "
|
||||||
|
f"GITEA_MCP_GENERATION_ID=gen-1"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
3002,
|
||||||
|
lstart,
|
||||||
|
(
|
||||||
|
f"GITEA_MCP_PROFILE=prgs-author GITEA_CLIENT_MANAGED=1 "
|
||||||
|
f"GITEA_MCP_CLIENT_INSTANCE={inst} "
|
||||||
|
f"GITEA_MCP_WORKER_IDENTITY=worker-2 "
|
||||||
|
f"GITEA_MCP_GENERATION_ID=gen-2"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
with mock.patch("subprocess.run") as mock_run, mock.patch(
|
||||||
|
"os.path.getmtime", return_value=datetime(2026, 7, 8, 11, 0, 0).timestamp()
|
||||||
|
), mock.patch("os.path.exists", return_value=True), mock.patch(
|
||||||
|
"os.getpid", return_value=9999
|
||||||
|
):
|
||||||
|
mock_run.side_effect = self._side_effect(rows)
|
||||||
|
reasons = server._check_mcp_runtimes_diagnostics(
|
||||||
|
"create_issue", ["prgs-author"]
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
any("Duplicate MCP server process(es) detected" in r for r in reasons),
|
||||||
|
reasons,
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
any("duplicate namespace" in r.lower() or "client_instance_id" in r for r in reasons),
|
||||||
|
reasons,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_missing_instance_evidence_still_fail_closed(self):
|
||||||
|
"""Two profile-sharing processes without trusted IDs remain blocked."""
|
||||||
|
import gitea_mcp_server as server
|
||||||
|
|
||||||
|
lstart = "Wed Jul 8 15:00:00 2026"
|
||||||
|
rows = [
|
||||||
|
(
|
||||||
|
4001,
|
||||||
|
lstart,
|
||||||
|
"GITEA_MCP_PROFILE=prgs-author GITEA_CLIENT_MANAGED=1",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
4002,
|
||||||
|
lstart,
|
||||||
|
"GITEA_MCP_PROFILE=prgs-author GITEA_CLIENT_MANAGED=1",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
with mock.patch("subprocess.run") as mock_run, mock.patch(
|
||||||
|
"os.path.getmtime", return_value=datetime(2026, 7, 8, 11, 0, 0).timestamp()
|
||||||
|
), mock.patch("os.path.exists", return_value=True), mock.patch(
|
||||||
|
"os.getpid", return_value=9999
|
||||||
|
):
|
||||||
|
mock_run.side_effect = self._side_effect(rows)
|
||||||
|
reasons = server._check_mcp_runtimes_diagnostics(
|
||||||
|
"create_issue", ["prgs-author"]
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
any("Duplicate MCP server process(es) detected" in r for r in reasons),
|
||||||
|
reasons,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_single_instance_still_ok(self):
|
||||||
|
import gitea_mcp_server as server
|
||||||
|
|
||||||
|
inst = fleet.generate_client_instance_id(
|
||||||
|
"codex", launch_nonce="single", now=NOW
|
||||||
|
)
|
||||||
|
lstart = "Wed Jul 8 15:00:00 2026"
|
||||||
|
rows = [
|
||||||
|
(
|
||||||
|
5001,
|
||||||
|
lstart,
|
||||||
|
(
|
||||||
|
f"GITEA_MCP_PROFILE=prgs-author GITEA_CLIENT_MANAGED=1 "
|
||||||
|
f"GITEA_MCP_CLIENT_INSTANCE={inst} "
|
||||||
|
f"GITEA_MCP_WORKER_IDENTITY=worker-only "
|
||||||
|
f"GITEA_MCP_GENERATION_ID=gen-only"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
with mock.patch("subprocess.run") as mock_run, mock.patch(
|
||||||
|
"os.path.getmtime", return_value=datetime(2026, 7, 8, 11, 0, 0).timestamp()
|
||||||
|
), mock.patch("os.path.exists", return_value=True), mock.patch(
|
||||||
|
"os.getpid", return_value=9999
|
||||||
|
):
|
||||||
|
mock_run.side_effect = self._side_effect(rows)
|
||||||
|
reasons = server._check_mcp_runtimes_diagnostics(
|
||||||
|
"create_issue", ["prgs-author"]
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
any("Duplicate MCP server process(es) detected" in r for r in reasons),
|
||||||
|
reasons,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user