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
30 changes: 30 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,36 @@ last eighty French names, which left **0 differences over 7 734 captured strings
of the diff proves nothing; the DOM does. And the DOM does not prove everything either — it
never opens the save path, which is why the two defects above needed a test apiece.

## What wins between the file and the command line

**The command line, on every setting — and for a long time that was true of nine out of
twenty-two.** `merge` was a hand-written list of `if`s, and it stopped being complete around
the fifth option added after it. Everything absent from the list was dropped without a word
the moment a `--config` sat on the line beside it: `--level coverage` measured everything,
`--jacoco-reports data` wrote its hundred and eighty files. Nothing failed — the run simply
did something other than what it had been asked, and the only way to notice was to count the
files afterwards. Found on 29 August 2026 while adding a key, not while reading the code.

The rule is now **applied rather than enumerated**: a setting that differs from a fresh
`Config` is one the command line set, and it overrides the file. A list has to be remembered
at every new option; a comparison does not. `SettingsPrecedenceTest` walks `Config`'s fields
by reflection and fails the build on any that does not come through — which is the guard a
longer list could never be.

**`SERVE_HOST` says where to listen, and never that one should.** The interface a machine
exposes is a property of that machine, so it belongs in the configuration; the decision to
serve is a gesture, so it stays on the command line. That split is the whole point: a
configuration file travels — into a repository, a ticket, another machine — and one that
could open a port by travelling would be unreadable safely. `--serve` remains the only thing
that puts the tool into listening, and a test holds it: exactly one `serve = true` in the
options switch.

Past the loopback, the report becomes readable — and annotatable — by whoever reaches the
port, and it carries the argument values captured from a real application. So the default
stays `127.0.0.1`, the key is shipped commented out with its price beside it, the tool warns
at start-up when it listens wider without a secret, and the documentation carries the
deployment that does it properly: the proxy terminates TLS and the tool answers only to it.

## Conventions

- **The tool speaks English, and so does the code now.** The switch happened in stages — the
Expand Down
47 changes: 47 additions & 0 deletions bin/acceptance-local.sh
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,53 @@ grep -q '"sourcesDisponibles":{"' reassemble/index.html
step $? "the annotated code is back in the report"
echo

# A configuration file and a command line say different things: the line wins. It used to
# win on nine settings out of twenty-two, and to lose in silence on the others.
echo "4 sexies. A --config beside the options does not swallow them"
cat > both.conf <<CONF
JAVA_CMD="java -jar $APP --iterations 200000"
SOURCE_DIRS="$REPO_DIR/sample-app/src/main/java"
CLASSES_DIR="$APP"
OUT_DIR="both"
LEVEL="full"
JACOCO_REPORTS="full"
SERVE_HOST="127.0.0.1"
CONF
java -jar "$JAR" --config both.conf --level coverage --jacoco-reports data \
--name "the line wins" > both.log 2>&1
step $? "a measurement with a --config AND options ends"
grep -q "no stack sampling" both.log
step $? " --level coverage was obeyed, not the file's \"full\""
grep -q "JACOCO_REPORTS=data" both.log
step $? " and --jacoco-reports data too"
[ "$(count both)" -lt 20 ]
step $? " which shows on the disk: $(count both) files, not the file's hundreds"

# The same, with the file found by its name instead of named on the line: it is still a
# file, and the options typed beside it were still typed.
mkdir -p implicit
( cd implicit && cat > runtime-xray.conf <<CONF
JAVA_CMD="java -jar $APP --iterations 200000"
SOURCE_DIRS="$REPO_DIR/sample-app/src/main/java"
CLASSES_DIR="$APP"
OUT_DIR="out"
LEVEL="full"
CONF
java -jar "$JAR" --level coverage --name "implicit" > implicit.log 2>&1 )
step $? "a measurement with the implicit runtime-xray.conf ends"
grep -q "Configuration read from" implicit/implicit.log
step $? " the file was indeed found by its name"
grep -q "no stack sampling" implicit/implicit.log
step $? " and --level coverage was obeyed all the same"

