diff --git a/.agents/skills/develop-maple-proxy/SKILL.md b/.agents/skills/develop-maple-proxy/SKILL.md index 42890b9bd..1b9d84cc6 100644 --- a/.agents/skills/develop-maple-proxy/SKILL.md +++ b/.agents/skills/develop-maple-proxy/SKILL.md @@ -5,7 +5,7 @@ description: Develop and review the maple-proxy Rust crate, binary, container, a # Develop Maple Proxy -Work from the `OpenSecretCloud/Maple` repository root; the component source is +Work from the `MaplePrivacyLabs/Maple` repository root; the component source is under `proxy/`. Read the repository-root `AGENTS.md`, `proxy/README.md`, affected source and tests, and the root `.github/workflows/proxy-*.yml` files relevant to the change. @@ -108,11 +108,11 @@ Release, `.github/workflows/proxy-publish.yml` independently compares that release's proxy version with the previous stable Maple Release. Unchanged versions skip, including the unbackfilled 0.3.3 baseline. A strictly newer, previously unpublished version automatically publishes Linux AMD64/ARM64 to -`ghcr.io/opensecretcloud/maple-proxy` with exact, minor, major, and `latest` +`ghcr.io/mapleprivacylabs/maple-proxy` with exact, minor, major, and `latest` tags. The workflow is serialized, rejects rollback and stale releases, verifies -the public manifest, and supports manual retry from `master`. Maple's Actions -repository must retain write access to that existing organization-scoped GHCR -package. Treat any namespace, trigger, version policy, or package-access change +the public manifest, and supports manual retry from `master`. The transferred Maple repository uses its `GITHUB_TOKEN` to publish in +`MaplePrivacyLabs`; the new package must be public and grant Maple Actions write +access. Existing old-namespace images remain available but receive no updates. Treat any namespace, trigger, version policy, or package-access change as a separate production-authority decision. ## Report diff --git a/.agents/skills/develop-maple/SKILL.md b/.agents/skills/develop-maple/SKILL.md index a0a8791a9..7d2e7fc48 100644 --- a/.agents/skills/develop-maple/SKILL.md +++ b/.agents/skills/develop-maple/SKILL.md @@ -5,7 +5,7 @@ description: Develop and debug ordinary non-Agent-Mode Maple features and fixes # Develop Maple -Work from the `OpenSecretCloud/Maple` repository root. Treat `justfile`, `frontend/package.json`, `flake.nix`, `scripts/ci/`, and `.github/workflows/` as the command sources of truth. Check them again when they disagree with prose documentation. +Work from the `MaplePrivacyLabs/Maple` repository root. Treat `justfile`, `frontend/package.json`, `flake.nix`, `scripts/ci/`, and `.github/workflows/` as the command sources of truth. Check them again when they disagree with prose documentation. ## Route Specialized Work diff --git a/.agents/skills/develop-opensecret-sdk/SKILL.md b/.agents/skills/develop-opensecret-sdk/SKILL.md index 4cc4daad3..db0285499 100644 --- a/.agents/skills/develop-opensecret-sdk/SKILL.md +++ b/.agents/skills/develop-opensecret-sdk/SKILL.md @@ -5,7 +5,7 @@ description: Develop and review the OpenSecret TypeScript/React and Rust SDKs un # Develop the OpenSecret SDK -Work from `OpenSecretCloud/Maple/sdk`. Read the repository-root `AGENTS.md`, +Work from `MaplePrivacyLabs/Maple/sdk`. Read the repository-root `AGENTS.md`, `sdk/README.md`, the affected implementation and tests, and the root `.github/workflows/sdk-*.yml` files relevant to the change. diff --git a/.agents/skills/release-maple/SKILL.md b/.agents/skills/release-maple/SKILL.md index f04426a48..ab0c993c5 100644 --- a/.agents/skills/release-maple/SKILL.md +++ b/.agents/skills/release-maple/SKILL.md @@ -33,13 +33,30 @@ commit, external effect, and authority provided by the user. and repairs minor, major, and `latest` aliases without rebuilding an existing exact image. Version `0.3.3` is the explicit unbackfilled migration baseline. The container remains separately versioned at - `ghcr.io/opensecretcloud/maple-proxy`. + `ghcr.io/mapleprivacylabs/maple-proxy`. - GitHub Release creation does not itself submit the release IPA or AAB to Apple App Store review or Google Play. Never push or merge `master`, create a release, retry a workflow, upload to a store, submit for review, or alter a rollout merely to see whether it works. +## Repository-transfer checkpoint + +After the transfer to `MaplePrivacyLabs/Maple`, merge the prepared canonical +repository metadata before creating a new release. Keep the existing updater +Worker serving its deployed metadata until the first normal new-org release. +Do not manually republish retained v3.3.10 updater metadata: its asset URLs use +`OpenSecretCloud/Maple`, while the publisher and new Worker correctly require +the current canonical owner. The next release generates new-owner URLs without +changing the legacy updater fallback compiled into existing clients. + +The first eligible proxy container publication creates +`ghcr.io/mapleprivacylabs/maple-proxy`. GitHub creates new packages privately; +make that package public in its settings, retain Maple Actions write access, +and rerun only the proxy publisher if anonymous verification stops there. Do +not create another release, change proxy versions, or overwrite exact tags to +repair package visibility. Existing old-namespace images receive no updates. + ## Prepare the version 1. Prepare the bump on a clean focused branch based on current `origin/master`; @@ -50,7 +67,7 @@ store, submit for review, or alter a rollout merely to see whether it works. git fetch origin master git merge-base --is-ancestor origin/master HEAD current_version="$(nix develop --no-update-lock-file .#ci -c just get-version | tail -n 1)" - released_version="$(gh api repos/OpenSecretCloud/Maple/releases/latest --jq '.tag_name | ltrimstr("v")')" + released_version="$(gh api repos/MaplePrivacyLabs/Maple/releases/latest --jq '.tag_name | ltrimstr("v")')" printf 'current=%s released=%s\n' "$current_version" "$released_version" ``` @@ -112,7 +129,7 @@ crates.io or GHCR publish. Preview GitHub's generated notes: ```bash -gh api --method POST repos/OpenSecretCloud/Maple/releases/generate-notes \ +gh api --method POST repos/MaplePrivacyLabs/Maple/releases/generate-notes \ -f tag_name="$tag" \ -f target_commitish="$head_sha" \ -f previous_tag_name="$previous_tag" | jq -r '.name, .body' @@ -134,7 +151,7 @@ Create the GitHub Release exactly once. This creates the tag in the same flow: ```bash gh release create "$tag" \ - --repo OpenSecretCloud/Maple \ + --repo MaplePrivacyLabs/Maple \ --target "$head_sha" \ --title "$tag" \ --notes-file "$notes_file" @@ -148,12 +165,12 @@ release and workflow resolve to `head_sha`. Find and watch the new `Release` run: ```bash -gh run list --repo OpenSecretCloud/Maple --workflow Release --event release \ +gh run list --repo MaplePrivacyLabs/Maple --workflow Release --event release \ --commit "$head_sha" --limit 10 \ --json databaseId,displayTitle,headSha,status,conclusion,url gh run watch RELEASE_RUN_ID \ - --repo OpenSecretCloud/Maple --exit-status --compact + --repo MaplePrivacyLabs/Maple --exit-status --compact ``` Stay with every platform build, signature/canonical proof, artifact upload, @@ -166,17 +183,17 @@ After `Release` succeeds, inspect the two required publication handoffs. Do not rerun the core Release to repair either sibling: ```bash -gh run list --repo OpenSecretCloud/Maple --workflow 'Publish updater metadata' \ +gh run list --repo MaplePrivacyLabs/Maple --workflow 'Publish updater metadata' \ --commit "$head_sha" --limit 10 \ --json databaseId,status,conclusion,headSha,createdAt,url pages_workflow='Promote Pages production' -pages_enabled="$(gh variable list --repo OpenSecretCloud/Maple --json name,value \ +pages_enabled="$(gh variable list --repo MaplePrivacyLabs/Maple --json name,value \ --jq '.[] | select(.name == "MAPLE_PAGES_PRODUCTION_ENABLED") | .value')" || exit 1 if [ "$pages_enabled" = true ]; then pages_workflow='Publish Pages' fi -gh run list --repo OpenSecretCloud/Maple --workflow "$pages_workflow" \ +gh run list --repo MaplePrivacyLabs/Maple --workflow "$pages_workflow" \ --limit 10 \ --json databaseId,status,conclusion,headSha,createdAt,url ``` @@ -192,7 +209,7 @@ the unbackfilled `0.3.3` baseline. Retry it with manual dispatch; never create a proxy tag or Release and never rerun the core Release to repair it: ```bash -gh run list --repo OpenSecretCloud/Maple --workflow 'Publish proxy container' \ +gh run list --repo MaplePrivacyLabs/Maple --workflow 'Publish proxy container' \ --limit 10 \ --json databaseId,status,conclusion,headSha,createdAt,url ``` @@ -209,14 +226,14 @@ Report and repair sibling failures without altering completed release artifacts. Confirm the production ref in either mode: ```bash -pages_sha="$(gh api repos/OpenSecretCloud/Maple/git/ref/heads/pages-production --jq .object.sha)" +pages_sha="$(gh api repos/MaplePrivacyLabs/Maple/git/ref/heads/pages-production --jq .object.sha)" [[ "$pages_sha" == "$head_sha" ]] ``` In legacy mode only, inspect Cloudflare's exact-commit check: ```bash -gh api "repos/OpenSecretCloud/Maple/commits/$head_sha/check-runs" --jq ' +gh api "repos/MaplePrivacyLabs/Maple/commits/$head_sha/check-runs" --jq ' [.check_runs[] | select(.name == "Cloudflare Pages") | select(.app.name == "Cloudflare Workers and Pages") @@ -240,13 +257,13 @@ recreate a Release or rerun the core release merely to repair Pages. On failure, read the failed logs before acting: ```bash -gh run view RELEASE_RUN_ID --repo OpenSecretCloud/Maple --log-failed +gh run view RELEASE_RUN_ID --repo MaplePrivacyLabs/Maple --log-failed ``` Retry only a terminal failure proven to be transient infrastructure trouble: ```bash -gh run rerun RELEASE_RUN_ID --repo OpenSecretCloud/Maple --failed +gh run rerun RELEASE_RUN_ID --repo MaplePrivacyLabs/Maple --failed ``` Do not classify version/proof mismatches, deterministic builds, signing @@ -258,11 +275,11 @@ or recreate a published release without separate explicit direction. Verify the published release and its assets: ```bash -gh release view "$tag" --repo OpenSecretCloud/Maple \ +gh release view "$tag" --repo MaplePrivacyLabs/Maple \ --json tagName,name,isDraft,isPrerelease,publishedAt,targetCommitish,url,assets mkdir -p artifacts -gh release download "$tag" --repo OpenSecretCloud/Maple --dir artifacts +gh release download "$tag" --repo MaplePrivacyLabs/Maple --dir artifacts nix develop --no-update-lock-file .#ci -c \ ./scripts/ci/verify-release-artifacts.sh artifacts proxy ``` @@ -303,7 +320,7 @@ trigger a release retry, or be reported as a Maple release failure. Inspect it only when Zapstore status is specifically useful: ```bash -gh run list --repo OpenSecretCloud/Maple --workflow 'Publish to Zapstore' \ +gh run list --repo MaplePrivacyLabs/Maple --workflow 'Publish to Zapstore' \ --commit "$head_sha" --limit 10 \ --json databaseId,status,conclusion,headSha,createdAt,url ``` diff --git a/.agents/skills/release-maple/scripts/preflight.sh b/.agents/skills/release-maple/scripts/preflight.sh index 53944e010..3208e279b 100755 --- a/.agents/skills/release-maple/scripts/preflight.sh +++ b/.agents/skills/release-maple/scripts/preflight.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash set -euo pipefail -readonly expected_repo="OpenSecretCloud/Maple" -readonly remote_url="https://github.com/OpenSecretCloud/Maple.git" +readonly expected_repo="MaplePrivacyLabs/Maple" +readonly remote_url="https://github.com/MaplePrivacyLabs/Maple.git" fail() { printf 'release preflight: %s\n' "$*" >&2 diff --git a/.github/workflows/proxy-publish.yml b/.github/workflows/proxy-publish.yml index daf4651bd..a016937b6 100644 --- a/.github/workflows/proxy-publish.yml +++ b/.github/workflows/proxy-publish.yml @@ -18,21 +18,28 @@ concurrency: env: REGISTRY: ghcr.io - IMAGE_NAME: opensecretcloud/maple-proxy + IMAGE_NAME: mapleprivacylabs/maple-proxy UNBACKFILLED_PROXY_BASELINE: 0.3.3 jobs: prepare: name: Validate proxy publication if: >- - (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/master') || + github.repository == 'MaplePrivacyLabs/Maple' && + github.repository_id == '923138240' && + github.repository_owner_id == '322649754' && + github.ref == 'refs/heads/master' && + ((github.event_name == 'workflow_dispatch') || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'release' && github.event.workflow_run.path == '.github/workflows/release.yml' && - github.event.workflow_run.head_repository.full_name == github.repository) + github.event.workflow_run.head_repository.full_name == github.repository)) runs-on: ubuntu-latest timeout-minutes: 10 + permissions: + contents: read + packages: read outputs: publish: ${{ steps.plan.outputs.publish }} reconcile: ${{ steps.plan.outputs.reconcile }} @@ -157,13 +164,9 @@ jobs: esac fi - registry_token="$( - curl --fail --silent --show-error \ - "https://${REGISTRY}/token?scope=repository:${IMAGE_NAME}:pull" | jq -er '.token' - )" - curl --fail --silent --show-error \ - --header "Authorization: Bearer ${registry_token}" \ - "https://${REGISTRY}/v2/${IMAGE_NAME}/tags/list?n=10000" \ + # Only authenticated package metadata can confirm initial creation. + # Anonymous denial/network errors must never mean an empty registry. + python3 -I scripts/ci/proxy_registry_inventory.py \ >"${RUNNER_TEMP}/proxy-tags.json" plan="${RUNNER_TEMP}/proxy-publish-plan" @@ -452,6 +455,10 @@ jobs: } assert_release_is_current + # GitHub creates a new container package as private. An operator must + # make it public before anonymous verification or alias publication. + # This workflow never changes package visibility on the user's behalf. + python3 -I scripts/ci/proxy_registry_inventory.py --require-public >/dev/null image="${REGISTRY}/${IMAGE_NAME}" source_digest_dir="" if [ "${PUBLISHED_NOW}" = "true" ]; then diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index d7bb8bcd0..4cea35304 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -4,7 +4,7 @@ version = "3.3.11" description = "Maple AI" authors = ["tony@trymaple.ai"] license = "MIT" -repository = "https://github.com/OpenSecretCloud/Maple" +repository = "https://github.com/MaplePrivacyLabs/Maple" edition = "2021" rust-version = "1.94.1" diff --git a/frontend/src/routes/proof.tsx b/frontend/src/routes/proof.tsx index aaf4aa3a6..cc6357017 100644 --- a/frontend/src/routes/proof.tsx +++ b/frontend/src/routes/proof.tsx @@ -612,7 +612,7 @@ function Verify() {
Both our{" "}
Our code is open source:{" "}
{
test("builds the current Maple GitHub URLs from the shared repository identity", () => {
- expect(GITHUB_REPOSITORY).toBe("OpenSecretCloud/Maple");
- expect(GITHUB_REPOSITORY_URL).toBe("https://github.com/OpenSecretCloud/Maple");
+ expect(GITHUB_REPOSITORY).toBe("MaplePrivacyLabs/Maple");
+ expect(GITHUB_REPOSITORY_URL).toBe("https://github.com/MaplePrivacyLabs/Maple");
expect(GITHUB_RELEASES_API_URL).toBe(
- "https://api.github.com/repos/OpenSecretCloud/Maple/releases/latest"
+ "https://api.github.com/repos/MaplePrivacyLabs/Maple/releases/latest"
);
expect(GITHUB_RELEASES_LATEST_URL).toBe(
- "https://github.com/OpenSecretCloud/Maple/releases/latest"
+ "https://github.com/MaplePrivacyLabs/Maple/releases/latest"
);
});
});
@@ -117,7 +117,7 @@ describe("getLatestDownloadInfo", () => {
tag_name: "v9.9.9",
name: "v9.9.9",
published_at: "2026-08-23T00:00:00Z",
- html_url: "https://github.com/OpenSecretCloud/Maple/releases/tag/v9.9.9",
+ html_url: "https://github.com/MaplePrivacyLabs/Maple/releases/tag/v9.9.9",
assets: [
{
name: "Maple_9.9.9_universal.dmg",
@@ -144,7 +144,7 @@ describe("getLatestDownloadInfo", () => {
linuxRpm: GITHUB_RELEASES_LATEST_URL,
androidApk: GITHUB_RELEASES_LATEST_URL
},
- releaseUrl: "https://github.com/OpenSecretCloud/Maple/releases/tag/v9.9.9"
+ releaseUrl: "https://github.com/MaplePrivacyLabs/Maple/releases/tag/v9.9.9"
});
});
diff --git a/proxy/Cargo.toml b/proxy/Cargo.toml
index c78dece6f..5b3132de3 100644
--- a/proxy/Cargo.toml
+++ b/proxy/Cargo.toml
@@ -5,8 +5,8 @@ edition = "2021"
authors = ["OpenSecret"]
description = "Lightweight OpenAI-compatible proxy server for Maple/OpenSecret TEE infrastructure"
license = "MIT"
-repository = "https://github.com/OpenSecretCloud/Maple"
-homepage = "https://github.com/OpenSecretCloud/Maple/tree/master/proxy"
+repository = "https://github.com/MaplePrivacyLabs/Maple"
+homepage = "https://github.com/MaplePrivacyLabs/Maple/tree/master/proxy"
keywords = ["openai", "proxy", "tee", "opensecret", "maple"]
categories = ["web-programming::http-server", "api-bindings"]
include = [
diff --git a/proxy/Dockerfile b/proxy/Dockerfile
index ca1f45c08..f2a5e9954 100644
--- a/proxy/Dockerfile
+++ b/proxy/Dockerfile
@@ -20,7 +20,7 @@ RUN cargo build --locked --release --bin maple-proxy
# Runtime stage - minimal image for production
FROM docker.io/debian:bookworm-slim AS runtime
-LABEL org.opencontainers.image.source="https://github.com/OpenSecretCloud/Maple"
+LABEL org.opencontainers.image.source="https://github.com/MaplePrivacyLabs/Maple"
# Install runtime dependencies
RUN apt-get update && apt-get install -y \
diff --git a/proxy/README.md b/proxy/README.md
index a704a9f94..38f380928 100644
--- a/proxy/README.md
+++ b/proxy/README.md
@@ -28,8 +28,8 @@ After that release, verify the assets are present and use the stable download
URLs, for example:
```bash
-curl -LO https://github.com/OpenSecretCloud/Maple/releases/latest/download/maple-proxy-linux-x86_64.tar.gz
-curl -LO https://github.com/OpenSecretCloud/Maple/releases/latest/download/maple-proxy-release-final.sha256
+curl -LO https://github.com/MaplePrivacyLabs/Maple/releases/latest/download/maple-proxy-linux-x86_64.tar.gz
+curl -LO https://github.com/MaplePrivacyLabs/Maple/releases/latest/download/maple-proxy-release-final.sha256
sha256sum --check --ignore-missing maple-proxy-release-final.sha256
```
@@ -37,7 +37,7 @@ There is no separate proxy GitHub Release or proxy release tag. To build from
source now:
```bash
-git clone https://github.com/OpenSecretCloud/Maple.git
+git clone https://github.com/MaplePrivacyLabs/Maple.git
cd Maple/proxy
cargo build --locked --release
```
@@ -289,24 +289,34 @@ cargo run --locked
### Pre-built Image
-The currently published GHCR `latest` image is the independently released
-standalone proxy 0.3.2. The first Maple-owned publication deliberately does not
-backfill the in-tree 0.3.3 version. After the next proxy version change ships in
-a successful stable Maple Release, the release-following publisher updates the
-same `ghcr.io/opensecretcloud/maple-proxy` package automatically.
+New container publications use `ghcr.io/mapleprivacylabs/maple-proxy` after the
+Maple repository transfer. The release-following publisher builds the eligible
+proxy version from a successful stable Maple Release and verifies its exact tag,
+platforms, provenance, and aliases. The first publication in this namespace must
+complete and the package must be public before the commands below will work.
+GitHub creates new packages privately. Once the first image exists, set the new
+package to public in GitHub package settings, retain Maple Actions write access,
+and rerun only the container publisher if anonymous verification was blocked.
+The retry verifies an existing exact version rather than overwriting it.
-To run the legacy image deliberately:
+The old `ghcr.io/opensecretcloud/maple-proxy` package remains available at its
+existing versions but receives no new publications. Existing deployments must
+change their image URL to receive future updates; registry image names do not
+follow the repository transfer redirect. Version `0.3.3` remains the explicit
+unbackfilled baseline. The next prepared proxy version is `0.3.4`.
+
+After the first new-namespace publication is verified:
```bash
# Pull the latest image
-docker pull ghcr.io/opensecretcloud/maple-proxy:latest
+docker pull ghcr.io/mapleprivacylabs/maple-proxy:latest
# Run with your API key
docker run -p 8080:8080 \
-e MAPLE_BACKEND_URL=https://enclave.trymaple.ai \
-e MAPLE_REQUEST_TIMEOUT_SECS=300 \
-e MAPLE_STREAM_IDLE_TIMEOUT_SECS=300 \
- ghcr.io/opensecretcloud/maple-proxy:latest
+ ghcr.io/mapleprivacylabs/maple-proxy:latest
```
### Build from Source
@@ -324,7 +334,7 @@ just docker-run
1. **Option A: Use the published image from GHCR**
```bash
# In your docker-compose.yml, use:
-image: ghcr.io/opensecretcloud/maple-proxy:latest
+image: ghcr.io/mapleprivacylabs/maple-proxy:latest
```
2. **Option B: Build your own image**
@@ -412,7 +422,7 @@ environment:
### Docker Images & CI/CD
-The source lives under [`proxy/`](https://github.com/OpenSecretCloud/Maple/tree/master/proxy)
+The source lives under [`proxy/`](https://github.com/MaplePrivacyLabs/Maple/tree/master/proxy)
in the Maple repository. Root, path-scoped workflows run locked Rust checks,
supply-chain policy, and non-publishing AMD64/ARM64 container builds for proxy
changes. After every successful stable Maple Release, a separate serialized
@@ -423,8 +433,10 @@ later version publishes native AMD64/ARM64 images with per-platform provenance,
verifies their exact build digests and release labels, then reconciles the minor,
major, and `latest` aliases. Existing exact versions are verified without being
overwritten, and manual dispatch safely retries verification or alias repair.
-Maple must have Actions write access to the existing organization-scoped GHCR
-package; local recipes intentionally cannot publish to it.
+The transferred Maple repository publishes with its `GITHUB_TOKEN` in the
+`MaplePrivacyLabs` organization. No old-organization package token is needed.
+The package must grant Maple Actions write access and be public for anonymous
+verification; local recipes intentionally cannot publish to it.
`proxy/Cargo.lock`, `sdk/rust/Cargo.lock`, and
`frontend/src-tauri/Cargo.lock` remain separate lockfiles. Runtime dependency
diff --git a/proxy/justfile b/proxy/justfile
index 016f3324e..8159c57b2 100644
--- a/proxy/justfile
+++ b/proxy/justfile
@@ -240,8 +240,8 @@ docker-clean:
# Pull from GitHub Container Registry
ghcr-pull tag="latest":
@echo "📥 Pulling from GitHub Container Registry..."
- @{{container}} pull ghcr.io/opensecretcloud/maple-proxy:{{tag}}
- @echo "✅ Pulled ghcr.io/opensecretcloud/maple-proxy:{{tag}}"
+ @{{container}} pull ghcr.io/mapleprivacylabs/maple-proxy:{{tag}}
+ @echo "✅ Pulled ghcr.io/mapleprivacylabs/maple-proxy:{{tag}}"
# Compatibility alias for the complete repository check.
check-all: check
diff --git a/repo.meta.json b/repo.meta.json
index 23014371d..90bb069c1 100644
--- a/repo.meta.json
+++ b/repo.meta.json
@@ -1,6 +1,6 @@
{
"github": {
- "owner": "OpenSecretCloud",
+ "owner": "MaplePrivacyLabs",
"repository": "Maple"
}
}
diff --git a/scripts/ci/proxy_registry_inventory.py b/scripts/ci/proxy_registry_inventory.py
new file mode 100644
index 000000000..8859021a5
--- /dev/null
+++ b/scripts/ci/proxy_registry_inventory.py
@@ -0,0 +1,177 @@
+#!/usr/bin/env python3
+"""Read the canonical proxy package inventory without masking registry failures.
+
+Only GitHub's authenticated package metadata can admit an initial package. A
+registry 401, 403, 404, timeout or malformed response is never an empty registry.
+The workflow supplies a repository-scoped GITHUB_TOKEN with packages: read.
+"""
+
+from __future__ import annotations
+
+import argparse
+from dataclasses import dataclass
+import json
+import os
+import re
+import sys
+from urllib.error import HTTPError, URLError
+from urllib.request import HTTPRedirectHandler, Request, build_opener
+
+OWNER = "MaplePrivacyLabs"
+OWNER_ID = 322649754
+REPOSITORY = "MaplePrivacyLabs/Maple"
+REPOSITORY_ID = 923138240
+PACKAGE = "maple-proxy"
+IMAGE = "mapleprivacylabs/maple-proxy"
+API = "https://api.github.com"
+REGISTRY = "https://ghcr.io"
+PACKAGE_URL = f"{API}/orgs/{OWNER}/packages/container/{PACKAGE}"
+MAX_JSON_BYTES = 4 * 1024 * 1024
+
+
+class InventoryError(ValueError):
+ """Fixed-category error, with no remote body or credential interpolation."""
+
+
+@dataclass(frozen=True)
+class Response:
+ status: int
+ data: object = None
+ link: str | None = None
+
+
+class NoRedirect(HTTPRedirectHandler):
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
+ return None
+
+
+def get_json(url: str, token: str | None = None) -> Response:
+ headers = {"Accept": "application/vnd.github+json", "User-Agent": "Maple-proxy-package-inventory"}
+ if url.startswith(API + "/"):
+ headers["X-GitHub-Api-Version"] = "2022-11-28"
+ if token:
+ headers["Authorization"] = "Bearer " + token
+ try:
+ with build_opener(NoRedirect).open(Request(url, headers=headers), timeout=30) as response:
+ body = response.read(MAX_JSON_BYTES + 1)
+ if len(body) > MAX_JSON_BYTES:
+ raise InventoryError("Package inventory response exceeds size limit")
+ return Response(response.status, json.loads(body), response.headers.get("Link"))
+ except HTTPError as error:
+ status = error.code
+ error.close()
+ return Response(status)
+ except (OSError, URLError, ValueError):
+ raise InventoryError("Package inventory request failed") from None
+
+
+def require_public(metadata: object) -> None:
+ if not isinstance(metadata, dict):
+ raise InventoryError("Invalid package metadata")
+ owner = metadata.get("owner") or {}
+ repository = metadata.get("repository") or {}
+ if (
+ metadata.get("name") != PACKAGE
+ or metadata.get("package_type") != "container"
+ or not isinstance(owner, dict)
+ or owner.get("id") != OWNER_ID
+ or owner.get("login") != OWNER
+ or not isinstance(repository, dict)
+ or repository.get("id") != REPOSITORY_ID
+ or repository.get("full_name") != REPOSITORY
+ ):
+ raise InventoryError("Package is not linked to the canonical Maple repository")
+ if metadata.get("visibility") != "public":
+ raise InventoryError(
+ "The new proxy package must be made public in GitHub package settings; "
+ "then rerun Publish proxy container to verify and reconcile its aliases"
+ )
+
+
+def confirm_missing(token: str, get=get_json) -> None:
+ # GitHub can mask unavailable resources as 404. Also require successful
+ # authenticated package enumeration; a missing packages permission must not
+ # authorize treating an inventory failure as an empty package. This lists
+ # packages readable by this repository token, not unrelated private packages.
+ # GitHub denies pushes to an existing package not connected to this repository.
+ for page in range(1, 101):
+ response = get(f"{API}/orgs/{OWNER}/packages?package_type=container&per_page=100&page={page}", token)
+ if response.status != 200 or not isinstance(response.data, list):
+ raise InventoryError("Cannot confirm initial package with authenticated package listing")
+ if len(response.data) > 100:
+ raise InventoryError("Invalid package listing length")
+ for package in response.data:
+ if not isinstance(package, dict) or not isinstance(package.get("name"), str):
+ raise InventoryError("Invalid package listing entry")
+ if package["name"].casefold() == PACKAGE:
+ raise InventoryError("Package metadata and package listing disagree")
+ if not response.link:
+ return
+ # Construct the next fixed-origin URL ourselves; never follow a Link URL
+ # carrying an API credential. Fail on unexpected pagination metadata.
+ link_pattern = r'<[^>\r\n]+>;\s*rel="(?:next|prev|first|last)"'
+ if not re.fullmatch(link_pattern + r'(?:,\s*' + link_pattern + r')*', response.link):
+ raise InventoryError("Invalid package listing pagination")
+ if 'rel="next"' not in response.link:
+ return
+ raise InventoryError("Package listing exceeds pagination limit")
+
+
+def inventory(token: str, *, public_only: bool = False, get=get_json) -> dict:
+ if not token:
+ raise InventoryError("Package inventory requires the workflow token")
+ response = get(PACKAGE_URL, token)
+ if response.status == 404 and not public_only:
+ confirm_missing(token, get)
+ return {"name": IMAGE, "tags": []}
+ if response.status != 200:
+ raise InventoryError("Authenticated proxy package metadata is unavailable")
+ require_public(response.data)
+ if public_only:
+ return {"name": IMAGE, "visibility": "public"}
+
+ # Public access is part of the contract. Do not use the workflow credential
+ # to hide a public-pull regression; this registry token is anonymous/pull-only.
+ response = get(f"{REGISTRY}/token?scope=repository:{IMAGE}:pull")
+ if response.status != 200 or not isinstance(response.data, dict):
+ raise InventoryError("Cannot obtain anonymous proxy registry token")
+ registry_token = response.data.get("token")
+ if not isinstance(registry_token, str) or not re.fullmatch(r"[A-Za-z0-9._~+/=-]{1,16384}", registry_token):
+ raise InventoryError("Invalid anonymous proxy registry token")
+ response = get(f"{REGISTRY}/v2/{IMAGE}/tags/list?n=10000", registry_token)
+ if response.status != 200 or not isinstance(response.data, dict) or response.link:
+ raise InventoryError("Public proxy tag inventory is unavailable or incomplete")
+ tags = response.data.get("tags")
+ if (
+ response.data.get("name") != IMAGE
+ or not isinstance(tags, list)
+ or len(tags) > 10000
+ or any(not isinstance(tag, str) or not re.fullmatch(r"[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}", tag) for tag in tags)
+ or len(set(tags)) != len(tags)
+ ):
+ raise InventoryError("Invalid public proxy tag inventory")
+ return {"name": IMAGE, "tags": tags}
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--require-public", action="store_true")
+ args = parser.parse_args()
+ try:
+ if (
+ os.environ.get("GITHUB_REPOSITORY") != REPOSITORY
+ or os.environ.get("GITHUB_REPOSITORY_ID") != str(REPOSITORY_ID)
+ or os.environ.get("GITHUB_REPOSITORY_OWNER_ID") != str(OWNER_ID)
+ or os.environ.get("IMAGE_NAME") != IMAGE
+ or os.environ.get("REGISTRY") != "ghcr.io"
+ ):
+ raise InventoryError("Proxy package inventory must run for the canonical repository and image")
+ print(json.dumps(inventory(os.environ.get("GH_TOKEN", ""), public_only=args.require_public), sort_keys=True))
+ except InventoryError as error:
+ print(f"Proxy package inventory failed: {error}", file=sys.stderr)
+ return 1
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/ci/release-verification-guide.sh b/scripts/ci/release-verification-guide.sh
index 8d87e84b6..e95eb818c 100755
--- a/scripts/ci/release-verification-guide.sh
+++ b/scripts/ci/release-verification-guide.sh
@@ -25,7 +25,7 @@ if [ ! -d "${artifacts_dir}" ]; then
exit 1
fi
-repo="${GITHUB_REPOSITORY:-OpenSecretCloud/Maple}"
+repo="${GITHUB_REPOSITORY:-MaplePrivacyLabs/Maple}"
release_tag="${RELEASE_TAG:-${GITHUB_REF_NAME:-}}"
if [ -z "${release_tag}" ]; then
echo "RELEASE_TAG or GITHUB_REF_NAME is required." >&2
diff --git a/scripts/ci/test-release-gates.sh b/scripts/ci/test-release-gates.sh
index 23e5e8ee5..f47b7a097 100755
--- a/scripts/ci/test-release-gates.sh
+++ b/scripts/ci/test-release-gates.sh
@@ -471,10 +471,10 @@ check(
proxy_container_publish.get("env")
== {
"REGISTRY": "ghcr.io",
- "IMAGE_NAME": "opensecretcloud/maple-proxy",
+ "IMAGE_NAME": "mapleprivacylabs/maple-proxy",
"UNBACKFILLED_PROXY_BASELINE": "0.3.3",
},
- "Proxy container publisher must preserve the existing GHCR package",
+ "Proxy container publisher must use the canonical MaplePrivacyLabs GHCR package",
)
container_jobs = proxy_container_publish.get("jobs", {})
@@ -485,12 +485,20 @@ check(
prepare = container_jobs["prepare"]
prepare_if = str(prepare.get("if", ""))
for required_gate in (
+ "github.repository == 'MaplePrivacyLabs/Maple'",
+ "github.repository_id == '923138240'",
+ "github.repository_owner_id == '322649754'",
+ "github.ref == 'refs/heads/master'",
"workflow_run.conclusion == 'success'",
"workflow_run.event == 'release'",
"workflow_run.path == '.github/workflows/release.yml'",
"workflow_run.head_repository.full_name == github.repository",
):
check(required_gate in prepare_if, f"Proxy container publisher is missing gate: {required_gate}")
+check(
+ prepare.get("permissions") == {"contents": "read", "packages": "read"},
+ "Proxy preparation must read package metadata without package write permission",
+)
prepare_runs = "\n".join(str(step.get("run", "")) for step in prepare.get("steps", []))
for required_control in (
"repos/${REPOSITORY}/releases/latest",
@@ -500,6 +508,7 @@ for required_control in (
"proxy container runtime inputs changed without a proxy version bump",
'"${UNBACKFILLED_PROXY_BASELINE}"',
"plan-proxy-container-publish.sh",
+ "python3 -I scripts/ci/proxy_registry_inventory.py",
):
check(required_control in prepare_runs, f"Proxy publication plan is missing control: {required_control}")
prepare_checkouts = [
@@ -595,6 +604,7 @@ check(
finalize_runs = "\n".join(str(step.get("run", "")) for step in container_finalize.get("steps", []))
for required_control in (
"inspect-proxy-container-manifest.sh",
+ "python3 -I scripts/ci/proxy_registry_inventory.py --require-public",
"published exact tag does not match the current proxy runtime inputs",
'for tag in "${PROXY_MINOR}" "${PROXY_MAJOR}" latest',
"docker buildx imagetools create",
@@ -619,4 +629,7 @@ pass "proxy release artifact verifier accepts only the complete native asset set
bash "${script_dir}/test-plan-proxy-container-publish.sh" >/dev/null
pass "proxy container publish planner preserves immutable versions and recovery"
+python3 "${script_dir}/test_proxy_registry_inventory.py" >/dev/null
+pass "proxy registry inventory distinguishes initial creation from failed access"
+
printf '1..%d\n' "${passed}"
diff --git a/scripts/ci/test_pages_deploy.py b/scripts/ci/test_pages_deploy.py
index 6342654ad..b6ec4b47a 100644
--- a/scripts/ci/test_pages_deploy.py
+++ b/scripts/ci/test_pages_deploy.py
@@ -7,6 +7,7 @@
import os
from pathlib import Path
import sys
+import tarfile
import tempfile
import unittest
from unittest.mock import Mock, patch
@@ -195,6 +196,82 @@ def test_current_release_can_be_redeployed_without_a_new_release(self):
self.event = {}
self.assertEqual(self.production()["previous_sha"], SHA)
+ def transferred_release_values(self):
+ self.setup_release()
+ self.values[self.root + "/git/ref/heads/pages-production"]["object"]["sha"] = SHA
+ for key in ("repository", "head_repository"):
+ self.run[key].update(full_name=self.repo, owner={"id": 185423582})
+ for asset in self.release["assets"]:
+ asset["browser_download_url"] = (
+ f"https://github.com/{self.repo}/releases/download/v3.3.10/{asset['name']}"
+ )
+ new_repo = "MaplePrivacyLabs/Maple"
+ new_root = "/repos/" + new_repo
+ moved = {key.replace(self.root, new_root, 1): value for key, value in self.values.items()}
+ moved[new_root] = {"id": REPO_ID, "default_branch": "master", "full_name": new_repo,
+ "owner": {"id": 322649754}}
+ return new_repo, moved
+
+ def test_manual_production_prepare_after_owner_transfer_uses_retained_release(self):
+ new_repo, moved = self.transferred_release_values()
+ archive = io.BytesIO()
+ contents = b"Retained production release"
+ with tarfile.open(fileobj=archive, mode="w:gz") as bundle:
+ member = tarfile.TarInfo("index.html")
+ member.size = len(contents)
+ bundle.addfile(member, io.BytesIO(contents))
+ archive_bytes = archive.getvalue()
+ archive_digest = hashlib.sha256(archive_bytes).hexdigest()
+ checksum = f"{archive_digest} maple-web-dist.tar.gz\n".encode()
+ new_root = "/repos/" + new_repo
+ downloads = {}
+ for asset, body in zip(self.release["assets"], (archive_bytes, checksum)):
+ asset.update(size=len(body), digest="sha256:" + hashlib.sha256(body).hexdigest())
+ downloads[f"{new_root}/releases/assets/{asset['id']}"] = body
+
+ def request(path, method="GET", data=None, accept="application/vnd.github+json"):
+ self.assertEqual(method, "GET")
+ self.assertIsNone(data)
+ self.assertTrue(path.startswith(new_root))
+ if path in downloads:
+ self.assertEqual(accept, "application/octet-stream")
+ return io.BytesIO(downloads[path])
+ return io.BytesIO(json.dumps(moved[path]).encode())
+
+ api = pages.API("https://api.github.com", "fake-transfer-canary")
+ gh = pages.GitHub(api, new_repo, REPO_ID)
+ with tempfile.TemporaryDirectory() as tmp, patch.object(api, "request", side_effect=request) as read:
+ state = Path(tmp) / "prepared"
+ plan = pages.prepare(gh, {}, "production", state)
+ self.assertEqual((plan["target"], plan["profile"], plan["sha"], plan["previous_sha"]),
+ ("production", "release", SHA, SHA))
+ self.assertEqual((plan["release_id"], plan["run_id"], plan["run_attempt"]), (900, 100, 2))
+ self.assertEqual((state / "assets/index.html").read_bytes(), contents)
+ saved = json.loads((state / "plan.json").read_text())
+ self.assertEqual(saved["files"], {"index.html": hashlib.sha256(contents).hexdigest()})
+ self.assertEqual([call.args[0] for call in read.call_args_list if call.args[0] in downloads],
+ list(downloads))
+
+ def test_transferred_production_rejects_wrong_or_missing_repository_ids_before_download(self):
+ new_repo, moved = self.transferred_release_values()
+ new_root = "/repos/" + new_repo
+ for location in ("current", "repository", "head_repository"):
+ for missing in (False, True):
+ with self.subTest(location=location, missing=missing):
+ values = copy.deepcopy(moved)
+ identity = (values[new_root] if location == "current"
+ else values[new_root + "/actions/runs/100"][location])
+ if missing:
+ del identity["id"]
+ else:
+ identity["id"] = 99
+ api = MemoryAPI(values)
+ api.download = Mock(side_effect=AssertionError("Untrusted release must not be downloaded"))
+ gh = pages.GitHub(api, new_repo, REPO_ID)
+ with tempfile.TemporaryDirectory() as tmp, self.assertRaises((pages.Rejected, KeyError)):
+ pages.prepare(gh, {}, "production", Path(tmp) / "prepared")
+ api.download.assert_not_called()
+
def test_draft_prerelease_and_malformed_tag_rejected(self):
self.setup_release()
for field, value in (("draft", True), ("prerelease", True), ("tag_name", "v3.3.10\n::error::injection")):
diff --git a/scripts/ci/test_proxy_registry_inventory.py b/scripts/ci/test_proxy_registry_inventory.py
new file mode 100644
index 000000000..9ed7a6775
--- /dev/null
+++ b/scripts/ci/test_proxy_registry_inventory.py
@@ -0,0 +1,201 @@
+#!/usr/bin/env python3
+"""Regression coverage for an empty namespace versus unavailable package access."""
+
+import contextlib
+import copy
+import io
+import json
+import os
+import sys
+import unittest
+from unittest.mock import patch
+from urllib.error import HTTPError, URLError
+
+import proxy_registry_inventory as inventory
+
+TOKEN = "FAKE_WORKFLOW_TOKEN_NOT_A_CREDENTIAL"
+REGISTRY_TOKEN = "FAKE_ANONYMOUS_PULL_TOKEN"
+PACKAGE = {
+ "name": inventory.PACKAGE,
+ "package_type": "container",
+ "visibility": "public",
+ "owner": {"id": inventory.OWNER_ID, "login": inventory.OWNER},
+ "repository": {"id": inventory.REPOSITORY_ID, "full_name": inventory.REPOSITORY},
+}
+
+
+class FakeGet:
+ def __init__(self, *responses):
+ self.responses = list(responses)
+ self.calls = []
+
+ def __call__(self, url, token=None):
+ self.calls.append((url, token))
+ if not self.responses:
+ raise AssertionError("Unexpected extra request")
+ result = self.responses.pop(0)
+ if isinstance(result, Exception):
+ raise result
+ return result
+
+
+class ProxyRegistryInventoryTests(unittest.TestCase):
+ def public_responses(self, tags=None):
+ return (
+ inventory.Response(200, copy.deepcopy(PACKAGE)),
+ inventory.Response(200, {"token": REGISTRY_TOKEN}),
+ inventory.Response(200, {"name": inventory.IMAGE, "tags": ["0.3.4", "latest"] if tags is None else tags}),
+ )
+
+ def test_missing_package_requires_authenticated_metadata_and_listing(self):
+ get = FakeGet(inventory.Response(404), inventory.Response(200, []))
+ self.assertEqual(inventory.inventory(TOKEN, get=get), {"name": inventory.IMAGE, "tags": []})
+ self.assertEqual(len(get.calls), 2)
+ self.assertTrue(all(url.startswith(inventory.API + "/") and token == TOKEN for url, token in get.calls))
+
+ def test_metadata_auth_network_and_service_errors_are_not_empty(self):
+ for status in (301, 400, 401, 403, 429, 500, 502, 503):
+ with self.subTest(status=status):
+ get = FakeGet(inventory.Response(status))
+ with self.assertRaises(inventory.InventoryError):
+ inventory.inventory(TOKEN, get=get)
+ self.assertEqual(len(get.calls), 1)
+ with self.assertRaises(inventory.InventoryError):
+ inventory.inventory(TOKEN, get=FakeGet(inventory.InventoryError("Package inventory request failed")))
+
+ def test_metadata_404_does_not_mask_unauthorized_or_failed_listing(self):
+ for status in (301, 401, 403, 404, 429, 500):
+ with self.subTest(status=status), self.assertRaises(inventory.InventoryError):
+ inventory.inventory(TOKEN, get=FakeGet(inventory.Response(404), inventory.Response(status)))
+ for data in ({}, None, [{"name": inventory.PACKAGE}], [{"name": "MAPLE-PROXY"}], ["invalid"], [{}]):
+ with self.subTest(data=data), self.assertRaises(inventory.InventoryError):
+ inventory.inventory(TOKEN, get=FakeGet(inventory.Response(404), inventory.Response(200, data)))
+
+ def test_missing_package_checks_all_readable_package_pages(self):
+ get = FakeGet(
+ inventory.Response(404),
+ inventory.Response(200, [{"name": "other"}], '