Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/skills/fix-issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -515,3 +515,4 @@ extracting `OffsetExpression`/`LimitExpression`.
| `DESCRIBE PAGE` output will not re-parse for any page carrying a **pluggable widget**: `mxcli check` on it fails with `extraneous input ':'` / `extraneous input '('` from the first widget onward. Separately, a boolean property the author set is missing from the description entirely | Two independent defects in the same path. **Emit**: explicit properties were written with a raw `%s`, so every string lost its quotes — a JSON `spec: {"a": 1}` then broke the parse at its first brace. **Read**: `extractExplicitProperties` skipped any value of `"true"`/`"false"` as a "common default", so booleans never reached the output | `mdl/executor/cmd_pages_describe_output.go` (`explicitPropValue`, `isBareLiteral`), `mdl/executor/cmd_pages_describe_pluggable.go` (`buildPropertyValueTypeMap`, `extractExplicitProperties`), `mdl/executor/cmd_pages_describe.go` (`rawExplicitProp.ValueType`) | **Fixing one half alone is worse than the bug.** Quote without emitting booleans and the description re-parses cleanly while silently dropping a property — a wrong page that validates. Both halves ship together or neither. **Quote by the DECLARED type, never the value's shape**: `ValueType.Type` sits in the widget's `Type.ObjectType.PropertyTypes`, the same array `buildPropertyTypeKeyMap` already walks for `PropertyKey` and throws away; a String property holding `"30"` or `"true"` is indistinguishable from a number once it is a string in BSON, and must still come back quoted. Where no type is declared, fall back to the value's shape and quote anything not plainly numeric or boolean — quoting is the safe direction, since an unquoted arbitrary string may not parse at all. **The round trip is the test, not the output**: describe → `check` → `exec` → describe must be byte-identical and leave `mx check` at 0 errors. Tests `mdl/executor/cmd_pages_describe_pluggable_roundtrip_test.go`; example `mdl-examples/bug-tests/pluggable-describe-roundtrip.mdl`, verified end to end on 11.12.1. **Uncovered while verifying, NOT fixed**: giving a property in a conditionally shown group a non-default value writes a widget Mendix rejects with CE0463 — ProgressCircle's `showLabel: true` and `labelType: 'percentage'` both do it with no DESCRIBE involved, while the same widget's General-group properties take non-default values happily. Reported in mxcli-ledger FINDINGS #104 |
| `DESCRIBE PAGE` output will not re-parse for any page carrying a **pluggable widget**: `mxcli check` on it fails with `extraneous input ':'` / `extraneous input '('` from the first widget onward. Separately, a boolean property the author set is missing from the description entirely | Two independent defects in the same path. **Emit**: explicit properties were written with a raw `%s`, so every string lost its quotes — a JSON `spec: {"a": 1}` then broke the parse at its first brace. **Read**: `extractExplicitProperties` skipped any value of `"true"`/`"false"` as a "common default", so booleans never reached the output | `mdl/executor/cmd_pages_describe_output.go` (`explicitPropValue`, `isBareLiteral`), `mdl/executor/cmd_pages_describe_pluggable.go` (`buildPropertyValueTypeMap`, `extractExplicitProperties`), `mdl/executor/cmd_pages_describe.go` (`rawExplicitProp.ValueType`) | **Fixing one half alone is worse than the bug.** Quote without emitting booleans and the description re-parses cleanly while silently dropping a property — a wrong page that validates. Both halves ship together or neither. **Quote by the DECLARED type, never the value's shape**: `ValueType.Type` sits in the widget's `Type.ObjectType.PropertyTypes`, the same array `buildPropertyTypeKeyMap` already walks for `PropertyKey` and throws away; a String property holding `"30"` or `"true"` is indistinguishable from a number once it is a string in BSON, and must still come back quoted. Where no type is declared, fall back to the value's shape and quote anything not plainly numeric or boolean — quoting is the safe direction, since an unquoted arbitrary string may not parse at all. **The round trip is the test, not the output**: describe → `check` → `exec` → describe must be byte-identical and leave `mx check` at 0 errors. Tests `mdl/executor/cmd_pages_describe_pluggable_roundtrip_test.go`; example `mdl-examples/bug-tests/pluggable-describe-roundtrip.mdl`, verified end to end on 11.12.1. Reported in mxcli-ledger FINDINGS #104 |
| Authoring a pluggable widget property that lives in a **conditionally shown group** writes a widget Mendix rejects with **CE0463**, while the same widget's other properties take non-default values happily. On ProgressCircle both `showLabel: true` and `labelType: 'percentage'` do it; `showLabel: false` and General-group properties are clean. No DESCRIBE involved | Two gaps on the same axis. **Serialization**: #574 nulls the TextTemplate of a HIDDEN conditional property, but left a VISIBLE one null — and Mendix stores an empty `Forms$ClientTemplate` there. Null and empty are each invalid in the other's state, so nulling hidden ones was only half the rule. **Extraction**: the editorConfig reader did not understand a ternary's ELSE branch (`cond ? (…) : hidePropertiesIn([…])`), so the `showLabel` gate was never seen and `labelText` read as visible whenever `labelType` was `"text"` — its default | `mdl/backend/widgetobj/builder.go` (`ApplyVisibilityRules`, `bsonFieldIsNil`), `mdl/executor/editorconfig_extract.go` (`parseGuard` `:` case, `ternaryCondition`, `trailingExpr`) | **Fixing one gap alone inverts the bug rather than closing it** — filling visible templates without the missing gate made `showLabel: false` fail where `true` had, because mxcli still thought labelText was visible. Measured both ways round before and after; a fix that moves which case fails is not a fix. **Let Mendix say what the shape should be**: `mx update-widgets` on a COPY of the failing project reconciles the widget, and diffing that against mxcli's output named the single meaningful path (`Object/Properties[N]/Value/TextTemplate` null vs `Forms$ClientTemplate`) out of 969. **The good/bad control does the isolation for free**: authoring the same widget with the boolean both ways gave two documents differing in exactly ONE path, so no other candidate needed testing. **Only CONDITIONAL properties are filled** — Studio Pro's convention for an unset TextTemplate is not uniform (a DataGrid custom-content column stores null for `tooltip` and an empty template for `exportValue`, per `emptyClientTemplateRules`), so filling every unset one would trade this bug for its mirror image. **The extractor's preamble matters**: the ternary is preceded by a whole `switch`, and walking back past the `?` to the function start yields a fragment with an unbalanced `}` that parses to nothing and looks like "unsupported shape" — hence `trailingExpr`. Regression signal: the widgetdemo showcase applies with **0 CE0463** (its 4 CE1613 are a pre-existing attribute reference). Tests `mdl/backend/widgetobj/widget_visibility_test.go`, `mdl/executor/editorconfig_extract_test.go`; example `mdl-examples/bug-tests/pluggable-describe-roundtrip.mdl` now exercises the group. Reported in mxcli-ledger FINDINGS #104 follow-on |
| Windows Defender flags the mxcli **Windows** release binary as `Trojan:Script/Sabsik.EN.A!ml`; enterprise EDR (Defender for Endpoint, CrowdStrike, SentinelOne) blocks it harder. Not the generic unsigned-Go-binary false positive of #185 | The binary genuinely embedded **chisel**, a dual-use tunnelling/pivoting tool (SSH over WebSocket), on every platform — although the tunnel only ever runs inside a Linux container. `run --hub` linked `chisel/client`, `tunnel-hub` linked `chisel/server`, so windows/darwin carried 32 packages incl. the whole `x/crypto/ssh` stack for a feature they cannot use | `cmd/mxcli/docker/tunnel_linux.go` + `tunnel_other.go` (client seam), `cmd/mxcli/tunnelhub/control_linux.go` + `control_other.go` (server seam), `scripts/check-tunnel-deps.sh` (guard) | **Never obfuscate, pack or rename to dodge the scanner** — attacker tradecraft, and it makes the binary less trustworthy, not more. **Code signing does not fix this class**: a signed binary containing chisel is still flagged behaviourally; signing only addresses #185's generic false positive. The fix is to stop shipping the capability where it is unused: one interface per seam, `_linux.go` impl + `!linux` stub, commands still registered everywhere but failing with an actionable message. **Prove absence three ways, and know that `go tool nm` is not one of them** — release ldflags `-s -w` strip the symbol table, so nm reports "no symbols" whether or not the code is linked and would give a false pass; use `go list -deps`, `go version -m`, and `strings` (nm only on a deliberately unstripped build). **Guard against the transitive path, not the name**: match the module list (`x/crypto/ssh`, `gorilla/websocket`, `armon/go-socks5`, `jpillora/*`) so re-entry without the word "chisel" still trips it, and assert a **positive control** (chisel IS in the linux graph) so the check cannot pass vacuously. Verified by re-adding the import and watching the guard fail on all four windows/darwin targets. Result: -13.5 MB (-14.7%) on windows+darwin, linux unchanged. See ADR-0009 |
4 changes: 3 additions & 1 deletion .claude/skills/mendix/bootstrap-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,9 @@ drop the `./` if it came pre-installed on `PATH`.
8. **(Optional) browser preview from a cloud session:**
`./mxcli run --hub https://hub.mxcli.org -p <AppName>.mpr`, and report the preview
URL it prints. Needs `MXCLI_HUB_KEY` on the environment; without it, continue as a
normal local run.
normal local run. `--hub` ships in the **Linux** build only (a cloud session is a
Linux container, so it works there); on a native Windows/macOS mxcli it fails with
an explanatory message — continue as a normal local run.

