Skip to main content

oceanctl — Ocean CLI

터미널에서 Ocean 에 작업을 제출하고 상태를 확인한다. AI 에이전트가 쓰는 것을 상한으로 설계했다.

라이선스: MIT(이 디렉터리 한정) · 배포: PyPI oceanctl

★4.0 — 머신타입이 사라졌다 (3.x 에서 올라온다면 여기부터)

Ocean 의 할당량이 머신타입 개수에서 자원 풀(CPU 코어 · 메모리 GiB · GPU 장수) 로 바뀌었다. 고를 프리셋이 없으므로 사양을 직접 적는다.

3.x 4.0
oceanctl machine-type list 없어졌다 — 고를 프리셋이 없다. 남은 자원은 oceanctl quota
--machine-type 48 --cpu 16 --memory 64 [--gpu 2:NVIDIA-RTX-A6000]
node list --machine-type 48 --for job node list --cpu 16 --memory 64 --gpu 2:NVIDIA-RTX-A6000 --for job
quota 의 QUOTA/FREE(개수) 풀 3축(CPU 코어 · 메모리 GiB · GPU 장수)
-f spec.json 의 "machineTypeId": 48 "resources": {"cpus": 16, "memory": 64, "gpus": 2, "gpuType": "NVIDIA-RTX-A6000"} — 구 스펙 파일은 조용히 무시되지 않고 spec_unknown_field 로 거부된다
★instance list --json 요약의 machineType ★spec = {cpus, memoryGib, gpus, gpuType}(파드에서 읽은 실제 사양)
★--details 대상 다섯 목록 ★넷(machine-type list 소멸)

★뒤 두 줄이 에이전트에게는 더 아픈 파기다 — 사람이 보는 표가 아니라 --json 계약이 바뀐다.

새 규칙 둘:

  • --cpu·--memory 는 정수만 받는다(서버와 같은 조건 — 소수는 조용히 올림되지 않고 거부된다). errorCode invalid_resource_flag.
  • --gpu 는 N:TYPE 이고 종류가 필수다(--gpu 1 은 거부 — errorCode invalid_gpu_flag). 종류를 모르면 어느 노드로 갈지 모른 채 뜨고 사용량이 엉뚱한 버킷에 잡힌다. 종류는 quota 의 GPU 행에 있다.

★아직 총량으로 전환되지 않은 조직은 quota 가 예전 머신타입 표를 그대로 보여준다. 다만 그 표의 id 를 넣을 플래그가 4.0 에는 없다 — 그런 조직은 웹을 쓴다. ★3.x 로 되돌아가는 것도 답이 아니다: 4.0 이 GET /api/machinetypes 와 프리셋 노드 후보 라우트를 CLI 토큰 allowlist 에서 뺐으므로 3.x 의 machine-type list·node list 는 서버에서 403 이다(다른 명령은 정상).

설치

최근 Debian/Ubuntu 는 시스템 Python 에 직접 설치하는 것을 막는다(PEP 668, externally-managed-environment). CLI 애플리케이션이므로 격리 설치가 맞다.

권장 — uv

uv tool install oceanctl     # pipx 와 같은 격리 설치, 더 빠르다
oceanctl --version           # PATH 에 자동 등록된다

uvx oceanctl whoami          # 설치 없이 일회성 실행도 된다

★uv 는 파이썬이 없는 머신에서도 파이썬을 알아서 받아 깐다 — "파이썬부터 깔아야 하나" 가 없는 유일한 경로라 권장 기본이다. 버전 고정은 uv tool install oceanctl==4.1.1.

pipx 도 그대로 된다

sudo apt install pipx        # 또는 python3 -m pip install --user pipx
pipx install oceanctl

레포 체크아웃에서 개발판을 깔려면 pipx install /path/to/ocean-all/ocean-cli (uv 는 uv tool install /path/to/ocean-all/ocean-cli).

단일 파일 — .pyz (kubectl 식)

릴리스마다 실행 가능한 zipapp 하나가 첨부된다. 런타임 의존성이 0 이라 가능한 형태다 — 파이썬 3.10+ 만 있으면 어떤 설치 도구도 없이 돈다:

gh release download oceanctl-v4.1.1 -p oceanctl.pyz -R AI-Ocean/ocean-all
chmod +x oceanctl.pyz && ./oceanctl.pyz --version    # 또는 python3 oceanctl.pyz
  • ★저장소가 private 인 동안 릴리스 자산도 private 다 — 레포 접근 권한이 있는 사람만 받을 수 있다(외부 사용자는 PyPI 경로를 쓴다). 저장소가 공개되면 curl -LO https://github.com/AI-Ocean/ocean-all/releases/download/oceanctl-v<버전>/oceanctl.pyz 가 된다.
  • ★모노레포라 releases/latest 는 다른 컴포넌트(agent·SM) 릴리스를 가리킬 수 있다 — 태그를 명시한 URL 만 쓴다.

pipx 가 없으면 — venv

python3 -m venv ~/.venvs/oceanctl
~/.venvs/oceanctl/bin/pip install /path/to/ocean-all/ocean-cli

# PATH 에 올리려면 (하나만 골라서)
ln -s ~/.venvs/oceanctl/bin/oceanctl ~/.local/bin/oceanctl
# 또는
echo 'alias oceanctl="$HOME/.venvs/oceanctl/bin/oceanctl"' >> ~/.bashrc

pip install --break-system-packages 는 쓰지 않는다 — 시스템 Python 을 깨뜨릴 수 있고, 이 도구는 격리해도 잃을 게 없다(의존성이 0이다).

설정

토큰은 웹에서 발급한다 — 사이드바 CLI 토큰 → 이름 입력 → 발급. 평문은 그때 한 번만 보인다(서버는 해시만 저장한다).

oceanctl configure --api-url https://api.aiocean.click
#   CLI token: ← 붙여넣기 (화면에 찍히지 않는다)

#   ✓ 저장됨   ~/.ocean/credentials (0600)
#   ✓ 확인     오션어드민 <lee@example.com> · 고려대 / korea
#                                            └ 어느 조직/클러스터에 붙었는지 그 자리에서 보인다

oceanctl configure --show
#   API URL  https://api.aiocean.click
#   토큰     ocn_3f9a7c21…  (전체는 표시하지 않음)
#   파일     ~/.ocean/credentials

비대화형(파이프)에서는 stdin 한 줄로 토큰을 받는다(printf '%s' "$TOKEN" | oceanctl configure --json). 토큰이 비어 있으면 token_required 봉투 + exit 1 이고(1.2.2+ — 그전엔 stderr 로만 죽었다), 서버 검증 실패는 invalid_cli_token — 어느 쪽도 파일을 만들거나 덮지 않는다.

여러 조직·클러스터를 오갈 때 — OCEAN_CONFIG_DIR

토큰이 조직 + 클러스터를 고정하므로(위 참조) 대상을 바꾸려면 토큰을 바꾼다. --cluster 플래그 대신 자격증명 파일의 위치를 환경변수로 가른다:

