Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"name": "magpie-utilities",
"source": "./plugins/magpie-utilities",
"version": "0.2.0.dev0",
"description": "Apache Magpie utilities family (4 skills)."
"description": "Apache Magpie utilities family (5 skills)."
}
]
}
6 changes: 3 additions & 3 deletions docs/setup/marketplaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ the model on **every** turn — see ["always-on" cost](#versioning) below).

**All-in-one — `magpie`**

- ✅ One install; all 70 skills; nothing to decide. Uses the real `skills/`
- ✅ One install; all 71 skills; nothing to decide. Uses the real `skills/`
directory, so **no symlinks** — works on Windows out of the box.
- ⚠️ Adds **~21.7k always-on tokens to every session**, including families you
may never use — that context (and cost) is spent whether or not you invoke a
Expand Down Expand Up @@ -145,10 +145,10 @@ so pick one approach.
| `magpie-issue` | 8 | ~2.4k |
| `magpie-repo-health` | 7 | ~2.1k |
| `magpie-contributor-growth` | 6 | ~1.8k |
| `magpie-utilities` | 4 | ~1.4k |
| `magpie-utilities` | 5 | ~1.7k |
| `magpie-mentoring` | 4 | ~1.2k |
| `magpie-pairing` | 2 | ~0.6k |
| **`magpie`** (all) | **70** | **~21.7k** |
| **`magpie`** (all) | **71** | **~22.0k** |

Skills are invoked under the installing plugin's namespace — e.g.
`/magpie:release-vote-tally` (all-in-one) or
Expand Down
2 changes: 1 addition & 1 deletion plugins/magpie-utilities/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "magpie-utilities",
"description": "Apache Magpie \u2014 framework meta-skills: write-skill, optimize-skill, skill-reconciler, list-skills.",
"description": "Apache Magpie \u2014 framework meta-skills: write-skill, optimize-skill, skill-reconciler, list-skills, report-framework-issue.",
"version": "0.2.0.dev0",
"author": {
"name": "Apache Magpie",
Expand Down
1 change: 1 addition & 0 deletions plugins/magpie-utilities/skills/report-framework-issue
1 change: 1 addition & 0 deletions skills/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ package = false
[tool.ruff]
line-length = 110
target-version = "py311"
include = ["**/*.py", "**/*.pyi"]

[tool.ruff.lint]
select = [
Expand Down
29 changes: 28 additions & 1 deletion tools/bitbucket/tests/test_bitbucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,14 @@

from magpie_bitbucket import cloud, datacenter
from magpie_bitbucket.cli import main
from magpie_bitbucket.client import BitbucketError, SameHostRedirectHandler, load_config, make_auth_header
from magpie_bitbucket.client import (
BitbucketError,
NoAuthRedirectHandler,
SameHostRedirectHandler,
_require_https,
load_config,
make_auth_header,
)
from magpie_bitbucket.normalize import (
created_issue_comment,
issue,
Expand Down Expand Up @@ -106,6 +113,26 @@ def urllib_request(url: str) -> urllib.request.Request:
)


def test_no_auth_redirect_handler_rejects_all_redirects() -> None:
handler = NoAuthRedirectHandler()
request = urllib_request("https://bitbucket.example.test/rest/api/1.0/foo")

with pytest.raises(BitbucketError, match="refusing to forward credentials"):
handler.redirect_request(
request,
None,
302,
"Found",
{},
"https://bitbucket.example.test/rest/api/1.0/bar",
)


def test_require_https_rejects_http() -> None:
with pytest.raises(BitbucketError, match="Bitbucket API URLs must use HTTPS"):
_require_https("http://bitbucket.example.test/rest/api/1.0/foo")


def test_same_host_redirect_handler_allows_same_origin() -> None:
handler = SameHostRedirectHandler()
request = urllib_request("https://bitbucket.example.test/rest/api/1.0/foo")
Expand Down
2 changes: 1 addition & 1 deletion tools/dev/check-family-plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@
"issue": "issue lifecycle: triage, reproduction, fix drafting, reassess, stale-sweep, dedup, backlog stats.",
"repo-health": "read-only repo-health audits: runner labels, workflow security, dependency/license/NOTICE, flaky tests, audit-finding fixes.",
"contributor-growth": "path-to-committer: activity sweeps, nominations, sentiment, readiness, committer/post-vote onboarding.",
"utilities": "framework meta-skills: write-skill, optimize-skill, skill-reconciler, list-skills.",
"utilities": "framework meta-skills: write-skill, optimize-skill, skill-reconciler, list-skills, report-framework-issue.",
"mentoring": "newcomer mentoring: welcome, newcomer-issue explanations, good-first-issue authoring + sweep.",
"pairing": "pair a change with a structured self-review or a multi-agent adversarial review.",
}
Expand Down
1 change: 1 addition & 0 deletions tools/skill-evals/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ packages = ["src/skill_evals"]
line-length = 110
target-version = "py311"
src = ["src"]
extend-exclude = ["evals"]

[tool.ruff.lint]
select = ["E", "W", "F", "I", "B", "UP", "SIM", "C4", "RUF"]
Expand Down
32 changes: 31 additions & 1 deletion tools/sourcehut/src/magpie_sourcehut/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import json
import os
import urllib.error
import urllib.parse
import urllib.request
from typing import Any

Expand All @@ -30,6 +31,28 @@ class SourceHutError(Exception):
"""General exception for SourceHut client errors."""


class NoAuthRedirectHandler(urllib.request.HTTPRedirectHandler):
"""Reject redirects so Authorization is not forwarded to another host."""

def redirect_request(
self,
req: urllib.request.Request,
fp: Any,
code: int,
msg: str,
headers: Any,
newurl: str,
) -> urllib.request.Request | None:
raise SourceHutError(f"SourceHut request redirected to {newurl}; refusing to forward credentials")


def _require_https(url: str) -> None:
"""Require HTTPS for SourceHut API URLs."""
parsed = urllib.parse.urlparse(url)
if parsed.scheme != "https":
raise SourceHutError("SourceHut API URLs must use HTTPS")


def query_graphql(service: str, query: str, variables: dict[str, Any] | None = None) -> dict[str, Any]:
"""Execute a GraphQL query/mutation against a specific SourceHut service.

Expand All @@ -46,6 +69,7 @@ def query_graphql(service: str, query: str, variables: dict[str, Any] | None = N
raise SourceHutError("SRHT_TOKEN environment variable is not set")

url = f"https://{service}.sr.ht/query"
_require_https(url)
payload: dict[str, Any] = {"query": query}
if variables:
payload["variables"] = variables
Expand All @@ -61,15 +85,21 @@ def query_graphql(service: str, query: str, variables: dict[str, Any] | None = N
method="POST",
)

# Writes never follow redirects: repeating a mutation at a redirected
# location is less safe than failing and requiring the caller to retry.
opener = urllib.request.build_opener(NoAuthRedirectHandler)

try:
with urllib.request.urlopen(req) as resp:
with opener.open(req) as resp:
body = resp.read().decode("utf-8")
res_json = json.loads(body)
errors = res_json.get("errors")
if errors:
err_msgs = [e.get("message", "Unknown error") for e in errors]
raise SourceHutError(f"GraphQL error from {service}.sr.ht: {'; '.join(err_msgs)}")
return res_json.get("data", {})
except SourceHutError:
raise
except urllib.error.HTTPError as exc:
err_msg = None
try:
Expand Down
Loading