Skip to content
Merged
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
105 changes: 30 additions & 75 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,87 +34,42 @@ 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 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
# `--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'
shell: bash
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
set -o pipefail
if cargo publish --workspace 2>&1 | tee /tmp/publish.log; then
exit 0
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
cargo publish -p "$crate_name" --locked --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"
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
16 changes: 16 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 5 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion crates/proxy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 10 additions & 8 deletions crates/proxy/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ pub enum RuntimeError {

impl RuntimeRegistry {
pub async fn account_usage(&self) -> Vec<agency_proxy_protocol::ProviderAccountUsage> {
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 {
Expand All @@ -139,14 +139,14 @@ impl RuntimeRegistry {
error: Some(error.to_string()),
},
}
},
))
}),
)
.await
}

pub async fn probe_providers(&self) -> Vec<agency_proxy_protocol::ProviderStatus> {
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 =
Expand Down Expand Up @@ -194,8 +194,8 @@ impl RuntimeRegistry {
plan,
login_hint,
}
},
))
}),
)
.await
}

Expand Down Expand Up @@ -601,6 +601,7 @@ fn agent_name(agent: Agent) -> &'static str {
Agent::Claude => "claude",
Agent::Codex => "codex",
Agent::Copilot => "copilot",
Agent::Grok => "grok",
}
}

Expand All @@ -609,6 +610,7 @@ fn agent_for_provider(provider: &str) -> Result<Agent, RuntimeError> {
"claude" => Ok(Agent::Claude),
"codex" => Ok(Agent::Codex),
"copilot" => Ok(Agent::Copilot),
"grok" => Ok(Agent::Grok),
other => Err(RuntimeError::Provider(other.into())),
}
}
Expand Down
Loading