mapsmith 0.2.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mapsmith-0.2.2/.dockerignore +35 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/.gitignore +6 -0
- mapsmith-0.2.2/CHANGELOG.md +336 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/CLAUDE.md +4 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/CONTRIBUTING.md +1 -1
- mapsmith-0.2.2/Dockerfile +32 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/MANIFESTO.md +13 -11
- mapsmith-0.2.2/PKG-INFO +466 -0
- mapsmith-0.2.2/README.md +430 -0
- mapsmith-0.2.2/SECURITY.md +159 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/docker-compose.yml +7 -3
- mapsmith-0.2.2/docs/benchmarks.md +473 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/examples/03_validated_plans.ipynb +24 -24
- mapsmith-0.2.2/funding.json +71 -0
- mapsmith-0.2.2/pyproject.toml +87 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/server.json +49 -49
- mapsmith-0.2.2/src/mapsmith/__init__.py +12 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/src/mapsmith/engines/dispatch.py +2 -1
- {mapsmith-0.2.0 → mapsmith-0.2.2}/src/mapsmith/engines/duckdb_engine.py +101 -24
- {mapsmith-0.2.0 → mapsmith-0.2.2}/src/mapsmith/engines/raster.py +2 -2
- {mapsmith-0.2.0 → mapsmith-0.2.2}/src/mapsmith/engines/vector.py +50 -15
- {mapsmith-0.2.0 → mapsmith-0.2.2}/src/mapsmith/engines/whitebox_engine.py +21 -8
- mapsmith-0.2.2/src/mapsmith/gdal_policy.py +128 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/src/mapsmith/jobs.py +95 -87
- {mapsmith-0.2.0 → mapsmith-0.2.2}/src/mapsmith/plans/executor.py +169 -157
- {mapsmith-0.2.0 → mapsmith-0.2.2}/src/mapsmith/preview.py +291 -272
- mapsmith-0.2.2/src/mapsmith/provenance.py +278 -0
- mapsmith-0.2.2/src/mapsmith/sql_policy.py +105 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/src/mapsmith/ui.py +385 -367
- {mapsmith-0.2.0 → mapsmith-0.2.2}/src/mapsmith/verify.py +702 -573
- mapsmith-0.2.2/src/mapsmith/workspace.py +201 -0
- mapsmith-0.2.2/tests/test_duckdb_sandbox.py +334 -0
- mapsmith-0.2.2/tests/test_gdal_policy.py +313 -0
- mapsmith-0.2.2/tests/test_geoparquet2.py +177 -0
- mapsmith-0.2.2/tests/test_redaction.py +207 -0
- mapsmith-0.2.2/tests/test_showcase.py +378 -0
- mapsmith-0.2.2/tests/test_sql_credentials.py +117 -0
- mapsmith-0.2.2/tests/test_verification_status.py +135 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/tests/test_verify_repair.py +30 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/tests/test_whitebox_encoding.py +7 -2
- {mapsmith-0.2.0 → mapsmith-0.2.2}/tests/test_workspace.py +52 -9
- mapsmith-0.2.0/CHANGELOG.md +0 -77
- mapsmith-0.2.0/Dockerfile +0 -17
- mapsmith-0.2.0/PKG-INFO +0 -288
- mapsmith-0.2.0/README.md +0 -252
- mapsmith-0.2.0/SECURITY.md +0 -53
- mapsmith-0.2.0/docs/benchmarks.md +0 -165
- mapsmith-0.2.0/pyproject.toml +0 -63
- mapsmith-0.2.0/src/mapsmith/__init__.py +0 -3
- mapsmith-0.2.0/src/mapsmith/provenance.py +0 -90
- mapsmith-0.2.0/src/mapsmith/workspace.py +0 -106
- mapsmith-0.2.0/tests/test_duckdb_sandbox.py +0 -184
- {mapsmith-0.2.0 → mapsmith-0.2.2}/LICENSE +0 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/TRADEMARKS.md +0 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/examples/01_verified_geoprocessing.ipynb +0 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/examples/02_terrain_hydrology.ipynb +0 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/examples/README.md +0 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/examples/fixtures/mount_st_helens_dem.tif +0 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/src/mapsmith/catalog.py +0 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/src/mapsmith/engines/__init__.py +0 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/src/mapsmith/engines/sedona_engine.py +0 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/src/mapsmith/plans/__init__.py +0 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/src/mapsmith/plans/models.py +0 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/src/mapsmith/plans/registry.py +0 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/src/mapsmith/plans/validator.py +0 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/src/mapsmith/server.py +0 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/tests/test_catalog.py +0 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/tests/test_catalog_retrieval.py +0 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/tests/test_engines.py +0 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/tests/test_plans.py +0 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/tests/test_preview.py +0 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/tests/test_raster.py +0 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/tests/test_smoke.py +0 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/tests/test_ui.py +0 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/tests/test_verify.py +0 -0
- {mapsmith-0.2.0 → mapsmith-0.2.2}/tests/test_whitebox.py +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# The build only needs pyproject/README/LICENSE/src, but everything else in the
|
|
2
|
+
# directory is still sent to the daemon without this file — including a local
|
|
3
|
+
# .venv, built wheels and any .env sitting next to them. CI builds from a clean
|
|
4
|
+
# checkout, so nothing private has ever reached a published layer; this is here
|
|
5
|
+
# so a `docker build .` on a working machine cannot be the first time.
|
|
6
|
+
.git/
|
|
7
|
+
.github/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
env/
|
|
11
|
+
dist/
|
|
12
|
+
build/
|
|
13
|
+
*.egg-info/
|
|
14
|
+
__pycache__/
|
|
15
|
+
*.py[cod]
|
|
16
|
+
.pytest_cache/
|
|
17
|
+
.ruff_cache/
|
|
18
|
+
.mypy_cache/
|
|
19
|
+
.ipynb_checkpoints/
|
|
20
|
+
|
|
21
|
+
# secrets and local configuration
|
|
22
|
+
.env
|
|
23
|
+
.env.*
|
|
24
|
+
*.pem
|
|
25
|
+
*.key
|
|
26
|
+
|
|
27
|
+
# large or irrelevant to the image
|
|
28
|
+
benchmarks/
|
|
29
|
+
docs/
|
|
30
|
+
examples/
|
|
31
|
+
tests/
|
|
32
|
+
deploy/
|
|
33
|
+
*.md
|
|
34
|
+
!README.md
|
|
35
|
+
uv.lock
|
|
@@ -22,6 +22,12 @@ venv/
|
|
|
22
22
|
*.provenance.json
|
|
23
23
|
data/
|
|
24
24
|
|
|
25
|
+
# Scratch databases: probing the DuckDB sandbox (ATTACH, COPY) drops these in
|
|
26
|
+
# the repo root, and one of them reached a public commit before we noticed.
|
|
27
|
+
*.db
|
|
28
|
+
*.duckdb
|
|
29
|
+
*.wal
|
|
30
|
+
|
|
25
31
|
# OS / editors
|
|
26
32
|
.DS_Store
|
|
27
33
|
Thumbs.db
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to MapSmith are documented here, in the format of
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project follows
|
|
5
|
+
[semantic versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.2.2] — 2026-08-22
|
|
10
|
+
|
|
11
|
+
A security release. The fixes below close holes present in 0.2.1, which is
|
|
12
|
+
published: **if you run MapSmith on data or paths an agent can influence,
|
|
13
|
+
upgrade.** The headline is that a plain local file could make GDAL fetch a URL
|
|
14
|
+
or read a dataset from outside the workspace, and that credentials could reach
|
|
15
|
+
a manifest.
|
|
16
|
+
|
|
17
|
+
Worth saying plainly, because it shaped the release: each fix here was found by
|
|
18
|
+
auditing the previous one. The remote opt-in left `.vrt` able to reach the
|
|
19
|
+
network; closing `.vrt` left `GDALG` and `MRF` doing the same thing; refusing
|
|
20
|
+
credential SQL wrote the credential into the refusal message. The pattern is
|
|
21
|
+
not bad luck — it is what happens when a guard is written against an instance
|
|
22
|
+
instead of a class, and the response in each case was to move the check one
|
|
23
|
+
level down rather than add another name to a list.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **`funding.json`** at the repository root ([FLOSS/fund
|
|
28
|
+
manifest](https://fundingjson.org/)), stating what the project would use
|
|
29
|
+
funding for: the correctness suite and the provenance specification. No
|
|
30
|
+
payment provider is published — arrangements are made in writing.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- **Remote and virtual paths are refused by default** (`MAPSMITH_ALLOW_REMOTE=1`
|
|
35
|
+
to allow them). GDAL `/vsi*` and `https://` forms used to be accepted whenever
|
|
36
|
+
no workspace was set, justified as being the user's own responsibility — and
|
|
37
|
+
the user is not who decides: the path is written by the model, from whatever it
|
|
38
|
+
read, so a third-party dataset carrying "the updated layer lives at
|
|
39
|
+
https://evil.tld/x.gpkg" was enough to have GDAL parse attacker-chosen bytes
|
|
40
|
+
in-process. The refusal now covers both path arguments and `run_sql` text,
|
|
41
|
+
which closes the SSRF that came with it (raw SQL could read any endpoint the
|
|
42
|
+
host can reach — internal services, cloud metadata — and return the content in
|
|
43
|
+
the tool result) as well as the `INSTALL ... FROM '<url>'` fetch. Cloud-native
|
|
44
|
+
data stays a supported use case: the capability is gated, not removed. A
|
|
45
|
+
workspace refuses remote forms regardless, and validated plans stay strict
|
|
46
|
+
whatever the setting.
|
|
47
|
+
- **The container is unprivileged and confined by default.** The published image
|
|
48
|
+
runs as uid 1000 instead of root and sets `MAPSMITH_WORKSPACE=/data` itself, so
|
|
49
|
+
`docker run -v your/data:/data ghcr.io/mapsmith-ai/mapsmith` gets the path jail
|
|
50
|
+
and the sandboxed SQL engine without the operator having to remember `-e` — the
|
|
51
|
+
wrong way round for a default. Two consequences before you upgrade: a bind
|
|
52
|
+
mount owned by another user is no longer writable (pass
|
|
53
|
+
`--user $(id -u):$(id -g)`), and everything outside `/data` is refused,
|
|
54
|
+
including remote paths, which a workspace refuses whatever
|
|
55
|
+
`MAPSMITH_ALLOW_REMOTE` says. The Kubernetes example states the same posture at
|
|
56
|
+
pod level (`runAsNonRoot`, `readOnlyRootFilesystem`, dropped capabilities).
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
|
|
60
|
+
- **GDAL indirection was closed as an instance, not as a class.** Deregistering
|
|
61
|
+
the drivers behind `.vrt` left `GDALG` and `MRF` doing the same job. GDALG
|
|
62
|
+
(*GDAL Streamed Algorithm*, GDAL 3.11) reads a JSON document holding a `gdal`
|
|
63
|
+
command line and runs it when the dataset is opened, and it is recognised by
|
|
64
|
+
**content, not by extension** — so the filename tells you nothing, and the
|
|
65
|
+
command line can name a local path as readily as a URL. Measured with remote
|
|
66
|
+
reads off **and** a workspace set: a file called `roads.geojson` inside the
|
|
67
|
+
workspace issued a GET, and another read a dataset from *outside* the
|
|
68
|
+
workspace and handed back its rows. That is containment broken, not only
|
|
69
|
+
egress. MRF is narrower — extension-gated, fetched on the first pixel read.
|
|
70
|
+
Both are now skipped, along with the remaining drivers GDAL's own security
|
|
71
|
+
page names as opening other datasets internally.
|
|
72
|
+
|
|
73
|
+
The list is not the fix. It was correct when written and became incomplete
|
|
74
|
+
because GDAL shipped a new driver, which will happen again. A test now
|
|
75
|
+
enumerates the drivers registered in a clean subprocess and fails on any name
|
|
76
|
+
nobody has reviewed — the only version of this check that keeps working
|
|
77
|
+
across upstream releases.
|
|
78
|
+
- **A workspace no longer loses to the opt-in.** `MAPSMITH_ALLOW_REMOTE=1`
|
|
79
|
+
together with `MAPSMITH_WORKSPACE` re-registered the indirection drivers,
|
|
80
|
+
because the predicate answering "is remote allowed" read only the environment
|
|
81
|
+
variable while its own documentation said a workspace overrides it. Two
|
|
82
|
+
callers compensated for that and one did not. The predicate answers the whole
|
|
83
|
+
question now: a check each caller has to remember to add is a check that will
|
|
84
|
+
be missing somewhere.
|
|
85
|
+
- **Plan manifests are redacted, and the credential refusal no longer quotes the
|
|
86
|
+
credential.** `<output>.plan.json` is written as a plain dict, so the redaction
|
|
87
|
+
applied to every provenance record never reached it — while `goal`, each
|
|
88
|
+
step's `comment` and a failed step's `error` all carry text written by the
|
|
89
|
+
model or the user. The sharp case: refusing `ATTACH 'postgres://user:pw@…'`
|
|
90
|
+
produced a message quoting the fragment, password included, and that message
|
|
91
|
+
is what the manifest recorded. The mechanism that exists to keep credentials
|
|
92
|
+
out of manifests was putting one in. Redaction now runs on the whole manifest
|
|
93
|
+
at the point it becomes a file.
|
|
94
|
+
- **A credential written with a quoted identifier is refused.**
|
|
95
|
+
`SET "s3_secret_access_key" = '…'` and `PRAGMA "…"='…'` defeated *both* layers:
|
|
96
|
+
the refusal pattern required the credential word to follow `SET` contiguously,
|
|
97
|
+
and the redaction pattern allowed only whitespace between the name and the
|
|
98
|
+
`=`, so the closing quote broke each of them. This was not one of the two
|
|
99
|
+
documented limits — the name was perfectly recognisable.
|
|
100
|
+
- **`crs_decisions` and `notes` are redacted on the paths that actually run.**
|
|
101
|
+
Redaction happened at construction, and no engine passes those fields to the
|
|
102
|
+
constructor — every one assigns afterwards. So two of the four fields
|
|
103
|
+
SECURITY.md lists as covered were never redacted in practice, and
|
|
104
|
+
`parameters_redacted` stayed false. Redaction now also runs in `write_for`,
|
|
105
|
+
the single point where a manifest becomes a file, and covers `verification`
|
|
106
|
+
and `repairs` as well. The test that claimed to cover this passed both fields
|
|
107
|
+
as constructor arguments — a shape no caller uses — so it was green and
|
|
108
|
+
proved nothing; it now uses the real one.
|
|
109
|
+
- **A local GDAL indirection file could reach the network from inside a
|
|
110
|
+
workspace.** A `.vrt` is a plain local path, so the path guard, the SQL scan
|
|
111
|
+
and DuckDB's `allowed_directories` all saw a local file while GDAL fetched
|
|
112
|
+
whatever its `<SrcDataSource>` named — measured on 0.2.1 as HEAD and GET
|
|
113
|
+
leaving the process with `MAPSMITH_ALLOW_REMOTE` unset **and**
|
|
114
|
+
`MAPSMITH_WORKSPACE` set, through the GeoPandas/pyogrio path. That
|
|
115
|
+
contradicted the one promise SECURITY.md states as testable, so the fix is at
|
|
116
|
+
GDAL's level: with remote reads off, the indirection and network drivers are
|
|
117
|
+
deregistered before the geospatial stack initialises. The opt-in restores
|
|
118
|
+
them, including when a parent process installed the policy — containers pass
|
|
119
|
+
their whole environment down, and a switch that cannot lift an inherited
|
|
120
|
+
policy is a switch that does nothing. Found by an adversarial audit of the
|
|
121
|
+
commit that introduced the opt-in, i.e. of the fix itself.
|
|
122
|
+
- **Credentials no longer reach provenance manifests or the job ledger.**
|
|
123
|
+
`run_sql` records the query, and manifests are made to be shared, so an agent
|
|
124
|
+
emitting `CREATE SECRET (... SECRET 'AKIA…')` in the same session used to write
|
|
125
|
+
that key into a file destined for a bug report. **SQL that configures a
|
|
126
|
+
credential is now refused before it runs** — `CREATE SECRET` in any spelling,
|
|
127
|
+
`SET`/`PRAGMA` of a credential-bearing setting, `ATTACH` carrying a password or
|
|
128
|
+
URI userinfo — with a message pointing at where credentials belong: the
|
|
129
|
+
environment of the process that starts the server, out of reach of a tool call.
|
|
130
|
+
Nothing documented used that path, and in MapSmith's sandbox only one secret
|
|
131
|
+
type was even constructible.
|
|
132
|
+
|
|
133
|
+
Redaction stays as the second layer for credentials that reach a manifest
|
|
134
|
+
without being SQL (a signed URL as an input path, a connection string as an
|
|
135
|
+
argument), now covering `crs_decisions`, `notes`, input paths and the ledger's
|
|
136
|
+
`error` column — an engine error quotes the statement that failed. Masked
|
|
137
|
+
values are quoted, so a redacted statement still parses when pasted back into
|
|
138
|
+
a client.
|
|
139
|
+
|
|
140
|
+
Refusal came first because redaction alone did not hold: an adversarial audit
|
|
141
|
+
of the shipped version escaped it with `MAP{'Authorization': 'Bearer …'}`, an
|
|
142
|
+
`E'…'` literal, dollar quoting and a comment between name and value — and the
|
|
143
|
+
`E'…'` case masked the *wrong* argument while keeping the secret, producing a
|
|
144
|
+
manifest both misleading and leaky. All four are regression tests now.
|
|
145
|
+
Remaining limits, documented rather than implied: detection is name-based, so
|
|
146
|
+
a bare positional secret is not caught, and neither is a URI that
|
|
147
|
+
percent-encodes the colon of its own userinfo.
|
|
148
|
+
- **GeoParquet 2.0 files are read instead of refused.** 2.0 (`v2.0.0-rc.1`)
|
|
149
|
+
moves geometry into Parquet's own `GEOMETRY`/`GEOGRAPHY` logical types and
|
|
150
|
+
makes the `geo` metadata key optional, and DuckDB already writes such files.
|
|
151
|
+
MapSmith read them inconsistently: `run_sql` worked, `describe_dataset` failed
|
|
152
|
+
with a raw GeoPandas `Missing geo metadata` error, and the CRS probe reported
|
|
153
|
+
`unknown` **even for a file that states its CRS** — which made the CRS
|
|
154
|
+
precondition refuse valid work for a wrong reason. The CRS now comes from the
|
|
155
|
+
logical type when there is no `geo` key, in all the forms met in practice: the
|
|
156
|
+
spec default (`OGC:CRS84`), an authority string, `projjson:<key>`, and the
|
|
157
|
+
whole PROJJSON document inline, which is what DuckDB writes.
|
|
158
|
+
|
|
159
|
+
One form is deliberately not resolved: `srid:<n>`. The spec defines it as a
|
|
160
|
+
numeric identifier and names no authority (its own example is `srid:0`), so
|
|
161
|
+
reading it as `EPSG:<n>` would be MapSmith inventing a coordinate system and
|
|
162
|
+
recording it as fact. Such a file is treated as having no CRS, so the CRS
|
|
163
|
+
precondition refuses it like any other input without one. The message does
|
|
164
|
+
not yet quote the `srid:` declaration that caused it, which would tell the
|
|
165
|
+
agent what to fix — tracked separately rather than claimed here.
|
|
166
|
+
|
|
167
|
+
**Writing now states its flavour instead of inheriting one.** `run_sql`
|
|
168
|
+
materialises with `geoparquet_version 'BOTH'`, so one output file carries
|
|
169
|
+
Parquet's native geometry types (CRS included as PROJJSON) *and* the 1.x `geo`
|
|
170
|
+
metadata — a 2.0-native reader and GeoPandas 1.x both open it, which is
|
|
171
|
+
asserted by a test that reads it back both ways. This was not a free choice:
|
|
172
|
+
DuckDB 1.4 wrote the native types by default and 1.5 changed the default back
|
|
173
|
+
to 1.x, so the installed engine version was silently deciding the canonical
|
|
174
|
+
output format of a provenance product. The GeoPandas writer path stays 1.x,
|
|
175
|
+
because GeoPandas 1.1 caps `schema_version` there.
|
|
176
|
+
- **Dependency floors raised for a correctness reason, not a housekeeping one.**
|
|
177
|
+
`pyarrow>=21`: measured on a file DuckDB itself produced, pyarrow 18 and 19
|
|
178
|
+
**raise** on Parquet's geospatial logical types ("Thrift LogicalType that is
|
|
179
|
+
not recognized"), 20 opens the file but reports the type as `Undefined`, and 21
|
|
180
|
+
reports `Geometry` with its CRS. Below 21, MapSmith could not read back its own
|
|
181
|
+
`run_sql` output — the CRS probe returned `unknown`, which made the CRS
|
|
182
|
+
precondition refuse a file MapSmith had just written. `duckdb>=1.5` is the
|
|
183
|
+
floor for the `geoparquet_version` option above.
|
|
184
|
+
- **Persistent DuckDB secrets can no longer be created from a tool call.** They
|
|
185
|
+
are written to `~/.duckdb/stored_secrets` — outside any workspace and beyond
|
|
186
|
+
the session. A workspace already refused that write; the connection now sets
|
|
187
|
+
`allow_persistent_secrets = false` in both modes, before locking the
|
|
188
|
+
configuration.
|
|
189
|
+
- **The `mcp` floor is 1.28.1.** The SDK range MapSmith allowed admitted three
|
|
190
|
+
High-severity advisories (CVE-2026-59950, CVE-2026-52869, CVE-2026-52870).
|
|
191
|
+
None is exploitable here — MapSmith uses stdio and stateless Streamable HTTP
|
|
192
|
+
with no authentication, never the websocket transport, never task handlers —
|
|
193
|
+
but a fresh install could resolve to a version carrying them, and a scanner
|
|
194
|
+
cannot know the difference.
|
|
195
|
+
- **`duckdb` is now capped below 2.0.** The floor was raised to 1.5 for a
|
|
196
|
+
correctness reason (above); the cap is for a different one. DuckDB 2.0 is on
|
|
197
|
+
the autumn-2026 calendar with breaking changes, and an unbounded requirement
|
|
198
|
+
means the first install after that release can fail with nothing on our side
|
|
199
|
+
having changed. A pin you have to lift deliberately beats an install that
|
|
200
|
+
breaks on someone else's schedule.
|
|
201
|
+
|
|
202
|
+
## [0.2.1] — 2026-08-20
|
|
203
|
+
|
|
204
|
+
Three fixes you would rather not find yourself. All came from reviewing 0.2.0
|
|
205
|
+
*after* it shipped, and all were reproduced through the real MCP tools instead
|
|
206
|
+
of read off a diff.
|
|
207
|
+
|
|
208
|
+
### Fixed
|
|
209
|
+
|
|
210
|
+
- **An empty spatial join no longer crashes before writing its manifest.**
|
|
211
|
+
DuckDB writes no GeoParquet `geo` metadata for a zero-row result, so reading
|
|
212
|
+
the output back raised — on the default engine path, for exactly the case the
|
|
213
|
+
verification checks exist to explain, while the tool description promised a
|
|
214
|
+
warning. A zero-row result is now written as a valid empty GeoParquet with the
|
|
215
|
+
analysis CRS and the joined schema, and the join goes through the same audited
|
|
216
|
+
writer as everything else, so the manifest exists even when a check fails.
|
|
217
|
+
- **A GeoParquet declaring `crs: null` is no longer read as CRS84.** MapSmith
|
|
218
|
+
invented a coordinate system and recorded it in the manifest as fact, with
|
|
219
|
+
`verified: true` — the worst class of bug a provenance tool can have. The
|
|
220
|
+
GeoParquet spec distinguishes an *absent* `crs` field, which does mean CRS84,
|
|
221
|
+
from an explicit null, which means unknown; so does MapSmith now, and an
|
|
222
|
+
unknown CRS is refused by the preconditions like any other missing CRS.
|
|
223
|
+
- **The DuckDB sandbox locks its configuration in every mode.** The lock used to
|
|
224
|
+
apply only under `MAPSMITH_WORKSPACE`, so a multi-statement call could switch
|
|
225
|
+
extension autoloading back on, and an explicit `LOAD httpfs` was never blocked
|
|
226
|
+
at all. Locking is now unconditional and DuckDB's HTTP and S3 filesystems are
|
|
227
|
+
disabled, while local reads keep working.
|
|
228
|
+
- **The security documentation no longer claims that unconfined mode blocks
|
|
229
|
+
network egress.** It does not, and it never did: GDAL carries its own HTTP
|
|
230
|
+
client, so `ST_Read('/vsicurl/https://…')` in raw SQL reads whatever the host
|
|
231
|
+
can reach — internal services and metadata endpoints included — and the URL it
|
|
232
|
+
names can carry data out. Remote reads are deliberately available while the
|
|
233
|
+
server is unconfined, because cloud-native data is a feature; the claim that
|
|
234
|
+
the network was closed anyway was the bug. README and SECURITY.md now state
|
|
235
|
+
the price of that choice, and two tests pin both halves of it: without a
|
|
236
|
+
workspace the read succeeds, and with one it is refused *before any request
|
|
237
|
+
leaves*, asserted by counting requests at a loopback server instead of
|
|
238
|
+
matching an error message. If you do not trust your `run_sql` input, set a
|
|
239
|
+
workspace.
|
|
240
|
+
- **The multi-layer guard fails closed.** A container whose layer list could not
|
|
241
|
+
be read looked like a single-layer file, and mechanical geometry repair would
|
|
242
|
+
then have destroyed the other layers while recording success.
|
|
243
|
+
- **`execute_plan` reports `repairs` per step.** Geometry MapSmith rewrote was
|
|
244
|
+
visible in a single-operation result and invisible at plan level.
|
|
245
|
+
- **Manifests record `EPSG:32632`, not 2.5 KB of PROJJSON**, when a GeoParquet
|
|
246
|
+
input carries its CRS as an embedded projection object.
|
|
247
|
+
- **The example `docker-compose.yml` binds MinIO to loopback.** It published
|
|
248
|
+
ports 9000/9001 on every interface with the documented development
|
|
249
|
+
credentials.
|
|
250
|
+
|
|
251
|
+
### Changed
|
|
252
|
+
|
|
253
|
+
- **The provenance badge has three states instead of two.** "All critical checks
|
|
254
|
+
passed" is vacuously true when no critical check ran — a `run_sql` manifest,
|
|
255
|
+
for one — so an output whose only check had *failed* rendered with the same
|
|
256
|
+
green tick as a verified buffer. `provenance_summary` now reports `verified`,
|
|
257
|
+
`failed` or `unchecked`, with the reason, and the map panel renders all three.
|
|
258
|
+
The `verified` boolean stays in the payload, computed correctly, so a client
|
|
259
|
+
reading it gets a fix rather than a breaking change.
|
|
260
|
+
- **The README says when *not* to use MapSmith**, and the
|
|
261
|
+
[benchmark results](docs/benchmarks.md) are linked from it — they were public
|
|
262
|
+
for a day with nothing pointing at them.
|
|
263
|
+
|
|
264
|
+
## [0.2.0] — 2026-08-20
|
|
265
|
+
|
|
266
|
+
The first release you can point an agent at and trust the answer: results are
|
|
267
|
+
verified on the way in and on the way out, plans are checked before anything
|
|
268
|
+
runs, and the server can be confined to a single directory.
|
|
269
|
+
|
|
270
|
+
### Added
|
|
271
|
+
|
|
272
|
+
- **Interactive map inside the chat.** `preview_map` renders your layers on a
|
|
273
|
+
pan/zoom map panel in any client implementing the
|
|
274
|
+
[MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview)
|
|
275
|
+
extension (field-tested on Claude Desktop), with an OpenStreetMap backdrop
|
|
276
|
+
and a provenance card per layer showing operation, engine and one of three
|
|
277
|
+
states: `verified ✓`, `verification failed`, or `not verifiable` when no
|
|
278
|
+
critical check ran. Fully self-contained; on clients without MCP Apps the
|
|
279
|
+
same call returns structured data.
|
|
280
|
+
- **Typed plans.** `validate_plan` statically checks a multi-step analysis —
|
|
281
|
+
operations exist and are installed, arguments complete and well-typed,
|
|
282
|
+
`$step` references resolve backwards, input files exist, outputs don't
|
|
283
|
+
collide, and the CRS of every intermediate is simulated from the real
|
|
284
|
+
inputs — and returns machine-actionable error codes. `execute_plan` then
|
|
285
|
+
runs the validated plan with per-step provenance plus a plan-level manifest
|
|
286
|
+
fingerprinting the exact plan that produced the result.
|
|
287
|
+
- **Terrain and hydrology** on the Whitebox Workflows engine (`[whitebox]`
|
|
288
|
+
extra): `hillshade`, `flow_accumulation` (D8, with depression filling) and
|
|
289
|
+
`watershed` (many pour points at once).
|
|
290
|
+
- **Zonal statistics** with exact fractional pixel coverage via exactextract
|
|
291
|
+
(`[raster]` extra).
|
|
292
|
+
- **A searchable operation catalog.** `list_operations` ranks capabilities by
|
|
293
|
+
relevance (BM25) so an agent can discover what exists — including what is
|
|
294
|
+
planned but not yet available — instead of guessing from a wall of tools.
|
|
295
|
+
- **Workspace confinement.** Set `MAPSMITH_WORKSPACE` and every path argument
|
|
296
|
+
must resolve inside it, `run_sql`'s DuckDB connection is sandboxed to that
|
|
297
|
+
directory with extension loading refused and memory/temp-disk capped, and
|
|
298
|
+
UNC hosts and NTFS alternate data streams are refused in every mode.
|
|
299
|
+
- **Verification on the way in.** Operations check their inputs for the
|
|
300
|
+
failures that produce plausible junk: a missing CRS is refused outright, and
|
|
301
|
+
empty inputs or extents that cannot possibly intersect come back as named
|
|
302
|
+
warnings with hints — in the tool result, not only in the manifest.
|
|
303
|
+
- **Bounded deterministic repair.** Mechanically broken output geometry is
|
|
304
|
+
repaired (`make_valid`, at most two rounds, written atomically) and every
|
|
305
|
+
attempt is recorded in the manifest: a repaired output never looks like one
|
|
306
|
+
that was right the first time.
|
|
307
|
+
- **A notebook gallery** (`examples/`) and a
|
|
308
|
+
[benchmarks page](docs/benchmarks.md) with the harness that produced it.
|
|
309
|
+
|
|
310
|
+
### Fixed
|
|
311
|
+
|
|
312
|
+
- **Wrong terrain results from ordinary compressed rasters.** Whitebox
|
|
313
|
+
Workflows 2.x does not undo the TIFF predictor when reading, so any DEM
|
|
314
|
+
saved with `PREDICTOR=2` or `3` — the standard encoding for elevation data —
|
|
315
|
+
produced hillshades and flow accumulations that looked like terrain and were
|
|
316
|
+
not. MapSmith now detects the predictor and converts the input first,
|
|
317
|
+
recording it in the manifest.
|
|
318
|
+
([upstream report](https://github.com/jblindsay/whitebox_next_gen/issues/32))
|
|
319
|
+
- **GeoParquet outputs from the vector engines** were written through a GDAL
|
|
320
|
+
path that produced unreadable files.
|
|
321
|
+
- `run_sql` materialisations and the DuckDB/SedonaDB join fast paths now run
|
|
322
|
+
the same deterministic verification as every other writer, and record their
|
|
323
|
+
CRS decisions.
|
|
324
|
+
|
|
325
|
+
### Changed
|
|
326
|
+
|
|
327
|
+
- `spatial_join` with `engine="auto"` falls back to GeoPandas when the inputs'
|
|
328
|
+
CRS differ or are unknown, instead of joining mismatched coordinates.
|
|
329
|
+
- The planning-failure figure quoted in the docs is stated as the upper bound
|
|
330
|
+
it is ("up to ~47%"), since the underlying study counts errors multi-label.
|
|
331
|
+
|
|
332
|
+
## [0.1.0] — 2026-08-18
|
|
333
|
+
|
|
334
|
+
First public release: the engine dispatcher (SedonaDB / DuckDB / GeoPandas),
|
|
335
|
+
`run_sql`, the job ledger, stateless Streamable HTTP transport, and provenance
|
|
336
|
+
manifests with deterministic verification on every writer.
|
|
@@ -19,6 +19,10 @@ MapSmith gives AI agents professional-grade geoprocessing via MCP, with **verifi
|
|
|
19
19
|
- Version pins are deliberate: `mcp>=1.26,<2` (1.26 is the floor for resource `meta`, needed by the MCP Apps map panel; v2 renamed FastMCP→MCPServer; migration planned with MCP Tasks), `ruff>=0.16,<0.17` (new ruff minors add default rules and break CI).
|
|
20
20
|
- Tests use closed-form expected values (e.g., a known 5×5 raster block → mean=22, sum=550) plus rejection-path tests; `pytest.importorskip` for extras.
|
|
21
21
|
- Run `python -m ruff check .` before committing; CI runs lint + tests on Python 3.10/3.12 + Docker build.
|
|
22
|
+
- **The floor is Python 3.10**, and local runs happen on a newer interpreter, so stdlib added
|
|
23
|
+
after 3.10 breaks only in CI: no `tomllib`, `datetime.UTC`, `StrEnum`, `contextlib.chdir`,
|
|
24
|
+
`ExceptionGroup` (3.11), no `itertools.batched`, `typing.override` (3.12). Ruff's
|
|
25
|
+
`target-version` catches too-new *syntax*; too-new *modules* are on you.
|
|
22
26
|
- Docker (or `uvx` where wheels work) is the only supported install path — keep it true in docs.
|
|
23
27
|
- Verify external-library APIs against primary documentation before coding against them; this repo has already been bitten by from-memory APIs three times.
|
|
24
28
|
|
|
@@ -30,7 +30,7 @@ ruff check .
|
|
|
30
30
|
|
|
31
31
|
## Contributor License Agreement
|
|
32
32
|
|
|
33
|
-
To keep dual-licensing possible (AGPL + commercial), we ask contributors to sign a lightweight CLA on their first PR
|
|
33
|
+
To keep dual-licensing possible (AGPL + commercial), we ask contributors to sign a lightweight CLA on their first PR. There is no bot yet: if you open the first outside PR, we will agree the wording with you in the thread rather than block the merge on tooling that does not exist. Your code always remains available under AGPL-3.0.
|
|
34
34
|
|
|
35
35
|
## Code style
|
|
36
36
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
FROM python:3.12-slim
|
|
2
|
+
|
|
3
|
+
# MCP Registry ownership proof: must equal the server.json "name" exactly.
|
|
4
|
+
LABEL io.modelcontextprotocol.server.name="io.github.mapsmith-ai/mapsmith"
|
|
5
|
+
|
|
6
|
+
# Geospatial wheels (pyogrio/shapely/pyproj) bundle their native libs; no system GDAL needed.
|
|
7
|
+
WORKDIR /app
|
|
8
|
+
COPY pyproject.toml README.md LICENSE ./
|
|
9
|
+
COPY src ./src
|
|
10
|
+
# raster + whitebox extras ship manylinux x86_64 wheels; the image is amd64-only.
|
|
11
|
+
RUN pip install --no-cache-dir ".[raster,whitebox]"
|
|
12
|
+
|
|
13
|
+
# Unprivileged by default (#19). uid 1000 on purpose rather than a high system
|
|
14
|
+
# uid: the supported way to run this is a bind mount of your own data directory,
|
|
15
|
+
# which on Linux is usually owned by the first human user — 1000. If yours is
|
|
16
|
+
# not, pass `--user $(id -u):$(id -g)`.
|
|
17
|
+
RUN useradd --create-home --uid 1000 --shell /usr/sbin/nologin mapsmith \
|
|
18
|
+
&& mkdir -p /data \
|
|
19
|
+
&& chown mapsmith:mapsmith /data
|
|
20
|
+
|
|
21
|
+
# Workspace for datasets: mount your data here. Confined BY DEFAULT — the
|
|
22
|
+
# supported path used to start unconfined unless the operator remembered `-e`,
|
|
23
|
+
# which is the wrong way round for a default.
|
|
24
|
+
VOLUME ["/data"]
|
|
25
|
+
WORKDIR /data
|
|
26
|
+
ENV MAPSMITH_WORKSPACE=/data \
|
|
27
|
+
HOME=/home/mapsmith \
|
|
28
|
+
MPLCONFIGDIR=/home/mapsmith/.config/matplotlib
|
|
29
|
+
|
|
30
|
+
USER mapsmith
|
|
31
|
+
|
|
32
|
+
ENTRYPOINT ["mapsmith"]
|
|
@@ -12,7 +12,7 @@ name under. These are the commitments that follow from taking that seriously.
|
|
|
12
12
|
## 1. Engines compute. Models orchestrate.
|
|
13
13
|
|
|
14
14
|
Every geometry and every number in a MapSmith result comes from executing a
|
|
15
|
-
deterministic engine — GDAL, GeoPandas, DuckDB Spatial,
|
|
15
|
+
deterministic engine — GDAL, GeoPandas, DuckDB Spatial, Whitebox Workflows,
|
|
16
16
|
SedonaDB. None
|
|
17
17
|
of it is generated by a model, ever. The model's job is to decide *what* to
|
|
18
18
|
run; it is never asked to produce a coordinate. This is the line that
|
|
@@ -38,8 +38,9 @@ that passed.
|
|
|
38
38
|
|
|
39
39
|
## 4. Coordinate systems are stated, never assumed.
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
A silent CRS mismatch is the classic way a geospatial analysis comes out
|
|
42
|
+
confidently wrong: the arithmetic succeeds, the map looks plausible, and the
|
|
43
|
+
numbers mean nothing. MapSmith refuses inputs without a CRS, never runs metric
|
|
43
44
|
operations on degrees without a declared reprojection, and writes the reason
|
|
44
45
|
for every transformation into the manifest.
|
|
45
46
|
|
|
@@ -71,14 +72,15 @@ engine actually ran goes in the manifest.
|
|
|
71
72
|
## 7. Your data stays on your machine.
|
|
72
73
|
|
|
73
74
|
No telemetry, no phoning home, no dataset uploads: MapSmith runs beside your
|
|
74
|
-
data — on a laptop, in a container, on your own infrastructure.
|
|
75
|
-
requests
|
|
76
|
-
in-chat map panel fetches OpenStreetMap background tiles (which
|
|
77
|
-
map view you are looking at, and falls back to a plain background
|
|
78
|
-
host blocks it), and the SQL engine downloads its spatial extension
|
|
79
|
-
environment. Neither carries your datasets, and both are avoidable —
|
|
80
|
-
panel, pre-install the extension.
|
|
81
|
-
|
|
75
|
+
data — on a laptop, in a container, on your own infrastructure. MapSmith itself
|
|
76
|
+
makes two outbound requests, and we would rather name them than have you find
|
|
77
|
+
them: the in-chat map panel fetches OpenStreetMap background tiles (which
|
|
78
|
+
reveals the map view you are looking at, and falls back to a plain background
|
|
79
|
+
when the host blocks it), and the SQL engine downloads its spatial extension
|
|
80
|
+
once per environment. Neither carries your datasets, and both are avoidable —
|
|
81
|
+
skip the panel, pre-install the extension. What an *agent* can reach through
|
|
82
|
+
the SQL engine is a separate question, answered precisely in `SECURITY.md`.
|
|
83
|
+
The provenance manifests MapSmith produces belong to you, and they happen to be exactly
|
|
82
84
|
the kind of record an audit asks for: what ran, on what inputs, with what
|
|
83
85
|
parameters, verified how. The EU AI Act's traceability obligations for
|
|
84
86
|
high-risk systems are not a burden if your tooling produces that trail as a
|