Commit Graph

5 Commits

Author SHA1 Message Date
f4117224fc feat(B6): served-usage ledger + reconciliation (#58)
All checks were successful
CI / Format (push) Successful in 35s
CI / CUDA type-check (push) Successful in 1m37s
CI / Clippy (push) Successful in 2m53s
CI / Test (push) Successful in 6m41s
CI / Build cortex SRPM (push) Has been skipped
CI / Build neuron SRPM (push) Has been skipped
CI / Publish cortex to COPR (push) Has been skipped
CI / Publish neuron to COPR (push) Has been skipped
CI / Bump version in source (push) Has been skipped
Operators are now metered for the tokens they serve on behalf of mesh
accounts, and the upstream rolls that up for compensation.

cortex-gateway:
- served_usage.rs: an in-process per-(account,key,UTC-day) served-token
  counter, incremented in metering::usage_sink alongside spend/settle for
  every authenticated request. A flush task (spawned in run() when
  [upstream].enabled, mirroring poller/evictor) POSTs ABSOLUTE cumulative
  counters to upstream on [upstream].served_usage_report_interval_secs.
- The no-limit infra key is the operator's local key with hard_cap=None
  (already supported) — it's metered for served-usage but never budget-
  refused and never hits upstream.

helexa-upstream:
- POST /authz/v1/served-usage: upserts rows keyed by (operator_id from the
  client bearer, account, key, period) with
  GREATEST(existing, incoming) — monotonic + idempotent, so re-sends,
  races, and a restarted cortex's lower counter never regress the total.
- reconcile.rs + `helexa-upstream reconcile` CLI: rolls up unreconciled
  served_usage per operator/period (SUM::bigint), stamps reconciled_at,
  prints the totals. Payout mechanism out of scope.

Validated against a throwaway Postgres: monotonic upsert (100→250, a 50
re-send stays 250, same-value idempotent) and reconcile rollup +
stamp-once; cortex counter unit test for per-principal accumulation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F6o3ddqmYNh9kzdwq6eowh
2026-06-23 11:45:57 +03:00
2348cc2234 feat(B5): single-use top-up codes (redeem + mint CLI)
All checks were successful
CI / Format (push) Successful in 33s
CI / CUDA type-check (push) Successful in 1m33s
CI / Clippy (push) Successful in 3m5s
CI / Test (push) Successful in 6m29s
CI / Build cortex SRPM (push) Has been skipped
CI / Publish cortex to COPR (push) Has been skipped
CI / Build neuron SRPM (push) Has been skipped
CI / Publish neuron to COPR (push) Has been skipped
CI / Bump version in source (push) Has been skipped
Completes the hybrid allocation model — the flat free grant (B4) plus
top-up codes that extend an account's allocation.

- topup.rs: redeem(account, raw_code) — timing-safe, single-use. A
  conditional `UPDATE … WHERE redeemed_by IS NULL RETURNING value` claims
  the code atomically (concurrent double-redeem → exactly one winner), then
  raises accounts.allocation_total in the same tx. Only sha256(code) is
  stored; a not-found code and an already-redeemed code return the SAME
  generic error via the same path (no "valid but spent" oracle). Raising
  the total automatically lifts every percent-limited key's effective cap;
  hardcap keys stay pinned (by design).
- mint(value, count, denomination) — inserts codes (hash-only), returns the
  raw codes once. Exposed as `helexa-upstream mint --value --count
  [--denomination]` (raw codes to stdout, one per line) — the seam the
  future faucet bot calls. Bot itself out of scope.
- POST /web/v1/redeem (session-protected) → {allocation_total} | generic 400.

Validated against Postgres: redeem raises the 1_000_000 free grant to
1_500_000, second redemption + unknown code both generic-400, and a
concurrent race for one code yields exactly one HTTP 200.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F6o3ddqmYNh9kzdwq6eowh
2026-06-23 10:50:43 +03:00
d94c62c143 feat(B4): /web/v1 account API + silent fingerprint multi-account abuse
All checks were successful
CI / Format (push) Successful in 44s
CI / CUDA type-check (push) Successful in 1m40s
CI / Clippy (push) Successful in 3m14s
CI / Test (push) Successful in 6m29s
CI / Build cortex SRPM (push) Has been skipped
CI / Publish cortex to COPR (push) Has been skipped
CI / Build neuron SRPM (push) Has been skipped
CI / Publish neuron to COPR (push) Has been skipped
CI / Bump version in source (push) Has been skipped
The human-facing account surface the helexa.ai frontend (F4) consumes,
on top of B2's authz surface. Email+password auth with JWT sessions
(distinct from inference API keys); plain-JSON errors (the #63 envelope
stays on the authz surface).

- Auth lifecycle: register → email-verify → login → password-reset
  (request/confirm). argon2id passwords; verify/reset via single-use
  sha256-hashed email tokens; register and reset-request always return
  202 (no account enumeration). Email via a pluggable EmailSender (lettre
  Smtp + dev Log transport).
- API keys: create (sk-helexa-<base62(32 OsRng)>, raw shown once, stored
  as sha256 + non-secret prefix), list (prefix never the secret), archive,
  PATCH per-key limit (percent|hardcap). Protected by a JWT session
  middleware.
- Account balance endpoint (allocation total/spent/reserved).
- Silent fingerprint abuse: register captures the browser fingerprint;
  >= threshold (default 5) accounts sharing one fingerprint are silently
  deactivated + flagged — registration still returns a normal 202, and a
  deactivated account's key resolves as an ordinary 401 at the authz
  surface (no "banned" signal anywhere).
- crypto: argon2 hash/verify + CSPRNG token/key minting (base62). config
  gains [auth] + [email]. CORS on the app for the browser SPA.

Validated against a throwaway Postgres 16: verify-once, full lifecycle
(register→verify→login→create key→account→list→authz resolve→archive→401),
and 5-same-fingerprint → all accounts silently deactivated + no-clue 401.
8 unit + 11 gated integration tests; all skip cleanly without
UPSTREAM_TEST_DATABASE_URL so CI stays green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F6o3ddqmYNh9kzdwq6eowh
2026-06-23 10:28:20 +03:00
46befde4cd feat(B2): /authz/v1 authority surface + client-auth + reservation sweeper
All checks were successful
CI / Format (push) Successful in 49s
CI / CUDA type-check (push) Successful in 1m42s
CI / Clippy (push) Successful in 3m11s
CI / Test (push) Successful in 7m16s
CI / Build cortex SRPM (push) Has been skipped
CI / Publish cortex to COPR (push) Has been skipped
CI / Build neuron SRPM (push) Has been skipped
CI / Publish neuron to COPR (push) Has been skipped
CI / Bump version in source (push) Has been skipped
The machine surface cortex's UpstreamEntitlementProvider (#57) consumes,
mirroring the EntitlementProvider trait 1:1 over the B1 ledger.

- `authz.rs`: POST /authz/v1/{resolve,reserve,settle,release,snapshot}.
  resolve → {principal, snapshot} | 401 invalid_api_key (a deactivated
  account resolves as the SAME 401 — the silent-abuse no-clue property).
  reserve returns 200 whether granted ({reservation_id}) or budget-refused
  ({rejected:{kind,...}}) — a refusal is an authoritative answer, not a
  transport failure; non-2xx means "fail closed" to the client. settle/
  release → 204 (idempotent). snapshot → {hard_cap,spent,reserved} | 404.
  Rejections use the shared #63 OpenAiError envelope (cortex-core dep).
- Client auth: shared-bearer middleware (constant-time compare via subtle)
  maps a token → operator_id (stamped into request extensions for #58
  served-usage); empty config = open dev surface (logged). mTLS deferred.
- ledger gains resolve_key (sha256 lookup, account-active-gated), snapshot,
  and sweep_stale (one data-modifying-CTE statement releasing aged-out open
  reservations and folding their reserved tokens back into accounts+keys).
- Sweeper task spawned in run(); [authz] ttl/interval + [client_auth]
  config; crypto::sha256 helper.

Validated against a throwaway Postgres 16 (fresh schema): resolve→reserve→
settle→snapshot round-trip, over-cap → 200 insufficient_quota rejection
(not retried away), deactivated account → 401 (no clue), missing/wrong
client bearer → 401 before any DB hit. 5 unit + 8 gated integration tests;
all skip cleanly without UPSTREAM_TEST_DATABASE_URL so CI stays green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F6o3ddqmYNh9kzdwq6eowh
2026-06-23 10:12:19 +03:00
71106afaf1 feat(B1): helexa-upstream crate skeleton + Postgres schema + ledger
All checks were successful
CI / CUDA type-check (push) Successful in 1m37s
CI / Format (push) Successful in 33s
CI / Clippy (push) Successful in 3m14s
CI / Test (push) Successful in 5m38s
CI / Build cortex SRPM (push) Has been skipped
CI / Publish cortex to COPR (push) Has been skipped
CI / Build neuron SRPM (push) Has been skipped
CI / Publish neuron to COPR (push) Has been skipped
CI / Bump version in source (push) Has been skipped
First milestone of the mesh-level account/authorization authority (#59).
New workspace crate `crates/helexa-upstream` (binary + lib), mirroring the
helexa-router skeleton: axum `build_app`/`run`, clap `serve --config`,
figment `UPSTREAM_`-prefixed config.

- **Storage:** PostgreSQL via sqlx (runtime query API — builds in CI with no
  DB or offline cache; correctness covered by gated integration tests).
  `migrations/0001_init.sql` is the full schema: users (argon2 hash slot,
  email_verified, registration_fingerprint), email_tokens, accounts
  (allocation total/spent/reserved + `accounts_no_overshoot` CHECK, silent
  `status` deactivation flag, fingerprint_flagged), api_keys (sha256 hash,
  percent|hardcap limit, cap_window, per-key ledger), reservations
  (BIGSERIAL id → maps to cortex Reservation.id u64), top_up_codes,
  served_usage, sessions. Migrations run on startup.
- **Ledger (no-overshoot core):** `ledger::{reserve,settle,release}` —
  reserve takes `SELECT … FOR UPDATE` on the account + key rows so
  concurrent reserves serialize and spent+reserved can never exceed the
  effective cap (= min(resolved key cap, remaining account allocation));
  the CHECK is the DB backstop. Settle clamps actual to [0,reserved] and is
  idempotent; release is idempotent. `resolve_abs_cap` (percent/hardcap,
  i128 math) is pure + unit-tested. Balance semantics here; rolling-window
  sub-caps + RateLimited land with the authz API (B2).
- `/health` does a DB round-trip.
- Config + example TOML ([server]/[db]/[grant] free grant/[abuse]
  fingerprint threshold).

Validated end-to-end against a throwaway Postgres 16: migration applies,
20 concurrent reserves of 100 against a 500 cap admit exactly 5 (reserved
== 500, never over), settle/release idempotent, hardcap key sub-cap binds
below the account, `/health` → db ok. CI runs the cap-math + config unit
tests; the DB integration tests skip cleanly when UPSTREAM_TEST_DATABASE_URL
is unset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F6o3ddqmYNh9kzdwq6eowh
2026-06-23 09:49:19 +03:00