Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ pnpm dev
**サインアップ不要:**
`arkor dev` は **Studio** と呼ばれるローカル Web UI を `http://localhost:4000` で開きます。初回起動時に使い捨ての匿名ワークスペースをプロビジョニングするので、すぐに実際のトレーニング実行を開始できます。

後からアカウントに紐付けたい場合は `arkor login --oauth` を実行してください
匿名ワークスペースはこのマシーンに紐づきます。クラウド API はサーバー側で単一端末ガードを強制しているので、`~/.arkor/credentials.json` を別端末にコピーする運用は想定外です。各匿名ワークスペースは発行されたマシーンで使う前提です。今のところ CLI は匿名トークンを自動 refresh しないので、コピーがしばらく動いてしまうことはあります。発行ユーザーがもう一度 `arkor login --anonymous` を実行する(または自動 refresh が入って保存中の jti が進む)と、そのタイミングで他の全コピーは次回呼び出しから単一端末エラーで失敗するようになります。OAuth に切り替えても既存の匿名ワークスペースは引き継げません。`arkor login --oauth` は `~/.arkor/credentials.json` を新しい OAuth identity で上書きしますが、既存の匿名ジョブや org は発行元の credentials ファイルからしか辿れません。**さらに、既存のプロジェクトディレクトリでの実行は自動的に新しい identity 側へ切り替わりません**: `.arkor/state.json` が古い `(orgSlug, projectSlug)` をピンしているため、そのファイルを削除する(その後再実行するか、OAuth アカウントなら `arkor init` を走らせる)まで、このディレクトリのコマンドは前のワークスペースを参照し続けます。複数端末で作業したくなった時点で `arkor login --oauth` を実行し、アカウント付きの新しいワークスペースで再スタートしてください

### テンプレートを選ぶ

Expand Down Expand Up @@ -85,7 +85,7 @@ Arkor はその基盤の上に立っています。
- [x] **ダッシュボードではなくコードでトレーニングに反応。** ライフサイクルコールバック (`onStarted`、`onLog`、`onCheckpoint`、`onCompleted`、`onFailed`) は、クラウドからストリーミングされる実行に応じて、完全に型付けされた状態で発火します。
- [x] **実行が終わる前にモデルを軽くチェック。** `onCheckpoint` の中から、トレーニング中のモデルに対して `infer({ messages })` を呼び出せます。
- [x] **ローカル Studio で実行を見守る。** `arkor dev` は、ジョブ一覧、ライブの loss チャート、ログテール、ファインチューニング済みモデルとチャットできる Playground を備えた UI を開きます。
- [x] **アカウントなしで試す。** `arkor dev` はそのまま新しい匿名ワークスペースで起動します。アカウントに紐付けたい場合は `arkor login --oauth` で Arkor Cloud の OAuth (PKCE) フローを開始してください
- [x] **アカウントなしで試す。** `arkor dev` はそのまま新しい匿名ワークスペースで起動します。匿名ワークスペースは単一端末専用で、発行したマシーンに紐付き、現状 OAuth アカウントへのマイグレーションパスはありません。複数端末でアカウント付きの作業を始めたいタイミングで `arkor login --oauth` を実行してください。それ以降の作業はアカウントに紐付きます

## これから来るもの

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ pnpm dev
**No signup required:**
`arkor dev` opens **Studio**, a local web UI at `http://localhost:4000`. On first launch it provisions a throwaway anonymous workspace so you can fire off a real training run right away.

Run `arkor login --oauth` later if you want to claim your work under an account.
Anonymous workspaces are tied to this machine. The cloud-api enforces a single-device guard server-side, so `~/.arkor/credentials.json` shouldn't be copied to a second machine. Each anonymous workspace is meant to live where it was issued. Today the CLI doesn't auto-refresh the anonymous token, so a copy may keep working for a while and only start failing once the issuing user runs `arkor login --anonymous` again (or auto-refresh ships and rotates the stored jti); when that happens, every other copy starts failing on its next call with a single-device error. Switching to OAuth does not migrate prior anonymous work: `arkor login --oauth` overwrites `~/.arkor/credentials.json` with a fresh OAuth identity, but existing anonymous jobs and orgs stay reachable only from the credentials file that issued them. **The CLI also won't automatically route runs in an existing project directory to the new identity**: `.arkor/state.json` pins the directory to the old `(orgSlug, projectSlug)`, so commands here will keep targeting the previous workspace until you delete that file (then re-run, or run `arkor init` for the OAuth account). Run `arkor login --oauth` whenever you're ready to start an account-backed workspace that follows you across devices.

### Pick a template