export OCEAN_CONFIG_DIR=~/.ocean/korea    # 이 디렉터리의 credentials 를 쓴다
oceanctl configure --api-url https://api.aiocean.click
oceanctl whoami                            # → 고려대 / korea

OCEAN_CONFIG_DIR=~/.ocean/other oceanctl quota   # 한 번만 다른 대상으로

생략하면 ~/.ocean/credentials 다. 프로파일 이름으로 전환하는 기능은 아직 없다 — 계약을 넓히는 방향이라 필요가 증명되면 붙일 수 있다.

인스턴스 안에서 쓸 때

Ocean 이 띄우는 이미지에는 oceanctl 이 들어 있지 않다. 안에서 쓰려면 위 .pyz 하나를 받으면 된다(런타임 의존성 0 · 파이썬 3.10+ 만 필요):

curl -sSL -o /usr/local/bin/oceanctl \
  https://github.com/AI-Ocean/ocean-all/releases/download/oceanctl-v4.1.1/oceanctl.pyz
chmod +x /usr/local/bin/oceanctl

★토큰은 인스턴스 안으로 복사해 넣어야 한다 — 파드에 자동 주입되지 않는다. 그 토큰은 만료가 없으므로(웹에서 폐기할 때까지 유효) 공유 인스턴스에 두는 것은 자격증명을 그 인스턴스에 두는 것과 같다는 점을 감안한다.

  • 자격증명은 ~/.ocean/credentials 에 0600 으로 저장된다.
  • ★토큰을 환경변수나 플래그로 주지 않는다. env 는 에이전트 프로세스에 상속돼 env 출력·로그·에러 리포트로 새고, 플래그는 셸 히스토리와 ps 에 남는다. 에이전트는 oceanctl ... 을 실행할 뿐 토큰 원문을 보지 않는다.
  • ★토큰은 조직 + 클러스터에 묶인다. 발급할 때 클러스터를 고르고, 그 토큰은 그 클러스터에서만 쓴다. 컨텍스트가 곧 자격증명이라 CLI 에 --cluster 플래그가 없다 — 대상을 바꾸려면 그 클러스터용 토큰으로 oceanctl configure 를 다시 한다(kubectl config use-context 대신 토큰 교체). 여러 토큰을 등록해 두고 이름으로 전환하는 프로파일은 뒤에 붙일 수 있다(계약을 넓히는 방향이라 안전하다).

명령

oceanctl configure [--api-url URL] [--show]   자격증명·API URL 설정 / 현재 설정 보기
oceanctl whoami                                이 토큰이 묶인 유저·조직·클러스터
oceanctl quota                                 남은 할당량(풀 3축)
oceanctl project list                          프로젝트 목록
oceanctl image list                            쓸 수 있는 컨테이너 이미지(create 에 넣을 이름)
oceanctl volume list                           마운트할 수 있는 볼륨(create 에 넣을 이름)
oceanctl volume usage NAME [--owner USER_ID]   그 볼륨의 실사용량(org ADMIN 전용)
oceanctl node list --cpu N --memory N [--gpu N:TYPE] --for job|instance
                                               지금 쓸 수 있는 노드 후보(--node 에 넣을 이름)
oceanctl instance list                         인스턴스 목록 + 접속 정보(SSH·VSCode)
oceanctl instance create --name N --image I --cpu N --memory N [--gpu N:TYPE] --volume V[:PATH[:ro]]
                                               인스턴스 생성 (`-f spec.json` 도 가능)
oceanctl instance delete --pod-uid UID --yes   인스턴스 삭제(본인 것만 · 1.2.0+ — 아래 참조)
oceanctl instance why --pod-uid UID            그 인스턴스가 왜 대기 중인가 (4.1.0+)
oceanctl job submit --name N --image I --cpu N --memory N [--gpu N:TYPE] --volume V[:PATH[:ro]] --command CMD
                                               잡 제출 (+ [--repeat N] [--project ID] [--node NAME] [--purpose TEXT])
oceanctl job submit -f spec.json               잡 제출 (스펙 파일 · `-f -` 는 stdin)
oceanctl job list                              잡 목록 · 상태 · uid
oceanctl job wait --job-uid UID [--timeout 초] [--interval 초]
                                               잡 종결 대기(성공 0 · 실패/초과 1 · 기본 600초/10초)
oceanctl job logs --job-uid U [--pod-uid U]     잡 로그(raw 스트림 — 아래 참조)
oceanctl job why --pod-uid U                   그 파드가 왜 대기 중인가

★instance delete 는 1.2.0 에서 열렸다(D4 2026-07-31 개정 — Running 인스턴스는 할당량을 계속 점유하는데 정리가 웹뿐이면 에이전트 루프가 닫히지 않아서다). 본인 것만 지워진다 (남의 podUid 는 404). --pod-uid 는 instance create --json 요약 또는 instance list --json 의 podUid 그 값이다. 확인 게이트: 비대화형/--json 은 --yes 필수(없으면 confirmation_required 봉투), TTY 는 y/N(취소 = delete_cancelled · exit 1 · 요청 0건).

★instance why 는 4.1.0 에서 열렸다 — 인스턴스가 Pending 이면 그때까지 이유를 알 길이 없어 에이전트가 무한 대기에 빠졌다(잡에는 job why 라는 출구가 있었다). 출력은 job why 와 같다: 사유(= category, 번역 없는 snake_case 5종 — insufficient_gpu·selector_mismatch· node_unavailable·insufficient_cpu_mem·other)와 설명. 파드가 대기 중이 아니면 --json 이 {"ok": true, "data": null} 이고 exit 는 0 이다 — 에러가 아니라 «이미 스케줄됐다»는 뜻이므로 폴링 루프가 여기서 정상 종료하면 된다.

★instance logs 는 없다 — 인스턴스는 instance list 가 주는 SSH·VSCode 로 접속해서 보는 대상이고, 로그 스트림은 «제출하고 결과를 회수하는» 잡 축에 붙는 도구다. 잡 축과 비대칭인 것이 의도다.

★stop 은 없고, connect/ssh 래퍼도 없다(D5) — instance list 가 sshCommand 를 그대로 보여주고 거기서 멈춘다. 감싸면 포트포워딩·키·에이전트 전달까지 떠안게 되고, 그건 ssh 클라이언트가 이미 하는 일이다. job delete·cancel 도 여전히 없다(D4 는 잡에 그대로) — 끝난 잡은 할당량을 먹지 않고 60일 뒤 자동으로 사라지므로 반복 실험이 그대로 돈다.

옵션(붙는 명령은 괄호 안에):

