Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
dc44f14
move fern definition files
fern-support Jun 4, 2026
3c29ed8
rename api folders
fern-support Jun 4, 2026
1734b63
fix paths in generators.yml
fern-support Jun 4, 2026
4447651
Rewire config to commit the spec and use it as input for the cli gene…
fern-support Jun 4, 2026
4ffbe5b
revert folder rename
fern-support Jun 4, 2026
9c9f823
fix stainless sync command
fern-support Jun 4, 2026
9ba612b
updates
fern-support Jun 4, 2026
22b12ef
Bump cli generator to 0.17.3 and rename Send type to avoid Rust trait…
cadesark Jun 10, 2026
f165504
Move Send→SendEvent rename out of the spec into a CLI overrides file
cadesark Jun 10, 2026
e163543
Override spec title so the generated CLI README says AgentMail, not "…
cadesark Jun 10, 2026
2fa9df8
Scope demo CLI npm package to @fern-demo/agentmail-cli
cadesark Jun 10, 2026
938f182
Nest CLI command groups via x-fern-sdk-group-name annotations
cadesark Jun 12, 2026
b3f1632
Bump cli generator to 0.17.8
cadesark Jun 12, 2026
f66350a
Add x-fern-sdk group/method annotations for all CLI operations
duharry0915 Jun 12, 2026
9bdd942
fix(cli): kebab method names, repair regen triggers, keep stainless s…
duharry0915 Jul 13, 2026
6ade710
Merge origin/main into fs/wire-cli-gen
duharry0915 Jul 13, 2026
614aef3
chore: re-export openapi from definition
duharry0915 Jul 13, 2026
7a1dc89
chore(cli): annotate endpoints added since June regen
duharry0915 Jul 13, 2026
26f6e17
chore(cli): point output at agentmail-to/agentmail-cli, prune orphane…
duharry0915 Jul 23, 2026
76c528c
firstCommit
duharry0915 Jul 28, 2026
2f8cf66
feat(cli): accept AGENTMAIL_API_KEY and AGENTMAIL_TOKEN as dual auth
duharry0915 Jul 29, 2026
5a985ac
test(cli): guard AGENTMAIL_API_KEY auth precedence
duharry0915 Jul 30, 2026
9aa7bd2
chore(cli): upgrade generator to 0.29.1, set package identity
duharry0915 Jul 30, 2026
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
37 changes: 37 additions & 0 deletions .github/workflows/release-cli.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Release CLI

on:
push:
branches:
- main
paths:
# a) API definition changes (source of truth)
- "fern/apis/api/definition/**"
# b) Committed OpenAPI export changes
- "openapi/openapi.yml"
# c) CLI generator config / overrides changes
- "fern/apis/cli/generators.yml"
- "fern/apis/cli/openapi-overrides.yml"
# d) Fern CLI version changes
- "fern/fern.config.json"
workflow_dispatch:

jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v6

- name: Setup Fern CLI
uses: fern-api/setup-fern-cli@v1

- name: Re-export OpenAPI from definition
env:
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
run: fern generate --api api --group openapi --local --force --no-prompt

- name: Generate CLI
env:
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
run: fern generate --api cli --group cli --log-level debug --version AUTO
7 changes: 3 additions & 4 deletions .github/workflows/sync-stainless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ on:
branches:
- main
paths:
- "fern/definition/**"
- "fern/generators.yml"
- "fern/api.yml"
- "fern/apis/api/definition/**"
- "fern/apis/api/generators.yml"

jobs:
sync:
Expand All @@ -22,7 +21,7 @@ jobs:

- name: Generate OpenAPI spec
working-directory: fern
run: fern generate --group openapi --local
run: fern generate --api api --group openapi --local --force --no-prompt
env:
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}

Expand Down
119 changes: 119 additions & 0 deletions bin/check-cli-auth-precedence.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/bin/env bash
#
# Regression check: AGENTMAIL_API_KEY must win over AGENTMAIL_TOKEN.
#
# The generated CLI declares two auth sources (main.rs):
# .auth(BearerAuth::new("BearerAuth").env("AGENTMAIL_API_KEY"))
# .auth(BearerAuth::new("TokenAuth").env("AGENTMAIL_TOKEN"))
# Both are `scheme: bearer`, so both write the SAME `Authorization` header.
# When both env vars are set the CLI can emit TWO Authorization headers; the
# credential a first-reading server honors is whichever is applied first.
#
# Empirically (verified 2026-07-29 by flipping every config knob and
# regenerating): precedence is NOT controlled by config order. It is decided
# by the SDK's alphabetical scheme-name sort (compose.rs `scheme_names.sort()`)
# plus the base spec anchoring `BearerAuth` first — so "BearerAuth" (< "Token")
# maps AGENTMAIL_API_KEY to the first header. A generator upgrade that changes
# that sort/merge could silently flip which credential wins. This test catches
# that: it sets BOTH env vars to distinct sentinels, hits a localhost echo
# server with a read-only GET, and asserts the API_KEY sentinel lands first.
#
# Usage:
# bin/check-cli-auth-precedence.sh /path/to/agentmail # binary, or
# AGENTMAIL_CLI_BIN=/path/to/agentmail bin/check-cli-auth-precedence.sh
# PORT override: AGENTMAIL_ECHO_PORT=8207 bin/check-cli-auth-precedence.sh ...
#
# Exit 0 = precedence correct (API_KEY first). Non-zero = flipped or broken.

