@fidt/saul-cli
v0.2.0
Published
CLI snapshot/apply YAML definitions & authorization cho Saul modules
Readme
@fidt/saul-cli
CLI snapshot/apply YAML cho definitions (module/service/function/model/error) và
authorization config (policy/permission) của một module Saul. Cùng mô hình
snapshot → plan → apply mà apps/directus dùng (pnpm save / pnpm plan /
pnpm apply): repo của module tự lưu YAML, review qua PR, rồi CI apply vào tenant
tương ứng — không cần đăng nhập tay vào Designer UI.
Khác với Directus, saul-cli không nói chuyện trực tiếp với Directus. Nó gọi
thẳng các RPC mà Saul backend đã tự expose sẵn (module hệ thống saul,
exportModuleYaml/importModuleYaml, exportPoliciesYaml/importPoliciesYaml,
exportPermissionsYaml/importPermissionsYaml) — nên mọi rule nghiệp vụ (chặn ghi
module hệ thống, giữ baseline PublicPolicy, tenant scoping...) đã được backend đảm bảo,
CLI không phải lặp lại.
Cài đặt
Trạng thái hiện tại: package này chưa được publish lên npm registry nào.
pnpm add -D @fidt/saul-cli/npx @fidt/saul-clisẽ báo 404 cho tới khi ai đó chủ độngnpm publish(xempackages/saul/interfacesđể biết cách các package@fidt/*khác đã publish trước đó). Đây là hành động công khai, không tự động làm khi build.
Test/chạy local (không cần publish) — chọn một trong hai cách link tuỳ package manager sẵn có:
cd packages/saul/saul-cli
pnpm install --ignore-workspace # cài dependency riêng cho package này
pnpm run build
npm link # tạo lệnh `saul` global trỏ vào dist/cli.js local
saul --help
# hoặc, nếu dùng Bun:
bun link # đăng ký @fidt/saul-cli vào Bun global link registry
bunx saul --help # bunx tự resolve ra bin đã link, không cần npm link song songbun link (không kèm tên package) chạy trong thư mục saul-cli — nó đọc package.json
tại đó để biết tên gói (@fidt/saul-cli) và field bin (saul), rồi tạo symlink:
~/.bun/install/global/node_modules/@fidt/saul-cli → packages/saul/saul-cli (nguyên thư mục)
~/.bun/bin/saul → .../dist/cli.js (bin)bunx saul sau đó chạy được từ bất kỳ thư mục nào, vì ~/.bun/bin nằm trong PATH và
bunx tự tìm bin đã link trước khi thử tải từ registry. Do symlink trỏ thẳng vào
dist/cli.js, mỗi lần sửa code phải pnpm run build lại (hoặc pnpm run dev chạy tsup
--watch) — không cần bun link lại. Gỡ bằng bun unlink trong packages/saul/saul-cli.
dist/cli.jscó shebang#!/usr/bin/env node, nênbunx saulmặc định vẫn chạy dưới Node, chỉ cóbunx(bộ quản lý gói) là Bun. Muốn ép chạy dưới Bun runtime:bunx --bun saul. Việc này chưa được test kỹ vớisaul login(mở HTTP server loopback) — ưu tiên Node cho các lệnh auth nếu gặp lỗi lạ.
Sau khi publish thật lên registry:
pnpm add -D @fidt/saul-cli
# hoặc chạy không cài
npx @fidt/saul-cli --help
bunx @fidt/saul-cli --helpHai danh tính: người dùng và M2M
CLI chạy được dưới hai danh tính, và tự chọn theo cấu hình đang có:
Luật chọn, xét theo thứ tự:
| # | Điều kiện | Chế độ |
|---|---|---|
| 1 | SAUL_CLI_AUTH_MODE=user\|m2m | đúng như khai (ép m2m mà thiếu secret ⇒ báo lỗi) |
| 2 | Đã saul login với đúng cặp (issuer, client id) đang dùng | user |
| 3 | Có SAUL_CLI_CLIENT_SECRET | m2m |
| 4 | Còn lại | user (và báo lỗi bảo chạy saul login) |
Có secret không mặc nhiên là M2M. Client confidential vẫn đăng nhập user được — nó chỉ
phải gửi kèm secret ở bước xác thực client. Nếu cứ thấy secret là nhảy sang M2M thì
saul login sẽ thành công rồi mọi lệnh sau đó lặng lẽ chạy dưới danh tính máy, sai hẳn ý
người dùng. CI không có ~/.saul nên quy tắc #2 không bao giờ khớp, vẫn rơi về m2m như
trước.
Backend nhận cả hai loại giống nhau — Authorization: Bearer <token> — nên mọi lệnh
snapshot/plan/apply không quan tâm mình đang chạy ở chế độ nào. Khác biệt duy nhất
nằm ở phân quyền: token M2M mang sẵn policies của client, token người dùng đi qua
membership + role của user trong tenant.
Cấu hình
Biến môi trường:
| Biến | Bắt buộc | Ý nghĩa |
|---|---|---|
| SAUL_CLI_BACKEND_URL | ✓* | Base URL Saul backend của tenant đích (vd https://saul.<tenant>.example.com) |
| SAUL_CLI_TOKEN_URL | ✓* | OIDC token endpoint (identity). Issuer được suy ra từ đây (bỏ hậu tố /token) |
| SAUL_CLI_CLIENT_ID | ✓* | Client id OIDC (oidc_client) |
| SAUL_CLI_CLIENT_SECRET | chỉ M2M | Client secret. Client public (khuyến nghị cho CLI) không cần |
| SAUL_CLI_MODULE | ✓* | Tên module cần snapshot/apply (có thể override bằng --module) |
| SAUL_CLI_TENANT | ✓* | Id (UUID, không phải tên) của tenant sở hữu module — xem giải thích bên dưới |
| SAUL_CLI_ISSUER_URL | | Ghi đè issuer nếu identity mount ở path khác thường |
| SAUL_CLI_SCOPE | | Scope xin lúc đăng nhập. Mặc định openid profile email offline_access |
| SAUL_CLI_REDIRECT_URI | | Chỉ cho saul login --use-browser. Mặc định http://localhost:8400/callback |
| SAUL_CLI_CONFIG_DIR | | Thư mục lưu phiên đăng nhập. Mặc định ~/.saul |
| SAUL_CLI_AUTH_MODE | | Ép user hoặc m2m, bỏ qua luật tự chọn ở mục trên |
* có thể khai thay bằng file saul.config.yaml ở root repo module (giá trị KHÔNG
nhạy cảm, commit được — clientId của client public không phải secret):
backendUrl: https://saul.acme.example.com
tokenUrl: https://identity.acme.example.com/api/oidc/token
clientId: saul-cli
module: wfm_bds
tenant: 9f1c2e34-xxxx-xxxx-xxxx-xxxxxxxxxxxx
outDir: saul # tuỳ chọn, mặc định "saul"Cấu hình global cho cả máy
saul.config.yaml gắn với thư mục, nên những giá trị không đổi giữa các repo (token
endpoint của identity, client id của CLI) phải khai lại ở mọi nơi. Khai một lần cho cả máy
bằng saul config — file nằm ở ~/.saul/config.yaml:
saul config set tokenUrl=https://iam.example.com/api/oidc/token clientId=saul-cli
saul config list # giá trị đang có hiệu lực + NGUỒN của từng giá trị
saul config get tokenUrl
saul config unset tokenUrl
saul config list --global-only # chỉ nội dung ~/.saul/config.yamlsaul config list in ra thứ CLI thực sự sẽ dùng, kèm nơi giá trị đó đến từ — hữu ích
khi một repo có saul.config.yaml riêng hoặc shell đang có biến ghi đè:
tokenUrl https://iam.example.com/api/oidc/token ← global (/home/you/.saul/config.yaml)
clientId app-2f49a1c9-... ← env (SAUL_CLI_CLIENT_ID)
issuerUrl https://iam.example.com/api/oidc ← default (suy từ tokenUrl)
redirectUri http://localhost:8400/callback ← default (mặc định)Khoá dùng được đúng bằng khoá của saul.config.yaml: backendUrl, tokenUrl,
issuerUrl, clientId, redirectUri, scope, module, tenant, outDir.
saul config set clientSecret=... bị từ chối — secret chỉ đi qua
SAUL_CLI_CLIENT_SECRET để CI quản lý được vòng đời của nó, không nằm trong file config.
Thứ tự ưu tiên đầy đủ
cờ CLI > env (kể cả nạp từ .env) > saul.config.yaml của repo
> ~/.saul/config.yaml > phiên đã saul login > mặc địnhsaul.config.yaml của repo đứng trên config global có chủ đích: repo của một module
ghim backend/tenant của riêng nó, và lựa chọn đó phải thắng mặc định toàn máy. Phiên đăng
nhập gần cuối vì nó là hệ quả của một lần saul login, không phải lựa chọn cấu hình tường
minh.
Vì sao SAUL_CLI_TENANT bắt buộc: exportPoliciesYaml/exportPermissionsYaml
phía backend là API cho màn Designer admin — trả về tất cả tenant gộp chung trong
một map phẳng, không tự lọc theo tenant của M2M client gọi vào (xem
AuthorizationService.scope() trong apps/saul/server, luôn chạy ở phạm vi hệ thống
để admin xem được toàn cục). Không khai SAUL_CLI_TENANT, saul snapshot sẽ kéo cả
policy/permission của những tenant khác vào repo của module — sai và rò rỉ dữ liệu.
CLI tự lọc theo SAUL_CLI_TENANT trước khi ghi file, và tự thêm lại đúng id-prefix của
tenant đích lúc apply (xem src/yaml/tenantScope.ts). Lấy id qua Designer UI (danh
sách tenant) hoặc gọi RPC saul.listTenants.
Ngược lại, module.yaml (definitions) không cần SAUL_CLI_TENANT để lọc — gateway
/saul đã tự chặn cross-tenant cho definitions (đối chiếu tenant của M2M token với
tenant đang sở hữu module, xem resolveScopeMiddleware). SAUL_CLI_TENANT chỉ ảnh
hưởng authorization.
Dùng file .env (khuyến nghị cho chạy local)
CLI tự nạp file .env ở thư mục hiện tại trước khi chạy lệnh (dùng
dotenv, cùng convention với
apps/saul/server). Tạo .env cạnh saul.config.yaml:
# .env — KHÔNG commit, nhớ thêm vào .gitignore
SAUL_CLI_BACKEND_URL=https://saul.acme.example.com
SAUL_CLI_TOKEN_URL=https://identity.acme.example.com/api/oidc/token
SAUL_CLI_CLIENT_ID=wfm_bds-ci
SAUL_CLI_CLIENT_SECRET=********
SAUL_CLI_MODULE=wfm_bds
SAUL_CLI_TENANT=9f1c2e34-xxxx-xxxx-xxxx-xxxxxxxxxxxxRồi chạy thẳng saul snapshot / saul plan / saul apply, không cần export tay từng
biến. Muốn trỏ tới file khác .env (vd test nhiều tenant): saul snapshot --env-file
.env.staging. Trong CI, secrets nên vẫn đi qua biến môi trường của CI (GitHub Actions
env:/secrets:) — .env chỉ để tiện chạy local.
Ví dụ trên là cấu hình M2M. Chạy local dưới danh tính người dùng thì bỏ
SAUL_CLI_CLIENT_SECRET, đổi SAUL_CLI_CLIENT_ID sang client public của CLI, rồi
saul login một lần — từ đó backendUrl/tenant cũng có thể lấy luôn từ phiên đã lưu.
Đăng nhập bằng tài khoản người dùng
Mô hình giống az login / az account của Azure CLI:
saul login # mở trình duyệt tới trang đăng nhập (mặc định)
saul login --use-device-code # nhập mã trên máy khác (SSH/container, không có browser)
saul account show # đang là ai, tenant nào, token hết hạn khi nào
saul account list # danh sách tenant nhìn thấy được (saul.listTenants)
saul account set -t <uuid> # chọn tenant mặc định cho các lệnh sau
saul account sessions # mọi phiên đã lưu ở máy này (nhiều môi trường song song)
saul account refresh # kiểm tra/làm mới access token
saul logout [--all] # xoá phiên khỏi máyHai luồng đăng nhập
Mặc định — mở trình duyệt (authorization code + PKCE S256, giống az login). CLI dựng
một HTTP server loopback, mở trình duyệt tới /api/oidc/auth, identity redirect tới trang
đăng nhập …/interaction/login/<uid> (dùng được cả social login), rồi redirect ngược về
http://localhost:8400/callback; CLI đổi code lấy token và tự tắt server.
--use-device-code — in ra một mã, user mở trang …/api/oidc/device trên máy bất kỳ
để nhập. Dùng khi máy chạy CLI không mở được trình duyệt (SSH vào server, container, WSL),
hoặc khi cổng loopback bận. Không cần redirect_uri nào cả.
Ràng buộc cổng của luồng mặc định: client OIDC trong Directus không có cột
application_type → oidc-provider coi mọi client là loại web, và client web bị so
khớp redirect_uri chính xác từng ký tự, kể cả cổng. Nới lỏng "loopback cổng bất kỳ"
của RFC 8252 chỉ áp dụng cho client native, nên CLI không bốc cổng ngẫu nhiên như az
làm được: nó bind đúng SAUL_CLI_REDIRECT_URI (mặc định http://localhost:8400/callback),
và URI đó phải nằm nguyên văn trong redirect_uris của client. Cổng bận là lỗi cứng — CLI
cố tình không đổi cổng vì identity sẽ từ chối; lối thoát là --use-device-code.
Phiên đăng nhập được lưu ở đâu
~/.saul/ (đổi bằng SAUL_CLI_CONFIG_DIR), tách làm hai file, cả hai đều mode 0600:
profile.json— metadata không nhạy cảm: account đang active, issuer, client id, backend url, tenant đang chọn, tên/email.saul account showchỉ đọc file này.tokens.json— access / refresh / id token.
Một cặp (issuer, client id) là một account riêng, nên đăng nhập song song nhiều môi trường
(dev/staging/prod) không đè lên nhau — xem saul account sessions.
Access token tự được làm mới bằng refresh token khi còn dưới 60 giây là hết hạn, nên phiên
sống lâu mà không phải đăng nhập lại. Muốn có refresh token thì scope phải chứa
offline_access (đã nằm trong mặc định).
saul logout chỉ xoá credential ở máy này, cố tình không gọi revocation endpoint. Muốn
thu hồi thật thì thu hồi phía identity.
saul account set --tenant KHÔNG đổi tenant của token
Tenant nằm trong access token do client OIDC quyết định (junction tenant_apps trong
Directus), không phải do caller chọn — một client id ứng với đúng một tenant. account set
chỉ đặt tenant mà CLI dùng để lọc/ghi YAML authorization (xem mục SAUL_CLI_TENANT ở
trên); backend vẫn áp quyền theo tenant trong token. saul account show in cả hai giá trị và
cảnh báo khi chúng lệch nhau.
Muốn làm việc thật sự với tenant khác thì đăng nhập bằng client của tenant đó:
saul login --client-id <client-của-tenant-kia>.
OIDC client cho saul login — đã có sẵn
Saul backend tự tạo client saul-cli lúc khởi động nếu chưa có, nên thường bạn không
phải làm gì: chỉ cần SAUL_CLI_CLIENT_ID=saul-cli. Xem
apps/saul/server/src/saul/system/systemOidcClient.ts.
Seed chỉ tạo khi chưa có — chỉnh tay row đó (đổi cổng redirect chẳng hạn) sẽ không bị
lần restart sau ghi đè. Tắt hẳn bằng SEED_CLI_OIDC_CLIENT=false phía backend.
Client được seed không gắn tenant nào: tenant trong access token đến từ chính client
OIDC, nên client gắn tenant chỉ dùng được cho đúng một tenant. Không gắn ⇒ Saul suy tenant
từ module đang gọi, và một client id dùng chung cho mọi tenant. Client này không tự cấp
quyền gì — người đăng nhập vẫn phải có role cấp các permission saul.* tương ứng.
Tạo tay OIDC client (khi tự quản lý)
Nếu tắt seed hoặc muốn client riêng, tạo trong Directus (collection oidc_client) hoặc
Designer UI:
| Trường | Giá trị |
|---|---|
| client_id | vd saul-cli |
| token_endpoint_auth_method | none (public client — CLI không giữ được secret) |
| grant_types | ["authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code"] |
| response_types | ["code"] |
| redirect_uris | ["http://localhost:8400/callback"] — phải khớp SAUL_CLI_REDIRECT_URI nguyên văn, kể cả cổng |
| tenant (qua tenant_apps) | tenant mà CLI sẽ làm việc |
Dropdown grant_types trong Directus mới liệt kê sẵn client_credentials /
authorization_code / refresh_token — URN của device code phải gõ tay vào, không có
gì chặn cả. Identity đã bật sẵn features.deviceFlow nên phía provider không cần đổi gì.
Với client public, oidc-provider bắt buộc PKCE — CLI luôn gửi code_challenge S256 nên
không phải cấu hình thêm.
Tạo M2M client cho một tenant
M2M client (client_credentials) được quản lý qua oidc_client — nguồn duy nhất cho
mọi Client trong hệ thống. Tạo/chỉnh trong Directus (collection oidc_client) hoặc qua
Designer UI của Saul (khu vực quản trị OIDC Client), gán đúng tenant + policy cho
phép các function saul.DefinitionService.exportModuleYaml,
saul.DefinitionService.importModuleYaml, saul.AuthorizationService.exportPoliciesYaml,
saul.AuthorizationService.importPoliciesYaml, saul.AuthorizationService.exportPermissionsYaml,
saul.AuthorizationService.importPermissionsYaml, saul.TenantModuleService.listUnassignedModules,
saul.TenantModuleService.listTenantModules, saul.TenantModuleService.assignModuleToTenant
(3 function cuối phục vụ bước tự gán tenant của apply, xem mục Lệnh bên dưới). Không có
các quyền này, CLI sẽ nhận lỗi 403 (xem mục Troubleshooting).
Permission định danh theo
<module>.<service>.<function>. Policy tạo trước khi Saul chuyển sang định danh này (dạngsaul.exportModuleYaml) được scriptpnpm migrate:permission-servicephía backend tự đổi — xemapps/saul/docs/service-scoped-functions-migration.md.
Layout YAML local
saul/
module.yaml # exportModuleYaml — toàn bộ service/function/model/error
authorization/
policies.yaml # exportPoliciesYaml — flat map {policyId: {...}}
permissions.yaml # exportPermissionsYaml — flat map {permissionId: {...}}Không có file index/manifest — sự tồn tại của file chính là index (giống cách
apps/directus/snapshot/ split theo collection). module.yaml là nguyên văn chuỗi
YAML server trả về (CLI không tự parse/re-serialize, tránh format trôi khỏi những gì
importModuleYaml chấp nhận). Hai file authorization/*.yaml thì KHÔNG nguyên văn: CLI
lọc theo SAUL_CLI_TENANT và bỏ tiền tố <tenantId>: khỏi id trước khi ghi (xem mục
cấu hình ở trên) — nhờ vậy cùng một policies.yaml/permissions.yaml áp được cho
nhiều tenant khác nhau (dev/staging/prod), không hardcode UUID của một tenant cụ thể.
Principal không được sync qua CLI: backend hiện chỉ có getPrincipals/
createPrincipal, chưa có updatePrincipal hay export/import YAML idempotent cho
principal. Principal cũng gắn với danh tính/service-account thật của từng môi trường
hơn là cấu hình đi theo module, nên hợp lý để quản lý thủ công qua Designer UI.
Lệnh
# Đăng nhập một lần trên máy dev (bỏ qua nếu chạy CI với SAUL_CLI_CLIENT_SECRET)
saul login
# Kéo state hiện tại của module về YAML local
saul snapshot --module wfm_bds --tenant 9f1c2e34-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# Diff YAML local với remote — dùng làm gate trong PR (exit code 1 nếu có drift)
saul plan --module wfm_bds --tenant 9f1c2e34-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# Đẩy YAML local lên backend (hỏi xác nhận nếu không có --yes)
saul apply --module wfm_bds --tenant 9f1c2e34-xxxx-xxxx-xxxx-xxxxxxxxxxxx --yes
# Nâng cấp authorization/*.yaml local sang định danh module.service.function (offline)
saul migrate-authz # ghi lại file
saul migrate-authz --check # CI gate: exit code 1 nếu còn định danh cũSau khi Saul chuyển sang định danh module.service.function
Permission giờ có dạng <module>.<service>.<function>.<tên> (trước là
<module>.<function>.<tên>). YAML đã commit trong repo vẫn ở dạng cũ:
saul applyvẫn chạy: server tự nâng cấp permission cũ lúc import (chỉ khi tên function duy nhất trong module; trùng tên thì báo lỗi, không đoán).saul plansẽ báo drift cho tới khi file local được cập nhật.
Cập nhật một lần rồi commit, bằng một trong hai cách:
saul migrate-authz— offline, đọcmodule.yamllocal để biết function thuộc service nào. Permission cấp module (<module>.wsPush,<module>.wsAgent) giữ nguyên 2 phần.saul snapshot— kéo lại từ server (sau khi server đã chạy script migrate).
Chi tiết thứ tự rollout: apps/saul/docs/service-scoped-functions-migration.md.
--out-dir đổi thư mục YAML (mặc định saul/). Không truyền --module/--tenant thì
CLI đọc SAUL_CLI_MODULE/SAUL_CLI_TENANT (env hoặc .env), module/tenant trong
saul.config.yaml, rồi cuối cùng là tenant của phiên đang đăng nhập.
Nhóm lệnh auth (login / logout / account) xem mục
Đăng nhập bằng tài khoản người dùng.
saul apply tự gán module vào SAUL_CLI_TENANT: module.yaml chỉ chứa nội dung
service/function/model/error — việc module thuộc tenant nào là một dòng dữ liệu RIÊNG
(saul_module.tenant) mà importModuleYaml không đụng tới. Sau khi ghi definitions,
apply tự gọi saul.assignModuleToTenant để đảm bảo module thuộc đúng
SAUL_CLI_TENANT:
- Module mới/chưa gán tenant → gán vào
SAUL_CLI_TENANT. - Module đã thuộc đúng
SAUL_CLI_TENANT→ bỏ qua (idempotent). - Module đang thuộc một tenant KHÁC → dừng lại và báo lỗi (không tự ý gỡ/ghi đè) — tự gỡ (detach) qua Designer UI trước nếu thật sự muốn chuyển tenant.
Ví dụ workflow CI (GitHub Actions)
Đây chỉ là ví dụ minh hoạ trong docs — repo module tự thêm workflow thật, saul-cli
không đi kèm file .github/workflows sẵn.
name: saul
on:
pull_request:
paths: ["saul/**"]
push:
branches: [main]
paths: ["saul/**"]
jobs:
saul:
runs-on: ubuntu-latest
env:
SAUL_CLI_CLIENT_ID: ${{ secrets.SAUL_CLI_CLIENT_ID }}
SAUL_CLI_CLIENT_SECRET: ${{ secrets.SAUL_CLI_CLIENT_SECRET }}
SAUL_CLI_TENANT: ${{ vars.SAUL_CLI_TENANT }}
steps:
- uses: actions/checkout@v4
- run: npx @fidt/saul-cli plan
if: github.event_name == 'pull_request'
- run: npx @fidt/saul-cli apply --yes
if: github.ref == 'refs/heads/main'Troubleshooting
- Lỗi 403 / "không có quyền": M2M client chưa được gán policy cho các function
saul.exportModuleYaml/importModuleYaml/exportPoliciesYaml/importPoliciesYaml/exportPermissionsYaml/importPermissionsYaml, hoặc client đang cố ghi vào module hệ thốngsaul(read-only, không thể import qua CLI). - Token hết hạn / 401 giữa chừng: CLI cache access token trong bộ nhớ suốt một lần
chạy — mỗi lần chạy CLI vốn ngắn nên bình thường không đụng TTL. Ở chế độ user, token
được refresh tự động trước khi hết hạn; 401 lúc này nghĩa là phiên đã bị thu hồi phía
identity → chạy lại
saul login. Ở chế độ M2M, kiểm tra TTL access token của client. saul loginbáoinvalid_client(401): client đang là confidential (token_endpoint_auth_methodkhácnone) nên device endpoint bắt buộc xác thực client. Đặtnonecho client CLI, hoặc khaiSAUL_CLI_CLIENT_SECRET. Đừng đổi một client đang dùngclient_credentialssangnone— grant đó bắt buộc phải xác thực client, đổi là hỏng luồng M2M của nó. Tạo client riêng cho CLI thay vì dùng chung.saul loginbáounauthorized_client: client chưa bật granturn:ietf:params:oauth:grant-type:device_codetrongoidc_client(dropdown của Directus không liệt kê sẵn URN này — phải gõ tay), xem mục "Tạo OIDC client chosaul login".- Discovery trả endpoint
http://dù site là https: identity chạy sau reverse proxy mà proxy không truyềnX-Forwarded-Proto, nênoidc-providerdựng URL sai giao thức. CLI tự nâng các endpoint của chính issuer lên https để không gửi secret/code_verifierqua HTTP trần, nhưng đây là lỗi cấu hình phía server cần sửa ở proxy — mọi OIDC client chuẩn khác đều sẽ vấp phải. TENANT_MISMATCHkhi snapshot/apply: client đang đăng nhập gắn với tenant khác tenant sở hữu module. Tenant của token do junctiontenant_appsquyết định, không đổi bằngsaul account setđược — xem mục "saul account set --tenantKHÔNG đổi tenant của token".saul loginbáoredirect_uri_mismatch:SAUL_CLI_REDIRECT_URIchưa nằm nguyên văn trongredirect_uriscủa client. Identity so khớp chính xác cả cổng, không chấp nhận cổng khác — hoặc đăng ký đúng URI đó, hoặc dùngsaul login --use-device-codevốn không cần redirect URI.saul loginbáo cổng đang bận: CLI cố tình không tự đổi cổng (đổi là identity từ chối). Đóng tiến trình đang giữ cổng, hoặc dùng--use-device-code.- Trình duyệt không tự mở (SSH, container, WSL): CLI vẫn in URL ra để mở tay, nhưng
trang callback phải về được
localhost:8400của máy chạy CLI — qua SSH thì cần forward cổng (ssh -L 8400:localhost:8400 …), đơn giản hơn là dùng--use-device-code. - Đã
saul loginnhưng lệnh vẫn chạy dưới danh tính M2M:SAUL_CLI_CLIENT_SECRETvẫn còn trong env hoặc.env— có secret thì M2M luôn thắng (xem mục "Hai danh tính"). saul account listbáo thiếu backend URL: lệnh này phải gọi RPCsaul.listTenantsnên cầnSAUL_CLI_BACKEND_URL(hoặcbackendUrltrongsaul.config.yaml, hoặc backend đã lưu kèm phiên đăng nhập).saul applybáo thiếu file local: chưa chạysaul snapshotlần nào, hoặc--out-dirkhông khớp với lúc snapshot.saul snapshotraauthorization/policies.yaml/permissions.yamlrỗng dù Designer UI có policy: saiSAUL_CLI_TENANT(không phải UUID của tenant đang mở trong Designer), hoặc policy đó đang là row toàn cục (SaulAdmin/PublicPolicyhệ thống — không sync qua CLI, xem mục Layout YAML local).saul applybáo "Module ... đang thuộc một tenant KHÁC": module đã được gán cho tenant khácSAUL_CLI_TENANTtừ trước (qua Designer UI hoặc lần apply trước với tenant khác). CLI cố tình KHÔNG tự gỡ/ghi đè — vào Designer UI, gỡ (detach) module khỏi tenant hiện tại rồi apply lại, nếu đúng là bạn muốn chuyển tenant.
