feat(cli): fix runHTTP auth bug + add management verbs #13

Merged
devops-engineer merged 4 commits from feat/management-cli-verbs into main 2026-06-01 09:47:20 +00:00
Member

Summary

Extends molecule-cli from a runtime-bridge into a management CLI, and fixes the auth bug first.

1. Auth bug fix (the priority)

internal/cmd/http.go runHTTP — and the internal/client Platform HTTP helpers, which had the same gap — sent no Authorization header, so management calls (workspace create/delete, secrets, tokens, …) 401'd a hardened tenant. Now every request attaches:

  • Authorization: Bearer $MOLECULE_API_KEY (the Org API Key — tenant-admin)
  • X-Molecule-Org-Id: $MOLECULE_ORG_ID when set (the tenant TenantGuard routing gate)

Headers are omitted when the env vars are unset, so fresh self-host/dev tenants keep working. Regression test TestRunHTTP_SetsAuthHeader asserts the header is set, and is proven load-bearing: reverting the fix makes it fail with Authorization header = "". A second test (TestClientAuthHeaders + the per-verb table) proves auth flows on every client method.

2. Management verbs (PLATFORM-MANAGEMENT-API.md §5(b))

Each wired to the documented endpoint at the correct auth tier:

Group Verbs
org list get create --slug/--name create --template export token {list,create,revoke} allowlist
workspace list get create delete restart pause resume budget billing-mode token mint
secret ws {list,set,delete} · org {list,set,delete}
template list import refresh
bundle export import
top-level events · approvals

Org-lifecycle verbs target the control plane (MOLECULE_CP_URL, default = --api-url); tenant verbs target the tenant host with the Org API Key. All honor --json (plus existing -o table|json|yaml).

OpenAPI / SSOT alignment

The brief pointed at a parallel feat/openapi-management-spec branch in molecule-corethat branch does not exist (verified via Gitea API; only the /schedules swagger stub is on main). Per SOP Phase 1 this load-bearing claim is falsified, so request/response shapes were reconciled against the actual SSOT: the live workspace-server/internal/router/router.go route table + handler structs, and the controlplane orgs.go / models.Organization. Concretely: budget = PATCH /workspaces/:id/budget {budget_limits: {hourly|daily|weekly|monthly: cents}}; billing-mode = PUT /admin/workspaces/:id/llm-billing-mode {mode}; org-from-template = POST /org/import {dir, mode}; secrets = {key,value}; template import = {name, files}; org create (CP) = POST /api/v1/orgs {slug, name}.

Tests / verification (Phase 4)

  • go build ./..., go vet ./..., go test ./... — all green
  • gofmt clean on edited files only (no wildcard)
  • Table-driven request-construction tests (method / path / body / auth) for all 30 management client methods against an httptest mock
  • cmd-layer branch tests: budget flag→limits mapping, billing-mode arg validation, template file-mapping parse/read errors, --json resolution, CP-url fallback, auth-helper env reads
  • Auth regression proven by revert (watch-it-fail)
  • Binary smoke-tested end-to-end: Authorization: Bearer … + X-Molecule-Org-Id reach the server; org list table and --json both render

Tier: tier:medium (touches auth). Do not self-merge — needs a non-author approval per SOP.

🤖 Generated with Claude Code

