Metadata-Version: 2.4
Name: opencosmo-portal
Version: 1.0.2
Summary: CLI and local MCP server for the OpenCosmo platform
Requires-Python: >=3.11
Requires-Dist: click>=8.1.0
Requires-Dist: httpx>=0.28.1
Requires-Dist: joserfc>=1.6.1
Requires-Dist: jsonschema>=4.26.0
Requires-Dist: mcp[cli]>=1.26.0
Requires-Dist: packaging>=25.0
Requires-Dist: pydantic>=2.0
Description-Content-Type: text/markdown

# OpenCosmo Portal CLI (`ocp`)

Command-line interface and local MCP server for the [OpenCosmo](../README.md) platform. Query cosmological simulation datasets at DOE Leadership Computing Facilities from your terminal.

## Installation

```bash
pip install opencosmo-portal
```

Or for development:

```bash
cd cli
uv sync
```

## Quick Start

The CLI ships with these profiles:

| Profile | API URL | Notes |
| ------- | ------- | ----- |
| `production` | `https://cosmoexplorer.alcf.anl.gov` | Default |
| `staging` | `https://opencosmo-test.cels.anl.gov` | Internal network only |
| `dev` | `http://localhost:8000` | Backend running locally without Docker |

Shipped profiles cannot be removed, but their URLs can be overwritten with
`ocp config add-profile`. When the CLI is upgraded, known former official URLs
automatically migrate to the latest shipped URL; user-overridden URLs remain
unchanged. Custom profiles can be added and removed normally.

To use production:

```bash
# Authenticate (Globus OAuth device code flow; --browser for redirect flow)
ocp auth login

# Browse available tasks
ocp task list
ocp task info <slug>

# Submit a task and monitor the run
ocp task run <slug>
ocp run status <run-id> --watch
ocp run results <run-id>
```

For development against a local backend, either pass `--profile dev` to each
command or make it the current profile with `ocp config set-profile dev`.

### Update Notifications

After an interactive command, the CLI checks PyPI at most once every 24 hours
and prints a package-neutral notice to stderr when a newer stable release is
available. The complete network check has a one-second wall-clock deadline.
Failures are silent, non-interactive and MCP sessions skip the check, and no
update is installed automatically. Set
`OPENCOSMO_DISABLE_UPDATE_CHECK=1` to disable the check. Cached update state is
stored in `~/.opencosmo/update-check.json`.

## Commands

### `ocp auth` — Authentication

| Command             | Description                                                        |
| ------------------- | ------------------------------------------------------------------ |
| `ocp auth login`    | Authenticate via Globus OAuth device code flow (default); works in SSH/headless sessions. `--browser` opens a local browser redirect flow instead (`--port` sets the callback port, default 8080) |
| `ocp auth logout`   | Clear stored tokens                                                |
| `ocp auth status`   | Show token status and expiry                                       |
| `ocp auth consent`  | Grant authorization for tasks that need it (e.g. to run computations on your behalf) |

### `ocp config` — Profile Management

| Command                                | Description              |
| -------------------------------------- | ------------------------ |
| `ocp config list`                      | List all profiles        |
| `ocp config add-profile <name> <url>`  | Add or update a profile  |
| `ocp config set-profile <name>`        | Set default profile      |
| `ocp config remove-profile <name>`     | Delete a custom profile  |
| `ocp config show [name]`               | Show profile details     |

### `ocp task` — Tasks

| Command                  | Description                                |
| ------------------------ | ------------------------------------------ |
| `ocp task list`          | List available tasks                       |
| `ocp task info <slug>`   | Show task details and input parameters     |
| `ocp task run <slug>`    | Submit a task (interactive or `--input`)   |

Interactive task entry keeps the task's nested JSON object intact while
presenting leaf fields in a numbered table. The `Constraints` column shows the
currently active numeric range or allowed values. Standard Draft 7 conditional
rules are recomputed after every edit. If a dependency change invalidates a
value, `ocp` first uses one valid active conditional default; otherwise it
clamps inclusive numeric bounds (or steps integers across exclusive bounds)
and repairs invalid enums with a valid ordinary default or the first allowed
value. Each automatic change is printed immediately with its field, old and
new values, and active constraint. Relaxing a rule preserves the current value
rather than restoring an earlier default.

Interactive entry also honors leaf-level `depends_on` visibility annotations
using the same parent-relative dot paths as the frontend. Hidden fields are
omitted from the parameter table and conditional repair, but their stored
values are preserved; revealing a field reactivates its constraints and repairs
an invalid value normally. Final submission still validates the complete value
object against the original schema. Explicit `--input` and `--file` payloads
are validated as supplied and do not receive visibility projection.

Exclusive floating-point bounds are not repaired with an invented epsilon;
the field remains invalid for manual correction unless a valid default exists.
Conflicting conditional defaults are reported as schema warnings, and
contradictory constraints are task-schema errors that prevent local
submission. Before leaving the editor, the complete nested input is validated
against the original Draft 7 schema; the backend repeats authoritative
validation on submission.

