Skip to content

Add browser-based CLI login and MCP OAuth#245

Merged
Makisuo merged 2 commits into
mainfrom
codex/cli-mcp-oauth-login
Jul 21, 2026
Merged

Add browser-based CLI login and MCP OAuth#245
Makisuo merged 2 commits into
mainfrom
codex/cli-mcp-oauth-login

Conversation

@Makisuo

@Makisuo Makisuo commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

Replaces token-paste-first Maple CLI authentication with a GitHub-style browser device flow, and reuses the same approval experience for standards-based MCP OAuth.

What changed

  • Adds maple auth login, status, and logout while retaining the existing compatibility aliases.
  • Opens a 15-minute device-code approval flow at /cli-login, with headless/manual URL support and stdin-only --with-token.
  • Stores credentials in macOS Keychain or Linux Secret Service, falling back to the existing 0600 config with an explicit warning.
  • Adds hashed/encrypted CLI authorization persistence, role-pinned CLI API keys, revocation, and public rate limiting.
  • Adds OAuth protected-resource and authorization-server discovery for /mcp.
  • Adds dynamic client registration, authorization code + S256 PKCE, rotating refresh tokens, revocation, and an authenticated /mcp-authorize consent page.
  • Audience-binds MCP access tokens to the exact Maple /mcp resource and captures the approving user's active workspace and roles.
  • Updates MCP settings and documentation to prefer OAuth while preserving manual API keys.
  • Adds Postgres migrations 0017 and 0018.

Security properties

  • Device codes, user codes, authorization codes, client IDs, and refresh tokens are persisted only as hashes where applicable.
  • Issued CLI credentials are encrypted only for their short-lived delivery window and never stored in plaintext.
  • OAuth authorization codes expire after five minutes and require S256 PKCE.
  • MCP access tokens expire after one hour; refresh tokens rotate and replay revokes the entire token family.
  • Malformed Maple-managed role metadata fails closed; existing ordinary API keys retain their legacy root-role behavior.
  • Forwarded origin handling is covered so audience validation remains correct behind Cloudflare/portless proxies.

Local end-to-end verification

Ran against the real local Postgres, API, web app, and Clerk test session:

  • CLI device flow: start → browser approve → poll/deliver → session validate → revoke → rejected after revocation.
  • MCP OAuth: discovery → DCR → browser consent → PKCE exchange → authenticated MCP initialize → refresh rotation → old access token rejected → refresh-family revocation.
  • The live OAuth pass found and fixed a forwarded-origin audience mismatch; a regression test now covers it.

Automated checks

  • API focused suite: 19 passed.
  • Web approval-route suite: 6 passed.
  • CLI suite: 113 passed.
  • API, web, CLI, DB, and domain typechecks.
  • Oxlint and formatting checks.
  • Web production build.
  • macOS Bun-compiled CLI smoke test for the new auth command surface.

Rollout

Deploy migration/API support first, then the web approval routes, then release the CLI. Existing API keys, legacy plaintext CLI configs, and older CLI aliases continue to work during rollout.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown

Ingest Rust Test + Benchmark Results

Commit: 3bed2ea063f4dd2ae05b433af8b2a7e998d83dbb

Load Benchmark — tinybird mode, median of 3 run(s) vs main

Metric main (median) PR (median) Delta
Requests/sec 9479.29 10090.21 +6.4% better
Rows/sec 94792.86 100902.07 +6.4% better
p50 latency 6.37 ms 5.99 ms -6.0% better
p95 latency 12.85 ms 11.61 ms -9.7% better
p99 latency 16.56 ms 16.94 ms +2.3% worse
Export catch-up 0.026 s 0.026 s +0.8% worse
Max RSS 15.17 MiB 18.42 MiB +21.4% worse
Failures 0 0 same

Same code path on both sides (same LOAD_TEST_INGEST_MODE), so the delta column is meaningful. Numbers come from ubuntu-latest, which is noisy — treat single-digit-percent deltas as noise.

