Read and mutate Olares Settings UI surfaces via olares-cli settings — the per-profile command-line mirror of every section the Olares Settings SPA exposes (h...
---
name: olares-settings
version: 1.2.0
description: "Read and mutate Olares Settings UI surfaces via olares-cli settings — the per-profile command-line mirror of every section the Olares Settings SPA exposes (https://docs.olares.com/manual/olares/settings/). Read-only coverage: users, appearance, apps (list, get, entrances, env, domain, policy), integration accounts, VPN (devices, ACL, SSH, subroutes, public-domain policy), network (reverse proxy, frp, hosts file), GPU, video, search status + dirs, backup plans + snapshots, restore plans, advanced (containerd registries, images, env system / user), plus a `me` self-service tree (whoami, version, check-update, SSO list). Verified mutating verbs: appearance language set, search rebuild, integration accounts add awss3|tencent / delete, VPN SSH enable/disable, VPN ACL add/remove. Use when the user mentions Olares Settings, olares-cli settings, the Olares Settings UI, role (owner / admin / normal), integration accounts, SSO tokens, GPU mode, search index, backup / restore plans, containerd registries, VPN ACLs, language preference, or 'who am I on this Olares instance'."
metadata:
requires:
bins: ["olares-cli"]
cliHelp: "olares-cli settings --help"
---
# settings (Olares Settings UI mirror)
**CRITICAL — before doing anything, MUST use the Read tool to read [`../olares-shared/SKILL.md`](../olares-shared/SKILL.md) for the profile selection, login, and HTTP 401/403 recovery rules that every command here depends on.**
## What this command tree is
`olares-cli settings ...` is the CLI mirror of the Olares desktop SPA's Settings page (the same surface documented at <https://docs.olares.com/manual/olares/settings/>). Identity and transport come from the active profile — same profile model, same access token, same edge-auth chain (Authelia + l4-bfl-proxy) the SPA uses.
The umbrella registers **13 area sub-trees** (see [`cli/cmd/ctl/settings/root.go`](cli/cmd/ctl/settings/root.go)):
```
users appearance apps integration vpn network
gpu video search backup restore advanced
```
…plus a 13th non-canonical tree, `me`, that hosts the SPA's avatar / Person dropdown self-service items. **`me` is intentionally outside the 12 canonical Settings docs sections**; it lives under `settings` for CLI discoverability, not because it's a docs section.
> The shape is always `olares-cli settings <area> <verb>` (or `<area> <noun> <verb>` when the area has multiple sub-resources, e.g. `settings vpn devices list`, `settings backup plans list`). Every verb runs against the currently-selected profile; switch with `olares-cli profile use <name>` ahead of time (there is no per-invocation override flag).
## Authentication transport
Every request goes through the factory-injected `*http.Client` and the resolved profile from `cmdutil.Factory`. There is no kubeconfig dependency.
- Base URL: **per-area split** between `rp.DesktopURL` (default) and `rp.SettingsURL`. The two SPAs serve from different per-user origins — desktop launcher SPA at `https://desktop.<terminus>` (nginx: [`apps/docker/system-frontend/nginx/desktop.conf`](apps/docker/system-frontend/nginx/desktop.conf)), Settings SPA at `https://settings.<terminus>` (nginx: [`apps/docker/system-frontend/nginx/settings.conf`](apps/docker/system-frontend/nginx/settings.conf), the origin Settings SPA itself uses via `tokenStore.setUrl(window.location.origin)` in [`settings/src/application/settings.ts`](settings/src/application/settings.ts)). The two nginx configs share `/api/*` (both forward to user-service:3010) but diverge elsewhere:
- **`rp.SettingsURL`** is required for areas that hit settings-only locations: `vpn` (`/headscale/*`), `backup` / `restore` (`/apis/backup/v1/*`). Settings nginx also exclusively exposes `/admin/*` (Infisical), `/drive`, `/vault`, `/images`, `/api/cloud/sign`.
- **`rp.DesktopURL`** is the default for every other area. Desktop nginx has dedicated reverse proxies that settings nginx does NOT: `/api/device → settings-service`, `/api/logout → authelia-svc`, `/api/refresh → authelia-backend-svc`. The `me` area's SSO list (`/api/device/sso`) and any future code reading `/api/device/*` is therefore wired through DesktopURL on purpose — moving it would silently swap backends. Same goes for the three `whoami` aliases (`profile whoami`, `settings users me`, `settings me whoami`), which read cached identity served from desktop ingress.
Refer to `olares.ID.DesktopURL` / `olares.ID.SettingsURL` doc comments for the canonical location-set tables.
- Auth header: `X-Authorization: <access_token>` (NOT `Authorization: Bearer …`). Injected by the factory's `refreshingTransport` (see [`cli/pkg/cmdutil/factory.go`](cli/pkg/cmdutil/factory.go)); the settings area `prepare()` helpers never call `req.Header.Set("X-Authorization", …)` themselves.
- **Expired access_tokens are auto-rotated.** When the server returns 401/403, the transport hits `/api/refresh`, persists the new token, and retries the original request once — transparently to the caller. Users do NOT need to run `profile login` just because their access_token aged out; only when the *refresh_token* itself is invalidated. Full mechanics — concurrency, cross-process flock, typed `*credential.ErrTokenInvalidated` / `*credential.ErrNotLoggedIn` errors — are documented in [`../olares-shared/SKILL.md`](../olares-shared/SKILL.md) under "Automatic token refresh". **Do not write retry loops on top of these typed errors** — once you see one, only `profile login` / `profile import` will help.
- Two ingress prefixes show up in this subtree:
- `/api/*` (the bulk of the surface) — terminates at user-service, which proxies BFL / app-service / Headscale / terminusd / search3 / HAMI etc.
- `/apis/backup/v1/*` (`settings backup`, `settings restore`) — terminates at BFL's backup-server directly.
- 401 / 403 that survive auto-refresh (i.e. the server still says no after the new token was issued) are translated into a CLI-friendly hint via the `WrapPermissionErr` + `PreflightRole` helpers in [`cli/pkg/whoami/preflight.go`](cli/pkg/whoami/preflight.go), wrapped per-area through [`cli/cmd/ctl/settings/internal/preflight`](cli/cmd/ctl/settings/internal/preflight). See "Soft preflight" below for which verbs have been retrofitted. **Token recovery is not handled here — defer to [`../olares-shared/SKILL.md`](../olares-shared/SKILL.md).**
## Role caching + soft preflight
A profile carries the role its user has on the Olares instance (`owner`, `admin`, or `normal`), cached locally so the CLI can short-circuit gated verbs without a round-trip. The cache lives in `ProfileConfig.OwnerRole` + `WhoamiRefreshedAt` (see [`cli/pkg/cliconfig`](cli/pkg/cliconfig)) and is populated three ways:
| Trigger | Behavior |
|---|---|
| `olares-cli profile login` succeeds | Eager whoami fetch, best-effort, 5-second timeout (see [`cli/cmd/ctl/profile/whoami_eager.go`](cli/cmd/ctl/profile/whoami_eager.go)). Failure does **not** abort the login. |
| `olares-cli profile import` succeeds | Same eager fetch as login. |
| User runs `olares-cli profile whoami --refresh` | Forced re-read of `/api/backend/v1/user-info`; cache rewritten in place. Plain `profile whoami` (no flag) prints the cached value if fresh. |
### Three aliases, one driver
These three commands all delegate to the same `pkg/whoami.Run` driver — same output, same caching, same `--refresh`:
```bash
olares-cli profile whoami
olares-cli settings users me
olares-cli settings me whoami
```
Use whichever path makes the surrounding workflow read better; **never** suggest the user "should use the other one" — they are aliases on purpose. All three accept `-o table` (default) and `-o json`.
### Soft preflight (wired per SPA UI gating)
`whoami.PreflightRole(...)` + `whoami.WrapPermissionErr(...)` (in [`cli/pkg/whoami/preflight.go`](cli/pkg/whoami/preflight.go)) are wrapped per-area by `preflight.Gate(...)` / `preflight.Wrap(...)` (in [`cli/cmd/ctl/settings/internal/preflight`](cli/cmd/ctl/settings/internal/preflight)) and called at the top of every admin-gated `RunE`.
Floor assignment mirrors the SPA's `apps/.../stores/settings/admin.ts:menus` + per-page `v-if="adminStore.isAdmin"` guards 1:1:
| Floor | Verbs |
|---|---|
| **Admin (owner / admin)** | `users list`, `users get`, `users create`, `users delete`; `network reverse-proxy get`, `network frp list`, `network hosts-file get`; `gpu list`; `advanced status`, `advanced registries list`, `advanced images list`; `vpn ssh status/enable/disable`, `vpn subroutes status`, `vpn acl all/get/add/remove`, `vpn public-domain-policy get` |
| **Normal (any authenticated user)** | `me whoami / version / check-update / sso list`; `apps list/get`, `apps entrances list`, `apps env get`, `apps domain get`, `apps policy get`; `vpn devices list`, `vpn devices routes <id>`; `appearance get`, `appearance language set`; `integration accounts list/list-by-type/get/add/delete`; `video config get`; `search status`, `search dirs list`, `search rebuild`; `backup plans list`, `backup snapshots list`; `restore plans list`; `advanced env system list`, `advanced env user list` |
Practical implications for the agent:
- Admin-floor verbs run an upfront check against the cached role on the active profile and fail fast with **`role required: this command needs role "<R>" or higher to <verb>, but profile "<id>" is cached as "<r>" — run \`olares-cli profile whoami --refresh\` if your role on the server changed`** before issuing any HTTP call.
- Both floors run their result through `WrapPermissionErr`, so a server-side 401 / 403 (e.g. role changed since last cache write) still gets the same refresh-and-retry hint suffix — even on verbs that don't preflight.
- If `OwnerRole` isn't cached yet (very fresh `profile import`, or the `whoami_eager` fetch failed), preflight is **soft**: it lets the call through and lets the server be authoritative. Recommend `olares-cli profile whoami --refresh` whenever a settings verb returns a permission-shaped error.
## Output convention
Every read verb accepts `-o / --output {table,json}` (default `table`):
- `table` is wrapped in [`text/tabwriter`](https://pkg.go.dev/text/tabwriter); columns differ per verb but always print a clear "no X" sentinel when the result set is empty.
- `json` round-trips the upstream's already-unwrapped data verbatim. **Use `-o json` whenever the agent needs to feed the result into another tool** (jq, downstream scripts, etc.) — column ordering, truncation, and human-friendly relabeling only happen in table mode.
- A handful of verbs (e.g. `settings video config get`, `settings advanced status`) deliberately downgrade the table output to a one-line summary because the upstream config is large + provider-versioned. The hint to switch to JSON is printed inline.
## Per-area wire format normalization
Different upstreams return JSON in different envelopes. Each area has its own `common.go` that picks the matching decoder; **the table below is the cheat sheet for "what's the wire format for area X"**.
| Area | Endpoint family | Decoder | Notes |
|---|---|---|---|
| `me` | `/api/olares-info`, `/api/checkLastOsVersion`, `/api/device/sso` | BFL envelope (`doGetEnvelope`) | self-service identity + version + SSO sessions on the active profile |
| `users` | `/api/users/v2` (list), `/api/users/:name` (single), `POST /api/users`, `DELETE /api/users/:name` | list: `decodeListResult`; get: envelope-aware `decodeObjectResult`; create/delete: `doMutateUsersAPI` (`decodeUsersMutateBody`) because Nest returns axios inner `{name}` | list: normal users see only themselves; mutating routes lack extra RBAC in user-service today |
| `apps` | `/api/myapps` | BFL envelope, `appInfo.ports` decoded as `[]servicePort` (5-field object array: name / host / port / exposePort / protocol) | `apps get <name>` filters the list client-side because there's no per-app endpoint; honours `--all` / `--show-system` to mirror the SPA's filters |
| `vpn` | `/headscale/machine`, `/headscale/machine/:id/routes` | Raw Headscale JSON (no envelope), `route.id` is `string` | SPA hits `<SettingsURL>/headscale/...` (settings nginx, not desktop) without `/api` prefix |
| `vpn` | `/api/launcher-public-domain-access-policy` | Already-unwrapped BFL inner data (`{deny_all: 0|1}`) | user-service strips the envelope |
| `network` | `/api/reverse-proxy`, `/api/frp-servers`, `/api/ssl/task-state`, `/api/system/hosts-file` | BFL envelope (`doGetEnvelope`) | hosts-file goes through terminusd → olaresd; user-service falls back to `X-Authorization` since the CLI doesn't yet JWS-sign reads |
| `appearance` | `/api/wallpaper/config/system` | BFL envelope | language + locale only; theme + wallpaper upload stay in the SPA |
| `integration` | `/api/account/all`, `/api/account/:type/:name` | BFL envelope | `accounts list` returns `accountMini`; `accounts get` returns `accountFull` (includes `raw_data`) |
| `gpu` | `/api/gpu/list` | BFL envelope (HAMI behind it) | distinct from the top-level `olares-cli gpu` (kubeconfig-driven, cluster-wide) |
| `video` | `/api/files/video/config` | BFL envelope, but inner data is decoded as `json.RawMessage` (`doGetEnvelopeRaw`) | Schema is provider-versioned; `--output table` collapses to a one-line summary |
| `search` | `/api/search/task/stats/merged`, `/api/search/monitorsetting/include-directory/full_content` | BFL envelope | `status` returns a string, `dirs list` returns `[]string` |
| `advanced` | `/api/system/status`, `/api/containerd/registries`, `/api/containerd/images?registry=<n>`, `POST /api/command/collectLogs` | BFL envelope (terminusd → olaresd `returnSucceed`) | `status` table view is a summary; `--output json` for the full struct. `POST /api/command/collectLogs` is for the **Terminus SPA Developer UI** (polls `/api/system/status`); it is **not** a `settings advanced` CLI verb — use top-level **`olares-cli logs`** for CLI log collection |
| `backup` | `/apis/backup/v1/plans/backup`, `/apis/backup/v1/plans/backup/:id/snapshots` | BFL envelope; **different ingress prefix** (`/apis/backup/v1`, not `/api`) | The SPA's axios global interceptor unwraps `data.data`, which is why upstream code reads `{backups: [...]}` directly |
| `restore` | `/apis/backup/v1/plans/restore` | BFL envelope; same `/apis/backup/v1` prefix | mirrors `settings backup plans list` shape |
If a future verb is missing from this table, look at the area's `common.go` to confirm which decoder it uses — every `prepare(...)` helper instantiates a `*whoami.HTTPClient` against `<DesktopURL>` so the path is the only thing that varies.
## Currently available — read-only commands
Read-only verbs across every area. Mutating verbs are listed in the next section. Anything not appearing in either list is either deferred to the next iteration or out of scope (see the "Deferred to next iteration" section near the bottom).
### `me` — self-service (any authenticated user)
```bash
olares-cli settings me whoami # cached role + olaresId
olares-cli settings me whoami --refresh # force a re-read of /api/backend/v1/user-info
olares-cli settings me whoami -o json
olares-cli settings me version # Olares OS version + osBuild + arch
olares-cli settings me check-update # current_version, new_version, is_new
olares-cli settings me sso list # SSO tokens currently bound to this profile (with ID column)
```
### `users` — instance roster
```bash
olares-cli settings users list # roster, server-side role-filtered
olares-cli settings users get alice # single user record
olares-cli settings users create bob --defaults # SPA preset: normal, 1 CPU, 4G memory; auto password; DID precheck; accepted-then-exit
olares-cli settings users create bob --defaults --watch # synchronous: block until provisioning reaches Created and the Wizard URL is materialized
olares-cli settings users create alice --role admin --cpu 2 --memory-gb 8 # explicit quota; default accepted-then-exit (use --watch to wait)
olares-cli settings users delete bob # accepted-then-exit (the row may still appear briefly in `users list`); type yes or use --yes
olares-cli settings users delete bob --yes --watch # synchronous: block until status is Deleted (same opt-in shape as `olares-cli market <verb> --watch`)
# owner accounts cannot be deleted (CLI rejects before DELETE, same as `olares-cli user delete`)
olares-cli settings users me # alias of `me whoami`
```
`create`/`delete` are accepted-then-exit by default; pass `-w/--watch` (with `--watch-timeout` / `--watch-interval`) to block until terminal state — the flag triple mirrors [`olares-cli market --watch`](../olares-market/SKILL.md#watch-flag) so both surfaces feel identical to operators. **Note**: this flips the prior default from "wait by default + `--no-wait`" — scripts that relied on blocking semantics must add `--watch` explicitly.
`create`/`delete` hit the Termipass user-service routes, not `olares-cli user create` (kube CR).
<a id="users-watch-flag"></a>
#### `users --watch` (block until terminal state)
Same shape as the market verb's [`--watch`](../olares-market/SKILL.md#watch-flag) — opt-in `-w/--watch`, with companion `--watch-timeout` (15m) and `--watch-interval` (2s). Implementation lives in [`cli/cmd/ctl/settings/users/watch.go`](cli/cmd/ctl/settings/users/watch.go) (`waitForUserState`, `newUserWatchTarget`, `userWatchTimeoutError`, `userWatchFailureError`); the loop wraps `signal.NotifyContext` for Ctrl-C and gives up after 5 consecutive transport errors.
Per-op terminal sets (from [`cli/cmd/ctl/settings/users/watch.go`](cli/cmd/ctl/settings/users/watch.go)):
| Op | Success | Failure | absentMeansSuccess |
|----------|------------|--------------------------|---------------------------|
| `create` | `Created` | `Failed` / `Deleted`* | false |
| `delete` | `Deleted` | (timeout only) | true (defensive HTTP 404) |
\* `Deleted` during a create watch means the row vanished mid-watch (controller cleanup raced), reported back as a failure so JSON consumers get a non-zero exit.
JSON output adds `final_status` when `--watch` is set:
```json
{"name":"alice","original_password":"...","status":"Created","final_status":"Created","wizard_url":"https://wizard-alice.example.com"}
{"name":"bob","status":"Deleted","final_status":"Deleted"}
```
Without `--watch`, JSON is `{"name":"…","status":"Accepted"}` (create additionally echoes `original_password`).
### `apps` — installed app inventory (mirror of Settings -> Apps)
```bash
olares-cli settings apps list # SPA-equivalent filtered view
olares-cli settings apps list --show-system # include system apps
olares-cli settings apps list --all # every state + every kind
olares-cli settings apps get firefox # detail view (entrances, shared entrances, …)
olares-cli settings apps entrances list firefox # live entrance vector (fresher than `apps get`)
olares-cli settings apps env get firefox # current env vector for the app
olares-cli settings apps domain get firefox www # custom domain on a single entrance
olares-cli settings apps policy get firefox www # two-factor / one-time-link policy on a single entrance
```
The `entrances list` / `domain get` / `policy get` reads target the BFL-style `/api/applications/<app>/<entrance>/setup/{domain,policy}` routes; pair them with `apps get` when you need both the lifecycle status and the per-entrance config.
### `vpn` — Headscale mesh
```bash
olares-cli settings vpn devices list # raw Headscale machines
olares-cli settings vpn devices routes <device-id>
olares-cli settings vpn ssh status # GET /api/acl/ssh/status
olares-cli settings vpn subroutes status # allow_subroutes flag + sub-route list (-o json: unwrapped []string the SPA reads)
olares-cli settings vpn acl all # every app that currently has an ACL row
olares-cli settings vpn acl get my-app # per-app ACL vector; -o json for the full payload
olares-cli settings vpn public-domain-policy get # deny_all flag (0/1)
```
### `network`
```bash
olares-cli settings network reverse-proxy get # mode collapsed into public-ip / frp / cloudflare / off
olares-cli settings network frp list # registry of FRP servers
olares-cli settings network hosts-file get # entries from /system/hosts-file (terminusd)
```
### `appearance` / `integration` / `gpu` / `video` / `search` / `advanced`
```bash
olares-cli settings appearance get
olares-cli settings integration accounts list
olares-cli settings integration accounts list-by-type google
olares-cli settings integration accounts get awss3 my-bucket
olares-cli settings gpu list
olares-cli settings video config get # raw config; -o json recommended
olares-cli settings search status
olares-cli settings search dirs list
olares-cli settings advanced status # large struct; -o json for the full payload
olares-cli settings advanced registries list
olares-cli settings advanced images list
olares-cli settings advanced images list --registry docker.io
```
### `backup` / `restore`
```bash
olares-cli settings backup plans list # --offset / --limit (default 50, mirrors SPA)
olares-cli settings backup snapshots list <backup-id> --limit 50
olares-cli settings restore plans list
```
## Currently available — mutating commands (smoke-verified)
Every mutating verb in this section has been confirmed against a live Olares instance in the latest smoke run (see [`cli/cmd/ctl/settings/scripts/local_report_phase15a.md`](cli/cmd/ctl/settings/scripts/local_report_phase15a.md)). All of them hit the `<DesktopURL>` ingress over `X-Authorization` and none require a JWS-signed body. Verbs that ship in the binary but were not exercised (or did not pass) live in [`cli/cmd/ctl/settings/scripts/UNVERIFIED_COMMANDS.md`](cli/cmd/ctl/settings/scripts/UNVERIFIED_COMMANDS.md) — treat them as experimental until they show up here.
**Also ships (awaiting smoke):** `users create` / `users delete` — **admin-floor** preflight; `create` uses `--defaults` (normal / 1 / 4G) or explicit `--role`, `--cpu`, `--memory-gb`; DID precheck; no `--password`; **both verbs default to accepted-then-exit**; pass **`-w/--watch`** (with `--watch-timeout` 15m, `--watch-interval` 2s — same triple as [`olares-cli market --watch`](../olares-market/SKILL.md#watch-flag)) to block until `Created` / `Deleted`; **`delete`** needs the whole word `yes` unless **`--yes`**; **`delete` refuses owner** (pre-check via GET, before confirmation). Smoke pairing: `INCLUDE_USERS_MUTATE=1`. Backend gap: user-service does not role-gate POST/DELETE beyond authentication.
### `appearance` — language
```bash
olares-cli settings appearance language set en-US # POST /api/wallpaper/update/language
olares-cli settings appearance language set --value zh-CN
olares-cli settings appearance language set ja-JP --force # bypass whitelist (use sparingly)
```
The CLI mirrors the SPA's `supportLanguages` whitelist client-side ([`apps/packages/app/src/i18n/index.ts:12`](apps/packages/app/src/i18n/index.ts) — currently `en-US`, `zh-CN`) because **neither user-service nor BFL validate the value today**: an unknown locale would land in the config-system CRD verbatim and the SPA would silently fall back to `defaultLanguage` on the next session. Pass `--force` only when the SPA has shipped a new locale ahead of this CLI build; the upstream still accepts arbitrary strings, so a typo with `--force` will appear to succeed but produce no visible change.
### `search` — index rebuild
```bash
olares-cli settings search rebuild # POST /api/search/task/rebuild
```
`rebuild` is async + heavy: the call returns as soon as search3 accepts the task; verify completion with `olares-cli settings search status` rather than waiting on the POST itself. `dirs` writes ship in the binary but are tracked in [`UNVERIFIED_COMMANDS.md`](cli/cmd/ctl/settings/scripts/UNVERIFIED_COMMANDS.md) until a smoke report greens them.
### `vpn ssh` — boolean ACL toggle
```bash
olares-cli settings vpn ssh enable # POST /api/acl/ssh/enable
olares-cli settings vpn ssh disable # POST /api/acl/ssh/disable
```
Both toggles send an explicit empty `{}` body to match the SPA's request shape, even though the upstream doesn't read the body. Use `vpn ssh status` (read section above) to confirm the resulting state.
### `vpn acl` — per-app ACL add / remove (read-modify-write)
```bash
olares-cli settings vpn acl add my-app --tcp '*:8080' # merge a TCP dst (any source, port 8080)
olares-cli settings vpn acl add my-app --tcp '192.168.1.0/24:22' # restrict source CIDR
olares-cli settings vpn acl remove my-app --tcp '*:80' # drop a TCP dst
olares-cli settings vpn acl rm my-app --udp '*:53' # alias of `remove`
olares-cli settings vpn acl add olares-app --any-proto '*:8080' # Web 'Add ACL' parity (proto="")
```
**Destination spec (important).** Every `--tcp` / `--udp` / `--any-proto` value is a Headscale destination spec `<host>:<port>`, **not** a bare port number. The upstream policy parser (Headscale, via BFL `parseDestination`) splits on the last `:` and rejects single-token values with `invalid port format`. Use `'*:8080'` for "any source, port 8080" (this is what the Web UI implicitly sends), `'192.168.1.0/24:22'` to restrict the source, `'tag:api:443'` for tag-based sources, or `'example-host:*'` to allow all ports on one host. The CLI now pre-validates this shape and surfaces a copy-pasteable suggestion rather than letting the BFL reject the POST. Both repeated flags (`--tcp '*:80' --tcp '*:443'`) and comma-separated values (`--tcp '*:80,*:443'`) work and are deduped client-side. The upstream still accepts ranges (`*:8000-8100`) and `*:*`; the CLI only checks that a `:` separates two non-empty halves.
**Web parity via `--any-proto`.** The Settings page's "Add ACL" dialog only collects a port, hardcodes the app to the system `olares-app`, sets the source host to `*`, and posts the entry with `proto=""` so Tailscale expands it to ICMPv4 / ICMPv6 / TCP / UDP. The CLI's `--tcp` / `--udp` flags are strictly more expressive (per-protocol slots, named app, custom hosts/CIDRs/tags) but they always emit a typed proto. To mirror the Web shape one-for-one — including the empty proto so the rule applies to all protocols — pass `--any-proto '<host>:<port>'`; e.g. `vpn acl add olares-app --any-proto '*:8080'` is the exact payload the "Add ACL" dialog would send for port 8080. `add` / `remove` use case-insensitive proto matching on the merge/subtract paths, so empty-proto entries created via `--any-proto` round-trip cleanly alongside `--tcp` / `--udp` ones.
The upstream replaces the **whole** per-app ACL vector on every POST; there is no add / remove endpoint. `vpn acl add` and `vpn acl remove` are read-modify-write sugar over the same POST so unrelated entries survive untouched (matching how the SPA's add / remove buttons work). Use `vpn acl get <app>` (read section) to inspect the current vector before mutating; `vpn acl all` (also read) lists every app that currently has an ACL row.
### `integration` — connected accounts
```bash
olares-cli settings integration accounts add awss3 \
--access-key-id "$AWS_ACCESS_KEY_ID" \
--access-key-secret "$AWS_SECRET_ACCESS_KEY" \
--endpoint "https://s3.amazonaws.com" \
--bucket "my-bucket" # optional
olares-cli settings integration accounts add tencent \
--access-key-id "$TENCENT_SECRET_ID" \
--access-key-secret "$TENCENT_SECRET_KEY" \
--endpoint "https://cos.ap-shanghai.myqcloud.com"
olares-cli settings integration accounts delete awss3 my-bucket
olares-cli settings integration accounts delete tencent # name-less, single-tenant
```
The store key is composed as `integration-account:<type>:<name>` (or `integration-account:<type>` when no name is supplied), matching the SPA's `getStoreKey` in [`apps/packages/app/src/stores/settings/integration.ts`](apps/packages/app/src/stores/settings/integration.ts). **Do not paste secret-key values into the agent transcript — pipe them via env vars or shell redirection.**
## Deferred to next iteration
The verbs below are **not shipped** in this release. They either need more design work or require JWS-signed bodies the CLI can't produce yet. Don't suggest them today; reach for the listed alternatives when an alternative exists.
- **App lifecycle: install / uninstall / upgrade / start / stop / cancel / clone** — these route through the market service rather than user-service. Use `olares-cli market install|uninstall|upgrade|start|stop|cancel|clone` instead of `settings apps`. (Per-app `suspend [--all]` / `resume` + `env set` + per-entrance `domain set` / `finish` / `policy set` / `auth-level set` ship in the settings binary but are not yet smoke-verified — see [`UNVERIFIED_COMMANDS.md`](cli/cmd/ctl/settings/scripts/UNVERIFIED_COMMANDS.md).)
- **Per-app secrets / permissions / providers** — Infisical-backed per-app secrets, declared permissions, and provider registries were not included in this release. If you need to inspect or write them, use the platform's admin / chart-side tooling instead.
- **Network writes that require a JWS-signed device-id header** — hosts-file write, FRP server register / delete, SSL enable / disable / update (the SPA carries these with `X-Signature` headers the CLI doesn't yet produce).
- **External-network master switch (read + write)** — BFL exposes `/api/external-network` and a Go implementation lives in [`cli/cmd/ctl/settings/network/external_network.go`](cli/cmd/ctl/settings/network/external_network.go), but neither the desktop SPA nor TermiPass has surfaced a UI for it yet, and the matching write still needs a JWS-signed device-id header. The verb is intentionally NOT registered on the command tree right now — shipping a read-only verb in isolation only confuses operators. Re-add `cmd.AddCommand(NewExternalNetworkCommand(f))` in [`cli/cmd/ctl/settings/network/root.go`](cli/cmd/ctl/settings/network/root.go) once the UI lands or the JWS key sourcing path is wired.
- **Containerd registry mutations** — `registries mirrors put / delete`, `images delete / prune` (also `X-Signature`-gated).
- **Hardware / restart-class** — reboot, shutdown, ssh-password, OS upgrade — these go through TermiPass-issued JWS over a QR callback URL; CLI support arrives once a JWS key sourcing path lands.
- **Backup plan create / update** — full `BackupPolicy` + `LocationConfig` vector; needs either a `--from-file plan.json` mode or an upstream "create from defaults" shortcut before shipping.
- **Restore plan update / non-cancel delete** — backup-server has no routes for these.
Every area's `--help` is the source of truth for what's currently implemented; if a verb isn't there, treat it as deferred.
Verbs implemented in this CLI but **not yet smoke-verified on this release** are catalogued in [`cli/cmd/ctl/settings/scripts/UNVERIFIED_COMMANDS.md`](cli/cmd/ctl/settings/scripts/UNVERIFIED_COMMANDS.md). Treat them as experimental until they appear in a green smoke report; the file lists per-verb status (FAIL / SKIP-destructive / SKIP-fixture-missing) and links back to the phase report row.
## Common errors → fixes
| Error message | Cause | Fix |
|---|---|---|
| `refresh token for <id> became invalid at <ts>; please run: olares-cli profile login --olares-id <id>` | `/api/refresh` itself returned 401/403 — the grant is dead (typed `*credential.ErrTokenInvalidated`) | `olares-cli profile login --olares-id <id>`. Defer the full recovery flow to [`../olares-shared/SKILL.md`](../olares-shared/SKILL.md). |
| `no access token for <id>; run: olares-cli profile login --olares-id <id>` | Profile selected but keychain has no entry (typed `*credential.ErrNotLoggedIn`) | `olares-cli profile login` or `profile import`. |
| `server rejected the access token (HTTP 401/403)` | Server still rejects after auto-refresh — rare, usually server-side state drift | Defer to [`../olares-shared/SKILL.md`](../olares-shared/SKILL.md) (login + profile rules). |
| `this command needs role "<R>" or higher to <verb>, but profile "<id>" is cached as "<r>"` | Cached role below the verb's requirement (emitted by admin-floor verbs — see the floor table in "Soft preflight"). | If your role on the server changed, run `olares-cli profile whoami --refresh`. Otherwise ask the owner to grant you the right role. |
| `HTTP 403 while attempting to <verb>` (with the same refresh hint appended) | Server rejected even though cache said OK — usually a stale **role** cache (NOT a stale token; the transport already handled that). Wrapped on every settings verb. | Run `olares-cli profile whoami --refresh`, then retry the verb. |
| `unsupported --output "<x>" (allowed: table, json)` | Typo on `-o` | Use `-o table` or `-o json`. |
| `GET <path>: upstream returned code <N>: <msg>` | The user-service / BFL / backup-server returned a non-success envelope | Read the message verbatim; it almost always carries actionable detail (e.g. "user not found"). |
| `internal error: settings <area> not wired with cmdutil.Factory` | Unexpected — would only happen if the umbrella was wired without the factory | This is a CLI bug; gather the command line and file an issue. |
## Typical workflows
Confirm who the active profile is, then enumerate what the user can see:
```bash
olares-cli profile whoami # cached role
olares-cli settings users list # only owners/admins see everyone
olares-cli settings apps list # everyone sees their own apps
```
Refresh the cache after a role change on the server:
```bash
olares-cli profile whoami --refresh
olares-cli settings advanced status # retry the gated verb
```
Hand a downstream tool the raw data:
```bash
olares-cli settings vpn devices list -o json | jq '.[] | {name, ip: .IPAddresses[0]}'
olares-cli settings backup plans list -o json | jq '.backups[] | select(.status=="failed")'
```
Inspect a single account's full payload (incl. `raw_data`):
```bash
olares-cli settings integration accounts get awss3 my-bucket -o json
```
## Security rules
- **Never** echo `<access_token>` or any field returned by `me sso list` into the terminal beyond what the table view already shows. SSO tokens identify a TermiPass-bound device session and should never be logged or pasted into chat.
- `settings users create` / `settings users delete` are destructive (`delete` needs the whole word `yes` unless **`--yes`**). **Both default to accepted-then-exit**; pass **`--watch`** to block until `Created` / `Deleted` (with `--watch-timeout` / `--watch-interval`, same as [`olares-cli market --watch`](../olares-market/SKILL.md#watch-flag)). **`delete` cannot remove the owner account** (fails before DELETE). `create` always generates the initial password once to stdout; treat transcripts accordingly.
- `settings users get <username>` returns the same record the SPA shows on the user detail page; treat its email / olaresId as PII and avoid forwarding it outside the requesting workflow.
- For writes that take secrets (`integration accounts add awss3|tencent` is the verified one in this surface), **always** read the secret from an env var or stdin pipe — never paste it into the chat or expand it inline in an `olares-cli ...` command line you suggest. Bash history retention is the user's responsibility; the agent should default to env-var / pipe style invocations (`--access-key-secret "$AWS_SECRET_ACCESS_KEY"`, `printf '%s\n' "$VAR" | ... --password-stdin`) whenever the verb supports it.
- Other secret-bearing verbs (e.g. `backup password set`, `restore plans check-url / create-from-url`) live in [`UNVERIFIED_COMMANDS.md`](cli/cmd/ctl/settings/scripts/UNVERIFIED_COMMANDS.md) until they're smoke-greened; the same env-var / stdin-pipe rule applies whenever you exercise them by hand.
- Read-only verbs do **not** carry "this will change X" prompts — only mutating verbs do, and the prompts they do carry come from the upstream server's own response messages. Don't fabricate one for read verbs.
- The `me whoami --refresh` recovery path is the only authentication-adjacent action this skill should ever recommend. **All** other auth recovery (login expiry, profile import, 2FA) belongs in [`../olares-shared/SKILL.md`](../olares-shared/SKILL.md).
## See also
- [`olares-shared`](../olares-shared/SKILL.md) — profile model, login, automatic token refresh, full auth-error recovery table. **Read this one first.**
- [`olares-files`](../olares-files/SKILL.md) — drive / sync / cache file browser, upload / download / share / chown.
- [`olares-market`](../olares-market/SKILL.md) — Olares app lifecycle (install / uninstall / upgrade / start / stop / cancel / clone).
- [`olares-dashboard`](../olares-dashboard/SKILL.md) — Olares Dashboard SPA proxy (overview, applications, GPU, fan, ranking).
- [`olares-cluster`](../olares-cluster/SKILL.md) — per-user Kubernetes view (pods / workloads / namespaces / jobs / cronjobs / nodes / middleware).
don't have the plugin yet? install it then click "run inline in claude" again.