Non-interactive submissions (`--input`/`--file`) are validated against the
same original schema before any `--dry-run` output or submission, with no
automatic repair — scripted input fails with per-field messages instead of
being silently adjusted. `ocp task info` lists parameters by their nested
dotted paths (e.g. `filters.sod_halo_mass.minval`), matching the nested JSON
that `--input`/`--file` expect.

### `ocp run` — Runs

| Command                        | Description          |
| ------------------------------ | -------------------- |
| `ocp run list`                 | List your runs       |
| `ocp run status <id> [--watch]`| Check run status     |
| `ocp run logs <id>`            | View run logs        |
| `ocp run results <id>`         | Download results     |
| `ocp run cancel <id>`          | Cancel a run         |
| `ocp run archive <id>`         | Archive a run        |

### `ocp admin tasks install` — Adapter Installation

Install task adapter definitions into the backend database. Discovery is
explicit and non-recursive: directory mode uses the directory argument directly,
while archive modes require `--subdir`; only direct child `*.json` files in that
selected directory are loaded.

```bash
ocp admin tasks install dir ./artifact/build/tasks
ocp admin tasks install dir ./artifact/build/tasks --dry-run
ocp admin tasks install dir ./artifact/build/tasks --strict

OPENCOSMO_API_URL=https://portal.example.org \
OPENCOSMO_API_KEY="$OPENCOSMO_API_KEY" \
ocp admin tasks install dir build/tasks

ocp admin tasks install url "https://.../artifacts/download?file_type=archive" \
  --subdir build/tasks \
  --header "PRIVATE-TOKEN:${GITLAB_TOKEN}"

ocp admin tasks install gitlab \
  --host https://git.cels.anl.gov \
  --project hacc/hacc-compute-portal \
  --ref master \
  --job adapter-build \
  --subdir build/tasks \
  --gitlab-token-env GITLAB_TOKEN
```

Supported archives are `.zip`, `.tar`, `.tar.gz`, and `.tgz`. Protected URL
tokens are accepted through explicit token/header options or environment
variables and are not persisted by the CLI or backend.

For CI, provide an admin API key through `OPENCOSMO_API_KEY` and set
`OPENCOSMO_API_URL` to the target portal. The CLI automatically exchanges the
API key for a JWT, uses that JWT for backend requests, and does not persist the
API key or env-derived JWT. `OPENCOSMO_API_KEY` takes precedence over stored
profile tokens.

For non-dry-run installs, the CLI first sends a dry-run request for every
discovered task. If any prevalidation request fails, no real install requests
are sent. Once real installation starts, requests are committed one task at a
time.

Each response may include structured warnings when a task's `category` does not
exactly match a registered category slug. Without `--strict`, warnings are
displayed and installation continues. `--strict` is available for `dir`, `url`,
and `gitlab`; it gathers all dry-run responses first and exits 1 without real
task requests if any warning is present. If a warning first appears during the
real install requests (for example, the category registry changed after
prevalidation), the command still exits 1, but the tasks have already been
installed; installs are idempotent, so re-running after fixing the registry is
safe. JSON output remains valid on either nonzero exit.

### `ocp admin tasks categories sync` — Category Registry

Category files are complete desired-state JSON objects containing a
`categories` array. Sync always sends a server-side dry run first, displays the
create/update/unchanged/delete actions, and confirms before the real `PUT`
unless `--yes` is supplied. Confirmation defaults to no when deletions exist.

```bash
ocp admin tasks categories sync build/categories.json --dry-run
ocp admin tasks categories sync build/categories.json --yes
ocp admin tasks install dir build/tasks --strict
```

Run category sync before strict adapter installation. `slug` is the stable
identity used by each task's existing `category` field. Names and headings can
change without task reinstall; a slug change temporarily renders the old task
value as a fallback group until adapters are reinstalled.

### `ocp admin datasets sync` — Dataset Registry

