aps-agent/server/core/registry/visibility.ts

66 lines
2.7 KiB
TypeScript
Raw Normal View History

/**
* 能力可见性(任务 A3.5):按身份与白名单决定谁能看到哪些能力。
*
* 两条边界(架构设计 5.1):
* - 工具列表与能力清单必须来自同一次过滤,否则模型能看到用户看不到的能力
* - 权限归业务系统(D9),这里只管「平台把哪些能力暴露给这个身份」,不管业务系统内部的角色
*
* 默认策略:开发期 defaultAllow=true(未配白名单时全部可见)。接入真实身份(A4.1)后应改成 false,
* 由授权表逐条给出——这与治理模型里「未登记默认拒绝」是同一条原则。
*/
import type { Capability } from '@aps/provider-sdk';
export interface CapabilityGrant {
/** 空表示不限用户 */
userId?: string;
/** 空表示不限租户 */
tenantId?: string;
/** 允许的能力 id 前缀,如 aps.schedule 或 fake. */
allow: string[];
}
export interface CapabilityVisibility {
defaultAllow: boolean;
denyWrite: boolean;
grants: CapabilityGrant[];
}
export interface VisibilitySubject {
userId: string;
tenantId: string;
}
const matchesPrefix = (capabilityId: string, prefixes: string[]): boolean =>
prefixes.some((prefix) => (prefix.endsWith('*') ? capabilityId.startsWith(prefix.slice(0, -1)) : capabilityId === prefix));
const grantApplies = (grant: CapabilityGrant, subject: VisibilitySubject): boolean => {
if (grant.userId !== undefined && grant.userId !== subject.userId) return false;
if (grant.tenantId !== undefined && grant.tenantId !== subject.tenantId) return false;
return true;
};
/** 这个身份能看到这个能力吗 */
export function isCapabilityVisible(capability: Capability, visibility: CapabilityVisibility, subject: VisibilitySubject): boolean {
// 写能力在只读模式下对所有人不可见(演示、审计或只读部署用)
if (visibility.denyWrite && capability.effect === 'write') return false;
const granted = visibility.grants.some((grant) => grantApplies(grant, subject) && matchesPrefix(capability.id, grant.allow));
if (granted) return true;
if (!visibility.defaultAllow) return false;
// 默认允许模式下仍可用 denyWrite 收口;白名单里的 deny 规则暂不做,等 A4 的策略层
return true;
}
export function visibleCapabilities(
capabilities: Capability[],
visibility: CapabilityVisibility,
subject: VisibilitySubject,
): Capability[] {
return capabilities.filter((capability) => isCapabilityVisible(capability, visibility, subject));
}
/** 能力集的指纹:用于判断运行时会话是否需要重建(工具集变了就得重建) */
export function capabilityFingerprint(capabilities: Capability[]): string {
return [...capabilities.map((capability) => capability.id)].sort().join(',');
}