maestro/src/bridge/a2a/delegation.ts
oss-sync b1292e34b2
Some checks failed
CI / build-and-test (push) Has been cancelled
sync: update from private repo (edc775f2)
2026-07-06 01:04:12 +00:00

103 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { Repository } from '../../db/repository.js';
import { resolveOrgIds } from '../auth.js';
export interface A2aDelegation {
/** Primary key of the a2a_delegations row. */
id: string;
userId: string;
clientId: string;
grantId: string;
grantedSpaceIds: string[];
grantedSkills: string[];
expiresAt: string | null;
revokedAt: string | null;
}
/** 委任が有効か(失効していない & 期限内。nowIso は呼び出し側が渡す(テスト決定論のため)。 */
export function isDelegationLive(
d: { expiresAt: string | null; revokedAt: string | null },
nowIso: string,
): boolean {
if (d.revokedAt) return false;
if (d.expiresAt && new Date(d.expiresAt).getTime() <= new Date(nowIso).getTime()) return false;
return true;
}
/**
* computeEffectiveScope に渡せる最小限の principal 型。
* A2aPrincipal (token-auth.ts) はこのインターフェースを満たす。
* 循環 import を避けるためここで独立定義。
*/
export interface A2aPrincipalLike {
actingUserId: string;
/** OAuth クライアント識別子。audit log 記録用(省略可)。 */
clientId?: string;
delegation: {
grantedSpaceIds: string[];
grantedSkills: string[];
};
}
export interface EffectiveScope {
viewer: Express.User;
userVisibleSpaceIds: string[];
allowlistsBySpace: Record<string, string[]>;
effectiveSpaceIds: string[];
effectiveSkills: string[];
}
/**
* acting user の実際の可視スペース・スキルを delegation の AND 交差で絞り込む。
* user が存在しないまたは active でない場合は null を返すfail-closed
*/
export async function computeEffectiveScope(
repo: Repository,
principal: A2aPrincipalLike,
): Promise<EffectiveScope | null> {
const u = repo.getUserById(principal.actingUserId);
if (!u || u.status !== 'active') return null;
const viewer = {
id: u.id,
role: u.role,
orgIds: resolveOrgIds(repo, u.id),
status: u.status,
name: u.name,
} as Express.User;
const visible = await repo.listSpaces({ viewer });
const userVisibleSpaceIds = visible.map(s => s.id);
const allowlistsBySpace: Record<string, string[]> = {};
for (const s of visible) {
allowlistsBySpace[s.id] = repo.getSpaceA2aSkills(s.id);
}
const effectiveSpaceIds = resolveEffectiveSpaces(
principal.delegation.grantedSpaceIds,
userVisibleSpaceIds,
);
const effectiveSkills = resolveEffectiveSkills(
principal.delegation.grantedSkills,
allowlistsBySpace,
effectiveSpaceIds,
);
return { viewer, userVisibleSpaceIds, allowlistsBySpace, effectiveSpaceIds, effectiveSkills };
}
/** AND 交差: 委任が許可したスペース ∩ acting user が実際に見えるスペース。 */
export function resolveEffectiveSpaces(grantedSpaceIds: string[], userVisibleSpaceIds: string[]): string[] {
const visible = new Set(userVisibleSpaceIds);
return [...new Set(grantedSpaceIds)].filter(id => visible.has(id));
}
/** AND 交差: 委任スキル ∩ (有効スペースの A2A 許可リストの和集合)。重複排除。 */
export function resolveEffectiveSkills(
grantedSkills: string[],
spaceAllowlistsBySpace: Record<string, string[]>,
effectiveSpaceIds: string[],
): string[] {
const allowed = new Set<string>();
for (const sid of effectiveSpaceIds) {
for (const skill of (spaceAllowlistsBySpace[sid] ?? [])) allowed.add(skill);
}
const grantedSet = new Set(grantedSkills);
return [...allowed].filter(s => grantedSet.has(s));
}