Expand Down Expand Up @@ -85,7 +85,7 @@ The phrase we keep coming back to: **ship the model the same way you ship the pr
- [x] **React to training in code, not in a dashboard.** Lifecycle callbacks (`onStarted`, `onLog`, `onCheckpoint`, `onCompleted`, `onFailed`) fire as the run streams from the cloud, fully typed.
- [x] **Sanity-check the model before the run finishes.** Inside `onCheckpoint`, call `infer({ messages })` against the model as it's being trained.
- [x] **Watch the run in a local Studio.** `arkor dev` opens a UI with a jobs list, live loss chart, log tail, and a Playground for chatting with your fine-tuned models.
- [x] **Try it without an account.** `arkor dev` boots straight into a fresh anonymous workspace. Run `arkor login --oauth` to start the Arkor Cloud OAuth (PKCE) flow and attach the work to your account.
- [x] **Try it without an account.** `arkor dev` boots straight into a fresh anonymous workspace. Anonymous workspaces are single-device: they live on the machine that issued them, and there's no migration path into an OAuth account today. Run `arkor login --oauth` whenever you want *future* work backed by an account that follows you across devices.

## What's coming next

Expand Down
21 changes: 17 additions & 4 deletions docs/cli/auth.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,14 @@ The loopback server is closed in a `finally` block so it does not stick around i

Anonymous credentials let you try Arkor without an account: training runs, jobs, and any work you do are tied to the local machine via the anonymous token. The anonymous path always mints a brand-new token (and a new `anonymousId`) and overwrites `~/.arkor/credentials.json`, so re-running `arkor login --anonymous` does not refresh the existing identity. Switching to OAuth (`arkor login --oauth`, or selecting `OAuth (browser)` in the picker) overwrites the credentials file the same way and does not migrate prior anonymous workspaces or jobs into the account. Merging anonymous work into an OAuth account once you sign in is on the roadmap; until that lands, run `arkor login --oauth` **before** you start the runs you want associated with the account.

