"terminal", "from_terminal" and "to_terminal" take the terminal's uuid
(as qet_element_info lists it) in place of its index. It names a
terminal of the op's own element -- for add_conductor, of that end's
element, "$name" references included -- and is turned at run time into
the index the call takes by qet.terminalIndex(); if the element has no
such terminal the op fails with a note. Unlike the index, a sort by
position, it is defined between two terminals at the same point. The
lookup is required only when a uuid is used, so index-only edits still
run on older builds.
README: the terminal uuids; wires of older projects now get a lasting
uuid (#1107) instead of staying unnamed.
Tests: script generation for every terminal-taking op, "$name" and folio
uuid resolution, the index still passed through; through the binary,
bobine_ka_a_remanence (file order A2, A1; index order A1, A2) wired A2 to
A1 by uuid lands on exactly those ends, and an unknown uuid stops the run
with its note. 255/255 against a build with qet.terminalIndex(); a build
without it is reported as missing it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of the previous commit:
- The load fallback compared a saved uuid with occurrence 0 only, so a
wire on the second of two terminals at one point of a symbol was lost
once the definition was replaced, and one on the first could go to
either of the pair (Element::m_terminals is sorted, not in definition
order). Element::parseTerminal() now records each terminal's rank among
the terminals of the definition at the same point, derivedUuid() uses
it, and fillMissing() starts from the same rank.
derivedUuidFoundAfterReplacement runs on perceuse.qet and industrial.qet
too: 154/156 and 670/671 wires without the rank, all with it.
qet-mcp: the first save of an older project now rewrites its wires from
the numbered form to the uuid form, and qet_diff keyed the two forms
differently, so an untouched resave showed every wire removed and added
(4 failures in test_qet_mcp.py). A uuid end is now resolved to the same
key as a numbered one: the terminal's definition position, moved to where
the wire docks, is the placed symbol's <terminal> record.
test_conductor_key_same_in_both_forms fails without it; 253/253 pass on
this build and on the previous stage's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qet_edit takes a folio as its index, and the index shifts when an earlier
op in the same run adds, inserts or removes a folio: remove folio 0, then
edit "folio 2", and the edit lands on the folio that was 3. Every other
item qet_edit addresses (elements, texts, shapes, pictures, tables, symbol
text fields) can already be named by uuid; folios could not.
- "folio" and "to_folio" take the folio's uuid as well as its index,
turned into the current index at run time by qet.folioIndex(). The
lookup is required only when a uuid is used, so an index-only edit still
runs on a build without it.
- qet_project_info lists each folio's uuid. A folio saved without one
(132 of the 133 in the shipped examples) shows it empty until the
project is saved once, and an empty "folio" says so.
Tests: the generated script for a uuid folio, on its own and inside a
table lookup and link_elements' to_folio; and a real run that removes
folio 0 and then retitles the old folio 2 by uuid, on a file saved without
folio uuids. The run fails against a build without qet.folioIndex().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every scripting call names a folio by its index, and the index shifts when
a folio is added, removed or moved: a script that removes folio 0 and then
edits "folio 2" edits the wrong one. Texts, shapes, pictures, tables and
symbol text fields already have a uuid lookup for the same reason; folios
had none.
- qet.folioUuid(index): the folio's uuid, or "".
- qet.folioIndex(uuid): the folio's current index, or -1.
A folio saved without a uuid (132 of the 133 in the shipped examples) is
given one on load, derived from the file, so it is the same on every load
and is written on the next save. No two folios share one: a clash is
renewed on load.
Tests in misc/qet-mcp's integration suite, which drives these through
--run: a folio followed across the removal of the one before it, on a file
saved without folio uuids, and a new folio's uuid found in the saved file.
Both fail against a build without this change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- An end conductorEnds() reports as "?" is never picked.
- An empty "conductor" says why: an older project's conductors have no
saved uuid, so qet_conductors reports it empty.
- The op description and README say set_conductor still changes the
whole potential when given a uuid, and that older projects' conductors
are named by element + terminal until #1103.
- test_conductor_by_uuid covers move_conductor_segment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
set_conductor, move_conductor_segment and delete_conductor named a
conductor by one of its terminals, which had to carry exactly one
conductor: where two meet at a terminal, neither could be named from it.
Each now also takes "conductor": "{uuid}" (as qet_conductors reports it)
in place of element + terminal. The generated script asks
qet.conductorEnds() for the conductor's two ends and passes the one whose
terminal carries only that conductor. Where both ends are shared, or no
conductor has that uuid, the op fails and its "note" says which. The
lookup is required only when a uuid is used; giving both forms is an
error.
Tests: the script generated for each form and the argument errors; on a
folio where one terminal carries two conductors, deleting either by uuid
leaves exactly the other; an unknown uuid is reported. With the end chosen
without checking its terminal carries only that conductor, the terminal
test fails. 248/248 with a build carrying qet.conductorEnds().
Stacked on the conductorUuids()/conductorEnds() scripting change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict in _extras(): keep master's tables and folio uuids and _angle(),
and read only the folio's own inputs/shapes/images as this branch does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
set_table_position and delete_table took a table's index, and
set_element_text / delete_element_text a text field's index. An index
shifts when an earlier item is deleted, so a run that deletes table 0 and
then moves "table 1" moved the wrong table.
Each now also takes the item's uuid, resolved at run time through
qet.tableIndex() and qet.elementTextIndex() (the previous commit), as
texts, shapes and pictures already are through textIndex() and friends. A
text field's lookup is scoped to the op's element, since copies of a symbol
share their fields' uuids. An index still works, and a build without the
lookups is refused with the usual missing-methods hint only when a uuid is
actually used.
Tests: the generated script (no binary), and end to end: delete one table
then move the other, both by uuid; and edit one copy's shared field by uuid,
leaving the other copy's untouched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A field with a uuid on a symbol with none cannot be keyed by
(symbol, field); the mutation audit showed either side's guard could be
dropped unnoticed. Now tested with the symbol uuid missing on one side,
then the other.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qet.textIndex(), shapeIndex() and imageIndex() turn a free text's, shape's
or picture's uuid into the index the other calls take. Tables and symbol
text fields had no such lookup, so a script could only name them by index,
and an index shifts when an earlier item is deleted: a script that deletes
table 0 and then moves "table 1" moves the wrong table.
- qet.tableIndex(folio, uuid): the table's current index in tables(folio),
or -1.
- qet.elementTextIndex(folio, elementUuid, textUuid): the field's current
index in elementTexts(folio, elementUuid), or -1. The element is part of
the address because a field's uuid is unique only within its element:
copying an element keeps its fields' uuids (2612_ats_singlephase.qet has
one field uuid on 20 copies).
Tests in misc/qet-mcp's integration suite, which drives these through
--run: a table followed across a deletion of the one before it, and the
same field uuid resolving on two copies to each copy's own field. Both
fail against a build without this change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qet_diff's texts, shapes and pictures sections collected every <input>,
<shape> and <image> under a folio with iter(). Symbols in older files carry
their own <inputs><input> texts, so those were counted as the folio's free
texts: schema_indus.qet folio 1 showed 124 where QElectroTech has 2, and
editing one of those symbol texts would have been reported as a free text
changing.
Only the folio's direct <inputs>, <shapes> and <images> children are read
now. Shapes and pictures have no nested copies in the shipped examples;
they are changed too so the three stay alike. Found by comparing the
tools' answers with QElectroTech's own lists over the shipped examples.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qet_edit addresses free texts, shapes, pictures and tables by uuid, and
qet_diff reports them by uuid, but no tool listed them: the only way to
learn an item's uuid was to read the .qet. qet_items lists every free
text, shape, picture, table and symbol text field per folio (counted from
1, as qet_elements), with its uuid and main fields; filter by folio and
kind; default limit 500.
Also a test that every tool argument holding a data path is in the
workspace policy (_DATA_PATHS). Nothing checked that direction: a new tool
left out of the policy would have read or written anywhere with every test
passing, as removing qet_items' entry showed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A symbol text field's uuid is unique only within its symbol: copying a
symbol keeps them, so 7 of the 24 shipped examples repeat one, up to 20
times (2612_ats_singlephase.qet). Keyed on the field uuid alone, those
fields merged and an edit to one copy could be reported on another. They
are now keyed on (symbol uuid, field uuid).
More generally, uuids are used as keys only when present on every item and
unique on each side; otherwise the old position/ends matching is kept, for
texts, shapes, pictures, tables, conductors and folios alike. Found by the
seeded-edit invariants (an untouched symbol's field reported changed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
QElectroTech now saves a uuid on conductors (those created since, or
loaded with one), folios, symbol text fields and tables, but qet_diff still
matched them by position or by ends:
- conductors: a rewire read as one wire removed and another added; by
uuid it is that conductor with changed "ends".
- folios: a reorder read as every later folio changing its fields; by uuid
it is one "reordered" entry, plus "added"/"removed" folios.
- symbol text fields: deleting the first of two read as the second
changing; by uuid it is that field removed.
- tables (<graphics_table>) were not compared at all; now a section of
their own.
Each is matched by uuid only when every item of that kind on both sides
has one; otherwise the old matching is kept, and each section says which
in "keyed_by", since older files and their first re-save mix the two.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rotating a symbol changes only its orientation attribute (quarter turns),
which qet_diff did not read, so a pure rotation reported no change in any
section. The elements section now has "rotated": each symbol whose
orientation changed, with the value before and after.
Rotating and undoing leaves QElectroTech writing text-field rotations as
"-270" where they were "90" (or "-90" for "270"); compared as strings that
read as a change. Rotations of texts, shapes, pictures and element text
fields are now compared reduced to [0, 360).
Found by a seeded-edit invariant run over the shipped examples: 51
rotations unreported across 20 projects, and 7 undo sequences reported as
changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mutation audit -- planting one small bug at a time in the server's read
and diff code (a flipped comparison, a skipped branch, a dropped output
field, a list cap off by one) and running this suite -- caught 78 of 241
planted bugs with the tests that need no QElectroTech binary. qet_elements,
qet_conductors and their row builders caught none: a filter that never
applied, a missing field or a wrong default limit all passed.
Two new classes pin exact output on small hand-made projects:
ReadToolContracts (qet_project_info, qet_elements, qet_conductors: every
field, every filter, limit and truncation, legacy and current conductor
keys) and DiffContracts (every qet_diff section: moves, relabels, info,
conductors and the unstable-key warning, texts/shapes/pictures by uuid and
by position, element text fields, folio and project fields, terminal
strips, and every list cap). The same audit now catches 240 of 241; the
one left is equivalent (rsplit("/", 1) vs rsplit("/", 2) then [-1]).
Tests only; no change to the server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A folio's conductor auto-numbering rule is saved as
<autonum><conductor><part .../></conductor></autonum>. qet_project_info
and qet_conductors counted every <conductor> tag under the folio, so the
rule appeared as a wire with no ends and no number: schema_indus.qet
folio 1 reported 70 conductors where QElectroTech holds 69.
Only children of <conductors> are wires now. A new corpus test compares
each folio's element and conductor counts with QElectroTech's own
elementCount()/conductorCount() after loading the file, over every shipped
example; it failed only on schema_indus.qet before this change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qet.checkContinuity() answers a folio index it has no folio for with an
empty list, so qet_continuity returned "0 findings" for it -- the same
answer as a clean folio. The index counts from 0 while qet_elements numbers
folios from 1, so passing the last folio's number checked nothing and said
so cleanly; any other folio's number checked the next folio instead.
An index with no folio is now refused before QElectroTech is launched, with
the valid range and the counting rule. Each finding also carries
folio_number (counted from 1) beside the existing folio index. The
qet_continuity and qet_conductors descriptions and the README say how each
tool counts. Nothing changes for a valid index.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qet_elements and qet_project_info number folios from 1, as the application
does; qet_edit passes "folio" straight to the scripting API, which counts
from 0. Using the number qet_elements showed addresses the next folio, and
the op fails with nothing but "returned False".
Nothing changes for a call that works. When an op fails and the element it
names is in the project on another folio, the hint now says which index to
use. The qet_edit description and the README say how folios are counted.
Counting from 1 instead was not done: it would silently move every existing
caller's edits to another folio.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
QElectroTech writes an empty label as an empty <elementInformation> and
drops it on the next save. qet_diff compared the raw information bags, so
re-saving grafcet.qet with no edit reported 9 of its 37 elements as
info_changed ({"label": ""} -> {}), and Projet_vierge.qet 4.
An empty field and a missing one now compare equal, and info_changed lists
only fields that hold a value. The resave test now also requires an empty
info_changed; it failed on grafcet.qet and Projet_vierge.qet before this
change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qelectrotech --export-dxf <project.qet> <output_dir> [--show-terminals]
writes one DXF per folio, named <NN>_<title>.dxf like the PNG and SVG
exports. Discussion #1072.
The DXF code moves out of ExportDialog into DxfExport, unchanged except
that its options come from an ExportProperties argument instead of the
dialog. The dialog and the command line both call it, and the command
line uses the dialog's default options (the preferences' export
settings), so both write the same entities.
- Createdxf answers a file it cannot open with a message box and
exit(0); the command line checks the file first and fails with a
message and exit code 1 instead.
- A note is printed when pictures become outline boxes, as the dialog
warns.
- Scripting: qet.exportDxf(outDir, showTerminals). MCP: format "dxf".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
A file written before drawn items carried a uuid gets them from the folio
uuid, the kind of item and its order in the file. Three integration tests
pin what that promises: opening the same file twice gives the same uuids,
the first save writes exactly those and a reopen keeps them, and a uuid
repeated in a hand-edited file ends up naming one item only.
Each fails when the derived uuid is replaced by a random one. They read
drawing_item_view unsorted, which is what found the first-row-only bug
in QETSql::execReadOnly() fixed by the previous commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
- qet_edit: every op taking a text, shape or image "index" also takes its
uuid, resolved at run time with textIndex()/shapeIndex()/imageIndex().
Those are only required when a uuid is used.
- qet_element_build writes a uuid on every part (the caller's, or a new
one) and returns them in part_uuids; qet_element_info lists part and
terminal uuids.
- README and tests: drawing_item_view, uuid addressing, part uuids.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Elements, conductors, terminals and tables already carry a uuid. The
drawing furniture beside them did not, so a script or the MCP server could
only name a line, a box or a note by its index in a position-sorted list,
which shifts whenever one is added or removed.
- QetShapeItem, IndependentTextItem and DiagramImageItem get uuid(),
newUuid() and setUuid(), read from and written to a "uuid" attribute.
- A folio loaded from a file written before this (or carrying a duplicate
uuid) derives one from the folio uuid, the item kind and its order in the
file, so the same file gives the same uuids on every load and a re-save
is stable -- the #754 lesson for conductors.
- Paste and folio duplication renew them, as they already do for elements
and conductors.
- Scripting: texts(), shapes() and images() end each line with the uuid;
textIndex(), shapeIndex() and imageIndex() turn one back into an index.
- misc/qet-mcp: qet_diff keys texts, shapes and images on uuid when both
sides have one, so a move or edit reads as a change to that item.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Info.plist template shipped LSMinimumSystemVersion=12.3.0, but
the CMake build embedded the host SDK's deployment target (minos
26.0 on recent build machines) into the binary's LC_BUILD_VERSION.
Launch Services relies on the binary's minos, not the plist, so it
refused to launch the app on macOS < 26 even though it ran fine
when invoked directly.
- Set CMAKE_OSX_DEPLOYMENT_TARGET=14.0 explicitly in the cmake
configure step, matching the real minimum imposed by the
Homebrew Qt6 toolchain used for this build.
- Sync misc/Info.plist's LSMinimumSystemVersion to 14.0.0, and have
MacQetDeploy_arm64_cmake.sh set it via PlistBuddy at bundle
install time so it can't drift from the build target again.
Reported-by: guillaume.ruivo (forum)
Windows reads the device through hid.dll and SetupAPI with ctypes, as
hidapi's Windows backend does: shared, so 3DxWare can keep running, and
dropping the 0 report ID Windows adds, as hidapi does, so the bytes are
the ones QET decodes. Windows does not give out the report descriptor,
so a Windows recording has none; tst_spacemousehid then uses its
fallback layout.
The docstring, printed in full by --help, now has step-by-step
instructions for Linux, macOS and Windows, download included.
Tested: Windows Python 3.12 (embeddable) under Wine 10, against the
virtual uhid 3D mouse: --list finds it, a push sent during `right`
lands in `right`, and a button press and release land in `buttons`,
with the same bytes the Linux path records for the same push. Linux
and the mocked macOS path still pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The recorder only read /dev/hidraw, so it could not run on a Mac. It now
also reads through IOKit via ctypes, using the calls hidapi's mac backend
makes: a shared open (as #1028 does) and the input report callback.
hidapi passes those bytes through unchanged, so a recording is exactly
what QET decodes on macOS. Standard library only, and no sudo.
If 3DxWare holds the device, or Input Monitoring is not granted, it says
which instead of recording nothing. The JSON adds "backend" and
"other_readers" (any 3DxWare or spacenavd process that was running).
Reports that queue up while it waits for Enter are now dropped, so each
step holds only its own movement. The Linux path is otherwise unchanged.
Tested: the Linux path against the virtual uhid device
(tools/hid-capture/fake-spacemouse.py in the docker harness). A stale
report was dropped and the step's own push was kept. The macOS path has
only been run against a mocked IOKit, not on a Mac.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SpacePilot PRO recording on #599 has a weak `right` step: pushed
too lightly to read above the cap's normal cross-axis noise. A longer
window makes it easier to hold a firm push and gives the decoder more
samples to average over.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
On macOS, 3DxWare installs a driver extension that takes the 3D mouse
over. With it installed, HidBackend opens the device but receives
nothing, so the mouse did nothing in QElectroTech until 3DxWare was
uninstalled (discussion #599, PR #1028). Most Mac owners of a 3D mouse
have 3DxWare installed.
ConnexionBackend asks 3DxWare for the motion instead, through
3DconnexionClient.framework, as Blender does. SpaceMouseListener tries
it first. When 3DxWare is not installed, or is installed but its driver
is not running, it falls back to HidBackend, so the device works in
both setups. Take-over mode stops 3DxWare's own actions in QET, so the
view does not move twice.
The library is loaded at run time from where 3DxWare installs it:
nothing is linked or bundled, and the build needs no SDK. The few
declarations are written here, from Blender's
GHOST_NDOFManagerCocoa.mm, because 3Dconnexion's SDK headers may not be
redistributed. 3DxWare's axes are y up and z away from the user; they
are mapped to QET's raw USB convention by comparing Blender's 3DxWare
and spacenavd code paths.
The release script signs with the hardened runtime, which refuses a
library another team signed. misc/qelectrotech.entitlements adds
com.apple.security.cs.disable-library-validation (Blender's notarized
build carries the same one), and MacQetDeploy_arm64_cmake.sh now passes
it to all four signings of the app, including the re-sign inside the
DMG.
Tested: tst_spacemouseconnexion runs the backend on every platform
against fakeconnexion, a stand-in library that answers from its own
thread as 3DxWare does: registration, the axis mapping, buttons, other
clients' messages, 3DxWare not installed or not running, deletion
with a message in flight. Flipping an axis sign or dropping the client
check turns it red. realLibrary() loads the real framework when
3DxWare is installed. Linux Qt 6 build with the 3D mouse enabled: all
18 tests pass.
Not tested: on a Mac with a real device. The axis signs and whether
buttons arrive as a bitmask with current 3DxWare are unverified.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For device owners on Linux: guided movements, with every raw report and
the device's report descriptor saved to one JSON file. Dropped into
tests/qttest/fixtures/spacemouse/, a recording is checked by
tst_spacemousehid against what the user was asked to do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#984 switches JavaScript scripting off by default, and five tools here
drive QElectroTech through --run: qet_query, qet_continuity, qet_check,
qet_project_new, qet_edit. Against such a build they all stop working, and
what came back was exit code 3 and a paragraph of French naming a settings
dialog nobody driving an MCP server is looking at.
Nothing needed building to make them work again -- _run_qet() inherits its
environment, so QET_ENABLE_SCRIPTING=1 in the "env" block of the client's
own configuration already reaches QElectroTech. Verified both ways against
a #984 binary: without it qet_query returns ok=false exit=3, with it
ok=true and the rows.
So this is about saying so. The refusal is now recognised and answered with
an instruction the caller can act on, keyed on QElectroTech naming the
variable with exit 3 as a fallback for a future build that words it
differently. Two older hints fitted the same symptom and were overwriting
it -- "the binary never ran the script ... is it a build with --run
support?" sends the reader to check the one thing that is fine -- so both
now yield to whatever the launch already reported. qet_check builds its
answer fresh rather than layering onto the launch result, so it carries the
reason across explicitly; without that every check read "no result came
back", which is true and tells nobody why.
The server does not set the variable itself, on purpose. A switch a program
turns on for itself is not a switch: whoever configured this server and
pointed it at a QElectroTech binary made that choice, and their interactive
QElectroTech keeps whatever its own setting says. README says this, and the
registration example now shows the env block with both variables in it.
Six tests, faking subprocess.run so they cost no launch. Two are structural
rather than behavioural: one fails if either older hint goes back to
assigning over the specific one, the other reads which tools actually pass
script= to _run_qet and fails if the hint's list of them drifts. Both were
mutation-checked by reintroducing exactly those mistakes.
176 tests pass with QET_BINARY, QET_ELEMENTS, QET_EXAMPLES and
QET_ENABLE_SCRIPTING set.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two security reviews of #980 landed on the same gap: every path in a tool
call is chosen by the model, and nothing checked where those paths pointed.
That made the server a read/write primitive for anything the process could
reach -- read any project on the disk, export one somewhere else, overwrite
an unrelated file, embed an arbitrary local image or PDF. The sandboxed HOME
each QElectroTech launch gets isolates settings, not the filesystem.
Data paths are now confined to a workspace: QET_MCP_WORKSPACE (os.pathsep
separated), defaulting to the directory the server was started in, which is
what an MCP host normally sets anyway. QET_MCP_ALLOW_ANY_PATH=1 turns the
check off; it exists so that is a visible choice rather than the default.
Paths are resolved before comparison, so a symlink planted inside the
workspace is judged by where it points -- the case a string-prefix check
gets wrong.
Two arguments are deliberately exempt: "binary" and "elements_dir". Those
are configuration, chosen once by whoever runs the server, and both normally
live in /usr or a build tree. Confining them would reject the ordinary case
while stopping nothing -- they are not where a model gets to point the
server at /etc.
Enforcement sits at the dispatcher, where model-supplied arguments enter,
not inside each tool. Importing the module and calling tool_export() from
Python stays unconfined and is meant to: that is the caller's own code with
the caller's own paths.
Separately, an existing "output" is now refused unless the call passes
"overwrite": true. qet_project_new already worked this way; qet_export,
qet_edit and qet_element_build now match it. Replacing a file is the one
step this server cannot undo.
17 tests cover it, including the symlink escape, the traversal, the
overwrite gate and the operation-level file paths that add_image and
add_pdf_page carry one level down. Two of them compare the policy table
against the tool schemas, because a write tool missing from either list
fails silently in opposite directions. Two more drive a real server process
over stdio, which is the only thing that shows a call is gated rather than
merely gate-able. Mutation-checked: removing the confinement fails 8, and
desynchronising the two lists fails the drift pair.
170 tests pass, with QET_BINARY, QET_ELEMENTS and QET_EXAMPLES set so none
are skipped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
link_elements() now correctly refuses a mismatched report link instead
of allowing it (see the two preceding commits), so the
report_link_mismatch reproduction can no longer be built by linking
two already-differently-coloured conductors -- that path is refused
before it happens. Updated to patch a saved file's XML directly
instead (the same technique test_continuity_detects_a_tampered_
potential already uses), since a file QElectroTech's own edits produce
can no longer end up in this state at all. Adds
test_link_elements_refuses_a_mismatched_report_link to cover the
refusal itself.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Brings misc/qet-mcp up to date with checkContinuity()'s new
report_link_mismatch finding: updated qet_continuity's description,
and two new tests reproducing #974 with the shipped 02going_arrow.elmt/
01coming_arrow.elmt pair (no custom fixtures needed -- this element
type already ships).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>