ADR 0027 — Launch scope is the credential boundary; `dir=` is never a token selector
- Status: Accepted (owner ruling 2026-07-15; enforcement tracked in #90, issue-filing carve-out implementation in #59)
- Date: 2026-07-15
- Tracking: #90
- Amended 2026-07-17 (#155): a third enumerated read-only carve-out — the whoami-scoped local-resolution report (see “Carve-out 3” below).
- Amended 2026-07-19 (#263): the residual-risk paragraph in Consequences restated honestly - the boundary is host-scoped by credential, not clone-scoped (the on-disk clone is fabricable friction, not authorization).
- Amended 2026-07-19 (#264): carve-out 2’s repo-prefix is now enforced structurally client-side - the dir= report client is confined by gitea.WithRepoScope, which refuses any request outside the target repo’s own subtree, not just by convention of which methods the carve-out handlers call.
Context
dir= began as a repo selector for local-git tools (ADR 0007) and was extended
to any git repo on disk (legacy tracker 1862), with ADR 0019 clamping foreign primaries to
the worktree→finish contribution flow. Separately, ADR 0025 made credential
resolution host-keyed and per-checkout: a clone of a Codeberg repo can carry
working Codeberg credentials while the session’s launch checkout is authed to
a different forge — or to nothing at all.
Those two lines converged into a security question on 2026-07-15. A session
launched in an unauthed scratch clone needed one forge read; the MCP surface
correctly failed (not configured), and the working path was to cd into a
different, correctly-authed checkout and run CLI operations on its token.
Nothing defined whether that was legitimate. Two issues were filed asking to
make it ergonomic (#59 for the typed API tools, #87 for the raw passthrough) —
turning dir= into a credential selector.
The owner’s ruling: an agent must be prevented from crossing outside the
launch pwd and using auth tokens it finds in other checkouts. An agent that
treats every authed clone on disk as an available identity is one cd away
from every account configured anywhere on the machine — and with the raw API
passthrough, one found token is POST /user/keys away from account takeover.
The counter-need, also the owner’s: bug context is perishable. An agent working inside tool A’s repo (authed to forge X) that stumbles over a bug in tool B (tracker on forge Y, authed only inside B’s own clone) must be able to file that bug at the moment of failure, with the error text, versions, and reproduction state still in context. The failure may never reproduce again; making a human re-assemble the report later is strictly worse data. Cross-repo issue filing therefore MUST work — and must be the only such crossing.
Decision
The MCP server’s launch scope is its credential scope. dir= selects a
repository or working tree; it never selects a token. Credential resolution
for an operation follows the launch scope (ADR 0025 chain against the server’s
launch-fixed directory), with exactly two enumerated carve-outs — both bound
by one invariant:
Credentials resolved from a foreign checkout may authenticate only operations targeting that checkout’s own origin repository — the job that checkout’s config declares — never other repos on the same forge, never user/org/admin endpoints, never arbitrary API calls.
Carve-out 1 — contribute (git plane, pre-existing). The sanctioned
foreign-repo flow of ADR 0019 (madt_worktrees action=add →
madt_commit dir=<worktree> → madt_finish dir= branch=, plus madt_fetch
and madt_push in its service) resolves the target repo’s credentials for the
push/PR/merge against that repo’s own origin. This ADR changes nothing there;
it names what was implicit.
Carve-out 2 — report (API plane, new). Cross-repo issue filing via the
typed issues tool with dir=: create issue, comment, plus the same-repo
reads needed to file well (list and get, including list’s q= text
filter, for dedup and convention-matching). The cross-repo search action is
refused under dir=: its endpoint scans every repository the credential can
see and cannot be narrowed to the target repo, so permitting it would reach
far beyond the checkout that authorizes the crossing. Constraints:
- The target repo is derived from the foreign clone’s origin; an
owner_repo=that names anything else is refused. The clone on disk is the authorization: “where do this tool’s bugs go, and as whom do we file” is a property of the target repo’s own checkout. - Filing-class writes only. No close, no edit of others’ content, no label/ milestone/dependency administration, no release/repo mutation — those are maintainer verbs, not reporter verbs.
- Typed tool only.
madt_api_callnever resolves foreign credentials (#87, closed): its endpoint surface is unbounded, so a borrowed token would reach/user/*,/orgs/*, and/admin/*— the account-takeover class this boundary exists to prevent.
Carve-out 3 — whoami local-resolution report (amendment 2026-07-17, #155).
madt_auth_whoami dir=<checkout> reports the identity that checkout’s resolution
chain WOULD use — url, user, source labels, and the credential/URL chain rungs —
from LOCAL config resolution ONLY. It authenticates NOTHING: the dir= form skips
the live /user verification, the repo probe, and the scope check entirely, makes
no network call with the target’s credentials, and surfaces no token bytes (a
has_token presence boolean only). It is admissible because it is a pure read of
config an operator (agent or human) can already inspect by walking directories —
the operating system’s own permission model is the boundary — and it exists for
orientation and confirming a checkout’s credentials match expectations. It is
scoped to whoami alone: dir= remains a repo selector, never a token selector,
everywhere else, and this creates no precedent for other tools. Because it resolves
credentials locally without the foreign-checkout resolver, it needs no
getConfigForDirSites allowlist entry; the dirscope parity guard covers the new
dir-taking surface.
Everything else refuses, with the refusal naming the boundary and the
sanctioned remedies for an unauthed launch scope: madtea auth login in the
launch dir, the connect-time warning (#88), and in-session OAuth login (#89) —
never token discovery.
The boundary is agent-facing and non-overridable (ADR 0009 posture): no flag, env var, or git-config key widens it. The CLI human surface stays unguarded, consistent with the ADR 0019 clamp — humans own their disk; the hooks and this MCP-side boundary are what agents land on.
Rationale
- Blast radius. Issue filing is append-only and non-code: it cannot merge, push, delete, or change settings. Worst abuse is tracker noise — visible, attributable, revertible. Contrast the passthrough, where one found token can mint SSH keys on the account.
- Scope coherence. Using a clone’s token against that clone’s own origin is the token doing precisely its configured job. What the ruling forbids is a token doing anyone else’s job.
- Symmetry. The git plane already has exactly one sanctioned crossing (“I can fix it” — worktree→finish). The API plane gets its twin (“I can report it” — issue filing). Two carve-outs, same shape: routed to the foreign repo’s own remote, enumerated here, pinned by test.
- Perishability. The report crossing exists because the alternative — stash the context and hope a human re-files it — demonstrably loses data.
Consequences
- #59 is implementable, not superseded: typed issues tool grows
dir=under carve-out 2’s constraints. #87 stays closed. #90 implements the refusals and the parity-style test pinning which tools may ever resolve foreign credentials, so a new tool cannot quietly widen the boundary. - The remedies for an unauthed launch scope are #88/#89, not borrowing: the connect-time banner warning and in-session OAuth login for the launch scope itself.
- Residual risk, accepted with mitigations: the boundary is host-scoped by
credential, not clone-scoped. The on-disk clone is friction, not
authorization, and that friction is fabricable:
git initplusgit remote add origin <any repo on an authed forge>makes any repo on a forge that carries a host-keyed credential a valid filing target, becausenewSvcForDirresolves owner/repo from that fabricated origin (OwnerRepoFromDir) andGetConfigForDirsupplies the host-keyed token (internal/mcp/helpers.go). So a prompt-injected agent could file junk issues to any tracker on a forge the operator has authed anywhere on the machine - the operator’s own forges, not the open internet, but not only the repos already cloned into place either. I accept this because it matches the posture these guards take: they are accident detectors, not a privilege boundary (ADR 0007), and an agent that can fabricate a clone already holds a shell and the unguarded CLI, so a clone-scoped check buys no real containment. Rate pacing bounds the damage, which stays append-only, attributable, and revertible. The real fix, if a harder boundary is ever wanted, is the deferredrequiresUserInteractionbelt (the #10 client-support matrix): it would surface a human prompt for exactly the boundary-crossing call without touching launch-scope filing. Deferred as a follow-up decision, not part of this ADR. - Identity is by design: cross-scope filings are authored as whatever identity the target clone’s forge config carries. That is the identity the operator configured for that forge — correct attribution, not spoofing.
Alternatives considered
- General
dir=credential resolution (the #87 ask). Rejected — the ruling’s core: it converts every authed checkout on disk into an ambient identity pool for any agent, and via the passthrough escalates from “file a bug” to “own the account”. Rejected even for read-only calls: the read surface (private repos, org membership) leaks by the same mechanism. - No carve-out at all (pure launch-scope lockdown). Rejected: it strands perishable bug reports, demonstrated the very week this was decided — the cross-repo filing scenario only worked because the operator was watching and directed it in the moment. A boundary that forces ad-hoc human exceptions under pressure will erode; an enumerated, constrained crossing will not.
- Human confirmation per cross-scope filing (elicitation /
requiresUserInteraction) as a requirement. Not adopted as a requirement: the filing class is low-blast-radius and the operator’s stated intent is that unattended agents succeed at it. Recorded above as an optional belt. - CLI-side enforcement. Rejected, matching ADR 0019/legacy tracker 1912: the CLI is the
human surface. The agent-facing steering (ADR 0009 hooks) plus this MCP-side
boundary are the enforcement points; a human running
madteain their own checkout is out of scope.
Relates to ADR 0009 (non-overridable agent guards), ADR 0013/0025 (credential storage and host-keyed resolution), ADR 0019 (foreign primaries immutable; the contribute carve-out), ADR 0023 (refusal strings must teach the remedy self-contained). Issues: #59, #87 (closed), #88, #89, #90.
Canonical source: docs/adr/0027-launch-scope-credential-boundary.md in the madtea repo.