PR load benchmark JSON (per-iteration)
[
  {
    "ingest_mode": "tinybird",
    "requests": 2000,
    "successes": 2000,
    "failures": 0,
    "rows_sent": 20000,
    "rows_exported": 20000,
    "imports": 12,
    "duration_seconds": 0.198211983,
    "export_catchup_seconds": 0.026596355,
    "request_rps": 10090.207311028213,
    "row_rps": 100902.07311028213,
    "p50_ms": 5.989,
    "p95_ms": 11.611,
    "p99_ms": 13.698,
    "max_rss_mb": 20.39453125,
    "max_cpu_percent": 20.0,
    "avg_cpu_percent": 20.0
  },
  {
    "ingest_mode": "tinybird",
    "requests": 2000,
    "successes": 2000,
    "failures": 0,
    "rows_sent": 20000,
    "rows_exported": 20000,
    "imports": 11,
    "duration_seconds": 0.189958855,
    "export_catchup_seconds": 0.025797039,
    "request_rps": 10528.59578459767,
    "row_rps": 105285.95784597669,
    "p50_ms": 5.727,
    "p95_ms": 11.37,
    "p99_ms": 16.941,
    "max_rss_mb": 15.44140625,
    "max_cpu_percent": 0.0,
    "avg_cpu_percent": 0.0
  },
  {
    "ingest_mode": "tinybird",
    "requests": 2000,
    "successes": 2000,
    "failures": 0,
    "rows_sent": 20000,
    "rows_exported": 20000,
    "imports": 12,
    "duration_seconds": 0.216943838,
    "export_catchup_seconds": 0.026337282,
    "request_rps": 9218.975834658184,
    "row_rps": 92189.75834658185,
    "p50_ms": 6.506,
    "p95_ms": 13.303,
    "p99_ms": 18.912,
    "max_rss_mb": 18.41796875,
    "max_cpu_percent": 16.6,
    "avg_cpu_percent": 16.6
  }
]
main load benchmark JSON (per-iteration)
[
  {
    "ingest_mode": "tinybird",
    "requests": 2000,
    "successes": 2000,
    "failures": 0,
    "rows_sent": 20000,
    "rows_exported": 20000,
    "imports": 13,
    "duration_seconds": 0.213134952,
    "export_catchup_seconds": 0.02666513,
    "request_rps": 9383.726044145027,
    "row_rps": 93837.26044145027,
    "p50_ms": 6.606,
    "p95_ms": 13.025,
    "p99_ms": 16.557,
    "max_rss_mb": 15.16796875,
    "max_cpu_percent": 0.0,
    "avg_cpu_percent": 0.0
  },
  {
    "ingest_mode": "tinybird",
    "requests": 2000,
    "successes": 2000,
    "failures": 0,
    "rows_sent": 20000,
    "rows_exported": 20000,
    "imports": 11,
    "duration_seconds": 0.210986364,
    "export_catchup_seconds": 0.025982331,
    "request_rps": 9479.285590228948,
    "row_rps": 94792.85590228949,
    "p50_ms": 6.37,
    "p95_ms": 12.854,
    "p99_ms": 18.847,
    "max_rss_mb": 14.80859375,
    "max_cpu_percent": 0.0,
    "avg_cpu_percent": 0.0
  },
  {
    "ingest_mode": "tinybird",
    "requests": 2000,
    "successes": 2000,
    "failures": 0,
    "rows_sent": 20000,
    "rows_exported": 20000,
    "imports": 12,
    "duration_seconds": 0.1923536,
    "export_catchup_seconds": 0.026140106,
    "request_rps": 10397.51790452583,
    "row_rps": 103975.17904525831,
    "p50_ms": 5.838,
    "p95_ms": 11.592,
    "p99_ms": 15.963,
    "max_rss_mb": 15.59375,
    "max_cpu_percent": 0.0,
    "avg_cpu_percent": 0.0
  }
]

WAL-acked microbench (cargo bench --bench ingest_bench)

   Compiling maple-ingest v0.1.0 (/home/runner/_work/maple/maple/apps/ingest)
    Finished `bench` profile [optimized] target(s) in 20.69s
     Running benches/ingest_bench.rs (target/release/deps/ingest_bench-581d2100de893627)
Gnuplot not found, using plotters backend
test ingest_accept/logs_10_rows_wal_ack ... bench:      128971 ns/iter (+/- 17512)
test ingest_accept/traces_10_spans_wal_ack ... bench:      151790 ns/iter (+/- 18484)

cargo test

test telemetry::tests::metric_encoder_matches_all_tinybird_datasource_shapes ... ok
test telemetry::tests::metrics_emit_exactly_the_jsonpaths_declared_in_datasources_ts ... ok
test telemetry::tests::migrate_legacy_shard_relocates_frames_into_lanes ... ok
test telemetry::tests::pipeline_can_start_for_clickhouse_only_without_tinybird_credentials ... ok
test telemetry::tests::clickhouse_export_drops_passworded_non_https_endpoint_without_sending ... ok
test telemetry::tests::sampling_keeps_errors_even_when_ratio_low ... ok
test telemetry::tests::scraper_contract::scraper_otlp_json_decodes_with_gateway_serde_and_encodes_to_rows ... ok
test telemetry::tests::signal_tag_round_trips_all_variants ... ok
test telemetry::tests::pipeline_e2e_exports_metrics_to_fake_tinybird ... ok
test telemetry::tests::telemetry_signal_as_str_is_canonical_lowercase ... ok
test telemetry::tests::timestamp_has_nano_precision ... ok
test telemetry::tests::timestamps_match_clickhouse_datetime64_nine_format ... ok
test telemetry::tests::trace_encoder_matches_tinybird_row_shape ... ok
test telemetry::tests::traces_emit_exactly_the_jsonpaths_declared_in_datasources_ts ... ok
test telemetry::tests::pipeline_e2e_exports_gzip_ndjson_to_fake_tinybird ... ok
test telemetry::tests::wal_round_trips_frame ... ok
test telemetry::tests::wal_partial_drain_advances_cursor_without_truncating ... ok
test telemetry::tests::wal_truncates_after_full_drain_allowing_further_appends ... ok
test telemetry::tests::pipeline_e2e_exports_traces_to_fake_tinybird ... ok
test telemetry::tests::pipeline_exports_ready_org_to_clickhouse_without_tinybird_calls ... ok
test telemetry::tests::slow_clickhouse_lane_does_not_block_cosharded_tinybird_org ... ok
test telemetry::tests::clickhouse_breaker_sheds_after_threshold_failures ... ok