옵션 설명
--json 모든 명령 — {ok, data} / {ok, error} 봉투(에이전트용). exit code 는 0/1. ★job logs 는 예외 — 아래
--details --json 을 응답 원형 전체로(목록 넷: job list·volume list — 2.0.0 · instance list·image list — 3.0.0. 기본은 요약. --json 전용 — 없이 주면 details_requires_json)
--project <id> 대상 프로젝트(quota·job submit·instance create — 생략 = 기본 프로젝트)
--limit <N> 가져올 개수(instance list·project list·volume list·job list — 생략 시 서버 기본 50, 상한 200)
--node <name> 이 노드에 올린다(job submit·instance create — 생략 = 스케줄러 선택)
--cpu N · --memory N 사양(정수만 — 코어 · GiB). job submit·instance create·node list 에서 필수
--gpu N:TYPE GPU 장수와 종류(선택 — 예: 1:NVIDIA-RTX-A6000). ★종류는 서버 계약상 필수다: 안 주면 어느 노드로 갈지 모른 채 뜨고 사용량이 엉뚱한 버킷에 잡힌다. 종류는 quota 의 GPU 행에 있다
--purpose <text> 용도 메모(job submit·instance create — 선택). 잡 목록의 annotations.purpose 로 되비친다

organizationId·clusterId 는 주지 않는다 — 토큰이 담고 서버 미들웨어가 채운다. 토큰이 고정한 것과 다른 값을 굳이 보내면 403 이다(조용히 덮지 않는다).

★CLI 토큰은 개인 작업용이다(owner 2026-07-29). 목록은 자기 자원만 보여준다 — 조직 관리자라 해도 CLI 로는 조직 전체를 볼 수 없다(showAll 을 보내면 403 cli_token_show_all_forbidden). 웹 관리자 화면은 그대로 조직 전체를 본다. 능력이 아니라 자격증명의 수명 때문이다: 웹 세션은 24시간 쿠키인데 이 토큰은 만료가 없고 파일에 있어, 유출 시 반경을 그 사람 것으로 묶어 두는 편이 안전하다.

★단서 하나(P6 실측, [[OD-113]]): job logs·job why·instance why 는 showAll 이 아니라 조직 역할로 범위가 정해진다 — 조직 관리자 토큰은 uid 를 이미 알고 있는 남의 잡 로그· 대기 사유를 읽을 수 있다. 웹 관리자와 같은 범위이고, uid 를 얻는 CLI 경로(job list· volume list·instance list)는 전부 자기 것만 주므로 목록으로 훑을 수는 없다.

★cluster list 는 없다(2026-07-28 제거). 토큰이 클러스터를 고정하므로 목록을 봐도 CLI 로 할 수 있는 일이 없다 — "지금 어느 클러스터에 붙어 있나" 는 whoami 가 답한다. 부수로 그 라우트(GET /api/organization/kubernetes)가 토큰 수용 목록에서 빠졌다: 클러스터 API 주소· prometheus 주소·조직 구성원 전원 명단을 실어 보내는데 CLI 는 id·name·status 만 쓰고 있었다.

출력

목록은 헤더가 있는 표다. 값이 없으면 빈칸이 아니라 - 로 채운다.

$ oceanctl whoami
오션어드민 <lee@example.com>
조직      고려대 (3)
클러스터  korea (11)
토큰      p5-dev · ocn_ab12cd34
API       https://api.aiocean.click

$ oceanctl quota
범위  legacy-5-3 (기본) · 총량 모드

인스턴스
AXIS    USED  TOTAL   FREE
CPU        1     33     32  코어
MEMORY   4Gi  132Gi  128Gi
GPU        -      -      -  부여 없음

잡
AXIS                  USED  TOTAL   FREE
CPU                      0     32     32  코어
MEMORY                 0Gi  128Gi  128Gi
GPU NVIDIA-RTX-A6000     0      4      4  장

지금 GPU 부여가 없어 GPU 워크로드는 인스턴스로 만들 수 없습니다.
$ oceanctl instance create --name devbox --image busybox:1.36 --cpu 16 --memory 64 --volume data
✓ 생성 요청됨  devbox  (Pending)
  접속 정보(SSH·VSCode)는 `oceanctl instance list` 에서 확인하세요(파드가 Running 이 되면 접속됩니다).

$ oceanctl instance list
NAME    STATUS   SPEC                                        NODE    SSH                          VSCODE
devbox  Running  CPU 16 / RAM 64Gi / GPU 2 NVIDIA-RTX-A6000  gpu-01  ssh ocean@10.0.0.5 -p 30022  http://10.0.0.5:30080
notebk  Running  CPU 1 / RAM 4Gi / GPU 없음                  gpu-01  ssh ocean@10.0.0.5 -p 30023  http://10.0.0.5:30081

총 2개

★목록은 조용히 잘리지 않는다. 서버 기본 limit 이 50 이므로 인스턴스·프로젝트가 그보다 많으면 표 뒤에 그 사실이 뜬다:

★총 120개 중 50개만 표시됐다 — `--limit 120` 로 전부 본다

total(잘리기 전 총계)은 서버가 원래 주고 있던 값이다 — CLI 가 그걸 버려서 51번째부터 있는데 없는 것처럼 보이고 있었다. 상한(200)을 넘으면 안 되는 값을 권하지 않고 웹으로 안내한다.

$ oceanctl image list
NAME                                            TYPE     SIZE
aicoean/custom:v1                               PRIVATE  0.04Gi
myaiocean/pytorch:2.1.2-cuda12.1-cudnn8-runtime  PUBLIC  12.35Gi
  • NAME 이 곧 instance create --image 에 넣을 값이다.
  • TYPE — PUBLIC 은 모두가 쓰는 것, PRIVATE 은 이 조직이 올린 것. 남의 조직 PRIVATE 은 서버가 애초에 안 준다.
  • 크기 단위는 Gi 다 — 백엔드가 레지스트리 bytes 를 1024³ 으로 나눠 저장하므로 실제로는 GiB 다(웹 화면은 "GB" 로 찍는데 그건 표기가 틀린 것이다).
  • ★이미지 등록·삭제 명령은 없다(볼륨과 같은 논리). 목록만 있는 이유는 --image 에 넣을 이름을 CLI 안에서 알 수 있어야 하기 때문이다.
  • 이 목록은 페이지네이션이 없어 --limit 이 없다.
  • --json 기본은 요약이다(3.0.0, [[OD-149]]): {name, imageType, size} — 표와 같은 재료를 서버 순서 그대로(이름 정렬은 사람 표에만). id·organizationId·userId·createdAt 이 필요하면 --json --details(원형).
$ oceanctl volume list
NAME       CAPACITY  STATUS  NODE    SHARED  WRITE  IN USE
workspace  100Gi     Bound   -       -       -           1
datasets   2Ti       Bound   -       yes     ro          0
scratch    1Ti       Bound   gpu-03  -       -           0

