Skip to content

About

RouterOS /app container for T3 Code (x86_64 + ARM64, GHCR, CHR-tested)

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

44 Commits

Folders and files

Repository files navigation

app-t3code

RouterOS /app container for T3 Code — the agentic coding tool that runs as a server with a web UI. This repo builds multi-arch (linux/amd64 + linux/arm64) OCI images to ghcr.io/tikoci/app-t3code, ships the /app YAML to install them, and proves the whole path on real CHR VMs (x86_64 + ARM64) with Bun tests.

Target device: MikroTik RDS2216 (ARM64, ROSE edition — container-ready out of the box). Anything here that says CHR also applies to RDS, except the CHR-only workarounds are marked.

Install

Prerequisites on the router (one time):

  1. RouterOS 7.24.6 or newer (the minimum supported version).
  2. The container extra package installed.
  3. Device-mode with container=yes (RDS ships rose mode with it on; other hardware needs one physical confirm).
  4. A formatted disk for apps (see Storage).

Then install the app for production (HTTPS via /ip/cloud or your own domain):

/app/add yaml=([/tool/fetch url="https://tikoci.dev/app-t3code/app/t3code.tikapp.yaml" check-certificate=yes output=user as-value]->"data") use-https=yes
/app/set [find where name="t3code"] disabled=no

Note

When using CHR, such as with tikoci/quickchr, change use-https=yes to use-https=no. Free-license CHR has no /ip/cloud for certificate provisioning. CI and the test commands below use this setting.

Once the container is running, follow Pairing with the web UI to get its browser URL and a one-time pairing token. Storage persists on the app disk, so logins, the T3 database, and ~/projects survive reboots.

Preview or edit the YAML

See in /app YAML Editor to inspect, validate, or customize the single-app definition in our sister project, tikoci/restraml. The share link embeds the YAML as a snapshot; the downloads below track the current published files. Both YAML files include schema comments for editor validation.

YAML downloads and app store

GitHub Pages mirrors the repository paths for Markdown and the two app YAML formats, serving the original files without rendering or rewriting them:

The install command above fetches the app YAML into memory and passes its contents to /app/add yaml= for a one-time installation.

Tip

Alternatively, add our app store to the router's catalog. This appends to app-store-urls, preserving existing stores. Before you install the app, the catalog can pick up changes to our template so installation uses the latest YAML we publish.

/app/settings/set app-store-urls=([get app-store-urls],"https://tikoci.dev/app-t3code/app/t3code.tikappstore.yaml")

The store requires a YAML array, so use .tikappstore.yaml for app-store-urls, and .tikapp.yaml for a single app installation. See MikroTik's custom app store documentation.

Storage

Use a separate disk for apps; the RouterOS root volume cannot be used. Format the app disk as ext4 or btrfs, then set its mount point in /app/settings. /app refuses to enable anything until this is configured:

/disk/print            # find the extra disk, e.g. *1
/disk/format *1 file-system=ext4
/app/settings/set disk=<mount-point>

The disk can be any supported storage type with enough space for the image and persistent data. On CHR, attach a second virtual disk. The app then lives under <disk>/apps/t3code/. The image is about 0.8GB compressed and 3GB extracted (amd64 build, 2026-11-07). The default tool groups add about 0.7GB in /data after the first start (see Optional tool groups), and up to about twice that after an image update that refreshes them, because the previous payloads are kept until t3app tools prune. Allow at least an 8GB app disk, plus space for projects, browser downloads, and any quickchr lab disks. Builds need extra space for intermediate layers and loading the finished image.

What survives an image replacement

The existing named volume t3code-data:/data holds the entire home. Both the image and app YAML explicitly set HOME=/data and T3CODE_HOME=/data/.t3. Keep the app name, volume name, and mount destination when upgrading. The executable in /opt/t3code comes from the new image; your home comes from the existing disk volume.

Container path Contents
/data/.t3/userdata T3 databases, settings, environment identity, pairing/auth state, secrets, attachments, browser profiles
/data/.t3/projects, /data/.t3/scratch, /data/.t3/worktrees T3-managed projects, projectless threads, and worktrees (including uncommitted files)
/data/projects Repositories you create or clone here
/data/.config, /data/.local/share, provider homes Provider configuration, logins, and session history
/data/.local, /data/.bun User-installed tools and Bun global packages

Projects outside /data need their own persistent mount. A project record in the database does not preserve a checkout stored in the container's writable layer. Provider settings that override a home or XDG path must also point inside persistent storage.

Keep one home volume: splitting only userdata from the rest of .t3 can preserve project records while losing their worktrees or scratch files. Do not clear .t3 during upgrades. Its runtime, tools, and caches directories contain downloadable runtimes and caches, but worktrees and userdata contain user work. Use T3's storage cleanup settings to manage disk use. The paths above are grounded in upstream's server path definitions and workspace folders.

Back up the complete /data volume with the app stopped before upgrading (databases and their sidecars must be copied together). Keep external project volumes in the same backup. Update the installed app in place:

/app/update [find where name="t3code"]

If the registry image is unchanged, this may retain the existing root filesystem. Existing installations receive the new image's home defaults and tools through this command; they do not need the YAML reapplied.

Warning

On CHR 7.25.1, assigning /app/set ... yaml=... recreated the app storage and deleted the existing named-volume contents. This reproduced with a comment-only YAML change on a running app. Treat any YAML change as a migration: stop the app and back up the complete /data volume, external project volumes, and every other app-owned volume somewhere outside <disk>/apps/t3code/. Record each volume's source and container destination. Apply the YAML, then keep the app stopped while restoring the home and other app-owned volumes into their newly selected mount sources. Verify external project volumes are mounted at their recorded destinations; restore their backups if their data was removed or their storage moved. Verify ownership and mount mappings for every volume before enabling the app. Splitting the home into more app-owned volumes does not avoid this reset.

RouterOS /app cleanup deletes app data as well as the image; it is a factory reset, not an upgrade. Deleting/recreating the app or changing the app disk also requires a backup and an explicit restore to the intended home volume.

If an upgrade appears to have lost projects, stop the app and check its /container mount mapping and the old app disk before creating new projects. Check whether the original .t3/userdata/statev2.sqlite and checkout folders still exist. A new mount, changed home, or a checkout outside /data can look like data loss even when the original files are still on disk.

Using T3 Code

The container serves plain HTTP on 3773 (T3CODE_HOST=0.0.0.0, T3CODE_PORT=3773). Put HTTPS in front for anything beyond localhost — the hosted web/mobile T3 apps require it.

