The milvus type-id: read-only browsing, queries, exact counts and vector searches over Milvus's gRPC API, with an admin's Load and Release, tested against Milvus 3.0.2.
Source: src/lib/db/providers/vector/milvus/.
Tests: tests/unit/db/milvus/ and tests/integration/db/milvus-provider.test.ts.
Tracking issues: #424 and #1089.
| Write reach | guarded: while read-only mode holds, Load and Release are refused by refuseReadOnly in write-policy.ts before any request; every console route in MILVUS_ROUTES is a read, and there is no object edit or transaction route |
Studio reads a Milvus server through a gRPC client of its own, shows its databases and collections in the object tree, and runs one Milvus request per run in the editor, written as Milvus's own REST v2 request: one POST /v2/vectordb/<route> line and one JSON body.
It reads only: no request Studio sends writes an entity, changes a schema or loads a collection, and an admin's Load and Release in Admin > Operations are the only state changes, each previewed, confirmed and audited.
It never makes the server call another service: server-side functions, model rankers and search aggregation are refused in every release.
It shows MCP clients and the agent the schema alone, never a value or a vector.
| Milvus | Studio |
|---|---|
| A database | A container, labelled Database |
| A collection | A row of Collections, opened in the Source tab |
| A field, the dynamic field | A column; the dynamic field is one column $meta of type JSON (dynamic) |
| A partition, an alias, an index | Details of a collection, in its Source and in the console routes |
| An entity | A result row |
| A vector field | A vector column, drawn by the vector cell renderer |
POST /v2/vectordb/entities/search and the other read routes |
One request per run in the editor |
| LoadCollection, ReleaseCollection | The Load and Release controls of Admin > Operations |
MilvusProvider extends BaseDatabaseProvider as the first member of the vector family, and every module talks to Milvus only through the MilvusClient interface of client.ts, each through the slice of methods it calls.
grpc-client.ts is the one file that imports @grpc/proto-loader and the generated descriptor, and reaches @grpc/grpc-js only through the shared transport in src/lib/db/grpc/; its stub holds exactly the allowlist of section 2.4.
routes.ts, guard.ts, the phase 0 half of request.ts, generators.ts, float32-text.ts, type-spelling.ts and labels.ts are pure and shipped to the browser, because the editor, the confirmation gate and the generators read them; everything else runs on the server.
| File | What it owns |
|---|---|
client.ts |
The seam: MilvusClient, its wire-mirroring types and MilvusError |
grpc-client.ts |
The filtered stub over the shared gRPC channel of src/lib/db/grpc/, the authorization metadata, db_name on every request, the deadlines and call.cancel() |
proto/ |
The vendored milvus-proto files of go-api/v3.0.2 and the descriptor generated from them by scripts/generate-milvus-descriptor.mjs |
connection-options.ts |
The connection to options, with the endpoint, credential, plaintext and TLS checks, the TLS identity and the seed stage |
routes.ts, request.ts, guard.ts |
The console dialect, the route table with its key schemas and caps, and the request rules in two phases |
expr.ts, placeholder-group.ts, codec.ts, field-data.ts |
Filters by template, query vectors on the wire, and every cell decoded from the answer |
schema.ts, results.ts, milvus-vocabulary.ts |
Columns, vector columns, the merge rule, the score column and the result budget |
execute.ts, objects.ts, source.ts |
One request within its bounds, the object surface and the two-part Source |
monitoring.ts, monitoring-reads.ts, maintenance.ts |
The monitoring panels, Load with its preview and Release |
write-policy.ts, versions.ts, errors.ts |
The read-only decisions, the version gates and the one error table |
metadata-request.ts, read-only-proof.ts, metadata-read.ts |
Agent mode: the reader of an agent request, the read-only proof of the agent's user, and the metadata-only answer (section 15) |
generators.ts, float32-text.ts, type-spelling.ts, labels.ts, index.ts |
The generated requests, the float text, the type spellings, the labels and the composition root |
The constructor validates nothing and opens nothing, so a provider built from any connection answers its capabilities and labels.
connect() checks the endpoint, the credentials, the plaintext rule, the TLS material and, for a seed connection, the credential record, all before any channel exists; it then opens one gRPC channel and calls GetVersion, which authenticates and records the version for the gates of section 4.8, and nothing else.
Test Connection runs connect() and then CheckHealth; a health failure after a good connect is reported as connected, with no health data.
disconnect() closes the channel and aborts a Load's progress poll.
The client is this repository's own, on @grpc/grpc-js 1.14.7 and @grpc/proto-loader 0.8.1 through the shared gRPC transport of src/lib/db/grpc/, which the etcd provider uses too, so the provider adds no package.
The descriptor is generated from milvus-proto go-api/v3.0.2 (commit 9e4f0ebc), vendored under proto/ with its licence and the SHA-256 of each file, and regenerated byte for byte by node scripts/generate-milvus-descriptor.mjs; it is never edited by hand.
The vendor SDK was set aside by measurement: its retry layer applied one insert twice, its telemetry sends the client's host name and error texts to the server, its global mode follows a server-named address with the token, and it adds 127 packages.
The client exposes exactly getVersion, checkHealth, getMetricsSystemInfo, listDatabases, describeDatabase, showCollections, describeCollection, batchDescribeCollection, describeIndex, getLoadState, getLoadingProgress, getCollectionStatistics, showPartitions, listAliases, describeAlias, query, search, hybridSearch, loadCollection, releaseCollection and close().
It has no Connect call and no telemetry method, so Studio registers nothing with the server and reports nothing about the machine it runs on.
The channel sets grpc.service_config_disable_resolution: 1, the receive cap of section 5.6, grpc.enable_http_proxy: 0, grpc.enable_retries: 0 and keepalive pings every 10 seconds with a 6-second timeout; no request is ever sent twice, and a Load or Release whose answer is lost reads "may have been applied".
The editor text is the form Milvus's REST reference documents, so a request can be pasted from it, and the provider lowers it to typed gRPC calls and never forwards the text anywhere. gRPC is used because only its wire carries the raw bytes of JSON and dynamic fields, so integers above 2^53 stay exact, and because a cancel stops the server's search. Where the result differs from what the REST endpoint would answer, section 5.4 says so.
Browsing, describing, statistics, queries, counts, searches, the agent's walk and inspect_schema never call LoadCollection, LoadPartitions, Prewarm or a refresh load.
An unloaded collection is fully browsable, and a query, get, count or search on it answers:
Collection unloaded_big is not loaded (state NotLoad). Query, get, count and search need a loaded collection, and loading uses query-node memory that every client of this cluster shares. An admin can load it from Operations; Studio never loads a collection on its own.
functionScore, functionChains and searchAggregation are refused by name on entities/search and entities/hybrid_search, and so is an endpoint, url, provider or credential-label key at any depth of searchParams and rerank.params, because the server would then send the candidates' text to that address.
Text query data is accepted only for a field a BM25 function produces; text aimed at a field an embedding function produces is refused, because the server would send the text to a third-party embedding provider on the cluster operator's key.
These refusals are permanent: promoting one is a new owner decision, not a measurement.
rerank takes rrf with params.k a number with 0 < k < 16384, or weighted with one weight in [0, 1] per sub-request, and nothing else; norm_score is refused until a measurement shows its effect.
With readOnly on the connection or in an agent execution profile, Load and Release are refused before any request, with a sentence naming where the mode was set.
Their preview is refused in the same sentence and reads nothing, so the dialog shows the refusal in place of a preview and offers no confirm button:
| Where the mode was set | Refusal |
|---|---|
| The operator's seed file | This connection is read-only (set in the operator's seed file). |
| A connection of the user's own | This connection is read-only: turn off Read-only in its settings to write. |
| An agent execution profile, on a connection that is not read-only | This run opens the connection read-only (agent execution profile). |
Studio does not offer what the mode would refuse: on a read-only connection the tree's row menu and the collection rows of Admin > Operations and of Monitoring > Tables draw neither Load nor Release, and Operations says "This connection is read-only: use a read-write connection for maintenance" beside the collections. A refusal that still happens is shown in the dialog that asked and written into the operation log entry.
Every console request of this version is a read, so the mode changes nothing else.
The mode binds a user only on a managed seed whose secret only the seed holds, and only when the server has authorization enabled: Milvus with authorizationEnabled: false, its default, accepts any credential or none, so a connection that works proves nothing about the server, and Studio cannot tell.
Double the mode on the server with a Milvus role that holds CollectionReadOnly and DatabaseReadOnly and nothing more.
On 3.0.2 DatabaseReadOnly is exactly ShowCollections and DescribeDatabase (DatabaseReadOnlyPrivileges in the server's pkg/util/constant.go), and CreateCollection belongs to DatabaseAdmin.
The 2.6.x and 3.0.x privilege-group tables on milvus.io list CreateCollection under DatabaseReadOnly; the server source and the 2.5.x table do not, and the server is what decides.
Measured on 3.0.2: the server lists DatabaseReadOnly as those two privileges (list-privilege-groups-root), and refuses a user holding it alone CreateCollection with PERMISSION_DENIED, as it refuses it every other write (write-create-collection-dbro and the write-*-dbro captures under tests/fixtures/milvus/3.0.2/agent-boundary/).
MCP clients may be given a Milvus seed connection (mcp: true): inspect_schema and the agent's grounding carry names and types only, never a value, a vector, a default value, a function's parameters or a sampled key.
Agent execution reads metadata only, as a user the server proves read-only before every request (section 15), MCP's run_milvus_read sends the same reads under the same proof (section 15.6), and MCP run_read_query refuses Milvus, naming that tool; plan mode drafts a request that a person runs.
No Milvus statement reaches an AI route: Explain is not offered, and the confirmation gate posts nothing to the query-safety analysis for a Milvus request.
Every integer typed in a request reaches the wire with exactly its digits after a range check for the field it reaches, never through a JavaScript number, and an Int64 in a result is an exact decimal string.
9223372036854775807 and -9223372036854775808 round-trip; 9223372036854775808, abc, 1.5, 12, 007, -0 and +5 are refused before any request.
| Field | Meaning |
|---|---|
| Host | A name or address, or a pasted http:// or https:// address such as a Zilliz Cloud endpoint, which is split into Host and Port. Port 9091 is Milvus's management port, which Studio never dials. |
| Port | 19530 by default; one port carries gRPC with TLS on or off |
| Database | Optional; empty means default. A dbName in a request body overrides it. |
| User | Optional. At most 32 characters, starting with a letter. Leave it empty to put a token in Password or token. |
| Password or token | Milvus receives the password or token on every call, so a password needs an SSL mode other than disable, unless the host is this machine or an SSH tunnel carries the connection. |
| SSL / TLS | The mode, the CA, and the client certificate and key |
| SSH Tunnel | A bastion that forwards the one endpoint |
| Read-only | Refuse Load and Release on this connection (section 3.4) |
The Host, Database, User and Password or token rows are the hints the connection dialog shows under each field.
A host that still carries a scheme when it reaches connect() is refused with "Host takes a name or address only; put the port in Port and choose TLS under SSL / TLS."
With a User, Studio sends user:password in the authorization metadata of every call, base64-encoded as Milvus expects; with User empty, it sends the password as a token, which open-source Milvus reads as user:password and Zilliz Cloud as an API key.
A user name is at most 32 characters and starts with a letter; carriage returns, line feeds and NUL are refused in either field, and neither is ever echoed in an error, a log or an audit row.
The dialog warns before Test Connection when the credential is the documented default root pair, also when it is written as one token in Password or token with User empty:
Credential warning: This is the documented default root credential every Milvus server starts with, and it stays valid until an administrator changes it, so anyone who knows Milvus can sign in with it. Change the root password on the server, or connect as a user of your own.
Studio never probes or changes the credential, and a read-only seed that uses it, or no password at all, is refused when the seed file loads and again after its references resolve (SEED_CONNECTIONS.md).
The five SSL modes map one way onto grpc-js credentials and are never retried weaker: disable sends plaintext, require encrypts without verifying the certificate, verify-system verifies against the system's CAs, and verify-ca and verify-full against the pasted CA.
require sends the password to a peer whose certificate it does not verify.
A client certificate and key are both present or both absent; a certificate whose extended key usage lacks clientAuth, an expired one and a key that does not match it are refused before any channel, naming the reason.
An IP-literal host is verified against the IP in the certificate, and through an SSH tunnel the identity is the tunnel's far end, never its local forward.
Bun reports the server's client-certificate alerts from 1.4.4, in BoringSSL's words, and they are named as under Node.
A TLS failure Bun reports no cause for, which is every alert before 1.4.4, names the client certificate when one is configured.
Studio dials exactly the configured host and port: the gRPC target is built from the validated parts, no proxy variable is read, no address a server names is dialled, and node addresses in a GetMetrics answer are dropped.
A host named unix, dns, ipv4 or ipv6 is dialled as a DNS name, never as a resolver.
gRPC is outside the HTTP egress guard, as Kafka's and etcd's connections are.
Offered: the client dials only the one endpoint, so a tunnel forwards all of it, and the TLS identity is the tunnel's far end.
A password or token over no TLS is refused unless the host is a loopback address or localhost, or an SSH tunnel carries the connection, because Milvus receives it on every call:
This connection would send its password without TLS to a host that is not this machine, where anyone on the path can read it. Choose an SSL mode under SSL / TLS, connect through an SSH tunnel, or, if authentication is off on this server, clear the password.
The Host box takes a pasted http:// or https:// address, such as a Zilliz Cloud endpoint, and splits it into Host and Port: an explicit port is kept as typed, 443 included, an address with no port takes the scheme's default, 80 or 443, and https:// raises an SSL mode that is off to verify-system without lowering one that is on.
A user name or password, a path, a query string or a fragment in the address is refused by name.
The connection-string box is a different reader, and it reads http:// and https:// as ClickHouse, so paste a Milvus or Zilliz address into the Host box, never into the connection-string box.
Tested against Milvus 3.0.2; other 3.0.x releases are expected to work and are not tested; 2.6 and older connect and are not tested. A 2.6 server ignores a parameter it does not know and answers success, so Studio refuses what a server cannot do instead of inferring it from the answer:
| Feature | 3.0 | Before 3.0 | Studio |
|---|---|---|---|
orderByFields |
sorts, with an explicit limit | ignored | refused before 3.0, naming 3.0 |
groupByFields |
exists | absent | refused on every version |
functionScore, functionChains, searchAggregation |
exist | functionScore exists |
refused on every version |
ids on search |
every key and vector type | an Int64 key and a dense field | no gate; a server without it answers an error |
range_filter without radius |
refused by the server | accepted | refused on every version |
query_mode=large_topk |
honoured | from 2.6.14 | refused on every version, as every key outside section 5.6's table is |
A server whose version Studio cannot read, or whose major is neither 2 nor 3, takes the pre-3.0 answer of every gate. Milvus Lite's server mode and clusters are not claimed.
Zilliz Cloud speaks the Milvus API and has not been tested with Studio.
To try it, paste the cluster endpoint into the Host box, or enter the host and port (19530 for Dedicated, 443 for Free and Serverless), choose verify-system, and put the API key in Password or token with User empty, or db_admin and its password.
Its gaps (no GetMetrics or GetReplicas, databases only on Dedicated) are found by the server's answer, never by the host name.
Prefer a cluster-scoped user or key to a project-wide key.
The claim waits for a test cluster to pass the provider's evidence gate (docs/BACKLOG.md D157).
Lists are filtered by Milvus's grants: a database or collection list reads "visible to this user", never "none". In the console that is the list column's type, and a list with no row says it in a warning, since the grid shows no column for an empty result. A search that names no metric and no search parameter runs without the IndexDetail privilege, and its score column says the metric is not readable; a request that names either needs IndexDetail, because its checks read the index.
milvus has no connection string scheme, no readTls, no paste and no seed load check: every field of its row in CONNECTION_STRING is null (connection-string.ts).
The rolled-back notice names COMMIT as the keyword that keeps an open transaction, and milvus has no consent deadline, so the agent's consent card shows no deadline chip or sentence for it.
Its row in EXECUTION_UI is declared in execution-ui.ts.
What a tree click and Generate Query write is MILVUS_GENERATORS, the milvus row of DIALECT_GENERATORS, declared in generators.ts and read by the engine-agnostic dispatcher in src/lib/query-generators.ts.
Its editor language, milvus, is the row in EDITOR_LANGUAGES declared in editor-language.ts: how the editor registers and tints it, and its completion source and formatter where it has one.
One request per run: optional comment lines starting with #, then one line POST /v2/vectordb/<route> (POST <route> also works), then one JSON object.
The language the editor and the agent read is generated from the route table, so it cannot drift:
A Milvus request, one per run: optional comment lines starting with #, then a line POST /v2/vectordb/<route> (POST <route> also works) naming one of databases/list, databases/describe, collections/list, collections/describe, collections/get_stats, collections/get_load_state, partitions/list, indexes/list, indexes/describe, aliases/list, aliases/describe, entities/query, entities/get, entities/search and entities/hybrid_search, then one JSON object with that route's Milvus REST v2 body keys, for example POST /v2/vectordb/entities/query {"collectionName": "docs", "filter": "year > 2020", "outputFields": ["title"], "limit": 10} or POST /v2/vectordb/entities/search {"collectionName": "docs", "annsField": "embedding", "data": [[0.1, 0.2]], "limit": 5}. Count rows with "outputFields": ["count(*)"] and no limit or offset. Filters are Milvus boolean expressions in the filter string. Query, get, count and search need a loaded collection. Not requests: writes, load, release, flush, compaction, users and roles, resource groups, snapshots and server-side functions.
| Route | Accepted keys |
|---|---|
databases/list |
none |
databases/describe |
dbName |
collections/list |
dbName |
collections/describe |
dbName, collectionName |
collections/get_stats |
dbName, collectionName |
collections/get_load_state |
dbName, collectionName, partitionNames |
partitions/list |
dbName, collectionName |
indexes/list |
dbName, collectionName |
indexes/describe |
dbName, collectionName, indexName |
aliases/list |
dbName, collectionName (optional) |
aliases/describe |
dbName, aliasName |
entities/query |
dbName, collectionName, filter, outputFields, limit, offset, partitionNames, exprParams, consistencyLevel, orderByFields (3.0 and later) |
entities/get |
dbName, collectionName, id, outputFields, partitionNames, consistencyLevel |
entities/search |
dbName, collectionName, data or ids, annsField, filter, limit, offset, outputFields, searchParams, partitionNames, exprParams, consistencyLevel, groupingField, groupSize, strictGroupSize |
entities/hybrid_search |
dbName, collectionName, search, rerank, limit, offset, outputFields, partitionNames, consistencyLevel, groupingField, groupSize, strictGroupSize |
A hybrid sub-request in search takes annsField, data, filter, exprParams, limit, params and metricType, and every sub-request's data has the request's length.
An unknown key is refused by name, at every level, although the server would drop it silently.
collections/load, collections/release and collections/refresh_load are refused with a sentence pointing at Admin > Operations; every other documented route (writes, schema changes, flush, compaction, users and roles, resource groups, snapshots and the rest) is refused with the list above.
A body takes no // comment (docs/BACKLOG.md D158).
- An absent
dbNamemeans the connection's database, elsedefault; adbNamein the body overrides the connection's database, so the field is a default and not a boundary. - A collection, alias or database name matches
^[A-Za-z_][A-Za-z0-9_]{0,254}$exactly; a field name is resolved against a fresh describe of the collection,$metareads every dynamic key, and a dynamic key is projected bare. - An absent
limitis sent as 100;limit: 0is refused, because REST reads it as unlimited; a query always carries a limit. - An exact count is
"outputFields": ["count(*)"]with nolimitand nooffset, which are refused; Milvus itself answers a limit with "count entities with pagination is not allowed" and ignores an offset alone. - An absent
outputFieldsis the primary key, every scalar field and the dynamic field, never a vector; name a vector field or"*"to see vectors. - Every query vector is checked against the field's type and dimension from a fresh describe before the search is sent, because a wrong placeholder costs seconds of server retries.
idsanddataare exclusive on a search; at most 10 ids, no duplicates, andannsFieldis always explicit.- On a partition-key collection,
partitionNamesis refused naming the key field. groupSizeis an integer from 1 to 10 and needsgroupingField;strictGroupSizeis a JSON boolean.- Consistency is
Strong,BoundedorEventually;SessionandCustomizedare refused. Bounded may miss a write from the last moments (about 300 ms on an idle standalone, at mostcommon.gracefulTime, 5 s by default), and a search by id inherits the request's consistency. - Ids and
exprParamsintegers go through Milvus's expression templates, never into the filter text;exprParamstakes at most 32 keys, each a scalar or an array of at most 1,000 scalars. - The filter is Milvus's own expression, which the server parses and type-checks; every filter of one request together is at most 64 KiB.
- Every search parameter is checked per index type before the request is sent, and every key outside the table of section 5.6 is refused by name although the server ignores it.
Milvus 3.0.2 can return enable_dynamic_field: true from DescribeCollection without listing $meta in schema.fields (#1417).
Studio uses that flag to include $meta in the default projection and accept an explicit "outputFields": ["$meta"], whether or not the field is listed.
The object tree exposes one nullable $meta column of type JSON (dynamic) in either case.
On a collection without dynamic fields, requesting $meta is refused with a message saying that the collection has no dynamic field.
POST /v2/vectordb/collections/list
{"dbName": "default"}
POST /v2/vectordb/entities/query
{"collectionName": "docs_int64", "filter": "seq >= 10 and title like \"doc 001%\"",
"outputFields": ["id", "seq", "title", "tags", "big_int"], "limit": 5}
POST /v2/vectordb/entities/query
{"collectionName": "docs_int64", "filter": "maybe_count is null", "outputFields": ["count(*)"]}
POST entities/get
{"collectionName": "docs_varchar", "id": ["vc-0001", "vc-0002"], "outputFields": ["pk", "label"]}
# nearest neighbours of a unit vector, filtered by seq
POST /v2/vectordb/entities/search
{"collectionName": "docs_int64", "annsField": "vec",
"data": [[0.35355338, 0.35355338, 0.35355338, 0.35355338, 0.35355338, 0.35355338, 0.35355338, 0.35355338]],
"filter": "seq >= 100", "searchParams": {"params": {"ef": 64}}, "outputFields": ["seq", "title"], "limit": 5}
POST /v2/vectordb/entities/search
{"collectionName": "fts", "annsField": "text_sparse", "data": ["vector index"], "outputFields": ["id", "text"], "limit": 5}
POST /v2/vectordb/entities/hybrid_search
{"collectionName": "docs_varchar",
"search": [{"annsField": "f16", "data": [[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]], "limit": 10},
{"annsField": "sparse", "data": [{"17": 0.4, "230": 0.2}], "limit": 10}],
"rerank": {"strategy": "rrf", "params": {"k": 60}}, "outputFields": ["pk", "label"], "limit": 5}
POST /v2/vectordb/entities/search
{"collectionName": "docs_varchar", "annsField": "f16", "ids": ["vc-0000", "vc-0001"], "outputFields": ["pk", "label"], "limit": 3}
POST /v2/vectordb/entities/search
{"collectionName": "docs_varchar", "annsField": "f16", "data": [[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]],
"groupingField": "label", "groupSize": 1, "outputFields": ["pk", "label"], "limit": 4}
| Behaviour | REST v2 | Studio |
|---|---|---|
limit absent, except a lone count |
100 rows | 100 rows, sent explicitly |
limit or offset on a lone count(*) |
dropped or ignored | refused before the request |
limit: 0 |
unlimited for a query | refused |
outputFields absent |
every field, vectors included | scalar and dynamic fields only |
| An unknown key | dropped | refused by name |
| A 3.0-only key on an older server | dropped | refused naming the version |
dbName absent |
default |
the connection's database |
| Integers above 2^53 in the dynamic field or a JSON field | lost, or a nested JSON string | exact |
| Float16, bfloat16, binary and Int8 vectors in results | base64 | numbers and byte arrays |
| Array fields | the raw protobuf structure | arrays |
entities/get by a VarChar id holding a quote |
differs from Studio on 3.0.2 | one template value, matched exactly |
| A non-finite score | an empty body | "Infinity" with a warning |
functionScore, functionChains, searchAggregation |
accepted | refused in every release |
rerank.strategy other than rrf and weighted |
an error from the server | refused before the request |
| Text data on an embedding-function field | sent to the embedding provider | refused |
Consistency Session, Customized |
accepted | refused |
| The bounds of section 5.6 | the server's wider limits | refused past them |
collections/list order |
REST's order | gRPC's order |
| The response | the REST envelope | the result grid |
Rows are built in the collection's field order, never in the order the server sends its columns.
Int64 values are exact decimal strings, JSON and dynamic values are parsed with integers above 2^53 kept exact (with a warning naming the fields), and a VarChar or Text cell is cut at 65,536 UTF-16 code units with a visible marker.
Each float vector element is the shortest decimal that reads back as the same float32, so a copied vector searches exactly as stored; float16, bfloat16 and Int8 vectors are numbers, a binary vector is dimension / 8 bytes, and a sparse vector is Milvus's own map of index to value.
Every vector column is declared to the grid, which draws it as a vector cell with its dimension.
A search's score is a distance column whose type names the metric and its direction, such as Float, COSINE, higher is closer; a non-finite score reads "Infinity", "-Infinity" or "NaN" with one warning.
A search with more than one query vector or id starts with a $query column, and a grouped search carries the group value in $group.
A static field always wins its own name: a dynamic key of the same name is shown as $meta.<key> with a warning, and only $meta["<key>"] in a filter reaches the dynamic value.
Decimal, Date, Time and Mol columns are shown empty with a warning, and an unknown type reads "unsupported type N".
| Bound | Value |
|---|---|
| Console text | 1,048,576 bytes of UTF-8, checked first, in the browser and on the server |
| Default page | 100 rows, sent explicitly; a lone count(*) carries none |
| Rows per query or get | 1 to 1,000 |
| Query window | offset plus limit at most 16,384 |
| Search | nq at most 10, limit at most 1,024, and nq times (offset plus limit) times the group size at most 10,240 |
| Hybrid search | at most 10 sub-requests of the same nq, and nq times the group size times (the sub-requests' limits plus the outer offset plus limit) at most 10,240 |
| Search parameters | per index type, each key in its own range: nprobe 1 to 65,536; ef, reorder_k and search_list at least offset plus limit and at most 65,536; refine_k 1 to 64; drop_ratio_search at least 0 and below 1; a key the index type does not take refused |
| Filter text | 65,536 bytes summed over every filter of one request |
exprParams |
32 keys, each a scalar or an array of at most 1,000 scalars |
| Arrays | entities/get ids at most 1,000; search ids at most 10; outputFields at most 256; partitionNames at most 1,024 |
| String cells | 65,536 UTF-16 code units, cut with a marker |
| Result budget | 8,388,608 bytes, the rows past it dropped whole, with a warning |
| Receive cap | 16,777,216 bytes per answer; a query or get past it is asked again with half its limit |
| Deadlines | 10 s for a metadata route and 30 s for queries and searches, each at most the connection's query timeout |
| In flight | 4 calls per connection and 16 across every Milvus connection of this Studio process, with a queue of 64 |
At 1,536 dimensions a row with one vector is about 19 KB, so about 430 such rows fit the budget; vectors are left out of the default output, so a browse never meets it.
Cancel stops a running call and the server stops the search; a queued request leaves the queue and sends nothing. The server keeps parsing a large filter after a cancel, which is why the filter bound is summed over the request. The confirmation gate reads a Milvus request with the provider's own guard, so what asks and what runs are one parse: every request of this version reads, so none asks, and a request the guard refuses is refused in the editor, before anything is sent and with nothing written to history.
How this engine's statement text is read is declared once, in src/lib/db/providers/vector/milvus/sql-grammar.ts, the milvus row of the SQL grammar registry in src/lib/db/sql-grammar/.
readsSqlText is false: milvus takes one Milvus console request, a POST /v2/vectordb/<route> line and one JSON body, whose strings escape with a backslash and whose filter is a Milvus expression the server parses; the provider extends BaseDatabaseProvider and parses the text itself.
The confirmation gate therefore answers from this engine's row of NON_SQL_DESTRUCTIVE_VOCABULARY (src/lib/db/providers/vector/milvus/destructive.ts, collected in src/lib/db/destructive-commands.ts) instead of a SQL reading.
Its grammar is DEFAULT_SQL_GRAMMAR, the compatibility default, since no SQL grammar of its own applies to this text.
Redaction is none: a redacted statement is stored as ?, because a reader guessing where a literal ends in a language it does not read can store the literal.
One container level, Database, from ListDatabases (a handle opened under an execution profile lists the connection's own database alone, section 15.6), and one kind, Collection, from one ShowCollections per database, names only.
Opening a collection reads DescribeCollection: a vector type is spelled with its dimension, FloatVector(768), a BM25 function's output as SparseFloatVector(BM25: text_bm25), and the tree shows the type before the parenthesis with the full text in the tooltip.
Partitions, aliases and indexes are details in the collection's Source, never tree objects, and the row count is never on the listing.
A collection's Source has two parts: Schema, in the vocabulary of the REST reference's create-collection request, and State, in the vocabulary of its describe answer, with the load state in Milvus's own words, the GetCollectionStatistics estimate labelled "estimate: flushed segments only, deletes not subtracted, may lag recent inserts", partitions and aliases.
A function's name, type and input and output fields are shown, and none of its parameters, which can carry a credential label or a service address.
A click on a collection runs entities/query with its database and name, an empty filter and a limit of 100; on an unloaded collection it answers the not-loaded sentence and never loads.
Generate Command writes a runnable search over the collection's first dense vector field with a probe vector of the right dimension, a comment naming the field, and a comment for each other vector field; a collection whose only vectors are sparse gets both forms as comments, and a BM25 output field a text search.
Its outputFields includes the scalar fields and $meta when dynamic fields are enabled, so the generated search returns dynamic keys too.
Generate Code, Profile, Generate Test Data, Generate Count Query and the SQL INSERT and DDL export formats are not offered.
No kind declares acceptsRowWrites or acceptsSourceEdits: this version writes no data (section 3.4).
The absence is milvus's entry in EXPECTED_EDIT_ABSTAINERS (tests/helpers/object-edit-expectation.ts), and tests/isolated/object-edit-declarations.test.ts is what holds that entry and this section together.
Health is CheckHealth. The overview gives the version and the selected database's collection count, "visible to this user". The Tables tab and Admin > Operations list the first 200 collections of the selected database with GetCollectionStatistics' estimate:
The first 200 collections of the database. Row counts are server estimates of flushed segments and do not subtract deletes; POST entities/query with outputFields ["count(*)"] is exact on a loaded collection.
Index statistics carry each index's native type, AUTOINDEX as Milvus reports it, and cover only collections whose IndexDetail the user holds.
Slow queries and sessions are not offered, and the two panels say why:
Milvus keeps query statistics on its management port 9091, which Studio does not dial
Milvus reports client sessions on its management port 9091, which Studio does not dial
For performance, storage and query-node metrics, point a Prometheus server at Milvus's :9091/metrics and read it through the Prometheus provider.
Two controls of Admin > Operations, on each collection, for an admin, each audited with the Studio user, the Milvus user, the database and the collection:
| Operation | Label | Confirmation | Description |
|---|---|---|---|
| Load | "Load" | the preview, then a click | Loads the collection into query-node memory |
| Release | "Release" | the preview, then the collection's exact name | Every other client's search and query on this collection then fails with code 101 until it is loaded again. |
Load's preview shows the load state, the row estimate, each vector field with its index state, query-node memory used of total and the data loaded on query nodes, "as reported by the server, possibly several seconds old", with the load's cost estimated as about the raw data again in shared memory. A collection with a vector field that has no index is refused before LoadCollection. Load sends LoadCollection, then reads the progress every second for at most 10 seconds, and reports "Loaded" or "Loading N%, continues on the server"; one Load runs at a time on a connection, and a collection the server is still loading is refused until it reads Loaded. A load the server continues after the 10 seconds is not covered by that lock. Release asks for the collection's exact, case-sensitive name. Both also need the Milvus user's own Load or Release privilege, which the server enforces; a lost answer reads "may have been applied" and is never sent again.
queryLanguage: "json" with queryDialect: "milvus", one container level labelled Database, the kind Collection, defaultPort: 19530, a guarded writeReach, supportsMaintenance: true with load and release, and false or absent for explain, table creation, transactions, inline row edits, result pagination, external query limiting and connection strings.
The password field is labelled "Password or token".
milvus/sql-writing.ts declares how a generated statement spells a literal, a name and a bind for this engine, and it is this engine's row of SQL_WRITING (src/lib/db/sql-writing/index.ts), which the readers in src/lib/sql/values.ts and src/lib/sql/identifier.ts look up.
A literal is written by literalStandard and read back by unquoteStandard: the quote is doubled and a backslash is data.
A name is quoted by identifierDoubleQuoted, the SQL standard form: a double-quoted name with an inner double quote doubled.
There is no placeholder (placeholder: null), so a generator quotes the value with quoteLiteral instead of emitting a bind.
This engine declares queryLanguage: "json", so no statement is ever built for it to read; what a generator emits for such a connection is portable SQL meant to run elsewhere, and the standard form is the only thing it can claim.
Inline row editing reads a key column's kind through the shared INSTANT_TYPE_NAMES, FLOAT64_TYPE_NAMES and EXACT_DECIMAL_TYPE_NAMES of the module's inlineEdit, and binds every edit as typed.
The module's export field writes a result export's CREATE TABLE and INSERT from the shared building blocks alone: STANDARD_DDL_TYPES for the column types, NOTHING_STANDS_ALONE and NO_BARE_SPELLING, so every bare declared name is re-spelled from its family, NULL_NON_FINITE for NaN and the infinities, and binaryStandardHex for a binary cell (X'0102').
The engine declares queryLanguage: "json", so no SQL statement is ever built for it to read, and a file exported from it is meant to run somewhere else: every bare name in it is re-spelled portably rather than kept as one engine's private word.
| Answer | What Studio says |
|---|---|
| gRPC 16 | Milvus refused the user name or password (or token), then the server's text |
| gRPC 7 | the Milvus user lacks the privilege, then the server's text naming it |
| gRPC 14, "Connection dropped" | the connection to Milvus was lost; run it again |
| A deadline, in any of its three shapes | the deadline of the call |
| gRPC 12 | not supported by this server version |
| Code 101 | the not-loaded sentence of section 3.2 |
| Code 1100 | the input sentence, then Milvus's text with the filter's line and column |
Code 100, or success with CollectionNotExists |
the collection does not exist in the database |
| Code 800 | the database does not exist |
| Code 2000, 2001 or 2099 | Milvus rejected the request on the query node, with no raw server text |
| A TLS handshake failure | the cause, or the client certificate where Bun reports none |
| Any other code | Studio's sentence naming the code, then Milvus's text |
Studio's sentence comes first and the server's text after it; a server text that holds any form of the configured password or token is withheld whole.
Success is code 0 together with Success.
The unit tests drive each module through a recorded client, the integration test runs the real provider, codecs and error table over answers captured from Milvus 3.0.2 (tests/fixtures/milvus/), and the TLS tests run real handshakes under Bun and Node; nothing mocks a module.
bun tests/run-tests.ts tests/integration/db/milvus-provider.test.ts
bun run testdatabase-compose.yml holds milvus (127.0.0.1:19530, authorization on, the management port as 127.0.0.1:19091), milvus-tls (19531) and milvus-mtls (19532) behind the milvus-tls profile, and the milvus-seed one-shot; docker/milvus/README.md says how to start and seed them and what each collection is for.
tests/live/milvus-live-check.ts runs a real MilvusProvider against the fixtures on Node 24, Node 26 and Bun, and fails on any change outside its own collections, after the copy loop, the REST equivalence run, every refusal corpus, the telemetry check, Load and Release on a collection of its own and the search ceilings.
Copy the users' passwords out first, into a directory outside the repository: docker cp libredb-milvus-seed:/credentials "$dir/credentials", then bun tests/live/milvus-live-check.ts --credentials "$dir/credentials".
Keep the management port 9091 off the network: on 3.0.2 it answers without authentication, its metrics and its telemetry list included, and Studio never dials it.
Turn authorization on (common.security.authorizationEnabled: true) and change the default root password before anyone connects; with authorization off, any credential or none connects.
Give Studio's seeds a user of their own with CollectionReadOnly and DatabaseReadOnly, and the read-only mode on top.
The same user is what agent mode needs: it refuses root, the admin role, any other privilege and a server with authorization off (section 15).
Milvus's RBAC hides neither the topology nor other databases' row counts from an authenticated user, because GetMetrics needs no privilege.
- Vectors and payloads reach query history and saved queries, as every statement's text does.
- A filter's text is echoed in errors and in the Studio log.
- A load holds shared query-node memory until it is released, for every client of the cluster.
- Milvus RBAC hides neither the topology nor other databases' row counts from any authenticated user, since GetMetrics needs no privilege.
- A
dbNamein a body overrides the connection's database, so only Milvus RBAC on a user that is not root scopes a seed to one database. - A seed's in-flight slots, queue and running requests are shared by every user of the seed, so any user who knows a run's id can cancel it, and the bound counts Studio's calls, not the server's work, which a large filter's parsing outlives.
readOnlybinds only on a managed seed whose secret only it holds, and only when the server has authorization enabled; Milvus withauthorizationEnabled: false, its default, accepts any credential or none, so a connection that works proves nothing about the server and Studio cannot tell.- TLS mode
requiresends the password to an unverified peer. - gRPC, and every request the Milvus server itself makes, is outside the HTTP egress guard.
- REST
entities/getandentities/deleteby a VarChar id holding a quote differ from Studio's template on 3.0.2. - A vendor address pasted into the connection-string box instead of the Host box still selects ClickHouse.
- Zilliz users should prefer a cluster-scoped user or key to a project-wide key; Zilliz Cloud is not tested (section 4.9).
- A hostile or impersonated endpoint holds the password it receives and can return it in an encoding Studio does not list, so Studio withholds only the forms it sends.
- The management port 9091 answers without authentication on 3.0.2, so the operator keeps it off the network.
- A load the server continues after Load's 10-second poll is not covered by the one-load lock.
- Clusters, Milvus Lite and 2.6 and older are not claimed.
MILVUS_MIGRATION_DDL is what the schema-diff migration generator writes for milvus, read through migrationDdlFor in src/lib/db/migration-ddl/.
It declines the whole diff: refusal is the one line the migration prints after its header, in place of any DDL, and transactionWrapper is null.
milvus text is a Milvus console request, not SQL, so there is no table DDL to wrap (#284).
The refusal reason it prints, verbatim:
- "A collection's schema is declared through Milvus's own collection API, not SQL DDL, so there is no column definition to change."
- The design: the vector-family design under issue #424, sections 3 to 5.
- Milvus's REST v2 reference (
rest-summary-v3.0.x) and the milvus-protogo-api/v3.0.2definitions. - Milvus's built-in privilege groups:
DatabaseReadOnlyPrivilegesand its neighbours inpkg/util/constant.goat v3.0.2, which the privilege-group table misstates forCreateCollection(section 3.4). docker/milvus/README.mdandtests/fixtures/milvus/README.md.
Agent mode reads Milvus's metadata through queryReadOnly in index.ts.
The request reader is metadata-request.ts, the read-only proof is read-only-proof.ts, and the metadata-only projection and the wording of a failure are metadata-read.ts.
Every claim below cites a capture under tests/fixtures/milvus/3.0.2/agent-boundary/, recorded by its run.sh against digest-pinned 3.0.2 servers, one with authorization on and one with it off, with every call sent raw, past every Studio check.
The boundary is a structural closed table: the gRPC stub is MilvusService filtered to MILVUS_ALLOWLISTED_RPCS in grpc-client.ts, whose only writes are LoadCollection and ReleaseCollection, which the admin's maintenance route alone sends, and an agent request reaches the 11 metadata reads of the console's 15 routes and nothing else.
Beside them the stub holds three privilege reads, ListCredUsers, SelectUser and SelectGrant, which only the read-only proof calls, with requests the proof writes itself.
On top of the table the user is proved read-only, so the server's own authorization refuses what the table could ever let through.
A user holding CollectionReadOnly with ShowCollections and DescribeDatabase, one holding DatabaseReadOnly, one holding a custom group of reads, one holding ClusterReadOnly and one holding no role are each refused with PERMISSION_DENIED (gRPC 7) a collection create and drop, an alias create and drop, an insert, an upsert, a delete of no row, a flush, a compaction, an index, a partition, a database, a credential, a grant to its own role, an import, a Load and a Release (write-*-reader, write-*-dbro, write-*-custom, write-*-clusterro, write-*-nobody), with the row count, the collections, the aliases, the partitions, the databases, the load state and the indexes root reads unchanged after (state-before-*, state-after-refused-*).
The same writes land for a CollectionReadWrite user where its group allows them, for the admin role and for root (write-*-rw, write-*-admin, write-*-root, state-after-writable-*).
A server with authorization off answers every write, with any credential or none (write-*-open-reader, write-*-open-anonymous, state-open-*): it has no server-side boundary.
A handle opened for the agent read-only profile by an acquisition that sends statements refuses a connection that names no user (no credential, or a token with no user: part) before any client is built.
After GetVersion, and again before every request under that request's permit and deadline with no verdict kept, the server is asked who the user is and what it may do, reads only:
rootis refused by name before anything is sent: 3.0.2 lists it no role (select-user-self-root), yet it may do anything.ListCredUsersmust be refused withPERMISSION_DENIED: 3.0.2 refuses it to every user without a cluster privilege, readers and writers alike (list-cred-users-reader,-dbro,-custom,-nobody,-midrun,-rw,-elsewhere,-rwother), and answers it to root, the admin role,ClusterReadOnlyand every caller on a server with authorization off (list-cred-users-root,-admin,-clusterro,-open-reader,-open-anonymous). This step proves authorization is on; the grant step below is what refuses a writer. Any other refusal is no proof.SelectUserof the user itself, with its roles, must name that user alone; the admin role, or more than 16 roles, is refused. 3.0.2 answers it to every user (select-user-self-*).SelectGrantof each role and ofpublic, which every user holds, must name onlyCollectionReadOnly,DatabaseReadOnly, a privilege either group holds on 3.0.2 (list-privilege-groups-root) orListDatabases; 3.0.2 answers a user its own roles' grants (select-grant-own-role-*,select-grant-public-*) and refuses it another role's (select-grant-admin-reader). The grants are read across every database, whichever one the connection names: aSelectGrantnaming no database answers only the grants ondefaultand*(select-grant-own-role-default-only-*), so it would miss a role that readsdefaultand inserts into a second database, or one that holdsCollectionReadWriteon a database other thandefault, and both write there (write-insert-other-db-elsewhere,-rwother); named*, it answers every database's grants (select-grant-own-role-elsewhere,-rwother), and both are refused. A grant names a group by its name (select-grant-own-role-custom), and a read-only user cannot read a custom group's members (list-privilege-groups-reader), so a custom group is refused, as isCollectionReadWriteand every privilege that writes, loads or releases.ClusterReadOnlyand each of its members butListDatabases(SelectUser,SelectOwnership, the resource and privilege group reads) are refused too, in a sentence of their own: they write nothing (write-*-clusterro), but they read the cluster's users, roles, privilege groups and resource groups, beyond what the collection grants reach, so Studio could not bound what the agent reads.
When the user list was answered and every grant reads only, the server does not enforce authorization, and the refusal says so (PROFILE_SERVER_UNENFORCED), since narrowing the user would change nothing.
A grant made since the last request is caught at the next one: Insert granted to a role during the run shows in its grants (select-grant-own-role-after-grant-midrun), and the proof refuses the request before it is sent.
The window between the proof's last read and the request's own read is a residual, and the proxy's privilege cache may lag a revoke; neither lets the user write, since no agent request names a write.
A transport failure is raised as itself, never read as a verdict.
No write is sent to prove a write is refused: the privilege reads decide, so the stub keeps no write an agent request could reach.
The operations profile sends no request of the model's, so it opens with any user and no proof, and plan mode keeps grounding on root.
One request in the console's own grammar, every body key closed (section 5.2), naming databases/list, databases/describe, collections/list, collections/describe, collections/get_stats, collections/get_load_state, partitions/list, indexes/list, indexes/describe, aliases/list or aliases/describe.
entities/query and entities/get answer stored scalar, vector and dynamic field values (query-values-reader), and entities/search with a text input against a function's output field makes the server run the function on it (search-text-reader), so every entity route is refused before anything is sent, by its route, never the request's own words.
A dbName naming a database other than the connection's is refused by the provider, which alone knows the connection's database, after the connection is open: on a connection not yet open, GetVersion and the read-only proof are sent first, and the request itself is never sent; every call names the connection's database, written by the adapter from the connection whatever the request was read as, and carries only the authorization metadata.
aliases/list names a collectionName: 3.0.2 answers every alias of a database to any user, those of collections outside its grants among them (list-aliases-few in tests/fixtures/milvus/3.0.2/mcp-scope/), so the database's whole list is refused before anything is sent.
What an agent request reads is held to what the user's own grants list, in two layers, each measured on 3.0.2 by the captures under tests/fixtures/milvus/3.0.2/mcp-scope/, which its run.sh records with a user holding DatabaseReadOnly and CollectionReadOnly on default (dbro), one holding CollectionReadOnly on two collections of default with ShowCollections and DescribeDatabase there (few), and one holding both groups on a second database only (onedb), every call sent raw.
The server's own grants, layer one, decide the listings: ListDatabases answers each user only the databases its grants reach (list-databases-*), ShowCollections only the collections they reach (show-collections-few lists two of three, show-collections-other-db-* none), and GetCollectionStatistics, GetLoadState and ShowPartitions of a collection outside them are refused PERMISSION_DENIED (statistics-ungranted-few).
They do not decide everything: the role public, which every user holds, holds DescribeCollection, IndexDetail and ListAliases on every database and collection (select-grant-public-*), so 3.0.2 describes any collection and its indexes to any user, in any database, lists any database's aliases and describes an alias whatever collection it names (describe-collection-ungranted-few, describe-collection-ungranted-onedb, describe-index-ungranted-*, list-aliases-other-db-*, describe-alias-ungranted-few), answers an alias written where a collection's name stands as the collection it names (describe-collection-alias-ungranted-few), and answers a name no collection has apart from one outside the grants (describe-collection-missing-few against describe-collection-ungranted-few).
Studio's client, layer two, closes that: every call is checked as it is sent, not as the request was read (scopedClient in metadata-read.ts).
| What | Stopped by |
|---|---|
| A database other than the connection's | Studio: every call names the connection's database, written from the connection |
| A collection outside the user's grants, by name | Studio: a call naming a collection is sent only when the user's own ShowCollections lists that name, read in the same request |
| An alias where a collection's name stands | Studio: an alias is no name ShowCollections lists, so it is refused as a collection outside the grants is |
| A describe answered for another collection than the one named, as for an alias made in that name since the list was read | Studio: the answer's schema must name the collection the call named |
| An alias of a collection outside the user's grants | Studio: aliases/describe is answered only when its collection is listed and its database is the connection's |
| Every alias of the database | Studio: aliases/list names a collection, and a ListAliases naming none is never sent |
A database outside the user's grants, in databases/list |
The server: ListDatabases is filtered by the grants |
A collection outside the user's grants, in collections/list |
The server: ShowCollections is filtered by the grants |
A collection outside the user's grants, an alias and a name no collection has are refused in one sentence, and an alias of a collection outside them and a name no alias has in another, so no answer tells a collection that exists from one that does not. The names that reach an answer are the user's own databases, collections and their fields, functions, partitions, indexes and aliases. A residual: a name is checked against the list read at the start of the request, so a collection an administrator drops and replaces in the same instant with an alias of that name could be read through for a statistics, load-state, partition or index read; a describe is refused, since its answer names its schema.
Milvus declares ProviderCapabilities.metadataOnly: every answer is projected through src/lib/db/redaction/metadata-only.ts by a typed projection per route, each column a name, a count or a flag read from a fixed place in the server's answer.
| Route | Columns |
|---|---|
databases/list |
database |
databases/describe |
database, properties (how many) |
collections/list |
collection |
collections/describe |
name, kind, type, element_type, dimension, max_length, primary, nullable, dynamic, partition_key, function_output, index, index_type, metric, shards, partitions, consistency, load_state |
collections/get_stats |
collection, row_count |
collections/get_load_state |
collection, load_state, progress |
partitions/list |
partition |
indexes/list, indexes/describe |
index, field, index_type, metric, state, indexed_rows, total_rows, pending_rows, failed |
aliases/list |
alias, collection |
aliases/describe |
alias, collection, database |
A collection read answers one row for the collection itself (kind collection, with its dynamic flag, shard and partition counts, consistency level and load state), then one per field (its name, data type, element type, dimension, maximum length, flags and index), one per struct array and its fields, and one per function (its name and type).
A column's name comes from the projection, never from a key the store holds, and nothing the projection does not name reaches a row: no field's default value, no description, no collection or database property's value, no index parameter and no function parameter, which can name a service or a credential.
A failed index build is answered as failed, never its reason, which is the server's text.
aliases/list names the collection on every row; aliases/describe names the collection and the database.
Names are the operator's own: a database, collection, field, partition, index, alias or function named after a stored value reaches the answer as that name, checked only to be printable text of 1 to 512 characters on one line.
A cell that does not fit its kind, such as a dimension that is not a whole number or a name holding a line break, withholds the whole answer, naming the column and never the value.
A server refusal is worded by its category alone, never the server's message, which names the user: a user without GetStatistics on a collection is answered PrivilegeGetStatistics: permission deny to scope_few in the default database (statistics-ungranted-few), and agent mode is told only that the call failed with permission-denied.
A result past the row or byte budget is refused, never cut, and an estimating plan is refused.
Statement-language family: milvusrpc.
Overlay: src/lib/agent/languages/milvusrpc/engines/milvus.ts, with its client row in src/lib/agent/languages/milvusrpc/engines/client.ts.
The agent reads it through runServerPack and agentEnginePack.
Both overlays are empty, so the engine resolves to the milvusrpc family pack itself.
The milvusrpc statement-language family has no dialect-bound member.
This overlay overrides no Shared member, so every Shared member is the milvusrpc statement-language family's own.
The family offers the investigation workflow, through run_read_query with one Milvus request, and the operations workflow; it offers no data analysis, since its answers carry no stored value, and no query optimization or database assessment, since the server composes no plan and no profile, and no workflow hands an answer to the editor.
The workflow classifier offers the model only these two.
A pack does not open agent mode: execution is gated by AGENT_EXECUTION_ENGINES and the execution-profile check.
MCP clients read Milvus with run_milvus_read, the tool the family declares in its MCP facet (src/lib/agent/languages/milvusrpc/mcp.ts), and MCP_STATEMENT_READ.milvus is true (section 15.6); what stays open is in B155.
run_milvus_read takes one request in the agent's own form, read and admitted by the same reader, and runs it through the same queryReadOnly under the agent read-only profile: the open proves the seed's user, the proof runs again before every request, every call is checked against the user's own collection list (section 15.3.1), and every answer is the projection of section 15.4.
So an MCP seed's user must pass the proof of section 15.2: root, the admin role, a writer, a custom group, ClusterReadOnly and a server with authorization off are refused at the open, each in its own sentence.
Recommended: a user granted CollectionReadOnly on the collections the client may read, with ShowCollections and DescribeDatabase on their database, or DatabaseReadOnly and CollectionReadOnly on one database.
The two data-scope layers of section 15.3.1 hold for MCP unchanged; the names MCP clients newly read through it, beside the collection and field names inspect_schema already lists, are the user's databases, partitions, functions, indexes and aliases.
inspect_schema opens its connection under the operations profile, unproved, and lists what the seed's user's own ShowCollections lists, so a narrow user's listing is its own collections; a root or writer seed is listed every collection there, while run_milvus_read refuses it.
It reads the seed's database alone: a handle opened under an execution profile lists the connection's own database as its one container, with no ListDatabases sent, so a schema naming another database is no schema of that connection and nothing is sent, as run_milvus_read refuses a dbName naming one.
A client cancel ends the wait, not the read: the request's gRPC calls, the proof's among them, run on until answered or until the provider cancels them at the time left of timeout_ms, and the channel has no retry or reconnect that could extend them; a timeout_ms cancels the call in flight.
A call that opens the seed's connection is the exception: the open's GetVersion and read-only proof run under the provider's own call deadline, 10 seconds or the connection's query timeout when shorter, not under timeout_ms, since the open is acquired before the request's budget reaches the provider and the acquisition carries no deadline of its own.
The client is still answered at timeout_ms, while the open runs on to its deadline: measured on 3.0.2 after a Studio restart with the server held, a run_milvus_read with a timeout_ms of 1000 is answered as the timeout at 1,017 ms, and the opening connection stays up 9.96 s.
Bounding the open by the time left would need the MCP acquisition to hand the provider a deadline, which is shared MCP code, so it is documented here rather than bounded.
Measured on 3.0.2 by tests/live/milvus-mcp-boundary.ts, run by tests/fixtures/milvus/3.0.2/mcp-boundary/run.sh, which records boundary.json beside it: with the server paused, a read at a timeout_ms of 500 is answered as the timeout at 501 ms and a cancel 300 ms in at 301 ms, the server answers the next call once it resumes, and no promise is left rejected; the same check replays the open's refusals, every admitted route, the writes and value reads refused with the admission removed beside a positive control, the data scope, an alias repointed under parallel reads and inspect_schema as the narrow user.
tests/unit/db/milvus/read-only-proof.test.ts: the privileges a read-only user may hold, held to 3.0.2's groups, and the proof over every principal's captures, a grant made during the run included.tests/unit/db/milvus/metadata-request.test.ts: the routes an agent request may name, and every refusal before the network.tests/unit/db/milvus/metadata-read.test.ts: every projection over the captured answers, answers that carry a stored value where a name, a count or a flag stands, and the data scope over the narrow principals' captures.tests/unit/db/milvus/agent-provider.test.ts: the open, the proof before every request, the exact RPCs, requests and metadata through the adapter, the wording of a failure and the budgets.tests/integration/db/milvus-provider.test.ts: the boundary replayed from the captures, the read-only principals' writes beside the positive controls, and the proof's verdict on every principal.tests/unit/lib/db/redaction/metadata-only.test.ts: the shared projection helpers.tests/integration/mcp/run-milvus-read.test.ts:run_milvus_readthrough the real route over the data-scope captures, the proof, the scope refusals, the admission and the limits.