총 3개
  • ★NODE 는 노드 로컬 볼륨이 묶인 노드다(아니면 -). 그 볼륨을 쓰는 워크로드는 그 노드에서만 뜨므로, 모르고 다른 노드에 --node 로 핀하면 영영 pending 이다. 후보는 node list 의 LOCAL 열과 그 아래 각주가 같은 사실을 반대편에서 보여준다.

  • NAME 이 곧 instance create --volume 에 넣을 값이다. 백엔드는 claim 이름·표시명·라벨· PV 이름을 전부 별칭으로 받지만(volumes/mount.py), 사람이 웹에서 보는 것과 같은 값이 이것이다.

  • IN USE = 그 볼륨을 마운트한 인스턴스 + 잡 개수. 서버는 객체를 통째로 주지만 표에는 개수만 낸다(원문은 --json --details).

  • WRITE 는 공유 볼륨에만 의미가 있다. ro 인 공유 볼륨을 rw 로 마운트하려 하면 422 로 거부된다.

  • ★볼륨 생성·삭제 명령은 없다(D4 와 같은 논리). 목록만 있는 이유는 --volume 에 넣을 이름을 CLI 안에서 알 수 있어야 하기 때문이다 — 웹을 봐야 아는 상태면 에이전트는 쓸 수 없다.

  • ★CAPACITY 는 할당량이지 사용량이 아니다. 실제로 얼마나 썼는지는 oceanctl volume usage NAME 이 답한다([[OD-112]]) — 할당/실사용을 함께 보여주고, 서버가 백그라운드로 계산 중이면 "계산 중" 이라고 말한다(잠시 후 재실행). ★현재 이 명령은 org ADMIN 전용이다(서버 라우트 게이트) — 일반 사용자 개방은 owner=self 강제 설계가 필요한 별건으로 남아 있다. 같은 이름의 볼륨이 여럿이면 --owner USER_ID 로 좁힌다.

  • ★--volume 은 필수다. 빼면 PVC 가 하나도 안 붙은 파드가 떠서 재시작·evict·수명 만료에 작업물이 사라진다. 웹은 그 상태를 아예 만들 수 없다(볼륨 0개면 생성 패널이 안 열린다) — 백엔드가 빈 목록을 받아줄 뿐 제품 계약에는 없는 상태다. 여러 개면 반복한다: --volume a --volume b.

  • 마운트 경로와 모드를 줄 수 있다 — --volume NAME[:PATH[:ro]](도커 -v 관례):

    --volume data                      # /volume/data (서버가 정한다 — 지금까지와 같다)
    --volume data:/volume/mydata       # 경로 지정
    --volume datasets:/volume/ds:ro    # 경로 + 읽기전용
    --volume datasets:ro               # 경로는 서버 기본, 읽기전용만 지정
    
    • 필드는 최대 셋이고 경로에 : 를 쓸 수 없다. 두 번째 필드가 / 로 시작하면 경로, 아니면 모드다. 모르는 모드는 거부한다(:readonly·:r 등) — 조용히 경로로 삼으면 요청한 읽기전용이 사라진다. 대소문자는 가리지 않는다(:RO = :ro).

    • 명시 :rw 는 아무것도 보내지 않는다(서버 기본값과 같다) — 서버가 기본을 바꾸면 CLI 가 그걸 덮어쓰지 않게 하기 위해서다.

    • 경로는 /volume/… 아래 또는 /home/ocean·/home/linuxbrew/.linuxbrew 만 된다. 예약 경로(/root/dataset·/dev/shm)·루트·상위 이동(..)·중복은 서버가 400 (invalid_volume_mount)으로 막는다([[OD-143]] 실측 정정 — 읽기전용 강등 거부만 422 다). CLI 는 규칙을 따라 검사하지 않는다(두 곳이 어긋나면 더 나쁘다) — 서버가 진실이다.

    • ★읽기전용 공유 볼륨(volume list 의 WRITE=ro)은 :ro 를 줘야 붙는다. 안 주면 서버가 422 로 거부한다(조용히 읽기전용으로 낮추지 않는다). 그전에는 CLI 로 아예 못 붙였다.

  • ★생성 응답에는 접속 정보가 없다. POST /api/instances 는 만들어진 k8s 파드·서비스 객체를 주는데, sshCommand·vscodeAddress 는 조회 시점에 계산되는 값이다 — 그래서 create 는 다음 행동을 알려주고 멈추고, 접속 정보는 instance list 가 답한다. 주소 자체는 바로 나온다(재료인 nodePort 가 Service 에서 오고 Service 는 생성 시점에 생긴다). 실제로 붙는 것은 파드가 Running 이 된 뒤다.

  • instance list 는 17개 응답 필드 중 6개만 표에 넣는다(podUid·image·limits·volumes 등은 폭 때문에 뺐다 — 뺀 이유는 oceanctl/cli.py 의 표에 적혀 있다). --json 기본도 같은 축의 요약 8키다(3.0.0, [[OD-148]] · ★4.0 에서 machineType → spec 으로 바뀌었다): {name, podUid, status, node, spec, sshCommand, vscodeAddress, vscodePrivateAddress} — spec 은 {cpus, memoryGib, gpus, gpuType} 이고 파드에서 읽은 실제 사양이다(못 읽으면 null). 자유 입력 워크로드에는 머신타입 라벨이 없어 예전 키가 영원히 null 이었다 — podUid 는 instance delete --pod-uid 에 넣을 값이고 (표에는 없어 이 요약이 유일한 출처), vscode 두 필드는 노출 모드에 따라 한쪽만 값이 있다 (NodePort ↔ Istio — 표의 VSCODE 칸이 or 로 잇는 것과 같은 사실이라 병합값을 발명하지 않고 둘 다 싣는다). 원형 17필드(annotations 이메일·image·limits 등)는 --json --details.

  • SSH 칸이 - 면 그 클러스터가 SSH 를 NodePort 로 노출하지 않는 것이다. 가장 흔한 원인은 Istio 모드(그때는 VSCODE 칸에 게이트웨이 URL 이 온다)지만 유일하지는 않다 — 클러스터 노출 주소(cluster.address)가 비어 있어도 같은 결과다. 즉 - 는 "이 경로로는 못 붙는다" 이지 "Istio 다" 가 아니다.

  • ★SPEC 은 파드에서 읽는다(4.0) — 머신타입 이름이 아니라 파드 limits + labels.accelerator 에서 조립한 실제 사양이다. 자유 입력 워크로드에는 머신타입 라벨이 없어 예전 열이 비어 있었고, 부수로 admin 이 머신타입 사양을 나중에 고쳐도 이미 뜬 파드의 표시가 소급해 바뀌지 않는다 (파드 spec 은 만들어질 때 값이다). 못 읽으면 -(0 으로 접지 않는다 — 거짓 사양은 빈칸보다 나쁘다).

  • 메모리 단위는 Gi 다 — 백엔드가 파드 스펙에 그대로 넣는 값이다({memory}Gi).

  • ★부여는 «풀 3축»이다(4.0) — CPU 코어 · 메모리 GiB · GPU 종류별 장수. FREE = TOTAL - USED 그뿐이다: CLI 는 "이 사양으로 몇 개 만들 수 있나" 를 계산하지 않는다(축이 서로 독립이라 그 곱셈은 참이 아니고, 그 계산이 3.x 에서 실제로 오독을 만들었다).

  • GPU 는 종류마다 한 행이다. 부여가 없으면 행을 지우지 않고 - - - 부여 없음 으로 남긴다 — 「0인가, 안 보여준 건가」를 구분할 수 있어야 한다.

  • 표 아래 한 줄이 붙을 수 있다("지금 GPU 부여가 없어…" · "… 소진 — 잔여 0"). 해당 없으면 아무 문장도 안 낸다. ★그 문장은 resourceQuota 에서 만든다 — --json 에 실려 오는 blockedAxes 를 쓰지 않는다(그건 머신타입 1개 기준 판정이라 풀 이야기로 옮기면 거짓이 된다).

  • ★아직 총량으로 전환되지 않은 조직은 예전 머신타입 표가 그대로 나온다(바이트 무변경). 전환은 워크로드별이라 «인스턴스는 축 표, 잡은 머신타입 표» 인 중간 상태도 정상이다.

  • 만료·회수 직후에는 USED > TOTAL 이 될 수 있다 — 그때 FREE 는 음수로 나오고 문장도 같은 값을 말한다(표와 문장이 다른 숫자를 말하지 않는다).

  • ★quota 의 기본 범위는 "기본 프로젝트" 다 — 전체 합산이 아니다. 제출(instance create· job submit)이 projectId 를 생략하면 서버가 기본 프로젝트로 넣으므로, quota 가 보여주는 숫자가 곧 그 제출을 막을 숫자여야 한다. 두 기본값이 어긋나 있으면 프로젝트가 여러 개일 때 quota 는 FREE 2 라고 하는데 제출은 거부되는 일이 생긴다. 다른 프로젝트를 보려면 --project <id>(id 는 oceanctl project list).

    • 프로젝트는 있는데 기본이 없으면 전체 합산으로 떨어지지 않고 에러다(default_project_not_found) — 틀린 숫자보다 낫다.
    • 프로젝트가 아예 없으면(할당량 승인 전 신규 유저) 에러가 아니라 범위 프로젝트 없음 으로 표시한다 — 프로젝트가 0개면 틀릴 숫자가 없고, 이때 죽이면 안내(project list)가 실행 불가다.