Pairing with the web UI

The browser needs a one-time token to connect to this container. The pairing screen shows how to get one: the image adds a short "Running on MikroTik RouterOS?" panel under T3's token field with this command, which prints a fresh token:

:put [/container/shell app-t3code cmd="t3app pairing" as-value]

The container log has the same command at every start. If you renamed the app, use its container name (/container/print) in place of app-t3code.

To get the browser URL as well, run this in the RouterOS terminal to print both the app's browser URL and a fresh token from t3app pairing inside the app-t3code container:

:put "** Using T3Code **\r\n\r\nIn your browser go to: $[/app/get t3code ui-url]\r\n\r\nThen, use the Token below in your browser to connect it with this container:\r\n$[/container/shell app-t3code cmd="t3app pairing" as-value]"

For example (illustrative token):

** Using T3Code **

In your browser go to: http://10.0.2.15:3773/

Then, use the Token below in your browser to connect it with this container:
Issued client pairing token 00000000-0000-4000-8000-000000000000.
Token: EXAMPLEONLY
Expires at: 2026-11-03T14:07:30.038Z

Open the printed URL and enter the value on the Token: line in the browser's pairing prompt. Tokens expire after about five minutes; run the command again if you need a fresh one.

Note

ui-url reflects the app's use-https setting. With use-https=yes, it reports the HTTPS URL (including the MikroTik hostname when using /ip/cloud). With use-https=no, it reports an HTTP URL like the CHR example above.

Note

T3 Code also prints a pairing token, URL and QR code to /container/log at every start, including the first one after install. That token expires after about five minutes, and the URL uses the container's internal address, so use the app URL and the command above instead. A RouterOS user in the read group can see that token too; see SECURITY.md.

If PUBLIC_URL is set in the container, t3app pairing can print a full pairing URL instead of a raw token.

The t3app command

t3app is the image's operator command: status, tool groups, provider CLIs and pairing, one command with an area and a verb. From RouterOS it runs through /container/shell, as the app user (node):

/container/shell app-t3code cmd="t3app status"
/container/shell app-t3code cmd="t3app tools install"
/container/shell app-t3code cmd="t3app providers install"
/container/shell app-t3code cmd="t3app tools prune"
:put [/container/shell app-t3code cmd="t3app pairing" as-value]
Command What it does
t3app status [--json] Tool groups and provider CLIs in one report: selections, states, last errors, log paths. Read-only
t3app tools status|install|prune|groups Optional tool groups; t3app tools help has the details
t3app providers status|install Provider CLIs; t3app providers help
t3app pairing [label] A fresh pairing token (a full link with PUBLIC_URL set)
t3app help The command reference

RouterOS drops a /container/shell command's output after about 60 s, so tools install and providers install start in the background and answer at once with the log to follow. t3app status shows the progress. In a T3 terminal, --wait runs them in place instead. A mistyped command or an extra argument prints the usage and exits 2, and nothing runs:

/container/shell app-t3code cmd="t3app tools install now"

Exit codes are 0 for ok, 1 when the operation failed, 2 for usage, and 3 for the wrong user. The shell runs as node unless you add user=root. As root, the tool commands switch to node (no file in /data becomes root-owned), and providers install refuses with exit 3.

Important

Renamed settings and commands (breaking). T3CODE_TOOL_GROUPS, T3CODE_PROVIDERS and T3CODE_TOOLS_HOME are now T3APP_TOOL_GROUPS, T3APP_PROVIDERS and T3APP_TOOLS_HOME. The old names are ignored, with one line in /container/log at start that names them, so an install that set them gets the defaults after /app/update until you set the new keys (see Changing groups on RouterOS). t3code-tools, t3code-providers and t3-pairing-link are now t3app tools, t3app providers and t3app pairing. Upstream T3's own T3CODE_* settings (T3CODE_HOST, T3CODE_PORT, …) are unchanged.

Environment and providers

Env Default Notes
T3CODE_HOST / T3CODE_PORT 0.0.0.0 / 3773 server bind
T3CODE_TELEMETRY_ENABLED false (image default and YAML) set true to opt back in
T3APP_TOOL_GROUPS capture labs vpn routing dev (image default and YAML) optional tool groups; remove names to skip them, none for core only; see below
T3APP_PROVIDERS claude codex (image default and YAML) provider CLIs to install into the home; see Providers
UV_PYTHON_DOWNLOADS manual (image default) uv uses the image's Python 3 and downloads another only on uv python install
PUBLIC_URL unset base URL baked into pairing links
ANTHROPIC_API_KEY, OPENAI_API_KEY, … inherited agents see container env
T3CODE_* passthrough e.g. T3CODE_SERVER_BROWSER_SANDBOX=0

Providers (T3APP_PROVIDERS)

No provider CLI is baked into the image. T3APP_PROVIDERS picks which ones install into the home, in the background after T3 starts, one explicit token per provider in T3's main Add provider list:

Token Provider How it arrives Size in /data (amd64)
claude Claude Code (claude) install-claude-code: the stable channel's native build, fetched as its .zst (about 99 MiB instead of 280 MiB) and size- and SHA-256-checked against Anthropic's manifest after decompressing, set up with Claude's own install (/data/.local/share/claude/versions/<ver>, linked from /data/.local/bin/claude). Its license forbids baking it into the image about 280 MiB
codex Codex (codex) npm install -g @openai/codex into /data/.local about 437 MiB
opencode OpenCode (opencode) npm install -g opencode-ai into /data/.local (npm installs both the regular and the baseline x64 build) about 409 MiB
pi Pi (pi) npm install -g --ignore-scripts @earendil-works/pi-coding-agent into /data/.local (T3's own install hint) about 152 MiB
grok Grok (grok) xAI's installer (https://x.ai/cli/install.sh): /data/.grok, linked as grok and agent from /data/.local/bin. The installer does not verify a checksum, and it adds its bin directory to ~/.bashrc about 192 MiB (arm64 164 MiB)
muse Muse Code (muse) Meta's installer (https://dev.meta.ai/install.sh, with MUSE_NO_MODIFY_PATH=1): a launcher in /data/.local/bin about 396 MiB
cursor Cursor nothing to install: Cursor's SDK ships inside T3. Sign in from Settings → Providers 0
antigravity Antigravity nothing installed by this image: T3 downloads its runtime into /data/.t3/tools/antigravity-acp when you install it from Settings → Providers about 916 MiB, once T3 installs it

The default is claude codex, set in the image ENV and both YAML files. Add tokens to install more ("claude codex opencode pi"); none installs nothing. Like the tool groups, it is an explicit list with no all, so a provider T3 adds later never starts downloading by itself. Change it the same way as the groups: with /app/set environment=, never the YAML (see Changing groups on RouterOS, with T3APP_PROVIDERS= as the key).

How the installs behave:

  • Only while missing. A provider installs only while its CLI is not on PATH. A CLI you installed yourself, or one T3's welcome screen installed, always wins and is never replaced.
  • T3 owns updates afterwards. These are the same installs a user would make, so T3's Update now recognizes them. For an npm install it runs npm install -g against /data/.local; for the others it runs the CLI's own updater (claude update, grok update).
  • Deselecting deletes nothing. Removing a token stops future installs. The CLI, its login and its history stay in /data; delete them yourself if you want the space back.
  • Installing is not enabling. Which providers T3 shows and uses is still Settings → Providers. T3 re-checks providers every 5 minutes and whenever Settings → Providers opens, so a CLI that finishes installing after the welcome screen appeared shows up then.
  • Retries. The background run retries for about a day (15 s, 1 m, 5 m, 15 m, 30 m, then hourly). t3app providers install retries now, and t3app providers status shows each provider's state, where its CLI resolves and its last error.
  • Disk. Each install needs about twice its size free in /data (the download or npm cache plus the install), plus 256 MiB. With less, that provider fails with a low disk error, nothing is written, and the retry schedule applies; T3 keeps running.
  • Vendor installers. grok and muse run the vendor's current install.sh over HTTPS, the same command their docs give. Nothing is pinned, so the vendor's own updater keeps working; the trust is the vendor's endpoint, as it is for a user running that command.
  • Logs. All background installs log under /data/.local/state/t3code/: providers.log (the provider installs, including Claude's) and tool-groups.log (the tool groups). Each start appends; the logs rotate at about 1 MiB and survive image replacement.

Codex also has a second path in T3: Continue with ChatGPT downloads T3's own managed Codex into /data/.t3/tools/codex, separate from the codex CLI. Use existing CLI uses the CLI installed here.

Other T3 drivers (Kiro, fx, and agents from the ACP registry, under Show more in T3) are not installed by this image. Install them yourself from a T3 terminal, into /data/.local/bin or another directory in the home, or open an issue to ask for a token. A token this image does not know is ignored with a warning in the container log. Provider logins, settings and sessions for every provider live in the home.

Important

Upgrading from an image that baked Codex and OpenCode. Earlier images carried codex and opencode in /usr/local. After /app/update, the default installs claude and codex into the home on first start (Codex is a download of about 166 MiB, then about 437 MiB in /data). OpenCode is no longer installed by default: add opencode to T3APP_PROVIDERS to keep it. Logins, settings and sessions in /data stay either way.

Installed package inventory

The image includes Debian command-line basics (bash, awk, sed, grep, find, diff, tar, gzip), Git/SSH, the GitHub CLI (gh), curl, jq, ripgrep, make, the sqlite3 CLI, zstd, Python 3 with uv and uvx, and Node.js/npm in addition to the agent and RouterOS tools below. This is a selected toolset; make does not imply a C/C++ compiler, and other language SDKs are out of scope. The native libraries for T3's built-in browser are core too (T3 downloads the browser itself on first use).

gh enables T3's GitHub source-control provider. Log in once with gh auth login from a T3 terminal, or set GH_TOKEN. The container has no keyring, so gh auth login stores the token in a file under gh's default config directory, /data/.config/gh, which survives image replacement. If you set GH_CONFIG_DIR, keep it under /data, or the login is lost on the next update. T3's other providers (GitLab, Forgejo/Gitea, Azure DevOps, GitCafe) need CLIs this image does not bundle yet; see issue #15. Plain git over HTTPS or SSH works with any forge.

Python has no pip or python3 -m venv; use uv instead. uv venv and uv sync create project environments with the image's Python, uvx <tool> runs a Python CLI in an isolated environment, and uv tool install puts its command in /data/.local/bin. uv's cache (/data/.cache/uv) and tool environments (/data/.local/share/uv/tools) are in the home, so they survive image replacement. UV_PYTHON_DOWNLOADS=manual keeps uv from fetching a managed Python unless you run uv python install. A venv built against one Python minor version may need recreating after an image update changes it. The image pins uv with the UV_VER build argument.

Every image build records its installed Debian packages (including transitive dependencies), versions, architecture, installed sizes, selected executable availability, runtime versions, and top-level global npm/Bun packages in /usr/share/t3code/inventory/. Read README.md, packages.tsv, commands.tsv, or inventory.json there. tool-groups.json holds t3app tools status --json --groups all as evaluated at build time. These describe the image before first boot; Claude, browser downloads, and tools installed in your home are outside this inventory. A command on PATH can also be a downloader shim.

The CI workflow uploads image-inventory-<source SHA> with separate linux_amd64/ and linux_arm64/ directories, build metadata, the source commit, and the actual CI host's full Debian/Ubuntu package inventory under runner/. Artifacts are retained for 90 days. Published builds also record the image digest; PR builds have no published registry digest. Curated documentation should cite the build/architecture it describes rather than copying a timeless package list into this README.

To inspect a local build without starting T3 or running the first-boot installer:

bun lib/image-inventory.ts --image app-t3code:test --output image-inventory

See the GitHub runner comparison and package recommendations for a dated amd64 snapshot, pinned Ubuntu 24.04/26.04 runner data for both architectures, dependency-cost estimates, and the proposed additions. Package choices are tracked in issue #5; the core network tools and group assignment are in Optional tool groups.

Optional tool groups

T3APP_TOOL_GROUPS selects optional tool groups from the same image and the same /app YAML. There are no profile tags or separate apps. Separate names with spaces, for example "capture labs". The parser also accepts + and commas, but RouterOS splits /app/set environment= values on commas, so use spaces on a router. Names are case-insensitive. An unknown name is logged as a warning and ignored. T3 always starts, whatever the selection says.

Value Selects
capture labs vpn routing dev the default: every current group, as an explicit list (the image ENV and the YAML both set it)
unset or empty the same default (a key dropped by /app/set environment= falls back to it)
a shorter list, e.g. "capture labs" only those groups
none core only
all every group, including groups added in later images

The default is a list rather than all so that a group added in a later image never starts downloading on an existing install by itself: you opt in by adding its name. Remove names to skip groups.

All five groups are delivered as persistent payloads in the home (dev from npm, the others from Debian packages; see Persistent payloads). They install one at a time in the background after T3 starts. With the default, the first start downloads about 120 MiB (about 75 MiB of .deb files plus the dev npm packages) and needs roughly 660 MiB more in /data once all five are installed (amd64 measurements in the table below), plus 256 MiB of headroom while installing. After an image update that changes the package pins, the refreshed payloads install next to the previous ones, which are kept (an offline fallback) until t3app tools prune: budget about twice that. On a fast link that takes a few minutes; at 1 Mbit/s plan for about 15 minutes. With too little space a group fails with a low disk error and T3 keeps running.

Important

Upgrading from an image before the five-group default. Older images treated an unset T3CODE_TOOL_GROUPS as core only. After /app/update to this image, an install that never set the key starts downloading all five groups in the background. To keep core only, set none first (see Changing groups on RouterOS); the setting survives /app/update. Never reapply the YAML to change it.

The same update removes the bundled muse binary. Muse logins and history in /data stay; add muse to T3APP_PROVIDERS to install it into the home again (see Providers).

browser and agents-extra are no longer groups. T3's browser libraries are core, and the provider CLIs are described in Environment and providers. A selection that still names them logs an unknown-group warning and otherwise works.

Group Contents Download → on disk (amd64, this image's locks)
capture tcpdump, TShark (dumpcap, capinfos, editcap, mergecap, text2pcap), nfdump (nfcapd) 31.1 MiB → 156 MiB (46 packages)
labs quickchr, QEMU x86/ARM64 emulators, qemu-img, SeaBIOS and ARM64 UEFI firmware 32.7 MiB → about 309 MiB after pruning firmware (32 packages); quickchr is in the image
vpn WireGuard tools (wg, wg-quick), strongSwan (pki, swanctl, charon), OpenVPN, ike-scan; no tunnel or service starts by default 3.6 MiB → 12 MiB (15 packages)
routing BIRD 2 (bird, birdc), ndisc6, arping, arp-scan, nftables, conntrack, nmap 8.3 MiB → 33 MiB (21 packages)
dev TypeScript, CSpell, markdownlint-cli2 and Biome fallbacks; project lockfiles win npm packages → about 154 MiB

Installing a tool does not grant the privileges to use it, and a group being ready means its files are installed and load, not that every operation is allowed. Live capture, raw-socket peers, tunnels and firewall changes need capabilities or devices that a RouterOS container may not have; see Privileges by tool. Each group's privileges field (and core.privileges) in the status output says the same. Granting them is an operator deployment choice: nothing in this image changes RouterOS settings, starts a privileged helper, or ships an alternate YAML.

At a glance, by what the operation needs beyond installation:

Needs Core capture labs vpn routing dev
Nothing client tools, UDP traceroute, ip/ethtool queries capture files, dumpcap -D, nfcapd ≥1024 TCG guests, user networking, qemu-img keys, certificates, ike-scan --sport=0 BIRD ≥1024, nmap -sT, ARP decode from files everything
CAP_NET_RAW traceroute -T/-I, radsniff live, mtr (bounding set), ping without ping_group_range live capture — — ARP/ND probes, raw scans, OSPF —
CAP_NET_ADMIN ip/ethtool changes — TAP (with /dev/net/tun) WireGuard and OpenVPN tunnels (OpenVPN with /dev/net/tun) nft, conntrack, kernel route sync —
CAP_NET_BIND_SERVICE (ports <1024, where restricted) low-port listeners — — ike-scan source port 500 BGP on 179 —
A device — — /dev/kvm for acceleration /dev/net/tun for OpenVPN — —
Root — — — wg-quick, charon — —

RouterOS 7.25 added a per-service privileged mode to app YAML. That is a deployment control; which effective capabilities the non-root app process then receives, and which devices and kernel features a given router provides, is unproven and tracked in #7.

Privileges by tool

The app runs as node (UID 1000) with no capabilities. "None" means the operation works that way. Other entries name what it needs: a host or namespace setting (such as net.ipv4.ping_group_range for ping), a capability, a device, or root. Most of those mean a privileged container. Whether RouterOS can provide any of them is #7. Tested means run here as node with CapEff 0 under Docker (habitat C): works or refused (with a permission error). Expected means not demonstrated.

Where Tool / operation Needs Tested
core dig, whois, SNMP, RADIUS (radclient) and MQTT clients, curl, ssh, centrs None expected
core nc, socat, iperf3 None on ports ≥1024; CAP_NET_BIND_SERVICE below that where the namespace restricts it expected
core ip, ethtool None to query; CAP_NET_ADMIN to change anything expected
core traceroute (UDP, default) None expected
core traceroute -T/-I CAP_NET_RAW refused
core ping ICMP echo sockets allowed by net.ipv4.ping_group_range (Docker allows all groups); otherwise CAP_NET_RAW. The image's ping has no file capability works under Docker
core mtr Its helper mtr-packet has a cap_net_raw file capability: CAP_NET_RAW must be in the container's bounding set works under Docker
core radsniff (from freeradius-utils) None for files; CAP_NET_RAW to capture refused
capture tshark -r, tcpdump -r, capinfos, editcap, mergecap, text2pcap, nfdump, dumpcap -D None works
capture nfcapd on a port ≥1024 None works (NetFlow v5)
capture tcpdump -i, dumpcap -i, tshark -i CAP_NET_RAW refused
vpn wg genkey/pubkey, pki keys/CA/certificates, openvpn --genkey, ike-scan --sport=0 None works
vpn swanctl A running charon's VICI socket no charon (reported, not refused)
vpn WireGuard interface (ip link add … type wireguard, wg set) CAP_NET_ADMIN and the kernel module (no /dev/net/tun) expected
vpn wg-quick up Root: it re-runs itself with sudo, which the image lacks fails as node (sudo: not found)
vpn OpenVPN tunnel CAP_NET_ADMIN and /dev/net/tun expected
vpn charon (IKE daemon) Root; shipped but not wired or proven expected
vpn ike-scan default source port 500 CAP_NET_BIND_SERVICE where ports below 1024 are restricted expected
routing bird -p, BGP on ports ≥1024, birdc on its socket None works (eBGP prefix exchange)
routing BGP on port 179 CAP_NET_BIND_SERVICE where ports below 1024 are restricted (Docker sets ip_unprivileged_port_start=0) expected
routing BIRD OSPF; kernel route sync CAP_NET_RAW; CAP_NET_ADMIN expected
routing nmap -sT, NSE script data None works
routing nmap -sS and other raw scans, raw NSE scripts CAP_NET_RAW refused (-sS)
routing arping, arp-scan on an interface CAP_NET_RAW refused
routing arp-scan --readpktfromfile (decode a capture, with vendor names) None works
routing rdisc6, ndisc6 CAP_NET_RAW expected
routing nft (even nft -c), conntrack -L CAP_NET_ADMIN and netfilter netlink refused
labs quickchr / QEMU with TCG and user networking, qemu-img None works (CHR 7.25.1 x86 and arm64 guests booted from the payload on amd64; ARM64 firmware discovery shipped in quickchr 0.5.6)
labs KVM acceleration /dev/kvm readable and writable by node, and host support fails without the device under Docker ("Could not access KVM kernel module"); untested with one
labs TAP networking CAP_NET_ADMIN and /dev/net/tun (or a TAP pre-created for node) fails without the device under Docker (could not open /dev/net/tun); untested with one
core T3's built-in browser sandbox User namespaces, if the host allows them expected
providers Provider CLIs in the home (claude, codex, …) None expected
dev everything None works

Tools changed or moved because of privileges, not size:

  • Moved to a Linux guest under labs: pppd, xl2tpd, sstpc, pppoe and lldpd. They cannot work from a payload under /data even in a privileged container. Selecting labs does not create that guest or install them: boot a Linux VM with QEMU yourself, then install them in it with its own package manager (as root, in the guest). pppd needs root and reads /etc/ppp/options before its arguments. xl2tpd and sstpc start /usr/sbin/pppd by a compiled-in path. lldpd needs root and a _lldpd user for privilege separation (no _lldpd user for privilege separation).
  • Shipped, but needs root: wg-quick and charon. As node, use wg with ip link for WireGuard, and pki for IKEv2 certificates.
  • Unprivileged defaults in tests and docs: ike-scan --sport=0, BIRD BGP on ports ≥1024, nmap -sT, NetFlow collection on ports ≥1024.
  • Readiness does not depend on privileges: nft and conntrack open a netfilter netlink socket even for --version. If a kernel (or QEMU user-mode emulation) lacks it, their probe still counts the tool as loaded, so one missing kernel feature does not fail routing.

Group status for agents

t3app tools status            # one line per group
t3app tools status --json     # runtime inventory for agents
t3app tools status --groups "vpn routing"   # evaluate another selection
t3app tools groups            # definitions and the network package assignment

Each group reports selected, a state, and whether its checks found the files (present, missing). The states are:

State Meaning
not-selected Not in T3APP_TOOL_GROUPS
pending Selected; waiting for the background installer
installing The background installer is working on it, including waiting to retry (retrying at … in t3app tools status)
ready Selected, and every check passed
failed Selected, and checks failed for tools the image should provide, or the last install failed and nothing usable is active
not-implemented Selected, but this image has no way to provide the group yet (no group today)

The selector reports defaulted: true when an unset or empty value selected the default list.

Checks are presence probes (PATH, files, ldconfig), not functional tests. Payload groups are checked at their own shims and manifest instead, so the result does not depend on the caller's PATH. labs never boots a CHR to check readiness; its install-time smoke tests boot firmware only (SeaBIOS to "No bootable device", and a 16-byte aarch64 program that powers the machine off). For a payload group, a file check is a path inside the payload, reported through groups/<group>/current/root/…. Core checks (t3, Node, Bun, Python with uv/uvx, sqlite3, zstd, Git/SSH, gh, centrs, the core network tools and the browser's native libraries) are reported separately as core.state, and each provider's state as providers (details: t3app providers status). At each start the entrypoint logs warnings and a summary to the container log and writes the same JSON to /tmp/t3code-tools/status.json. The output also points at the immutable build inventory in /usr/share/t3code/inventory.

Persistent payloads

A payload group installs into the home volume, not the image, so it costs nothing in the image (it downloads only while selected) and survives image replacement and auto-update. dev is built from an npm lockfile; capture, labs, vpn and routing from pinned Debian packages (see Debian-package payloads). All share the same lifecycle:

  • Start never waits. At each start the entrypoint relinks payloads that are already in the home (offline, no download) and then runs t3app tools install in the background. T3 is serving while a first install is pending or installing.
  • Pinned and verified. For dev, the image ships the group's package.json and package-lock.json (with npm integrity hashes); the installer runs npm ci --ignore-scripts from them. Each command must answer --version (or its group's declared probe) before the payload gets a manifest.json and is activated. The Debian groups also run functional smoke tests: capture decodes a test capture, vpn generates a strongSwan key and a WireGuard key, routing loads nmap's NSE help and parses a BIRD BGP config. A payload without a manifest is never trusted.
  • Versioned and atomic. The payload id is derived from the lock and the runtime: CPU architecture and Node's module ABI for dev, dpkg architecture and Debian release for the Debian groups. Payloads live in /data/.local/share/t3code-tools/groups/<group>/<id>/; a current link is swapped atomically, and shims in /data/.local/share/t3code-tools/bin are on PATH after /data/.local/bin and /data/.bun/bin. A tool you install yourself wins, and a project's own node_modules/.bin (bun run, npx) still wins.
  • Refresh without downtime. A new image with a changed lock (a security refresh) or a different Node ABI wants a new payload. The previous one stays active and the group stays ready (payload.stale: true) until the new one is verified. If the refresh fails, the previous payload keeps working. The installer keeps the active payload and the one before it and deletes older ones.
  • Bounded. Groups install one at a time under a lock (a kernel flock on install.lock, released automatically if the holder dies; the next run also stops an npm or dpkg-deb left behind by a killed installer before reclaiming its files). Retries run in rounds: each round tries every group still missing once, so one failing group never holds up the others, and the lock is released while waiting (a round that finds it taken, say by a prune, waits and tries again without using an attempt). The entrypoint's background run (install --keep-trying) keeps retrying for about a day (15 s, 1, 5, 15 and 30 minutes, then hourly), because a router often starts the container before its uplink or DNS is up; verified .deb files are kept between attempts. Each attempt is capped at 30 minutes. t3app tools install starts a run of three attempts in about 75 s in the background (--wait runs it in place). The installer needs room for the payload and its downloads (for dev, twice the payload size; for the Debian groups, the pinned .deb sizes not already verified in the cache, plus their unpacked sizes) plus 256 MiB, and otherwise fails with a low disk error rather than filling the volume. It never deletes projects, VMs or other home data. Interrupted installs (.staging-*) are removed on the next run.
  • Deselecting a group removes its shims at the next start but keeps its payload, so selecting it again needs no download. t3app tools prune deletes payloads that no selected group uses, and any kept .deb downloads. Switching to none therefore frees no space until you run t3app tools prune (in a T3 terminal or through /container/shell); prune touches only /data/.local/share/t3code-tools.

t3app tools status --json reports payload.desired, payload.active, stale, installed, the verified activeVersions and the last installer result with its error. The installer log is /data/.local/share/t3code-tools/install.log. t3app tools install retries now, including while the background installer waits; otherwise the next start retries a group that ran out of attempts. If an install is already running, it says so and starts nothing. t3app tools help is the command reference (commands, selection syntax, files, exit codes). install, prune and init run as the home's owner: started as root (/container/shell user=root, docker exec -u 0), they re-execute as node with setpriv, so no payload file becomes root-owned.

The dev payload is about 154 MiB on amd64 (TypeScript 7.0.2, CSpell 10.4.0, markdownlint-cli2 0.24.0, Biome 2.6.5). Maintainers refresh the pins with make dev-lock after editing lib/groups/dev/package.json.

Debian-package payloads

capture is too big to bake into every image (about 157 MB more on disk and 42 MB more to pull on every image update, measured on amd64), so it is a payload too. vpn and routing are smaller, but use the same machinery rather than adding 13 MB to every image pull for routers that never select them (write-up). labs took 565 MiB of QEMU and firmware out of every image (write-up). Each installs as the app user with no root at runtime:

  • Pinned at image build. The Dockerfile runs lib/deb-lock.ts after the last apt layer. It asks apt for the group's dependency closure that the image does not already have, and records each package's version, architecture, size and SHA256 from the apt indexes. apt has already checked those indexes against Debian's signing keys. The lock is /usr/local/lib/t3app/groups/<group>/debs.lock.json (also in the build inventory as <group>-debs.lock.json). Every image rebuild picks up Debian security updates, and a changed lock is a new payload id, which refreshes as described above. An unchanged lock keeps the same id, so nothing is downloaded again.
  • Verified before extraction. Each .deb is downloaded from deb.debian.org (https first) and must match its pinned size and SHA256. A download is cut off as soon as it exceeds the pinned size. Its control fields must also name the pinned package, version and architecture. Only then is it unpacked with dpkg-deb -x. Maintainer scripts never run. If a superseded version has left the mirror, it is fetched from snapshot.debian.org and checked the same way. Verified downloads survive an interrupted install in groups/<group>/.cache-debs, so a slow link resumes rather than starting over. They are removed once the payload is ready.
  • No global library path, no /usr overlay. Each command gets a small wrapper in the payload's bin/ that sets LD_LIBRARY_PATH to the payload's own library directory for that process and its children only, then runs the real binary from the payload. The payload holds only packages the image lacks, so it does not replace the image's copies. If apt would upgrade one of a group's root packages (for capture: tcpdump, tshark, wireshark-common or nfdump) already in the image, lock generation fails: update that image package before rebuilding. An older tool answering --version is not proof that its security update was applied. For other image dependencies, the lock records proposed upgrades under imageUpgrades and the build log warns, but the package is not extracted. Version probes and smoke tests catch a tool that cannot load at all, but they do not cover every operation, so a payload can be activated and still fail later for want of a newer image library. If imageUpgrades lists a library the tools use, rebuild the image. Wireshark finds its data files and plugins relative to the binary, so tshark decodes with its full dissector set, RADIUS dictionaries and plugins from the payload; nmap finds its NSE scripts and service names the same way. BIRD takes its config and socket paths as arguments (bird -c … -s …, birdc -s …); the image has no /etc/bird or /run/bird. strongSwan only knows compiled-in paths (/etc/strongswan.conf, /usr/lib/ipsec/plugins), so the wrappers of pki, swanctl and pt-tls-client default STRONGSWAN_CONF and SWANCTL_DIR to the payload's copies (set either yourself to use your own config; put it outside the payload, which a refresh replaces) and LD_PRELOAD the payload's plugins by file name. Only plugins that load against libstrongswan alone are preloaded (the crypto, encoding and credential ones; checked at install time and recorded in the manifest under deb.wired); strongSwan finds a preloaded plugin by symbol before it searches its plugin directory. Like a stock Debian install, pki and swanctl still print failed to load lines for plugins Debian builds elsewhere or that need charon. arp-scan only looks for its vendor files in the current directory and at compiled-in paths, so its wrapper passes the payload's --ouifile and --macfile first; your own options come later and win. Debian's QEMU looks for its BIOS, option ROMs and keymaps only in directories compiled into it (/usr/share/qemu, /usr/share/seabios, /usr/lib/ipxe/qemu), so the wrappers of the qemu-system-* emulators pass the payload's copies with -L first (QEMU searches -L dirs in order, so a -L of your own comes after them) and default QEMU_MODULE_DIR to the payload's module directory (display, USB and virtio-gpu modules). quickchr finds the ARM64 UEFI images by asking that QEMU for its data dirs (qemu-system-aarch64 -L help, shipped in quickchr 0.5.6, available on @next). Images built with older quickchr need rebuilding to gain ARM64 guest support with this payload. A group can also prune files it never uses after extraction: labs drops the secure-boot AAVMF variants (four 64 MiB images) and keeps the pair quickchr boots (AAVMF_CODE.fd, AAVMF_VARS.fd). The prune list is part of the payload id and recorded in the manifest under deb.pruned. A payload command never shadows a command of the same name in the image, including one the image gains later. A required command that the image already provides (quickchr in /opt/bun/bin) is checked and used from the image.
  • Stale native payloads are checked. While a refresh is pending after an image update, the previous payload stays linked only if every required command it still provides passes its version probe against the new image. Commands now provided by the image use the image copy instead.

Measured on amd64 (habitat C, Debian trixie, Wireshark 4.4.19, tcpdump 4.99.5, nfdump 1.7.5): 42 packages, 30.5 MiB to download, 152 MiB unpacked, about 5 s to install on a fast link (computed: about 4.3 minutes at 1 Mbit/s). Wrapper overhead is about 1 ms per command. On arm64 the same code installed and passed the decoding, capture-file and NetFlow checks under QEMU user-mode emulation on an amd64 host. That is not native hardware, and interface listing and the live-capture refusal could not be tested there. See the spike write-up.

Measured on amd64 for the other two (same image): vpn is 14 packages, 3.5 MiB to download and 11.4 MiB on disk; routing is 21 packages, 8.3 MiB and 32.5 MiB. labs is 32 packages, 32.7 MiB to download, 564 MiB unpacked and 309 MiB on disk after pruning 256 MiB of firmware, about 6 s to install on a fast link (computed: about 4.6 minutes at 1 Mbit/s). Removing QEMU from the image also moved a few libraries it used to supply into the capture and vpn closures, so homes with those payloads download them once more after that image update.

Installing capture grants no capture privileges. Offline decoding (tshark -r, tcpdump -r, capinfos, editcap, mergecap, text2pcap), dumpcap -D (listing interfaces) and an nfcapd NetFlow collector on a port of 1024 or above (tested with NetFlow v5) all work as the unprivileged app user. Live capture with tcpdump -i/dumpcap -i needs CAP_NET_RAW and is refused without it. Without privileges, save a capture on the router (/tool/sniffer with file-name=) and copy the file over, or point /ip/traffic-flow at nfcapd. Receiving a live TZSP stream without privileges needs a UDP listener in front of the decoder and is not tested here.

vpn and routing grant nothing either. Without privileges, the strongSwan pki CA and certificate workflow (the same certificates RouterOS IKEv2 imports), WireGuard and OpenVPN keys, ike-scan --sport=0, a BIRD BGP session on ports of 1024 or above (two BIRDs exchanged a prefix over eBGP on loopback), and nmap connect scans all work. The rest is in Privileges by tool, including the PPP peers and lldpd, which moved to a Linux guest you provision under labs.

Changing groups on RouterOS

Use /app/set environment=. Never reapply or edit the YAML to change groups. On 7.25.1 and 7.26beta5, assigning yaml= recreates the app's volumes, which wipes the home, and leaves the app disabled (lab results). environment= restarts the container and keeps its volumes. The setting survives /app/update and reboots.

environment= replaces the whole environment list. The YAML's environment: block is only the initial value, and it does not come back as a default. A single-key set like environment="t3code:T3APP_TOOL_GROUPS=vpn" silently drops every other key. Every setting the app needs is therefore also an image ENV default, so a dropped key falls back to the same value. Keys you added yourself, such as API keys or PUBLIC_URL, are not.

To replace only the group key and keep the rest, run this script from the RouterOS terminal (or paste it into a /system/script). Change val:

:local app "t3code"; :local key "t3code:T3APP_TOOL_GROUPS="; :local val "capture labs"
:local keep ({}); :foreach e in=[/app/get $app environment] do={ :if ([:find $e $key] != 0) do={ :set keep ($keep, $e) } }
/app/set $app environment=($keep, ($key . $val))

Check the result with :put [/app/get t3code environment], then run t3app tools status in the container. Running the script again with another value replaces the key; it does not add a duplicate. Tested on a CHR 7.25.1 app named t3code with service t3code: the other key was kept, the container restarted, and the volume's contents survived.

Network tool assignment

Every network package from the 2026-11-05 research is assigned here. t3app tools groups prints the same table as JSON.

Where Packages
core (in the image) iproute2, iputils-ping, bind9-dnsutils, netcat-openbsd, socat, traceroute, mtr-tiny, iperf3, ethtool, whois, snmp, freeradius-utils, mosquitto-clients
capture tcpdump, tshark, wireshark-common, nfdump
vpn wireguard-tools, strongswan-swanctl, strongswan-pki, strongswan-charon, openvpn, ike-scan
routing bird2, ndisc6, arping, arp-scan, nftables, conntrack, nmap
A Linux guest you boot under labs and provision yourself (not shipped) FRR, keepalived, a FreeRADIUS server, lldpd (needs root and a _lldpd user), and the PPP-based peers sstp-client, ppp, xl2tpd, pppoe (they need root and /usr/sbin/pppd)
not shipped pptp-linux (legacy), zerotier-one (not in Debian main), smbclient (large Samba dependencies), snmptrapd (port 162 needs bind privilege; use a uvx pysnmp receiver), eapoltest and ntpsec-ntpdate (on request)

Core gets only small user-space clients. With dependencies, their image layer measured 29.3 MB uncompressed on amd64. The largest are snmp (9 MiB with libsnmp and pci.ids) and bind9-dnsutils (6 MiB), per the apt estimates in the cost data. Ordinary client use needs no privileges, with two exceptions worth knowing: ping relies on ICMP echo sockets being allowed (net.ipv4.ping_group_range) and mtr on a cap_net_raw file capability; see Privileges by tool. centrs already covers MikroTik btest, MNDP, MAC-Telnet and RoMON.

routing is its own group rather than part of labs. labs is about 310 MiB of QEMU and firmware (a 33 MiB download) with one readiness contract: quickchr can boot a CHR. The routing peers are a few MiB, and they are most useful without QEMU, peering with the host router from a privileged container. Folding them into labs would make a small router download QEMU to get bird. It would also mix tools that need CAP_NET_RAW/CAP_NET_ADMIN into a group whose default mode (TCG, user networking) needs no privileges. nftables and conntrack join routing for the same reason: they are Linux router tools that need CAP_NET_ADMIN.

Group status and open work

dev, capture, labs, vpn and routing install as persistent payloads under /data in the background, after T3 starts, and reuse compatible payloads across image replacements (write-ups: capture, vpn and routing, labs). The image default selects all five by name. browser is not a group (its libraries stay in core, and T3's browser setup is unchanged), and provider CLIs are not a group either: they have their own selector, T3APP_PROVIDERS. The labs matrix (native ARM64 hosts, nested RouterOS, transfer, snapshots and cleanup) is still open; see the final-slice notes. RouterOS privileges and devices are issue #7, and automatic upstream-release and security delivery is issue #6. The Roadmap issue lists all open work.

Bun and RouterOS tools for agents

The image includes Bun 1.4.4, bunx, and global installations of:

They are available to agents and login shells without a first-run install:

bun --version
quickchr --help
centrs explain '/ip/address print' --json
uv --version

QEMU can use software emulation inside a RouterOS container. KVM acceleration requires an accessible /dev/kvm and host support; installing quickchr does not grant it. Size lab storage and memory for both T3 and any guest routers.

Image-owned Bun packages live in /opt/bun; user bun install --global packages live in /data/.bun. PATH searches /data/.local/bin, /data/.bun/bin, then /opt/bun/bin before system tools. User-installed overrides persist and take precedence after an image upgrade. Build arguments BUN_VER, QUICKCHR_VER, and CENTRS_VER can select specific releases instead of the defaults.

Good upstream navigators (pingdotgg/t3code docs):

  • install / requirements (t3 serve, providers) — install.md
  • remote access (t3 up, --lan, T3 Connect relay, pairing) — remote-access.md
  • background service (memory/OOM guidance, t3 service) — background-service.md
  • telemetry opt-out — telemetry.md
  • project settings / storage cleanup — project-settings.md
  • FAQ (disk usage is worktree cleanup) — faq.md

The image only documents one requirement: an installed, authenticated provider. There are no published CPU/RAM minimums; size from your workload (agents + browser), not from a datasheet.

Tags

Images track upstream stable T3 Code releases only — no nightlies (CI still accepts any T3CODE_VER input for one-off builds):

  • ghcr.io/tikoci/app-t3code:0.1.8 — exact release
  • ghcr.io/tikoci/app-t3code:0.1, :0, :latest — stability aliases
  • ghcr.io/tikoci/app-t3code:<version>-app.<N> — one promoted build, never moved. N is the ci workflow's run number. PR and branch runs use numbers too, so promoted builds skip some. A re-run that rebuilds appends the attempt (-app.<N>.2). Pin this to stay on a build; CHANGELOG.md headings use the same name.
  • ghcr.io/tikoci/app-t3code:sha-<commit> — the candidate CI pushes from main; not for deployment

Each pushed image carries its build name in the OCI label org.opencontainers.image.version (and the commit in org.opencontainers.image.revision), so a sha- candidate that never got promoted still says which run built it.

A main push publishes only the candidate. Its arm64 image then runs the container replacement test natively on a GitHub arm64 runner (amd64 already ran it before the push), and only then does CI move the release tags onto the same manifest, by digest. A failed arm64 run leaves :0.1 where it was, and a run that finishes after main has moved on skips promotion, so the tags never go back to an older build.

The /app YAML references the :0.1-style stability tag, and test/app-yaml.test.ts fails the build if it drifts from package.json's major.minor. Bumping T3 versions = bump T3CODE_VER (default in Dockerfile + Makefile), package.json, and the YAML tag together.

Developing

bun install
bun run check          # biome + tsc --noEmit + markdownlint
make lint              # check, plus hadolint/ShellCheck and actionlint (need them on PATH)
bun run test           # offline, Linux only: YAML contract, payloads, installers
make freshness         # needs network: upstream T3, Claude download contract, share link
APP_T3CODE_INTEGRATION=1 bun test test/integration/
# Build the image, then prove the tools and container-replacement persistence:
docker build -t app-t3code:test .
APP_T3CODE_DOCKER_IMAGE=app-t3code:test bun test test/container-replacement.test.ts

The integration test boots a real CHR per arch with @tikoci/quickchr@next, drives it with @tikoci/centrs@next, and asserts the web UI answers with the expected serverVersion:

APP_T3CODE_ROS=stable APP_T3CODE_ARCH=arm64 APP_T3CODE_INTEGRATION=1 \
  bun test test/integration/

The Docker replacement test removes the original container and starts a new one with only the same home volume. Every container sets its selection explicitly, except one that checks the image default. It installs the dev, capture, labs, vpn and routing payloads under all, runs each Debian group's tools as the unprivileged user (capture, labs, vpn, routing), and relinks the payloads in later containers, including with no network, then relinks the image default offline and checks that none leaves only core. Against an emulated (QEMU user-mode) image, set CAPTURE_TOOLS_QEMU_USER=1, ROUTING_TOOLS_QEMU_USER=1 and/or LABS_TOOLS_QEMU_USER=1 for the test; they are forwarded to the scripts, which then skip only the checks emulation breaks (or makes too slow) before any permission check. It checks tool discovery in ordinary and login shells, a real T3 project record and environment identity, settings (including the restarted server's effective preference through read-only MCP), uncommitted worktree/project files, a user-installed Bun CLI, provider configuration in the home, and that a user-installed muse in /data/.local/bin wins on PATH. CI runs this against the built x86 image before publishing, and builds both image architectures. It does not authenticate providers or run paid model requests.

Hands-on CHR runs (install, /app/update with an offline /data backup, commands inside the container, before/after manifests of the home) follow .agents/skills/chr-app-lab, and their results are in docs/notes/ (dated run logs and closeouts; topic research is in docs/research/). They have confirmed that /app/update keeps /data (projects, threads, settings, logins, tool payloads and the environment ID). Assigning /app/set yaml= to an installed app does not: it wipes the app-owned volume, even for a comment-only change.

Both helpers float on next on purpose: if a helper breaks, report it to centrs / quickchr instead of working around it silently. CI (ci.yml) lints, tests, builds both arches, runs the replacement test on amd64 (every build) and natively on arm64 (main and dispatch only), then promotes the tags (see Tags); integration.yml runs the CHR matrix weekly and on demand (against :0.1), and after each successful main publish (against that commit's :sha-<commit> candidate, via APP_T3CODE_IMAGE).

The pages.yml workflow publishes only tracked *.md, *.tikapp.yaml, and *.tikappstore.yaml files, preserving their paths and contents. Automatic deployments run only when those files change on main; manual dispatch is also available. Pages must use GitHub Actions as its publishing source in the repository's Settings → Pages. No site generator is used.

Verified install flow on CHR 7.25 (x86, free license):

  1. first-boot deviceMode: { enable: ["container"] } → rose mode (the guest-side /system/device-mode/update never completes);
  2. installPackage("container") (/app 404s without the package);
  3. /disk/format <id> file-system=ext4 (7.25 has ext4, no ext3), then /app/settings/set disk=<mount-point>;
  4. api … app -X PUT -f yaml=… -f use-https=no --yes (colon-style 3773:3773:web:tcp ports parse as firewall-redirects);
  5. /app/set … disabled=no (PUT leaves the app disabled:true);
  6. poll /app + /container (.running is the string "true") + GET /.well-known/t3/environment → serverVersion.

The routeros-app-yaml, routeros-container, routeros-tikoci-centrs, routeros-tikoci-quickchr-cli and tikoci-crossref skills govern this repo; AGENTS.md has the working notes.

Security and changes

SECURITY.md covers how to report a vulnerability, the trust boundaries (RouterOS access implies container access), the startup pairing token in /container/log, and what installs at runtime. CHANGELOG.md lists the user-visible changes in each promoted build.

License

MIT — see LICENSE.

About

RouterOS /app container for T3 Code (x86_64 + ARM64, GHCR, CHR-tested)

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Packages

Contributors

Languages