Dataset files are complete desired-state JSON objects containing `vocabulary`
and `datasets` arrays (the exporter's `build/datasets.json`). Sync always
sends a server-side dry run first, displays the create/update/unchanged/delete
actions plus vocabulary changes and loose-reference warnings, and confirms
before the real `PUT` unless `--yes` is supplied. Confirmation defaults to no
when deletions exist.

```bash
ocp admin datasets sync build/datasets.json --dry-run
ocp admin datasets sync build/datasets.json --yes
```

CI ordering is categories → datasets → tasks: run this command after
`ocp admin tasks categories sync` and before `ocp admin tasks install`. Task
install validates binding tags against the synced vocabulary, so binding-task
installs fail on a fresh instance until `datasets sync` has run. Deleting a
dataset that installed tasks still match is allowed (task references are
deliberately loose) but produces a warning listing the affected task slugs.

### `ocp admin docs deploy` — Documentation Deployment

Deploy a complete Markdown documentation tree into the backend database.
Production docs are auto-deployed from the external documentation repository
(internal: `git.cels.anl.gov/hacc/hacc-compute-portal`). This command remains
the deployment interface for automation, recovery, and manual testing.

For `dir`, pass the docs root itself — the directory that directly contains the
root `index.md` (e.g. `content/documentation` inside a docs checkout or
extracted artifact), not the checkout or artifact root.

```bash
ocp admin docs deploy dir /path/to/docs-checkout/content/documentation --dry-run
ocp admin docs deploy dir /path/to/docs-checkout/content/documentation

ocp admin docs deploy url "https://.../artifacts/download?file_type=archive" \
  --subdir content/documentation \
  --header "PRIVATE-TOKEN:${GITLAB_TOKEN}"

ocp admin docs deploy gitlab \
  --host https://git.cels.anl.gov \
  --project hacc/hacc-compute-portal \
  --ref master \
  --job docs-build \
  --subdir content/documentation \
  --gitlab-token-env GITLAB_TOKEN
```

Only Markdown files at the selected docs root and one subdirectory level are
loaded. Non-Markdown files are ignored; deeper Markdown files fail validation.
Real deploys perform one backend dry-run prevalidation call, print the planned
actions, and ask for confirmation before the real deploy call (the prompt
defaults to no whenever pages would be removed). Pass `--yes`/`-y` to skip the
confirmation in CI pipelines. The backend requires root `index.md` and deletes
DB docs omitted from the submitted tree.

### `ocp admin service-accounts` — Service Accounts

Manage API-backed service accounts. All commands require admin permissions.

```bash
ocp --profile production admin service-accounts list
ocp --profile production admin service-accounts create ci-task-manager \
  --profile task-manager \
  --description "CI task adapter manager"
ocp --profile production admin service-accounts update ci-task-manager --profile task-manager
ocp --profile production admin service-accounts rotate-key ci-task-manager
ocp --profile production admin service-accounts revoke-key ci-task-manager KEY_ID --yes
ocp --profile production admin service-accounts disable ci-task-manager --yes
```

Create and rotate print the plaintext API key exactly once. Enabled service
accounts are automatically valid via `urn:opencosmo:service-account`; no
service-account entry is needed in `AUTH_VALID_GROUPS`. For `task-manager`, add
the printed profile-specific URN to backend admin authorization:

```bash
AUTH_ADMINS="urn:globus:groups:id:<admin-group>;urn:opencosmo:service-account:task-manager"
```

OpenCosmo JWTs store group URNs without an issuer field. Group provenance is
enforced before JWT minting through provider-owned namespace validation.
Identity providers declare the group URN prefixes they are allowed to mint;
Globus declares `urn:globus:groups:id:*`. Service accounts get the exact common
valid group `urn:opencosmo:service-account` at runtime and profile role groups
must use the `urn:opencosmo:service-account:` prefix. Callback, session,
auth-code, refresh-token, and device-code paths enforce validated effective
group authorization before accepting or minting tokens.

### `ocp whoami` — User

Show current user info.

### `ocp mcp` — MCP Server

Start a local stdio MCP server for AI assistants like Claude Desktop.

```bash
ocp mcp start
```

Configure in Claude Desktop (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "opencosmo": {
      "command": "ocp",
      "args": ["mcp", "start"]
    }
  }
}
```

## Global Options

| Option             | Description                                |
| ------------------ | ------------------------------------------ |
| `--profile, -p`    | Use a specific profile                     |
| `--format, -f`     | Output format: `table` (default) or `json` |
| `--version`        | Show version                               |
| `--help`           | Show help for any command                  |

## Releasing

The CLI is published to PyPI as `opencosmo-portal` via GitHub Actions (trusted publisher / OIDC).

### Automated (recommended)

Use the **Release CLI** workflow in GitHub Actions:

1. Go to **Actions → Release CLI → Run workflow**
2. Select the bump type (`patch`, `minor`, or `major`)
3. The workflow bumps `cli/pyproject.toml`, commits, tags, and pushes — which
   triggers the publish workflow automatically

### Manual

1. Bump `version` in `cli/pyproject.toml` (or run `cd cli && uv version --bump patch`)
2. Commit: `git commit -am "release: CLI v0.2.0"`
3. Tag: `git tag cli-v0.2.0`
4. Push both: `git push origin main cli-v0.2.0`

The [`publish-cli.yml`](../.github/workflows/publish-cli.yml) workflow will:
- Run the full test suite
- Verify the tag version matches `pyproject.toml`
- Build and publish to PyPI

### First-time setup

Register a [trusted publisher](https://docs.pypi.org/trusted-publishers/) on pypi.org:

| Field        | Value                  |
| ------------ | ---------------------- |
| Package name | `opencosmo-portal`     |
| Owner        | `ArgonneCPAC`          |
| Repository   | `OpenCosmoPortal`      |
| Workflow     | `publish-cli.yml`      |
| Environment  | `pypi`                 |

Then create a `pypi` environment in GitHub repo settings → Environments.

## Configuration

Config and tokens are stored in `~/.opencosmo/`:

```
~/.opencosmo/
├── config.json          # Profiles (name → API URL)
└── tokens/
    └── <profile>.json   # OAuth tokens per profile
```