잡

제출 → 목록에서 uid 확인 → 로그. 이 셋이 한 흐름이다.

$ oceanctl job submit --name exp --image nvcr.io/nvidia/pytorch:24.01-py3 \
    --cpu 16 --memory 64 --gpu 1:NVIDIA-RTX-A6000 --volume data --command "python train.py"
✓ 제출됨  exp  (잡 1개)
  상태와 uid 는 `oceanctl job list`, 안 뜨는 이유는 `oceanctl job why` 로 봅니다.

$ oceanctl job list
NAME   STATUS   NODE    JOB UID                               POD UID
exp-0  Running  gpu-01  7d0a1c9e-3f2b-4f7a-9c11-8e5a1b2c3d4e  1b9f8e7d-6c5b-4a39-8271-0f1e2d3c4b5a

총 1개 잡 그룹

$ oceanctl job logs --job-uid 7d0a1c9e-…
2026-07-29T01:00:11.123Z Epoch 1/10 loss=2.31
2026-07-29T01:00:39.884Z Epoch 2/10 loss=1.87

$ oceanctl job why --pod-uid 1b9f8e7d-…
사유  insufficient_gpu
설명  0/3 nodes are available: 3 Insufficient nvidia.com/gpu.
  • ★job submit --json 은 축약 봉투다(2.0.0, [[OD-144]] — instance create 와 같은 논리): {"name", "jobUids", "status"} 셋뿐이고 서버 응답(JobsResponse) 원문을 싣지 않는다. 원문에는 command 원문(자격증명이 실릴 수 있는 필드)과 이메일이 들어 있어 에이전트 transcript 에 영구 기록되기 때문이다. jobUids 는 --repeat N 이면 N 개(→ job wait·job logs 의 --job-uid). podUid 는 없다 — 파드는 제출 직후 비동기라 job list 가 답한다(status 가 대개 null 인 것도 같은 이유 — 키는 항상 있고 없으면 null).
  • --volume 은 여기서도 필수다. 잡은 인스턴스보다 더 아프다 — 끝나면 파드가 사라지므로 결과물을 PVC 에 안 썼으면 회수할 방법이 없다(웹은 볼륨 0개인 잡을 아예 만들 수 없다).
  • --repeat 은 기본 1 이다(웹 생성 폼과 같은 기본값). N 을 주면 같은 잡이 N개 제출되고 할당량을 N개 먹는다 — oceanctl quota 의 FREE 를 먼저 보라.
  • --project <id> 로 프로젝트를 고른다(생략 = 기본 프로젝트, 지금까지와 같다). id 는 oceanctl project list. quota --project 와 같은 범위를 가리키므로, 제출 전에 본 숫자가 곧 그 제출을 막을 숫자다.
  • --node <name> 으로 노드를 지정한다(생략 = 스케줄러가 고른다). 후보는 oceanctl node list. ★핀은 honored 다 — 그 노드에 자리가 없으면 거부가 아니라 pending 이고(--repeat 은 모든 반복이 같은 노드를 겨냥한다), 이유는 oceanctl job why 가 답한다.
  • ★없는 노드 이름은 서버가 막는다(404 node_not_found, [[OD-117]]). 예전에는 그대로 받아 파드가 영원히 pending 이었고 그동안 할당량을 먹었다 — 웹은 목록에서 고르게 해 그 실수가 불가능했지만 CLI 는 자유 입력이라 열려 있었다. 지금은 아무것도 만들지 않고 거부한다.
  • 노드가 있는데 자리가 없으면 그건 pending 이 맞다(honored 핀의 설계) — oceanctl job why 가 insufficient_gpu·selector_mismatch 로 답한다.
$ oceanctl node list --cpu 16 --memory 64 --gpu 2:NVIDIA-RTX-A6000 --for job
범위  CPU 16 / RAM 64Gi / GPU 2 NVIDIA-RTX-A6000 · job

NODE       FREE GPU  FITS  LOCAL  OWNER GROUP  SHARING    MINE
gpusystem         2  yes       -  물리학과     PUBLIC     -
ds02              0  no        2  내 랩        EXCLUSIVE  yes

