fix(server): make index.malloy the one list of what dashboards, notebooks and the model GET can read - #1236
Merged
Conversation
…ckage surface
A root index.malloy used to hide every dashboard, and the only way to get
one back was an explores listing index.malloy and each dashboard file.
Now every tagged dashboard is listed and served whatever the surface is.
What a dashboard can read is the surface itself: the names index.malloy
exports (or, on a legacy package, what the files explores lists export).
- A dashboard file is a query entry point but admits nothing of its own:
its export {} and its own named queries no longer make a hidden source
queryable, even when explores lists the file.
- A dashboard's own named query (single-query dashboards, suggest
query=) is checked by the source it reads, after compiling.
- A source the dashboard declares on top of a surface source
(source: big is orders extend {...}) is admitted; one on top of a
hidden source is not.
- The load lint reports each tile, single query, or filter suggestion
that will 404, in one sentence plus the fix, and re-runs after a
metadata PATCH.
- Dashboard files no longer appear as models or count toward the
surface in listings and surface warnings.
The held-back and withheld-drill warnings are gone with the gate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: James Swirhun <james@credibledata.com>
…y what it publishes
GET .../models/{path} returned any model file, off the surface or not,
with its full compiled IR and its text. Hiding a file from the listing
did not stop anyone opening it by URL, and even the surface file's own
response carried every source it imports in modelDef.
- A file off the surface answers 404, with the query route's words.
Inert with no surface and under queryableSources "all". Dashboard
files pass. The controller no longer wraps that 404 into a 500.
- modelDef.contents and exports, modelInfo, sources and sourceInfos are
limited to the names the file publishes. For a dashboard that is the
names that trace to the surface. imports and every other key stay.
- sourceText is withheld when the file declares something it does not
publish. A dashboard's text is always returned: the editor needs it to
save.
- The dashboard editor builds its field catalog from the published
models, limited to what the dashboard's imports can see, instead of
fetching the files it imports (now 404 when they are off the surface).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: James Swirhun <james@credibledata.com>
…y what they may read
A notebook's cells ran with no surface check, so GET .../cells/{i}
returned rows from a source index.malloy hides, and the notebook GET
listed every imported source's schema.
- Each code cell runs the query route's two checks before anything
else, so a hidden source answers 404, and 404 rather than 403 when it
is also gated. A source an earlier cell derives from a published one
(source: mine is customers extend {...}) still counts as published.
- The notebook GET and the cell response show only the sources, queries
and newSources the notebook may read.
- A cell's own source over a raw table has no published base, so on a
curated package it is refused. Inert with no surface and under "all".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: James Swirhun <james@credibledata.com>
… two sentences The warnings still sent people to explores for three jobs: opting out, serving dashboards, and listing several files. Dashboards no longer need it, and index.malloy answers the other two. The warnings also ran 60 to 150 words and read as documentation. Each warning is now what is wrong in this package, then "Fix:" and the one edit, with the package's real file names: - A root index.malloy with no keys, the recommended shape, gets no warning (INDEX_MODEL_IS_THE_SURFACE is gone). - Every non-empty explores is deprecated. The fix names the files index.malloy must import, leaving out index.malloy and dashboard entries, which need no replacement. - explores: [] is deprecated too: beside an index.malloy, the opt-out is now renaming the file; without one, the key does nothing. - queryableSources "declared" does nothing; "all" is kept, with no warning, as the one way to hide a gated source from listings while it stays queryable. - A root file that differs from index.malloy only in case (Index.malloy) is said to be ignored. The exact-match rule is unchanged. - The malformed-explores refusal, the surface-widened notice and the get_context empty-package hint no longer point at explores. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: James Swirhun <james@credibledata.com>
…ebooks read The skills and the scaffolder still told agents to declare explores to serve dashboards, and said a several-file explores and queryableSources "all" were supported without a warning. - malloy-publish: explores is deprecated in every form; several files are one index.malloy importing them. "all" is kept only for hiding an #(authorize)-gated source from listings. - malloy-dashboards: Step 0 is gone. The lint section explains the tile-won't-load warning and its fix, and that dropping # artifact is how to hide a dashboard. - malloy-notebooks, malloy-source-unreachable: cells, tiles and the model GET answer 404 for a hidden source. - create-malloy-package: the generated briefing no longer mentions explores, and the index.malloy template's "all" note stands alone. - init_truth_package.py stops writing explores. It does not add an index.malloy: goldens are queried at truth.malloy, which would then be off the surface. - skills_bundle.json regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: James Swirhun <james@credibledata.com>
- dashboards.md, discovery-and-access.md, packages.md, api-doc.yaml: dashboards are always listed and read only the surface; the model GET answers 404 off the surface and returns only published names; notebook cells are held to the surface; the new warnings. - queryableSources is no longer marked deprecated in the API spec: only "declared" is, and "all" stays supported. - materialization.md: builds ignore the surface, so a hidden persist intermediate is still built. - RELEASE_NOTES: an Unreleased (BREAKING) section, which reverses the 0.7.0 advice to use explores for dashboards and explores: [] to opt out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: James Swirhun <james@credibledata.com>
The previous commit dropped `deprecated: true` from the field. The key stays deprecated: "declared" warns because it does nothing, and "all" loads with no warning only because nothing replaces it yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: James Swirhun <james@credibledata.com>
The running server showed the surface file's response still named a hidden source it imports, in three places the earlier commit missed: sourceRegistry (keyed name@file), queryList (top-level run: statements, with their source inline), and references (editor go-to-definition data). The first two are now limited to published names; references is dropped under curation, since nothing that reads this response uses it. The test that should have caught this searched for a quoted name, which never matches inside the JSON-encoded modelDef. It now searches the text, and fails without this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: James Swirhun <james@credibledata.com>
… rename breaks From review of this PR. - sourceText was withheld only when the file declared an unpublished source. A surface file that imports a hidden source and runs it (run: hidden -> ...) declares nothing and still returned its text. It is now withheld when the text names any source the file does not publish, read outside comments and string literals. - A published source built on a hidden one carried extends: "hidden@file" in modelDef, and a join carried the joined source's sourceID and referenceID even when renamed. Those identities are now removed for unpublished sources. The join's own name stays: it is part of the published field paths. - The explores: [] and "index.malloy is ignored" fixes said to rename index.malloy without saying that every import of it then fails, which takes the whole package down. Both now name the imports. - The release note says plainly that a dashboard file is a query path for any caller, and that a query there over a published source can join a hidden source the dashboard imports, as index.malloy already allows. Only tiles are checked at load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: James Swirhun <james@credibledata.com>
Sha-Bang
approved these changes
Sep 25, 2026
Sha-Bang
left a comment
Collaborator
There was a problem hiding this comment.
Approving. Three should-fix items inline, none blocking.
packages/server/src/service/model.ts:6800:showsFileTextmisses hidden names that are backticked with a hyphen or are non-ASCIIpackages/server/src/service/model.ts:6866: the notebook GET still returns each cell'squeryInfoschema unfilteredpackages/server/src/service/package.ts:548: an untaggeddashboards/*.malloylisted inexploresis still an entry point for its own exports
…o load The editor now fetches every published model for its field list, not just the files a dashboard imports, so one failed fetch (a reload racing it, say) emptied every suggestion with no error shown. Build the catalog from the models that loaded. Signed-off-by: James Swirhun <james@credibledata.com>
dashboard.ts pointed at the deleted Package.isQueryableEntryPoint, and the dashboards-convention fixture comment contradicted the tests under it. Signed-off-by: James Swirhun <james@credibledata.com>
…tebook and dashboard reads - Model GET: a join to a hidden source carried that source's whole compiled definition (table path or SQL, connection, every column). It is now cut to the join's name and its fields' names and types, which is what a query through the join needs. Querying through the join is unchanged; the full model is still what queries compile against. - Model GET sourceText: the check read only ASCII words, so a hidden `orders-staging` or `café` never matched and the text went out. It now uses the same identifier pattern as buildDerivationBaseMap (moved into a shared malloyIdentifiers in query_text.ts). - Notebook GET: queryInfo is left out for a cell the query route would refuse, since its schema lists the hidden source's columns. - Dashboard lint: a warning named the source even in a model gated with #(authorize), where the query's own 404 keeps it back. It now names it only for an explained (OffSurfaceError) refusal. A non-compile error in surfaceRefusal is rethrown so lintDashboards reports the lint as incomplete, rather than counting the tile as fine. The per-tile checks run with Promise.all. - A dashboards/ file with no # artifact tag, listed in explores, was still an entry point for its own exports: hidden but queryable. It is no longer an entry point. Tests: each new assertion fails on the previous head and passes here. Signed-off-by: James Swirhun <james@credibledata.com>
An exported named query is listed and runs, even through a join to a hidden source, without carrying that source's SQL. One the surface file declares but does not export, and one in a hidden file, are left out of the response and refused by name. Signed-off-by: James Swirhun <james@credibledata.com>
…, as main does Only a tagged file is a dashboard, so the exclusions from the model list, the surface, and the load warnings now ask discovery rather than matching the dashboards/ path. An untagged file explores lists is published like any other listed file again: listed, queryable, and its exports count. This replaces the previous commit's choice of refusing it, which changed what the deprecated explores key means. Signed-off-by: James Swirhun <james@credibledata.com>
The release notes, the malloy-publish skill, discovery-and-access, packages.md and the spec now say the same thing: the files explores lists are listed and queryable, and their exports are the surface, wherever they live. The one change is a tagged dashboard it lists, which reads the surface and adds nothing to it. Skills bundle regenerated. Signed-off-by: James Swirhun <james@credibledata.com>
A reader with no history gets nothing from "as before"; say what the key does. Signed-off-by: James Swirhun <james@credibledata.com>
jswir
enabled auto-merge (squash)
September 25, 2026 04:20
jswir
disabled auto-merge
September 25, 2026 04:23
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A package's root
index.malloyis now the one list of what anyone can read, through every route. An agent queryingindex.malloy, a dashboard tile, a notebook cell and the model GET all see the same sources. Builds are unaffected.What stops working (read first)
These remove access that works on 0.8.0. All of them are inert with no surface and under
queryableSources: "all".GET .../models/{path}answers 404 for a file off the surface, with the query route's message. Files it does return carry only the names they publish:modelDef(contents,exports,sourceRegistry,queryList;referencesis dropped, and a published source no longer carries theextends/sourceID/referenceIDof a hidden one),modelInfo,sourcesandsourceInfos. A join to a hidden source keeps its name and its fields' names and types, which is what querying through it needs, but not the hidden source's table, SQL or connection.sourceTextis left out when the text names a source the file does not publish, backticked and non-ASCII names included. A dashboard's text is always returned, because the editor saves with it.queryInfofor a cell that would be refused. A source an earlier cell derives from a published one still works. A cell's own source over a raw table has nothing published under it, so on a curated package it answers 404.export {}and its own named queries no longer make a hidden source queryable, even whenexploreslists the file. A tile that relied on that answers 404 and is named in the load warnings.exploresleft out on purpose are now listed. To hide one, remove its# artifacttag, and take it out ofexploresif that lists it: an untagged fileexploreslists is published like any other file, as on main.run: secretthere answers 404, but a query over a published source can still join a hidden source the dashboard imports:run: orders extend { join_one: s is secret on id = s.id } -> { group_by: s.x }returns rows. That is howindex.malloyalready behaves, since the check is on the source a query runs. Only tiles are checked at load. Gate such sources with#(authorize).What changes
suggestread only the surface. A source the dashboard declares on top of a published one can be read (source: big is orders extend {...}); one over a hidden source cannot. The load warns once per tile, query or filter that will fail:#(authorize), the warning says "a source" rather than naming it, as the query's own 404 does. An error other than a failed compile makes the lint report itself incomplete instead of passing the tile.Fix:and the one edit. A rootindex.malloywith no keys gets none. Every non-emptyexploresis deprecated, and the fix names the filesindex.malloymust import.explores: []is deprecated: to publish everything, renameindex.malloy.Index.malloyin any other case is reported as ignored. The held-back and withheld-drill warnings are gone.queryableSourcesstays as feat(server): a package's index.malloy is its published surface #1206 left it: marked deprecated in the spec,"declared"warns, and"all"loads with no warning. Nothing replaces"all"for hiding an#(authorize)-gated source from listings while authorized callers still query it.api-doc.yaml, the scaffolder briefing andRELEASE_NOTES.md(an[Unreleased] (BREAKING)section) teach the one rule.Unchanged on purpose
#@ persistintermediate is still built and an exported source reads its table./compileandcompile_modelstay exempt.#(authorize)is the lock.How it was checked
19 small test packages, one per surface shape (no surface, root
index.malloy,exploresin each form, dashboards, a notebook, a case-variant file name, an index that exports nothing, a dashboard with tiles over hidden and derived sources, and a hidden persist intermediate) on a local build of this branch: 54 of 54 checks pass. Covered: listings, query status per route, model GET contents, exact warning text, notebook cells, and a materialization build.MCP:
get_contexton anexplores-listed dashboard package offers onlyindex.malloy::orders, andexecute_queryrefuses a hidden source through a dashboard path.SDK: 1059 pass. Server unit: 4266 pass. The 35 failures are in
config.specandconfig.theme.spec, and they fail on unmodified main too (bun 1.4). Dashboards and index-convention integration: 67 pass. The full integration run passed exceptconcurrent_package, which times out downloading from GCS without credentials here.A real-server run found three
modelDefkeys still naming a hidden source that the unit test missed, because it searched for a quoted name inside JSON-encoded text. Review then found two more:sourceTextof a file that runs a hidden source, andextends/ join identities inside a published source. All are fixed, and the test fails without the fix.The dashboard join path was confirmed against the real compiler:
run: secreton a dashboard path is refused, the join form returns rows, identically onindex.malloy.Review follow-ups (the last five commits) were checked on a local build: all 19 test packages again, plus a demo package with dashboards, a notebook, named queries and a gated source, clicked through in the Console. Named queries are covered by a new test: an exported one is listed and runs, even through a join to a hidden source; an unexported one and one in a hidden file are neither listed nor runnable. Each new assertion fails on the commit before it. Server unit: 4268 pass (the same 35 config failures). Integration: 359 of 360 (
concurrent_package, as above). SDK, skills, scaffolder, hammer, scripts, examples and the Python skill tests all pass.Known issues, not fixed here
#(authorize)-gated source answers 403 naming the source, not the surface's 404. The dashboard's named query defers the surface check to after compile, and the early authorize check refuses first (model.ts, theassertAuthorized(earlySource, …)fast path). A directrun: <source> -> …on the same path correctly answers 404. Reachable only through this PR's dashboard path; to be fixed in a follow-up.Filter expression parse error: [object Object].The Malloy compiler builds the message from the parse log entry rather than itsmessage. Publisher forwards given values unchecked, and the SDK draws a free-text box forfilter<boolean>. Present on main; separate fixes upstream and in Publisher.Not done
"all", and refusing a surface that exports nothing (such a package still publishes, with a warning).🤖 Generated with Claude Code