Skip to content

0.10.0

Choose a tag to compare

@mapsmith-ai mapsmith-ai released this 25 Oct 15:48
· 59 commits to main since this release

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/share grid was opened over SMB; with PROJ networking on, an
    http:// grid made requests to any host (measured 2026-10-23, present since
    reproject_layer exists). 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
    (@null allowed), 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.md records it, and names
    what stays open: datasets in other GDAL formats, which GDAL opens first.
  • run_sql under 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_SetCRS followed by a two-argument ST_Transform, while read_text on
    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 in ST_Transform, ST_SetCRS or a GEOMETRY('…') type must be a
    literal the CRS policy accepts, every path given to ST_Read,
    ST_Read_Meta, read_parquet or named in FROM must 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 in run_sql --
    and the rest is refused with how to write it: statements other than
    SELECT (pass output_path to 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
    native GEOMETRY/GEOGRAPHY type, not only in the geo key.
    A file
    with the CRS there and no geo key 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). The projjson:<key> and srid:<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_operations returns for a given request can.

Fixed

  • run_sql can no longer crash the server with ST_Read of a layer that
    is not there.
    DuckDB's spatial extension (1.5.5) ends the process with a
    segmentation fault on ST_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_Meta lists and refuses a missing one with the list, in every
    mode. A statement other than SELECT cannot be checked that way, so one
    that names ST_Read and a layer is refused with the advice to write the
    SELECT and pass output_path; that matters without a workspace, where
    such statements run (CREATE TABLE … AS still crashed after the first
    version of this fix, found by the pre-release review). Found by the audit
    of the run_sql change above.
  • A datum shift written into the CRS itself is no longer recorded as a
    ballpark.
    For a CRS defined with +towgs84 (or WKT TOWGS84[]) 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
    every crs_decisions.transformation read 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=helmert and moved the data 79 m,
    and the record said is_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 says is_ballpark: false, accuracy_m: null (nobody has
    stated one) and carries x-mapsmith:shift_from_crs_definition: true to say
    why the two go together; docs/manifest-vocabulary.md has 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 way reproject_layer does.
    reproject_layer has 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, through to_crs. On a Monte Mario (Rome, EPSG:4806) layer PROJ's
    pick is the ballpark, so a parcel sat 79.5 m apart in clip_layer and in
    reproject_layer, each record true of its own run. The eight now go through
    the same chooser and apply it themselves, and crs_decisions.transformation
    describes what was chosen, with x-mapsmith:default_was_ballpark where 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_profile and least_cost_path, the
    zones and mask of zonal_statistics and clip_raster, and the pour points
    and stations of watershed and viewshed. What still lets the engine pick:
    reproject_raster, because rasterio warps the pixels itself, and the one-way
    measures measure_area and measure_length take in WGS 84, where the
    difference is parts per million of the answer. A test keeps every remaining
    to_crs to 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, as reproject_layer's output already did; and
    transform_by_control_points fits 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.py declared three facets where the README's headline
    figures declare four (geometry has 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
    from discovery_report.LEVELS[FULLEST] rather than written again; the
    history series' reachable column reads 78 of 78 from here on, and earlier
    rows were asked a weaker question.
  • nearest_neighbour_index no 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_distance refuses 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.md gives the reason
    the two cases where 0.8.0's reproject_layer moved 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.md says "through 0.8.0" where it meant it.