This file is written for future AI coding agents (Copilot, Claude, etc.) working on this repository. It explains the architecture, key patterns, common tasks, and critical gotchas so you can be productive immediately without extensive archaeology of the codebase.
restraml generates API schema documentation for the MikroTik RouterOS REST API.
The pipeline is:
- Boot a RouterOS CHR (Cloud Hosted Router) directly in QEMU on the GitHub Actions runner
- Query the router's
/console/inspectREST endpoint to extract the full command/API tree - Convert that tree to RAML 1.0 schema format and OpenAPI 3.0
- Commit everything to
/docs/and publish via GitHub Pages
The generated schemas live at https://tikoci.github.io/restraml — with per-version folders in /docs/.
restraml/
├── rest2raml.js # Main script: connects to RouterOS REST API → RAML 1.0
├── validraml.cjs # Validates RAML 1.0 (uses webapi-parser, requires Node.js)
├── appyamlvalidate.js # Validates /app YAML schemas and built-in /app YAMLs (Bun)
├── deep-inspect.ts # Deep inspection of RouterOS API tree (Bun)
├── enrich-openapi.ts # Enriches generated OpenAPI schemas (Bun)
├── validate-openapi.ts # Validates OpenAPI 3.0 schemas (Bun)
├── ros-api-protocol.ts # Vendored RouterOS native API wire protocol (Bun)
├── validate-workflows.mjs # Runs actionlint over .github/workflows/ (bun run lint:workflows)
├── *.test.ts # Unit + integration tests (bun test)
├── Dockerfile.chr-qemu # Alpine image that runs RouterOS CHR in QEMU (for local use)
├── scripts/
│ ├── entrypoint.sh # QEMU launcher used by Dockerfile.chr-qemu (user-mode networking)
│ ├── test-with-qemu.sh # Integration tests (deep-inspect) against local QEMU CHR
│ ├── test-ros-api.sh # Integration + stress tests (ros-api-protocol) against local CHR
│ ├── benchmark-qemu.sh # REST vs native API timing benchmark against local CHR
│ ├── deep-inspect-multi-arch.ts # Per-arch deep-inspect orchestrator (quickchr library, x86 + arm64)
│ ├── diff-deep-inspect.ts # Diff two deep-inspect.<arch>.json files (enum drift + path delta)
│ ├── build-docs-index.mjs # Generate docs/docs-index.json from the checked-in docs tree
│ ├── experiment-arm64-reboot-timing.sh # Local QEMU arm64 reboot-timing experiment helper
│ ├── test-crash-path-memory.ts # Reproduce /console/inspect crash-path hang at varied RAM (SUP-127641)
│ ├── test-native-api-drops.ts # Diagnostic: native API completion non-determinism (drops)
│ ├── test-native-api-tags.ts # Diagnostic: native API tag-multiplexing probe
│ ├── analyze_appports.js # Analyze /app port mappings (Bun)
│ ├── analyze_appyamls.py # Analyze /app YAML patterns (Python)
│ ├── extract_appyamls.py # Extract /app YAMLs from app.json (Python)
│ └── native-api-investigation/ # Archived native API non-determinism research scripts + README
├── .env # Local dev env vars (URLBASE, BASICAUTH) — not committed secrets
├── docs/ # GitHub Pages root; one subdirectory per RouterOS version
│ ├── index.html # Main SPA: version list, diff tool, download links
│ ├── lookup.html # RouterOS command search tool (fully event-driven, no buttons)
│ ├── diff.html # Schema diff tool (side-by-side / line-by-line diff between versions)
│ ├── openapi.html # OpenAPI 3.0 API Explorer (Scalar)
│ ├── tikapp.html # /app YAML editor with Monaco + live validation
│ ├── tikapp-manual.html # /app YAML documentation / manual reference
│ ├── deep-inspect.md # Deep-inspect design/history reference
│ ├── mikrotik-bug-native-api-inspect.md # Native API request=completion non-determinism bug report
│ ├── deep-inspect.schema.json # Strict JSON Schema for current deep-inspect artifacts
│ ├── deep-inspect.future.schema.json # Design-target schema for future merge/provenance output
│ ├── restraml-shared.js # Shared JS utilities for all docs/*.html pages
│ ├── restraml-shared.css # Shared CSS: fonts, logo, theme, guide, modal, utilities
│ ├── docs-index.json # Published JSON inventory of root docs assets and per-version files
│ ├── routeros-app-yaml-schema.latest.json # /app YAML schema (strict, CI validation)
│ ├── routeros-app-yaml-schema.editor.json # /app YAML schema (relaxed, SchemaStore/editor)
│ ├── routeros-app-yaml-store-schema.latest.json # /app store schema (strict, CI validation)
│ ├── routeros-app-yaml-store-schema.editor.json # /app store schema (relaxed, SchemaStore/editor)
│ ├── {version}/
│ │ ├── schema.raml # RAML 1.0 schema (presence = "this version is built")
│ │ ├── inspect.json # Raw /console/inspect output from RouterOS
│ │ ├── openapi.json # OpenAPI 3.0 schema (when generated; see docs-index.json)
│ │ ├── app.json # Raw GET /rest/app output (built-in /app YAMLs)
│ │ ├── routeros-app-yaml-schema.json # /app YAML schema for this version
│ │ └── routeros-app-yaml-store-schema.json # /app store schema for this version
│ └── {version}/extra/ # Same files, but built with all_packages (extra features)
├── CLAUDE.md # Full architecture guide for AI agents
├── AGENTS.md # GitHub Copilot agent-specific instructions
└── .github/
├── actions/
│ └── publish-with-retry/ # Composite action: reset-to-origin publish + index rebuild + retry
└── workflows/
├── auto.yaml # Daily cron: detect new versions, trigger builds
├── manual-using-docker-in-docker.yaml # Build: base RouterOS schema
├── manual-using-extra-docker-in-docker.yaml # Build: schema with extra packages
├── appyamlschemas.yaml # Build: validate and publish /app YAML schemas
├── deep-inspect-multi-arch.yaml # Build: per-arch deep-inspect (x86 KVM + arm64 KVM/TCG fallback) with diff
├── manual-from-secrets.yaml # Build: using a real RouterOS device (secrets)
├── codeql.yml # Code scanning (JS/TS + GitHub Actions)
└── dependency-review.yml # PR dependency review
MikroTik publishes the current version for each release channel at:
https://upgrade.mikrotik.com/routeros/NEWESTa7.<channel>
Channels: stable, testing, development, long-term
To check if a version is already built, check for docs/{version}/schema.raml.
To check if a version's /app YAML schemas are built, check for docs/{version}/routeros-app-yaml-schema.json.
The repository publishes a static JSON inventory at docs/docs-index.json. It is generated by
scripts/build-docs-index.mjs from the checked-in docs/ tree and committed by every workflow
that publishes versioned artifacts.
Purpose:
- Lets GitHub Pages tool pages discover versions and file availability without using the GitHub Contents API
- Prevents GitHub API rate-limit failures from leaking into browser UX and SEO crawlers
- Gives pages like
docs/tikapp.htmla generic file inventory so they can infer capabilities from actual published files instead of hardcoded version thresholds
Shape (summary):
- Top-level metadata:
format,generatedAt,rootPath,latestVersion,latestStableVersion files: root-level published docs files (HTML, shared JS/CSS, root schemas, etc.)versions: per-version directory entries withname,path,type,files, and recursivedirs
docs/restraml-shared.js fetchVersionList() now reads docs-index.json from GitHub Pages and
returns versions[] (same {name, path, type} core shape as before, with extra file metadata).
Existing pages can keep using fetchVersionList(); pages that need file-aware filtering can inspect
the added files / dirs fields.
RouterOS 7.22 introduced /app — a docker-compose-lite YAML format for defining custom container
applications. The restraml project provides JSON Schema files to validate this YAML format.
Root-level schema files (do not rename or move these files):
Two variants exist for both the single-app and store schemas:
| File | Purpose | Used by |
|---|---|---|
docs/routeros-app-yaml-schema.latest.json |
Strict validation (regex patterns, tight enums) | CI (appyamlvalidate.js), public URL |
docs/routeros-app-yaml-schema.editor.json |
Relaxed for editor UX (no regex blockers) | SchemaStore, VSCode/Monaco autocompletion |
docs/routeros-app-yaml-store-schema.latest.json |
Strict store schema (array of /app YAMLs) | CI, app-store-urls= validation |
docs/routeros-app-yaml-store-schema.editor.json |
Relaxed store schema | SchemaStore/editor |
History: *.latest.json vs *.editor.json — Originally there was a *.dev.json intended
for beta/RC versions. The regex-blocks-autocompletion problem in VSCode YAML extension led to
creating *.editor.json as a relaxed variant instead. The *.dev.json file was removed; the
"editor" variant solved the immediate completion problem but conflated two dimensions: (1) strict
vs editor-friendly and (2) stable vs testing/beta versions. See "Schema file naming" in
BACKLOG.md for the backlog item on rethinking this scheme.
Per-version schema files (generated by appyamlschemas.yaml workflow):
docs/{version}/routeros-app-yaml-schema.json— single /app YAML schema with version-specific$iddocs/{version}/routeros-app-yaml-store-schema.json— store schema referencing per-version single schema
Per-version schemas are generated from the base *.latest.json schemas with version-specific $id URLs
(https://tikoci.github.io/restraml/{version}/routeros-app-yaml-schema.json). They do NOT use .latest
in the filename since the parent directory already implies the version.
appyamlvalidate.js — Bun script:
- Run as:
bun appyamlvalidate.js <version> - Generates per-version schema files under
docs/{version}/ - Part 1: Validates both schemas against JSON Schema meta-schema (AJV with draft-07) — required for potential SchemaStore publication
- Part 2: If
URLBASEis set, fetches all built-in /app YAMLs from the live RouterOS CHR (GET /rest/app) and validates each against the schema — exit code 2 means at least one /app YAML failed validation - Requires:
bun install(deps are inpackage.json)
appyamlschemas.yaml workflow:
- Takes
rosverinput; boots CHR with extra packages (container/app feature requires them) - Runs
appyamlvalidate.jsagainst the live CHR - Handles three distinct exit codes from
appyamlvalidate.js:- Exit 0 (all passed) → commits
app.json+ per-version schemas todocs/{version}/ - Exit 1 (meta-validation failed — schemas are invalid JSON Schema) → fails immediately, commits nothing
- Exit 2 (live validation failed — schemas valid but some MikroTik apps don't conform) → commits
app.jsononly (for debugging); schemas are NOT committed soauto.yamlwill detect them as missing and retry the build
- Exit 0 (all passed) → commits
- Creates a GitHub Issue listing each failing built-in /app app name when exit code is 2
- Uses two separate commit steps: one for
app.json(always if fetched), one for schemas (only on exit 0) - Dispatched by
auto.yamlwhendocs/{version}/routeros-app-yaml-schema.jsonis missing
RouterOS /app YAML format notes:
- Resembles
docker-composebut is NOT compatible — RouterOS has specific differences - Reference: https://forum.mikrotik.com/t/amm0s-manual-for-custom-app-containers-7-22beta/268036/22
- Top-level keys:
name,descr,page,category,icon,default-credentials,services,configs,volumes,networks - Each service under
services:maps to one container - Placeholders
[accessIP],[accessPort],[containerIP],[routerIP]etc. are expanded at deploy time - Port format: two styles are supported:
- Old (OCI-style):
host-port:container-port[/tcp|/udp][:label](e.g.,8080:80/tcp:web) - New (RouterOS 7.23+ style):
host-port:container-port[:label][:tcp|:udp](e.g.,8080:80:web:tcp) Both forms are valid; new apps from 7.23beta2 onward use the colon-separated:tcp/:udpsuffix.
- Old (OCI-style):
- The
/appREST endpoint (GET /rest/app) requires the container extra package
VSCode / Editor Integration:
The /app YAML schemas work with the RedHat YAML VSCode extension.
Add to VSCode settings or .vscode/settings.json. Files must be named using the configured ending (e.g. my-app.tikapp.yaml for a single app, my-store.tikappstore.yaml for a store):
{
"yaml.schemas": {
"https://tikoci.github.io/restraml/routeros-app-yaml-schema.latest.json": "*.tikapp.yaml",
"https://tikoci.github.io/restraml/routeros-app-yaml-store-schema.latest.json": "*.tikappstore.yaml"
}
}Important: Regex patterns in the schema prevent autocompletion in VSCode YAML extension. A future task may create a "loose" version of the schema without strict regex patterns to improve editor UX (autocompletion). The current schema is intentionally strict for validation correctness. The SchemaStore (https://www.schemastore.org) can automatically apply schemas to matching filenames — once published there, editors like VSCode pick up the schema without manual configuration.
How the schema evolves — app.json as source of truth:
MikroTik's built-in /app collection (the array returned by GET /rest/app, stored per-version in
docs/{version}/app.json) is the canonical source of truth for schema updates. When the CI
appyamlschemas.yaml workflow fails with exit code 2 (validation failures), it creates a GitHub Issue
listing the failing app names and error messages. To resolve:
- Download the CI artifact
app-yaml-schema-results-<version>.zip— it containsapp.jsonfrom the live CHR that was not committed because validation failed. - Inspect the failing entries: parse each failing app's
yamlfield (it's a RouterOS YAML string) and examine the properties that cause errors. - Identify new patterns: map each error class to a schema change:
must NOT have additional properties→ new service property (e.g.,entrypoint,devices)must be equal to one of the allowed values→ new enum value (e.g., newcategory)must match pattern→ port format change or new regex syntax
- Update
docs/routeros-app-yaml-schema.latest.json(strict, CI validation) anddocs/routeros-app-yaml-schema.editor.json(relaxed, SchemaStore/editor) with the new properties. - Copy
app.jsonfrom the CI artifact todocs/{version}/app.jsonso it is committed. - Run
bun appyamlvalidate.js <version>locally to confirm the schema is valid JSON Schema; also manually validate the YAML fields from failing apps against the updated schema. - Per-version
routeros-app-yaml-schema.jsonfiles are regenerated on the next CI run.
Key: make schema changes only in *.latest.json and *.editor.json — do NOT edit per-version
docs/{version}/routeros-app-yaml-schema.json files directly; those are generated by appyamlvalidate.js.
- Stable releases (e.g.,
7.22, no qualifier): only ondownload.mikrotik.com - Beta/RC/testing releases (e.g.,
7.22rc2,7.22beta4): oncdn.mikrotik.com
Both the CI workflows and Dockerfile.chr-qemu try download.mikrotik.com first, then fall
back to cdn.mikrotik.com. This is why versions without a qualifier (like 7.22) previously
failed when only CDN was tried. Do not change this order.
- Runs under Bun (not Node.js) — uses
Bun.argvfor CLI args - Connects to RouterOS via HTTP REST API:
URLBASEandBASICAUTHenv vars - Uses
POST /console/inspectwith{"request": "child", "path": "..."}to walk the API tree - Certain paths crash the REST server and are skipped:
where,do,else,rule,command,on-error - Writes two outputs:
ros-inspect-*.json(raw) andros-rest-*.raml(processed RAML) --versionflag: prints the RouterOS version and exits (used in CI to capture the actual version)INSPECTFILEenv var: skip live router query and load from a saved JSON file (useful for offline testing)
- Runs under Bun. Takes an existing
inspect.jsonand enriches it withrequest=completiondata from the RouterOS REST API. Outputsdeep-inspect.jsonandopenapi.json. - Supports two transports: REST (
RouterOSClient) and native API (NativeRouterOSClient) - CI production uses
--transport restonly — do not change without reading the note below
The per-arch deep-inspect.{x86,arm64}.json files under docs/{version}/extra/ are a published
contract consumed by tikoci/rosetta (SQL/RAG import). Before changing the emitted shape:
- The stable field shape is pinned by
docs/deep-inspect.schema.json(strict) and the consumer-perspective tests inrosetta-consumer.test.ts(_metaenvelope, node_type/desc, and_completionentry shape). Runbun testafter any change todeep-inspect.tsoutput. docs/deep-inspect.md→ "Downstream consumers" lists who reads what; rosetta expects both the x86 and arm64 files.docs/deep-inspect.future.schema.jsonis design-target only — do not emit its merge/provenance fields until the P2 merge task lands.- Consumers that need completion-enriched data read
deep-inspect.*.json, notinspect.json.
- Vendored from
tikoci/tiktui. Zero external dependencies. Fully functional binary protocol. - Exports:
RosAPI,RosError,RosErrorCode,Sentence,CommandResult - Supports tag-multiplexed concurrent commands on a single TCP connection
Decision (May 2026): All schema generation (both crawl and completion enrichment) uses the
REST API transport. The native API transport (--transport native) is NOT used in CI.
Why: RouterOS /console/inspect with request=completion returns non-deterministic results
over the native API binary protocol. The same command issued repeatedly on the same TCP connection
randomly drops completion entries ~20-30% of the time. REST is 100% deterministic. This is a
confirmed RouterOS bug, not a client-side issue.
Key facts:
- REST: 9,357 paths with completions, 55,730 total entries — deterministic across all runs
- Native: 9,302 paths, 53,935 entries — randomly drops entries, always a strict subset of REST
- Only
request=completionis affected;request=childandrequest=syntaxare reliable - The bug is per-call random, not cumulative or session-dependent
- Native API is 22× faster for tree crawl and 2× faster per-call — but unusable for completions
What this means for agents:
- Do NOT change CI workflows to
--transport nativeor--transport autofor enrichment ros-api-protocol.tsandNativeRouterOSClientindeep-inspect.tsremain for potential future use if MikroTik fixes the bug;benchmark.test.tsandnative-api.test.tswere removed (research artifacts)--transport restis now the explicit default indeep-inspect.ts- If MikroTik fixes the bug, the hybrid approach (REST crawl + native enrichment) becomes viable
- See
docs/mikrotik-bug-native-api-inspect.mdfor the full investigation anddocs/deep-inspect.mdfor deep-inspect pipeline context.
- Base (
manual-using-docker-in-docker.yaml): base RouterOS only - Extra (
manual-using-extra-docker-in-docker.yaml): all_packages including container, iot, zerotier, etc.- Extra packages are downloaded separately from
download.mikrotik.com/routeros/{ver}/all_packages-x86-{ver}.zip - Uploaded to CHR root via SCP, then CHR is rebooted to activate them
- Extra packages are downloaded separately from
CI workflows run RouterOS CHR directly in QEMU on the ubuntu-latest runner (no Docker-in-Docker, no docker-compose). The key steps are:
- Install
qemu-system-x86andqemu-utilsvia apt- Note: The Ubuntu package is
qemu-system-x86(notqemu-system-x86_64— that's the binary name, not the apt package)
- Note: The Ubuntu package is
- Enable KVM via udev rules (
/dev/kvmis available on GitHub hosted runners) - Download the CHR
.vdiimage (primary:download.mikrotik.com, fallback:cdn.mikrotik.com) - Convert
.vdito.qcow2usingqemu-img convert -f vdi -O qcow2(native QEMU format) - Launch QEMU in background with user-mode networking and port forwarding:
- host:9180 → VM:80 (RouterOS REST API)
- host:9122 → VM:22 (RouterOS SSH, used for SCP in extra-packages workflow)
- Disk:
-drive file=chr.qcow2,format=qcow2,if=virtio(virtio recommended by MikroTik for CHR) - Network:
-netdev user,id=net0,... -device virtio-net-pci,netdev=net0(virtio NIC)
- Wait up to 5 minutes (30 × 10s) for the API to respond — fail fast if not up in time
- Cleanup:
killthe QEMU PID stored in/tmp/qemu.pid
KVM is critical for performance — without it CHR boots very slowly in software emulation.
If the wait loop times out, check /tmp/qemu.log in the artifact or CI logs for QEMU errors.
QEMU settings for CHR (MikroTik recommended):
- Disk: virtio (
if=virtio) — confirmed to work on amd64/intel - Network: virtio-net-pci — use
-netdev user,id=net0,... -device virtio-net-pci,netdev=net0 - Memory: 256 MB is sufficient for base schema generation; use 1024 MB for workflows that install all extra packages
- Do NOT use
-nicshorthand (less control); use-netdev+-deviceinstead for explicit virtio
Dockerfile.chr-qemu + scripts/entrypoint.sh are provided for local development use only
(not used in CI). They use the same user-mode networking approach. To run locally:
docker build --build-arg ARG_ROUTEROS_VERSION=7.22 -t chr-qemu -f Dockerfile.chr-qemu .
docker run --rm -d --device /dev/kvm -p 9180:80 -p 9122:22 chr-qemuAfter schema generation, files are committed directly to main branch by github-actions[bot].
The commit structure is:
docs/{rosver}/{schema.raml,inspect.json,openapi.json}
docs/{rosver}/extra/{schema.raml,...} # extra-packages build only
Multiple workflows (base + extra, multiple RouterOS versions) can run at the same time. All push
to main, so a simple git push will fail if another job committed first. Because every publish
workflow also regenerates docs/docs-index.json, replaying a local publish commit with
git pull --rebase can conflict in that generated file even when the version-specific docs trees
do not overlap.
All publish workflows now use the shared composite action
.github/actions/publish-with-retry. It implements the required contract:
- stage the publish paths plus
docs/docs-index.json - skip empty commits cleanly on reruns / identical output
- capture
LOCAL_COMMITonce immediately after the initial publish commit - on push rejection,
git fetch+git reset --hard origin/main, restore the original publish paths fromLOCAL_COMMIT, rebuilddocs/docs-index.json, and retry - fail immediately if
git restoreorbun run docs:indexfails - use exponential backoff with jitter between retries
Typical usage:
- uses: ./.github/actions/publish-with-retry
with:
publish-paths: |
docs/${{ steps.connection-check.outputs.rosver }}/
commit-message: Publish ${{ steps.connection-check.outputs.rosver }} [${{ github.workflow }}]This is safe because each build writes to its own docs/{version}/ directory — there are no
real file conflicts between concurrent jobs, and docs/docs-index.json is always regenerated
from the full post-sync tree before retrying. Do not revert to a simple git pull + git push
or git pull --rebase pattern, and do not inline a separate retry loop when the shared action
already fits the workflow.
For the /app app.json publish step, pass the not-yet-published per-version schema files via
the action's docs-index-excludes input so docs-index.json is rebuilt without prematurely
advertising schema artifacts that were intentionally withheld after validation failures.
Do NOT add git clean -fd to the retry loop. The previous rebase-based pattern needed it
because bun install left modified tracked files (package.json, bun.lock) that would block
git pull --rebase. The new git reset --hard origin/main silently overwrites tracked-file
modifications, so the clean step is unnecessary — and it would delete the untracked build outputs
at the repo root (ros-rest*.raml, ros-inspect*.json, deep-inspect*.json, openapi.json,
index.html) that the subsequent "Save build artifacts" / "Upload all build artifacts" steps
upload. The empty-commit guard (git diff --cached --quiet after the initial git add) is also
required so re-runs and identical-output cases exit cleanly instead of failing on an empty commit.
The docs/*.html pages expose WebMCP tools
(Chrome 146+, chrome://flags/#enable-webmcp-testing) via the imperative API
(navigator.modelContext.registerTool()). This lets agentic browsers discover and call the
pages' data functions directly, returning structured JSON instead of requiring screen-scraping.
Implementation is progressive enhancement — all code is behind webMCPAvailable() feature
detection in restraml-shared.js. Zero impact on browsers without WebMCP support.
webMCPAvailable()— returnstrueifnavigator.modelContext.registerToolexistsregisterWebMCPTools()— registers the sharedlist_routeros_versionstool, returns{ register(toolDef, registerOptions) }helper for page-specific tools. The helper forwards the nativeregisterTool()options bag (for example{ signal }) so pages can dynamically unregister tools withAbortController. Called once per page.
| Tool Name | Page | Description |
|---|---|---|
list_routeros_versions |
All pages | List published versions with metadata (shared) |
lookup_routeros_command |
lookup.html |
Look up a CLI path/attribute in inspect.json |
diff_routeros_versions |
diff.html |
Compare two versions — delta stats + added/removed paths |
get_routeros_changelog |
index.html |
Fetch & parse MikroTik CHANGELOG into structured entries |
get_tikapp_editor_content |
tikapp.html |
Read the current Monaco editor buffer and metadata |
set_tikapp_editor_content |
tikapp.html |
Replace the current Monaco editor buffer and return validation results |
set_editor_mode |
tikapp.html |
Switch editor schema mode (single /app vs app-store-urls= array) and/or schema version |
get_routeros_app_yaml_schema_outline |
tikapp.html |
Get a concise outline of allowed /app YAML keys and service properties |
validate_routeros_app_yaml |
tikapp.html |
Validate /app YAML against JSON Schema |
list_builtin_apps |
tikapp.html |
List built-in /app container applications |
get_openapi_schema_url |
openapi.html |
Get OpenAPI 3.0 schema download URL + availability |
- All
executefunctions returnJSON.stringify(result)— structured JSON, not HTML - Error responses:
JSON.stringify({ error: "message" })with descriptive text for agent self-correction - Large data is summarized (diff returns stats + capped path lists, not full unified patch)
- Version parameters default to latest stable when omitted
list_routeros_versionsshould be called first to discover valid version strings- Validate strictly in code even when
inputSchemaalready constrains the type — schemas help the agent, but runtime checks and descriptive errors are what let the agent recover and retry - All tools include
annotations: { readOnlyHint, untrustedContentHint }— see next section
Every registered tool sets a ToolAnnotations object (added to the WebMCP spec on Apr 24, 2026,
PR #169,
issue #136):
readOnlyHint: true— use for tools that only read or compute. Most restraml tools are read-only.readOnlyHint: false— use for tools that change page state. Currently this isset_tikapp_editor_content, which replaces the Monaco editor buffer intikapp.html.untrustedContentHint: true— set whenever the tool's output contains data the page author did not produce. The agent should treat strings in the result as data, not instructions (mitigates prompt-injection via tool output). For restraml this applies to:list_routeros_versions,lookup_routeros_command,diff_routeros_versions,list_builtin_apps— relay JSON derived from MikroTik's RouterOS images.get_routeros_changelog— fetches MikroTik release notes verbatim.get_tikapp_editor_content,set_tikapp_editor_content— expose and mutate user/editor content, which is untrusted from the page author's perspective.validate_routeros_app_yaml— echoes user-supplied YAML in error messages.
- Tools with
untrustedContentHint: falsecurrently are:get_openapi_schema_url— returns only a URL string we construct from a known template.get_routeros_app_yaml_schema_outline— summarizes the locally published schema files we author in this repo.
When adding or modifying a WebMCP tool, decide the hint based on this rule:
if any string in the response originates outside this repo's source code, set
untrustedContentHint: true.
- In the page's main
<script>, get the helper:const _wmcp = registerWebMCPTools() - Register tools:
_wmcp.register({ name, description, annotations, inputSchema, execute }) - Follow the naming convention:
verb_routeros_nouninsnake_case - Set
annotations— usereadOnlyHint: falsefor any tool that mutates page state; decideuntrustedContentHintper the rule above. - Return JSON strings from
execute(always wrap in try/catch) - Add the tool to the table above
The WebMCP spec (Mar 27, 2026 update) removed unregisterTool() in favor of an AbortSignal
passed via the second arg: navigator.modelContext.registerTool(tool, { signal: ctrl.signal }),
then ctrl.abort() to remove. registerWebMCPTools() now forwards the native options bag for
both the shared tool and page-specific tools, so pages can opt into state-driven lifecycle
management without bypassing the helper. We currently register all tools once at page load,
but for future page-state-dependent tools (for example only exposing submit_app after the
YAML is valid), create an AbortController, pass { signal: ctrl.signal }, and abort when
the tool no longer makes sense for the current UI state.
The WebMCP early preview expects developers to test tools directly, not only through an LLM:
- Enable Chrome 146+ flag
chrome://flags/#enable-webmcp-testing - Install the Model Context Tool Inspector extension
- Use the extension to confirm the tool list, descriptions, and JSON schemas exposed by the page
- Manually execute each tool with representative arguments before relying on agent-driven tests
- Prefer fixing descriptions and runtime error messages when the agent picks the wrong tool or retries poorly
All HTML pages served from docs/ (GitHub Pages) follow these non-negotiable conventions:
- Pico CSS (
@picocss/pico@2) — the only CSS framework, loaded from CDN. No Bootstrap, Tailwind, or other CSS frameworks. Exception:docs/openapi.htmlintentionally does not load Pico because Pico's global element styles conflict with Scalar's generated API-reference DOM; it defines the small--pico-*variable subset needed by shared styles instead. - JetBrains Mono — the primary font for all pages. Use it creatively: monospace weight
variation, italic, variable fonts,
letter-spacing,font-feature-settingsfor ligatures, etc. The font can be used for fun visual effects — the constraint is the font choice, not how it's used. - Semantic HTML — use proper
<header>,<main>,<section>,<nav>,<article>,<details>,<summary>, etc. No<div>soup. - No web frameworks — no React, Vue, Angular, Svelte, etc. Vanilla JS only.
- No build tools — no webpack, Vite, npm scripts for the HTML page itself. Single
.htmlfile. restraml-shared.js— shared JS utilities (version parsing, theme switcher, share modal, published docs-index fetch). Alldocs/*.htmlpages load this via<script src="restraml-shared.js"></script>. When modifying shared behavior, change this file — not inline copies. When creating a new page, include this script before page-specific code.restraml-shared.css— shared CSS loaded by all pages via<link rel="stylesheet" href="restraml-shared.css">(after Pico CSS, before page<style>;openapi.htmlloads it without Pico as the Scalar exception). Contains: font overrides (JetBrains Mono + Manrope), inline code/kbd tightening (prevents line-height bloat in paragraphs), MikroTik logo dark/light swap, theme switcher icon sizing, Tools dropdown LTR fix,.page-guidepattern (collapsible help sections),.share-modalstyling, and utility classes (.ml-1,.text-right,.mt-1,.inline-select,.switch-controls,.grid-2fr-1fr,.share-link). When adding shared visual patterns, add them here — not as inline styles or duplicated<style>blocks.- Avoid submit buttons — prefer JS event listeners (
input,change,keydown) over explicit submit/lookup buttons. Use debouncing (~400 ms) for text inputs; fire immediately onchangeevents for checkboxes and<select>elements. Cancellation tokens (incrementing counter compared before and after eachawait) prevent stale results from racing async fetches. - Client-side SPA — all logic runs in the browser. There is no backend. GitHub Pages serves
static files only. Prefer the published
docs/docs-index.jsoninventory for version and file availability; use the GitHub REST API or GitHub GraphQL API only for metadata that is not already published there. - Minimal dependencies — only add a CDN library if it meaningfully solves a problem (e.g.,
json-diff,highlight.js,deep-diff,jsonpath). Keep the CDN dependency count low. - Shareable URLs — query string pattern: All tool pages support query strings that populate
controls and trigger results on load. Use
history.replaceState()to update the URL as the user interacts (notpushState— no new history entries). Read params after the async version list loads so<select>options exist before being set. Invalid/unknown params are silently ignored. Parameter names per page:index.html:compare1,compare2,extra(false to disable),testing(true to enable)diff.html:compare1,compare2,extra,testing,format(side-by-side|line-by-line),context(0|3),hunks(showing|hiding),changelog(open),changelogFilter(filter text),changelogSort(newest-first|oldest-first)lookup.html:path(without leading slash),attr,version,allVersions(true),testing(true),extra(true)
- Share button — two patterns exist:
- Preferred: inline "Copied!" button (
lookup.html,tikapp.html,diff.html,openapi.html): A<button>that callswriteQueryParams(), copies the URL vianavigator.clipboard.writeText(), and swaps its text to "✓ Copied!" for 1.8 seconds. No modal, no dialog. Place it right-aligned on the same line as the Results heading. All pages use this pattern. - Legacy:
<dialog>modal (initShareModal):restraml-shared.jsexportsinitShareModal()wiring a<dialog class="share-modal">with URL input and "Copy to clipboard" button. No page currently uses it — all have migrated to the inline button. Styling inrestraml-shared.css. Kept in the shared library for potential future use; new pages should prefer the inline button.
- Preferred: inline "Copied!" button (
docs/index.html is the primary SPA. Key page-specific patterns:
- Custom events:
builddirandinspectdownloadevents decouple data fetching from UI updates. - Early-event queue:
_pendingBuildDirsqueues events that fire beforeDOMContentLoaded. - MikroTik logo trick: two
<img>tags withdata-theme="dark"/data-theme="light"— CSS rules swap which is visible based on the current theme. inspect.jsonis the data source for diffs and stats. Usejsonpathfor structured queries. Usejson-diff+highlight.jsfor textual diff. Usedeep-difffor change statistics.- Plausible analytics:
plausible("Event Name", { props: { key: value } })for tracking user interactions. Always include event tracking for new interactive features.
docs/lookup.html is a fully event-driven command search tool. Key patterns:
- Inline controls layout: CLI Path (~55%), Attribute (~27%), and Version (~17%) are on a single row. Switches (extra-packages, check all versions, include testing) sit below.
- Combined path+cmd input: a single text field accepts the full path including the command
as the last segment (e.g.
/ip/address/set). No separate path and command inputs. - Dynamic, no submit button: results update as the user types (400 ms debounce on text,
immediate on
changefor checkboxes/selects). - Smart results summary: single-version searches name the version; multi-version searches describe the scope ("all 25 stable versions"). Schema type (base/extra-packages) is stated.
- Dynamic column header: the "Details" column header changes to "Attributes" (when the terminal node is a command) or "Commands" (when a directory/path) based on the search result.
- Inline share button: "Share" button copies the URL with "✓ Copied!" feedback (no modal).
- inspect.json cache: fetched data cached per version+subdir in
inspectCache. - Cancellation tokens:
runLookupIdcounter prevents stale async results from updating DOM.
Beyond docs/index.html, agents may be asked (via GitHub Issues) to create additional pages
in docs/ offering different views of the schema data. Pattern: docs/custom-view.html.
Rules for custom pages:
- Must follow all the web page conventions above (Pico CSS, JetBrains Mono, semantic HTML, client-side only, minimal dependencies).
- Prefer the published docs inventory for version discovery:
docs/docs-index.jsonis the static source-of-truth for published versions/files on GitHub Pages. Use GitHub API/GraphQL only when the data is not already published there (for example GitHub metadata that does not exist indocs/). URLs follow the patternhttps://tikoci.github.io/restraml/{version}/inspect.jsonandhttps://tikoci.github.io/restraml/{version}/schema.raml. - No server-side code, no backend, no build step.
- Include the shared Tools nav dropdown (see "Tools Nav Dropdown" section below) for consistent navigation.
- Include
<link rel="stylesheet" href="restraml-shared.css">after Pico CSS and Google Fonts, before any page-specific<style>block (exceptopenapi.html, which avoids Pico for Scalar compatibility). This provides fonts, logo swap, theme switcher, page-guide, share-modal, and utility classes — no need to duplicate these in page styles. - Include
<script src="restraml-shared.js"></script>before page-specific scripts. CallinitThemeSwitcher(). For sharing, use the inline "Copied!" button pattern (see Share button section above). UsefetchVersionList()andRESTRAML.pagesUrlfrom the shared utilities. - Keep JavaScript in the single
.htmlfile (no separate.jsfiles unless there is a very strong reason for separation). - Issues requesting custom views will typically describe a desired user-facing feature (e.g., "show a visual graph of RouterOS commands", "compare two versions side by side"). Interpret the request creatively — the font and aesthetic constraint is intentional.
Example inspiration (from GitHub Issues by fischerdouglas and others):
- A visual tree/graph of the RouterOS command hierarchy
- A filterable/searchable table of commands and arguments
- A changelog-style page showing what changed between versions
- A diff page highlighting only added/removed commands between two versions
All docs/*.html pages include a Tools dropdown in the top nav (Pico CSS <details class="dropdown">),
providing consistent navigation between tools. The dropdown lists all tools; mark the current page with
aria-current="page".
The dropdown currently contains both local tool pages and cross-project external links:
<!-- In the right-side <ul> of the header <nav>, before the theme switcher -->
<li>
<details class="dropdown">
<summary>Tools</summary>
<ul dir="rtl">
<li><a href="https://tikoci.github.io/project-map.html">Project Map</a></li>
<li><a href="https://tikoci.github.io/chr-images.html">CHR Images</a></li>
<li><a href="https://tikoci.github.io/p/netinstall">Netinstall</a></li>
<li><a href="openapi.html">API Explorer</a></li>
<li><a href="tikapp.html">/app Editor</a></li>
<li><a href="diff.html">Schema Diff</a></li>
<li><a href="lookup.html">Command Lookup</a></li>
</ul>
</details>
</li>- Use
dir="rtl"on the<ul>so the dropdown aligns right when placed in the right nav column. - The theme switcher
<li>follows the Tools<li>in the same<ul>. - When adding a new
docs/*.htmltool page, add it to the dropdown list in all existing pages. - On
index.html, replace direct links to other tools in the intro text with "check the Tools menu above".
Dark mode is handled by initThemeSwitcher() in restraml-shared.js. All pages call this function.
Critical Pico v2 gotcha: data-theme='auto' is not a valid Pico CSS v2 value — it silently
forces light mode. The shared code handles this correctly by removing the attribute for "auto" state.
CSS pattern for third-party components in dark mode (covers auto+OS-dark AND explicit dark):
/* Auto mode + OS dark */
@media (prefers-color-scheme: dark) {
:root:not([data-theme=light]) #mycomponent { /* dark styles */ }
}
/* Explicit dark */
[data-theme=dark] #mycomponent { /* dark styles */ }docs/tikapp.html embeds Monaco Editor. Pico CSS's global button and [role="button"] rules
leak into Monaco's internal widget DOM and style Monaco's hover tooltips, zone widgets (problem
panels), and action links as large styled buttons. The fix is a targeted CSS reset, but the
selector specificity is critical — get it wrong and you either don't fix Pico's overrides or
you break Monaco's own internal styling.
The specificity sandwich:
| Rule | Specificity | Must… |
|---|---|---|
Pico global button { } |
(0,0,1) |
be beaten by our reset |
Pico global [role="button"] { } |
(0,1,0) |
be beaten by our reset |
| Our reset | (0,2,1) |
sits between the two |
Monaco widget .zone-widget button etc. |
(0,2,1)+ |
beat our reset so Monaco's own styles win |
Note: .monaco-editor :is(button, a[role="button"]) resolves to (0,2,1) because .monaco-editor
contributes (0,1,0) and :is(button, a[role="button"]) contributes (0,1,1) (the max specificity
of its forgiving list).
The correct two-rule pattern:
/* Box model reset for both <button> and <a role="button"> */
.monaco-editor :is(button, a[role="button"]) {
padding: 0; border: 0; background: transparent;
box-shadow: none; width: auto; inline-size: auto;
min-height: 0; margin: 0; border-radius: 0;
}
/* Font reset for <a role="button"> ONLY ("View Problem (F8)" etc.)
Scoped to <a> not <button> — Monaco controls icon font-size on <button>.codicon */
.monaco-editor a[role="button"] {
font-size: inherit;
line-height: inherit;
font-weight: inherit;
}Anti-patterns that cause bugs:
#editor-container :is(...)— ID gives specificity(1,0,1)/(1,1,1), which is HIGHER than Monaco's own.zone-widget buttonrules(0,2,1). This overrides Monaco's internal widget styles and causes close/navigate icon buttons to render as "bars" (bare codicon characters with all Monaco chrome removed).appearance: auto— tells the browser to render OS-native button chrome. On macOS dark mode withbackground: transparent, this produces visible striped bars on previously styled buttons.font: inherit— codicon icon glyphs in.monaco-editor ::beforepseudo-elements rely onfont-family: codiconbeing set by Monaco's own rules on the pseudo-element; addingfont: inheriton the parent element can interfere with glyph rendering depending on where Monaco puts the icon character (text content vs::before). Omit it.color: inherit— may affect icon color in dark vs light modes. Let Monaco control it.- Adding
font-size/weight/line-heightto the:is(button, a[role="button"])rule — Pico inflates font on[role="button"]anchors ("View Problem" link becomes oversized), but resetting font-size on<button>too can interfere with Monaco's codicon icon sizing. Split it: reset font properties on a separatea[role="button"]-only rule (see pattern above).
Monaco widget types and which elements they use:
- Hover widget ("View Problem (F8)" etc.): uses
<a role="button">— must be in the selector. - Zone widget (problem navigation panel): uses
<button>and<a role="button">for close/navigate. - Overflow widgets (hover tooltip): only appear outside
.monaco-editoriffixedOverflowWidgets: true(default isfalse, so they stay inside.monaco-editorand ARE covered by the selector).
docs/diff.html uses diff2html with jsdiff.
The CSS in diff.html already handles all these issues. If modifying the diff page:
colorSchemeoption is a no-op inDiff2Html.html()— dark mode must use CSS overrides.- Pico CSS overrides all
td/th— reset within#diffoutputusing ID-prefixed selectors. - diff2html CSS sets opaque white backgrounds — reset
.d2h-file-wrapperetc. totransparent. - Context lines are a jsdiff option (patch level), not diff2html (render level).
Cache
_lastText1/_lastText2for context re-renders; cache_lastPatchfor format-only.
Tool pages (diff.html, lookup.html) include a collapsed <details> section that serves as
lightweight in-page documentation. This avoids linking to external docs and keeps the single-HTML
file pattern. The guide is collapsed by default so it doesn't clutter the UI for experienced users.
Structure:
<details id="my-guide" class="page-guide">
<summary><b>How to read this?</b> …</summary>
<article>
<header><strong>Section Title</strong></header>
<!-- Usage explanation -->
<hr>
<!-- Notation / syntax explanation -->
<hr>
<div class="behind-curtain">
<small><b>Behind the curtain</b> — ...</small>
</div>
<footer>
<small>Bug/feature links, README link</small>
</footer>
</article>
</details>Flow: Explain using the tool first (controls, options), then the notation/syntax, then a
brief "how the sausage is made" peek at the data source. Keep it tight — more trivia than
architecture. The <article> gives Pico's card styling; <header> and <footer> add structure.
CSS: The .page-guide class in restraml-shared.css provides all guide styling (summary font,
article sizing, pre left-border, .behind-curtain callout). No per-page CSS needed — just add
class="page-guide" to the <details> element.
"Found a bug?" and README links live inside the guide's <footer>, not as a standalone
section — keeps the page clean when the guide is collapsed.
These Pico v2 patterns are used across docs/*.html pages. Prefer these over <div> + classes:
| Element / Pattern | What Pico Does | Used For |
|---|---|---|
<article> |
Card with padding, border, rounded corners | Guide sections, callouts |
<article> + <header> / <footer> |
Card with distinct header/footer sections | Structured cards |
<details> / <summary> |
Native accordion, styled with arrow | Collapsible guide, TOC |
<details> + name="group" |
Exclusive accordion (only one open) | Grouped sections |
<summary role="button"> |
Summary styled as a button | Prominent toggles |
<mark> |
Highlighted inline text (yellow/primary) | Key terms, toggle names |
<kbd> |
Keyboard-key styled inline | Package names, key combos |
<figure> + <figcaption> |
Captioned content block | Code examples with notes |
<ins> / <del> |
Green/red inline text | Showing diff semantics |
<hr> inside <article> |
Subtle section divider within a card | Separating guide topics |
role="switch" on checkbox |
Toggle switch appearance | Extra-packages, testing toggles |
<nav> with <ul> |
Horizontal flex layout | Controls bar, toolbar |
Consistent switch labels: When a <nav> has multiple role="switch" toggles, give the
<nav> an ID and apply font-size: 0.88rem; font-style: italic to all labels via CSS. Use
<code> (with font-style: normal) for technical terms within labels. Remove individual <i>
tags — let CSS handle italic consistently.
This repository uses GitHub Copilot coding agents (running Claude Sonnet) triggered from GitHub Issues and PRs. The following notes apply to any AI agent working on this repo:
An AGENTS.md file exists at the repo root. It provides instructions specific to Copilot agents:
- Technology stack and constraints for this repo
- PR conventions
- Specific areas where agent work is expected
- Schema build fixes: changes to
.github/workflows/*.yamlto fix or improve the CI pipeline - Custom web views: creating new
docs/*.htmlpages based on GitHub Issue requests - CLAUDE.md / AGENTS.md updates: keep these files current with any architectural changes
- Agents commit to a branch and open PRs; the human reviews and merges.
- Build workflows are triggered by
auto.yaml(daily cron) orworkflow_dispatch. - Agents must not break existing build workflows. Always validate YAML syntax before committing.
- The
GITHUB_TOKENavailable in Actions has write access to push tomain; agents must not hardcode or leak this token.
The auto.yaml workflow runs daily and handles this automatically. If you need to trigger it
manually, dispatch auto.yaml via workflow_dispatch with no inputs. Alternatively, dispatch
manual-using-docker-in-docker.yaml directly with the version string as rosver.
- Check the GitHub Actions logs for the failed workflow run
- Common failures:
- Image download fails: The CHR
.vdi.zipcouldn't be fetched — check ifdownload.mikrotik.comandcdn.mikrotik.comboth serve the version. Workflows try both with primary+fallback. qemu-system-x86install fails: On Ubuntu, the apt package isqemu-system-x86(notqemu-system-x86_64— that's the binary name). Also installqemu-utilsforqemu-img.- Wait loop times out (5 min): CHR didn't boot — check
/tmp/qemu.login CI output for QEMU errors. Most likely KVM is unavailable or the image is corrupt. rosveroutput is empty: Thebun rest2raml.js --versionstep's output parsing failed; check thexargscommand in theconnection-checkstep
- Image download fails: The CHR
RouterOS versions follow MAJOR.MINOR[QUALIFIER] where qualifier is one of:
- (none) — stable release, e.g.
7.22 beta1,beta2, ... — beta builds, on cdn.mikrotik.comrc1,rc2, ... — release candidates, on cdn.mikrotik.com
The download URL pattern is:
https://download.mikrotik.com/routeros/{version}/chr-{version}.vdi.zip # stable
https://cdn.mikrotik.com/routeros/{version}/chr-{version}.vdi.zip # beta/rc
# Install Bun: https://bun.sh/
bun install
URLBASE=http://192.168.88.1/rest BASICAUTH=admin: bun rest2raml.js
# Just get the RouterOS version:
URLBASE=http://192.168.88.1/rest BASICAUTH=admin: bun rest2raml.js --version
# Generate schema for a subtree only:
URLBASE=http://192.168.88.1/rest BASICAUTH=admin: bun rest2raml.js ip address
# Use a cached inspect.json (skip live router):
INSPECTFILE=./ros-inspect-all.json URLBASE=http://unused/rest BASICAUTH=x: bun rest2raml.js# RAML validation (webapi-parser requires Node.js, not Bun)
node validraml.cjs ros-rest-all.raml
# OpenAPI 3.0 validation
bun run validate:openapi
# /app YAML schema validation (requires live CHR or just meta-validates schemas)
bun appyamlvalidate.js <version>bun test # Unit tests — no router needed; runs in CI
bun run test:ros-api # ros-api-protocol integration + stress tests (local CHR)
bun run test:qemu # deep-inspect integration tests (local CHR)
bun run test:benchmark # REST vs native API timing benchmark (local CHR)
bun run lint # Biome lint + TypeScript type check
bun run lint:fix # Auto-fix Biome issues
bun run typecheck # TypeScript type check only
bun run deep-inspect # Deep API tree inspection (single CHR via env vars)
bun run deep-inspect:multi-arch # Per-arch enrichment via quickchr (x86 + arm64)
bun run deep-inspect:diff # Diff two deep-inspect.<arch>.json filesPer-arch enrichment (deep-inspect:multi-arch) boots a fresh CHR per arch
via the quickchr library (installAllPackages: true), applies a p1 trial
license if MikroTik web credentials are available (env vars for CI,
Bun.secrets for local via quickchr login), runs deep-inspect.ts --live
as a subprocess, and writes deep-inspect.<arch>.json / openapi.<arch>.json
side-by-side. Exits nonzero if any arch has crash paths or failed args —
anomalies are the point, not something to tolerate. See
docs/deep-inspect.md for design rules, Phase 3 history, and first-run
findings.
CI currently passes --skip-openapi in deep-inspect-multi-arch.yaml and publishes only
deep-inspect.{x86,arm64}.json plus diff-deep-inspect.json. Per-arch OpenAPI publication is
tracked as a separate BACKLOG.md task so openapi.html behavior does not change accidentally.
Diff tool (deep-inspect:diff) reports structural delta (paths only in
one arch), completion enum drift (same arg, different _completion keysets —
this is the bucket that surfaces real schema gaps), and a _meta side-by-side.
Does not merge, does not decide who is right; always exits 0. The text report
is published by the multi-arch CI workflow as a diagnostic artifact.
Local CHR test scripts (test:ros-api, test:qemu, test:benchmark) boot a RouterOS CHR
VM using mikropkl machine directories searched in this order:
~/Lab/mikropkl/Machines/, ~/GitHub/mikropkl/Machines/, or $MIKROPKL_DIR/Machines/.
You can also pass a machine path directly: ./scripts/test-ros-api.sh /path/to/machine.utm.
Short-term limitation: Integration scripts currently require a local mikropkl directory. The long-term goal is for
test:ros-apiandtest:qemuto run in CI using the same QEMU+CHR+KVM infrastructure that the build workflows already use (see "CHR Boot Pattern" above). Contributions to add CI jobs are welcome.
test:ros-api stress test (scripts/test-ros-api.sh) is the regression canary for the
ghost-command bug where writeAbortable() without /cancel left 50 orphaned RouterOS commands
blocking the inspect queue. The stress test fires 50 concurrent writeAbortable() calls, aborts
half mid-flight, then probes the router — clean queue = <5 s; ghost regression = ~60 s timeout.
| Workflow | Trigger | What it does |
|---|---|---|
auto.yaml |
Daily cron + manual | Checks all 4 RouterOS channels; per unique version, independently checks base schema, extra schema, /app YAML schema, and deep-inspect multi-arch artifacts (deep-inspect.x86.json, deep-inspect.arm64.json, diff-deep-inspect.json) and dispatches only the builds that are missing; outputs a step summary table. Accepts a skip_versions input (see below). |
manual-using-docker-in-docker.yaml |
Manual (rosver input) or auto.yaml |
Installs QEMU, boots CHR, builds base schema, commits to /docs/{version}/ |
manual-using-extra-docker-in-docker.yaml |
Manual (rosver input) or auto.yaml |
Same as above + installs extra packages, commits to /docs/{version}/extra/ |
appyamlschemas.yaml |
Manual (rosver input) or auto.yaml |
Boots CHR with extra packages, validates /app YAML schemas (exit codes 0/1/2), commits app.json always; commits per-version schemas only on full pass (exit 0); files GitHub issue on exit 2 |
deep-inspect-multi-arch.yaml |
Manual (rosver input) or auto.yaml |
Boots x86 (KVM) and arm64 (KVM preferred, TCG fallback) CHRs with extra packages in parallel, runs live deep-inspect crawl on each, diffs results, publishes deep-inspect.{x86,arm64}.json and diff-deep-inspect.json to /docs/{version}/extra/. Per-arch OpenAPI publication is deferred to BACKLOG.md P2. |
manual-from-secrets.yaml |
Manual | Builds using a real router via GitHub Secrets (no QEMU) |
codeql.yml |
Push + PR + weekly schedule | Runs repository-managed CodeQL for JavaScript/TypeScript and GitHub Actions, using repo path ignores for generated versioned docs artifacts |
dependency-review.yml |
Pull requests | Uses GitHub dependency review to block new high-severity vulnerable dependency changes |
All builds commit schema files to main as github-actions[bot] and publish via GitHub Pages.
auto.yaml accepts a skip_versions workflow_dispatch input (comma-separated list of RouterOS
version strings, e.g. 7.23beta5,7.23beta6). Versions in this list are excluded from all
build types (base, extra-packages, /app YAML schemas, and deep-inspect multi-arch) for that run.
The workflow_dispatch default can be broader for historical manual backfill; the scheduled
fallback in the needs-builds step intentionally stays narrower so current channels can recover
automatically if MikroTik fixes upstream beta/RC issues.
When to add a version to skip_versions:
- A beta/RC version's built-in
/appYAML collection contains entries that fail validation, and the failure is due to an upstream MikroTik bug (not a missing schema pattern) — so the correct action is to wait for MikroTik to fix their app YAML rather than relaxing the schema. - Common upstream bugs: duplicate YAML mapping keys (invalid pure YAML, MikroTik parser is
permissive);
[placeholder]values parsed as YAML arrays instead of strings. - In these cases
appyamlschemas.yamlwill keep returning exit code 2 → filing a new GitHub Issue on every daily run. Adding the version toskip_versionsstops the retry loop while the issue remains open.
How to update the skip list without a code change:
Trigger auto.yaml via workflow_dispatch in the GitHub UI and set the skip_versions
input explicitly (for example, 7.23beta5,7.23rc3 to skip multiple versions). Leaving this
input blank uses the manual default; scheduled runs use the fallback hardcoded in the
needs-builds step. To change cron behavior, update that fallback deliberately.
Unresolved /app schema policy questions live in BACKLOG.md under "Decision-needed tasks"
instead of being duplicated here. Current decision buckets:
- Parser leniency for duplicate YAML keys and placeholder arrays.
- Retry / skip policy for repeated
appyamlschemas.yamlexit-code-2 failures. - Schema naming and beta/RC release-scope policy for
*.latest.jsonand*.editor.json.
- Bun is the primary runtime for all
.jsand.tsscripts. Usebun(notnode) andbun install(notnpm install). The only exception isvalidraml.cjswhich requires Node.js for thewebapi-parserpackage. - Biome (v2.x) is the linter:
bun run lint(bunx @biomejs/biome check .). Run this after modifying any.jsor.htmlfile and fix reported errors in your changed code before presenting changes. Auto-fix fixable issues withbun run lint:fix. Formatter and assist (import sorting) are intentionally disabled inbiome.json— linting only, no automated reformatting. Do not add Prettier.bun run lintshould produce zero errors on a clean checkout. Biome overrides inbiome.jsonsuppress pre-existing patterns in legacy code:docs/*.html: Plausible analytics boilerplate (noCommaOperator,noArguments, etc.), Pico CSSrole="switch"without staticaria-checked(useAriaPropsForRole),forEachcallbacks with implicit returns (useIterableCallbackReturn)rest2raml.js:noDoubleEquals(string comparisons),useLiteralKeys(bracket notation for"get"),useConst/noVar(legacy style). Note:noUnusedVariablesandnoUnusedFunctionParametersare intentionally kept on — they catch real issues and provide valuable double-check discipline in agentic workflowsappyamlvalidate.js:useLiteralKeysdocs/restraml-shared.js:noUnusedVariables(exports consumed by other pages) Fix issues in any code you add or modify. Do not add new suppressions without justification.
- Dependencies are declared in
package.jsonwithbun.lock. - context7 MCP is configured in
.mcp.json. Use it to fetch up-to-date documentation forbun,biome, or any third-party CDN library rather than relying on training data. These tools evolve quickly — prefer context7 when uncertain about an API, CLI flag, or config schema.
The arm64 CI job now works under both KVM (when available) and TCG (software
emulation). Previous failures were caused by insufficient RAM (256 MB), not
TCG being inherently slow. Any workflow that installs all extra packages should
use 1024 MB RAM. See docs/deep-inspect.md for the full postmortem, measured
baselines, and implementation details.
RouterOS 7.22.1 (and likely all 7.x) returns non-deterministic results for request=completion
queries via the native API binary protocol. ~20-30% of calls randomly drop entries. REST is
unaffected. See docs/mikrotik-bug-native-api-inspect.md.
Workaround: All CI uses --transport rest (now the default). NativeRouterOSClient and
ros-api-protocol.ts remain in the codebase for potential use if MikroTik fixes the bug.
Research test files (benchmark.test.ts, native-api.test.ts) and the experimental CI
workflow (test-transport-equivalence.yaml) were removed as part of the REST-only decision.
These rules apply to all agents working on CI workflows in this repository:
-
Never increase a timeout without first understanding why the step is slow. If a step takes 10× longer than the measured baseline, the timeout is not the problem. ARM64 TCG on x86_64 boots in ~20s, not 600s. See
docs/deep-inspect.mdfor measured timing baselines. -
Always verify extra packages are actually installed. After any reboot step that activates packages,
GET /rest/system/packagemust show >10 packages. If it shows only["routeros"], the job MUST fail — not continue with base-only output. -
Check the output, not just the exit code. A green CI badge means nothing if the arm64 file has 577 args instead of 36,023. Add assertions for expected arg counts and diff ranges.
-
Do not violate deep-inspect design rules to work around bugs. Both arches must do their own live crawl, and per-arch files must be published before any merge. If the arm64 crawl is "too slow", the fix is to debug why, not to skip it.
-
Verify locally before pushing to CI. CI builds take 30–90 minutes per attempt. Use
scripts/experiment-arm64-reboot-timing.shfor local QEMU experiments (5–10 minutes). -
Give QEMU enough RAM. 256 MB is fine for base RouterOS under KVM, but with 17 extra packages under TCG, it causes memory pressure that inflates REST calls from ~70ms to ~10s+. Use 1024 MB for any job that installs extra packages (matches quickchr's default). This RAM/REST-stall behaviour is a separate issue from the
/console/inspect path=dohang tracked as MikroTik SUP-127641. Thedo-path hang on 7.20.8 reproduces identically at 128 MB and 512 MB (confirmed April 2026 viabun scripts/test-crash-path-memory.ts) — it is a pure code-level deadlock, not memory pressure. These are two distinct bugs. Seedeep-inspect.tsCRASH_PATHScomment block for the full per-version crash-path table. -
ARM64 QEMU boot timing reference:
Host → Guest Accelerator Boot time x86_64 → x86_64 KVM <5s x86_64 → aarch64 TCG ~20s aarch64 → aarch64 KVM/TCG <5s / ~25s aarch64 → x86_64 TCG >300s — NOT VIABLE
| Variable | Where set | Purpose |
|---|---|---|
URLBASE |
.env, workflow env: |
Base URL for RouterOS REST API, e.g. http://localhost:9180/rest |
BASICAUTH |
.env, workflow env: |
Credentials as user:pass, e.g. admin: (empty password) |
INSPECTFILE |
Optional, local | Path to pre-fetched inspect JSON — skips live router query |
| Package | Used by | Purpose |
|---|---|---|
js-yaml |
rest2raml.js, appyamlvalidate.js (Bun) |
Serialize RAML output as YAML; parse /app YAML for validation |
ajv |
appyamlvalidate.js (Bun) |
JSON Schema validation (draft-07) for /app YAML schemas |
ajv-formats |
appyamlvalidate.js (Bun) |
AJV plugin for format validators (uri, email, etc.) |
@apidevtools/swagger-parser |
validate-openapi.ts (Bun) |
OpenAPI 3.0 schema validation |
bun-types |
TypeScript files (dev) | Bun API type definitions for TypeScript |
raml2html |
(retired) | Previously generated HTML from RAML; replaced by OpenAPI 3 + API Explorer |
raml2html-slate-theme |
(retired) | Previously used as raml2html theme |
webapi-parser |
validraml.cjs |
RAML validation |
qemu-system-x86 |
CI workflows (apt) | Runs RouterOS CHR VM (Ubuntu package; provides qemu-system-x86_64 binary) |
qemu-utils |
CI workflows (apt) | Provides qemu-img for VDI→qcow2 disk conversion |
qemu-system-x86_64 |
Dockerfile.chr-qemu (Alpine apk) |
Runs RouterOS CHR VM in local Docker |
qemu-img |
Dockerfile.chr-qemu (Alpine apk) |
Provides qemu-img for VDI→qcow2 conversion |
github/codeql-action |
codeql.yml |
GitHub code scanning for JavaScript/TypeScript and Actions |
actions/dependency-review-action |
dependency-review.yml |
GitHub dependency review on pull requests |
restraml-shared.js |
docs/*.html pages |
Shared JS: version parsing, theme switcher, share modal, published docs index fetch |
restraml-shared.css |
docs/*.html pages |
Shared CSS: fonts, logo swap, theme icon, page-guide, share-modal, utility classes |
scripts/build-docs-index.mjs |
Build workflows | Generate docs/docs-index.json from the checked-in docs tree |