# The interface is a setting; putting the tool into listening is a gesture. A file that
# travels — into a repository, a ticket, another machine — must not be able to open a port.
java -jar "$JAR" --config both.conf --report-only > serves-not.log 2>&1
step $? "reading with SERVE_HOST set and no --serve ends instead of listening"
! grep -q "Report served at" serves-not.log
step $? " nothing was put into listening"
echo

echo "5. The server writes the annotations beside the measurements"
PORT="$(python3 -c 'import socket;s=socket.socket();s.bind(("127.0.0.1",0));print(s.getsockname()[1]);s.close()')"
java -jar "$JAR" --report-only --out out \
Expand Down
69 changes: 69 additions & 0 deletions docs/outil/annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,75 @@ diagnostic tool, not a service: the only write it accepts is a run's annotation,
whose name it chooses itself, and the file serving refuses any path that would leave the
served directory.

### Deploying it, so nobody has to remember the options

Repeating `--serve-host` on every launch is how one ends up not repeating it. Two things
carry it instead — and neither of them ever *starts* a server, which is the point:

**In the project's configuration**, because which interface a machine exposes is a property
of that machine:

```conf
SERVE_HOST="0.0.0.0"
```

> ⚠️ **This key widens what is readable, and it is off by default.** Past the loopback, the
> report — with the argument values captured from a real application — becomes readable by
> whoever reaches the port, and the annotation writes become theirs too. The key sets the
> interface and nothing else: `--serve` remains the only gesture that puts the tool into
> listening, so a configuration file copied into a repository, a ticket or another machine
> cannot open a port by travelling.

**In a service**, for a machine that serves results permanently. The secret goes through the
environment rather than the command line, where `ps` would show it:

```ini
# /etc/systemd/system/runtime-xray.service
[Unit]
Description=Runtime X-Ray — shared report
After=network-online.target

[Service]
User=xray
WorkingDirectory=/srv/xray
EnvironmentFile=/etc/runtime-xray.env # XRAY_SERVE_TOKEN=…, chmod 600
ExecStart=/usr/bin/java -jar /opt/runtime-xray/runtime-xray.jar \
--report-only --out /srv/xray/campaigns \
--serve 8787 --serve-host 127.0.0.1
Restart=on-failure

[Install]
WantedBy=multi-user.target
```

Note the `127.0.0.1` in a unit meant to serve a whole team: **the TLS proxy is what listens
outside**, and the tool answers only to it. That way the plain HTTP never leaves the machine,
which is what the first caveat above asks for.

```nginx
server {
listen 443 ssl;
server_name xray.internal.example.com;
ssl_certificate /etc/ssl/certs/xray.pem;
ssl_certificate_key /etc/ssl/private/xray.key;

location / {
proxy_pass http://127.0.0.1:8787;
proxy_set_header Host $host;
# The report is a lot of small files, and some of them are large enough to matter.
proxy_buffering off;
}
}
```

Two things to check before opening it, and they are the ones people skip:

- **The output directory is the perimeter.** Everything under `--out` is served, and a
campaign carries the observed application's log and its captured values. Serve a directory
that holds only what you meant to hand over.
- **`--serve-host 0.0.0.0` without a proxy is a decision**, not a shortcut. It works, the tool
warns about it at start-up, and the warning is the whole of the protection you then have.

## Where the file is written