## Summary Extends `molecule-cli` from a runtime-bridge into a management CLI, and **fixes the auth bug first**. ### 1. Auth bug fix (the priority) `internal/cmd/http.go` `runHTTP` — and the `internal/client` `Platform` HTTP helpers, which had the **same gap** — sent **no `Authorization` header**, so management calls (workspace create/delete, secrets, tokens, …) 401'd a hardened tenant. Now every request attaches: - `Authorization: Bearer $MOLECULE_API_KEY` (the Org API Key — tenant-admin) - `X-Molecule-Org-Id: $MOLECULE_ORG_ID` when set (the tenant `TenantGuard` routing gate) Headers are omitted when the env vars are unset, so fresh self-host/dev tenants keep working. **Regression test** `TestRunHTTP_SetsAuthHeader` asserts the header is set, and is proven load-bearing: reverting the fix makes it fail with `Authorization header = ""`. A second test (`TestClientAuthHeaders` + the per-verb table) proves auth flows on every client method. ### 2. Management verbs (PLATFORM-MANAGEMENT-API.md §5(b)) Each wired to the documented endpoint at the correct auth tier: | Group | Verbs | |---|---| | `org` | `list` `get` `create --slug/--name` `create --template` `export` `token {list,create,revoke}` `allowlist` | | `workspace` | `list` `get` `create` `delete` `restart` `pause` `resume` `budget` `billing-mode` `token mint` | | `secret` | `ws {list,set,delete}` · `org {list,set,delete}` | | `template` | `list` `import` `refresh` | | `bundle` | `export` `import` | | top-level | `events` · `approvals` | Org-lifecycle verbs target the **control plane** (`MOLECULE_CP_URL`, default = `--api-url`); tenant verbs target the tenant host with the Org API Key. All honor `--json` (plus existing `-o table|json|yaml`). ### OpenAPI / SSOT alignment The brief pointed at a parallel `feat/openapi-management-spec` branch in `molecule-core` — **that branch does not exist** (verified via Gitea API; only the `/schedules` swagger stub is on `main`). Per SOP Phase 1 this load-bearing claim is falsified, so request/response shapes were reconciled against the **actual SSOT**: the live `workspace-server/internal/router/router.go` route table + handler structs, and the controlplane `orgs.go` / `models.Organization`. Concretely: budget = `PATCH /workspaces/:id/budget {budget_limits: {hourly|daily|weekly|monthly: cents}}`; billing-mode = `PUT /admin/workspaces/:id/llm-billing-mode {mode}`; org-from-template = `POST /org/import {dir, mode}`; secrets = `{key,value}`; template import = `{name, files}`; org create (CP) = `POST /api/v1/orgs {slug, name}`. ## Tests / verification (Phase 4) - `go build ./...`, `go vet ./...`, `go test ./...` — all green - `gofmt` clean on edited files only (no wildcard) - Table-driven request-construction tests (method / path / body / auth) for **all 30 management client methods** against an `httptest` mock - cmd-layer branch tests: budget flag→limits mapping, billing-mode arg validation, template file-mapping parse/read errors, `--json` resolution, CP-url fallback, auth-helper env reads - Auth regression proven by revert (watch-it-fail) - Binary smoke-tested end-to-end: `Authorization: Bearer …` + `X-Molecule-Org-Id` reach the server; `org list` table and `--json` both render **Tier:** `tier:medium` (touches auth). Do **not** self-merge — needs a non-author approval per SOP. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
sdk-dev added 1 commit 2026-06-01 03:53:37 +00:00
feat(cli): fix runHTTP auth bug + add management verbs
Release Go binaries / release (pull_request) Blocked by required conditions
CI / Test / test (pull_request) Successful in 1m51s
Release Go binaries / test (pull_request) Successful in 1m11s
a002b412e9
Fix the auth bug FIRST: internal/cmd/http.go runHTTP (and the
internal/client Platform HTTP helpers, which had the same gap) sent NO
Authorization header, so management calls (workspace create/delete,
secrets, tokens, …) 401'd a hardened tenant. Now every request attaches
`Authorization: Bearer $MOLECULE_API_KEY` and, when set,
`X-Molecule-Org-Id: $MOLECULE_ORG_ID` (the tenant TenantGuard routing
gate). Headers are omitted when the env vars are unset so fresh
self-host/dev tenants keep working. Regression test
TestRunHTTP_SetsAuthHeader asserts the header is set and is proven
load-bearing (fails with `Authorization header = ""` when the fix is
reverted).

