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
102 changes: 77 additions & 25 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
name: ci

# Pull requests run on the PR; a push runs only on main. (Both triggers on every branch ran everything twice per PR push.)
on:
push:
branches: [main]
pull_request:

# The workflow reads the repository and nothing else: no secrets, no writes.
permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
plugin:
name: plugin (harness)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
Expand All @@ -20,9 +31,32 @@ jobs:
# The harness's crafted-copy battery: every evasion must fail, every lookalike must pass.
- run: node tools/verify-plugin-selftest.mjs

lint:
name: lint (names, syntax)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- uses: actions/setup-python@v5
with:
python-version: '3.11'
# Undefined and unused names, and syntax errors: the class of defect that needs no test to find.
- run: pip install ruff && ruff check --select F,E9 console tools
# Every first-party script must at least parse.
- run: for f in console/frontend/*.js plugin/plugin.js; do node --check "$f"; done
- run: for f in install.sh console/start.sh tools/sync-live.sh bin/is-enabled.sh; do bash -n "$f"; done
# The vendored bundles are what was recorded (a hand edit or a half-applied upgrade fails here), and the
# generated preload list follows the module graph.
- run: python tools/vendor-sums.py --check
- run: python tools/modulepreload.py --check

tests:
name: tests (unit suite, py${{ matrix.python }})
name: tests (py${{ matrix.python }})
runs-on: ubuntu-latest
timeout-minutes: 25
strategy:
fail-fast: false
# 3.14 is the interpreter the operator actually runs: argparse there rejects a bare `%` in a help
Expand All @@ -34,46 +68,64 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
# The suite is stdlib-only by design: the study store, the chat bridge, the chart bridge and
# the push channel are all covered without a browser, a network, or the app — so this step
# installs nothing at all.
- name: unit suite (no dependencies)
run: python -m unittest discover -s console/backend/tests -t console/backend/tests -v
# The MCP wrapper is the one piece that needs a library (fastmcp). REQUIRE_MCP makes a missing
# fastmcp a failure instead of a silent skip, so the layer can never go untested by accident.
- name: MCP tool layer, against a stub console
cache: pip
cache-dependency-path: console/requirements-test.txt
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: pip install -r console/requirements-test.txt
# ONE run of the whole suite with everything it can use. REQUIRE_* turn a missing fastmcp or node into a
# failure instead of 155 + 77 silent skips (the suite used to end OK with 164 tests skipped).
- name: unit suite
env:
TRADER_CHART_REQUIRE_MCP: '1'
TRADER_CHART_REQUIRE_NODE: '1'
TRADER_CONSOLE_TOKEN_FILE: /tmp/ci-console.token # the suite starts servers: keep their token off $HOME
run: |
pip install fastmcp
python -m unittest discover -s console/backend/tests -t console/backend/tests \
-p 'test_mcp_server.py' -v
# The tool contract (annotations) and the Edge Stats tools, end to end: tool -> console -> a fake
# engine speaking the real engine's responses. Both skip without fastmcp, so they live here.
python -m unittest discover -s console/backend/tests -t console/backend/tests \
-p 'test_mcp_tool_annotations.py' -v
python -m unittest discover -s console/backend/tests -t console/backend/tests \
-p 'test_edgestats_mcp.py' -v
python -m unittest discover -s console/backend/tests -t console/backend/tests 2>&1 | tee /tmp/suite.log
tail -3 /tmp/suite.log
# With fastmcp and node present only the real-engine tests (needs `--with-edge`) and the live-tree test skip.
# A larger number means a tool went missing.
skipped=$(grep -Eo 'skipped=[0-9]+' /tmp/suite.log | tail -1 | cut -d= -f2)
if [ "${skipped:-0}" -gt 12 ]; then echo "too many skipped tests: $skipped (expected <= 12)"; exit 1; fi
- name: stdlib only (the console's own promise)
if: matrix.python == '3.11'
run: |
python -m venv /tmp/bare
/tmp/bare/bin/python -m unittest discover -s console/backend/tests -t console/backend/tests 2>&1 | tail -3

backend:
name: backend (compile + boot)
name: backend (compile + boot, mcp ${{ matrix.mcp }})
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
mcp: ['>=1,<2', '>=2,<3'] # users get whatever pip resolves today: test both majors
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: the one non-stdlib dependency
run: pip install 'mcp>=1,<2'
run: pip install "mcp${{ matrix.mcp }}"
- name: syntax
run: python -m compileall -q console/backend console/mcp
- name: boots and answers /api/health without an MCP session
run: |
cd console/backend
python server.py --port 8899 --frontend ../frontend --no-mcp &
for i in $(seq 1 20); do
if curl -fsS http://127.0.0.1:8899/api/health > /tmp/health.json; then break; fi
python server.py --port 8899 --frontend ../frontend --no-mcp > /tmp/console.log 2>&1 &
pid=$!
ready=0
for i in $(seq 1 40); do
if curl -fsS http://127.0.0.1:8899/api/health > /tmp/health.json; then ready=1; break; fi
kill -0 "$pid" 2>/dev/null || break # the server died: stop waiting
sleep 0.5
done
cat /tmp/health.json
kill %1
cat /tmp/health.json 2>/dev/null || true
if [ "$ready" != 1 ]; then echo "the console never answered /api/health"; cat /tmp/console.log; exit 1; fi
python -c "import json; d = json.load(open('/tmp/health.json')); assert d['ok'] is True, d"
# the page itself, with its security headers
curl -fsS -D - -o /dev/null http://127.0.0.1:8899/ | tee /tmp/headers.txt
grep -qi '^content-security-policy:' /tmp/headers.txt
kill "$pid"
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ venv/
/tmp/
# local test screenshots (grim captures from UI verification runs)
/shots/
docs/shots/
# docs/shots/ holds the README's images: they ARE tracked (ignoring the folder left three of them out of every clone)

# PR Lens writes its previews here. They are rebuilt on demand.
.pr-lens/
73 changes: 73 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,79 @@ All notable changes to this project are documented here. Format: [Keep a Changel

## [Unreleased]

### Changed

- **Vela 0.8.1 → 0.8.3** (`@luxalgo/vela`, published 5 and 7 Oct; vela-pinets 0.2.15 already accepts it and is unchanged). The
files are LuxAlgo's, byte for byte (nothing in `vendor/vela/` is patched); only the vendored set moved: the ES modules, the
Binance / Coinbase / Hyperliquid providers and the global build (`chunk-*.js` names changed; the preload list, the vendor
checksums and the docs follow). What 0.8.2/0.8.3 bring arrives with it: the maximise / restore control on Vela's panels, the
price-axis tick hook and price-style change events in the renderer API, surface open / close events, a "Reverse" action, touch-only
pan inertia (a mouse release now stops dead). **Nothing of ours was touched to take it**, and it was held to that: the same 56
agent actions (add / remove studies, layouts, 10 drawing types, marks, view settings, themes, replay, fullscreen, a Pine run
with inputs, the drawer, a screenshot) answer identically on both versions; the keyboard focus ring, the ⋯ menu band, the 200 % zoom
layout, the drawer and Edge sheet focus handling all pass unchanged; the CSP blocks nothing; first canvas is 1,544 ms against
1,549 ms (median of six, alternating runs) for +58 KB. `test_vela_doors` now finds Vela's drawings chunk by content, because its
hashed file name changes with every release.

### Fixed (the 8 Oct 2026 full audit: seven read-only lanes over `3c2ad34`, handed to Hermes as a report)

- **The chart bridge no longer loses or duplicates commands.** Its three files were read-modify-written with no lock and one
shared temp name: 60 parallel command POSTs gave 11 to 23 HTTP 500s and duplicate ids (the page drops a duplicate id, so a
command silently never ran). One lock, a temp file of its own per write, and command ids from a counter that only grows.
A result must name the command it answers (400 otherwise) and now keeps the fields the page reports (`trades`, `overlay`…).
- **Replay prices only the symbol being replayed.** An ETH order used to fill at the BTC cursor price. The page sends the
symbol with the push; another symbol is priced live.
- **Paper broker:** the entry fee is in `realized` (it now reconciles with equity); an approve that cannot be saved leaves the
account exactly as it was; a read error on `paper.json` no longer resets the account to 10,000; large quantities sell
without a ghost position; prices are read outside the lock; order ids continue across a reset.
- **`once_per_view` now reaches the stream** (`reload` runs in every view). **A month is a month:** `1M` was mapped to one minute.
Intervals Binance does not serve are refused with the list. A partial catalogue is no longer cached for 12 h.
- **The console listens at once** (it waited up to 60 s for the LuxAlgo MCP first); SIGTERM now stops it cleanly; a busy port
is one line, not a traceback; bad numbers are a 400, not a 500.
- **Trust boundaries.** `GET /api/session` goes to the console's own page and to local tools, not to other pages or LAN
peers (`--host 0.0.0.0` no longer hands out the token; `TRADERS_AGENT_ALLOW_LAN=1` opts in). The Origin check compares the
port, so another localhost app cannot post. Cross-site `/api` GETs are refused (`Sec-Fetch-Site`). The page carries a
Content-Security-Policy; every answer is `nosniff`. Idle sockets time out (30 s), connections are capped, SSE queues are
bounded. `run_id` and the backtest service's `local:` names cannot leave their folder; the backtest service answers the
console only; thumbnails come from exact hosts, are content-sniffed, follow no redirect and are written atomically. Agent
definitions are validated (a toolset such as `--yolo` is refused); the chat route's workdir and timeout are the operator's.
Loopback calls from the MCP server and CLIs bypass `http_proxy`.
- **The agent is no longer told things that are not true.** `library_source` / `library_indicator` cut long scripts silently
at 6,000 / 8,000 characters while claiming the full source: they now return up to 60,000 and say `truncated … offset=N`
(as do the edge, prop-firm and related lists). The tool and skill no longer say `while` / `for…in` cannot run (only
`import` is refused). `bt_*` answer instead of raising; `chart_shot` uses the same view gate as every tool; `chart_batch`
takes a list; all 54 tools state `destructiveHint`; an empty chart is called empty; `chart_add_indicator` refuses an unknown
name before Vela is asked (no ghost study row) and suggests the lower-case spelling.
- **Docs match the code.** The skill is 139 lines (the history moved to `docs/desk-engineering-notes.md`) and covers the paper
broker and replay; README corrects "no order placement", "nothing written outside the repo", "synthetic bars", the removed
buttons, ports and `LUXALGO_CONSOLE`; `docs/MCP-TOOLS.md` groups tools by their real annotations.
- **Installer:** flags are parsed (an unknown flag or `--help` no longer installs), `--uninstall`, `PORT=` is baked into the
installed plugin, `--doctor` no longer `eval`s paths and checks the Python version and the MCP registration.
- **UI and accessibility:** the Vela top bar has a keyboard focus ring; the ⋯ menu is no longer covered by the pane at 1020-1069 px;
the run status stays on screen at 200 % zoom; the highlight layer is no longer a Tab stop; the drawer and Edge sheet are
dialogs that make the rest inert and return focus to what opened them; Esc from ⋯ returns focus; filled buttons are 4.9:1
in the dark theme; inputs keep a forced-colours focus outline; `<title>` is "Trader's Agent".
- **Frontend races:** a market switch during a re-run no longer leaves another symbol's labels; the newest run wins; a failed
first `/api/session` is retried; the poll loop cannot stall on a hung request; the stream no longer moves the poll's
high-water mark; the library detail pane shows the last row opened; the script draft is saved when the page goes away;
catalogue links must be `https:`; two untitled inline inputs no longer share one override.
- **Plugin:** the reveal-on-launch default is off, as documented, and waits for the stored choice.

### Removed

- `docs/studies/luxalgo-reference/smart-money-concepts-smc.pine`: LuxAlgo's CC BY-NC-SA 4.0 script was committed in this (public)
repository. It remains in git history; rewriting history is the maintainer's call.
- `./install.sh --vendor` (it rewrote committed files without checking them). See `console/frontend/vendor/VENDORING.md`.

### Added (from the same audit)

- `NOTICE` (the AGPL status of the distribution and the source offer for the patched engine), the MIT texts of the vendored Zag
packages, `console/frontend/vendor/SHA256SUMS` with `tools/vendor-sums.py --check`, a "Network" section in `THIRD-PARTY.md`.
- CI: `permissions: contents: read`, one run per PR, a lint job (`ruff F,E9`, `node --check`, `bash -n`, vendor sums, preload list),
the whole suite once with fastmcp and node REQUIRED and a skip ceiling, both `mcp` majors, pinned test dependencies
(`console/requirements*.txt`), and a boot check that fails when the server never answers.
- About 130 new tests that drive the real code: threads against the bridge, a live socket for the gates, a fastmcp stub console.

### Added

- **Syntax colouring in the script editor.** Comments, `//@directives`, strings, numbers and `#RRGGBB`
Expand Down
27 changes: 27 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Trader's Agent: licence notice
==============================

1. This project's own code (everything outside console/frontend/vendor/) is MIT-licensed: see LICENSE.

2. The distribution as a whole is NOT only MIT. It redistributes, unmodified or patched, these components:

console/frontend/vendor/pinets/ PineTS, patched build (AGPL-3.0-only; LICENSE beside it)
console/frontend/vendor/vela-pinets/ Vela-PineTS (AGPL-3.0-only; LICENSE beside it)
console/frontend/vendor/vela/ Vela (Apache-2.0; LICENSE and NOTICE beside it)
console/frontend/vendor/zag/ Zag, Floating UI, proxy-compare (MIT; texts in THIRD-PARTY-LICENSES.md)

Because AGPL-3.0 code is redistributed here, the combined distribution (this repository with its vendor/ folder, and a
console served from it) is offered under the AGPL-3.0 terms for those parts: anyone who receives it, including a person
using a console reached over a network, may ask for the complete corresponding source. Details: THIRD-PARTY.md.

3. Written offer of source for the patched engine (console/frontend/vendor/pinets/pinets.min.browser.es.js):
repository https://github.com/godzillacode0000/PineTS (fork of https://github.com/LuxAlgo/PineTS)
branch fix/p1-p8-on-0.11
built from commit f5ec571
build npm ci && npm run build:prod:browser-es
console/frontend/vendor/pinets/PROVENANCE.md records the patches, the file's size and its sha256. The source for the
rest of this distribution is this repository.

4. Third-party content that is deliberately NOT in this repository: LuxAlgo Library indicators (CC BY-NC-SA 4.0). They are
fetched on demand at run time and never committed or redistributed here. Product names (TradingView, LuxAlgo, Hermes,
Binance) belong to their owners; this is an unofficial plugin.
Loading
Loading