---

Expand Down
10 changes: 9 additions & 1 deletion .claude/skills/mendix/run-local.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,10 +322,18 @@ which makes it look like a change you just made broke authentication.

## External browser preview (`--hub`)

> **Linux builds only.** `--hub` and `mxcli tunnel-hub` ship in the **Linux** build
> only. The tunnel embeds a general-purpose tunnelling tool that gets the Windows
> and macOS binaries flagged by Defender and enterprise EDR for a capability they
> can never use, so it is left out of them. On Windows/macOS the commands exist and
> show help, but fail with an explanatory message — run mxcli inside the project's
> devcontainer (where the warm loop already runs) to use `--hub`. See
> [ADR-0009](https://github.com/mendixlabs/mxcli/blob/main/docs/13-decisions/0009-tunnel-is-linux-only.md).

`--hub <url>` exposes the running app in a **browser at a public URL** without the app
leaving this machine and without committing — for reviewing work-in-progress from a
phone/tablet, or from an egress-only environment like Claude Code on the web. The app
stays here; a **chisel reverse tunnel** dials *out* to a hub over 443 and the hub proxies
stays here; a **reverse tunnel** dials *out* to a hub over 443 and the hub proxies
browser requests back down it. Nothing is pushed — only live HTTP — and everything rides
one 443 connection, so it works through an egress-only proxy.

Expand Down
52 changes: 52 additions & 0 deletions .github/workflows/push-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,51 @@ permissions:
contents: read

jobs:
# The tunnel seam has a !linux half (stub + its tests) that the ubuntu job can
# only compile, never run. This job actually executes it on real Windows and
# macOS runners, so "--hub fails with an actionable message" is a tested claim
# rather than a cross-compile that type-checked. See ADR-0009.
#
# Neither package depends on the generated ANTLR parser, so this needs no
# grammar step and stays fast.
tunnel-seam-cross-platform:
strategy:
fail-fast: false
matrix:
os: [windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: '1.26.6'
- name: Test the tunnel seam
shell: bash
# Scoped with -run to the seam's own tests. The full test binaries are still
# COMPILED for this platform, so a Windows/macOS build break is still caught;
# only the !linux stub behaviour is executed.
#
# Running the whole packages here fails on Windows for reasons that predate
# this change and are unrelated to the tunnel: several tests assert POSIX file
# modes (0600) that Windows does not implement — os.Chmod only toggles the
# read-only bit, so Stat reports 666 — plus one path-separator assumption.
# Tracked separately in #897; widening this job is that issue's job, not this
# one's.
#
# -run can pass vacuously if the tests are renamed or deleted, so assert that
# the expected number actually ran.
run: |
out=$(go test -v -count=1 -run 'Unsupported' ./cmd/mxcli/docker/... ./cmd/mxcli/tunnelhub/...)
echo "$out"
n=$(printf '%s\n' "$out" | grep -c '^--- PASS: Test.*Unsupported' || true)
echo "seam tests executed: $n"
if [ "$n" -lt 4 ]; then
echo "FAIL: expected at least 4 tunnel-seam tests to run, -run matched $n."
echo " The !linux stubs in cmd/mxcli/docker and cmd/mxcli/tunnelhub"
echo " must each keep a test whose name contains 'Unsupported'."
exit 1
fi

build-and-test:
runs-on: ubuntu-latest
steps:
Expand All @@ -28,6 +73,13 @@ jobs:
run: make build
- name: Test
run: make test
- name: Check tunnel stays Linux-only
# The embedded tunnel (chisel) must never reach the Windows/macOS builds —
# it gets mxcli flagged by Defender and enterprise EDR on managed corporate
# endpoints, which is most of our audience. See ADR-0009. The script also
# asserts a positive control (chisel IS in the linux graph) so it cannot
# pass vacuously.
run: ./scripts/check-tunnel-deps.sh
- name: Check MDL example scripts
# Single source of truth: `make check-mdl` covers BOTH doctype-tests/ and
# bug-tests/ (skipping *.test.mdl, inverting *.fail.mdl negative tests, and
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Changed

- **The embedded tunnel now ships in the Linux build only** (`mxcli run --hub`, `mxcli tunnel-hub`). The tunnel embeds [chisel](https://github.com/jpillora/chisel), a dual-use tunnelling tool that appears in threat intelligence as a pivoting component. It only ever runs inside a Linux container, but every platform linked it — so Microsoft Defender flagged the Windows binary (`Trojan:Script/Sabsik.EN.A!ml`) and enterprise EDR flags this class of payload harder still, blocking mxcli on the managed corporate endpoints most Mendix developers use. Both chisel imports now sit behind a one-interface, Linux-only seam; a CI guard (`scripts/check-tunnel-deps.sh`, `make check-tunnel-deps`) fails the build if chisel or its SSH/websocket/socks dependencies reappear in a windows/darwin dependency graph. **Windows and macOS release binaries are 13.5 MB smaller (-14.7%)** and contain no tunnelling code. The Linux build is unchanged. On other platforms the two commands remain registered and documented but fail with an actionable message. Note that this is a *different* problem from the `Wacatac.C!ml` report in [#185](https://github.com/mendixlabs/mxcli/issues/185), which was a genuine generic Go-binary false positive: here the capability really was in the binary, and code signing would not have addressed it. We did not obfuscate or repack anything — the fix is not shipping the capability where it is unused. ([#890](https://github.com/mendixlabs/mxcli/issues/890), [ADR-0009](docs/13-decisions/0009-tunnel-is-linux-only.md))
- **Breaking, narrow:** `mxcli tunnel-hub` can no longer be hosted on Windows or macOS — move the hub to a Linux host. Developers on native Windows/macOS installs must run mxcli inside the project's devcontainer to use `--hub`.
- `tunnelhub.ServerOptions.ChiselAddr` is renamed to `ControlAddr` (it addresses the platform-agnostic control server).

- **Go toolchain 1.26.5 → 1.26.6** for GO-2026-6218 (`net/url`), GO-2026-6090 (`crypto/tls`), GO-2026-6089 (`net/http`), GO-2026-6088 (`encoding/xml`), GO-2026-5972 (`encoding/asn1`) and GO-2026-5026 (`net/http`, via `golang.org/x/net/idna`). All six are standard-library advisories fixed in go1.26.6; no mxcli code changed. Bumped in `go.mod` and in all three workflows (`push-test`, `release`, `nightly`) together, so released binaries are not still linked against the vulnerable standard library.

## [0.17.0] - 2026-08-10
Expand Down
32 changes: 32 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,38 @@ disable identity preservation. **Any test asserting "nothing changed" must inclu
the control run with it set** — otherwise the test passes against a build that
never had the fix, which is exactly how PR #125 shipped green.

### The Tunnel Is Linux-Only, On Purpose — Do Not "Restore" It

`mxcli run --hub` and `mxcli tunnel-hub` embed [chisel](https://github.com/jpillora/chisel),
a dual-use tunnelling tool that appears in threat intelligence as a pivoting
component. Shipping it in the Windows and macOS binaries — where the tunnel can
never run — got them flagged by Defender (`Trojan:Script/Sabsik.EN.A!ml`) and
denied by enterprise EDR, which blocks mxcli for corporate Mendix developers on
managed endpoints. It is now built **for Linux only**. See
[ADR-0009](docs/13-decisions/0009-tunnel-is-linux-only.md).

This looks like a portability gap and is not one. Making the tunnel cross-platform
again re-introduces the detection for the large majority of downloads.

- **All chisel imports live behind two seams**, one interface each:
`tunnelConn` / `startTunnel` (`cmd/mxcli/docker/tunnel_linux.go` + `tunnel_other.go`)
and `controlServer` / `newControlServer` (`cmd/mxcli/tunnelhub/control_linux.go`
+ `control_other.go`). Adding a chisel import anywhere else is the mistake the
guard exists to catch.
- **`scripts/check-tunnel-deps.sh` (CI, and `make check-tunnel-deps`) fails the
build** if chisel or its tunnelling-specific dependencies — the SSH/websocket/
socks stack included, which is how it would come back without the word "chisel"
appearing — reach a windows/darwin dependency graph. It asserts a positive
control first (chisel *is* in the linux graph), so it cannot pass vacuously.
- **The hub seam is at `Start`, not construction**, so the portable front
(registry, API, auth, routing) stays testable on every platform.
- **Never obfuscate, pack, or rename to evade detection.** That is attacker
tradecraft and makes things strictly worse. The only legitimate fix is not
shipping the capability where it is unused. Code signing does **not** substitute:
a signed binary containing chisel is still flagged behaviourally.
- Do not conflate this with #185 (`Wacatac.C!ml`), which was a genuine generic
Go-binary false positive with a different remedy.

### Theme Files: Where SCSS Actually Compiles

Styling written to the wrong place fails **silently** — the build succeeds and the
Expand Down
Loading
Loading