feat(tools): create_issue — file structured Gitea bug reports #53
Reference in New Issue
Block a user
Delete Branch "feat/create-issue-tool"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Adds a
create_issueMCP tool so an operator or agent can file a structured bug report into Gitea — giving the maintenance/dev team an actionable, uniformly-shaped ticket instead of free-text chat that gets lost.What the caller passes (context it already holds)
title,description(required),severity,external(customer-facing vs internal),org_id/org_slug,workspace_id,agent_role,component,environment,related_ids(PRs/run ids/request ids/EC2 ids),reproduction,logs_excerpt, extralabels.It renders these into a consistent issue body (a context table + free-text sections + provenance footer) and a label taxonomy (
severity/*,tenancy/external|internal,component/*,env/*,source/mcp-filed).Design
src/tools/issues.ts: purebuildIssueBody/deriveLabelNames(unit-tested, no network) + a Gitea client modelled exactly ontools/management/client.ts::mgmtCall— never throws, sameApiErrorenvelope (SSOT).GITEA_ISSUE_TOKEN(scopedissue:write), deliberately NOT a tenant/admin credential. Target repo fromGITEA_ISSUE_REPO(triage queue) or per-callrepo('owner/name').Tests
src/__tests__/issues.test.tscovers rendering, label derivation/dedup, AUTH_ERROR with no token, repo validation, id resolution + POST shape, unmatched-label reporting, and Gitea-error passthrough.npm run buildclean; full unit suite green (283 passed).Follow-up (deploy-time, not code)
Provision
GITEA_ISSUE_TOKEN(a scoped issue-bot identity) +GITEA_ISSUE_REPO(the triage repo) on the deployed management MCP server. Until then the tool returns a cleanAUTH_ERRORand never crashes startup. Happy to mint the bot identity + wire the env once you pick the triage repo.Adds a `create_issue` MCP tool so an operator or agent can file a STRUCTURED bug report into Gitea, giving the maintenance/dev team an actionable, uniformly-shaped ticket instead of free-text chat that gets lost. The caller supplies the context it already holds and the tool renders it into a consistent issue body + triage labels: - title, description (required) - severity (critical|high|medium|low) - external (customer-facing tenant) vs internal - org_id / org_slug, workspace_id, agent_role - component, environment (prod|staging|dev) - related_ids (PRs, run ids, request ids, EC2 ids), reproduction, logs_excerpt - extra labels Implementation: - src/tools/issues.ts: pure renderers buildIssueBody / deriveLabelNames (a Markdown context table + free-text sections + provenance footer; a severity/tenancy/component/env/source label taxonomy) plus a Gitea client modelled exactly on tools/management/client.ts::mgmtCall — never throws, returns the same ApiError envelope (SSOT). Labels are best-effort resolved to existing ids (unmatched are REPORTED, not auto-created — no silent caps). - Auth: dedicated GITEA_ISSUE_TOKEN (issue:write), NOT a tenant/admin cred. Target repo GITEA_ISSUE_REPO (the triage queue) or per-call `repo`. - Registered in BOTH server modes (unique tool name). Default-surface tool count 88 -> 89. Tests: src/__tests__/issues.test.ts (rendering, label derivation/dedup, AUTH_ERROR with no token, repo validation, id resolution + POST, unmatched-label reporting, Gitea-error passthrough). Full unit suite green (283 passed). Follow-up (deploy-time, not code): provision GITEA_ISSUE_TOKEN (scoped issue-bot identity) + GITEA_ISSUE_REPO on the deployed management MCP server. Until then the tool returns a clean AUTH_ERROR and never crashes startup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>qa 2nd-lane (full-SHA
a2c6928acc). feat(tools): create_issue — files structured Gitea issues from the mcp-server. 5-axis:(1) CORRECTNESS — SOUND. Inputs are zod-validated (title/description required; severity z.enum(critical|high|medium|low); environment z.enum(prod|staging|dev); org_id/slug/workspace_id/agent_role optional strings; related_ids/labels string arrays). buildIssueBody renders a structured Severity/Tenancy/Component table + Description/Reproduction/Related/Logs sections; deriveLabelNames maps fields → severity/component/env labels. Pure renderers exported + unit-tested; handleCreateIssue tested with mocked global.fetch (no real Gitea calls).
(2) SECURITY — SOUND. giteaHeaders() is FAIL-CLOSED: reads GITEA_ISSUE_TOKEN || GITEA_TOKEN and errors ('GITEA_ISSUE_TOKEN is not set') if absent — uses a DEDICATED narrow issue-bot token, not a broad one (per the file's auth comment). Target repo is env-locked (GITEA_ISSUE_REPO, trailing-slash-stripped) — NOT user-controlled, so no arbitrary-repo issue injection. The POST is JSON (no shell interpolation → no command injection). actor is from MOLECULE_AUDIT_ACTOR env, not user input.
(3) CONTENT-SECURITY — good hygiene: logs_excerpt is wrapped in a code-fence with an explicit 'Redact secrets before filing — this body is stored in Gitea' warning. (Non-blocking note: user fields render into a markdown table via
| k | v |without escaping|/newlines — worst case is cosmetic table-breakage in an internal triage issue body, not HTML/shell injection; optional to escape table cells for clean rendering.)(4) PERFORMANCE — single JSON POST per issue; negligible.
(5) TEST-COVERAGE — issues.test.ts (+179): pure-function assertions (body table contents, label derivation) + mocked-fetch handler paths.
Clean, well-tested, fail-closed tool addition. APPROVED (non-blocking: optional markdown-table-cell escaping).
Deploy config resolved (triage repo is live)
The
create_issuetriage queue is set up and the end-to-end path is verified working:GITEA_ISSUE_REPO=hongming/bug-reports— created, with the full 17-label taxonomy (severity/*,tenancy/external|internal,env/*,component/*,source/mcp-filed,triage/needs-triage) + a README field-guide.GITEA_ISSUE_TOKEN= thedevops-engineeridentity (added as awritecollaborator; scopewrite:issue+write:repository— narrower than the admin ceo-assistant). Swappable for a dedicated bug-filer bot later.GITEA_API_URLdefault (https://git.moleculesai.app/api/v1) andtoken <…>auth are correct.Remaining on merge: inject
GITEA_ISSUE_REPO+GITEA_ISSUE_TOKENinto the deployed management MCP server env (via Infisical SSOT). Until then the tool returns a cleanAUTH_ERROR(never crashes). I'll wire it when this lands.Security 5-axis — APPROVE (head
a2c6928acc). feat(tools): create_issue — files structured Gitea issues (+527/-3, 4 files). Took the 2nd distinct lane (CR-B agent-reviewer holds the 1st on-head).giteaApiUrl()from env, trailing-slash-stripped);repoonly fills the URL path, and is validated by/^[^/\s]+\/[^/\s]+$/— exactlyowner/name, no extra slashes/whitespace. This rejects../../x, absolute URLs (http://evil/...→ multi-slash), and traversal. The token can never be redirected to another host. ✓ (Minor, non-blocking: the name segment still admits?/#, but those degrade to a same-host 404 on the trusted Gitea — not exploitable.)GITEA_ISSUE_TOKEN(|| GITEA_TOKEN fallback), scoped to issue:write on the triage repo; returns AUTH_ERROR if unset; token is sent only in the Authorization header and never logged (logError records url=base only). ✓resolveLabelIds) — only matched IDs are POSTed, unmatched are reported (not silently created), so no arbitrary label/privilege injection. Title/body are inert markdown stored as an issue. ✓logs_excerpt/description/reproduction) that could carry secrets, posted to a stored Gitea issue. Mitigation is ADVISORY only — schema + body carry "REDACT secrets — stored in Gitea", and the tool description says "Do NOT include secrets". Acceptable for v1 since the target is an INTERNAL authenticated triage repo (not a public surface). Non-blocking recommendation: add a lightweight programmatic redactor (scrubAKIA…,-----BEGIN … KEY-----, long bearer/token patterns) before POST as defense-in-depth, since the caller is an LLM that may paste raw logs.giteaCallmaps 401/403→AUTH, 404→NOT_FOUND, 429→RATE_LIMITED, others→HTTP n; handles empty body + JSON-parse failure; fetch wrapped in try/catch → structured "unreachable".resolveLabelIdscaps at limit=100 and reports overflow as unmatched (honest, no silent drop). ✓create_issueis wired — catches a silent registration drop. ✓Gate GREEN (CI/test ✓). Author devops-engineer ≠ me. Sound — APPROVE. With CR-B's 1st lane → 2-distinct-genuine → merge.