local-boot
Boot the Horizon UI dev env (BFF + UI) against a local OAP or the public Apache demo OAP. Uses the repo's committed, env-driven horizon.yaml — the same config the image ships — and injects the OAP target + dev users purely via HORIZON_* environment variables. Handles the apps/bff cwd / HORIZON_CONFIG gotcha and the demo OAP password (kept out of git via the cached oap-demo-env-auth.key). Also covers booting with the AI assistant enabled (Amazon Bedrock / DeepSeek), keyed from the git-ignored bedrocks-api.key.
- License 许可
- Apache-2.0
- 最近更新
- 2026-11-11
- Stars
- ★ 23
- Forks
- 10
01 前言 Front Matter
- name
- local-boot
- description
- Boot the Horizon UI dev env (BFF + UI) against a local OAP or the public Apache demo OAP. Uses the repo's committed, env-driven horizon.yaml — the same config the image ships — and injects the OAP target + dev users purely via HORIZON_* environment variables. Handles the apps/bff cwd / HORIZON_CONFIG gotcha and the demo OAP password (kept out of git via the cached oap-demo-env-auth.key). Also covers booting with the AI assistant enabled (Amazon Bedrock / DeepSeek), keyed from the git-ignored bedrocks-api.key.
- user-invocable
- true
02 资源文件 191
- 资产文件assets/JetBrainsMono-OFL.txt4.3 KB
- 资产文件assets/template.html768.9 KB
- 脚本bin/archify.mjs282.0 KB
- 脚本bin/delivery-update.mjs3.6 KB
- 脚本bin/finalize.mjs48.9 KB
- 脚本bin/open-artifact.mjs4.0 KB
- 脚本bin/preview.mjs35.8 KB
- 脚本bin/recover-output.mjs1.3 KB
- 脚本bin/visual-check.mjs117.7 KB
- 参考文档brand-marks/catalog.json13.8 KB
- 参考文档brand-marks/README.md1.4 KB
- 脚本delta/architecture-delta.mjs77.2 KB
- 参考文档dev-ldap.json581 B
- 参考文档dev-users.json636 B
- 参考文档examples/agent-run.lifecycle.json3.2 KB
- 参考文档examples/agent-tool-call.workflow.json4.8 KB
- 参考文档examples/archify-dev-activity.timeline.json4.4 KB
- 参考文档examples/archify-repository.tree.json4.3 KB
- 参考文档examples/async-job-roundtrip.sequence.json3.7 KB
- 参考文档examples/brand-aware-delivery.architecture.json2.7 KB
- 参考文档examples/cache-miss-request.sequence.json3.7 KB
- 参考文档examples/checkout-platform.base.architecture.json2.2 KB
- 参考文档examples/checkout-platform.head.architecture.json2.4 KB
- 参考文档examples/checkout-request.waterfall.json1.2 KB
- 参考文档examples/class-payment-processors-rendered.html854.5 KB
- 参考文档examples/class-payments-rendered.html821.1 KB
- 参考文档examples/dataflow-product-analytics.html824.1 KB
- 参考文档examples/delivery.explainer.json4.5 KB
- 参考文档examples/deployment-release.lifecycle.json4.1 KB
- 参考文档examples/erd-orders-rendered.html841.1 KB
- 参考文档examples/event-stream.dataflow.json4.7 KB
- 参考文档examples/example-rebuild.waterfall.json4.5 KB
- 参考文档examples/holiday-planning.workflow.json2.9 KB
- 参考文档examples/incident-response.workflow.json4.6 KB
- 参考文档examples/lifecycle-agent-run.html827.8 KB
- 参考文档examples/locales/de.json27.8 KB
- 参考文档examples/locales/fr.json28.6 KB
- 参考文档examples/locales/fr.partial.json959 B
- 参考文档examples/locales/it.json28.1 KB
- 参考文档examples/locales/ja.json31.7 KB
- 参考文档examples/locales/pt.json27.8 KB
- 参考文档examples/locales/ru.json36.1 KB
- 参考文档examples/orders.erd.json7.1 KB
- 参考文档examples/payment-incident.timeline.json1.4 KB
- 参考文档examples/payment-platform.tree.json1.2 KB
- 参考文档examples/payment-processors.class.json7.1 KB
- 参考文档examples/payments.class.json2.8 KB
- 参考文档examples/product-analytics.dataflow.json4.9 KB
- 参考文档examples/production-deployment.architecture.json4.6 KB
- 参考文档examples/release-delivery.workflow.json4.4 KB
- 参考文档examples/sequence-cache-miss-request.html821.8 KB
- 参考文档examples/subscription-billing-rendered.html863.5 KB
- 参考文档examples/subscription-billing.erd.json11.5 KB
- 参考文档examples/timeline-archify-dev-activity-rendered.html846.2 KB
- 参考文档examples/timeline-payment-incident-rendered.html814.5 KB
- 参考文档examples/tree-archify-repository-rendered.html856.8 KB
- 参考文档examples/tree-payment-platform-rendered.html820.0 KB
- 参考文档examples/waterfall-checkout-request-rendered.html813.9 KB
- 参考文档examples/waterfall-example-rebuild-rendered.html843.0 KB
- 参考文档examples/web-app-rendered.html826.4 KB
- 参考文档examples/web-app.architecture.json3.1 KB
- 参考文档examples/workflow-agent-tool-call-rendered.html831.3 KB
- 脚本explainer/browser-check.mjs30.0 KB
- 脚本explainer/charts.mjs5.9 KB
- 脚本explainer/check.mjs14.5 KB
- 脚本explainer/diagrams.mjs37.8 KB
- 脚本explainer/diff.mjs8.3 KB
- 脚本explainer/inline.mjs8.5 KB
- 脚本explainer/layout.mjs2.5 KB
- 脚本explainer/page-bundle.mjs1.1 KB
- 脚本explainer/parse.mjs26.6 KB
- 脚本explainer/patch.mjs11.8 KB
- 脚本explainer/prepare.mjs9.6 KB
- 其他explainer/reader.css46.1 KB
- 脚本explainer/reader.js50.5 KB
- 脚本explainer/render.mjs33.2 KB
- 脚本explainer/revision.mjs1.9 KB
- 脚本explainer/sources.mjs20.4 KB
- 脚本explainer/validate.mjs14.2 KB
- 脚本explainer/writing.mjs11.1 KB
- 其他ldap-seed.ldif1.1 KB
- 许可证LICENSE11.1 KB
- 许可证LICENSE-MIT1.1 KB
- 参考文档locales/ar.json33.0 KB
- 参考文档locales/en.json26.7 KB
- 参考文档locales/es.json28.8 KB
- 参考文档locales/ko.json30.1 KB
- 参考文档locales/manifest.json293 B
- 参考文档locales/README.md2.2 KB
- 参考文档locales/zh-CN.json26.4 KB
- 参考文档locales/zh-TW.json26.5 KB
- 脚本migrations/workflow-v2.mjs9.0 KB
- 其他NOTICE1.3 KB
- 参考文档package-lock.json3.2 KB
- 参考文档package.json1.5 KB
- 参考文档rbac-demo/docker-compose.yml5.1 KB
- 脚本rbac-demo/down.sh1.2 KB
- 参考文档rbac-demo/otel-collector-config.yaml1.2 KB
- 参考文档rbac-demo/roles.json747 B
- 脚本rbac-demo/up.sh4.2 KB
- 参考文档rbac-demo/users.json1.5 KB
- 脚本recipes/scenarios.mjs40.5 KB
- 参考文档references/architecture-layout-repair.md4.3 KB
- 参考文档references/authoring-contract.md41.7 KB
- 参考文档references/authoring-defaults.md12.6 KB
- 参考文档references/brand-marks.md5.6 KB
- 参考文档references/delivery-contract.md48.7 KB
- 参考文档references/explainer-authoring.md18.5 KB
- 参考文档references/repository-authoring.md7.8 KB
- 参考文档references/update-awareness.md1.9 KB
- 参考文档references/viewer-runtime.md5.6 KB
- 脚本renderers/architecture/grid.mjs2.0 KB
- 脚本renderers/architecture/labels.mjs155 B
- 脚本renderers/architecture/render-architecture.mjs57.2 KB
- 脚本renderers/architecture/routing.mjs72.7 KB
- 参考文档renderers/class/README.md6.6 KB
- 脚本renderers/class/render-class.mjs52.3 KB
- 脚本renderers/dataflow/dataflow-compiler.mjs89.1 KB
- 脚本renderers/dataflow/joint-label-routing.mjs10.6 KB
- 脚本renderers/dataflow/label-capacity.mjs4.3 KB
- 参考文档renderers/dataflow/README.md9.0 KB
- 脚本renderers/dataflow/render-dataflow.mjs960 B
- 脚本renderers/erd/grid.mjs6.9 KB
- 参考文档renderers/erd/README.md14.2 KB
- 脚本renderers/erd/render-erd.mjs78.6 KB
- 脚本renderers/explainer/render-explainer.mjs3.2 KB
- 参考文档renderers/lifecycle/README.md6.0 KB
- 脚本renderers/lifecycle/render-lifecycle.mjs59.9 KB
- 参考文档renderers/sequence/README.md8.5 KB
- 脚本renderers/sequence/render-sequence.mjs55.8 KB
- 脚本renderers/shared/atomic-output.mjs68.7 KB
- 脚本renderers/shared/automatic-labels.mjs14.4 KB
- 脚本renderers/shared/brand-marks.mjs28.3 KB
- 脚本renderers/shared/cli.mjs18.6 KB
- 脚本renderers/shared/desktop-readability.mjs6.8 KB
- 脚本renderers/shared/diagnostics.mjs8.1 KB
- 脚本renderers/shared/engineering-profiles.mjs6.8 KB
- 脚本renderers/shared/generated-brand-marks.mjs159.8 KB
- 脚本renderers/shared/generated-validators.mjs996.1 KB
- 脚本renderers/shared/geometry.mjs87.8 KB
- 脚本renderers/shared/i18n.mjs13.5 KB
- 脚本renderers/shared/layout-report.mjs1.5 KB
- 脚本renderers/shared/legend.mjs9.0 KB
- 脚本renderers/shared/node-card-style.mjs2.4 KB
- 脚本renderers/shared/output-path.mjs18.6 KB
- 脚本renderers/shared/owned-temp-directory.mjs3.1 KB
- 脚本renderers/shared/path-semantics.mjs31.6 KB
- 脚本renderers/shared/portable-path.mjs12.2 KB
- 脚本renderers/shared/repository-evidence.mjs19.9 KB
- 脚本renderers/shared/repository-location.mjs4.2 KB
- 脚本renderers/shared/route-quality.mjs26.3 KB
- 脚本renderers/shared/sidecar-path.mjs1.2 KB
- 脚本renderers/shared/software-notices.mjs506 B
- 脚本renderers/shared/spatial-grid.mjs2.4 KB
- 脚本renderers/shared/svg-path-analysis.mjs5.3 KB
- 脚本renderers/shared/text-fit.mjs5.1 KB
- 脚本renderers/shared/utils.mjs12.7 KB
- 脚本renderers/shared/validator.mjs3.3 KB
- 参考文档renderers/timeline/README.md4.0 KB
- 脚本renderers/timeline/render-timeline.mjs29.6 KB
- 参考文档renderers/tree/README.md4.2 KB
- 脚本renderers/tree/render-tree.mjs29.9 KB
- 参考文档renderers/waterfall/README.md3.4 KB
- 脚本renderers/waterfall/render-waterfall.mjs22.5 KB
- 参考文档renderers/workflow/README.md17.3 KB
- 脚本renderers/workflow/render-workflow.mjs1.6 KB
- 脚本renderers/workflow/workflow-compiler.mjs244.3 KB
- 脚本renderers/workflow/workflow-migration-geometry.mjs6.2 KB
- 脚本renderers/workflow/workflow-placement-repair.mjs4.7 KB
- 参考文档schemas/architecture.schema.json5.9 KB
- 参考文档schemas/class.schema.json7.9 KB
- 参考文档schemas/common.schema.json5.6 KB
- 参考文档schemas/dataflow.schema.json6.2 KB
- 参考文档schemas/erd.schema.json9.1 KB
- 参考文档schemas/explainer.schema.json18.8 KB
- 参考文档schemas/lifecycle.schema.json5.2 KB
- 参考文档schemas/README.md14.7 KB
- 参考文档schemas/sequence.schema.json6.0 KB
- 参考文档schemas/timeline.schema.json3.9 KB
- 参考文档schemas/tree.schema.json3.1 KB
- 参考文档schemas/waterfall.schema.json3.4 KB
- 参考文档schemas/workflow.schema.json9.9 KB
- 脚本scripts/check-render-output.mjs66.9 KB
- 脚本scripts/check-update.mjs57.9 KB
- 脚本scripts/delivery-update-child.mjs1.3 KB
- 脚本scripts/generate-brand-marks.mjs5.0 KB
- 脚本scripts/generate-validators.mjs4.1 KB
- 脚本scripts/render-examples.mjs2.0 KB
- 脚本scripts/update-contract.mjs6.7 KB
- 参考文档skill-release.json260 B
- 参考文档THIRD_PARTY_NOTICES.md5.5 KB
03 SKILL.md 正文
Boot the Horizon UI local dev env
There is one config file: the repo's committed horizon.yaml at the
repo root. Every field in it is a ${HORIZON_…:default} token, so dev boots
use the SAME file the Docker image ships and override only what they need via
environment variables — there are no per-scenario config files anymore.
Two committed helpers live next to this file (throwaway dev values, safe to commit):
dev-users.json— the four local login users (viewer/maintainer/operator/admin, password == username), as a single-line JSON array forHORIZON_AUTH_LOCAL_USERS.dev-ldap.json— the test-OpenLDAP config forHORIZON_AUTH_LDAP(single line).
The stack: BFF (Fastify) on :8081, UI (Vite) on :9091 proxying
/api → :8081. Open http://127.0.0.1:9091 (use the IPv4 literal, not
localhost — see the proxy/IPv4 section). Both ports are overridable — see
"Custom ports".
JSON env values must be SINGLE-LINE.
HORIZON_AUTH_LOCAL_USERS,HORIZON_OAP_AUTH,HORIZON_AUTH_LDAP, etc. are spliced into the YAML before parsing; a multi-line JSON value breaks the parse (YAMLParseError: Flow sequence … must be sufficiently indented). Thedev-*.jsonfiles are already single-line — keep them that way.
Environment variables (the dev contract)
Local dev runs as two processes / two ports: the BFF (Fastify, owns /api)
and Vite (serves the UI, proxies /api → BFF). The BFF reads HORIZON_* via the
config loader's ${VAR:default} interpolation; Vite reads BFF_PORT +
UI_DEV_PORT directly in apps/ui/vite.config.ts.
| Variable | Default | Used by | Purpose |
|---|---|---|---|
HORIZON_CONFIG |
(none — required) | BFF | Absolute path to the config. Always "$REPO/horizon.yaml". A bare ./horizon.yaml resolves under apps/bff/ — see "The one gotcha". |
HORIZON_SERVER_PORT |
8081 |
BFF | BFF listen port (server.port). For a custom port set this AND BFF_PORT to the same value. |
BFF_PORT |
8081 |
UI | Vite's /api proxy target. Must match HORIZON_SERVER_PORT. |
UI_DEV_PORT |
9091 |
UI | Vite listen port. Dev-only. |
HORIZON_OAP_QUERY_URL |
http://127.0.0.1:12800 |
BFF | OAP GraphQL endpoint. |
HORIZON_OAP_ADMIN_URL |
http://127.0.0.1:17128 |
BFF | OAP admin REST endpoint. Often a different port from GraphQL (the demo splits on :17128) — if the BFF logs UITemplate 404, this is wrong. |
HORIZON_OAP_ZIPKIN_URL |
http://127.0.0.1:9412/zipkin |
BFF | Zipkin query endpoint (Zipkin trace layer). |
HORIZON_OAP_TIMEOUT_MS |
15000 |
BFF | OAP request timeout. Lower it (4000) when previewing the "OAP unreachable" landing block so errors surface fast. |
HORIZON_OAP_AUTH |
(none) | BFF | OAP basic-auth as JSON, e.g. {"username":"admin","password":"…"}. The demo needs it (password from oap-demo-env-auth.key). |
HORIZON_AUTH_LOCAL_USERS |
[] |
BFF | Local login users (JSON array, single-line). Use $(cat dev-users.json). |
HORIZON_AUTH_TOKENS_FILE |
(none) | BFF | Path to an API-token file — the credential for non-browser callers (scripts, CI, MCP clients). Point it at "$SK/dev-tokens.json", which is git-ignored and not committed: mint your own the first time with pnpm --filter @skywalking-horizon-ui/bff cli:token admin --label dev, then paste the printed entry into that file. A working token must never be committed, even a dev one — the repo is public. |
HORIZON_AUTH_BACKEND |
local |
BFF | local or ldap. |
HORIZON_AUTH_LDAP |
(none) | BFF | LDAP config (JSON, single-line) when backend=ldap. Use $(cat dev-ldap.json). |
HORIZON_TEMPLATES_MODE |
live |
BFF | live (seed + read ui_template) or readonly (render the bundle, config read-only). |
RBAC is left to the built-in role defaults (the horizon.yaml token
roles: ${HORIZON_RBAC_ROLES:null} falls through to them), so no roles env var
is needed for dev.
The one gotcha that bites every time
The BFF dev script is tsx watch src/server.ts and pnpm runs it with cwd =
apps/bff. The config path defaults to ./horizon.yaml, resolved relative to
cwd — so a bare ./horizon.yaml points at the non-existent apps/bff/horizon.yaml
(NOT the repo-root one), and the loader silently falls back to schema defaults
with zero users (every login then fails "invalid credentials").
Always pass HORIZON_CONFIG as an ABSOLUTE path: "$REPO/horizon.yaml".
Proxy + IPv4 (the second gotcha)
Two things make the UI look "not accessible" even when Vite is running:
- Vite binds IPv6
[::1]by default, so127.0.0.1:9091(IPv4) has nothing and many browsers / curl fail. Force IPv4 with--host 127.0.0.1. Notepnpm --filter ui run dev -- --host …does NOT forward the flag — run the Vite binary directly (apps/ui/node_modules/.bin/vite --host 127.0.0.1). - A local proxy (ClashX / v2ray etc. —
http_proxy/https_proxy/all_proxypointing at127.0.0.1:<port>) intercepts loopback and returns502. Detect and bypass it for the dev hosts.
Detect a local proxy before booting:
env | grep -iE '^(http_proxy|https_proxy|all_proxy)=' && echo "local proxy detected — will bypass loopback"
The browser uses its OWN proxy settings (not the shell's), so the developer must also let 127.0.0.1 / localhost go direct (ClashX "bypass localhost" / system proxy no-proxy list). The env-level bypass below only fixes CLI tools and Vite's own fetches.
The stale-process trap (why a config switch "didn't take")
tsx watch keeps the old BFF alive, so a freshly launched BFF silently dies on EADDRINUSE: 127.0.0.1:8081 while the OLD process keeps serving the OLD env. Symptom: you switch demo↔local, everything looks fine, but the UI still shows the previous OAP's data.
pkill -f "tsx watch src/server.ts"may miss the actual listener — alsopkill -f "tsx/dist/cli.mjs watch", and verify the port is free withlsofbefore relaunching (loop until free; the watcher respawns a child).- After boot, confirm which OAP the LIVE process uses — don't trust that your new process won the port:
curl -s --noproxy '*' -b /tmp/sw.cookies "http://127.0.0.1:8081/api/oap/config" | grep -oE '"adminUrl":"[^"]*"' # local => http://127.0.0.1:17128 ; demo => https://demo.skywalking.apache.org:17128
Boot against the public demo OAP
The demo OAP needs basic-auth (network username admin). The password is NOT
committed — it lives in oap-demo-env-auth.key next to this file (git-ignored via
the .claude/skills/local-boot/*.key rule). Source it; if missing, ask the developer and
recreate it (one line, the password only).
REPO="$(git rev-parse --show-toplevel)"
SECRET="$REPO/.claude/skills/local-boot/oap-demo-env-auth.key"
if [ -s "$SECRET" ]; then
OAP_PASSWORD="$(cat "$SECRET")"; export OAP_PASSWORD
else
read -rsp "Demo OAP password: " OAP_PASSWORD && export OAP_PASSWORD && echo
printf '%s\n' "$OAP_PASSWORD" > "$SECRET" && chmod 600 "$SECRET" # cache for next boot
fi
# Kill prior dev servers + confirm the port is free (see stale-process trap).
pkill -f "tsx watch src/server.ts" 2>/dev/null
pkill -f "tsx/dist/cli.mjs watch" 2>/dev/null; pkill -f vite 2>/dev/null
until ! lsof -nP -iTCP:8081 -sTCP:LISTEN >/dev/null 2>&1; do sleep 1; done
# BFF — repo horizon.yaml + everything via env. JSON values are single-line.
SK="$REPO/.claude/skills/local-boot"
HORIZON_CONFIG="$REPO/horizon.yaml" \
HORIZON_OAP_QUERY_URL=https://demo.skywalking.apache.org:12800 \
HORIZON_OAP_ADMIN_URL=https://demo.skywalking.apache.org:17128 \
HORIZON_OAP_ZIPKIN_URL=https://demo.skywalking.apache.org:9412/zipkin \
HORIZON_OAP_AUTH="{\"username\":\"admin\",\"password\":\"$OAP_PASSWORD\"}" \
HORIZON_AUTH_LOCAL_USERS="$(cat "$SK/dev-users.json")" \
HORIZON_AUTH_TOKENS_FILE="$SK/dev-tokens.json" \
pnpm --filter @skywalking-horizon-ui/bff run dev &
# UI — IPv4 host + loopback proxy bypass (run the binary directly so --host applies).
( cd "$REPO/apps/ui" && \
env -u http_proxy -u https_proxy -u all_proxy -u HTTP_PROXY -u HTTPS_PROXY -u ALL_PROXY \
no_proxy="localhost,127.0.0.1,::1" NO_PROXY="localhost,127.0.0.1,::1" \
node_modules/.bin/vite --host 127.0.0.1 & )
Then open http://127.0.0.1:9091 and log in as admin / admin. To boot in
read-only template mode, add HORIZON_TEMPLATES_MODE=readonly to the BFF line.
Boot with the AI assistant enabled (Bedrock / DeepSeek)
The AI assistant (the ai: block in horizon.yaml) is off by default — the
launcher stays hidden until the feature is enabled AND a usable provider is
configured. To exercise it in dev, add the HORIZON_AI_* env vars below to any
boot above — usually the demo boot, so there is live data to investigate.
The dev provider is Amazon Bedrock running DeepSeek (deepseek.v3.2,
region us-west-2). The Bedrock key is a secret kept next to this file:
bedrocks-api.key— a BedrockABSK…bearer token, one line, git-ignored via the.claude/skills/local-boot/*.keyrule (same rule as the OAP password). If missing, mint one in the AWS console (Bedrock → API keys) and drop it here.
Our config field is HORIZON_AI_API_KEY — the BFF passes it to the SDK
explicitly as bedrockBearerToken. Do NOT set AWS_BEARER_TOKEN_BEDROCK:
that is the AWS SDK's own env var, which our code deliberately bypasses so
concurrent requests stay isolated. The only bedrock-specific
extra is HORIZON_AI_REGION (it falls back to AWS_REGION when blank). Our
bedrock path REQUIRES the bearer key and does not use the AWS SSO / IAM
credential chain — a missing key is a clean 503, not an SSO prompt.
| Variable | Value (dev) | Purpose |
|---|---|---|
HORIZON_AI_ENABLED |
true |
Master switch (default false). |
HORIZON_AI_PROVIDER |
bedrock |
Transport — openai-compatible (default) or bedrock. |
HORIZON_AI_MODEL |
deepseek.v3.2 |
Bedrock model / inference-profile id. |
HORIZON_AI_REGION |
us-west-2 |
Bedrock-only extra; falls back to AWS_REGION. |
HORIZON_AI_API_KEY |
$(cat bedrocks-api.key) |
Secret ABSK bearer. Redacted from logs. |
Add these lines to the demo boot's BFF invocation (keep the SECRET / pkill / port-free steps from the demo section):
SK="$REPO/.claude/skills/local-boot"
HORIZON_CONFIG="$REPO/horizon.yaml" \
HORIZON_OAP_QUERY_URL=https://demo.skywalking.apache.org:12800 \
HORIZON_OAP_ADMIN_URL=https://demo.skywalking.apache.org:17128 \
HORIZON_OAP_ZIPKIN_URL=https://demo.skywalking.apache.org:9412/zipkin \
HORIZON_OAP_AUTH="{\"username\":\"admin\",\"password\":\"$OAP_PASSWORD\"}" \
HORIZON_AUTH_LOCAL_USERS="$(cat "$SK/dev-users.json")" \
HORIZON_AI_ENABLED=true \
HORIZON_AI_PROVIDER=bedrock \
HORIZON_AI_MODEL=deepseek.v3.2 \
HORIZON_AI_REGION=us-west-2 \
HORIZON_AI_API_KEY="$(cat "$SK/bedrocks-api.key")" \
pnpm --filter @skywalking-horizon-ui/bff run dev &
Verify readiness after login — expect ready:true:
curl -s --noproxy '*' -b /tmp/sw.cookies "http://127.0.0.1:8081/api/ai/config"
Then the floating AI Assistant launcher appears on the right edge; the chat
streams an answer with inline figures (open /ai for the full page). On-demand
pod logs work when the OAP itself runs in Kubernetes (the public demo does) and
its enableOnDemandPodLog is on — the assistant's fetch_pod_logs tool and the
per-layer Pod Logs tab both read them live.
Boot with single sign-on (Google and/or GitHub)
Horizon can hand the login to identity providers (auth.sso). The local dev
config is split three ways, mirroring the split the config schema is heading
toward — a provider says how to authenticate; roles say what you get, and are
not per-provider:
| File | Holds |
|---|---|
gmail_oauth.config |
Google provider: client id/secret + endpoints. Connection only. |
github_oauth.config |
GitHub provider: client id/secret + endpoints, namePath: login, emailsEndpoint. Connection only. |
oauth.config |
ONE role table for every provider: defaultRoles, roleByEmail, roleByDomain. Goes into auth.sso.roles verbatim. |
All three are git-ignored and must never be committed: this directory is
deny-by-default in .gitignore, with only SKILL.md, dev-users.json,
dev-ldap.json and ldap-seed.ldif allow-listed.
Why one role table. Roles resolve from the EMAIL ADDRESS alone — a token carries no provider — so a per-provider role table is a promise the architecture cannot keep. Keeping the addresses in one file also avoids the failure that cost a debugging session: two providers whose tables disagreed about the same address once resolved to NO roles, and an API token with no roles is refused, so adding a second provider logged every agent out with an unexplained 401.
Use the PACKAGED build, on ONE port. The dev BFF is API-only, so the
post-login redirect lands on an SPA route it does not serve and you get a 404
that looks like a login failure. dist/server.js serves the UI and the API
together, exactly as a real deployment does.
REPO="$(git rev-parse --show-toplevel)"; SK="$REPO/.claude/skills/local-boot"
pnpm package # if dist/ is stale
# Compose the providers with the shared role table. Drop either provider from
# the list to boot with just one.
export HORIZON_AUTH_SSO=$(python3 -c "
import json
roles = json.load(open('$SK/oauth.config'))
prov = lambda f: json.load(open(f))['horizon']
print(json.dumps({
'providers': [prov('$SK/gmail_oauth.config'), prov('$SK/github_oauth.config')],
'roles': {k: roles[k] for k in ('defaultRoles', 'roleByEmail', 'roleByDomain') if k in roles},
}))")
cd "$REPO/dist" && NODE_USE_ENV_PROXY=1 \
NO_PROXY="demo.skywalking.apache.org,127.0.0.1,localhost,::1" \
HORIZON_CONFIG=./horizon.yaml \
HORIZON_SERVER_HOST=127.0.0.1 HORIZON_SERVER_PORT=9091 \
HORIZON_PUBLIC_URL=http://127.0.0.1:9091 \
HORIZON_OAP_QUERY_URL=https://demo.skywalking.apache.org:12800 \
HORIZON_OAP_ADMIN_URL=https://demo.skywalking.apache.org:17128 \
HORIZON_OAP_AUTH="{\"username\":\"admin\",\"password\":\"$(cat "$SK/oap-demo-env-auth.key")\"}" \
HORIZON_AUTH_LOCAL_USERS="$(cat "$SK/dev-users.json")" \
HORIZON_AUTH_TOKENS_FILE="$SK/dev-tokens.json" \
HORIZON_TEMPLATES_MODE=readonly \
node server.js
Open http://127.0.0.1:9091. One provider renders as a button; two render
as a themed picker with an arrow to continue.
Things that each cost a debugging cycle:
NODE_USE_ENV_PROXY=1plusNO_PROXY. Node'sfetchignoreshttp_proxy, so behind a proxy OIDC discovery fails withprovider_unreachable. But the proxy also breaks the demo OAP's TLS, so the OAP hosts have to be excluded — you need both, not either.HORIZON_PUBLIC_URLmust byte-match a registered redirect URI.localhostand127.0.0.1are DIFFERENT registrations, and so is a different port. Register<publicUrl>/api/auth/oidc/callback. Google takes a list; GitHub takes up to 10 per OAuth App, so one app can serve dev and prod.- GitHub needs
emailsEndpoint./userreportsemail: nullfor any account without a PUBLIC profile address, which is the default. The address lives at/user/emails, and only entries markedverified: trueare accepted — Gitee spells verificationstate, so its list cannot be used here at all. templates.mode=readonlyskips the OAP template seed. Drop it to exercise the live template store.
Failures land back on /login?sso_error=<reason> and the page turns that into a
sentence; the provider's own words stay in the server log deliberately.
Boot with the OAuth authorization server (agent login from a CLI)
oauth.enabled makes Horizon issue its own tokens, so an MCP client can send
its operator through the browser instead of being handed one. It is OFF by
default and needs a real secret:
HORIZON_OAUTH_ENABLED=true \
HORIZON_OAUTH_SIGNING_KEY="$(openssl rand -base64 32)" \
# …plus the SSO boot above
The key must be 32+ characters. Anything shorter is treated as no key at
all: the authorization server stays OFF, its endpoints answer 404, and the boot
warning names the length it got. It signs every token, authorization code and
client registration, and none of them are stored — so a guessable key mints
valid credentials for any user and nothing can tell the difference. A throwaway
like local-test-key will NOT start it any more.
Verify the whole flow against the real client:
codex -c 'mcp_servers.hz.url="http://127.0.0.1:9091/api/mcp"' mcp login hz
It prints an authorize URL, registers itself dynamically, and after you approve
the consent screen reports Successfully logged in. The GUI has no OAuth path —
only the CLI does — so a desktop client needs an API token instead.
Boot against a local / remote no-auth OAP
Same recipe, different OAP env vars (no HORIZON_OAP_AUTH for a no-auth OAP):
REPO="$(git rev-parse --show-toplevel)"; SK="$REPO/.claude/skills/local-boot"
pkill -f "tsx watch src/server.ts" 2>/dev/null; pkill -f "tsx/dist/cli.mjs watch" 2>/dev/null; pkill -f vite 2>/dev/null
until ! lsof -nP -iTCP:8081 -sTCP:LISTEN >/dev/null 2>&1; do sleep 1; done
# LOCAL OAP (defaults already point at 127.0.0.1, so only users are needed):
HORIZON_CONFIG="$REPO/horizon.yaml" \
HORIZON_AUTH_LOCAL_USERS="$(cat "$SK/dev-users.json")" \
pnpm --filter @skywalking-horizon-ui/bff run dev &
# REMOTE no-auth OAP (GraphQL 12800, admin split on 17128):
HORIZON_CONFIG="$REPO/horizon.yaml" \
HORIZON_OAP_QUERY_URL=http://oap.dev.example:12800 \
HORIZON_OAP_ADMIN_URL=http://oap.dev.example:17128 \
HORIZON_AUTH_LOCAL_USERS="$(cat "$SK/dev-users.json")" \
pnpm --filter @skywalking-horizon-ui/bff run dev &
# UNREACHABLE-OAP landing-block preview (short timeout so errors surface fast):
HORIZON_CONFIG="$REPO/horizon.yaml" \
HORIZON_OAP_QUERY_URL=http://127.0.0.1:12801 HORIZON_OAP_ADMIN_URL=http://127.0.0.1:12801 \
HORIZON_OAP_TIMEOUT_MS=4000 \
HORIZON_AUTH_LOCAL_USERS="$(cat "$SK/dev-users.json")" \
pnpm --filter @skywalking-horizon-ui/bff run dev &
Custom ports (two parallel envs)
Defaults are BFF :8081, UI :9091. To run a second Horizon side-by-side, set
the BFF port via HORIZON_SERVER_PORT (the config's server.port token) AND
BFF_PORT (Vite's proxy target) to the same value, plus UI_DEV_PORT:
HORIZON_SERVER_PORT=10081 BFF_PORT=10081 UI_DEV_PORT=10091 \
HORIZON_CONFIG="$REPO/horizon.yaml" \
HORIZON_AUTH_LOCAL_USERS="$(cat "$SK/dev-users.json")" \
pnpm --filter @skywalking-horizon-ui/bff run dev &
( cd "$REPO/apps/ui" && BFF_PORT=10081 UI_DEV_PORT=10091 \
env -u http_proxy -u https_proxy -u all_proxy \
node_modules/.bin/vite --host 127.0.0.1 & )
Prod is single-port. The BFF serves the built UI as static files, so
UI_DEV_PORT is meaningless outside Vite; only HORIZON_SERVER_PORT matters.
Boot the layer / group grant demo (two service groups in one layer)
The public demo OAP has one service group per layer, so it cannot show a
group-limited role beside a whole-layer one. rbac-demo/ stands up a local
OAP (BanyanDB, the e2e-pinned images) with payments::checkout,
payments::ledger and risk::scorer in GENERAL, the OAP's own metrics in
SO11Y_OAP (a Platform monitoring layer), and a BFF on :10083 + Vite on
:10093 with one user per grant shape — password == username:
| User | Grant | Sees |
|---|---|---|
viewer |
built-in | every layer but Platform monitoring; both GENERAL groups |
maintainer |
built-in | every layer, Platform monitoring included |
payments |
…:read@GENERAL[payments] |
the payments entry of GENERAL only |
risk |
…:read@GENERAL[risk] |
the risk entry only; its map still names payments::ledger as a neighbour |
general |
…:read@GENERAL |
the whole layer, both entries |
so11y |
metrics:read@SO11Y_OAP |
that operate layer, without cluster:read |
mixed |
metrics on payments, logs on risk, plain alarms:read |
both entries, metrics on one side only |
.claude/skills/local-boot/rbac-demo/up.sh # compose up → BFF → Vite → split GENERAL by group
.claude/skills/local-boot/rbac-demo/down.sh # stop both processes, compose down -v
up.sh ends by pushing GENERAL's bundled template with splitByServiceGroup: true to that OAP, the way the Layer dashboards admin would, so the sidebar shows one entry per group. The first metrics appear a minute or two after the load generator starts. Images come from test/e2e/script/env; export a variable first to override a pin (SW_OTEL_COLLECTOR_IMAGE=…).
Boot against an LDAP directory (test)
Stand up a throwaway OpenLDAP, seed it from ldap-seed.ldif, then boot with
HORIZON_AUTH_BACKEND=ldap and the LDAP config from dev-ldap.json. Test logins
mirror the local set (password == username): admin → admin, operator →
operator, maintainer → maintainer, viewer → viewer (* fallback). The bind
account is cn=admin,dc=horizon,dc=test / admin.
REPO="$(git rev-parse --show-toplevel)"; SK="$REPO/.claude/skills/local-boot"
docker rm -f horizon-ldap 2>/dev/null
docker run -d --name horizon-ldap -p 389:389 -p 636:636 \
--env LDAP_ORGANISATION="Horizon Test" --env LDAP_DOMAIN="horizon.test" \
--env LDAP_ADMIN_PASSWORD="admin" osixia/openldap:1.5.0
until docker exec horizon-ldap ldapwhoami -x -H ldap://localhost \
-D "cn=admin,dc=horizon,dc=test" -w admin >/dev/null 2>&1; do sleep 1; done
docker cp "$SK/ldap-seed.ldif" horizon-ldap:/tmp/seed.ldif
docker exec horizon-ldap ldapadd -x -H ldap://localhost \
-D "cn=admin,dc=horizon,dc=test" -w admin -f /tmp/seed.ldif
pkill -f "tsx watch src/server.ts" 2>/dev/null; pkill -f "tsx/dist/cli.mjs watch" 2>/dev/null
until ! lsof -nP -iTCP:8081 -sTCP:LISTEN >/dev/null 2>&1; do sleep 1; done
HORIZON_CONFIG="$REPO/horizon.yaml" \
HORIZON_AUTH_BACKEND=ldap \
HORIZON_AUTH_LDAP="$(cat "$SK/dev-ldap.json")" \
pnpm --filter @skywalking-horizon-ui/bff run dev &
dev-ldap.json reads the bind password as the test value admin; for a real
directory, edit it (or build the JSON with your own bind password) and never
commit a real one. Group resolution runs on the service bind, not the user's
credentials.
Verify the BFF is healthy (no browser)
# --noproxy so a local proxy (ClashX etc.) doesn't 502 the loopback call.
until curl -s --noproxy '*' -m2 -o /dev/null "http://127.0.0.1:8081/api/auth/health"; do sleep 1; done
curl -s --noproxy '*' -c /tmp/sw.cookies -H 'Content-Type: application/json' -X POST \
"http://127.0.0.1:8081/api/auth/login" -d '{"username":"admin","password":"admin"}'
# Expect 200 with {username, roles, verbs, landingRoute}. A 401 "invalid
# credentials" usually means HORIZON_AUTH_LOCAL_USERS wasn't passed (or the
# wrong HORIZON_CONFIG path / a stale BFF holds the port).
Editing the dev values
horizon.yaml (repo root) is the committed, env-driven config — leave it as-is
and override via HORIZON_* env vars. The dev users / LDAP config live in
dev-users.json / dev-ldap.json here (throwaway, single-line JSON). Real
secrets stay out of git via the .claude/skills/local-boot/*.key rule: the demo
OAP password in oap-demo-env-auth.key and the Bedrock bearer in
bedrocks-api.key are both git-ignored; real LDAP bind passwords are supplied at
boot. To mint a new local-user hash: pnpm --filter @skywalking-horizon-ui/bff cli:hash.
04 同仓库更多 Skills
pull-request
Open a pull request for the current Horizon UI branch the right way — run the full preflight gate first (changelog + docs updated, type-check + build "compiling passed", new UI strings translated across all 8 locales, license headers, lint + unit tests, live-OAP validation), commit with NO AI trailers, push, and open the PR against apache/skywalking-horizon-ui. After it merges, return to main, fast-forward to latest, and delete the merged local branch.
build-dashboard
暂无简介