ds02 의 내 노드 로컬 볼륨: scratch, imagenet-2024
  • ★LOCAL 은 그 노드에 있는 내 노드 로컬 볼륨 개수다(없으면 -). 이름은 표 아래에 전부 적힌다. 그 볼륨을 마운트하려면 이 노드에 핀해야 하므로(--node), 모르고 다른 노드를 고르면 영영 pending 이다.

  • ★이름을 칸에 넣지 않는 이유: 이어 붙이면 표 폭이 볼륨 개수·이름 길이에 종속되고, 표는 모든 행을 가장 넓은 칸에 맞추므로 노드 하나가 표 전체를 밀어낸다(실측: 볼륨 3개면 97칸이라 80칸 터미널에서 접힌다). 각주는 접혀도 읽히지만 표는 접히면 무너진다.

  • 각주 순서는 표와 같다 — 표에서 개수를 보고 각주에서 이름을 찾는 흐름이라 그래야 한다.

  • ★사양이 어느 노드에도 안 들어가면 표 대신 한 문장이 나온다 — 예: "NVIDIA-RTX-A6000 은 한 노드에 최대 4장입니다(요청 8장). 파드는 노드 하나 안에서만 실행됩니다." 그건 노드별이 아니라 클러스터 전체 판정이라(노드를 바꿔도 답이 같다) 목록을 보여줄 이유가 없다.

  • ★FREE GPU 는 노드별이다 — 총합이 아니라 노드별이라야 "이 노드에 핀하면 지금 뜨는가" 를 안다. 여유가 0인 노드도 숨기지 않는다(그것도 판단 재료다).

  • ★FITS 가 그 판단을 대신 해 준다. 전에는 "후보"가 GPU 종류·컴퓨팅타입이 맞는 노드를 뜻할 뿐 지금 들어갈 수 있는 노드가 아니어서, 2 GPU 를 요청했는데 여유 1인 노드도 올라왔다. 그걸 --node 로 핀하면 영영 pending 이다(2026-07-29 도그푸딩에서 실제로 그랬다). 전부 no 면 표 아래에 그렇게 적는다. 목록에서 지우지는 않는다 — --node 를 생략한 자동 배치는 여전히 가능하다. ★그 안내는 세 갈래다([[OD-166]]): 전부 기다려도 안 되는 부족이면 "어느 노드에도 배치되지 않습니다", 전부 여유 부족이면 종전대로 "기다리면 뜹니다", 섞여 있으면 둘 다 적는다 (몇 개가 규모 미달이고 나머지는 여유 부족인지). 그 성질은 서버가 permanent 로 말해 준다 — CLI 가 축 이름으로 짐작하지 않는다.

  • ★판정은 서버가 한다(fits). CLI 는 읽어서 그릴 뿐이다 — 잠깐 웹과 CLI 가 같은 규칙을 각각 갖고 있었는데, 서버 필터가 바뀌면 둘 다 따로 어긋나는 모양이라 한 곳으로 모았다. 네 값이 각각 다르다:

    값 뜻
    yes 서버가 막을 이유를 못 찾았다. "확실히 뜬다"가 아니다 — 아래 참조
    no 서버가 부족을 확인했다(insufficient)
    ? 서버가 판정을 안 줬다(구버전). no 와 구분해서 읽어야 한다
  • ★4.0 에서 -(해당 없음)가 사라졌다. 프리셋 시대에는 GPU 를 안 쓰는 머신타입을 «판정 대상 아님» 으로 그렸는데, 자유 입력 판본은 서버가 노드 총량(allocatable) 으로 CPU·메모리까지 판정한다 — GPU 0 요청도 yes/no 로 답이 나온다.

  • ★단 GPU 여유 판정은 여전히 사용량 지표 기반이다(Ocean namespace 파드만 센다 — 다른 namespace 가 잡아 둔 몫은 안 보인다). 그래서 yes 를 받고도 pending 일 수 있고, 그때 이유는 oceanctl job why 가 답한다. CPU·메모리 쪽은 총량 비교라 no 면 «영영 안 됨» 이다 (permanent) — 표 아래 안내가 그 둘을 갈라 말한다.

  • --for 는 필수다. 서버가 이 값으로 후보를 거르므로(노드마다 잡용/인스턴스용 라벨이 있다) 생략하면 조용히 틀린 목록이 된다.

  • OWNER GROUP 이 내 랩이 아니어도 고를 수는 있다(소유는 advisory). SHARING 이 그 노드의 공유 정책이다 — EXCLUSIVE·SHARED_RECLAIMABLE·PUBLIC.

  • ★노드 변경 명령은 없다 — 소유·타입 지정은 ADMIN 운영 작업이다. 목록만 있는 이유는 --node 에 넣을 이름을 CLI 안에서 알 수 있어야 하기 때문이다(볼륨·이미지와 같은 논리).

  • job list 는 파드마다 한 행이다. --repeat 3 으로 만든 잡 그룹은 세 행으로 보인다 — 로그·대기사유가 podUid 를 받으므로 행 단위가 파드여야 한다. 그래서 표 뒤의 총계는 잡 그룹 수(서버가 세는 단위)라고 밝혀 둔다.

  • JOB UID·POD UID 를 그대로 복사해 job logs·job why 에 넣는다. 이름으로는 못 찾는다 — 같은 이름으로 여러 번 제출할 수 있어서 CLI 가 이름→uid 규칙을 발명해야 하기 때문이다.

  • ★job logs 는 --job-uid 하나면 된다. 잡의 파드가 하나면(오늘의 모든 잡) 서버가 그것을 고른다. --pod-uid 는 파드가 여럿일 때만 필요하고, 그때 서버가 pod_selection_required (400)로 후보 uid 를 details.podUids 에 담아 알려준다. --job-uid 를 남긴 이유는 나중에 멀티노드 잡이 와도 잡 uid 는 그대로 정체성이기 때문이다. (후보 uid 는 --json 의 error.details.podUids 에 온다 — 사람용 출력은 메시지와 코드만 낸다. oceanctl job list 의 POD UID 컬럼에서도 같은 값을 볼 수 있다.)

  • ★끝난 잡의 로그는 사라진다. 파드가 정리되면(수명 제한 초과·GC) job logs 는 "파드가 남아 있지 않다" 는 에러(job_pod_not_found)로 답한다 — 기다려도 안 오는 것을 "준비 중" 이라고 하면 에이전트가 무한히 폴링한다.

  • ★job logs 는 --json 봉투를 씌우지 않는다(설계 §5 가 정한 명시적 예외). 서버가 text/plain 스트림으로 주기 때문에, 봉투를 씌우려면 전부 모았다가 한 번에 내야 해서 스트리밍이 아니게 된다. --json 을 줘도 본문은 원문 그대로이고 다음만 달라진다:

    언제 어디로 모양
    로그 본문 stdout 원문 그대로
    스트림이 시작되기 전 에러(404·403·연결 실패) --json 이면 stdout, 아니면 stderr 평소 봉투
    스트림 도중 끊김 항상 stderr + exit 1 ✗ … 한 줄

    마지막 줄이 핵심이다 — 이미 로그가 stdout 에 흐른 뒤 봉투를 stdout 에 얹으면 둘 다 못 읽는다.

  • 로그는 현재 로그(마지막 50줄) 를 흘리고 끝난다 — tail -f 가 아니다(서버가 follow=False). 실행 중인 잡에서도 멈추지 않으므로 에이전트가 매달릴 일이 없다.

  • 파이프로 잘라 써도 된다: oceanctl job logs … | head -20 처럼 읽는 쪽이 먼저 닫으면 조용히 끝난다(exit 0 — 원하는 만큼 받고 닫은 것이지 실패가 아니다).

  • ★job why 는 null 을 줄 수 있다 — 파드가 대기 중이 아니면(이미 스케줄됐거나 끝났으면) 에러가 아니라 {"ok": true, "data": null} 이다. 에이전트는 이걸 "기다릴 이유가 없다" 로 읽는다. category 는 안정적인 값이라 분기 축으로 쓴다: insufficient_gpu·selector_mismatch· node_unavailable·insufficient_cpu_mem·other.

  • 설명 은 쿠버네티스 스케줄러가 남긴 원문이다(영문). 서버가 요약하지 않고 그대로 싣고 ("진실 은폐 금지") category 만 얹는다 — 분류에 안 걸리면 other + 원문이다.

