1 Commits

Author SHA1 Message Date
sysadmin df840855cf docs: update safety and boundary docs for Jenkins/GlitchTip (#79) 2026-07-02 14:34:31 -04:00
11 changed files with 2 additions and 1007 deletions
-41
View File
@@ -1,41 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
## [v1.1.0] - 2026-07-02
### Added
- Read-only identity and eligibility tooling: `gitea_whoami` authenticated-user lookup (#11), `gitea_get_profile` runtime-profile discovery (#13), and `gitea_check_pr_eligibility` fail-closed PR eligibility checks (#14).
- Identity lookup aliases (`gitea_get_authenticated_user` and `gitea_get_current_user`) for common MCP/LLM tool discovery (#9).
- Gated PR review actions (`gitea_submit_pr_review`) reusing the eligibility gates (#15).
- Gated PR merge workflow (`gitea_merge_pr`) with explicit `MERGE PR <n>` confirmation, head-SHA and changed-file pinning, and self-merge blocking as the only merge path (#16).
- Task-scoped Gitea MCP execution profiles: documented profile model (#12) and runtime profiles via environment config with `allowed_operations` (#19).
- Audit logging for all mutating MCP actions with execution-profile metadata and secret redaction (#18).
- Shared API pagination (`api_get_all`) and hardened failure handling in `gitea_auth.api_request`: request timeouts, clear network/DNS errors, explicit 502/503/504 upstream errors, malformed-JSON handling, and redacted error text (#67).
- `scripts/release-tag` SemVer-gated annotated-tag helper (safe-by-default, master-only, tests required) (#50).
- Automatic `status:in-progress` release on issue close and PR close/merge (#56, #58).
- `LLM-Agent-SHA` opaque agent attribution convention (Phase 0): documentation, handoff/review templates, and negative tests proving the SHA can never bypass self-review/self-merge gates (#86).
- macOS `com.apple.provenance` cleanup helper tool and documentation (#3).
- `manage_labels.py` refactored into reusable modes (`--create-labels`, `--apply-mapping`, `--add-label`) (#6).
### Changed
- HTTP 429 responses now honor `Retry-After` with jittered exponential backoff (#27).
- Read-only list tools (`gitea_list_issues`, `gitea_list_prs`, `gitea_list_labels`) now paginate across pages with bounded page caps (#67).
- Automatic `status:in-progress` cleanup on issue/PR close and merge.
- Label cleanup now utilizes safe targeted label deletion behavior rather than replacing the entire label set.
### Documentation
- MCP security model and trust-boundary documentation (#8).
- Developer testing guidelines (#70).
- Jenkins read-only build-status tools design (#72).
- Jenkins repo/branch/PR → job mapping design (#77).
- Safety and boundary docs updated for Jenkins/GlitchTip: `glitchtip-mcp` boundary, read-only-first policy, mutation gating (#79).
- Proposed label taxonomy for Jenkins/GlitchTip workflows (#80).
- GlitchTip read-only error/event tools design (#73).
- Multi-service MCP profile model extension (#76).
## [v1.0.1]
- Fix Recent Timesheets Remove button text clipping and copy theme/whats_new in build.
## [v1.0.0]
- Initial versioned release.
@@ -1,174 +0,0 @@
# GlitchTip Read-Only Error/Event Tools — Design Notes
- **Status:** Design (implementation-ready notes; **no implementation in this repo**)
- **Issue:** #73 (umbrella: #75; boundary decision: ADR-0001, #71)
- **Related:** #74 (GlitchTip→Gitea filing workflow — composes these read tools),
#78 (dedup/linking, child of #74), #76 (per-service profile schema)
- **Date:** 2026-07-02
## 1. Purpose and scope
Define the minimum **read-only** GlitchTip MCP tool set that lets an LLM answer:
*"What unresolved errors does project X have (by environment/release), and what
is this specific error?"* — with privacy-safe output suitable for LLM context,
issue bodies, and audit logs.
Strictly read-only, per ADR-0001:
- **No mutation tools** — no resolving/ignoring/assigning issues, no comment
posting, no project/team/key administration, no deletes.
- **No automatic GlitchTip→Gitea filing** (that is #74's *orchestrated,
explicitly-invoked* workflow; it composes these read tools and Gitea write
tools — never one dual-credential server).
- **This server never holds Gitea write credentials.**
## 2. Boundary placement (namespace pending)
These tools belong to the GlitchTip observability boundary of the MCP Control
Plane — `glitchtip-mcp` (ADR-0001's recommendation), `observability-mcp`, or
folded into `ops-mcp`. **ADR-0001 open owner decision #2 picks the name; this
design does not assume it.** Tool names below use the `glitchtip_` prefix for
readability and rename mechanically with the decision.
Fixed regardless of the name (per `tool-boundaries.md`,
`credential-isolation.md`):
- Own server process, own `.env`, GlitchTip credentials only.
- No Gitea, Jenkins, or Ops tokens in this runtime; no GlitchTip token
anywhere else.
## 3. API surface note (Sentry compatibility)
GlitchTip implements a Sentry-compatible REST API (`/api/0/...` — organizations,
projects, issues, events). The design targets **GlitchTip's documented subset**
only; Sentry-only endpoints must not be assumed. The implementation should pin
against a tested GlitchTip version and treat missing endpoints/fields as
degraded-but-safe (omit field, never crash).
## 4. Minimum read-only tool set
| Tool | Purpose |
|---|---|
| `glitchtip_whoami` | Verify authenticated identity + active profile (mirror of `gitea_whoami`; fail-closed identity proof) |
| `glitchtip_list_projects` | Projects visible to the token (org-scoped), with pagination bounds |
| `glitchtip_list_unresolved` | Unresolved issues for a project, filterable (§6), sorted by last-seen |
| `glitchtip_get_issue` | Safe detail of one issue (fields §5) |
| `glitchtip_recent_events` | Recent events for an issue (summaries only, §5) |
| `glitchtip_search` | Issue search within a project (query + filters §6) |
All tools are `GET`-only. No tool issues PUT/POST/DELETE.
## 5. Privacy: field-level allowlist (the core rule)
Error events routinely contain PII and secrets (request bodies, cookies,
headers, tokens, user emails/IPs, local variables). Therefore: **allowlist
projection only — raw event/issue payloads are never passed through.**
### Issue-level safe fields (`glitchtip_list_unresolved`, `glitchtip_get_issue`, `glitchtip_search`)
| Field | Notes |
|---|---|
| `issue_id` | GlitchTip issue ID (dedup key for #78) |
| `fingerprint` | When available (dedup key for #78) |
| `title` / `culprit` | Error type + short message/transaction — redactor-passed |
| `project` | Slug |
| `level` | error/warning/… |
| `status` | unresolved/… |
| `environment` | When filtered/available |
| `release` | Version string |
| `first_seen` / `last_seen` | ISO-8601 UTC |
| `event_count` / `user_count` | Numbers only — never user identities |
| `permalink` | GlitchTip web URL (the "link, not dump" principle) |
### Event-level safe fields (`glitchtip_recent_events`)
`event_id`, `timestamp`, `level`, `environment`, `release`, redactor-passed
`message`, and a **stack summary** only: top N (default 5) frames as
`module/filename:function:line` — in-app frames preferred.
### Redact / omit — never returned
Request headers; cookies; auth/session fields; user emails, usernames, IPs;
request/form bodies; query strings; local variables; full raw stack frames
(source context lines); SDK/device metadata beyond platform name; breadcrumbs;
any `extra`/`context` blobs.
Full raw frames or request context require a **separate, explicitly approved**
operation (`glitchtip.event.read_raw`) that is absent from default profiles —
same pattern as `jenkins.console.read` in the #72 design. Even then, output
passes the shared secret redactor; redaction failure ⇒ error, never raw text.
**Default output = fingerprint / release / summary + permalink.** The
permalink carries the human to the full data in GlitchTip's own UI, where its
access control applies — the MCP layer does not re-serve raw payloads.
## 6. Filtering and pagination
Filters (all optional, combinable): `project` (required for issue/event
queries), `environment`, `release`, `fingerprint`, free-text `query`
(GlitchTip search syntax, e.g. `is:unresolved`).
Pagination: cursor-based per the API. Bounds: per-page cap 50; default overall
cap 100 items; hard cap `max_pages` (default 10) against runaway loops —
mirroring `gitea_auth.api_get_all`. Truncation is **explicit** in the return
(`"truncated": true`) — never silent.
## 7. Credentials and profile requirements
Per-service profile model (`gitea-execution-profiles.md`, extended by #76):
- Env/config: `GLITCHTIP_URL`, `GLITCHTIP_ORG`, `GLITCHTIP_TOKEN_SOURCE_NAME`
(secret **name** only; value resolved at runtime, never logged/committed).
- Profile: e.g. `glitchtip-readonly` with namespaced
`allowed_operations: ["glitchtip.read", "glitchtip.event.read"]`
(+ `glitchtip.event.read_raw` only with explicit approval);
`forbidden_operations: ["glitchtip.issue.mutate", "glitchtip.admin"]`
belt-and-braces though no mutating tool exists.
- Missing URL/org/token/profile ⇒ **fail closed** before any network call.
- Read-only ⇒ no confirmation gates; identity (`glitchtip_whoami`) must work so
workflows can prove which account they read as.
## 8. Failure behavior (fail closed, clear, safe)
| Condition | Behavior |
|---|---|
| Unknown project/issue | Explicit `{"found": false, ...}` — no fuzzy matching |
| GlitchTip unreachable (DNS/timeout) | `"network error contacting GlitchTip: <redacted reason>"` — mirror `gitea_auth.api_request` conversion |
| 502/503/504 | "GlitchTip upstream unavailable" |
| 401/403 | "GlitchTip auth failed / insufficient permissions" — no credential echo |
| 429 | Honor Retry-After with capped jittered backoff (as `gitea_auth`) |
| Malformed JSON | "malformed JSON response from GlitchTip" — no raw-body dump |
| Missing profile/creds | Fail closed before any network call (§7) |
All error text passes the shared secret redactor.
## 9. Testing strategy (mocked; for the implementing package)
Mocked-GlitchTip unit tests only, per `docs/developer-testing-guidelines.md`:
- Assert method is always `GET`; URL/filter/cursor shape correct.
- **Projection tests:** response fixtures containing emails, IPs, cookies,
headers, request bodies, locals, full frames ⇒ none appear in output
(explicit negative assertions per §5's redact list).
- Stack summary: top-N frame cap enforced; source-context lines absent.
- Pagination: per-page/overall/max-pages caps; explicit `truncated` flag.
- Filters: environment/release/fingerprint/query passed through correctly.
- Failure matrix of §8 incl. no-token-in-error assertions.
- Profile gate: missing/insufficient profile ⇒ no network call
(`mock_api.assert_not_called()` pattern).
- `read_raw` op absent ⇒ raw-frame request refused without an API call.
## 10. Implementation-readiness checklist
Ready to implement once:
1. ADR-0001 owner decision #2 (namespace/placement) is made — mechanical
rename of the `glitchtip_` prefix if needed.
2. ADR-0001 owner decision #1 (repo home) is made.
3. #76 profile schema exists (or a minimal `glitchtip-readonly` profile is
hand-rolled to the same rules).
4. A pinned GlitchTip version is chosen for API-subset testing (§3).
Explicitly **not** unlocked by this document: any GlitchTip mutation, any
automatic Gitea filing (#74 designs that as a gated, explicitly-invoked
orchestrated workflow), any Gitea credentials in this boundary.
@@ -1,165 +0,0 @@
# Jenkins Repo/Branch/PR → Job Mapping — Design Notes
- **Status:** Design (implementation-ready notes; **no implementation in this repo**)
- **Issue:** #77 (parent: #72 read-only tools design; umbrella: #75; boundary: ADR-0001, #71)
- **Related docs:** [`jenkins-readonly-build-status-design.md`](jenkins-readonly-build-status-design.md)
- **Date:** 2026-07-02
## 1. Purpose
The #72 tool set addresses Jenkins jobs by **explicit fully-qualified job
path**. This document designs the layer above it: how a *(repository, branch,
PR)* tuple — the vocabulary of Gitea workflows — resolves deterministically to
a Jenkins job path, so an LLM can ask "did the build for `Gitea-Tools`
`master` pass?" without knowing Jenkins internals.
Hard constraints inherited from #72 / ADR-0001:
- **No silent guessing of job names.** Unmapped input returns an explicit
"no mapping" result — never a fuzzy match, never a constructed-and-probed
name.
- **Read-only.** Mapping introduces no Jenkins write actions.
- Lives in the **`jenkins-mcp`** boundary; no Gitea credentials involved.
## 2. Mapping format
Declarative, versioned config (TOML or JSON — match whatever config format
`jenkins-mcp` adopts; illustrated here as TOML):
```toml
version = 1
[[mapping]]
# Source side (what the caller supplies)
repo = "Scaled-Tech-Consulting/Gitea-Tools" # org/repo, exact
# Target side (where it lives in Jenkins)
job = "scaled-tech/gitea-tools" # foldered job path
type = "multibranch" # "multibranch" | "single" | "parameterized-view"
[[mapping]]
repo = "Scaled-Tech-Consulting/Timesheet"
branch = "master" # optional: branch-specific override
job = "scaled-tech/timesheet-master"
type = "single"
```
Field semantics:
| Field | Required | Meaning |
|---|---|---|
| `repo` | yes | Exact `org/repo` (case-insensitive compare, stored canonical) |
| `branch` | no | Exact branch name this entry pins; absent = all branches |
| `job` | yes | Fully-qualified Jenkins job path, folders `/`-joined |
| `type` | yes | How branch/PR resolves under the job (§3) |
Rules:
- **Exact matching only** on `repo` and `branch`. No globs in v1 (globs invite
accidental over-matching; add later behind an explicit `pattern = true` flag
if ever needed).
- Unknown `type` or malformed entry ⇒ config load fails closed with a clear
error naming the entry — a broken mapping file must not half-load.
- Duplicate `(repo, branch)` keys ⇒ load error (ambiguity is refused, not
resolved).
## 3. Resolution semantics by job type
Given caller input `(repo, branch?, pr?)`:
- **`multibranch`** — branch job addressed as `<job>/<url-encoded-branch>`
(e.g. `feature/x``feature%2Fx`). PRs addressed as `<job>/PR-<number>`
(Jenkins multibranch PR-discovery naming). Both per #72 §8.
- **`single`** — the job path is used as-is; `branch`/`pr` input beyond the
entry's pinned branch is a **no-mapping** result (a single job cannot answer
for arbitrary branches).
- **`parameterized-view`** — read-only variant for jobs that encode branch as
a build parameter: resolution returns the base job path plus a
`branch_param` filter hint the status tools may apply client-side when
scanning recent builds. It never triggers anything (read-only rule).
## 4. Precedence
Most-specific entry wins, evaluated in this order:
1. `(repo, branch)` exact entry — branch-pinned override.
2. `(repo)` entry — repo-wide (multibranch typical).
3. Nothing → **no mapping** (§5).
PR input resolves through the same chain: a PR belongs to its **base repo**'s
mapping; forks never introduce their own mapping (a fork's head repo is not
consulted — CI runs live under the base repo's job). If the base repo is
unmapped, the PR is unmapped.
Ties are impossible by construction (duplicate keys refused at load).
## 5. No-match behavior
```json
{
"mapped": false,
"repo": "org/unknown-repo",
"branch": "master",
"error": "no Jenkins job mapping for this repo/branch",
"hint": "add an entry to the jenkins-mcp mapping config"
}
```
- Deterministic, explicit, machine-checkable (`mapped: false`).
- **Never** falls back to name construction ("repo name probably equals job
name"), never probes Jenkins for candidates, never string-similarity ranks.
- The hint names the config, not a guessed job.
## 6. Where the mapping config lives
- **In the `jenkins-mcp` package/deployment** (e.g. `jenkins-mcp/mapping.toml`),
version-controlled next to the server that consumes it — *not* in Gitea-Tools
and *not* in per-user env vars (mappings are shared team facts, not
credentials).
- Path overridable via env (`JENKINS_MCP_MAPPING_FILE`) for tests/containers.
- Contains **no secrets** — job paths and repo names only — so it is safe to
commit and review like any other config.
- Reloaded at server start; a hot-reload tool is out of scope (restart is the
documented path).
## 7. Exposed tool surface (read-only)
One addition to the #72 tool set:
| Tool | Purpose |
|---|---|
| `jenkins_resolve_job` | `(repo, branch?, pr?)``{mapped, job, addressed_path, type}` or the §5 no-match result. Pure config lookup — **no Jenkins API call at all.** |
Status tools (`jenkins_latest_build` etc.) accept either an explicit job path
(as designed in #72) **or** `(repo, branch)` which they resolve via the same
mapping layer first. Resolution failure surfaces the §5 payload rather than
querying Jenkins.
## 8. Testing strategy (mocked; for the implementing package)
Config-layer tests (no network at all):
- Exact-match hit: repo-wide and branch-pinned entries.
- Precedence: branch-pinned beats repo-wide.
- Multibranch encoding: `feature/x``<job>/feature%2Fx`; PR → `<job>/PR-7`.
- `single` type with non-pinned branch ⇒ no-mapping.
- Fork PR resolves through base repo; unmapped base ⇒ no-mapping.
- Unknown repo/branch ⇒ §5 payload, and **no Jenkins client call**
(`mock_api.assert_not_called()`).
- Malformed config / duplicate keys / unknown type ⇒ load fails closed with
entry-naming error.
- No-secret check: mapping load/error paths never touch or print credentials.
Integration with mocked Jenkins API (per #72 §9): resolved path is used
verbatim in the GET URL; no write verbs anywhere.
## 9. Standalone-worthiness and readiness
#77 was split from #72 on the condition it stays "standalone only if mapping
is nontrivial." The precedence rules, fork/PR semantics, three job types, and
fail-closed config loading above are the nontrivial part; this document is the
justification.
Ready to implement in `jenkins-mcp` when #72's readiness checklist clears
(ADR-0001 owner decision #1; profile schema per #76 or hand-rolled
`jenkins-readonly`). Nothing here unlocks build triggers, deploys, or
parameterized launches.
@@ -1,151 +0,0 @@
# Jenkins Read-Only Build Status Tools — Design Notes
- **Status:** Design (implementation-ready notes; **no implementation in this repo**)
- **Issue:** #72 (parent umbrella: #75; boundary decision: ADR-0001, #71)
- **Related:** #77 (repo/branch/PR → job mapping, designed separately)
- **Date:** 2026-07-02
## 1. Purpose and scope
Define the minimum **read-only** Jenkins MCP tool set that lets an LLM answer:
*"Did the latest build for this project/branch succeed or fail?"* — plus enough
detail (build URL, number, timing, result) to report or investigate.
Phase 1 is **strictly read-only**, per ADR-0001
([`adr-0001-mcp-control-plane-boundaries.md`](adr-0001-mcp-control-plane-boundaries.md)):
- **Excluded: build triggers.**
- **Excluded: deploy triggers.**
- **Excluded: parameterized job launches.**
- Excluded: job creation/deletion/config changes, queue manipulation, node
management — any Jenkins mutation whatsoever.
## 2. Boundary placement
These tools belong to the **`jenkins-mcp`** package/server of the MCP Control
Plane — **never** inside `gitea-mcp` (`mcp_server.py` in this repo).
Consequences (from `tool-boundaries.md`, `credential-isolation.md`, ADR-0001):
- `jenkins-mcp` runs as its own server process with its own `.env`.
- **Jenkins credentials never enter the Gitea MCP runtime**, and Gitea
credentials never enter `jenkins-mcp`.
- This document lands in this repo only because the repo currently hosts the
Control Plane's architecture docs; the code ships elsewhere (owner decision
#1 of ADR-0001).
## 3. Minimum read-only tool set
| Tool | Purpose |
|---|---|
| `jenkins_whoami` | Verify authenticated Jenkins identity + active profile (mirror of `gitea_whoami`; fail-closed identity proof before anything else) |
| `jenkins_list_jobs` | List visible jobs (supports folder paths), with pagination bounds |
| `jenkins_latest_build` | The primary question: latest build of a job (or job+branch for multibranch) → status summary |
| `jenkins_build_status` | Status of a specific build number (job, number) |
| `jenkins_get_build` | Full safe detail of a build (fields in §4) |
| `jenkins_console_tail` | Bounded, redacted tail of a build's console log (§6) — optional, approval-gated addition |
All tools are `GET`-only against the Jenkins JSON API (`/api/json`,
`.../lastBuild/api/json`, `.../consoleText`). No tool issues POST/PUT/DELETE.
## 4. Return payloads (safe fields)
`jenkins_latest_build` / `jenkins_build_status` / `jenkins_get_build` return:
| Field | Source | Notes |
|---|---|---|
| `job` | request | Fully-qualified job path (folders joined with `/`) |
| `build_number` | `number` | int |
| `result` | `result` | `SUCCESS` / `FAILURE` / `UNSTABLE` / `ABORTED` / `NOT_BUILT`; `null``IN_PROGRESS` when `building=true` |
| `building` | `building` | bool |
| `url` | `url` | Build URL |
| `branch` | multibranch job name / SCM action | Best-effort; omitted when unknown |
| `timestamp` | `timestamp` | ISO-8601 UTC (converted from epoch ms) |
| `duration_seconds` | `duration` | 0/omitted while building |
| `commit_sha` | SCM build action | Best-effort; omitted when unknown |
Rules: no raw Jenkins payload passthrough (allowlist projection only); no
`Authorization` header, token, or crumb material in any output or error
(reuse the shared redaction approach of `safety-model.md` §3 / `gitea_audit`).
## 5. Failure behavior (fail closed, clear, safe)
| Condition | Behavior |
|---|---|
| Unknown job | Explicit `{"found": false, "job": "<path>", "error": "job not found"}` — never guess or fuzzy-match a job name (hard rule; see also #77) |
| Jenkins unreachable (DNS/timeout/conn refused) | Clear `"network error contacting Jenkins: <redacted reason>"`; no retry storm — mirror `gitea_auth.api_request` timeout + failure conversion |
| 502/503/504 | Explicit "Jenkins upstream unavailable" |
| 401/403 | "Jenkins auth failed / insufficient permissions" — **without** echoing credentials or the request's auth material |
| Malformed JSON | "malformed JSON response from Jenkins" (no raw-body dump) |
| Missing profile/creds | Fail closed before any network call (§7) |
## 6. Console tail safety (`jenkins_console_tail`)
Console logs are the highest-risk surface (secrets, tokens, internal hosts
routinely leak into build logs). If included at all (owner may defer it):
- **Bounded:** hard server-side cap (default: last 200 lines AND ≤ 64 KiB,
whichever is smaller; caller may request less, never more).
- **Redacted:** pass through the shared secret redactor (token/`Basic`/`Bearer`/
password/key-value patterns) before returning; redaction failure ⇒ return an
error, never the raw text.
- **Default off:** summary fields (`result`, failing stage if cheaply available)
are preferred; the tail requires an explicit `allowed_operations` entry
(`jenkins.console.read`) distinct from plain `jenkins.build.read`.
## 7. Credentials and profile requirements
Follows the per-service profile model (`gitea-execution-profiles.md`, extended
by #76):
- Env/config: `JENKINS_URL`, `JENKINS_USER`, `JENKINS_TOKEN_SOURCE_NAME`
(name-of-secret only — value resolved at runtime, never logged/committed).
- Profile: e.g. `jenkins-readonly` with namespaced
`allowed_operations: ["jenkins.read", "jenkins.build.read"]`
(+ `jenkins.console.read` only if the tail tool is approved);
`forbidden_operations: ["jenkins.build.trigger", "jenkins.deploy", "jenkins.job.configure"]`
as belt-and-braces even though no mutating tool exists.
- Missing URL/user/token/profile ⇒ **fail closed** with a clear message.
- Since every tool is read-only, no confirmation gates are needed — but
identity (`jenkins_whoami`) must still work so workflows can prove which
Jenkins account they act as.
## 8. Job addressing and mapping
Tools accept an explicit fully-qualified job path (folder-aware:
`folder/subfolder/job`). How a *repo/branch/PR* resolves to that job path is
**out of scope here** and designed in **#77**, with these fixed constraints:
- No silent guessing of job names — unmapped input returns an explicit
"no mapping" result.
- Multibranch pipelines address a branch job as `<job>/<branch>` with proper
URL-encoding of branch names (e.g. `feature%2Fx`).
## 9. Testing strategy (for the implementing package)
Mocked-Jenkins unit tests only (no live Jenkins in unit CI), mirroring this
repo's conventions (`docs/developer-testing-guidelines.md`):
- Patch the HTTP client; assert method is always `GET` and URL shape is correct
(folders, multibranch encoding).
- Success projections: field allowlist exactly as §4; unknown fields dropped.
- `result=null + building=true``IN_PROGRESS`.
- Unknown job ⇒ found:false, no fuzzy match, no API retry.
- Timeout/DNS/5xx/malformed-JSON ⇒ safe errors, no secret/credential leakage
(explicit no-token-in-error assertions).
- Console tail: cap enforcement (lines and bytes), redaction applied, redaction
failure ⇒ error not raw text, gated behind `jenkins.console.read`.
- Profile gate: missing/insufficient profile ⇒ no network call
(`mock_api.assert_not_called()` pattern).
## 10. Implementation-readiness checklist
Ready to implement in `jenkins-mcp` once:
1. ADR-0001 owner decision #1 (where `jenkins-mcp` lives) is made.
2. #76 profile schema exists (or a minimal `jenkins-readonly` profile is
hand-rolled to the same rules).
3. #77 mapping design is accepted (or tools ship path-addressed only, mapping
deferred).
Explicitly **not** unlocked by this document: build triggers, deploys,
parameterized launches, any Jenkins code in `mcp_server.py`.
@@ -1,68 +0,0 @@
# Multi-Service MCP Profile and Configuration Model
- **Status:** Design (no implementation in this repo yet)
- **Issue:** #76 (parent umbrella: #75; boundary decision: ADR-0001, #71)
- **Date:** 2026-07-02
## 1. Purpose and Scope
Extend the existing Gitea execution-profile model (`docs/gitea-execution-profiles.md`) into a generic **per-service** MCP profile/config model. This supports integrating Jenkins and GlitchTip into the MCP Control Plane while strictly preserving isolation and fail-closed safety.
**Crucial Constraints:**
* The shared profile/config model is a **schema / library**, **not a shared credential pool**.
* Tokens remain **service-local**; profiles are **per service**.
* Orchestrators **must not** directly hold every service credential.
## 2. Profile Schema (Per Service)
The schema reuses the proven Gitea field model, adapted per service.
```json
{
"profile_name": "readonly-metrics",
"service": "glitchtip",
"token_source_name": "GLITCHTIP_API_TOKEN_READONLY",
"allowed_operations": [
"glitchtip.event.read",
"glitchtip.issue.read"
],
"forbidden_operations": [
"glitchtip.issue.resolve",
"glitchtip.issue.delete"
]
}
```
### Schema Rules
* `allowed_operations` are **namespaced** (e.g., `gitea.issue.create`, `jenkins.build.read`, `glitchtip.event.read`).
* `forbidden_operations`, if present, **always override** `allowed_operations`.
* `token_source_name` records the source **name only, never the value**. Tokens must never be printed, logged, or included in telemetry.
## 3. Fail-Closed Behavior
The model enforces strict fail-closed constraints before any network call occurs:
* **Missing Profile:** If a requested profile is undefined for the target service, the operation fails immediately.
* **Missing Credentials:** If the `token_source_name` cannot be resolved to a valid token at runtime, the operation fails immediately without retrying or prompting.
## 4. Environment Overrides
Profiles can be dynamically overridden or injected via environment variables, following the established hierarchy:
1. **Explicit Environment Variable:** (Highest precedence) e.g., `MCP_GLITCHTIP_TOKEN` overrides any JSON profile.
2. **Profile Mapping in JSON:** Resolved via `token_source_name` (e.g., `GLITCHTIP_API_TOKEN_READONLY`) mapping to an environment variable or secret store.
3. **No Auth:** Fails closed.
## 5. Audit Logging
To maintain accountability across multi-service workflows, all mutating actions must include the audit identity and source:
* The audit log must record the `profile_name`, the orchestrator source (e.g., `sysadmin`, `jenkins-mcp`), and the action taken.
* The audit system must sanitize all output to ensure tokens are stripped (see `safety-model.md`).
## 6. Backward Compatibility
The existing Gitea profile behavior (`gitea_whoami`, etc.) remains strictly backward compatible. The generic profile library will parse existing Gitea profile objects without requiring them to migrate their schemas, defaulting the `service` attribute to `gitea`.
## 7. Implementation Boundary
Per the namespace decisions in #71 and #75, this generic model belongs in the `common` package or library. It will be imported by `gitea-mcp` (this repo), `jenkins-mcp`, and `glitchtip-mcp` without forcing a monolithic architecture.
-34
View File
@@ -1,34 +0,0 @@
# Label Taxonomy
This document catalogs the issue labels used for MCP workflows, including Jenkins and GlitchTip (observability).
> **Approval Required:** Do not create or apply new labels in `manage_labels.py` without explicit owner approval of this document.
## Existing Labels
* **`jenkins`**
* Description: Jenkins integration
* Color: `d93f0b`
* Use: Used to mark issues, PRs, or tasks that involve the `jenkins-mcp` boundaries, CI/CD designs, or build failures.
* **`glitchtip`**
* Description: GlitchTip integration
* Color: `b60205`
* Use: Used to mark issues related to the `glitchtip-mcp` boundary and observability integration.
## Proposed / Missing Labels
* **`observability`**
* Proposed Description: Observability, metrics, and monitoring tasks
* Proposed Color: `5319e7`
* Use: Broader than GlitchTip alone; covers logging, metrics, traces, and general observability pipeline improvements.
* **`source:glitchtip`**
* Proposed Description: Issue filed automatically by GlitchTip orchestration
* Proposed Color: `b60205`
* Use: Applied automatically by the orchestrator when a GlitchTip error event is converted into a Gitea issue.
* **`status:triage`**
* Proposed Description: Issue needs human or orchestrator triage
* Proposed Color: `fbca04`
* Use: Used for incoming issues (especially automated ones like `source:glitchtip`) that have not yet been evaluated for priority or resolution.
-138
View File
@@ -1,138 +0,0 @@
# LLM-Agent-SHA — Opaque Agent Attribution (Phase 0)
Convention for attributing work to a specific LLM session/workstream across
issues, branches, PRs, and review handoffs, without exposing a human or model
identity. Approved by the owner decision on issue #86
(`#issuecomment-1354`); this document implements **Phase 0 only**.
## The one rule that matters
`LLM-Agent-SHA` is **informational attribution metadata only**. It must never
be used for authentication, authorization, review eligibility, merge
eligibility, profile permissions, or any other security decision.
The security gates remain, unchanged:
- the **authenticated Gitea user** (self-review/self-merge protection),
- the **active MCP profile** and its `allowed_operations`
(see [`gitea-execution-profiles.md`](gitea-execution-profiles.md)),
- the fail-closed eligibility checks in `gitea_check_pr_eligibility`.
Two sessions with different `LLM-Agent-SHA` values that authenticate as the
same Gitea user are **the same actor** for review/merge safety. A different
SHA never unlocks self-review or self-merge. `tests/test_llm_agent_sha.py`
proves the eligibility logic never consults the SHA.
## Format
```text
LLM-Agent-SHA: llm-<12 lowercase hex chars>
```
Validation regex:
```text
^llm-[0-9a-f]{12}$
```
Examples: `llm-8f3a9c2d6b41`, `llm-41d0e7aa9f2c`, `llm-b7c93d441a08`.
### Generation
Generate 48 random bits, e.g. `python3 -c "import secrets; print('llm-' +
secrets.token_hex(6))"`, or hash a non-secret session UUID. An
operator-provided opaque ID is also fine.
Do **not** derive the value from any of:
- a Gitea token or other secret,
- an email address or username,
- a machine hostname or private filesystem path,
- a model or provider name,
- conversation contents.
The SHA must contain no model name, provider name, human name, email,
hostname, token, private path, or conversation-derived content. It is safe to
include in PR bodies, issue comments, and audit logs — and only there.
## Lifetime
Canonical lifetime is **per PR/workstream**: pick one SHA when starting an
issue and keep it through the branch, PR, and handoff for that workstream. A
per-session SHA is acceptable when the session maps cleanly to one
workstream. Do not reuse a SHA across unrelated workstreams.
## Placement
Phase 0 uses **visible markdown metadata blocks** (not hidden HTML
comments). Include the block in PR bodies and review handoffs; keep it out of
ordinary comments unless attribution is genuinely useful there.
**Never put the SHA in branch or worktree names.** Branches stay
issue-linked and human-readable (`docs/issue-86-llm-agent-sha-phase0`), per
the branch standard.
### Handoff metadata block (implementer → PR body / handoff report)
```markdown
LLM Handoff Metadata:
- LLM-Agent-SHA: llm-8f3a9c2d6b41
- LLM-Role: implementer
- Authenticated-Gitea-User: jcwalker3
- MCP-Profile: gitea-default
- Branch: docs/example-branch
- Worktree: branches/docs-example-branch
- Self-review allowed: no
```
### Review metadata block (reviewer → review comment)
```markdown
Review Metadata:
- LLM-Agent-SHA: llm-41d0e7aa9f2c
- LLM-Role: reviewer
- Authenticated-Gitea-User: sysadmin
- MCP-Profile: prgs-reviewer
- Eligibility: passed
```
## Same SHA vs same user vs same profile
Reviewers and operators must keep three distinct identities straight:
| Comparison | Meaning | Effect on eligibility |
|---|---|---|
| same `LLM-Agent-SHA` | same LLM session/workstream wrote both artifacts | **none — attribution only** |
| same authenticated Gitea user | same Gitea actor | **blocks** self-review / self-merge, regardless of SHA |
| same MCP profile | same capability set | governs `allowed_operations` (what actions are permitted at all) |
Concretely: an implementer session (`llm-8f3a…`, user `jcwalker3`) and a
would-be reviewer session (`llm-41d0…`, also user `jcwalker3`) have different
SHAs but the **same Gitea user** — the reviewer session is still the PR
author to Gitea and must not review, approve, or merge. Review handoffs
require a genuinely different authenticated user (e.g. `sysadmin` /
`prgs-reviewer`).
## Phase 0 scope (and what is deferred)
Phase 0 is documentation, handoff/review templates, and negative tests only.
Deferred to later owner-approved phases; none of this exists yet:
- launcher-enforced SHA generation,
- `LLM_AGENT_SHA` / `LLM_AGENT_ROLE` environment injection,
- `gitea_whoami` returning SHA/role,
- automatic PR body injection by MCP tools,
- audit schema changes requiring the SHA,
- release/orchestrator lineage tracking.
MCP tools neither read nor emit the SHA. Setting an `LLM_AGENT_SHA`
environment variable has no effect on any tool; the negative tests assert
eligibility results are byte-identical with and without it.
## Related documents
- [`llm-workflow-runbooks.md`](llm-workflow-runbooks.md) — the runbooks whose
handoffs carry these blocks
- [`gitea-execution-profiles.md`](gitea-execution-profiles.md) — profiles and
`allowed_operations` (the real permission gate)
- [`safety-model.md`](safety-model.md) — audit, redaction, confirmation gates
+2 -20
View File
@@ -45,18 +45,6 @@ Use any eligible reviewer profile to review PR #N.
Use any eligible merger profile to merge PR #N if checks pass.
```
### Attribution: `LLM-Agent-SHA` (metadata only)
Sessions may attribute their work with an opaque `LLM-Agent-SHA`
(`llm-<12 lowercase hex>`, e.g. `llm-8f3a9c2d6b41`) in PR-body and
review-handoff metadata blocks — see
[`llm-agent-sha.md`](llm-agent-sha.md) for the full convention. It is
**attribution only**: eligibility is decided solely by the authenticated
Gitea user and the profile's allowed operations. Two sessions with different
SHAs under the same Gitea user are the same actor — a different SHA never
permits self-review or self-merge. Keep the SHA out of branch and worktree
names.
## Prerequisites: canonical config + thin launchers
Runtime profiles live in **one canonical JSON file**, referenced by every LLM
@@ -286,8 +274,7 @@ touching anything.
`fix/...` / `docs/...`); `cd` into that worktree; implement narrowly; add or
update tests if behavior changes; run the full suite; commit with an
issue-linked message; open a PR to `master`. **Do not** review or merge your
own PR. Include an `LLM Handoff Metadata` block (with `LLM-Agent-SHA`) in
the PR body — see [`llm-agent-sha.md`](llm-agent-sha.md).
own PR.
- **Prompt:** `Use an author profile to implement issue #N and open a PR to
master. Do not self-review or self-merge.`
@@ -298,11 +285,7 @@ touching anything.
- **Steps:** confirm identity + eligibility (menu eligibility check or
`gitea_check_pr_eligibility`); read the diff; confirm scope matches the linked
issue; post the review (`comment` / `request_changes` / `approve`) via the
gated review tool. Pin the reviewed head SHA where supported. Include a
`Review Metadata` block (with your own `LLM-Agent-SHA`) in the review —
and remember: a different `LLM-Agent-SHA` does **not** make you a different
actor; only a different authenticated Gitea user does
([`llm-agent-sha.md`](llm-agent-sha.md)).
gated review tool. Pin the reviewed head SHA where supported.
- **Prompt:** `Use any eligible reviewer profile to review PR #N. Approve only
if scope matches issue #M and checks pass; otherwise request changes.`
@@ -408,7 +391,6 @@ scripts/release-tag v0.4.0 --notes-file /tmp/release-notes.md --push
- [`../skills/llm-project-workflow/SKILL.md`](../skills/llm-project-workflow/SKILL.md) — portable cross-project LLM workflow skill.
- [`gitea-execution-profiles.md`](gitea-execution-profiles.md) — the profile model.
- [`llm-agent-sha.md`](llm-agent-sha.md) — opaque agent attribution metadata (never an eligibility input).
- [`safety-model.md`](safety-model.md) — trust boundaries and audit logging.
- [`tool-boundaries.md`](tool-boundaries.md) — per-tool allowed operations.
- [`credential-isolation.md`](credential-isolation.md) — credential handling.
@@ -8,8 +8,6 @@ Task: review PR #<pr> for issue #<n>.
Rules (llm-project-workflow):
- Review in a SEPARATE detached review worktree, never the author's folder.
- You must NOT be the PR author. If the authenticated user == PR author, stop.
A different LLM-Agent-SHA does NOT make you a different actor — only a
different authenticated Gitea user does (docs/llm-agent-sha.md).
- Do not merge if any check fails.
Steps:
@@ -23,14 +21,6 @@ Steps:
6. Run the test suite; note results.
7. Post the review verdict: approve only if scope is clean and checks pass;
otherwise request changes with specifics. Never merge from this review step.
Include a "Review Metadata" block (attribution only — docs/llm-agent-sha.md):
Review Metadata:
- LLM-Agent-SHA: llm-<12 lowercase hex, e.g. llm-41d0e7aa9f2c>
- LLM-Role: reviewer
- Authenticated-Gitea-User: <whoami result>
- MCP-Profile: <profile name>
- Eligibility: passed/failed
Handoff: reviewer identity, PR author, scope verdict, checks + results, decision.
```
@@ -23,17 +23,6 @@ Steps:
6. Checks: run the test suite, compile/lint changed files, git diff --check,
and scan the diff for secrets.
7. Commit (issue-linked message), push the branch, open a PR to master.
Include an "LLM Handoff Metadata" block in the PR body (attribution only;
never an eligibility input — docs/llm-agent-sha.md):
LLM Handoff Metadata:
- LLM-Agent-SHA: llm-<12 lowercase hex, e.g. llm-8f3a9c2d6b41>
- LLM-Role: implementer
- Authenticated-Gitea-User: <whoami result>
- MCP-Profile: <profile name>
- Branch: <branch>
- Worktree: <worktree path>
- Self-review allowed: no
8. Stop before review/merge — you are the author.
Handoff: issue #, branch, worktree path, files changed, checks + results, PR URL.
-195
View File
@@ -1,195 +0,0 @@
"""Negative tests for LLM-Agent-SHA attribution (#86, Phase 0).
``LLM-Agent-SHA`` (docs/llm-agent-sha.md) is attribution metadata ONLY. These
tests prove it can never bypass the review/merge safety gates:
1. Same Gitea user + different LLM-Agent-SHA still fails self-review/approval.
2. Same Gitea user + different LLM-Agent-SHA still fails self-merge.
3. The eligibility logic does not consult SHA metadata at all — results are
identical with no SHA, one SHA, or a different SHA in the environment, and
no gate accepts an agent-SHA input.
Phase 0 adds no SHA support to any MCP tool; the environment variables set
here (``LLM_AGENT_SHA`` / ``LLM_AGENT_ROLE``) simulate a future launcher and
must be ignored by every gate.
"""
import inspect
import os
import re
import sys
import unittest
from unittest.mock import patch
sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent.parent))
from mcp_server import ( # noqa: E402
gitea_check_pr_eligibility,
gitea_merge_pr,
gitea_review_pr,
gitea_submit_pr_review,
)
FAKE_AUTH = "Basic dGVzdDp0ZXN0"
SHA_PATTERN = re.compile(r"^llm-[0-9a-f]{12}$")
# Two distinct, well-formed agent SHAs "belonging" to the same Gitea user.
SHA_IMPLEMENTER = "llm-8f3a9c2d6b41"
SHA_WOULD_BE_REVIEWER = "llm-41d0e7aa9f2c"
def _pr(author, state="open", sha="abc123", mergeable=True):
return {
"user": {"login": author},
"state": state,
"head": {"sha": sha},
"mergeable": mergeable,
}
class TestShaFormatConvention(unittest.TestCase):
"""The documented format: llm-<12 lowercase hex>, nothing identifying."""
def test_documented_examples_are_valid(self):
for value in (SHA_IMPLEMENTER, SHA_WOULD_BE_REVIEWER, "llm-b7c93d441a08"):
self.assertRegex(value, SHA_PATTERN)
def test_identifying_or_malformed_values_are_rejected(self):
for bad in (
"llm-8F3A9C2D6B41", # uppercase
"llm-8f3a9c2d6b4", # too short
"llm-8f3a9c2d6b411", # too long
"llm-opus4", # model name, not hex
"claude-8f3a9c2d6b41", # provider prefix
"jcwalker3", # username
"llm-user@example.com", # email
"8f3a9c2d6b41", # missing prefix
"",
):
self.assertNotRegex(bad, SHA_PATTERN)
class TestShaCannotBypassSelfReview(unittest.TestCase):
"""Scenario: user A (SHA 1) authored the PR; user A (SHA 2) tries to act."""
def _env(self, agent_sha, role):
# Reviewer-capable profile + a simulated launcher-injected agent SHA.
return {
"GITEA_PROFILE_NAME": "gitea-reviewer",
"GITEA_ALLOWED_OPERATIONS": "read,review,approve,merge",
"LLM_AGENT_SHA": agent_sha,
"LLM_AGENT_ROLE": role,
}
@patch("mcp_server.api_request")
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
def test_same_user_different_sha_cannot_approve(self, _auth, mock_api):
mock_api.side_effect = [{"login": "jcwalker3"}, _pr("jcwalker3")]
env = self._env(SHA_WOULD_BE_REVIEWER, "reviewer")
with patch.dict(os.environ, env, clear=True):
r = gitea_check_pr_eligibility(pr_number=9, action="approve", remote="prgs")
self.assertFalse(r["eligible"])
self.assertIn("authenticated user is PR author", r["reasons"])
@patch("mcp_server.api_request")
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
def test_same_user_different_sha_cannot_merge(self, _auth, mock_api):
mock_api.side_effect = [{"login": "jcwalker3"}, _pr("jcwalker3")]
env = self._env(SHA_WOULD_BE_REVIEWER, "merger")
with patch.dict(os.environ, env, clear=True):
r = gitea_check_pr_eligibility(pr_number=9, action="merge", remote="prgs")
self.assertFalse(r["eligible"])
self.assertIn("authenticated user is PR author", r["reasons"])
@patch("mcp_server.api_request")
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
def test_gated_merge_tool_refuses_self_merge_despite_sha(self, _auth, mock_api):
# Even the fully-confirmed gated merge path must refuse: correct
# confirmation string, mergeable PR, merge-capable profile — but the
# authenticated user is the PR author, whatever the agent SHA says.
mock_api.side_effect = [{"login": "jcwalker3"}, _pr("jcwalker3")]
env = self._env(SHA_WOULD_BE_REVIEWER, "merger")
with patch.dict(os.environ, env, clear=True):
r = gitea_merge_pr(
pr_number=9, confirmation="MERGE PR 9", remote="prgs")
self.assertFalse(r.get("performed"))
for call in mock_api.call_args_list:
method, url = call.args[0], call.args[1]
self.assertFalse(
method == "POST" and url.endswith("/merge"),
f"self-merge mutation reached the API: {method} {url}",
)
@patch("mcp_server.api_request")
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
def test_review_tool_refuses_self_approval_despite_sha(self, _auth, mock_api):
mock_api.side_effect = [{"login": "jcwalker3"}, _pr("jcwalker3")]
env = self._env(SHA_WOULD_BE_REVIEWER, "reviewer")
with patch.dict(os.environ, env, clear=True):
r = gitea_review_pr(
pr_number=9, event="APPROVE", body="self approve", merge=False,
remote="prgs")
self.assertFalse(r["success"])
self.assertIn("authenticated user is PR author", r["message"])
for call in mock_api.call_args_list:
method, url = call.args[0], call.args[1]
self.assertFalse(
method == "POST" and url.endswith("/reviews"),
f"self-review mutation reached the API: {method} {url}",
)
class TestEligibilityNeverConsultsSha(unittest.TestCase):
"""The gates have no SHA input: not via env, not via parameters."""
def _run_eligibility(self, extra_env):
env = {
"GITEA_PROFILE_NAME": "gitea-reviewer",
"GITEA_ALLOWED_OPERATIONS": "read,review,approve",
}
env.update(extra_env)
with patch("mcp_server.get_auth_header", return_value=FAKE_AUTH), \
patch("mcp_server.api_request") as mock_api:
mock_api.side_effect = [{"login": "reviewer-bot"}, _pr("author-bot")]
with patch.dict(os.environ, env, clear=True):
return gitea_check_pr_eligibility(
pr_number=5, action="approve", remote="prgs")
def test_result_identical_with_without_and_across_shas(self):
baseline = self._run_eligibility({})
with_one = self._run_eligibility(
{"LLM_AGENT_SHA": SHA_IMPLEMENTER, "LLM_AGENT_ROLE": "implementer"})
with_other = self._run_eligibility(
{"LLM_AGENT_SHA": SHA_WOULD_BE_REVIEWER, "LLM_AGENT_ROLE": "reviewer"})
self.assertEqual(baseline, with_one)
self.assertEqual(baseline, with_other)
self.assertTrue(baseline["eligible"]) # sanity: a real decision ran
def test_eligibility_result_carries_no_agent_sha(self):
r = self._run_eligibility(
{"LLM_AGENT_SHA": SHA_IMPLEMENTER, "LLM_AGENT_ROLE": "implementer"})
blob = repr(r)
self.assertNotIn(SHA_IMPLEMENTER, blob)
self.assertNotIn("LLM_AGENT", blob)
def test_gate_functions_accept_no_agent_sha_parameter(self):
for fn in (gitea_check_pr_eligibility, gitea_merge_pr,
gitea_review_pr, gitea_submit_pr_review):
for param in inspect.signature(fn).parameters:
lowered = param.lower()
self.assertNotIn("agent", lowered,
f"{fn.__name__} accepts agent param {param!r}")
self.assertNotIn("llm", lowered,
f"{fn.__name__} accepts llm param {param!r}")
def test_gate_sources_never_read_agent_sha(self):
# Phase 0 guarantee: no gate reads LLM_AGENT_* metadata at all.
for fn in (gitea_check_pr_eligibility, gitea_merge_pr,
gitea_review_pr, gitea_submit_pr_review):
src = inspect.getsource(fn)
self.assertNotIn("LLM_AGENT", src,
f"{fn.__name__} reads LLM_AGENT metadata")
self.assertNotIn("agent_sha", src,
f"{fn.__name__} consults an agent SHA")
if __name__ == "__main__":
unittest.main()