Add the management verbs (PLATFORM-MANAGEMENT-API.md §5(b)), each wired
to the OpenAPI-documented endpoint at the correct auth tier (verified
against the live workspace-server router.go + handlers and controlplane
orgs handler, since the parallel feat/openapi-management-spec branch
does not exist in molecule-core — reconciled to the actual handler
source instead):

  org   list|get|create --slug/--name|create --template|export
        token list|create|revoke | allowlist
  workspace list|get|create|delete|restart|pause|resume
            budget|billing-mode|token mint
  secret  ws  list|set|delete
          org list|set|delete
  template list|import|refresh
  bundle  export|import
  events
  approvals

Org-lifecycle verbs target the control plane (MOLECULE_CP_URL, default
= api-url); tenant verbs target the tenant host with the Org API Key.
All verbs honor --json (and existing -o table|json|yaml). Request/
response shapes match the handler structs (budget USD-cents
budget_limits; billing-mode {mode}; org import {dir,mode}; secrets
{key,value}; template import {name,files}; etc.).

Tests: table-driven request-construction tests (method/path/body/auth)
for all 30 management methods against an httptest mock, plus
cmd-layer branch tests (budget flag→limits, billing-mode validation,
template file mapping, --json resolution, CP-url fallback). Existing
workspace/agent/platform commands switched to the authenticated client.

go build ./..., go vet ./..., go test ./... all green; gofmt clean on
edited files. Binary smoke-tested end-to-end: auth headers reach the
server and --json output renders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sdk-dev added the tier:medium label 2026-06-01 03:54:01 +00:00
sdk-dev added 1 commit 2026-06-01 05:35:35 +00:00
fix(cli): repoint org create/list to CP-admin bearer; fail-fast get/export
CI / Test / test (pull_request) Waiting to run
Release Go binaries / release (pull_request) Blocked by required conditions
Release Go binaries / test (pull_request) Waiting to run
442d1ebaf4
Review fix for #13. The CP org verbs targeted /api/v1/orgs*, which is gated by
RequireSession() (WorkOS cookie-only) — a bearer-token CLI can't authenticate
and these 401 in prod; the tenant Org API Key has no standing on the CP at all.

- org create/list now target the CP ADMIN routes (POST/GET /api/v1/admin/orgs,
  AdminGate bearer), authenticated with a DISTINCT credential MOLECULE_CP_ADMIN_TOKEN
  (never the tenant MOLECULE_API_KEY). create now requires --owner-user-id, per
  controlplane adminCreateOrgRequest{slug,name,owner_user_id}. ListOrgs decodes
  the {limit,offset,orgs[]} admin-summary envelope. Two-credential split is
  documented in `org`/`org create` help text; the org key is never sent to the CP.
- org get/export have NO AdminGate-reachable route on the CP (session-only), so
  they fail fast with a clear "session-only, use the dashboard" error instead of
  shipping verbs that 401.
- cpAdminClient() fails fast with guidance when MOLECULE_CP_ADMIN_TOKEN is unset
  (wrong-credential path), rather than silently sending the org key to the CP.
