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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Changed

- **`scripts/recover.sh status` tier line now surfaces Pro license expiry date.** The status output's `tier` line parses the JWT `exp` claim from the configured Pro license token and renders one of three shapes: `Pro tier active (expires YYYY-MM-DD, N days remaining)` when active, `Free tier (Pro expired YYYY-MM-DD — visit https://getaxonflow.com/pro to renew)` when the token is on disk but its `exp` has passed (plugin will not forward an expired token), or `Free tier (no AXON- license token configured)` when no token is loaded. Lets users see their renewal date without hitting the agent and catches the lapsed-token state before their next governed call. Display only — JWT signature validation remains the platform's job. Pre-existing `Pro tier active` and `Free tier` substring assertions still hold.
- **`scripts/recover.sh status` now surfaces tenant_id + upgrade URL.** Free-tier users need to find their `tenant_id` (`cs_<uuid>`) to paste into the Stripe Checkout custom field at `getaxonflow.com/pro`. The status output now reads `~/.config/axonflow/try-registration.json` (the auto-bootstrap registration file) and prints the tenant_id alongside endpoint + license-token state. Adds an `upgrade` line (default `https://getaxonflow.com/pro`, override via `AXONFLOW_UPGRADE_URL`) and copy-paste-ready upgrade instructions. Token still redacted to last 4 chars (no full bearer credential in stdout — see PR #41).

### Added
Expand Down
54 changes: 54 additions & 0 deletions runtime-e2e/v1-paid-tier/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,60 @@ else
fi
rm -rf "$TMP_REG_DIR"

# 6f: V1 SaaS Plugin Pro tier-line surface parity. The status `tier` line
# must surface the JWT `exp` claim from the configured license token in
# three shapes:
# - "Pro tier active (expires YYYY-MM-DD, N days remaining)" (exp future)
# - "Free tier (Pro expired YYYY-MM-DD — visit ... to renew)" (exp past)
# - "Free tier (no AXON- license token configured)" (no token)
mint_axon_jwt() {
local exp_epoch="$1"
local hdr
hdr=$(printf '%s' '{"alg":"EdDSA","typ":"JWT"}' | base64 | tr '+/' '-_' | tr -d '=')
local payload
payload=$(printf '{"sub":"smoke","exp":%s}' "$exp_epoch" | base64 | tr '+/' '-_' | tr -d '=')
local sig="placeholder-signature-padding-padding-padding-padding-padding-pa"
printf 'AXON-%s.%s.%s' "$hdr" "$payload" "$sig"
}

# Pro-active path: exp ~30d future. Expect explicit YYYY-MM-DD + days remaining.
PRO_EXP=$(( $(date -u +%s) + 30 * 86400 ))
PRO_TOKEN=$(mint_axon_jwt "$PRO_EXP")
PRO_STATUS=$(HOME="$TMP_HOME3" AXONFLOW_LICENSE_TOKEN="$PRO_TOKEN" bash "$RECOVER" status 2>&1 || true)
if echo "$PRO_STATUS" | grep -qE "tier[[:space:]]+Pro tier active \(expires [0-9]{4}-[0-9]{2}-[0-9]{2}, [0-9]+ days remaining\)"; then
PASS "status Pro-active tier line shape (expires YYYY-MM-DD, N days remaining)"
else
FAIL "status Pro-active line missing expected shape: $PRO_STATUS"
fi
if echo "$PRO_STATUS" | grep -qF "$PRO_TOKEN"; then
FAIL "status leaked Pro-active token to stdout"
else
PASS "status redacts Pro-active token"
fi

# Pro-expired path: exp ~365d past. Expect "Free tier (Pro expired YYYY-MM-DD — visit ... to renew)".
EXPIRED_EXP=$(( $(date -u +%s) - 365 * 86400 ))
EXPIRED_TOKEN=$(mint_axon_jwt "$EXPIRED_EXP")
EXPIRED_STATUS=$(HOME="$TMP_HOME3" AXONFLOW_LICENSE_TOKEN="$EXPIRED_TOKEN" bash "$RECOVER" status 2>&1 || true)
if echo "$EXPIRED_STATUS" | grep -qE "tier[[:space:]]+Free tier \(Pro expired [0-9]{4}-[0-9]{2}-[0-9]{2} — visit https?://[^ ]+ to renew\)"; then
PASS "status Pro-expired tier line shape (Pro expired YYYY-MM-DD — visit ... to renew)"
else
FAIL "status Pro-expired line missing expected shape: $EXPIRED_STATUS"
fi
if echo "$EXPIRED_STATUS" | grep -qF "$EXPIRED_TOKEN"; then
FAIL "status leaked expired token to stdout"
else
PASS "status redacts expired token"
fi

# Free path: no token at all. Expect "Free tier (no AXON- license token configured)".
FREE_STATUS=$(HOME="$TMP_HOME3" bash "$RECOVER" status 2>&1 || true)
if echo "$FREE_STATUS" | grep -qE "tier[[:space:]]+Free tier \(no AXON- license token configured\)"; then
PASS "status Free-tier line shape (no AXON- license token configured)"
else
FAIL "status Free-tier line missing expected shape: $FREE_STATUS"
fi

# -----------------------------------------------------------------------------
# Test 7: apply-token persists into TOML.
# -----------------------------------------------------------------------------
Expand Down
95 changes: 92 additions & 3 deletions scripts/recover.sh
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,48 @@ cmd_apply_token() {
printf 'Pro-tier license token persisted. Restart Codex to pick it up.\n' >&2
}

# extract_jwt_exp <token> → prints unix-epoch integer to stdout, exits 0
# on success, non-zero on any parse failure. Pure stdout/stderr; never
# raises. The caller decides how to render a parse failure.
#
# AxonFlow license tokens are formatted `AXON-<JWT>` where <JWT> is a
# standard `header.payload.signature` triple. We base64url-decode the
# middle segment, then look for `"exp":<digits>`. Signature is NEVER
# validated here — display only.
extract_jwt_exp() {
local tok="$1"
[ -n "$tok" ] || return 1
local jwt="${tok#AXON-}"
local payload
payload=$(printf '%s' "$jwt" | cut -d. -f2)
[ -n "$payload" ] || return 1
payload=$(printf '%s' "$payload" | tr '_-' '/+')
local pad=$(( 4 - ${#payload} % 4 ))
if [ "$pad" -ne 4 ]; then
payload="${payload}$(printf '=%.0s' $(seq 1 "$pad"))"
fi
local decoded
decoded=$(printf '%s' "$payload" | base64 -d 2>/dev/null) \
|| decoded=$(printf '%s' "$payload" | base64 -D 2>/dev/null) \
|| return 1
[ -n "$decoded" ] || return 1
local exp
exp=$(printf '%s' "$decoded" | grep -oE '"exp"[[:space:]]*:[[:space:]]*[0-9]+' | head -1 | grep -oE '[0-9]+$')
[ -n "$exp" ] || return 1
printf '%s' "$exp"
}

# format_unix_to_date <unix-epoch> → prints YYYY-MM-DD (UTC) to stdout.
format_unix_to_date() {
local epoch="$1"
[ -n "$epoch" ] || return 1
local out
out=$(date -u -d "@${epoch}" +%Y-%m-%d 2>/dev/null) \
|| out=$(date -u -r "${epoch}" +%Y-%m-%d 2>/dev/null) \
|| return 1
printf '%s' "$out"
}

cmd_status() {
axonflow_resolve_license_token
local file="${AXONFLOW_CODEX_CONFIG:-$HOME/.codex/axonflow.toml}"
Expand All @@ -278,9 +320,21 @@ cmd_status() {
fi
fi

local upgrade_url="${AXONFLOW_UPGRADE_URL:-https://getaxonflow.com/pro}"

# Tier-line resolution. Three shapes (V1 SaaS Plugin Pro tier-line
# surface parity across the AxonFlow plugin set):
# - "Pro tier active (expires YYYY-MM-DD, N days remaining)" exp future
# - "Pro tier active (expires UNKNOWN — could not parse token)" parse fail
# - "Free tier (Pro expired YYYY-MM-DD — visit <url> to renew)" exp past
# - "Free tier (no AXON- license token configured)" no token
#
# The leading "Pro tier active" / "Free tier" preserves the existing
# contract (runtime-e2e/v1-paid-tier/test.sh greps for "Pro tier active"
# and "Free tier") so we extend without breaking the older assertions.
local tier token_display
local pro_expired_flag=0
if [ -n "${AXONFLOW_LICENSE_TOKEN_RESOLVED:-}" ]; then
tier="Pro tier active"
# Never print the full token to a terminal — it's a bearer credential and
# `recover.sh status` may be screen-shared, copy-pasted into a support
# ticket, or logged. Show a fixed prefix + last 4 chars only, padding
Expand All @@ -290,13 +344,34 @@ cmd_status() {
tail4="${AXONFLOW_LICENSE_TOKEN_RESOLVED: -4}"
fi
token_display="set (AXON-...${tail4})"

local exp_epoch
exp_epoch=$(extract_jwt_exp "$AXONFLOW_LICENSE_TOKEN_RESOLVED" 2>/dev/null || true)
if [ -n "$exp_epoch" ]; then
local exp_date
exp_date=$(format_unix_to_date "$exp_epoch" 2>/dev/null || true)
if [ -n "$exp_date" ]; then
local now_epoch
now_epoch=$(date -u +%s)
if [ "$exp_epoch" -gt "$now_epoch" ]; then
local secs_left=$(( exp_epoch - now_epoch ))
local days_left=$(( (secs_left + 86399) / 86400 ))
tier="Pro tier active (expires ${exp_date}, ${days_left} days remaining)"
else
tier="Free tier (Pro expired ${exp_date} — visit ${upgrade_url} to renew)"
pro_expired_flag=1
fi
else
tier="Pro tier active (expires UNKNOWN — could not parse token)"
fi
else
tier="Pro tier active (expires UNKNOWN — could not parse token)"
fi
else
tier="Free tier (no AXON- license token configured)"
token_display="unset"
fi

local upgrade_url="${AXONFLOW_UPGRADE_URL:-https://getaxonflow.com/pro}"

cat <<EOF
AxonFlow Codex plugin — status

Expand All @@ -307,6 +382,20 @@ AxonFlow Codex plugin — status
tier $tier
upgrade $upgrade_url

EOF

if [ "$pro_expired_flag" -eq 1 ]; then
cat <<EOF
Your Pro license token is on disk but its 'exp' has passed; the plugin will
not forward an expired token. After buying a renewal, replace the token via:
AXONFLOW_LICENSE_TOKEN=AXON-... codex …
or persist with:
scripts/recover.sh apply-token

EOF
fi

cat <<EOF
To upgrade to Pro (\$9.99 one-time), copy your tenant_id above, then visit
$upgrade_url, paste the tenant_id into the "Your AxonFlow tenant ID" field,
and complete checkout. The license token arrives by email; set it via:
Expand Down
27 changes: 21 additions & 6 deletions skills/pro-tier-status/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,41 @@
---
name: pro-tier-status
description: Report the user's current AxonFlow tier (free or Pro), endpoint, and whether a license token is configured. Use when the user asks "am I on Pro?", "what tier am I on?", "is my license active?", or wants to know which AxonFlow they're talking to.
description: Report the user's current AxonFlow tier (Free or Pro), Pro license expiry date, endpoint, and whether a license token is configured. Use when the user asks "am I on Pro?", "what tier am I on?", "when does my Pro license expire?", "is my license active?", or wants to know which AxonFlow they're talking to.
---

The Codex plugin runs in one of two tiers:

- **Free.** No `AXONFLOW_LICENSE_TOKEN` env var and no `license_token = "..."` line in `~/.codex/axonflow.toml`. The plugin omits the `X-License-Token` HTTP header on every governed request, and the agent applies free-tier quota / retention defaults.
- **Pro tier active.** Either `AXONFLOW_LICENSE_TOKEN` is exported in the Codex environment (operator override; CI use) or `~/.codex/axonflow.toml` contains a `license_token = "AXON-..."` line. The plugin sends `X-License-Token: <token>` on every governed request, and the agent's `PluginClaimMiddleware` validates the Ed25519 signature + DB row, then stamps a Pro-tier context on the request.
- **Free.** No `AXONFLOW_LICENSE_TOKEN` env var and no `license_token = "..."` line in `~/.codex/axonflow.toml` (or the line is there but its JWT `exp` is in the past — the plugin will not forward an expired token). The plugin omits the `X-License-Token` HTTP header on every governed request, and the agent applies free-tier quota / retention defaults.
- **Pro tier active.** Either `AXONFLOW_LICENSE_TOKEN` is exported in the Codex environment (operator override; CI use) or `~/.codex/axonflow.toml` contains a `license_token = "AXON-..."` line whose JWT `exp` is in the future. The plugin sends `X-License-Token: <token>` on every governed request, and the agent's `PluginClaimMiddleware` validates the Ed25519 signature + DB row, then stamps a Pro-tier context on the request.

Invoke the status surface via `exec_command`:

```bash
bash $PLUGIN_DIR/scripts/recover.sh status
```

The output reports:
## Tier line shape

The script's `tier` line takes one of three shapes — surface whichever one the user got:

- `tier Pro tier active (expires 2026-08-03, 90 days remaining)` — paid Pro tier active.
- `tier Pro tier active (expires UNKNOWN — could not parse token)` — token configured but the JWT body did not parse. Treat as Pro for display; the platform is the source of truth on validity.
- `tier Free tier (Pro expired 2026-02-04 — visit https://getaxonflow.com/pro to renew)` — token is on disk but its `exp` has passed. The plugin will not forward an expired token; the user must buy a renewal and replace the token via `AXONFLOW_LICENSE_TOKEN=<new>` or `scripts/recover.sh apply-token`.
- `tier Free tier (no AXON- license token configured)` — no token loaded.

When the user lands on `Free tier (Pro expired …)`, point them at the renew URL embedded in the line and the `scripts/recover.sh apply-token` hint the script prints below.

## Other lines the script reports

- the active endpoint (`AXONFLOW_ENDPOINT` or the community-saas default)
- whether `~/.codex/axonflow.toml` exists
- whether a license token is currently resolvable
- the tier (`Pro tier active` or `Free tier (no AXON- license token configured)`)
- the user's `tenant_id` (read from `~/.config/axonflow/try-registration.json`) — needed to paste into the Stripe checkout custom field at /pro
- a redacted preview of the configured license token (`set (AXON-...XXXX)` — last 4 chars only, never the full bearer credential)

## Renewal + upgrade path

If the user is on Free and asks about upgrading, tell them: a Pro license token arrives by email after Stripe Checkout completes, and they install it with `scripts/recover.sh apply-token` (or by setting `AXONFLOW_LICENSE_TOKEN`). Don't paste the token into chat — the script reads from stdin or env.

For richer governance activity (policy hits, override usage, audit volume), point the user to the `governance-status` skill, which calls the platform's `get_policy_stats` MCP tool.

The script extracts the JWT `exp` claim for display only; signature validation is the platform's job.
Loading