feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
@@ -0,0 +1,256 @@
import type {
AppendProjectRoleBindingResult,
ProjectRecord,
ProjectRole,
ProjectRoleBindingRecord,
ProjectRoleBindingState,
ProjectStatus,
} from './projectPolicy';
import type { SecurityPolicyFence, SecuritySubject } from '../security';
import type {
SecurityAuditRecord,
SecurityAuditSink,
} from '../audit/securityAudit';
export const MAX_LOCAL_PROJECT_QUERY_PAGE_SIZE = 64;
export const MAX_LOCAL_PROJECT_ROLE_BINDING_QUERY_PAGE_SIZE = 64;
export interface LocalProjectQueryCursor {
readonly slug: string;
readonly projectId: string;
}
export type LocalProjectQueryStatus = ProjectStatus | 'all';
export interface LocalProjectAdministrationAuthorization {
readonly authorityProjectId: string;
readonly actor: SecuritySubject;
readonly fence: SecurityPolicyFence;
}
export interface InspectAuthorizedLocalProjectCommand {
readonly projectId: string;
readonly authorization: LocalProjectAdministrationAuthorization;
readonly audit: SecurityAuditRecord;
}
export interface InspectAuthorizedLocalProjectResult {
readonly project: Readonly<ProjectRecord> | null;
readonly audit: Readonly<SecurityAuditRecord>;
}
export interface ListAuthorizedLocalProjectsCommand {
readonly limit: number;
readonly status: LocalProjectQueryStatus;
readonly after?: LocalProjectQueryCursor;
readonly authorization: LocalProjectAdministrationAuthorization;
readonly audit: SecurityAuditRecord;
}
export interface ListAuthorizedLocalProjectsResult {
readonly projects: readonly Readonly<ProjectRecord>[];
readonly nextCursor: Readonly<LocalProjectQueryCursor> | null;
readonly audit: Readonly<SecurityAuditRecord>;
}
export interface LocalProjectRoleBindingQueryCursor {
readonly subjectType: SecuritySubject['type'];
readonly subjectId: string;
}
export type LocalProjectRoleBindingQueryState = ProjectRoleBindingState | 'all';
export type LocalProjectRoleBindingQueryRole = ProjectRole | 'all';
export interface LocalProjectRoleBindingAdministrationAuthorization {
readonly projectId: string;
readonly actor: SecuritySubject;
readonly fence: SecurityPolicyFence;
}
export interface InspectAuthorizedLocalProjectRoleBindingCommand {
readonly target: SecuritySubject;
readonly authorization: LocalProjectRoleBindingAdministrationAuthorization;
readonly audit: SecurityAuditRecord;
}
export interface InspectAuthorizedLocalProjectRoleBindingResult {
readonly binding: Readonly<ProjectRoleBindingRecord> | null;
readonly audit: Readonly<SecurityAuditRecord>;
}
export interface ListAuthorizedLocalProjectRoleBindingsCommand {
readonly limit: number;
readonly state: LocalProjectRoleBindingQueryState;
readonly role: LocalProjectRoleBindingQueryRole;
readonly after?: LocalProjectRoleBindingQueryCursor;
readonly authorization: LocalProjectRoleBindingAdministrationAuthorization;
readonly audit: SecurityAuditRecord;
}
export interface ListAuthorizedLocalProjectRoleBindingsResult {
readonly bindings: readonly Readonly<ProjectRoleBindingRecord>[];
readonly nextCursor: Readonly<LocalProjectRoleBindingQueryCursor> | null;
readonly audit: Readonly<SecurityAuditRecord>;
}
export interface AppendAuthorizedProjectRoleBindingCommand {
readonly expectedCurrentVersion: number;
readonly binding: ProjectRoleBindingRecord;
readonly actor: SecuritySubject;
readonly fence: SecurityPolicyFence;
readonly audit: SecurityAuditRecord;
}
export interface AppendAuthorizedProjectRoleBindingResult
extends AppendProjectRoleBindingResult {
readonly audit: Readonly<SecurityAuditRecord>;
}
export type LocalProjectAdministrationOperation =
| 'create'
| 'archive'
| 'restore';
export interface LocalProjectAdministrationMutationRecord {
readonly mutationId: string;
readonly operation: LocalProjectAdministrationOperation;
readonly authorityProjectId: string;
readonly project: Readonly<ProjectRecord>;
readonly expectedPreviousVersion: number;
readonly changedBy: SecuritySubject;
readonly createdAtMs: number;
}
interface AppendAuthorizedProjectBaseCommand {
readonly authorityProjectId: string;
readonly projectId: string;
readonly expectedCurrentVersion: number;
readonly mutationId: string;
readonly actor: SecuritySubject;
readonly fence: SecurityPolicyFence;
readonly audit: SecurityAuditRecord;
readonly occurredAtMs: number;
}
export interface AppendAuthorizedProjectCreateCommand
extends AppendAuthorizedProjectBaseCommand {
readonly operation: 'create';
readonly name: string;
readonly slug: string;
}
export interface AppendAuthorizedProjectTransitionCommand
extends AppendAuthorizedProjectBaseCommand {
readonly operation: 'archive' | 'restore';
}
export type AppendAuthorizedProjectCommand =
| AppendAuthorizedProjectCreateCommand
| AppendAuthorizedProjectTransitionCommand;
export interface AppendAuthorizedProjectResult {
readonly status: 'inserted' | 'existing';
readonly project: Readonly<ProjectRecord>;
readonly mutation: Readonly<LocalProjectAdministrationMutationRecord>;
readonly initialOwnerBinding: Readonly<ProjectRoleBindingRecord> | null;
readonly audit: Readonly<SecurityAuditRecord>;
}
/**
* Short-lived policy administration authority. Implementations must revalidate
* the actor's Project/RoleBinding fence and atomically commit each query or
* mutation with its audit record.
*/
export interface LocalProjectPolicyAdministrationRepository
extends SecurityAuditSink {
inspectAuthorizedProjectRoleBinding(
command: InspectAuthorizedLocalProjectRoleBindingCommand,
): Promise<InspectAuthorizedLocalProjectRoleBindingResult>;
listAuthorizedProjectRoleBindings(
command: ListAuthorizedLocalProjectRoleBindingsCommand,
): Promise<ListAuthorizedLocalProjectRoleBindingsResult>;
inspectAuthorizedProject(
command: InspectAuthorizedLocalProjectCommand,
): Promise<InspectAuthorizedLocalProjectResult>;
listAuthorizedProjects(
command: ListAuthorizedLocalProjectsCommand,
): Promise<ListAuthorizedLocalProjectsResult>;
appendAuthorizedProject(
command: AppendAuthorizedProjectCommand,
): Promise<AppendAuthorizedProjectResult>;
appendAuthorizedProjectRoleBinding(
command: AppendAuthorizedProjectRoleBindingCommand,
): Promise<AppendAuthorizedProjectRoleBindingResult>;
}
export class LocalProjectPolicyAuthorizationFenceConflictError extends Error {
readonly code = 'LOCAL_PROJECT_POLICY_AUTHORIZATION_FENCE_CONFLICT';
constructor() {
super('Local Project policy authorization changed');
this.name = 'LocalProjectPolicyAuthorizationFenceConflictError';
}
}
export class LocalProjectPolicyLastOwnerError extends Error {
readonly code = 'LOCAL_PROJECT_POLICY_LAST_OWNER';
constructor() {
super('Local Project must retain at least one active User owner');
this.name = 'LocalProjectPolicyLastOwnerError';
}
}
export class LocalProjectPolicyOwnerCredentialRequiredError extends Error {
readonly code = 'LOCAL_PROJECT_POLICY_OWNER_CREDENTIAL_REQUIRED';
constructor() {
super('A new Local Project owner must have an active credential');
this.name = 'LocalProjectPolicyOwnerCredentialRequiredError';
}
}
export class LocalProjectPolicyProjectVersionConflictError extends Error {
readonly code = 'LOCAL_PROJECT_POLICY_PROJECT_VERSION_CONFLICT';
constructor() {
super('Local Project current version changed');
this.name = 'LocalProjectPolicyProjectVersionConflictError';
}
}
export class LocalProjectPolicyProjectMutationConflictError extends Error {
readonly code = 'LOCAL_PROJECT_POLICY_PROJECT_MUTATION_CONFLICT';
constructor() {
super('Local Project mutation conflicts with its previous request');
this.name = 'LocalProjectPolicyProjectMutationConflictError';
}
}
export class LocalProjectPolicyProjectIdentityConflictError extends Error {
readonly code = 'LOCAL_PROJECT_POLICY_PROJECT_IDENTITY_CONFLICT';
constructor() {
super('Local Project identity or slug is already in use');
this.name = 'LocalProjectPolicyProjectIdentityConflictError';
}
}
export class LocalProjectPolicyProjectCapacityError extends Error {
readonly code = 'LOCAL_PROJECT_POLICY_PROJECT_CAPACITY_EXCEEDED';
constructor() {
super('Local Project capacity is exhausted');
this.name = 'LocalProjectPolicyProjectCapacityError';
}
}
export class LocalProjectPolicyAuthorityProjectProtectedError extends Error {
readonly code = 'LOCAL_PROJECT_POLICY_AUTHORITY_PROJECT_PROTECTED';
constructor() {
super('Local instance authority Project cannot be archived');
this.name = 'LocalProjectPolicyAuthorityProjectProtectedError';
}
}
@@ -0,0 +1,489 @@
import {
SECURITY_SUBJECT_TYPES,
normalizeSecurityPolicyDecision,
type SecurityPolicyDecision,
type SecurityPrincipal,
type SecuritySubject,
} from '../security';
export const PROJECT_STATUSES = ['active', 'archived'] as const;
export const PROJECT_ROLES = ['owner', 'admin', 'operator', 'viewer'] as const;
export const PROJECT_ROLE_BINDING_STATES = ['active', 'revoked'] as const;
export const STATIC_PROJECT_PERMISSIONS = [
'project.read',
'project.manage',
'task.read',
'task.create',
'task.update',
'task.delete',
'trigger.read',
'trigger.create',
'trigger.update',
'run.read',
'run.start',
'run.stop',
'run.retry',
'model.invoke',
'artifact.read',
'approval.read',
'package.manage',
'secret.use',
'secret.manage',
'worker.manage',
'policy.manage',
'approval.decide',
'approval.recover',
] as const;
export type ProjectStatus = (typeof PROJECT_STATUSES)[number];
export type ProjectRole = (typeof PROJECT_ROLES)[number];
export type ProjectRoleBindingState =
(typeof PROJECT_ROLE_BINDING_STATES)[number];
export type StaticProjectPermission =
(typeof STATIC_PROJECT_PERMISSIONS)[number];
export type ProjectPermission = StaticProjectPermission | `tool.call:${string}`;
export interface ProjectRecord {
readonly id: string;
readonly name: string;
readonly slug: string;
readonly status: ProjectStatus;
readonly version: number;
readonly createdAtMs: number;
readonly updatedAtMs: number;
}
export interface ProjectRoleBindingRecord {
readonly projectId: string;
readonly subject: SecuritySubject;
readonly version: number;
readonly state: ProjectRoleBindingState;
readonly role?: ProjectRole;
readonly mutationId: string;
readonly changedBy: SecuritySubject;
readonly createdAtMs: number;
}
export interface ProjectPolicySnapshot {
readonly project: Readonly<ProjectRecord>;
readonly binding?: Readonly<ProjectRoleBindingRecord>;
}
export interface ProjectPolicyRequest {
readonly subject: SecuritySubject;
readonly projectId: string;
readonly permission: ProjectPermission;
}
export interface AppendProjectRoleBindingCommand {
readonly expectedCurrentVersion: number;
readonly binding: ProjectRoleBindingRecord;
}
export interface AppendProjectRoleBindingResult {
readonly status: 'inserted' | 'existing';
readonly binding: Readonly<ProjectRoleBindingRecord>;
}
export interface ProjectPolicyRepository {
resolve(
projectId: string,
subject: Readonly<SecuritySubject>,
): Promise<Readonly<ProjectPolicySnapshot> | null>;
append(
command: AppendProjectRoleBindingCommand,
): Promise<AppendProjectRoleBindingResult>;
}
export const MAX_PROJECT_ROLE_BINDING_VERSION = 2_147_483_647;
export class InvalidProjectPolicyValueError extends TypeError {
constructor(message: string) {
super(`Project policy contract is invalid: ${message}`);
this.name = 'InvalidProjectPolicyValueError';
}
}
export class ProjectPolicyUnavailableError extends Error {
readonly code = 'PROJECT_POLICY_UNAVAILABLE';
constructor() {
super('Project policy is unavailable');
this.name = 'ProjectPolicyUnavailableError';
}
}
export class ProjectPolicyProjectNotFoundError extends Error {
readonly code = 'PROJECT_NOT_FOUND';
constructor() {
super('Project does not exist');
this.name = 'ProjectPolicyProjectNotFoundError';
}
}
export class ProjectRoleBindingVersionConflictError extends Error {
readonly code = 'PROJECT_ROLE_BINDING_VERSION_CONFLICT';
constructor() {
super('Project role binding current version changed');
this.name = 'ProjectRoleBindingVersionConflictError';
}
}
export class ProjectRoleBindingMutationConflictError extends Error {
readonly code = 'PROJECT_ROLE_BINDING_MUTATION_CONFLICT';
constructor() {
super('Project role binding mutation conflicts with its previous request');
this.name = 'ProjectRoleBindingMutationConflictError';
}
}
const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
const SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,126}[a-z0-9])?$/;
const MUTATION_PATTERN = /^[A-Za-z0-9._:-]{1,64}$/;
const TOOL_PERMISSION_PATTERN =
/^tool\.call:[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
function exactKeys(
value: object,
expected: readonly string[],
name: string,
): void {
const keys = Object.keys(value).sort();
const canonical = [...expected].sort();
if (
keys.length !== canonical.length ||
keys.some((key, index) => key !== canonical[index])
) {
throw new InvalidProjectPolicyValueError(`${name} shape is invalid`);
}
}
function boundedText(value: string, maximum: number, name: string): string {
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > maximum ||
CONTROL_PATTERN.test(value)
) {
throw new InvalidProjectPolicyValueError(`${name} is invalid`);
}
return value;
}
function timestamp(value: number, name: string): number {
if (!Number.isSafeInteger(value) || value < 0) {
throw new InvalidProjectPolicyValueError(`${name} is invalid`);
}
return value;
}
function version(value: number, name: string): number {
if (
!Number.isSafeInteger(value) ||
value < 1 ||
value > MAX_PROJECT_ROLE_BINDING_VERSION
) {
throw new InvalidProjectPolicyValueError(`${name} is invalid`);
}
return value;
}
export function normalizeProjectPolicySubject(
value: SecuritySubject,
): Readonly<SecuritySubject> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidProjectPolicyValueError('subject must be an object');
}
exactKeys(value, ['type', 'id'], 'subject');
if (!SECURITY_SUBJECT_TYPES.includes(value.type)) {
throw new InvalidProjectPolicyValueError('subject type is invalid');
}
return Object.freeze({
type: value.type,
id: boundedText(value.id, 255, 'subject id'),
});
}
export function assertProjectPolicyProjectId(value: string): void {
boundedText(value, 128, 'project id');
}
export function normalizeProjectPermission(value: string): ProjectPermission {
if (
STATIC_PROJECT_PERMISSIONS.includes(value as StaticProjectPermission) ||
(typeof value === 'string' && TOOL_PERMISSION_PATTERN.test(value))
) {
return value as ProjectPermission;
}
throw new InvalidProjectPolicyValueError('permission is invalid');
}
export function normalizeProjectRecord(
value: ProjectRecord,
): Readonly<ProjectRecord> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidProjectPolicyValueError('project must be an object');
}
exactKeys(
value,
['id', 'name', 'slug', 'status', 'version', 'createdAtMs', 'updatedAtMs'],
'project',
);
const createdAtMs = timestamp(value.createdAtMs, 'project createdAtMs');
const updatedAtMs = timestamp(value.updatedAtMs, 'project updatedAtMs');
if (updatedAtMs < createdAtMs) {
throw new InvalidProjectPolicyValueError('project timestamps are invalid');
}
if (!PROJECT_STATUSES.includes(value.status)) {
throw new InvalidProjectPolicyValueError('project status is invalid');
}
if (!SLUG_PATTERN.test(value.slug)) {
throw new InvalidProjectPolicyValueError('project slug is invalid');
}
assertProjectPolicyProjectId(value.id);
return Object.freeze({
id: value.id,
name: boundedText(value.name, 255, 'project name'),
slug: value.slug,
status: value.status,
version: version(value.version, 'project version'),
createdAtMs,
updatedAtMs,
});
}
export function normalizeProjectRoleBinding(
value: ProjectRoleBindingRecord,
): Readonly<ProjectRoleBindingRecord> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidProjectPolicyValueError('role binding must be an object');
}
exactKeys(
value,
value.state === 'active'
? [
'projectId',
'subject',
'version',
'state',
'role',
'mutationId',
'changedBy',
'createdAtMs',
]
: [
'projectId',
'subject',
'version',
'state',
'mutationId',
'changedBy',
'createdAtMs',
],
'role binding',
);
if (!PROJECT_ROLE_BINDING_STATES.includes(value.state)) {
throw new InvalidProjectPolicyValueError('role binding state is invalid');
}
if (
(value.state === 'active' &&
(!value.role || !PROJECT_ROLES.includes(value.role))) ||
(value.state === 'revoked' && value.role !== undefined)
) {
throw new InvalidProjectPolicyValueError('role binding role is invalid');
}
if (
typeof value.mutationId !== 'string' ||
!MUTATION_PATTERN.test(value.mutationId)
) {
throw new InvalidProjectPolicyValueError('mutation id is invalid');
}
assertProjectPolicyProjectId(value.projectId);
return Object.freeze({
projectId: value.projectId,
subject: normalizeProjectPolicySubject(value.subject),
version: version(value.version, 'role binding version'),
state: value.state,
...(value.role ? { role: value.role } : {}),
mutationId: value.mutationId,
changedBy: normalizeProjectPolicySubject(value.changedBy),
createdAtMs: timestamp(value.createdAtMs, 'role binding createdAtMs'),
});
}
export function normalizeProjectPolicySnapshot(
value: ProjectPolicySnapshot,
): Readonly<ProjectPolicySnapshot> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidProjectPolicyValueError(
'policy snapshot must be an object',
);
}
exactKeys(
value,
value.binding ? ['project', 'binding'] : ['project'],
'snapshot',
);
const project = normalizeProjectRecord(value.project);
const binding = value.binding
? normalizeProjectRoleBinding(value.binding)
: undefined;
if (binding && binding.projectId !== project.id) {
throw new InvalidProjectPolicyValueError(
'binding belongs to another project',
);
}
return Object.freeze({ project, ...(binding ? { binding } : {}) });
}
export function assertExpectedProjectRoleBindingVersion(value: number): void {
if (
!Number.isSafeInteger(value) ||
value < 0 ||
value >= MAX_PROJECT_ROLE_BINDING_VERSION
) {
throw new InvalidProjectPolicyValueError(
'expected role binding version is invalid',
);
}
}
const READ_ONLY_PERMISSIONS = new Set<ProjectPermission>([
'project.read',
'task.read',
'trigger.read',
'run.read',
'artifact.read',
'approval.read',
]);
const OPERATOR_PERMISSIONS = new Set<ProjectPermission>([
...READ_ONLY_PERMISSIONS,
'task.create',
'task.update',
'trigger.create',
'trigger.update',
'run.start',
'run.stop',
'run.retry',
'model.invoke',
'secret.use',
]);
const AGENT_APPROVAL_PERMISSIONS = new Set<ProjectPermission>([
'project.manage',
'task.create',
'task.update',
'task.delete',
'trigger.create',
'trigger.update',
'run.start',
'run.stop',
'run.retry',
'model.invoke',
'secret.use',
'secret.manage',
'package.manage',
'worker.manage',
'policy.manage',
'approval.decide',
]);
function roleAllows(role: ProjectRole, permission: ProjectPermission): boolean {
if (role === 'owner') return true;
if (role === 'admin')
return (
permission.startsWith('tool.call:') || permission !== 'project.manage'
);
if (role === 'operator')
return (
permission.startsWith('tool.call:') ||
OPERATOR_PERMISSIONS.has(permission)
);
return READ_ONLY_PERMISSIONS.has(permission);
}
/** Evaluates one immutable Project/RoleBinding snapshot and returns its fence. */
export class ProjectPolicyEngine {
constructor(
private readonly repository: Pick<ProjectPolicyRepository, 'resolve'>,
) {
if (!repository || typeof repository.resolve !== 'function') {
throw new TypeError('Project policy repository is invalid');
}
}
async decide(request: ProjectPolicyRequest): Promise<SecurityPolicyDecision> {
if (!request || typeof request !== 'object' || Array.isArray(request)) {
throw new InvalidProjectPolicyValueError('request must be an object');
}
exactKeys(request, ['subject', 'projectId', 'permission'], 'request');
const subject = normalizeProjectPolicySubject(request.subject);
assertProjectPolicyProjectId(request.projectId);
const projectId = request.projectId;
const permission = normalizeProjectPermission(request.permission);
let snapshot: Readonly<ProjectPolicySnapshot> | null;
try {
const resolved = await this.repository.resolve(projectId, subject);
snapshot = resolved ? normalizeProjectPolicySnapshot(resolved) : null;
} catch {
throw new ProjectPolicyUnavailableError();
}
if (!snapshot) {
return normalizeSecurityPolicyDecision({
effect: 'deny',
reasons: ['project_not_found'],
fence: null,
});
}
const fence = {
projectVersion: snapshot.project.version,
bindingVersion: snapshot.binding?.version ?? null,
};
if (!snapshot.binding || snapshot.binding.state === 'revoked') {
return normalizeSecurityPolicyDecision({
effect: 'deny',
reasons: ['subject_unbound'],
fence,
});
}
if (
snapshot.project.status === 'archived' &&
!READ_ONLY_PERMISSIONS.has(permission)
) {
return normalizeSecurityPolicyDecision({
effect: 'deny',
reasons: ['project_archived'],
fence,
});
}
if (!roleAllows(snapshot.binding.role!, permission)) {
return normalizeSecurityPolicyDecision({
effect: 'deny',
reasons: ['permission_missing'],
fence,
});
}
if (
subject.type === 'agent' &&
(permission.startsWith('tool.call:') ||
AGENT_APPROVAL_PERMISSIONS.has(permission))
) {
return normalizeSecurityPolicyDecision({
effect: 'require_approval',
reasons: ['agent_action_requires_approval'],
fence,
});
}
return normalizeSecurityPolicyDecision({
effect: 'allow',
reasons: ['role_grant'],
fence,
});
}
async authorize(
principal: Readonly<SecurityPrincipal>,
projectId: string,
permission: ProjectPermission,
): Promise<SecurityPolicyDecision> {
return this.decide({ subject: principal.subject, projectId, permission });
}
}