diff --git a/.claude/session-start-global-deny.sh b/.claude/session-start-global-deny.sh index 84e5775..9930259 100755 --- a/.claude/session-start-global-deny.sh +++ b/.claude/session-start-global-deny.sh @@ -1,10 +1,29 @@ #!/bin/bash -# Session start hook: ensure global Claude Code deny rules and git proxy config +# CANONICAL SOURCE — managed by runcycles/.github/shared-config/ +# Do not edit this file in individual repos. Changes should be made here +# and synced to all repos via scripts/sync-claude-config.sh. # -# 1. Writes MCP deny rules to ~/.claude/settings.json so mcp__github__ -# file-mutation tools are blocked globally (even in cross-repo sessions). -# 2. Fixes git remote URLs to use the local git proxy when available, -# so native git push works instead of falling back to MCP tools. +# Session start hook: ensure global Claude Code deny rules and git proxy config. +# +# This script runs at session start and does TWO things: +# +# 1. (Per-user, idempotent) Writes MCP deny rules to ~/.claude/settings.json +# so mcp__github__ file-mutation tools are blocked globally — even in +# cross-repo sessions where deny rules in a single repo wouldn't apply. +# +# 2. (MULTI-REPO MUTATION) If a local git proxy is detected in any sibling +# repo under /home/user/*, rewrites the `origin` remote URL of EVERY +# sibling github.com repo under /home/user/ to route through the proxy. +# This is intentional for Claude Code remote-environment workflows where +# multiple Cycles repos are cloned side-by-side and all need the same +# proxy. It is surprising if you only know about the per-repo .claude/ +# hook, so it is called out explicitly here. +# +# OPT-OUT: set CYCLES_CLAUDE_SKIP_REMOTE_REWRITE=1 to disable Part 2 entirely +# (useful for local Claude Code runs where you do not want sibling repos +# touched). Part 1 always runs. +# +# Issue: runcycles/.github#63 set -e @@ -56,9 +75,18 @@ EOF fi fi -# --- Part 2: Fix git remote URLs to use local proxy --- +# --- Part 2: Fix git remote URLs to use local proxy (MULTI-REPO) --- # Some sessions clone repos via github.com directly, which lacks push credentials. # If the local git proxy is running, rewrite remote URLs to use it. +# +# WARNING: this part iterates every directory under /home/user/ and mutates the +# `origin` remote of any github.com repo it finds, not just the current +# checkout. See the file-level header for the rationale and opt-out. + +if [ "${CYCLES_CLAUDE_SKIP_REMOTE_REWRITE:-}" = "1" ]; then + # Opt-out path for local Claude Code runs that should not touch sibling repos. + exit 0 +fi # Detect local git proxy: look for the proxy in any sibling repo's remote URL PROXY_BASE="" diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 542616f..3fccf3b 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -32,6 +32,24 @@ jobs: with: python-version: "3.12" + # Fail-fast before build/publish if (a) pyproject.toml declares a pre-release + # version (e.g. 1.0.0.dev0, 1.0.0a1) when a final-version tag (vX.Y.Z) was + # pushed, or (b) the tag version doesn't match pyproject.toml. PyPI rejects + # duplicate versions server-side, but a local guard surfaces the operator + # error before the upload phase. See runcycles/.github#61. + - name: Verify pyproject version matches tag + if: startsWith(github.ref, 'refs/tags/v') + run: | + python -c "import tomllib,sys; v=tomllib.load(open('pyproject.toml','rb'))['project']['version']; print(v)" > /tmp/pyproject_version + PYPROJECT_VERSION=$(cat /tmp/pyproject_version) + TAG_VERSION="${GITHUB_REF_NAME#v}" + echo "pyproject.toml version: ${PYPROJECT_VERSION}" + echo "Git tag version: ${TAG_VERSION}" + if [ "${PYPROJECT_VERSION}" != "${TAG_VERSION}" ]; then + echo "::error::Version mismatch: pyproject.toml has '${PYPROJECT_VERSION}' but tag is 'v${TAG_VERSION}'. Bump pyproject.toml or retag." + exit 1 + fi + - name: Install build tool run: python -m pip install --upgrade pip build diff --git a/AUDIT.md b/AUDIT.md index 0acd90f..dace609 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -253,3 +253,18 @@ Documentation-only update pointing users at the new sibling package [`langchain- Background: LangChain 1.x introduced an `AgentMiddleware` API with `wrap_tool_call`, `before_model`, and `wrap_model_call` hooks. The new package wraps that API on top of this SDK's existing `decide` / `create_reservation` / `commit_reservation` / `release_reservation` surface — no new SDK methods needed. Splitting into a sibling repo follows LangChain's [publishing guidance](https://docs.langchain.com/oss/python/contributing/publish-langchain) ("New integrations should be published as standalone PyPI packages") and the `langchain-` naming convention used by `langchain-anthropic`, `langchain-openai`, etc. Protocol conformance: No protocol or wire-format changes. The new sibling package consumes this SDK as a normal dependency. + +## Infrastructure Hardening (added 2026-05-12) + +**Files:** `.claude/session-start-global-deny.sh`, `.github/workflows/python-publish.yml` +**Version:** unreleased (CI/Claude-config only — no package version change) + +Cross-cutting hardening landed in response to org-wide tracking issues filed in `runcycles/.github`. Two distinct changes; both are infra-only. + +- **`.claude/session-start-global-deny.sh`** synced from the new canonical at `runcycles/.github/shared-config/`. The script now (a) carries a top-of-file callout explaining that Part 2 mutates the `origin` remote of every sibling repo under `/home/user/*`, not just the current checkout, and (b) honors a `CYCLES_CLAUDE_SKIP_REMOTE_REWRITE=1` opt-out env var. Part 1 (MCP deny rules) is unchanged. Tracks `runcycles/.github#63`. + +- **`.github/workflows/python-publish.yml`** gained a `Verify pyproject version matches tag` step that runs on tag-triggered builds (`refs/tags/v*`). The step parses `pyproject.toml` via `tomllib` and fails the workflow before the build phase if the declared version doesn't match the tag (e.g., tag `v0.5.0` against `pyproject.toml` still on `0.4.1` or a `dev0` pre-release). PyPI already rejects duplicate versions server-side, but this surfaces operator error earlier in the pipeline. Python analog of the Java SNAPSHOT-guard tracked in `runcycles/.github#61`. + +Not included in this change: bumping the reusable-workflow ref `runcycles/.github/.github/workflows/ci-python.yml@main` to `@v1` (`runcycles/.github#60`). That bump is intentionally split into a separate follow-up PR — it depends on the `v1` tag existing in `runcycles/.github`, which is being cut after the canonical-script PR (`runcycles/.github#64`) merges. + +Protocol conformance: No protocol or wire-format changes. No SDK source touched. Test suite unaffected.