docs+smoke(v7): visibility section in virtual-llms.md + register/list smoke
Some checks failed
CI/CD / typecheck (pull_request) Successful in 55s
CI/CD / test (pull_request) Successful in 1m11s
CI/CD / lint (pull_request) Successful in 2m49s
CI/CD / smoke (pull_request) Failing after 1m42s
CI/CD / build (pull_request) Successful in 5m37s
CI/CD / publish (pull_request) Has been skipped

Wraps up v7 Stage 3:

- docs/virtual-llms.md gains a "Visibility scope (v7)" section that
  explains public-vs-private semantics, who skips the filter (owner +
  `*` admin), how to grant single-row exceptions via name-scoped RBAC,
  per-row override syntax in mcplocal config, the `--visibility` flag
  on `mcpctl create llm`/`create agent`, and YAML round-trip behavior.
- New smoke (virtual-llm-visibility.smoke.test.ts) publishes one
  public + one private virtual Llm via the registrar against the
  live mcpd and asserts the GET /llms response carries visibility
  + a non-empty ownerId for both, and that GET /llms/<name> returns
  the private row to its owner without 404. Cross-user filtering is
  covered by mcpd's visibility-filter unit tests; smoke proves the
  fields make the round-trip end-to-end.

Will pass once mcpd is rebuilt + deployed via fulldeploy.sh on this
branch (current main is v6, doesn't yet serialize visibility).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michal
2026-04-29 01:08:03 +01:00
parent 2c98a21323
commit 2b2444a2c5
2 changed files with 300 additions and 2 deletions

View File

@@ -431,10 +431,100 @@ mid-task reverts the row to pending instead of failing the caller.
See [inference-tasks.md](./inference-tasks.md) for the full data
model, async API, lifecycle, RBAC, and CLI surface.
## Visibility scope (v7)
Virtual Llms and Agents now carry an explicit **visibility** field that
decides who can see the row in listings.
| Visibility | Meaning |
|-------------|----------------------------------------------------------------------------------|
| `public` | Visible to anyone with `view:llms` / `view:agents`. Default for hand-created Llms. |
| `private` | Only the **owner** plus principals with a name-scoped grant can see it. Default for virtual Llms and Agents on first publish. |
The owner is whichever user authenticated the publishing
`POST /api/v1/llms/_provider-register` (or `mcpctl create llm`). For
mcplocal that's whichever `~/.mcpctl/credentials` token is on disk.
Legacy rows from before v7 default to `visibility=public, ownerId=NULL`,
so the upgrade is a no-op for everything that already exists.
### Who skips the filter?
Two principals see every row regardless of visibility:
1. The **row owner** (`ownerId === request.userId`).
2. Anyone with a **cross-resource admin** grant — RBAC binding
`{ resource: '*' }`. Operationally this is the SRE / cluster admin.
A plain `view:llms` resource grant is *not* the same as admin: it's a
RBAC wildcard for name-scoping (you can name any Llm), but the
visibility filter still applies on top. This is the v7 split that
prevents a user with `view:llms` from enumerating every developer's
private virtual Llm.
### Granting a single-row exception
When alice wants bob to see her private virtual Llm `alice-vllm-local`
without making it public, she binds:
```sh
mcpctl create rbac bob view:llms --name alice-vllm-local
```
Same shape as any other name-scoped binding. Removing the binding
flips bob back to "row not found".
### Publishing as private from mcplocal
mcplocal defaults to `private` for every published provider and agent.
Override per-row in `~/.mcpctl/config.json`:
```jsonc
{
"llm": {
"providers": [
{ "name": "vllm-local", "type": "vllm", "model": "...", "publish": true,
"visibility": "private" }, // default; explicit for clarity
{ "name": "shared-qwen", "type": "vllm", "model": "...", "publish": true,
"visibility": "public" } // every team member can chat with it
]
},
"agents": [
{ "name": "local-coder", "llm": "vllm-local",
"visibility": "private" } // private agents pinned to private Llms
]
}
```
On a sticky reconnect (`providerSessionId` matches an existing row)
the visibility is **only** updated when the publisher explicitly sends
it — leaving the field off keeps whatever the row already has,
including any field admin set out-of-band.
### Hand-created Llms
`mcpctl create llm` defaults to `public` (matches pre-v7 behavior).
Pass `--visibility private` to opt in:
```sh
mcpctl create llm my-key --type openai --model gpt-4o \
--api-key-ref my-secret/key --visibility private
```
The same `--visibility` flag is on `mcpctl create agent`.
### CLI surface
`mcpctl get llm` and `mcpctl get agent` show a `VISIBILITY` column.
YAML round-trips cleanly: `mcpctl get llm X -o yaml | mcpctl apply -f -`
preserves visibility, and `ownerId` is stripped from the apply doc
because it's server-side state (the apply re-stamps the ownerId of the
authenticated caller, not the original creator).
## Roadmap (later stages)
(LB pool by name landed in v4; durable task queue landed in v5.)
- **v6** — multi-instance mcpd via pg `LISTEN/NOTIFY` (replaces the
(LB pool by name landed in v4; durable task queue landed in v5;
visibility scope landed in v7.)
- **v8** — multi-instance mcpd via pg `LISTEN/NOTIFY` (replaces the
per-instance EventEmitter wakeup), per-session worker capacity,
remote cancel protocol over the SSE channel.