스펙 파일로 제출하기 (-f)

repeat 를 생략하면 플래그 경로와 같은 1 이다([[OD-127]]) — "스펙=서버 body 원형" 계약의 유일한 예외.

플래그가 늘면 한 줄이 길어진다. -f 로 JSON 을 주면 된다 — 스키마는 서버가 받는 요청 본문 그대로다(CLI 전용 스키마가 없다).

★주의: --json 출력은 이 파일과 모양이 다르다(제출 출력은 {name, jobUids, status} 축약 봉투다 — [[OD-144]]). "--json 으로 나온 것을 그대로 -f 에 넣는다" 는 성립하지 않는다 — 처음 이 문서에 그렇게 적었다가 무맥락 리뷰가 잡았다. 재사용하려면 보낸 스펙 파일을 보관하면 된다.

$ cat job.json
{
  "name": "exp-42",
  "image": "myaiocean/pytorch:2.1.2-cuda12.1-cudnn8-runtime",
  "resources": {"cpus": 16, "memory": 64, "gpus": 1, "gpuType": "NVIDIA-RTX-A6000"},
  "command": "python train.py",
  "repeat": 1,
  "projectId": 7,
  "nodeName": "gpusystem",
  "purpose": "lr 3e-4 재현",
  "volumes": [
    {"volumeName": "test0521", "mountPath": "/volume/data"},
    {"volumeName": "datasets", "readOnly": true}
  ]
}

$ oceanctl job submit -f job.json
$ cat job.json | oceanctl job submit -f -        # stdin
  • -f 와 다른 플래그를 함께 쓸 수 없다(v1). 병합 규칙(무엇이 이기나)을 지금 정하면 검증하지 않은 규칙이 계약이 되기 때문이다 — 필요가 증명되면 넓힌다(넓히기는 안전하다).
  • ★모르는 필드는 CLI 가 거부한다. 백엔드는 모르는 필드를 조용히 무시하므로 "comand" 오타가 "command 누락" 422 로 둔갑하거나, 선택 필드 오타는 아무 말 없이 사라진다.
  • instance create 도 같다. 단 스펙에 command·repeat 은 없다(잡 전용) — 넣으면 거부한다.
  • 허용 필드에는 위 예시 밖의 top-level volumeName(legacy 단수) 도 있다([[OD-143]] 정체 확인). 서버가 지금도 유효하게 받는 필드라("스펙 = 서버 body 원형" 계약) 목록에 남아 있다 — 의미는 volumes: [{"volumeName": …}] 한 개와 같고, volumes 와 동시에 쓰면 서버가 400 으로 거부한다. 새 스펙은 volumes 를 쓰면 된다.
  • purpose 는 자유 텍스트 메모(200자)다. 목록에 보이지만 검색은 안 된다 — 실행 조건을 적어 두는 용도다. 4.0 부터 --purpose 플래그로도 받는다(파일 전용이 아니다).
  • YAML 은 지원하지 않는다 — 이 CLI 는 런타임 의존성 0이 계약이고 YAML 은 새 의존성이 필요하다.

에이전트가 쓸 때

oceanctl quota --json
# 총량 모드(4.0): resourceQuota·totalQuotaMode 가 함께 온다 — 사람용 표가 읽는 것도 이 둘이다
# {"ok": true, "data": {"instances": [...], "jobs": [...],
#                       "totalQuotaMode": true,
#                       "resourceQuota": {"instance": {"cpuCores": {"total": 33.0, "used": 1.0},
#                                                      "memoryGib": {...}, "gpus": []}, "job": {...}}}}

oceanctl project list --json
# {"ok": false, "error": {"errorCode": "invalid_cli_token", "message": "..."}}   # exit 1

★job list --json 의 기본은 평면 요약이다(2.0.0, [[OD-145]]). 원형 전체가 기본이던 1.x 는 역사 잡 8그룹 기준 실측 18.9KB 로 에이전트 컨텍스트를 먹었고, 상태가 3단 깊이(jobs[]. jobPodInfos[].status)에 있어 첫 폴러가 스키마를 몰라 헛돌았다([[OD-126]]). 이제 사람용 표와 같은 단위(파드마다 한 항목)의 다섯 키가 전부이고, 이 모양이 계약이다:

{"ok": true, "data": {"items": [
  {"name": "exp-0",                    // 잡 이름(그룹 아님 — repeat 이 -0·-1 을 만든다)
   "status": "Running", "node": "…",   // 제출 직후 파드가 아직 없으면 status·node·podUid = null
   "jobUid": "…",                      // ← job logs --job-uid · job wait --job-uid 에 넣는 값
   "podUid": "…"}                      // ← job why --pod-uid 에 넣는 값
], "total": 1}}                        // total = 잡 그룹 수(서버가 세는 단위 — 잘림 감지 재료)

★잘림 판정은 total vs 보낸 --limit(생략 시 서버 기본 50)으로 한다 — items 개수와 비교하지 말 것. items 는 파드 단위고 total 은 그룹 단위라, repeat 그룹이 섞이면 그룹이 잘렸는데도 len(items) >= total 이 될 수 있다(그룹 5개만 받았는데 그 안의 파드가 12개 > total 10 같은 모양). total > limit 이면 잘린 것이다 — total 이 200 이하면 --limit <total> 로 다시 부르고, 200 을 넘으면 상한이 200 이라(넘겨 보내면 검증 400) CLI 로는 거기까지만 보고 나머지는 웹에서 본다(사람용 표의 안내와 같은 규칙).

volume list --json 도 같은 방식의 요약이다(실측 11.3KB → 여섯 키: name·capacity·status· nodeName·shared·writeAllowed — --volume 인자와 :ro 판단, 그리고 노드 로컬 볼륨이면 어느 노드인지. 4.0 에서 nodeName 이 늘었다). ★원형 전체가 필요하면 --details 를 붙인다 — 2.0.0 전의 기본이던 원형(잡 = 그룹→jobs[]→jobPodInfos[] 3단 · 볼륨 = 워크로드 원문·annotations 포함)이 그대로 나온다. 서버가 원형의 원천인 것은 그대로고, CLI 의 기본 표현만 요약이다(설계 §5 개정).

