mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-10-18 07:20:46 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66d793d1a6 | ||
|
|
f0b2c2406e |
No files matched your search
@@ -0,0 +1,142 @@
|
||||
name: Publish to MCP registry
|
||||
|
||||
# Lists the released PyPI package in the official MCP registry
|
||||
# (registry.modelcontextprotocol.io) from server.json at the release tag.
|
||||
# GitHub OIDC proves ownership of the io.github.wiltodelta namespace, so no
|
||||
# interactive login or stored secret is involved. The registry validates the
|
||||
# PyPI release itself (the mcp-name marker in its description), and the Release
|
||||
# event fires in parallel with publish.yml's upload, so this waits until PyPI
|
||||
# serves the version first. A version the registry already lists is skipped.
|
||||
# It is a workflow of its own, not a distribute.yml job, so a manual dispatch
|
||||
# re-lists one version without re-running Homebrew, the Space, or ComfyUI, and
|
||||
# a registry outage never fails release verification.
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Released version to list (no leading v), e.g. 0.46.0"
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
# Model-only prereleases are not Python package releases.
|
||||
if: ${{ github.event_name != 'release' || (startsWith(github.event.release.tag_name, 'v') && !github.event.release.prerelease) }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
SERVER_NAME: io.github.wiltodelta/remove-ai-watermarks
|
||||
# The same name percent-encoded, as the registry's path segment takes it.
|
||||
SERVER_PATH: https://registry.modelcontextprotocol.io/v0/servers/io.github.wiltodelta%2Fremove-ai-watermarks
|
||||
# Pinned publisher release; the digest is GitHub's sha256 for the asset.
|
||||
PUBLISHER_URL: https://github.com/modelcontextprotocol/registry/releases/download/v1.8.1/mcp-publisher_linux_amd64.tar.gz
|
||||
PUBLISHER_SHA256: a06c9096dcb9727c13555b6be26c7effa707b01f06a4c561ba7a3635443cf2cc
|
||||
|
||||
steps:
|
||||
- name: Resolve target version
|
||||
id: v
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
INPUT_VERSION: ${{ github.event.inputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ "$EVENT_NAME" = "release" ]; then
|
||||
v="${RELEASE_TAG#v}"
|
||||
else
|
||||
v="$INPUT_VERSION"
|
||||
fi
|
||||
# The version becomes a git ref and a URL component. Reject malformed input.
|
||||
[[ "$v" =~ ^[0-9]+\.[0-9]+\.[0-9]+([a-zA-Z0-9.+-]*)$ ]]
|
||||
echo "version=$v" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Skip a version the registry already lists
|
||||
id: listed
|
||||
env:
|
||||
VERSION: ${{ steps.v.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
status=$(curl -sS --connect-timeout 10 --max-time 20 -o /dev/null -w '%{http_code}' "$SERVER_PATH/versions/$VERSION")
|
||||
case "$status" in
|
||||
200) echo "listed=true" >> "$GITHUB_OUTPUT"
|
||||
echo "- MCP registry: $SERVER_NAME $VERSION was already listed" >> "$GITHUB_STEP_SUMMARY" ;;
|
||||
404) echo "listed=false" >> "$GITHUB_OUTPUT" ;;
|
||||
*) echo "::error::Registry lookup returned HTTP $status"; exit 1 ;;
|
||||
esac
|
||||
|
||||
# mcp-publisher reads only ./server.json, so fetch nothing else.
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
if: ${{ steps.listed.outputs.listed == 'false' }}
|
||||
with:
|
||||
ref: v${{ steps.v.outputs.version }}
|
||||
sparse-checkout: server.json
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Check server.json matches the release
|
||||
if: ${{ steps.listed.outputs.listed == 'false' }}
|
||||
env:
|
||||
VERSION: ${{ steps.v.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 - <<'PYTHON'
|
||||
import json, os
|
||||
manifest = json.load(open("server.json", encoding="utf-8"))
|
||||
version = os.environ["VERSION"]
|
||||
assert manifest["name"] == os.environ["SERVER_NAME"], manifest["name"]
|
||||
found = [manifest["version"], *(p["version"] for p in manifest["packages"])]
|
||||
assert all(v == version for v in found), f"server.json versions {found} != {version}"
|
||||
PYTHON
|
||||
|
||||
- name: Wait for PyPI to serve the release
|
||||
if: ${{ steps.listed.outputs.listed == 'false' }}
|
||||
env:
|
||||
VERSION: ${{ steps.v.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for attempt in $(seq 1 40); do
|
||||
status=$(curl -sS --connect-timeout 10 --max-time 20 -o /dev/null -w '%{http_code}' \
|
||||
"https://pypi.org/pypi/remove-ai-watermarks/$VERSION/json" || true)
|
||||
if [ "$status" = "200" ]; then
|
||||
echo "PyPI serves $VERSION"
|
||||
exit 0
|
||||
fi
|
||||
echo "Attempt $attempt: PyPI returned HTTP $status for $VERSION"
|
||||
sleep 30
|
||||
done
|
||||
echo "::error::PyPI never served $VERSION"
|
||||
exit 1
|
||||
|
||||
- name: Publish with a verified mcp-publisher
|
||||
if: ${{ steps.listed.outputs.listed == 'false' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -fsSL -o mcp-publisher.tar.gz "$PUBLISHER_URL"
|
||||
echo "$PUBLISHER_SHA256 mcp-publisher.tar.gz" | sha256sum -c -
|
||||
tar xzf mcp-publisher.tar.gz mcp-publisher
|
||||
./mcp-publisher login github-oidc
|
||||
./mcp-publisher publish
|
||||
|
||||
- name: Read the entry back
|
||||
if: ${{ steps.listed.outputs.listed == 'false' }}
|
||||
env:
|
||||
VERSION: ${{ steps.v.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for attempt in $(seq 1 10); do
|
||||
status=$(curl -sS --connect-timeout 10 --max-time 20 -o /dev/null -w '%{http_code}' \
|
||||
"$SERVER_PATH/versions/$VERSION" || true)
|
||||
if [ "$status" = "200" ]; then
|
||||
echo "- MCP registry: $SERVER_NAME $VERSION is listed" >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 0
|
||||
fi
|
||||
echo "Attempt $attempt: registry returned HTTP $status"
|
||||
sleep 15
|
||||
done
|
||||
echo "::error::The registry does not list $SERVER_NAME $VERSION"
|
||||
exit 1
|
||||
@@ -36,7 +36,7 @@ The gate does not pass `--ignore-unfixed` to uv-secure, so a transitive CVE with
|
||||
|
||||
Command, gate, typing, and model-test invariants auto-load from [`.claude/rules/development.md`](.claude/rules/development.md). Environment recovery, CI behavior, and fixture policy live in [`docs/development.md`](docs/development.md).
|
||||
|
||||
Before a release, read [`docs/release-and-distribution.md`](docs/release-and-distribution.md). Treat the release as complete only after PyPI, Homebrew, the Hugging Face Space, and the ComfyUI Registry are verified; conda is not published. Keep the source-distribution public allowlist.
|
||||
Before a release, read [`docs/release-and-distribution.md`](docs/release-and-distribution.md). Treat the release as complete only after PyPI, Homebrew, the Hugging Face Space, and the ComfyUI Registry are verified; conda is not published. The MCP registry listing publishes itself from `publish-mcp-registry.yml` and does not block a release. Keep the source-distribution public allowlist.
|
||||
|
||||
The published Agent Skill is [`skills/remove-ai-watermarks/`](skills/remove-ai-watermarks/). Update it in the same change as any CLI edit that alters commands, extras, exit codes, mark keys, or the intended-use boundary. Install and publish steps: [`docs/agent-skill.md`](docs/agent-skill.md).
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ Read this reference for environment setup, dependency recovery, CI behavior, and
|
||||
- On an unreliable connection, sync `dev` plus only the required feature extras, such as `diffusion`, and run the checks directly instead of downloading every optional learned backend.
|
||||
- Run `uv` from the repository root or it may create a bare environment without the project dependencies.
|
||||
|
||||
`maintain.sh` starts with `uv sync --all-extras`, so a lean environment is replaced whenever the gate runs. Syncing back down can leave `opencv-python-headless` half-removed: `import cv2` then succeeds against an empty namespace while `cv2.INTER_LINEAR` is gone, which surfaces as a suite that stalls or reports hundreds of unrelated failures. Repair it with `uv sync --frozen --extra dev --reinstall-package opencv-python-headless` rather than re-reading the diff.
|
||||
`maintain.sh` starts with `uv sync --all-extras`, so a lean environment is replaced whenever the gate runs. Syncing back down can leave `opencv-python-headless` half-removed: `import cv2` then succeeds against an empty namespace while `cv2.INTER_LINEAR` is gone, which surfaces as a suite that stalls or reports hundreds of unrelated failures. Repair it with `uv sync --frozen --extra dev --reinstall-package opencv-python-headless` rather than re-reading the diff. A stale worktree environment can fail differently: `import cv2` raises `partially initialized module 'cv2' has no attribute 'gapi_wip_gst_GStreamerPipeline'`, because the lock installs `opencv-contrib-python` 4.x beside `opencv-python` 5.x into one `cv2` directory. Reinstalling the three OpenCV packages did not fix that on 2026-10-17; deleting the worktree's `.venv` and running `uv sync --frozen --all-extras` did.
|
||||
|
||||
Pyright is only meaningful with the extras its imports need. Scoped to `src/`, a lean environment reports `TrustMark`, `transformers` and `huggingface_hub` as unresolved, which reads as a type error and is not one; add `--extra trustmark --extra classify` before believing it.
|
||||
|
||||
|
||||
+3
-2
@@ -87,5 +87,6 @@ default mode, receives method-not-found and falls back to the handshake.
|
||||
`io.github.wiltodelta/remove-ai-watermarks`. The registry verifies ownership by
|
||||
the `mcp-name:` marker in `README.md`, which becomes the PyPI description, so it
|
||||
can only verify a release whose README carries that marker.
|
||||
[`glama.json`](../glama.json) names the maintainer for Glama. Publishing steps
|
||||
are in [release and distribution](release-and-distribution.md#mcp-registry).
|
||||
[`glama.json`](../glama.json) names the maintainer for Glama. Each release is
|
||||
published automatically; the workflow is described in
|
||||
[release and distribution](release-and-distribution.md#mcp-registry).
|
||||
@@ -227,14 +227,13 @@ submission state and links live in
|
||||
`uvx remove-ai-watermarks mcp`. The registry accepts it only after PyPI serves a
|
||||
release whose description, the README, carries the `mcp-name:` marker, and
|
||||
`tests/test_mcp_server.py` pins `server.json` to the package version. The listing
|
||||
is not a release surface that blocks a release; after PyPI publishes a version:
|
||||
is not a release surface that blocks a release.
|
||||
|
||||
1. `mcp-publisher login github` with an account that owns `wiltodelta`;
|
||||
2. `mcp-publisher validate` and then `mcp-publisher publish` from the
|
||||
repository root;
|
||||
3. read the entry back from
|
||||
`https://registry.modelcontextprotocol.io/v0/servers?search=io.github.wiltodelta/remove-ai-watermarks`
|
||||
and check its version.
|
||||
[`publish-mcp-registry.yml`](../.github/workflows/publish-mcp-registry.yml)
|
||||
publishes it on every published GitHub Release, once PyPI serves that version.
|
||||
GitHub OIDC proves ownership of the `io.github.wiltodelta` namespace, so no login
|
||||
or secret is stored. To list a version already on PyPI, dispatch the workflow
|
||||
with that version; a version the registry already lists is skipped.
|
||||
|
||||
[`glama.json`](../glama.json) names the maintainer for Glama's claim flow.
|
||||
Client setup and the tool contract are in [the MCP server guide](mcp-server.md).
|
||||
|
||||
@@ -280,6 +280,12 @@ protocol is in [`synthid-detector-removal-plan.md`](synthid-detector-removal-pla
|
||||
Scope honestly: this tier certifies strength floors on a handful of images per vendor, and
|
||||
that is all it can do. See `docs/synthid.md`.
|
||||
|
||||
The project therefore publishes no aggregate pass rate per model or resolution: a large
|
||||
enough sample needs bulk automated checks, which the OpenAI restriction above and the
|
||||
SynthID Detector's [additional terms](https://synthid.com/terms) both forbid. Public
|
||||
removal claims rest on this method and on individual hash-bound rows such as
|
||||
`data/evaluations/video-synthid-oracle.csv`, never on an extrapolated rate.
|
||||
|
||||
### D1. Sampling frame
|
||||
|
||||
Select a representative local sample without committing images, identifiers, dataset sizes, or oracle results. Stratify by signal family and preserve the sampling method outside the public repository.
|
||||
|
||||
@@ -214,3 +214,61 @@ def test_clawhub_uses_local_install(tmp_path: Path) -> None:
|
||||
"login --token synthetic",
|
||||
"inspect remove-ai-watermarks --file SKILL.md",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name != "posix" or shutil.which("bash") is None, reason="Executes an Ubuntu Bash workflow")
|
||||
@pytest.mark.parametrize(("status", "listed"), [("200", "true"), ("404", "false"), ("503", None)])
|
||||
def test_mcp_registry_lookup_skips_listed_versions_and_fails_on_other_replies(
|
||||
tmp_path: Path, status: str, listed: str | None
|
||||
) -> None:
|
||||
workflow = _workflow("publish-mcp-registry.yml")
|
||||
script = _step("publish-mcp-registry.yml", "publish", "Skip a version the registry already lists")["run"]
|
||||
bin_dir = tmp_path / "bin"
|
||||
bin_dir.mkdir()
|
||||
curl = bin_dir / "curl"
|
||||
curl.write_text(f'#!/bin/sh\nprintf "%s\\n" "$*" >> "$CALL_LOG"\nprintf {status}\n')
|
||||
curl.chmod(0o755)
|
||||
env = {
|
||||
**os.environ,
|
||||
**workflow["jobs"]["publish"]["env"],
|
||||
"PATH": str(bin_dir) + os.pathsep + os.environ["PATH"],
|
||||
"VERSION": "0.46.0",
|
||||
"GITHUB_OUTPUT": str(tmp_path / "output"),
|
||||
"GITHUB_STEP_SUMMARY": str(tmp_path / "summary"),
|
||||
"CALL_LOG": str(tmp_path / "calls"),
|
||||
}
|
||||
result = subprocess.run(["bash", "-c", script], cwd=tmp_path, env=env, capture_output=True, text=True)
|
||||
# The server name carries a slash, so it must reach the registry percent-encoded.
|
||||
assert "/v0/servers/io.github.wiltodelta%2Fremove-ai-watermarks/versions/0.46.0" in (tmp_path / "calls").read_text()
|
||||
if listed is None:
|
||||
assert result.returncode != 0
|
||||
return
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert f"listed={listed}" in (tmp_path / "output").read_text()
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name != "posix" or shutil.which("bash") is None, reason="Executes an Ubuntu Bash workflow")
|
||||
@pytest.mark.parametrize(
|
||||
("release", "package", "accepted"),
|
||||
[("1.0.0", "1.0.0", True), ("1.0.1", "1.0.0", False), ("1.0.0", "1.0.1", False)],
|
||||
)
|
||||
def test_mcp_registry_publishes_only_a_server_json_that_matches_the_release(
|
||||
tmp_path: Path, release: str, package: str, accepted: bool
|
||||
) -> None:
|
||||
workflow = _workflow("publish-mcp-registry.yml")
|
||||
script = _step("publish-mcp-registry.yml", "publish", "Check server.json matches the release")["run"]
|
||||
manifest = json.loads((ROOT / "server.json").read_text(encoding="utf-8"))
|
||||
manifest["version"] = "1.0.0"
|
||||
# A stale package entry under a bumped server version must not publish either.
|
||||
manifest["packages"][0]["version"] = package
|
||||
(tmp_path / "server.json").write_text(json.dumps(manifest), encoding="utf-8")
|
||||
env = {**os.environ, **workflow["jobs"]["publish"]["env"], "VERSION": release}
|
||||
result = subprocess.run(["bash", "-c", script], cwd=tmp_path, env=env, capture_output=True, text=True)
|
||||
assert (result.returncode == 0) == accepted, result.stderr
|
||||
|
||||
|
||||
def test_mcp_registry_publishes_on_release_with_an_oidc_token() -> None:
|
||||
workflow = _workflow("publish-mcp-registry.yml")
|
||||
assert workflow["permissions"]["id-token"] == "write"
|
||||
# PyYAML reads the bare `on:` key as the boolean True.
|
||||
assert set(workflow[True]) == {"release", "workflow_dispatch"}
|
||||
Reference in new issue
Block a user