Anonymous accounts are intentionally **single-device**: the cloud-api stores a `latest_jti` per anonymous user and `userAuth` rejects every authenticated request whose JWT carries a different jti. The jti only changes when the token rotates, so the practical timing has two regimes. **Today** the CLI does not auto-refresh anonymous tokens (that wiring lives in `@arkor/cloud-api-client`'s `getToken()` and is on the [SDK roadmap](/roadmap)), so no rotation ever happens client-side and a copy of `~/.arkor/credentials.json` on a second machine usually keeps working — until the issuing user explicitly mints a new identity (`arkor login --anonymous` overwrites the file with a fresh jti) or an admin rotates server-side, at which point every other copy starts failing on its next call. **Once auto-refresh ships**, any refresh rotates `latest_jti` immediately, so the older-jti copy on any other device (or an older backup of the same file on this machine) starts failing on its next call without warning. Either way, the losing client receives an HTTP 401 / 409 with `code: "anonymous_token_single_device"`, which `cli/main.ts` surfaces as actionable guidance; deletion of the underlying anonymous row surfaces as `code: "anonymous_account_not_found"` the same way. The recovery hint is deployment-aware: on OAuth-supporting deployments the CLI points at `arkor login --oauth` so you can sign up for an account that supports multiple devices, while on anon-only deployments (where OAuth is not configured) it points at `arkor login --anonymous` instead — `--oauth` would fail there, and minting a fresh anonymous identity is the only recovery available. Note that **neither path migrates existing anonymous work** into the new identity; the previous workspace stays reachable only from the credentials file that issued it.

### Anonymous issuance output

Both anonymous paths surface the new `anonymousId` and an explanation that the same id is how Arkor Cloud recognises this client across sessions, though the line shape differs by entry point. `arkor login` (`--anonymous` flag or picker → **Anonymous**) prints `Anonymous id: <id>` as the spinner stop and then a separate info line saying that keeping the credentials file (`credentialsPath()`, typically `~/.arkor/credentials.json` on Linux and macOS) is what preserves the identity. `arkor dev`'s auto-bootstrap skips the spinner and emits a single info line that already embeds the id and the same explanation. The picker → **Anonymous** path additionally surfaces a one-line warn alongside the success message — ``Anonymous sessions aren't guaranteed to persist — sign in with `arkor login --oauth` to tie future work to your Arkor Cloud account.`` — so the upgrade hint is visible at issuance time. The explicit `--anonymous` shortcut suppresses that warn because it skips the `/v1/auth/cli/config` fetch and so cannot tell whether `arkor login --oauth` would succeed on this deployment; pointing at it on a rare anon-only deployment would steer users at a command that fails.

Every anonymous issuance path also surfaces the single-device limitation as a separate info line. The exact wording is gated on `oauthAvailable` (same contract as the persistence nudge). On OAuth-supporting deployments callers see the upgrade-flavoured ``Note: anonymous accounts work on this machine only. Run `arkor login --oauth` to sign up for multi-device access.``, while on anon-only deployments (and on `arkor login --anonymous` where the cfg fetch is skipped intentionally) callers see only the bare ``Note: anonymous accounts work on this machine only.``, so users on those deployments aren't pointed at a command that would fail. `arkor whoami` on an anonymous identity also emits the bare variant on stderr, and only when both stdout and stderr are TTYs. A redirected stream (lost `stdout.isTTY`) or a CI runner that treats stderr-on-success as a warning marker (lost `stderr.isTTY`) sees no extra prose. The note is purely a UX hint; it doesn't gate any behaviour. The stdout shape (the user JSON object printed by `JSON.stringify(user, null, 2)`, optionally followed by a single human-readable `Orgs: <slug>, …` line when the user belongs to any orgs) is unchanged from the pre-note behaviour. Don't pipe the full output through a strict JSON parser like `jq`; the `Orgs:` line is a summary tail, not part of the JSON document.

## `arkor logout`

```bash
Expand Down Expand Up @@ -98,7 +102,8 @@ When signed in, the command prints the JSON `user` object pretty-printed, then a
- `0`: signed in, identity printed.
- `0`: not signed in; the message is informational only.
- `1`: the cloud-api returned `426 Upgrade Required`. The CLI prints the upgrade hint (and the upgrade command for your detected package manager) and sets `process.exitCode = 1` so the deprecation-warning flush in `arkor`'s shutdown hook still runs before the process exits.
- Other 4xx / 5xx are reported as `Failed to fetch /v1/me (<status>). Token may be expired.` and exit `0`.
- `1`: the cloud-api returned a structured anonymous-auth dead-end (`code: "anonymous_token_single_device"` or `code: "anonymous_account_not_found"`). `cli/main.ts` formats the deployment-aware recovery hint to stderr; see [Token expiry](#token-expiry) for the exact wording.
- `1`: any other non-2xx (transient 5xx, an unmapped 4xx, etc.). The previous "exit `0` with a `Failed to fetch /v1/me (<status>). Token may be expired.` line on stdout" behaviour was removed in this release: `arkor whoami` now raises a `CloudApiError`, and `bin.ts` renders it concisely as `err.message` (e.g. `cloud-api 503`) on stderr before exiting non-zero. Stack traces are reserved for unknown `Error` shapes (likely SDK bugs); routine HTTP failures stay one-line so they don't drown a wrapper's logs.

## Where the credentials live

Expand All @@ -110,10 +115,16 @@ For OAuth sessions, the credentials file records both the access token and the i

In practice that means:

- An expired access token shows up as a `Failed to fetch /v1/me (401). Token may be expired.` from `arkor whoami`, or analogous failures from anything that talks to the cloud-api.
- An expired or revoked token shows up as a non-2xx response from the cloud-api. `arkor whoami` no longer prints a generic "Token may be expired" hint; instead it raises a `CloudApiError` carrying the upstream `code` (when present), and the top-level handler in `cli/main.ts` formats two known auth-state codes as actionable guidance. Before formatting, `main()` does a best-effort fetch of `/v1/auth/cli/config` so the recovery hint matches the deployment shape:
- `code: "anonymous_token_single_device"` →
- on OAuth-supporting deployments: ``Anonymous credentials were rejected as single-device. Anonymous accounts only work on one machine. Sign up for an account that supports multiple devices: arkor login --oauth`` (exit `1`).
- on anon-only deployments: ``Anonymous credentials were rejected as single-device. Anonymous accounts only work on one machine. This deployment does not advertise OAuth, so the only recovery is to mint a new anonymous identity (your previous workspace data cannot be recovered): arkor login --anonymous`` (exit `1`).
- `code: "anonymous_account_not_found"` → analogous OAuth-vs-anon-only split, ending in `arkor login --oauth` or `arkor login --anonymous`.

Errors without a known `code` (and any non-`CloudApiError` exceptions) are rethrown out of `main()`. `bin.ts` wraps the top-level `await main(...)` in a try/catch that distinguishes by error shape: a `CloudApiError` is rendered as just `err.message` (e.g. the upstream `error` body, or `cloud-api <status>` when the body was empty), keeping routine HTTP failures one-line and easy to diagnose; any other `Error` is logged with `err.stack ?? err.message` so genuine SDK bugs still surface a frame. Either way `process.exitCode = 1` is set rather than calling `process.exit(1)` so the deprecation-warning flush + telemetry shutdown in `main()`'s `finally` block still run before the process exits. (The explicit catch is also there to avoid Node's default unhandled-rejection handler, which would dump the bundled minified frame for top-level rejections, and to keep the stderr flush deterministic across the supported Node range.)
- The fix for an OAuth session is to re-run `arkor login --oauth`, which goes through the full PKCE flow again and overwrites `~/.arkor/credentials.json` with fresh tokens.

Anonymous tokens have no client-side expiry tracking; the cloud-api decides when they stop working. If an anonymous session starts failing, run `arkor login --anonymous` to mint a new one (this issues a new `anonymousId`, so it is effectively a different workspace).
Anonymous tokens have a server-side 90-day TTL, but the CLI does not yet auto-refresh them; that wiring lives in `@arkor/cloud-api-client`'s `getToken()` and is on the SDK roadmap. If an anonymous session starts failing today, run `arkor login --anonymous` to mint a new one (this issues a new `anonymousId`, so it is effectively a different workspace).

## Common errors

Expand All @@ -125,5 +136,7 @@ Anonymous tokens have no client-side expiry tracking; the cloud-api decides when
| `Auth0 did not return a refresh token. Make sure the Application has 'offline_access' scope enabled.` | `arkor login --oauth` | The OAuth token exchange succeeded but the response had no `refresh_token`, so the CLI cannot keep the session alive past the access token's lifetime. Usually a deployment-side misconfiguration. | Enable the `offline_access` scope on the OAuth (Auth0) application, then re-run `arkor login --oauth`. |
| `No credentials on file.` | `arkor logout` | `~/.arkor/credentials.json` does not exist. Nothing to delete. | Run `arkor login` first if you wanted to sign in. |
| `Not signed in. Run \`arkor login\` or \`arkor login --anonymous\`.` | `arkor whoami` | Same condition as above, surfaced from a different command. | Same fix. |
| `Failed to fetch /v1/me (<status>). Token may be expired.` | `arkor whoami` | The cloud-api rejected the request with a non-200, non-426 status. Most often expired access tokens. | Re-run `arkor login` matching the current `mode`: `arkor login --oauth` for `mode: "auth0"`, `arkor login --anonymous` for `mode: "anon"` (the latter mints a new `anonymousId`, so it lands in a new workspace). The exit code stays `0` so wrapper scripts can inspect the message. |
| ``Anonymous credentials were rejected as single-device. …`` (followed by either `arkor login --oauth` or `arkor login --anonymous` depending on deployment) | `arkor whoami` and any other authenticated command | The cloud-api rejected the request with `code: "anonymous_token_single_device"` (HTTP 401 from the userAuth jti check or HTTP 409 from the rotate-jti CAS). Either the credentials file was copied to a second device, or another local process refreshed past this token within the recovery window. | Follow the command in the message: `arkor login --oauth` to sign up for an OAuth account on supporting deployments, or `arkor login --anonymous` to mint a new anonymous identity on anon-only deployments. Either path is a *new* identity; existing anonymous work cannot be migrated. The CLI exits `1`. |
| ``Your anonymous credentials are no longer valid. …`` (followed by either `arkor login --oauth` or `arkor login --anonymous`) | `arkor whoami` and any other authenticated command | The cloud-api rejected the request with `code: "anonymous_account_not_found"` (HTTP 401). The underlying `anonymous_users` row was deleted (admin / cascade / explicit revocation). | Same as above — follow the deployment-aware command in the message. The previous anonymous workspace cannot be recovered. The CLI exits `1`. |
| `cloud-api <status>` (one line, no stack) | `arkor whoami` and any other authenticated command | A non-200 / non-426 cloud-api response without a structured auth-state `code` (transient 5xx, an unmapped 4xx, etc.). `cli/main.ts` rethrows the `CloudApiError`; `bin.ts` catches it at the top of the stack and renders just `err.message` (the upstream `error` body, or `cloud-api <status>` when the body was empty) before setting `process.exitCode = 1`. Stacks are reserved for unknown `Error` shapes (likely SDK bugs). | Inspect the message verbatim; if it looks like a transport/server failure, retry. For an expired OAuth access token, re-run `arkor login --oauth`. For an expired anonymous token, re-run `arkor login --anonymous` (mints a new `anonymousId`). |
| `426 Upgrade Required` (with upgrade hint) | `arkor whoami` (and other cloud-api calls) | The deployment requires a newer SDK version. The CLI prints the upgrade command for your detected package manager and sets `process.exitCode = 1`. | Upgrade the `arkor` package and re-run. |
Loading
Loading