종결만 기다릴 거면 이 스키마도 파싱할 필요가 없다 — job wait 가 폴링을 안으로 접는다 ([[OD-128]]): oceanctl job wait --job-uid UID --json → 성공 종결 exit 0 · 실패/시간초과 exit 1.

whoami --json 의 모양은 계약이므로 여기 박아 둔다 — 바꾸면 에이전트 스크립트가 깨진다:

{"ok": true, "data": {
  "user":         {"id": 5, "name": "오션어드민"},
  "email":        "lee@example.com",
  "organization": {"id": 3,  "name": "고려대"},
  "cluster":      {"id": 11, "name": "korea"},
  "token":        {"id": 7, "name": "p5-dev", "tokenPrefix": "ocn_ab12cd34",
                   "createdAt": "2026-07-28T00:00:00Z"},
  "apiUrl":       "https://api.aiocean.click"
}}

★2026-07-28 에 이 모양이 한 번 바뀌었다(name·email 이 최상위 → user.name·email). whoami 가 GET /api/users/me 대신 토큰 검증 라우트를 쓰게 되면서다. 사용자가 owner 뿐이라 그때는 무해했지만, 다음 변경은 계약 변경이다.

★--json 봉투의 기본값은 다음 행동에 필요한 것이다(2.0.0·3.0.0, 설계 §5 개정): 제출 응답은 축약 봉투([[OD-133]]·[[OD-144]]), 목록 넷(job list·volume list — 2.0.0 · instance list· image list — 3.0.0, [[OD-148]]·[[OD-149]])은 요약 projection — 원형은 --details 로 연다. 요약 스키마는 각 명령 절에 있고, 바깥 모양은 서버를 따른다: 페이지 봉투 라우트(instance list)는 {items, total} 그대로, 평면 리스트(image list)는 평면 그대로 — --details 로 갈아타도 바깥 모양은 안 바뀐다. 요약이 없는 나머지(node list· project list·quota)는 받은 응답을 그대로 싣는다(owner 결정을 거친 목록만 요약한다). 사람용 표의 정렬·단위·- 채움은 어느 쪽 봉투에도 들어가지 않으므로, 사람용 출력을 손봐도 에이전트 파서가 깨지지 않는다.

  • ★필수 플래그가 빠지면 exit 1 + 봉투다(missing_required_flags) — 어떤 플래그가 빠졌는지 메시지에 이름으로 들어 있다. 예전에는 argparse 사용법이 exit 2 로 나갔는데, -f 와 양자택일이 되면서 검사를 코드로 옮겼고 그 편이 "exit code 는 0/1" 계약에도 맞다. ★버전 경계: create/submit 밖의 명령(node list·instance delete 등)은 1.1.1~1.2.1 에선 이 경우 invalid_arguments 였다 — 1.2.2 부터 전 명령이 이 코드로 통일됐다 (위치인자 누락은 계속 invalid_arguments).
  • ★파싱 오류도 exit 1 + 봉투다(invalid_arguments, 1.1.1+) — 모르는 플래그(--nope), 숫자여야 하는 자리에 문자(--project abc), 없는 서브커맨드. 메시지가 oceanctl job submit: … 처럼 어느 명령의 인자가 틀렸는지를 접두로 알려 준다. (1.1.0 까지는 이 부류만 argparse 가 exit 2 + stderr 로 죽었다 — 외부 에이전트 도그푸딩이 잡은 세 번째 실패 모양이었고, 이제 실패 모양은 봉투 하나다. --help/--version 은 그대로 exit 0.) ★--json 이면 파싱 오류의 stderr 가 완전히 빈다(2.0.0, [[OD-146]]) — 1.x 는 usage 를 stderr 로도 찍어서, 스트림을 2>&1 로 합쳐 읽는 에이전트에게 JSON 앞에 usage 가 끼는 잡음이 있었다. 사람 모드의 usage 안내는 그대로다.
  • ★configure 의 토큰 필요도 봉투다(token_required, 1.2.2+) — 비대화형 프로비저닝이 stdin 을 비워 보낸 경우. 서버 검증 실패(invalid_cli_token)와 함께 어느 쪽도 파일을 만들거나 덮지 않는다.
  • ★파괴적 명령의 확인 게이트도 봉투다(1.2.0+): instance delete 를 --yes 없이 --json/비대화형으로 부르면 confirmation_required(요청은 서버에 안 나감 — --yes 붙여 재시도), TTY 에서 취소하면 delete_cancelled(역시 요청 0건 · exit 1 — 아무것도 안 지웠는데 0 이면 스크립트가 성공으로 읽기 때문).
  • 성공/실패는 exit code 0/1 로 먼저 갈린다. 세부 분기는 error.errorCode 로 한다 (서버가 정의한 안정적인 snake_case 코드 — cluster_id_required·invalid_cli_token 등).
  • --json 은 성공·실패 모두 stdout 하나로 나간다.
  • CLI 는 재시도하지 않는다. 재시도 여부는 호출자가 errorCode 를 보고 판단한다 (예: cluster_tunnel_disconnected 는 잠시 뒤 다시, permission_forbidden 은 재시도 무의미).

개발

python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
.venv/bin/pytest
.venv/bin/ruff check .

CI 게이트는 .github/workflows/python-services.yml(Tier 0 — ruff hard · pytest hard).

Metadata

Release files for oceanctl 4.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for oceanctl 4.1.1
File Size Uploaded
oceanctl-4.1.1.tar.gz 114.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for oceanctl 4.1.1
File Interpreter ABI Platform
oceanctl-4.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 194.8 kB

Release files / oceanctl-4.1.1.tar.gz

Download URL oceanctl-4.1.1.tar.gz
Size 114.7 kB
Tags Source
SHA-256 checksum
How to use checksums
ccc087ee1272ee3c056c2423e45ab0a86e4bbb2dbc53c9dac3673d3c2d95fcfd
BLAKE2b-256 checksum
How to use checksums
d250c59584158b046ebb2a95d3a4dee7c0ec34f0008f026300404ac5ef4f0898
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 18, 2026.

Transparency log

Release files / oceanctl-4.1.1-py3-none-any.whl

Download URL oceanctl-4.1.1-py3-none-any.whl
Size 80.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
605f3552b47274dfd7363f0cd056fe1a071c1790e038bf450c69e6473eb38247
BLAKE2b-256 checksum
How to use checksums
8f3b738a331acbe6769febb9b294b02c6805c3dc986dd4ac485d31425ed6a4aa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 18, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

4.1.1 This release

2 release files

4.1.0

2 release files

4.0.1

2 release files

4.0.0

2 release files

3.0.1

2 release files

3.0.0

2 release files

2.0.1

2 release files

2.0.0

2 release files

1.2.2

2 release files

1.2.1

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page