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:
@@ -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' });
|
||||
});
|
||||
|
||||
105
src/mcpd/tests/visibility-filter.test.ts
Normal file
105
src/mcpd/tests/visibility-filter.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user