Skip to content

feat: add mnfst management CLI with browser OAuth login - #2673

Draft
guillaumegay13 wants to merge 59 commits into
mnfst:mainfrom
guillaumegay13:feat/mnfst-cli
Draft

feat: add mnfst management CLI with browser OAuth login#2673
guillaumegay13 wants to merge 59 commits into
mnfst:mainfrom
guillaumegay13:feat/mnfst-cli

Conversation

@guillaumegay13

@guillaumegay13 guillaumegay13 commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

✨ What changed

  • New packages/cli: mnfst, an agent-first management CLI for Manifest (zero runtime deps, JSON stdout, 237 tests, 100% line coverage).
  • Browser OAuth login: mnfst login opens the dashboard, one click on the new /cli/auth page mints a 30-day sliding-expiry token via a one-time code exchange (cli_auth_codes table, single-use, 5 min TTL). Headless keeps --token-stdin/--token-env; logout revokes server-side.
  • Expiring API keys: nullable api_keys.expires_at, slid forward on use by ApiKeyGuard (CLI_TOKEN_TTL_DAYS, default 30). Existing keys stay non-expiring.
  • Agent lifecycle: agent create --if-absent (returns per-platform setup instructions), configure (default route + fallbacks via --models a,b,c, custom header tiers via --tier), setup [--reveal], env, key path|show [--raw], managed keystore, rotate-key (revocation now instant), provider enable|disable, run (env-injected child processes).
  • Providers: positional connect with per-platform auth (api_key, browser OAuth for xai/openai/gemini, paste-code for Anthropic, device flow for Kiro/MiniMax), catalog, custom add|list|remove (probe-first), per-agent enable/disable.
  • Routing: status (composite readback), test (one real request through the agent platform's actual API surface — messages vs chat completions — with fake-200 Manifest errors unmasked), custom tier lifecycle. The deprecated complexity tiers are absent from the CLI by design.
  • Observability: requests get with API-faithful cursor pagination, decision-relevant row trim + --full; models <agent> [--cost] [--capabilities] mirroring /v1/models.
  • Generated catalogs: providers, platforms + API surfaces (PLATFORM_API_SURFACES, new in manifest-shared), setup snippets (moved to manifest-shared, dashboard re-exports), and categories are all codegen'd from shared at build with drift specs plus a CI git diff --exit-code gate.
  • Anonymous CLI telemetry: one event per command (registry key, version, os, ok, duration — never arguments or keys), persistent anonymous install id, MANIFEST_TELEMETRY_DISABLED=1 opt-out, disclosed in --help and README. Ingest lands via mnfst/peacock#222.
  • Agent-facing skill at .claude/skills/mnfst-cli/ — operating guide tested against cold-start agents.
  • Backend fixes: AgentKeyAuthGuard caches made class-static (Nest instantiates class-referenced @UseGuards per host module, so rotation cleared a different instance than the proxy authenticated with — revoked keys survived up to 5 min); e2e tenant-request-usage-migration date-bomb fix (fixtures were pinned to July 2026 and fail everywhere — including upstream main — since Aug 1; the quota window is now derived from the clock).

💭 Why

Coding agents and orchestrators should operate Manifest without the dashboard: provision an agent per automation, route it, verify it, wire it, observe it — one credential, no key ever crossing a transcript. Humans get one browser click; agents get env auth and JSON everywhere.

👤 For users

mnfst loginagent create --name bot --platform openai-sdk --if-absentagent configure bot --models cheap,fallback --provider xairouting test botagent env bot >> .env. Display names work everywhere (CLI slugifies like the backend). Destructive ops require --yes; multi-auth providers require explicit --auth-type.

🔧 For operators

Three additive migrations (api_keys.expires_at, cli_auth_codes, plus the guard change is code-only). New optional envs: CLI_TOKEN_TTL_DAYS, MANIFEST_TELEMETRY_DISABLED, MANIFEST_CLI_TELEMETRY_ENDPOINT. CI: cli job builds shared first and gates on generated-catalog freshness. The e2e date-bomb fix also applies to main (its CI will fail on the next run without it — consider cherry-picking).

📝 Notes

  • A PAT cannot mint another PAT; the raw token never transits the browser; agent keys live in a 0600 keystore or explicit --key-file, never stdout.
  • All expiry timestamps share one clock (Node, naive-local) — the review loop killed three timezone bugs of that class.
  • Follow-ups tracked: dashboard PAT list/revoke UI, self-hosted API_KEY tenant binding + setup admin, limits CRUD, tab completion, npm publishing.

@codecov

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.46%. Comparing base (e4b6e6c) to head (7d9e404).
⚠️ Report is 76 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #2673      +/-   ##
==========================================
+ Coverage   96.44%   96.46%   +0.01%     
==========================================
  Files         599      605       +6     
  Lines       35231    35372     +141     
  Branches     9815     9841      +26     
==========================================
+ Hits        33979    34122     +143     
+ Misses        691      689       -2     
  Partials      561      561              
Flag Coverage Δ
backend 95.64% <100.00%> (+0.02%) ⬆️
frontend 97.32% <100.00%> (+0.01%) ⬆️
shared 99.45% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…ertions

The expired-code test wrote SQL now() (the DB container's UTC) into
cli_auth_codes.expires_at, whose invariant is the Node process's local wall
clock. West of UTC the cutoff landed hours in the future and the test expected
400 but got 200. Compute the cutoff with toLocalSqlTimestamp() and scope the
UPDATE to the row under test.

Also: @IsString() + readable messages on the DTOs, count cli keys on the happy
path so a double-mint fails, pin expiresAt as ISO-8601 UTC at the configured
TTL, and retitle the happy-path test so it does not claim to prove the PAT
authenticates.
@guillaumegay13 guillaumegay13 changed the title feat: add mnfst management CLI and /api/v1/me endpoint feat: add mnfst management CLI with browser OAuth login Aug 2, 2026
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Database schema changes

Migrations in this PR:

  • 1801700000000-AddApiKeyExpiresAt.spec.ts
  • 1801700000000-AddApiKeyExpiresAt.ts
  • 1801710000000-CreateCliAuthCodes.spec.ts
  • 1801710000000-CreateCliAuthCodes.ts

Summary

  • 🟢 Added tables: cli_auth_codes
  • 🟡 Modified tables: api_keys (+1 col)

Schema diagram

Showing only changed tables. 🟢 added, 🟡 modified, 🔴 removed. Changed columns are annotated inline.

erDiagram
    api_keys["🟡 api_keys (modified)"] {
        varchar id PK
        varchar key
        varchar created_by_user_id
        varchar name
        timestamp_without_time_zone created_at
        timestamp_without_time_zone last_used_at
        varchar_128_ key_hash
        varchar_12_ key_prefix
        varchar tenant_id
        timestamp_without_time_zone expires_at "🟢 added"
    }
    cli_auth_codes["🟢 cli_auth_codes (added)"] {
        varchar id PK
        varchar_64_ code_hash
        varchar_128_ state
        varchar tenant_id
        varchar user_id
        timestamp_without_time_zone created_at
        timestamp_without_time_zone expires_at
    }
Loading
Detailed column-level changes

🟢 cli_auth_codes (new table)

  • id varchar NOT NULL PK
  • code_hash varchar(64) NOT NULL
  • state varchar(128) NOT NULL
  • tenant_id varchar NOT NULL
  • user_id varchar NULL
  • created_at timestamp without time zone NOT NULL
  • expires_at timestamp without time zone NOT NULL

🟡 api_keys

  • ➕ added expires_at timestamp without time zone NULL

Generated by the db-diagram workflow • only tables with schema changes are shown.

@codecov

codecov Bot commented Aug 2, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 2.8kB (0.18%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
manifest-frontend-esm 1.55MB 2.8kB (0.18%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: manifest-frontend-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/index-*.js 155 bytes 138.57kB 0.11%
assets/SetupStepAddProvider-*.js 75 bytes 35.96kB 0.21%
assets/CliAuth-*.js (New) 2.57kB 2.57kB 100.0% 🚀

Comment thread packages/cli/src/slug.ts Fixed
… and custom header tiers, drop deprecated complexity tiers
The quota counter buckets a request into
GREATEST(date_trunc('month', timestamp), PLAN_REQUEST_QUOTA_RESET_AT).
The fixture asserted against the shipped reset default (2026-07-09), which
only equals that window while the clock sits inside July 2026, while the
rows it inserts as "live" are stamped from clock_timestamp() at migration
time. From 2026-08-01 the trigger bucketed live rows into the calendar-month
window and the assertions kept reading the July row, so three tests saw only
the historical baseline (3 -> 2, 3 -> 2, 4 -> 2). Upstream main reproduces
this identically today; no production code is involved.

Override PLAN_REQUEST_QUOTA_RESET_AT for the suite with
max(now - 2h, start of current UTC month) and derive the asserted window and
the historical seed timestamps from it, so every fixture row shares one
window regardless of the month the suite runs in.
…, oauth cadence

Twelve localized findings from the final review of the mnfst CLI.

Routing:
- routing test defaulted /v1/messages to a hardcoded claude-sonnet-5 on the
  false premise that "auto" is not a route override there; every public
  surface routes "auto" and reads a concrete model as an explicit override,
  so both surfaces now default to auto (--model stays the override).
- --as is validated against PLATFORM_CATALOG before any network call; an
  unknown value used to silently test the wrong surface.

Catalog drift:
- CI now runs git diff --exit-code on provider-catalog.gen.ts after the cli
  build: the build regenerates the file, so the drift specs can never catch
  a stale commit. The generator formats its output with the repo prettier
  config so the committed file and a fresh gen are byte-identical.
- The spec drift-compares SETUP_TEMPLATES too, and the generator emits
  CATEGORY_CATALOG from manifest-shared AGENT_CATEGORIES so --category is
  validated client-side on agent create/update.

Keys and setup:
- The masked "$(mnfst agent key show <slug>)" placeholder never expanded:
  key show printed JSON, and the placeholder sits in JSON/single-quoted
  contexts. Added `agent key show --raw` (bare key, still never in argv) and
  replaced the placeholder with a literal ref naming that command.
- agent create --if-absent now also returns setup, rendered from the stored
  platform, so the idempotent path is not poorer than the create path.

OAuth device flow:
- The computed poll interval was clamped to 2s, violating Kiro's 5s cadence,
  and per-poll pollIntervalMs was ignored. Sleep the server-suggested value
  (1s floor, no cap) and honor a mid-flow slow_down; tests drive it through
  OAUTH_POLL.deviceIntervalOverrideMs.

Hygiene:
- Telemetry drains the response body so an unread socket cannot delay exit,
  and both USAGE and the CLI README now disclose the persistent anonymous
  install id, the exact payload fields, the opt-out, and the endpoint override.
- USAGE matches the real flag sets (no --yes on provider enable, --key-label
  on agent configure, --agent on provider custom, --enabled only on set).
- AgentKeyAuthGuard clears its static caches in onModuleDestroy.
- Corrected the custom-provider probe hint's anthropic models path.
CodeQL flags /^-+|-+$/ as js/polynomial-redos (high). The trim is now a
linear index scan with identical output.
Three empty-context sims (CrewAI crew, day-zero install, OpenClaw
onboarding) exposed lines that misled: hollow connections
(cached_model_count: 0) read as usable, wrong-host 401s read as auth
failures, agent-env collisions in multi-agent .envs, and the
single-provider constraint on configure fallback chains.
…dation parity

Four gaps the CLI left to guesswork:

- `mnfst doctor` runs config → host → auth → providers → agents in
  dependency order and prints one JSON verdict. Ordering is the point: a
  wrong MANIFEST_URL and a wrong key produce identical errors from any
  single command, so a live host that rejects the credential is reported
  as a wrong-install/wrong-key problem instead of "run mnfst login" —
  which fixes nothing when the credential came from the environment. A
  check whose prerequisite failed is marked skipped, not failed.

- `mnfst provider refresh [<provider>]` re-runs model discovery
  tenant-wide, then reads the connections back so the caller sees the
  new per-connection model counts. It is the fix for a connection that
  is active with zero cached models, which routing cannot use.

- `agent configure` and `routing custom create` now share one
  discovered-model gate (model-check.ts), so the two ways of writing a
  route cannot disagree. Custom-tier creation validates before the tier
  is created, so a typo leaves nothing behind. `--force` skips the check
  on both — the backend still routes uncatalogued models through
  provider-qualified passthrough, and the CLI must not be what makes a
  brand-new model unusable. The error names both remedies.

- `mnfst model prices [--provider <p>]` reports install-wide pricing with
  no agent required, opening a `model *` namespace for tenant-wide model
  readouts (`models <agent>` keeps the per-agent question).
… and model prices

The wrong-host 401 row told the reader to curl /api/v1/health by hand;
`mnfst doctor` answers the same question and separates a dead host from a
key that is wrong for this host. The two hollow-connection rows now name
`mnfst provider refresh` as the fix instead of "reconnect and hope", and
the model-validation row covers `routing custom create` and `--force` as
well. Pricing before any agent exists no longer needs a probe agent —
`mnfst model prices` is install-wide.
…ning it

The operating guide for this CLI lived only in the repo, so the agents that
most need it — the ones driving `mnfst` from a coding session — never saw it.

- A gen step embeds .claude/skills/mnfst-cli/SKILL.md into
  src/skill-content.gen.ts (committed, prettier-stable, same contract as
  provider-catalog.gen.ts). CI's drift gate now diffs both gen files after
  rebuilding; a drift spec fails locally before that.

- `mnfst skill show` prints the markdown raw — the second command that is not
  JSON, after `agent env`, because the output is meant to be read or piped,
  not parsed. `mnfst skill install` writes <dir>/mnfst-cli/SKILL.md and is
  idempotent (updated:false on identical content), with --agents-dir and
  --project for the other two conventions.

- Runtime detection (agent-runtime.ts) reads an explicit marker table —
  CLAUDECODE, CURSOR_TRACE_ID, CODEX_SANDBOX/CODEX_HOME. No process sniffing
  and no heuristics: the only thing detection drives is an unsolicited stderr
  line, so a wrong guess is worse than no guess. Adding a runtime is one line.

  It feeds three things: `skill install` defaults to the detected runtime's
  own skills directory (installing where the running agent does not look
  would be theatre), telemetry carries a coarse `agent_runtime` id only when
  detected (key omitted otherwise, so schema_version stays 1), and a
  once-per-runtime stderr hint fires after a successful command when the
  guide is missing. The hint is state-tracked in
  ~/.config/manifest/skill-nudge.json, never runs on failure or on the skill
  commands themselves, and swallows every error — a nudge that throws would
  turn a working command into a broken one.

- doctor gains an informational `skill` check that reports where the guide
  was found, or not_found; it never fails the run.
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.

2 participants