feat(launcher): trusted client-instance identity for project-scoped launches (Closes #985) #986
Open
jcwalker3
wants to merge 1 commits from
feat/issue-985-project-scoped-launcher-identity into master
pull from: feat/issue-985-project-scoped-launcher-identity
merge into: :master
:master
:fix/issue-987-native-mcp-bootstrap
:feat/issue-985-project-scoped-launcher-identity
:fix/issue-983-cross-repo-base-ref
:feat/issue-980-stale-worker-retirement
:fix/issue-975-client-identity-heartbeat
:fix/issue-973-cross-repo-canonical-roots
:fix/issue-970-safely-resolve-missing-worktrees
:fix/issue-969-native-mcp-bootstrap
:feat/issue-664-break-glass-restart
:feat/issue-708-mcp-namespace-attachment
:feat/issue-665-restart-audit
:fix/issue-700-durable-walls
:fix/issue-704-prevent-env-workspace-bindings
:feat/issue-707-cross-project-boundaries
:fix/issue-690-review-profile-switch-guard
:fix/issue-953-bootstrap-lock-provenance
:feat/issue-949-native-fleet-inventory
:fix/issue-943-runtime-context-helpers
:fix/issue-945-owning-pr-renewal-evidence
:fix/issue-941-scope-guard-bootstrap-wiring
:docs/issue-930-remote-mcp-coupling-inventory
:fix/issue-892-author-bootstrap-deadlock
:fix/issue-686-detect-reject-manual-mcp
:fix/issue-672-mcp-config-drift
:fix/issue-689-deterministic-mcp-namespace
:feat/issue-666-concurrent-mcp-restart-tests
:feat/issue-659-maintenance-drain-mode
:feat/issue-648-notifications-console
:fix/issue-670-direct-master-incident
:feat/issue-644-console-recovery
:feat/issue-650-providers-insights
:feat/issue-669-scoped-component-recovery
:docs/issue-668-mcp-ha-rolling-restart
:feat/issue-667-console-restart-controls
:feat/issue-645-linkage-console
:feat/issue-643-request-preview-initiate
:fix/issue-897-permission-stale-runtime-classification
:feat/issue-641-runtime-session-view
:feat/issue-663-restart-classes
:feat/issue-661-drain-proof-hard-gate
:fix/issue-854-semantic-container-exclusion
:issue-640
:fix/issue-682-starlette-httpx2
Labels
Clear labels
allocator
anti-stomp
architecture
bug
chore
codex
concurrency
contamination
control-plane
dashboard
database
design
documentation
enhancement
gitea
glitchtip
important
incident
incident-bridge
integration
jenkins
labels
leases
mcp
mcp-health
mcp-menu
multi-project
mutating
nice-to-have
observability
portability
preflight
protected-branch
queue
read-only
reconnect
recovery
refactor
release
reliability
resumable-review
reviewer
roadmap
safety
security
self-hosted
sentry
stale-runtime
status:blocked
status:in-progress
status:pr-open
status:ready
terminal-lock
testing
tracker
type:bug
type:feature
type:feature
type:guardrail
visibility
workflow
workflow-hardening
workflow-hardening
Controller-owned work allocator
Prevent concurrent LLM session stomping
Architecture / structural design
OpenAI Codex client / workflow session surface
Concurrent session safety
Workflow or session contamination incident
MCP control-plane coordination and allocation authority
MCP operational dashboard/queue view
Internal coordination storage (SQLite/Postgres)
Design / investigation, no implementation
Docs / runbooks
New feature or improvement
Gitea MCP workflow
GlitchTip integration
Operational or process incident requiring durable audit trail
Sentry-to-Gitea incident bridging
Integration testing
Jenkins integration
Label taxonomy management
Lease adopt/release/expire lifecycle
MCP server / tooling
MCP namespace and runtime health
MCP menu surface
Work spanning multiple monitoring projects or Gitea repos
Mutating action; requires gating
Observability, metrics, traces, error reporting
Cross-platform / portability
Shared preflight gates before mutation
Protected branch / stable-branch policy concern
Work queue visibility and allocation
Read-only, no mutation
MCP client reconnect/reload recovery path
Recovery paths for stale/foreign leases
Code refactor / restructure
Release / versioning
Reliability / failure handling
Persist and resume prepared review verdicts across sessions
Reviewer workflow tooling
Roadmap / umbrella issue
Safety rails and fail-closed mutation guards
Security / trust boundary
Self-hosted infrastructure integration
Sentry error monitoring integration
Stale backend daemon / runtime-vs-master parity failures
Issue is blocked
Issue is being worked on
Issue has an open pull request
Issue is ready for work
Terminal review lock (#332) path
Tests / test coverage
Issue tracker hygiene / meta
Bug or defect
Feature or enhancement
Feature or enhancement
Safety gate or guardrail
Workflow state visibility for LLMs/operators
Cross-tool workflow
LLM workflow coordination hardening
LLM workflow coordination hardening
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: Scaled-Tech-Consulting/Gitea-Tools#986
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Implements #985. The launcher could already mint one trusted
GITEA_MCP_CLIENT_INSTANCEper launch, but two gaps meant real launches neverwent through it and fell back to untrusted
legacy-pid-*identities:build_application_mcp_serversdemanded a profile for all five sanctionednamespaces, so a project-scoped configuration exposing only author,
reviewer, and merger could not use it without inventing controller and
reconciler profiles that must not exist.
so every real launch bypassed it entirely.
Changes
Validated namespace subset
resolve_launch_namespaces()is an allow-list, not a filter: unknown,duplicate, and empty selections are refused rather than quietly shrinking a
launch to fewer workers than intended. Omitting it resolves to all five, so
every existing caller is untouched.
build_application_mcp_servers()takes that subset, requires profiles only forthe launched namespaces, starts only those workers, and returns
excluded_namespaces/project_scopedso a caller can prove a controller orreconciler worker was never started.
collect_instance_ids_from_mcp_servers()previously hardcoded five namespaceservers, so a three-namespace launch would have reported two absent workers and
failed its own attribution proof. It now inspects the launch's own namespaces
and reports
missing_servers.Runnable launch path
Mints one trusted ID, writes a per-launch
0600mcpServersconfig, and execsclaude --mcp-config <per-launch.json> --strict-mcp-config.--dry-runprintsthe plan and starts nothing.
CLIENT_LAUNCH_SPECSis a data-driven registry, so adding another supportedclient is one entry; because the argv builder only ever receives a config this
module wrote, every client necessarily goes through one mint-once,
propagate-to-all mechanism.
The config is per-launch and never
.mcp.json: persisting a trusted ID intoa shared reused file would hand two concurrent sessions one identity, which is
precisely the reuse case the duplicate gate must reject.
Provenance sealing now fails closed
The
inst-…format is public and reproducible, so format alone could let anyoneable to set one environment variable hand-write a valid-looking ID and be
believed. Trust now additionally requires
GITEA_MCP_INSTANCE_PROVENANCE=trusted_launcher, which only the launcherwrites. A well-formed but unsealed value is classified
unsealed_launcherandrefused, while still being reported for diagnosis. Manually asserted trust is no
longer possible.
Deliberate contract change — reviewer please confirm
tests/test_issue_978_instance_fleet_snapshot.py::test_client_hints_trusted_when_setasserted that a well-formed ID alone was trusted. That is the behaviour #985
tightens, so the test now supplies the launcher seal, and a new companion test
asserts the unsealed case fails closed. No assertion was weakened and no test
was skipped; the contract got stricter. This is the one intentional behavioural
change to pre-existing covered behaviour and is the thing most worth a second
opinion.
Tests
tests/test_issue_985_project_scoped_launcher.py— 47 passed, 3 subtestspassed. Covers subset validation (canonical ordering, case normalisation,
unknown/typo/empty/duplicate refusal), the three-namespace launch, excluded
namespaces not started, shared trusted ID across all workers of one launch,
per-worker sealing, concurrent launches receiving distinct IDs, two launches on
one profile staying distinguishable, no static per-project ID, fail-closed for
missing / legacy
pid-*/ malformed / unsealed / wrong-marker / conflicting /absent-worker identities, resume reuse versus independent launches,
five-namespace backward compatibility, the
gitea_configwrapper in both modes,argv construction, per-launch
0600config files, and the CLI including itsrefusals.
Full suite, both runs from
branches/worktrees:32ab8392commover the sortedFAILEDlines reports no line on either side — thefailing sets are byte-identical. Zero regressions introduced, zero pre-existing
failures masked. The +48 passing delta is 47 new #985 tests plus the new #978
unsealed-case test. The 28 baseline failures reproduce on unmodified master and
fall outside this scope.
Scope
Six files:
mcp_application_launcher.py,mcp_fleet_snapshot.py(one newprovenance constant),
gitea_config.py(one pass-through keyword),docs/instance-fleet-identity.md, the #978 test, and the new #985 test. Noduplicate-worker or duplicate-cohort detection was weakened, no fleet or
mutation gate relaxed, no credential, profile, session, or running process
touched, and no static instance ID introduced anywhere.
Reviewer focus
The sealing fail-closed change and its blast radius through
_client_identity_hints; whetherresolve_launch_namespacesshould refuseanything it currently accepts; and whether per-launch temp configs need an
explicit cleanup owner in long-lived sessions.
Closes #985
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #986
issue: #985
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 15889-ffe3bfa44c10
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr986-7e19079b
phase: claimed
candidate_head:
7e19079b5ctarget_branch: master
target_branch_sha:
32ab839289last_activity: 2026-07-31T09:52:58Z
expires_at: 2026-07-31T10:02:58Z
blocker: none
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #986
issue: #985
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 15889-0c398b0ebfd3
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr986-7e19079b
phase: claimed
candidate_head:
7e19079b5ctarget_branch: master
target_branch_sha:
32ab839289last_activity: 2026-07-31T10:24:50Z
expires_at: 2026-07-31T10:34:50Z
blocker: none
Canonical PR State
STATE: Reviewed at head 7e19079b5cc6b029aed729945c91e9edc5a1c721; REQUEST_CHANGES submitted with three blocking findings B1, B2, B3.
WHO_IS_NEXT: author
NEXT_ACTION: Remediate B1, B2, and B3 on branch feat/issue-985-project-scoped-launcher-identity, add the three named regression tests, rerun the full suite against master baseline
32ab839289, and post an author handoff comment for re-review.NEXT_PROMPT: Author remediation prompt for PR #986 follows in the block below.
WHAT_HAPPENED: Independent review at the pinned head. Diff is six files on a single commit with merge-base exactly
32ab839289and no unrelated, generated, or accidental changes. The namespace-subset allow-list, project-scoped launches, mint-once/propagate-to-all identity, per-launch 0600 configs, argv construction, and the tightened #978 seal contract were all verified correct. Three blocking defects remain, all rooted in the launcher seal not being applied consistently across every trust-decision site.WHY: The PR's central security claim is that an inst- identity is trusted only when launcher-sealed, and docs/instance-fleet-identity.md added here states that manually asserted trust is not possible. That claim holds for a worker's own identity but not at the duplicate-cohort gate, which still accepts unsealed identities as trusted. Issue #985 explicitly requires not permitting manually asserted trust and not weakening duplicate-worker detection, so shipping the documented claim while the gate remains permissive is a blocking correctness and security gap.
ISSUE: 985
HEAD_SHA:
7e19079b5cREVIEW_STATUS: REQUEST_CHANGES
MERGE_READY: no
BLOCKERS: B1 peer scan at gitea_mcp_server.py:22447 grants trusted attribution to unsealed inst- identities, defeating duplicate-cohort detection; B2 --dry-run writes a per-launch 0600 config at mcp_application_launcher.py:595 that nothing removes; B3 collect_instance_ids_from_mcp_servers at mcp_application_launcher.py:415-419 omits the seal from its trusted count, so the documented post-launch verification can report green for a launch that fails closed at runtime.
VALIDATION: tests/test_issue_985_project_scoped_launcher.py 47 passed 3 subtests; tests/test_issue_978_instance_fleet_snapshot.py plus tests/test_config_menu.py 84 passed; clear_provenance, 686, 948, 975, duplicate_gate, lock_provenance, work_duplicate_gate 142 passed 22 subtests; full suite at
7e19079b28 failed 6252 passed 6 skipped 1109 subtests; full suite independently rerun at master32ab839228 failed 6204 passed 6 skipped 1106 subtests; comm over sorted FAILED sets empty both directions, so zero regressions and zero masked failures.LAST_UPDATED_BY: sysadmin (prgs-reviewer)
REQUEST_CHANGES — PR #986 (Issue #985) at
7e19079b5cc6b029aed729945c91e9edc5a1c721Reviewer
sysadmin/ profileprgs-reviewer, independent of authorjcwalker3. Basemasterat32ab839289e6. Reviewed in a dedicated detached worktree at the pinned head.Deliberate contract change — confirmed intended and strengthening
Confirmed.
test_client_hints_trusted_when_setnow supplies the launcher seal and additionally assertsinstance_launcher_sealed, and the newtest_client_hints_wellformed_but_unsealed_not_trustedgives explicit negative coverage for a well-formed but unsealed identity (untrusted, unsealed,provenance == unsealed_launcher, still reported for diagnosis). No assertion was weakened, nothing was skipped, and the revised assertion strengthens the trust boundary rather than hiding a regression. This is the correct #985 security contract.Verified correct
resolve_launch_namespaces()is a genuine allow-list — unknown, duplicate, and empty selections raise; canonical ordering and case normalisation hold;Nonepreserves whole-fleet behaviour.excluded_namespaces/project_scoped.namespace_worker_env();extra_envcannot override the protected instance, provenance, client, and managed keys. Separate launches receive distinct IDs.0600viaos.fchmodon the open fd, is never.mcp.json, and uses a distinct file per launch. Failure paths refuse before writing anything, since thebuild_client_launch_argvprobe precedeswrite_launch_config.CLIENT_LAUNCH_SPECSbuilds argv from a fixed literal command with no shell involvement — no injection surface, no client-specific ambiguity. The config env carries no credentials; tokens remain in the keychain.--mcp-config <per-launch.json> --strict-mcp-configmatches the documented behaviour.GITEA_MCP_CLIENT_INSTANCEandGITEA_MCP_INSTANCE_PROVENANCEwere already inRECOGNIZED_GITEA_ENV_KEYSat master, so this introduces no new unrecognized key and does not reintroduce the PR #976 fleet-wide wall._client_identity_hints()already routes throughinherit_or_refuse_client_instance(), so the new seal requirement does reach the worker's own identity without touchinggitea_mcp_server.py.B1 — the launcher seal is not enforced at the fleet / duplicate-cohort gate
Location:
gitea_mcp_server.py:22447, inside_check_mcp_runtimes_diagnostics().Failure path. The peer scan classifies other processes with
mcp_fleet_snapshot.assess_instance_identity(peer_instance_raw), which this PR left seal-unaware —INSTANCE_ID_PROVENANCE_UNSEALEDis defined atmcp_fleet_snapshot.py:90but never used in that module. Verified directly against the module: a well-formedinst-…value with no seal returnstrusted: True, provenance: trusted_launcher. Two client-managed processes sharing one profile, each exporting a distinct hand-writteninst-…value and no seal, therefore land in separate buckets at line 22491, hitlen(group) <= 1 -> continueat 22500, and no duplicate-namespace-worker reason is emitted — including the branch at 22543 whose whole purpose is to tell the operator to relaunch through the production launcher.peer_envalready capturesGITEA_MCP_INSTANCE_PROVENANCEthrough the regex at 22428, so the marker is available and simply not consulted.Impact. Two trust classifiers now disagree on identical input, and the permissive one guards the fleet gate: a worker reports itself untrusted while peers scanning it read it as trusted. Setting one environment variable to a public, reproducible format still buys trusted attribution at the duplicate-cohort gate. This contradicts the Issue #985 requirement not to permit manually asserted trust, and contradicts the sentence this PR adds to
docs/instance-fleet-identity.md.Required remediation. Route the peer assessment through a seal-aware path — either
inherit_or_refuse_client_instance(peer_env)or a seal-checking helper inmcp_fleet_snapshotused at both sites — so an unsealed peer falls into__untrusted_or_missing__and stays subject to duplicate detection.Regression test to add. Two simulated peers on one profile with distinct well-formed but unsealed
inst-…values must still raise the duplicate-MCP-process reason; the same pair with valid seals must not.B2 —
--dry-runwrites a per-launch config that nothing ever removesLocation:
mcp_application_launcher.py:595(prepare_application_launch) versus the dry-run branch at line 730 (main).Failure path.
prepare_application_launchcallswrite_launch_config()unconditionally, beforemaininspects--dry-run. A run documented as printing the plan and starting nothing therefore leaves a0600file holding the minted identity and itstrusted_launcherseal, with no component owning removal. The PR's own tests show this:tests/test_issue_985_project_scoped_launcher.py:582and:659registeraddCleanup(... os.unlink(path))against files produced by dry runs, so the tests compensate for production behaviour. Repeated dry runs accumulate indefinitely.Impact. Lifecycle and cleanup defect rather than a credential leak — the file is owner-only and holds no tokens. It does leave reusable sealed launch configs on disk, in tension with the issue's requirement to avoid persistent per-project instance IDs.
Required remediation. Do not write a config on the dry-run path, or unlink before returning. For the exec path, document the cleanup owner, since
os.execvpmakes post-launch cleanup impossible from this process.Regression test to add. A successful
--dry-runleaves no file at the reportedlaunch_config_path, asserted without test-side cleanup.B3 — the documented post-launch verification helper also ignores the seal
Location:
mcp_application_launcher.py:415-419incollect_instance_ids_from_mcp_servers().Failure path. The
trustedlist is computed withfleet.assess_instance_identity(i)over config values only, so a config whose entries carry validinst-…values but lackGITEA_MCP_INSTANCE_PROVENANCEreportsshared_single_trusted_id: Truewhile the workers it starts will refuse trusted attribution at runtime.docs/instance-fleet-identity.mdnames this call as the post-launch verification step, so an operator following the new documentation can get a green result for a launch that fails closed. Separately, withnamespaces=Nonethe expected set is derived from whichever servers are present, somissing_serverscan never be non-empty in that mode.Impact. The verification helper can disagree with the runtime gate in the optimistic direction, which is the wrong direction for a proof used to confirm fleet safety.
Required remediation. Include the per-entry
env[INSTANCE_PROVENANCE_ENV] == LAUNCHER_PROVENANCE_VALUEcheck in the trusted count, and have the docs recommend the explicitnamespaces=[...]form somissing_serversis meaningful.Regression test to add. A built config with valid instance IDs whose provenance key has been stripped reports
shared_single_trusted_id: False.Non-blocking observations
main()treats--namespaces ""as absent, so an empty string silently resolves to all five rather than being refused; it then fails later with a missing-profile error.resolve_launch_namespaces([])correctly raises, so this is a CLI-edge inconsistency only.mint_application_launch()returns a hardcoded five-entrynamespaceslist; harmless becausebuild_application_mcp_serversoverwrites it at line 270, but misleading if called directly.Summary
The namespace-subset work, the runnable launch path, mint-once/propagate-to-all, per-launch
0600configs, and the tightened #978 contract are correct and well covered. The blocker is that the PR's central security claim is implemented at one of the two trust-decision sites, and the site it misses is the duplicate-cohort gate, while the documentation added here states the property holds unconditionally. Close B1, B2, and B3 and this is ready for another look.No code was changed and no merge was performed during this review.
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #986
issue: #985
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 15889-0c398b0ebfd3
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr986-7e19079b
phase: released
candidate_head:
7e19079b5ctarget_branch: master
target_branch_sha:
32ab839289last_activity: 2026-07-31T10:29:24Z
expires_at: 2026-07-31T10:39:24Z
blocker: manual-release
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.