Skip to content

DAH-2589: lium describe — one pod manifest an agent can act on - #101

Merged
arhangel66 merged 3 commits into
mainfrom
DAH-2589-pod-describe
Aug 6, 2026
Merged

DAH-2589: lium describe — one pod manifest an agent can act on#101
arhangel66 merged 3 commits into
mainfrom
DAH-2589-pod-describe

Conversation

@arhangel66

Copy link
Copy Markdown
Collaborator

Step A of DAH-2589 (machine layer, sub-task of DAH-1942). An agent that rents a pod has to piece together what it got from ps, the dashboard and guesswork. lium describe <pod> answers it in one document.

lium describe eager-wolf-aa          # human-readable table
lium describe eager-wolf-aa --json   # one manifest, ready for jq

What the manifest carries

pod (id/huid/name/status/uptime), gpu (type, count, model and driver from the executor specs, max CUDA), machine (executor id, ip, location, tier, DinD), ports, access (ssh command, jupyter), template (name + docker image), storage (volume encryption), billing (price/h, spent so far, scheduled removal).

The ports section is the reason this command exists. The API returns ports_mapping keyed by the port inside the container, and getting that direction wrong is the most common way an agent burns time on a pod. So the manifest states the direction explicitly, singles out the external port that reaches SSH, and lists the remaining ports a service can be published on:

"ports": {
  "mapping": {"22": 34567, "8000": 34568},
  "direction": "internal -> external",
  "ssh_external": 34567,
  "service_ports": [{"internal": 8000, "external": 34568}]
}

Decisions worth reviewing

API only, no SSH. The manifest is assembled from data the backend already returns, so it also answers for a pod that has stopped responding. Live in-pod state (free disk, is nvcc present, who is listening) is deliberately out of scope — that is step C of the task, a lium-capabilities command running inside the pod against this same schema.

--json, not --format json. It follows the DAH-2556 contract: handle_errors keys the JSON error envelope off a json_output kwarg, so --json gets a machine-readable failure and a meaningful exit code for free. An unknown pod exits EXIT_POD_NOT_FOUND (5) with {"ok": false, "error": {...}} on stderr and clean stdout. ls and ps still use --format table|json and do not get that envelope — unifying the two spellings is its own change.

ensure_config() runs only on the human path. A --json caller is behind a pipe and cannot answer the interactive setup prompt, so a missing API key surfaces as the JSON envelope with the configuration exit code instead of hanging on a question nobody will read.

Timestamp parsing and spend rounding are imported from ps.display rather than reimplemented, so describe and ps can never disagree about what a pod has cost.

Tests

test/test_describe_cli.py — 9 tests: port direction and the SSH port, service ports excluding 22, GPU read from specs, a pod whose executor the API omitted, spend by uptime, --json parsing as-is, resolution by huid, and the not-found exit code. Full suite: 358 passed, 11 pre-existing failures unrelated to this change (provider, gpu splitting, release binary — same 11 fail on a clean main).

Follow-ups, not in this PR

  • lium/SKILL.md in the lium-skill repo needs a "working inside a pod" section pointing at describe — separate repo, separate PR.
  • The exit-code table in lium-docs should gain the command.

@arhangel66

Copy link
Copy Markdown
Collaborator Author

Review pass after a second-model read of the diff. Three changes:

SDK errors no longer borrow the pod-not-found code. The first cut classified failures by substring — if "not found" in result.error — and the SDK's own 404 text is Resource not found: .... A misrouted endpoint or a proxy 404 would have exited 5 with pod_not_found, telling an agent its pod id was wrong when the API call itself had failed. Resolution now follows the shape exec already uses (resolve_pods_or_fail): a missing pod raises CliFailure directly, and SDK exceptions travel up untouched for handle_errors to map. ActionResult is gone from this command along with the string matching.

Port keys are compared as strings, everywhere. str(internal) != "22" excluded the SSH port from service_ports while mapping.get("22") missed it under an integer key — so a mapping built in Python would report ssh_external: null and still hide port 22. Both paths now normalize. A non-numeric key such as 8000/tcp is passed through instead of raising ValueError.

Tests: 9 → 14. spent_usd is now checked as uptime × price rather than merely positive, and timestamps are relative to now instead of a hardcoded date that would fail on a machine whose clock is behind. New: integer port keys, non-numeric port keys, ports_mapping: null, the table path on a pod the API barely described, the error-envelope code and empty stdout on failure, and the api-error-is-not-pod-not-found case that would have caught the defect above.

Also added describe to the command list in README.md.

Suite: 363 passed, same 11 pre-existing failures as on a clean main.

Not taken, worth a separate change: describe imports _parse_timestamp / _spent_usd from ps.display to keep the two commands' spend arithmetic identical. Promoting them to a shared public helper (there is already a public parse_timestamp in utils.py) is a cleanup of existing code, not of this diff.

@arhangel66
arhangel66 requested a review from taiberium August 6, 2026 04:13
Comment thread test/test_describe_cli.py Outdated
monkeypatch, [], error=LiumNotFoundError("Resource not found: /pods")
)

assert result.exit_code == EXIT_GENERAL_ERROR

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: #104 remaps LiumError to exit 3, so this assert breaks once it lands. Safer to assert it is not EXIT_POD_NOT_FOUND.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right — #104 moves LiumError to exit 3. Changed to assert result.exit_code not in (0, EXIT_POD_NOT_FOUND), which pins what the test is actually about and survives either mapping.

Comment thread lium/cli/describe/display.py Outdated
return round((datetime.now(timezone.utc) - dt_created).total_seconds() / 3600, 2)


def _port_number(port) -> object:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: this returns int or str, never a bare object, and port has no type at all.

Suggested change
def _port_number(port) -> object:
def _port_number(port: str | int) -> int | str:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied your signature verbatim: def _port_number(port: str | int) -> int | str:.

Comment thread README.md
- `lium ls [GPU_TYPE]` - List available nodes
- `lium up [NODE_ID]` - Create a pod (use node ID or filters like `--gpu`, `--count`, `--country`)
- `lium ps` - List active pods
- `lium describe <POD>` - Full manifest of one pod: ports, GPU, template, billing (add `--json` for machine-readable output)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: lium-docs has no describe.md yet and no row in the CLI reference index.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and left as is on purpose. This row is the repo README CLI Reference — a plain bullet list, not a link into lium-docs, so it is accurate standalone. docs/ here is Sphinx SDK API only and has no CLI index. The docs.lium.io describe.md page plus the CLI reference row is a separate lium-docs PR.

@arhangel66
arhangel66 merged commit 80c4b14 into main Aug 6, 2026
9 checks passed
@arhangel66 arhangel66 mentioned this pull request Aug 7, 2026
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