test result: ok. 36 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.74s

     Running unittests src/bin/load_test.rs (target/debug/deps/load_test-661a0aa1eb3f6d6d)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/debug/deps/maple_ingest-c33bf80c577edb95)

running 38 tests
test autumn::tests::allowed_only_no_balance_field ... ok
test autumn::tests::flat_hardcap_depleted_blocks ... ok
test autumn::tests::flat_hardcap_with_remaining_allows ... ok
test autumn::tests::flat_overage_allows ... ok
test autumn::tests::flat_sub_one_gb_remaining_still_allows ... ok
test autumn::tests::nested_overage_allows ... ok
test autumn::tests::nested_balance_object_depleted_blocks ... ok
test autumn::tests::flat_unlimited_allows ... ok
test autumn::tests::nested_balance_object_with_remaining_allows ... ok
test tests::api_error_kind_maps_status_to_stable_label ... ok
test tests::clickhouse_destination_is_terminal_in_dual_mode ... ok
test tests::api_error_from_pipeline_maps_variants_to_status ... ok
test autumn::tests::null_balance_no_subscription_blocks ... ok
test tests::clickhouse_destination_uses_native_pipeline_even_in_forward_mode ... ok
test tests::clickhouse_target_resolver_rejects_password_over_http ... ok
test tests::clickhouse_target_resolver_decrypts_current_schema_password ... ok
test autumn::tests::unrecognized_shape_returns_none ... ok
test tests::cloudflare_log_record_maps_body_severity_and_attributes ... ok
test tests::cloudflare_timestamps_support_rfc3339_unix_and_unix_nano ... ok
test tests::clickhouse_target_resolver_requires_current_schema ... ok
test tests::cloudflare_validation_payload_is_detected ... ok
test tests::cloudflare_ndjson_payload_parses_multiple_records ... ok
test tests::enrichment_overwrites_tenant_fields ... ok
test tests::decrypt_aes256_gcm_matches_node_crypto_fixture ... ok
test tests::extract_ingest_key_returns_sentinel_literal_unchanged ... ok
test tests::rejection_span_status_is_error_only_for_5xx ... ok
test tests::hash_is_deterministic ... ok
test tests::resolve_ingest_key_returns_none_when_hash_missing ... ok
test tests::resolve_ingest_key_returns_self_managed_false_when_no_settings_row ... ok
test tests::resolve_ingest_key_returns_self_managed_true_when_active_settings_row ... ok
test tests::resolve_ingest_key_keeps_stale_schema_on_managed_native_path ... ok
test tests::sentinel_token_matches_only_exact_literal ... ok
test tests::tinybird_destination_keeps_forward_mode_on_forward_path ... ok
test tests::resolve_ingest_key_refreshes_routing_before_auth_cache_expires ... ok
test tests::resolve_connector_refreshes_routing_before_auth_cache_expires ... ok
test autumn::tests::fails_open_on_transport_error ... ok
test tests::resolve_ingest_key_serves_last_known_routing_when_refresh_fails ... ok
test tests::forward_mode_switches_ready_org_to_clickhouse_without_forwarding_again ... ok

test result: ok. 38 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.20s

   Doc-tests maple_ingest

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

The criterion bench hashes every frame to a single org, so all frames land
in one WAL lane. That lane only truncates when the exporter's cursor fully
catches up with the appender, which never happens under sustained bench
load — so the lane grows monotonically and a fast enough runner trips the
128 MiB per-lane cap mid-run:

    panicked at benches/ingest_bench.rs:73:22:
    accept traces: QueueUnavailable("Telemetry WAL lane is full")

The bench measures accept latency (encode + WAL append + ack), not
back-pressure, so uncap the queue budgets instead of tuning a threshold
that is really a function of runner speed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Makisuo
Makisuo marked this pull request as ready for review July 21, 2026 21:33
@pullfrog

pullfrog Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Your Pullfrog Router balance is exhausted.

You have a card on file but auto-reload is disabled, so runs paused once your balance went past the overdraft buffer.

Top up balance → · Enable auto-reload →

Pullfrog  | ⚠️ this action is pinned to a commit SHA, which freezes the cleanup step — switch to @v0 or keep the SHA fresh with Dependabot | Rerun failed job ➔View workflow run | via Pullfrog | Using Claude Opus𝕏

@Makisuo
Makisuo merged commit bd80182 into main Jul 21, 2026
9 checks passed
@Makisuo
Makisuo deleted the codex/cli-mcp-oauth-login branch July 21, 2026 21:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant