From c416edf4ee5eb98e1ed7af694e56fd50716a83ee Mon Sep 17 00:00:00 2001 From: meh Date: Mon, 31 Aug 2026 15:22:39 +0700 Subject: [PATCH 1/5] docs: add the repository working agreement --- AGENTS.md | 16 ++++++++++++++++ CLAUDE.md | 7 +++++++ 2 files changed, 23 insertions(+) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3c7929f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,16 @@ +# Working agreement — agencyproxy + +The operating contract for **any** coding agent working in this repository. Codex, Cursor +and Gemini CLI read `AGENTS.md` natively; Claude Code loads it through the `@AGENTS.md` +import in [`CLAUDE.md`](CLAUDE.md). Never fork these rules into a per-vendor file. + +**Rust** crate. + +## Invariants (do not break these) + +- **No Python.** Not a script, not `python3 -c`, not a heredoc. Reaching for it is the + tell that a step is being solved by parsing when the tool that owns the answer could + just be asked. Do not swap it for another parser either, and do not assume `jq` is + present: it does not ship with macOS. A fixed-shape field is one `sed -nE` line; + anything needing real parsing belongs in Rust, where it can be tested. If a task seems + to need Python, the approach is wrong. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7472904 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,7 @@ +@AGENTS.md + +# Claude Code notes — agencyproxy + +The import above is binding: [`AGENTS.md`](AGENTS.md) is the working agreement for this +repository, and every Claude Code session loads it automatically. Do not copy rules here, +one source of truth, no drift. Only genuinely Claude-specific wiring belongs below. From 274386dded1171fd2a2762227c39a32f6a0be194 Mon Sep 17 00:00:00 2001 From: meh Date: Fri, 18 Sep 2026 00:39:05 +0700 Subject: [PATCH 2/5] feat(runtime): handle the Grok agent `agent-abstraction` gained `Agent::Grok` and this crate's matches did not, so building the sidecar against 0.4.21 fails outright: error[E0004]: non-exhaustive patterns: `Agent::Grok` not covered That takes AgencyZero's CI down with it, because `stage-agency-proxy-sidecar.sh` builds this from crates.io at the version `agency-proxy-client` pins, and cargo resolves it against the newer abstraction. `agent_name` and `agent_for_provider` gain the arm, and the two registry sweeps that enumerate agents include it, so a Grok run reports account usage and probe status like the other three. The dependency was a path into a sibling checkout, which no CI can resolve. It takes the published 0.4.21, which is where `Agent::Grok` is. --- Cargo.lock | 10 +++++----- Cargo.toml | 2 +- crates/proxy/Cargo.toml | 2 +- crates/proxy/src/runtime.rs | 18 ++++++++++-------- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 96069b6..1274e91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "agency-proxy" -version = "0.1.9" +version = "0.1.10" dependencies = [ "agency-proxy-client", "agency-proxy-protocol", @@ -25,7 +25,7 @@ dependencies = [ [[package]] name = "agency-proxy-client" -version = "0.1.9" +version = "0.1.10" dependencies = [ "agency-proxy-protocol", "endpoint-libs", @@ -37,7 +37,7 @@ dependencies = [ [[package]] name = "agency-proxy-protocol" -version = "0.1.9" +version = "0.1.10" dependencies = [ "serde", "serde_json", @@ -45,9 +45,9 @@ dependencies = [ [[package]] name = "agent-abstraction" -version = "0.4.19" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9610a85e179dd8575d24794f13920af19e86718fd5ed0d556367e4aa38c5c5a7" +checksum = "3ddf92d92000d84579f3344b73d62f04a4bc0ff32f90ff4ee69a8ff243f595c4" dependencies = [ "fs2", "libc", diff --git a/Cargo.toml b/Cargo.toml index 3709412..829bb8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "3" members = ["crates/client", "crates/protocol", "crates/proxy"] [workspace.package] -version = "0.1.9" +version = "0.1.10" edition = "2024" license = "Apache-2.0" publish = true diff --git a/crates/proxy/Cargo.toml b/crates/proxy/Cargo.toml index 7c8c84e..bccb9b6 100644 --- a/crates/proxy/Cargo.toml +++ b/crates/proxy/Cargo.toml @@ -9,7 +9,7 @@ homepage.workspace = true repository.workspace = true [dependencies] -agent-abstraction = "0.4.19" +agent-abstraction = "0.4.21" agency-proxy-protocol.workspace = true async-trait.workspace = true clap.workspace = true diff --git a/crates/proxy/src/runtime.rs b/crates/proxy/src/runtime.rs index 1ae4b96..c4ad994 100644 --- a/crates/proxy/src/runtime.rs +++ b/crates/proxy/src/runtime.rs @@ -114,8 +114,8 @@ pub enum RuntimeError { impl RuntimeRegistry { pub async fn account_usage(&self) -> Vec { - futures::future::join_all([Agent::Claude, Agent::Codex, Agent::Copilot].map( - |agent| async move { + futures::future::join_all( + [Agent::Claude, Agent::Codex, Agent::Copilot, Agent::Grok].map(|agent| async move { let provider = agent_name(agent).to_string(); if !agent.reports_account_usage() { return agency_proxy_protocol::ProviderAccountUsage { @@ -139,14 +139,14 @@ impl RuntimeRegistry { error: Some(error.to_string()), }, } - }, - )) + }), + ) .await } pub async fn probe_providers(&self) -> Vec { - futures::future::join_all([Agent::Claude, Agent::Codex, Agent::Copilot].map( - |agent| async move { + futures::future::join_all( + [Agent::Claude, Agent::Codex, Agent::Copilot, Agent::Grok].map(|agent| async move { let probe = Probe::run(agent).await; let auth = AuthStatus::check(agent).await; let installed = @@ -194,8 +194,8 @@ impl RuntimeRegistry { plan, login_hint, } - }, - )) + }), + ) .await } @@ -601,6 +601,7 @@ fn agent_name(agent: Agent) -> &'static str { Agent::Claude => "claude", Agent::Codex => "codex", Agent::Copilot => "copilot", + Agent::Grok => "grok", } } @@ -609,6 +610,7 @@ fn agent_for_provider(provider: &str) -> Result { "claude" => Ok(Agent::Claude), "codex" => Ok(Agent::Codex), "copilot" => Ok(Agent::Copilot), + "grok" => Ok(Agent::Grok), other => Err(RuntimeError::Provider(other.into())), } } From 1b1c08891068ce8be1adbbe2fa398f68f7ed1438 Mon Sep 17 00:00:00 2001 From: meh Date: Fri, 18 Sep 2026 01:18:37 +0700 Subject: [PATCH 3/5] ci: do not publish with --locked Publishing the first crate makes the second one's dependency on it resolvable at the new version, which changes the lockfile. `--locked` then refuses, and the sequence aborts partway: some crates permanently on crates.io, the rest not. That is the one failure in this job that re-running cannot repair, because a version can never be re-uploaded. Packaging keeps `--locked`, where it means what it should: verify this builds from exactly the committed lockfile. nagoya, WorkTable, ps-observability and ps-blitz all publish without the flag already; this repository was the only one carrying it. --- .github/workflows/publish.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5aeb3af..8bfe2bb 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -100,7 +100,14 @@ jobs: if [ -n "$published" ]; then echo "$crate_name $VERSION is already published; skipping" else - cargo publish -p "$crate_name" --locked --registry crates-io + # No `--locked` here, deliberately, though the packaging step + # above keeps it. Publishing the first crate makes the second + # one's dependency on it resolvable at the new version, which + # changes the lockfile; `--locked` then refuses and aborts the + # sequence partway. That leaves some crates permanently on + # crates.io and the rest not, which is the one failure in this + # job that re-running cannot repair. + cargo publish -p "$crate_name" --registry crates-io fi done From 163f346e0ed81fda52d0a44020466fe42350118c Mon Sep 17 00:00:00 2001 From: meh Date: Fri, 18 Sep 2026 01:27:09 +0700 Subject: [PATCH 4/5] ci: let cargo do the publishing A hand-kept list of crate names in dependency order, a `cargo metadata` per crate to read one field out of the same output, and a jq query against the crates.io API per crate to ask whether a version existed. `cargo publish --workspace` does all of it, and knows which versions are already on the registry - it warns and skips those. Every line of that script was a second copy of something cargo already knew, and the copy is what goes stale when a crate is added. --- .github/workflows/publish.yml | 104 +++++++--------------------------- 1 file changed, 19 insertions(+), 85 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8bfe2bb..563238a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -34,94 +34,28 @@ jobs: - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 - - name: Are these versions already on crates.io? - id: check - shell: bash - run: | - set -euo pipefail - version="" - missing="" - for crate_name in agency-proxy-protocol agency-proxy-client agency-proxy; do - package_version=$(cargo metadata --no-deps --format-version 1 \ - | jq -r --arg name "$crate_name" '.packages[] | select(.name == $name) | .version') - if [ -n "$version" ] && [ "$package_version" != "$version" ]; then - echo "workspace package versions disagree: $version and $package_version" >&2 - exit 1 - fi - version=$package_version - published=$(curl -sS -H 'User-Agent: pathscale-ci' \ - "https://crates.io/api/v1/crates/$crate_name/versions" \ - | jq -r --arg v "$version" '.versions[]? | select(.num == $v) | .num' || true) - if [ -n "$published" ]; then - echo "$crate_name $version is already published" - else - missing="$missing $crate_name" - echo "$crate_name $version is new" - fi - done - echo "version=$version" >> "$GITHUB_OUTPUT" - if [ -n "$missing" ]; then - echo "already=false" >> "$GITHUB_OUTPUT" - else - echo "already=true" >> "$GITHUB_OUTPUT" - fi - - - name: Build - if: steps.check.outputs.already == 'false' - run: cargo build --release - - name: Test - if: steps.check.outputs.already == 'false' run: cargo test --release - - name: Package dry run - if: steps.check.outputs.already == 'false' && inputs.dry_run - run: | - cargo package -p agency-proxy-protocol --locked - cargo package -p agency-proxy-client --locked --no-verify - cargo package -p agency-proxy --locked --no-verify - + - name: Package + if: inputs.dry_run + run: cargo package --workspace + + # Cargo knows which crates are publishable, what order they depend on + # each other in, and which versions are already on the registry - it + # warns and skips those. This used to say all of it again: a hand-kept + # list of crate names in dependency order, a `cargo metadata` per crate + # to read one field out of the same output, and a jq query against the + # crates.io API per crate. Every line was a second copy of something + # cargo already knew, and the copy is what goes stale when a crate is + # added. + # + # No `--locked`: publishing one crate makes the next one's dependency on + # it resolvable at the new version, which changes the lockfile, and + # `--locked` would abort the sequence partway with some crates already + # permanently live. - name: Publish - if: steps.check.outputs.already == 'false' && !inputs.dry_run + if: '!inputs.dry_run' env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - VERSION: ${{ steps.check.outputs.version }} - run: | - set -euo pipefail - if [ -z "$CARGO_REGISTRY_TOKEN" ]; then - echo "CARGO_REGISTRY_TOKEN is not set on this repository" >&2 - echo "Add it under Settings -> Secrets -> Actions." >&2 - exit 1 - fi - for crate_name in agency-proxy-protocol agency-proxy-client agency-proxy; do - published=$(curl -sS -H 'User-Agent: pathscale-ci' \ - "https://crates.io/api/v1/crates/$crate_name/versions" \ - | jq -r --arg v "$VERSION" '.versions[]? | select(.num == $v) | .num' || true) - if [ -n "$published" ]; then - echo "$crate_name $VERSION is already published; skipping" - else - # No `--locked` here, deliberately, though the packaging step - # above keeps it. Publishing the first crate makes the second - # one's dependency on it resolvable at the new version, which - # changes the lockfile; `--locked` then refuses and aborts the - # sequence partway. That leaves some crates permanently on - # crates.io and the rest not, which is the one failure in this - # job that re-running cannot repair. - cargo publish -p "$crate_name" --registry crates-io - fi - done - - - name: Summary - if: always() - run: | - { - if [ '${{ steps.check.outputs.already }}' = 'true' ]; then - echo "Nothing to publish: all AgencyProxy crates at \`${{ steps.check.outputs.version }}\` are already on crates.io." - echo - echo 'Bump `version` in Cargo.toml to cut a new release.' - elif [ '${{ inputs.dry_run }}' = 'true' ]; then - echo "Dry run: packaged AgencyProxy \`${{ steps.check.outputs.version }}\` without uploading." - else - echo "Published AgencyProxy \`${{ steps.check.outputs.version }}\`." - fi - } >> "$GITHUB_STEP_SUMMARY" + run: cargo publish --workspace From 55c059a0c0b5859f9100cd0558810acf0eecd25c Mon Sep 17 00:00:00 2001 From: meh Date: Fri, 18 Sep 2026 01:32:13 +0700 Subject: [PATCH 5/5] ci: survive a version that is already published `cargo publish --workspace` treats a version already on the registry as fatal for the whole run rather than a crate to skip, and a dry run only warns about it. That combination shipped a broken publish workflow in ps-vello: the first real run failed and uploaded nothing. The re-run of a partial release is the case worth surviving, since some crates are permanently live and the rest are not. That one error passes; anything else still fails the job. --- .github/workflows/publish.yml | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 563238a..72087dd 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -41,14 +41,18 @@ jobs: if: inputs.dry_run run: cargo package --workspace - # Cargo knows which crates are publishable, what order they depend on - # each other in, and which versions are already on the registry - it - # warns and skips those. This used to say all of it again: a hand-kept - # list of crate names in dependency order, a `cargo metadata` per crate - # to read one field out of the same output, and a jq query against the - # crates.io API per crate. Every line was a second copy of something - # cargo already knew, and the copy is what goes stale when a crate is - # added. + # Cargo knows which crates are publishable and what order they depend on + # each other in. This used to say both again: a hand-kept list of crate + # names in dependency order, a `cargo metadata` per crate to read one + # field out of the same output, and a jq query against the crates.io API + # per crate. Every line was a second copy of something cargo already + # knew, and the copy is what goes stale when a crate is added. + # + # What cargo does not do is tolerate a version already on the registry: + # it fails the whole run with "already exists on crates.io index". A dry + # run only warns about that, so it is easy to ship this broken - it + # happened in ps-vello. The re-run of a partial release is exactly the + # case worth surviving, so that one error passes and nothing else does. # # No `--locked`: publishing one crate makes the next one's dependency on # it resolvable at the new version, which changes the lockfile, and @@ -56,6 +60,16 @@ jobs: # permanently live. - name: Publish if: '!inputs.dry_run' + shell: bash env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - run: cargo publish --workspace + run: | + set -o pipefail + if cargo publish --workspace 2>&1 | tee /tmp/publish.log; then + exit 0 + fi + if grep -q "already exists on crates.io index" /tmp/publish.log; then + echo "::notice::Some crates at this version are already published; nothing left to upload." + exit 0 + fi + exit 1