refactor(contracts,agent): single capability spec and agent run event definitions in contracts
B3: CapabilityDescriptor gains replay; CapabilitySpec + capabilitySpec(id) assemble CAPABILITY_CATALOG + CAPABILITY_IO; drop CapabilityDefinition/CapabilityRisk. agent registry registers contracts CapabilitySpec (kind/allowedKinds instead of its own scope enum; approve also requires authorize). AgentRun port renamed AgentRunHandle. B9: contracts AgentRunEvent/AgentRunEventBody/AgentRunEventRow/AgentUsageTotals with row and SSE mapping helpers; agent onEvent receives AgentRunEvent, delivered serially in seq order, firstSeq continues numbering across processes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
8f59bf3772
commit
db5352eecd
18 files changed
+553
-293
No files matched your search
@@ -1,24 +1,29 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
ApiScope,
|
||||
AUTHORIZED_KINDS,
|
||||
CapabilityKind,
|
||||
CapabilityReplay,
|
||||
type CapabilitySpec,
|
||||
RiskLevel,
|
||||
} from "@umt/contracts";
|
||||
import { z } from "zod";
|
||||
import { summarizeForLog } from "../redact.ts";
|
||||
import { CapabilityError, isCapabilityError } from "./errors.ts";
|
||||
import { capabilityIdToToolName, isCapabilityId } from "./tool-name.ts";
|
||||
import {
|
||||
type AuditSink,
|
||||
type AuthorizationChecker,
|
||||
type AuthorizationDecision,
|
||||
CAPABILITY_SCOPES,
|
||||
type CapabilityAuditRecord,
|
||||
type CapabilityDescriptor,
|
||||
type CapabilityHandler,
|
||||
type CapabilityScope,
|
||||
type HandlerContext,
|
||||
type IdempotencyKeyGenerator,
|
||||
type InvokeContext,
|
||||
import type {
|
||||
AuditSink,
|
||||
AuthorizationChecker,
|
||||
AuthorizationDecision,
|
||||
CapabilityAuditRecord,
|
||||
CapabilityHandler,
|
||||
HandlerContext,
|
||||
IdempotencyKeyGenerator,
|
||||
InvokeContext,
|
||||
} from "./types.ts";
|
||||
|
||||
export interface RegisteredCapability {
|
||||
descriptor: CapabilityDescriptor;
|
||||
descriptor: CapabilitySpec;
|
||||
handler: CapabilityHandler;
|
||||
toolName: string;
|
||||
}
|
||||
@@ -26,11 +31,11 @@ export interface RegisteredCapability {
|
||||
export interface CapabilityRegistryOptions {
|
||||
/**
|
||||
* 授权回调(服务端注入):检查 automation_grant / approval。
|
||||
* 未注入时:read/preview 放行,propose/execute 一律拒绝(安全默认)。
|
||||
* 未注入时:read/preview 放行,propose/approve/execute 一律拒绝(安全默认)。
|
||||
*/
|
||||
authorize?: AuthorizationChecker;
|
||||
/** 需要走授权回调的作用域,默认 propose + execute */
|
||||
authorizeScopes?: readonly CapabilityScope[];
|
||||
/** 需要走授权回调的类别,默认 contracts 的 AUTHORIZED_KINDS(propose/approve/execute) */
|
||||
authorizeKinds?: readonly CapabilityKind[];
|
||||
/** 审计回调;每次调用(含拒绝/校验失败)都会触发一次 */
|
||||
audit?: AuditSink;
|
||||
/** 幂等键生成(服务端基于操作语义生成,不信任模型);默认对 id+version+规范化输入做 sha256 */
|
||||
@@ -64,7 +69,7 @@ export class CapabilityRegistry {
|
||||
}
|
||||
|
||||
register<I extends z.ZodType, O extends z.ZodType>(
|
||||
descriptor: CapabilityDescriptor<I, O>,
|
||||
descriptor: CapabilitySpec<I, O>,
|
||||
handler: CapabilityHandler<I, O>,
|
||||
): this {
|
||||
if (!isCapabilityId(descriptor.id)) throw new Error(`能力 ID 必须是 域.动作 形式: ${descriptor.id}`);
|
||||
@@ -72,14 +77,17 @@ export class CapabilityRegistry {
|
||||
if (!Number.isInteger(descriptor.version) || descriptor.version < 1) {
|
||||
throw new Error(`能力版本必须是正整数: ${descriptor.id}`);
|
||||
}
|
||||
if (!CAPABILITY_SCOPES.includes(descriptor.scope)) throw new Error(`未知作用域: ${descriptor.scope}`);
|
||||
if (!CapabilityKind.safeParse(descriptor.kind).success) throw new Error(`未知能力类别: ${descriptor.kind}`);
|
||||
if (!ApiScope.safeParse(descriptor.scope).success) throw new Error(`未知 API scope: ${descriptor.scope}`);
|
||||
if (!RiskLevel.safeParse(descriptor.riskLevel).success) throw new Error(`未知风险级别: ${descriptor.riskLevel}`);
|
||||
if (!CapabilityReplay.safeParse(descriptor.replay).success) throw new Error(`未知 replay: ${descriptor.replay}`);
|
||||
if (!descriptor.description.trim()) throw new Error(`能力缺少给模型看的说明: ${descriptor.id}`);
|
||||
if (descriptor.scope === "execute" && descriptor.replay === "safe" && !descriptor.idempotent) {
|
||||
if (sideEffecting(descriptor.kind) && descriptor.replay === "safe" && !descriptor.idempotent) {
|
||||
throw new Error(`有副作用且非幂等的能力不能声明 replay:'safe': ${descriptor.id}`);
|
||||
}
|
||||
const toolName = capabilityIdToToolName(descriptor.id);
|
||||
this.entries.set(descriptor.id, {
|
||||
descriptor: descriptor as unknown as CapabilityDescriptor,
|
||||
descriptor: descriptor as unknown as CapabilitySpec,
|
||||
handler: handler as unknown as CapabilityHandler,
|
||||
toolName,
|
||||
});
|
||||
@@ -131,7 +139,7 @@ export class CapabilityRegistry {
|
||||
const record: CapabilityAuditRecord = {
|
||||
capabilityId: descriptor.id,
|
||||
version: descriptor.version,
|
||||
scope: descriptor.scope,
|
||||
kind: descriptor.kind,
|
||||
riskLevel: descriptor.riskLevel,
|
||||
actorId: ctx.actorId,
|
||||
taskId: ctx.taskId,
|
||||
@@ -156,10 +164,10 @@ export class CapabilityRegistry {
|
||||
|
||||
if (ctx.signal.aborted) throw await fail(new CapabilityError("precondition", "调用已取消"), "denied");
|
||||
|
||||
// 1. 作用域:调用方给出的上限
|
||||
if (ctx.allowedScopes && !ctx.allowedScopes.includes(descriptor.scope)) {
|
||||
// 1. 类别:调用方给出的上限
|
||||
if (ctx.allowedKinds && !ctx.allowedKinds.includes(descriptor.kind)) {
|
||||
throw await fail(
|
||||
new CapabilityError("unauthorized", `当前任务不允许 ${descriptor.scope} 级能力: ${descriptor.id}`),
|
||||
new CapabilityError("unauthorized", `当前任务不允许 ${descriptor.kind} 类能力: ${descriptor.id}`),
|
||||
"denied",
|
||||
);
|
||||
}
|
||||
@@ -177,9 +185,9 @@ export class CapabilityRegistry {
|
||||
}
|
||||
|
||||
// 3. 授权(服务端判定,模型声明无效)
|
||||
const authorizeScopes = this.options.authorizeScopes ?? ["propose", "execute"];
|
||||
const authorizeKinds = this.options.authorizeKinds ?? AUTHORIZED_KINDS;
|
||||
let decision: AuthorizationDecision | undefined;
|
||||
if (authorizeScopes.includes(descriptor.scope)) {
|
||||
if (authorizeKinds.includes(descriptor.kind)) {
|
||||
if (!this.options.authorize) {
|
||||
throw await fail(new CapabilityError("unauthorized", `未配置授权校验,拒绝 ${descriptor.id}`), "denied");
|
||||
}
|
||||
@@ -193,9 +201,9 @@ export class CapabilityRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 幂等键(只对有副作用的作用域生成)
|
||||
// 4. 幂等键(只对有副作用的类别生成)
|
||||
let idempotencyKey: string | undefined;
|
||||
if (descriptor.scope === "propose" || descriptor.scope === "execute") {
|
||||
if (sideEffecting(descriptor.kind)) {
|
||||
idempotencyKey = (this.options.idempotencyKey ?? defaultIdempotencyKey)(descriptor, parsed.data, ctx);
|
||||
}
|
||||
|
||||
@@ -233,6 +241,11 @@ export class CapabilityRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
/** propose/approve/execute 会落变更;read/preview 不会 */
|
||||
function sideEffecting(kind: CapabilityKind): boolean {
|
||||
return kind === "propose" || kind === "approve" || kind === "execute";
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
@@ -1,43 +1,18 @@
|
||||
import type { CapabilityContext } from "@umt/contracts";
|
||||
import type { CapabilityContext, CapabilityKind, CapabilitySpec, RiskLevel } from "@umt/contracts";
|
||||
import type { z } from "zod";
|
||||
|
||||
/**
|
||||
* 能力作用域(由弱到强):
|
||||
* - read:只读查询
|
||||
* - preview:生成预览/方案,不落任何变更
|
||||
* - propose:生成并保存变更计划(MutationPlan),等待授权
|
||||
* - execute:产生外部副作用(下载提交、Copy/Move、策略发布…)
|
||||
* 能力描述统一取自 @umt/contracts(B3):
|
||||
* - 运行时规格 `CapabilitySpec`(描述符字段 + 真实 zod schema),首批能力用 `capabilitySpec(id)` 组装;
|
||||
* - 类别 `CapabilityKind`(read/preview/propose/approve/execute),风险 `RiskLevel`,重放 `CapabilityReplay`。
|
||||
* 本包不再定义自己的一套。
|
||||
*/
|
||||
export const CAPABILITY_SCOPES = ["read", "preview", "propose", "execute"] as const;
|
||||
export type CapabilityScope = (typeof CAPABILITY_SCOPES)[number];
|
||||
|
||||
export const CAPABILITY_RISK_LEVELS = ["low", "medium", "high", "critical"] as const;
|
||||
export type CapabilityRiskLevel = (typeof CAPABILITY_RISK_LEVELS)[number];
|
||||
|
||||
/**
|
||||
* 能力描述。description 会原样给模型看,用中文写清用途、边界与返回含义。
|
||||
* replay:崩溃恢复时 harness 是否可以用持久化参数重跑;有副作用的一律 'never'。
|
||||
*/
|
||||
export interface CapabilityDescriptor<I extends z.ZodType = z.ZodType, O extends z.ZodType = z.ZodType> {
|
||||
id: string;
|
||||
version: number;
|
||||
title: string;
|
||||
description: string;
|
||||
inputSchema: I;
|
||||
outputSchema: O;
|
||||
scope: CapabilityScope;
|
||||
riskLevel: CapabilityRiskLevel;
|
||||
idempotent: boolean;
|
||||
supportsPreview: boolean;
|
||||
replay: "safe" | "never";
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用上下文:contracts 的 CapabilityContext(actorId/taskId/scopeIds/signal)加上本包需要的字段。
|
||||
* allowedScopes:本次调用方允许触达的最高作用域集合(例如"先给方案"模式只给 read/preview/propose)。
|
||||
* allowedKinds:本次调用方允许触达的能力类别(例如"先给方案"模式只给 read/preview/propose)。
|
||||
*/
|
||||
export interface InvokeContext extends CapabilityContext {
|
||||
allowedScopes?: readonly CapabilityScope[];
|
||||
allowedKinds?: readonly CapabilityKind[];
|
||||
/** 调用来源,仅用于审计 */
|
||||
source?: "agent" | "web" | "cli" | "mcp" | "task";
|
||||
/** agent 调用时的 tool call id / invocation id,用于审计对账 */
|
||||
@@ -57,7 +32,7 @@ export type CapabilityHandler<I extends z.ZodType = z.ZodType, O extends z.ZodTy
|
||||
) => Promise<z.input<O>> | z.input<O>;
|
||||
|
||||
export interface AuthorizationRequest {
|
||||
descriptor: CapabilityDescriptor;
|
||||
descriptor: CapabilitySpec;
|
||||
input: unknown;
|
||||
ctx: InvokeContext;
|
||||
}
|
||||
@@ -74,15 +49,15 @@ export type AuthorizationChecker = (
|
||||
request: AuthorizationRequest,
|
||||
) => Promise<AuthorizationDecision> | AuthorizationDecision;
|
||||
|
||||
export type IdempotencyKeyGenerator = (descriptor: CapabilityDescriptor, input: unknown, ctx: InvokeContext) => string;
|
||||
export type IdempotencyKeyGenerator = (descriptor: CapabilitySpec, input: unknown, ctx: InvokeContext) => string;
|
||||
|
||||
export type AuditOutcome = "ok" | "denied" | "invalid" | "error";
|
||||
|
||||
export interface CapabilityAuditRecord {
|
||||
capabilityId: string;
|
||||
version: number;
|
||||
scope: CapabilityScope;
|
||||
riskLevel: CapabilityRiskLevel;
|
||||
kind: CapabilityKind;
|
||||
riskLevel: RiskLevel;
|
||||
actorId: string;
|
||||
taskId: string;
|
||||
source?: InvokeContext["source"];
|
||||
|
||||
Reference in new issue
Block a user