merge: integrate v7-rbac-visibility on top of skills feature

Brings together the two unmerged feature lines onto one integration branch:
- skills/review/proposals/revisions (via fix/mcpd-instance-health-and-retry,
  which contains the full skills-1..7 chain + mcpd env/retry/readiness fix)
- v7 visibility scope + ownership for Llms and Agents

Auto-merge resolved schema.prisma, mcpd main.ts, mcplocal config, CLI create,
and completions with no conflicts. Both Prisma migrations coexist.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michal
2026-06-16 21:31:35 +01:00
24 changed files with 828 additions and 45 deletions

View File

@@ -75,6 +75,7 @@ describe('POST /api/v1/llms/_provider-register', () => {
expect(register).toHaveBeenCalledWith({
providerSessionId: 'sess-xyz',
providers: [{ name: 'vllm-local', type: 'openai', model: 'm', tier: 'fast', extraConfig: { gpu: 1 } }],
ownerId: 'system',
});
expect(res.json()).toMatchObject({ providerSessionId: 'sess-xyz' });
});

View File

@@ -0,0 +1,105 @@
/**
* v7 Stage 1 — pure-function tests for the visibility predicate.
* Lives separately from the service-level tests because the predicate
* is the single source of truth for "can user X see this row" and is
* called from both LlmService.list and AgentService.list. We exercise
* every branch of the decision tree to lock the semantics in.
*/
import { describe, it, expect } from 'vitest';
import { isLlmVisibleTo, type Viewer } from '../src/services/llm.service.js';
import { isAgentVisibleTo, type AgentViewer } from '../src/services/agent.service.js';
const llmRow = (overrides: { name?: string; ownerId?: string | null; visibility?: string } = {}): { name: string; ownerId: string | null; visibility: string } => ({
name: 'vllm-alice',
ownerId: 'alice',
visibility: 'private',
...overrides,
});
const agentRow = (overrides: { name?: string; ownerId?: string; visibility?: string } = {}): { name: string; ownerId: string; visibility: string } => ({
name: 'reviewer',
ownerId: 'alice',
visibility: 'private',
...overrides,
});
describe('isLlmVisibleTo (v7)', () => {
it('null viewer skips the filter — internal callers see everything', () => {
// Cron sweeps, audit collectors, and tests without a request
// context get a null viewer. The visibility filter is then a
// no-op, which matches the pre-v7 behavior of those code paths.
expect(isLlmVisibleTo(llmRow(), null)).toBe(true);
});
it('public rows are visible to anyone with the resource grant', () => {
const v: Viewer = { userId: 'bob', wildcard: false, allowedNames: new Set() };
expect(isLlmVisibleTo(llmRow({ visibility: 'public' }), v)).toBe(true);
});
it('wildcard viewer (admin) sees private rows owned by others', () => {
const v: Viewer = { userId: 'admin', wildcard: true, allowedNames: new Set() };
expect(isLlmVisibleTo(llmRow({ visibility: 'private', ownerId: 'alice' }), v)).toBe(true);
});
it('owner sees their own private row', () => {
const v: Viewer = { userId: 'alice', wildcard: false, allowedNames: new Set() };
expect(isLlmVisibleTo(llmRow({ visibility: 'private', ownerId: 'alice' }), v)).toBe(true);
});
it('non-owner without name-scoped grant cannot see a private row', () => {
const v: Viewer = { userId: 'bob', wildcard: false, allowedNames: new Set() };
expect(isLlmVisibleTo(llmRow({ visibility: 'private', ownerId: 'alice' }), v)).toBe(false);
});
it('non-owner WITH name-scoped grant can see a private row', () => {
// alice published vllm-alice as private; alice ran
// `mcpctl create rbac binding view:llms:vllm-alice --user bob`,
// so bob now sees the row in his list output.
const v: Viewer = { userId: 'bob', wildcard: false, allowedNames: new Set(['vllm-alice']) };
expect(isLlmVisibleTo(llmRow({ name: 'vllm-alice', visibility: 'private', ownerId: 'alice' }), v)).toBe(true);
});
it('treats null ownerId as no-owner (legacy rows pre-v7 backfill stay visible if public)', () => {
// The migration sets visibility='public' for legacy rows, so they
// pass the public-visibility check before the ownerId branch is
// ever reached. A row with NULL ownerId AND visibility='private'
// is unreachable via normal flows, but we still want the predicate
// to behave: no owner + bob viewing = not visible.
const v: Viewer = { userId: 'bob', wildcard: false, allowedNames: new Set() };
expect(isLlmVisibleTo(llmRow({ ownerId: null, visibility: 'private' }), v)).toBe(false);
});
});
describe('isAgentVisibleTo (v7)', () => {
// Same shape as Llm; agents always have a non-null ownerId because
// `Agent.ownerId` is required, so we don't need the legacy-null
// branch test.
it('null viewer = visible (internal calls bypass filter)', () => {
expect(isAgentVisibleTo(agentRow(), null)).toBe(true);
});
it('public agents visible to anyone with resource grant', () => {
const v: AgentViewer = { userId: 'bob', wildcard: false, allowedNames: new Set() };
expect(isAgentVisibleTo(agentRow({ visibility: 'public' }), v)).toBe(true);
});
it('owner sees own private agent', () => {
const v: AgentViewer = { userId: 'alice', wildcard: false, allowedNames: new Set() };
expect(isAgentVisibleTo(agentRow({ visibility: 'private', ownerId: 'alice' }), v)).toBe(true);
});
it('non-owner without grant blocked from private agent', () => {
const v: AgentViewer = { userId: 'bob', wildcard: false, allowedNames: new Set() };
expect(isAgentVisibleTo(agentRow({ visibility: 'private', ownerId: 'alice' }), v)).toBe(false);
});
it('non-owner WITH name-scoped grant can see private agent', () => {
const v: AgentViewer = { userId: 'bob', wildcard: false, allowedNames: new Set(['reviewer']) };
expect(isAgentVisibleTo(agentRow({ name: 'reviewer', visibility: 'private', ownerId: 'alice' }), v)).toBe(true);
});
it('wildcard viewer sees private agent owned by another user', () => {
const v: AgentViewer = { userId: 'admin', wildcard: true, allowedNames: new Set() };
expect(isAgentVisibleTo(agentRow({ visibility: 'private', ownerId: 'alice' }), v)).toBe(true);
});
});