Parent: #1936
Problem
requireRepoManageAccess and requireRepoApprovalQueueAccess (src/mcp/server.ts:1782, 1803) both return immediately — skipping every repo-scoped permission check — when this.identity.kind !== "session". authenticatePrivateToken (src/auth/security.ts:106) maps any caller presenting the single global Worker secret GITTENSORY_MCP_TOKEN to { kind: "static", actor: "mcp" }. This token is documented in packages/gittensory-mcp/README.md:186 and the CLI (bin/gittensory-mcp.js:1838,2424,3065-3067) as a normal alternative to gittensory-mcp login — nothing marks it operator-only, so any user (miner or maintainer) can plausibly export it locally believing it's their own credential.
decidePendingAction (src/mcp/server.ts:2497-2500) confirms the consequence precisely: after the no-op requireRepoApprovalQueueAccess call, it only scopes an action ID to the requested repo (pending.repoFullName !== fullName) — it never restricts which repoFullName a static-identity caller is allowed to pass in the first place.
Failure scenario: anyone who sets export GITTENSORY_MCP_TOKEN=<value> and points an MCP client at the deployed server can call gittensory_decide_pending_action to accept (execute) a staged merge/close/approve on a repo they have no GitHub write access to and are not a maintainer of, or call gittensory_propose_action to stage new actuation into that repo's queue.
Requirements
- A static-token identity must be scoped to the repos the operator actually intends it to control — not implicitly trusted for every installed repo.
- The fix must be config-driven (operator-configurable scope), not a hardcoded allowlist, since self-hosters run this against many different repo sets.
Deliverables
- Remove the unconditional early
return in requireRepoManageAccess/requireRepoApprovalQueueAccess for non-session identities; require an explicit, operator-configured repo scope for static-token callers (e.g. an env-configured allowlist of repos the static token may act on, or a stronger per-token-per-repo credential model).
- At minimum, ensure
decidePendingAction/proposeAction validate the static caller's permitted repo scope before touching any repo's pending-action queue, not just before touching a specific already-known action ID.
- Update the MCP README/CLI docs so
GITTENSORY_MCP_TOKEN is clearly described as an operator-scoped credential, not a general end-user login alternative, if the broader authorization model isn't changed.
- Add a regression test asserting a static-identity caller is denied
gittensory_decide_pending_action/gittensory_propose_action against a repo outside its configured scope.
Acceptance criteria
- A static-token caller can only propose/list/decide pending actions for repos explicitly within its configured scope.
- Session-based (per-user, GitHub-authenticated) access is unaffected.
Expected outcome
The MCP actuation surface can no longer be reached fleet-wide by any holder of one shared secret — access is scoped the same way every other actuation entrypoint already is.
Parent: #1936
Problem
requireRepoManageAccessandrequireRepoApprovalQueueAccess(src/mcp/server.ts:1782,1803) bothreturnimmediately — skipping every repo-scoped permission check — whenthis.identity.kind !== "session".authenticatePrivateToken(src/auth/security.ts:106) maps any caller presenting the single global Worker secretGITTENSORY_MCP_TOKENto{ kind: "static", actor: "mcp" }. This token is documented inpackages/gittensory-mcp/README.md:186and the CLI (bin/gittensory-mcp.js:1838,2424,3065-3067) as a normal alternative togittensory-mcp login— nothing marks it operator-only, so any user (miner or maintainer) can plausibly export it locally believing it's their own credential.decidePendingAction(src/mcp/server.ts:2497-2500) confirms the consequence precisely: after the no-oprequireRepoApprovalQueueAccesscall, it only scopes an action ID to the requested repo (pending.repoFullName !== fullName) — it never restricts whichrepoFullNamea static-identity caller is allowed to pass in the first place.Failure scenario: anyone who sets
export GITTENSORY_MCP_TOKEN=<value>and points an MCP client at the deployed server can callgittensory_decide_pending_actionto accept (execute) a staged merge/close/approve on a repo they have no GitHub write access to and are not a maintainer of, or callgittensory_propose_actionto stage new actuation into that repo's queue.Requirements
Deliverables
returninrequireRepoManageAccess/requireRepoApprovalQueueAccessfor non-session identities; require an explicit, operator-configured repo scope for static-token callers (e.g. an env-configured allowlist of repos the static token may act on, or a stronger per-token-per-repo credential model).decidePendingAction/proposeActionvalidate the static caller's permitted repo scope before touching any repo's pending-action queue, not just before touching a specific already-known action ID.GITTENSORY_MCP_TOKENis clearly described as an operator-scoped credential, not a general end-user login alternative, if the broader authorization model isn't changed.gittensory_decide_pending_action/gittensory_propose_actionagainst a repo outside its configured scope.Acceptance criteria
Expected outcome
The MCP actuation surface can no longer be reached fleet-wide by any holder of one shared secret — access is scoped the same way every other actuation entrypoint already is.