set -euo pipefail

BIN="${1:-${AGENTMAIL_CLI_BIN:-}}"
if [[ -z "$BIN" ]]; then
echo "ERROR: pass the agentmail CLI binary path as \$1 or via AGENTMAIL_CLI_BIN." >&2
exit 2
fi
if [[ ! -x "$BIN" ]]; then
echo "ERROR: not an executable binary: $BIN" >&2
exit 2
fi

PORT="${AGENTMAIL_ECHO_PORT:-8207}"
API_KEY_SENTINEL="api-key-should-win"
TOKEN_SENTINEL="token-should-lose"

workdir="$(mktemp -d)"
srv_py="$workdir/echo.py"
auth_file="$workdir/auth"

cat > "$srv_py" <<'PY'
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer
auth_file = sys.argv[1]
class H(BaseHTTPRequestHandler):
def do_GET(self):
# Capture EVERY Authorization header (both schemes are `scheme: bearer`,
# so both land here when both env vars are set). The /health readiness
# probe carries no auth and is ignored; only real API paths are recorded.
if self.path != "/health":
auths = self.headers.get_all("Authorization") or ["<none>"]
with open(auth_file, "w") as f:
f.write("\n".join(auths))
b = b'{"inboxes":[],"count":0}'
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(b)))
self.end_headers()
self.wfile.write(b)
def log_message(self, *a):
pass
HTTPServer(("127.0.0.1", int(sys.argv[2]), ), H).serve_forever()
PY

python3 "$srv_py" "$auth_file" "$PORT" &
srv_pid=$!
cleanup() { kill "$srv_pid" 2>/dev/null || true; wait "$srv_pid" 2>/dev/null || true; rm -rf "$workdir"; }
trap cleanup EXIT

# Wait until the server actually accepts connections (proven-reliable readiness).
ready=""
for _ in $(seq 1 50); do
if curl -s -o /dev/null "http://127.0.0.1:$PORT/health"; then ready=1; break; fi
sleep 0.1
done
if [[ -z "$ready" ]]; then echo "ERROR: echo server never became ready on :$PORT" >&2; exit 3; fi

# read-only GET with BOTH credentials set to distinct values
env AGENTMAIL_API_KEY="$API_KEY_SENTINEL" AGENTMAIL_TOKEN="$TOKEN_SENTINEL" \
"$BIN" inboxes list --base-url "http://127.0.0.1:$PORT" >/dev/null 2>&1 || true

if [[ ! -f "$auth_file" ]]; then
echo "FAIL: the CLI never reached the echo server (no request recorded)." >&2
exit 3
fi
# Read one Authorization header per line (bash 3.2 compatible; macOS ships 3.2,
# which has no `mapfile`).
auths=()
while IFS= read -r line || [[ -n "$line" ]]; do auths+=("$line"); done < "$auth_file"
first="${auths[0]:-<none>}"
want="Bearer $API_KEY_SENTINEL"

echo "Authorization header(s) sent (${#auths[@]}):"
printf ' %s\n' "${auths[@]}"

if (( ${#auths[@]} > 1 )); then
echo "WARNING: ${#auths[@]} Authorization headers were sent (duplicate-header bug;" \
"both schemes are 'scheme: bearer'). Precedence then depends on how the" \
"server reads duplicate headers." >&2
fi

if [[ "$first" == "$want" ]]; then
echo "PASS: AGENTMAIL_API_KEY is the first credential the server sees (expected)."
exit 0
fi
echo "FAIL: expected first Authorization to be '$want' but it was '$first'." >&2
if [[ "$first" == "Bearer $TOKEN_SENTINEL" ]]; then
echo " -> precedence FLIPPED: AGENTMAIL_TOKEN is now first." >&2
echo " -> the SDK applies schemes in alphabetical name order" \
"(compose.rs scheme_names.sort()); a generator change likely altered it." >&2
fi
exit 1
Loading