Seen as files, **a run is a directory**. Its annotation can live in three places, and the
Expand Down
2 changes: 1 addition & 1 deletion docs/outil/mode-emploi.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ the first go — **[Reducing the footprint on a large codebase](empreinte.md)**
| `--follow [port]` | Serves **a page showing the run in progress** (default: 8788, local loopback): activity band, busy cores, output produced, tail of the log. The `progression.jsonl` file, for its part, is **always** written: `tail -f <out>/progression.jsonl` follows the run with no browser and no open port |
| `--context ["question"]` | Writes on standard output **a bounded extract of the report, ready to hand to a language model**: the facts that answer the question, their vocabulary, and at the top what was *not* measured. Sends nothing anywhere. The families picked up are **announced on standard error**; `--help` gives the table of recognised words, **in English** — French works too, undocumented — see [Having a report read by an AI](integration-ia.md) |
| `--families a,b` | Names the fact families to attach **instead of deducing them from the question**. This is the path for scripts: the result no longer depends on the words used. An unknown family stops, with the list of those that exist |
| `--serve-host <host>` | Listening interface (default: `127.0.0.1`). `0.0.0.0` for a shared server |
| `--serve-host <host>` / `SERVE_HOST` | Listening interface (default: `127.0.0.1`), or one interface in particular. **Neither ever starts a server** — only `--serve` does — so a configuration file that travels cannot open a port. Past the loopback, the report and the values captured in it become readable and annotatable by whoever reaches the port: see [deploying it](annotations.md#deploying-it-so-nobody-has-to-remember-the-options) |
| `--serve-token [secret]` | Guards the served report with a **shared secret**, asked once then remembered for twelve hours. With no value, a secret is drawn at random and shown. `XRAY_SERVE_TOKEN` does the same without exposing it in `ps`. Without the option, nothing is asked: to be kept for the local loopback or an already filtered network — see [what that secret is worth](annotations.md#what-that-secret-is-worth-and-what-it-is-not) |

These options combine: `--report-only --serve` serves measurements already taken, without
Expand Down
36 changes: 30 additions & 6 deletions orchestrator/src/main/java/lab/xray/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,23 @@ public final class Config {
*/
public String archive = "";
public String runName = "";
/**
* The interface the served report listens on — {@code 127.0.0.1} unless said otherwise.
*
* <p>It is here because <b>which interface a machine exposes is a property of that
* machine</b>, and repeating it on every launch is how one ends up not repeating it.
* What is <i>not</i> here, and must not be, is the decision to serve at all: this key
* says where to listen when {@code --serve} is given, and never puts anything into
* listening by itself. A configuration file travels — into a repository, into a ticket,
* onto another machine — and a file that can open a port by travelling is a file nobody
* can read safely.
*
* <p>Widening it beyond the loopback publishes the captured argument values of a real
* application to whoever reaches the port, and opens the annotation writes to them too.
* The tool says so at start-up when there is no shared secret; the documentation says
* what that secret is worth.
*/
public String serveHost = "127.0.0.1";
public int attachAfterSeconds = 8;
public int maxSeconds = 600;
public int watchCount = 10;
Expand Down Expand Up @@ -276,6 +293,7 @@ void set(String key, String value) {
case "LEVEL", "NIVEAU" -> level = value;
case "COVER_INCLUDES" -> coverIncludes = value;
case "JACOCO_REPORTS" -> jacocoReports = value;
case "SERVE_HOST" -> serveHost = value;
case "ARCHIVE" -> archive = value;
case "SAMPLE_INTERVAL_MS" -> sampleIntervalMs = parse(value, sampleIntervalMs);
case "FOLLOW_PORT", "SUIVI_PORT" -> followPort = parse(value, followPort);
Expand Down Expand Up @@ -560,12 +578,18 @@ public static void writeTemplate(Path file) throws IOException {
# or "all". The files go into <run>/exports/.
#EXPORT="cpuprofile,lcov"

# Serving the report is not set here: it is a way of launching, not a property
# of the project. "--serve" serves the output directory and lets the page write
# its annotations beside the runs; "--serve-host 0.0.0.0" makes it a shared
# server, where several people annotate in parallel, which "--serve-token"
# closes with a secret (XRAY_SERVE_TOKEN so as not to expose it in "ps"). A
# secret does not belong in a file under version control.
# WHERE the report is served, when it is. "--serve" alone starts the server and
# lets the page write its annotations beside the runs; this key only decides the
# interface it listens on, and never puts anything into listening by itself —
# a configuration file travels, and one that could open a port by travelling
# would be unreadable safely.
#
# Beyond the loopback, the report becomes readable — and annotatable — by whoever
# reaches the port: it carries the captured argument values of a real
# application. Guard it with "--serve-token" (XRAY_SERVE_TOKEN so as not to
# expose the secret in "ps"), and put TLS in front, since this speaks plain HTTP.
# A secret does not belong in a file under version control.
#SERVE_HOST="0.0.0.0"

# The repository to fetch the analysis components from, once. On a closed
# network, name the internal mirror: it is the only setting that matters for
Expand Down
Loading
Loading