- Wire Execute() through handleErr so SilenceErrors'd exitError messages actually
  print (they were previously swallowed by main's bare os.Exit(1)) — required for
  the fail-fast guidance to reach the user.
- Optional cleanup: extract resolveBillingMode()/budgetLimitsFromFlags() so prod
  and tests share one definition.
- Tests: client + cmd assert org verbs hit /api/v1/admin/orgs with the CP-admin
  bearer (no org-id header, no org-key leak), the missing-owner and
  missing-admin-token fail-fast paths, get/export fail-fast, and an e2e CLI test
  that `org list` without the admin token exits non-zero naming MOLECULE_CP_ADMIN_TOKEN.

Budget shape (budget_limits) left unchanged — confirmed correct; the OpenAPI
spec is the stale one (fixed separately on #2056).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sdk-dev added 1 commit 2026-06-01 06:45:01 +00:00
fix(cli): address CR2 review on #13 — path-escaping, config binding, CP-admin targeting
CI / Test / test (pull_request) Successful in 2m3s
Release Go binaries / test (pull_request) Successful in 2m9s
Release Go binaries / release (pull_request) Waiting to run
0ec3db81e6
CR2 review findings on PR #13 (branch feat/management-cli-verbs):

1. [HIGH] PathEscape user-controlled path segments. platform.go built paths
   via fmt.Sprintf on raw caller IDs (GetWorkspace/DeleteWorkspace/
   RestartWorkspace/ListWorkspaceAgents/GetAgent/GetPeers/GetDelegations) and
   the agent-send / workspace-delegate runHTTP call sites concatenated raw IDs.
   An ID with '/', '?' or '#' could alter the endpoint or leak into the query.
   Wrapped every caller-supplied segment in url.PathEscape (management.go
   already did this). DeleteWorkspace's ?confirm=true is now injection-safe.
   Severity note: this runs under the user's own management creds, so it is
   primarily robustness/correctness rather than a privilege-escalation hole.

2. [MED] Config not bound to globals. viper read the config file but the
   flag-backed apiURL/outputFormat globals were never populated from it, so
   `molecule config set api_url` did not affect newClient()/cpURL(). Added
   applyConfigDefaults(): config file is adopted only when no env override and
   the global is still at its built-in default, so precedence stays
   flag > env > config file > default.

3. [MED] MintWorkspaceToken sent a nil body → JSON `null`. Now sends an empty
   object (struct{}{}) → `{}`, matching sibling tooling and avoiding rejection
   by a handler that decodes into a struct/map.

4. [MED] cpURL defaulted to apiURL (tenant host), so an unset MOLECULE_CP_URL
   would send the privileged CP-admin bearer to a tenant host. cpURL() no
   longer falls back to apiURL; cpAdminClient() now requires an explicit
   MOLECULE_CP_URL and fails fast otherwise. Updated org.go help text.

5. [LOW] config set now os.MkdirAll's the config dir before WriteConfig/
   SafeWriteConfig, which otherwise fail on a fresh machine where ~/.config
   doesn't exist yet.

Tests: added path-segment escaping coverage (platform + delete), MintWorkspaceToken
body={}, applyConfigDefaults precedence, config-set mkdir, and CP-admin credential
targeting; retargeted TestCPURLFallback → TestCPURLNoTenantFallback.
go build/vet/test all green; gofmt clean on edited files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sdk-dev added 1 commit 2026-06-01 08:26:56 +00:00
docs(client): fix misleading auth-tier package comment (CR cleanup #13)
CI / Test / test (pull_request) Successful in 1m12s
Release Go binaries / test (pull_request) Successful in 1m41s
Release Go binaries / release (pull_request) Has been skipped
e878dca78b
The internal/client package comment said CP org calls "use the same
key/CP-admin bearer", which implied the tenant Org API Key could be
sent to the control plane. The code (cpAdminClient) uses a DISTINCT
CP-admin bearer (MOLECULE_CP_ADMIN_TOKEN) and never sends the tenant
key to the CP; align the comment to match the detailed doc-comment in
the orgs section and the implementation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
hongming-ceo-delegated approved these changes 2026-06-01 09:47:18 +00:00
hongming-ceo-delegated left a comment
Member

Approved per comprehensive pre-merge review at HEAD e878dca — build/vet/test green, cpURL no-fallback, PathEscape, CP-admin routing verified vs controlplane router. CTO-authorized.

Approved per comprehensive pre-merge review at HEAD e878dca — build/vet/test green, cpURL no-fallback, PathEscape, CP-admin routing verified vs controlplane router. CTO-authorized.
devops-engineer approved these changes 2026-06-01 09:47:19 +00:00
devops-engineer left a comment
Member

Second approval under CTO authorization.

Second approval under CTO authorization.
devops-engineer merged commit 99fa0157ba into main 2026-06-01 09:47:20 +00:00
Sign in to join this conversation.
3 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: molecule-ai/molecule-cli#13