Repository navigation
0.10.0
Update if you run MapSmith under MAPSMITH_WORKSPACE. A coordinate
reference system -- a tool argument, or the one a file declares -- could make
PROJ read a file outside the workspace or open a network share, and in
run_sql DuckDB's own PROJ could do the same, in every release since the
workspace and the refusal of network shares arrived (0.2.0). Both are closed
here (Security below, and SECURITY.md), and both are in one advisory,
GHSA-3wvg-2qh8-8gg6.
Records still declare spec_version 1.0.0-draft.10, and no key is renamed.
Two keys of MapSmith's own are new inside crs_decisions.transformation:
x-mapsmith:shift_from_crs_definition, when the datum shift is written into
the CRS itself (+towgs84), and x-mapsmith:default_was_ballpark, when the
engine's own choice would have applied no shift. Results move where an
operation that brings a vector input onto another CRS now chooses the datum
transformation as reproject_layer does (Monte Mario from Rome: up to about
80 m); nearest_neighbour_index refuses a feature with no position. Under a
workspace, run_sql runs only SELECT and takes coordinate systems and
dataset paths as literals.
Security
- A coordinate reference system that reaches MapSmith's own code can no
longer make PROJ read a file outside the workspace or contact a host.+nadgrids=,+geoidgrids=and the WKT
and PROJJSON spellings of a parameter file name grid files by path,+init=
names a file to parse, and a CRS reaches PROJ as a tool argument
(target_crs) and from the metadata of the files an agent reads. Under a
workspace a path outside it was read and its content moved the output; a
//host/sharegrid was opened over SMB; with PROJ networking on, an
http://grid made requests to any host (measured 2026-10-23, present since
reproject_layerexists). A CRS policy now refuses, wherever a CRS enters --
tool arguments, the CRS a file declares, every reader, the datum module, and
the admission of every input path, before GDAL probes the file -- any grid,
init or model name that is not a bare name from PROJ's data directories
(@nullallowed), any transformation written as a PROJ string, and any
declared SRS PROJ cannot read; the declared SRS is read as text from the
file's own catalogue (GeoPackage by its bytes, shapefile.prj, GeoParquet
geo), and the server switches PROJ networking off under a workspace at
startup, for pyproj's PROJ and GDAL's.SECURITY.mdrecords it, and names
what stays open: datasets in other GDAL formats, which GDAL opens first. run_sqlunder a workspace can no longer make DuckDB's own PROJ read a
file outside it. DuckDB's spatial extension carries a PROJ of its own,
which opens files natively and never met the connection sandbox:
ST_Transform(geom, 'EPSG:4326', '+init=<a file outside>:1')read that file
-- the output moved by the false easting written in it -- and so did
ST_SetCRSfollowed by a two-argumentST_Transform, whileread_texton
the same path was refused (measured 2026-10-25; every release with the
sandboxed connection, 0.2.0 to 0.9.0). Under a workspace the statement is
now read with DuckDB's own parser (json_serialize_sql) before it runs:
every CRS inST_Transform,ST_SetCRSor aGEOMETRY('…')type must be a
literal the CRS policy accepts, every path given toST_Read,
ST_Read_Meta,read_parquetor named inFROMmust be a literal the path
gate accepts (globs are expanded by the sandboxed connection and each match
is checked), what GDAL would open must be a GeoPackage, a shapefile's.shp
or a GeoParquet, recognised by its bytes -- FlatGeobuf, GeoJSON and the other
formats whose SRS GDAL reads before MapSmith can are refused inrun_sql--
and the rest is refused with how to write it: statements other than
SELECT(passoutput_pathto keep a result), SQL held in a string
(query()), GDAL open options, and a CRS or a path computed in SQL. Without
a workspace nothing changes: file access there is unconfined by design.- The path gate reads the CRS a Parquet file declares in the column's
nativeGEOMETRY/GEOGRAPHYtype, not only in thegeokey. A file
with the CRS there and nogeokey at all passed the gate with nothing to
check, and DuckDB read that CRS and handed it to its PROJ: through
run_sql, a grid outside the workspace was read (found by the audit of the
change above, before release). Theprojjson:<key>andsrid:<n>forms
are followed too. Every tool's input admission goes through the same gate.
Added
- 103 phrasings on 36 operations, taken from how the QGIS Training Manual
poses its exercises. Each is a paraphrase of a problem a lesson sets ("strip
along both banks of a stream", "distance along the roads, not as the crow
flies"), written from half of the 220 exercises extracted from that manual
(GNU FDL 1.3, docs.qgis.org) and measured on the other half, which the
phrasings never saw: BM25 found@3 on words alone 47% -> 59%, with the
caller's facts 40% -> 51%. On the 118 independent requests of the published
table every level moved up (words alone 29% -> 30%, fullest declaration
60% -> 62%; the embedding engine 18% -> 19% and 56% -> 62%), and the README,
docs/catalog-entry-spec.md, the site and the tool instructions an agent
reads say the new figures. The ranking code did not change; the order
list_operationsreturns for a given request can.
Fixed
run_sqlcan no longer crash the server withST_Readof a layer that
is not there. DuckDB's spatial extension (1.5.5) ends the process with a
segmentation fault onST_Read('roads.gpkg', layer := 'nope'), and the
MCP server with it: one line of agent-written SQL was a denial of service.
The defect is upstream (reproduced on a bare connection); MapSmith now
checks the layer, in either spelling (:=or=), against the names
ST_Read_Metalists and refuses a missing one with the list, in every
mode. A statement other thanSELECTcannot be checked that way, so one
that namesST_Readand a layer is refused with the advice to write the
SELECTand passoutput_path; that matters without a workspace, where
such statements run (CREATE TABLE … ASstill crashed after the first
version of this fix, found by the pre-release review). Found by the audit
of therun_sqlchange above.- A datum shift written into the CRS itself is no longer recorded as a
ballpark. For a CRS defined with+towgs84(or WKTTOWGS84[]) PROJ
builds a Helmert from those parameters and applies it in full, but reports
accuracy -1 for it -- the value it also reports for the ballpark -- and
everycrs_decisions.transformationread that value as "no datum shift
was applied":reproject_layer's since 0.3.0, every other operation's
since 0.5.0. Measured on Monte Mario with the seven IGM
parameters: the pipeline carried+proj=helmertand moved the data 79 m,
and the record saidis_ballpark: true. The verdict now comes from PROJ's
own name for the operation it used ("Ballpark geographic offset" is its
word, and a concatenation carries every step's) and, failing a name, from
the pipeline it ran: a shift step is a shift. The name of a shift a CRS
declares itself is written by whoever wrote the file, so it is cut out of
the description before the word is looked for: a Helmert called "Ballpark
geocentric translation" is still a shift (security review, same day). A CRS that carries its own
shift is also no longer rebuilt from a registry code on the way in, which
dropped the+towgs84. Such a record saysis_ballpark: false,accuracy_m: null(nobody has
stated one) and carriesx-mapsmith:shift_from_crs_definition: trueto say
why the two go together;docs/manifest-vocabulary.mdhas the entry. Data
and coordinates are unchanged -- only the record was wrong, in the direction
that hides a shift. Found by the pre-release review of 0.9.0. - Every operation that brings a vector input onto another CRS chooses the
datum transformation the wayreproject_layerdoes.reproject_layerhas chosen its transformation since 0.5.0 (datum.best_operation:
a published operation that covers the data, or a datum's Greenwich twin), but
the eight vector operations that bring a second input onto the first one's CRS
--clip_layer,overlay_layers,nearest_join,merge_layers,spatial_join,
count_in_polygons,summarize_points_in_polygons,apportion_by_area-- let
PROJ pick, throughto_crs. On a Monte Mario (Rome, EPSG:4806) layer PROJ's
pick is the ballpark, so a parcel sat 79.5 m apart inclip_layerand in
reproject_layer, each record true of its own run. The eight now go through
the same chooser and apply it themselves, andcrs_decisions.transformation
describes what was chosen, withx-mapsmith:default_was_ballparkwhere the
engine's own choice would have applied no shift. The twin route also serves a
target counted from another meridian (a WGS 84 mask onto a Rome layer), which
it did not. Operations that compute in an estimated UTM zone and come back are
unchanged: the way out and the way back largely cancel. The coordinates of the eight
change only where PROJ's default is the ballpark and a covering operation or a
twin exists; everywhere else the chosen transformation is PROJ's own. The
same chooser now serves every operation that brings a vector input onto
another CRS:snap_layer,line_intersections,transform_by_control_points,
compare_layers,nearest_neighbour_index, the points of
sample_raster_at_points,elevation_profileandleast_cost_path, the
zones and mask ofzonal_statisticsandclip_raster, and the pour points
and stations ofwatershedandviewshed. What still lets the engine pick:
reproject_raster, because rasterio warps the pixels itself, and the one-way
measuresmeasure_areaandmeasure_lengthtake in WGS 84, where the
difference is parts per million of the answer. A test keeps every remaining
to_crsto the recorded round trips, by function and by count. Two
consequences to know about: on a Rome-meridian input, sampled values, zonal
statistics, crossings, snaps, basins and viewsheds move with the input by up
to about 80 m, asreproject_layer's output already did; and
transform_by_control_pointsfits against control points moved by the
chosen operation, so the fit moves with them. No new cost: the record
already surveyed the vertices before this change (about 7 s on 200,000
Monte Mario polygons, before and after), and within one datum the survey is
now skipped (1.0 s against 2.7 s). - The dashboard's "reachable with facets" probe asks at the published level.
benchmarks/dashboard.pydeclared three facets where the README's headline
figures declare four (geometryhas been a facet since 0.8.0), so at 78
entries 31 candidates survived -- one above the threshold below which the
whole set is handed over -- and three operations were reported "unreachable
even with facets" that the published level delivers. The tuple is now taken
fromdiscovery_report.LEVELS[FULLEST]rather than written again; the
history series'reachablecolumn reads 78 of 78 from here on, and earlier
rows were asked a weaker question. nearest_neighbour_indexno longer needs memory in the square of its
points. It built every pairwise distance before taking the nearest, so
100,000 points asked for 149 GiB before computing anything -- over HTTP, a
denial of service from one ordinary file. It now asks a tree for each
point's nearest other point, exactly and in linear memory; duplicated
records still count as a neighbour at zero, and still raise the
coincident-points note. The answers are the same. Present since 0.4.0.
A layer with a feature that has no or an empty geometry is now refused, as
cluster_points_by_distancerefuses it: 0.9.0 crashed on it, and the first
version of the tree counted the row in the number of points but not in the
distances, answering R = 4.47 for four points on a square whose R is 4.0
(found by the pre-release review).- Documentation, no code that runs.
SECURITY.mdgives the reason
the two cases where 0.8.0'sreproject_layermoved data with an operation
from elsewhere did not get an advisory; a 0.5.x entry below and a docstring
said buffering NAD27 crossed a datum "seven metres each way", which is the
operation's stated accuracy -- the shift is tens of metres; and
docs/benchmarks.mdsays "through 0.8.0" where it meant it.