import type { CapabilitySpec } from "@umt/contracts"; import { sql } from "@umt/db"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { z } from "zod"; import { createAuthorizer } from "../../src/modules/agent/authorize.ts"; import { createCapabilityRegistry } from "../../src/modules/agent/capability-registry.ts"; import { createAgentModule } from "../../src/modules/agent/index.ts"; import { ensureSetupToken } from "../../src/modules/auth-settings/auth-service.ts"; import { Browser, bearer, createHarness, type Harness, pgAvailable } from "../auth-settings/harness.ts"; import { DEV_KEY, json } from "../helpers.ts"; // 验收 S1 / S4:单次审批的申请与决定只接受控制台管理员会话;服务端授权器的审批分支 // (过期、revision / payload hash 变化、已使用、已撤销、计划不符)全部拒绝,通过一次后不可重放。 const hasPg = await pgAvailable(); const PASSWORD = "correct horse battery"; // organize.execute 的最小规格:只关心授权与账本,处理器计数 const ORGANIZE_SPEC = { id: "organize.execute", version: 1, title: "执行整理", description: "测试用", kind: "execute", scope: "organize.execute", riskLevel: "high", idempotent: true, supportsPreview: false, replay: "never", inputSchema: z.object({ planId: z.string(), approvalId: z.string().optional() }), outputSchema: z.object({ ok: z.literal(true) }), } as unknown as CapabilitySpec; describe.skipIf(!hasPg)("单次审批(真实 PG)", () => { let h: Harness; let admin: Browser; let orgKey: string; let adminKey: string; beforeAll(async () => { h = await createHarness({ extraModules: [createAgentModule()] }); const token = await ensureSetupToken(h.ctx); admin = new Browser(h.app); const r = await admin.req("POST", "/auth/setup", { setupToken: token, username: "admin", password: PASSWORD }); expect(r.status).toBe(200); const mk = async (name: string, scopes: string[]) => (await json(await admin.req("POST", "/api-keys", { name, scopes }))).data.plaintextKey as string; orgKey = await mk("organizer", ["organize.execute"]); adminKey = await mk("ops", ["admin"]); }, 120_000); afterAll(async () => { await h?.close(); }); /** 整理计划(artifact kind=plan):payload hash 即 content_hash,revision 固定 1 */ async function seedPlan(hash = "hash-v1") { const r = await h.db .insertInto("artifact") .values({ kind: "plan", title: "organize plan", content: JSON.stringify({}), content_hash: hash }) .returning("id") .executeTakeFirstOrThrow(); return r.id; } async function seedApproval( planId: string, over: Partial<{ status: string; payload_hash: string; expected_revision: number; expires_at: Date | null }> = {}, ) { const r = await h.db .insertInto("approval") .values({ plan_type: "organize_plan", plan_id: planId, payload_hash: over.payload_hash ?? "hash-v1", expected_revision: over.expected_revision ?? 1, status: (over.status ?? "approved") as never, requested_by: "user:admin", expires_at: over.expires_at === undefined ? new Date(Date.now() + 3600_000) : over.expires_at, }) .returning("id") .executeTakeFirstOrThrow(); return r.id; } describe("审批端点只接受管理员会话(S1)", () => { it("持 organize.execute 的 API Key 建审批 403;自批 403;admin scope 的 Key 同样 403", async () => { const planId = await seedPlan(); const org = bearer(h.app, orgKey); const ops = bearer(h.app, adminKey); expect((await org("POST", "/approvals", { planType: "organize_plan", planId })).status).toBe(403); expect((await ops("POST", "/approvals", { planType: "organize_plan", planId })).status).toBe(403); expect((await bearer(h.app, DEV_KEY)("POST", "/approvals", { planType: "organize_plan", planId })).status).toBe( 403, ); // 管理员会话申请一条 pending 审批;Key 想替自己的计划批准 → 403,状态不变 const created = await admin.req("POST", "/approvals", { planType: "organize_plan", planId }); expect(created.status).toBe(201); const ap = (await json(created)).data; expect(ap).toMatchObject({ status: "pending", payloadHash: "hash-v1", expectedRevision: 1 }); for (const k of [org, ops]) { const r = await k("PATCH", `/approvals/${ap.id}`, { status: "approved", payloadHash: ap.payloadHash }); expect(r.status).toBe(403); } const still = await h.db.selectFrom("approval").select("status").where("id", "=", ap.id).executeTakeFirst(); expect(still?.status).toBe("pending"); // 查看仍按 read scope 开放给 Key expect((await ops("GET", `/approvals/${ap.id}`)).status).toBe(200); }); it("管理员会话可批准;缺 CSRF 403;payloadHash 不一致 409;重复决定 409", async () => { const planId = await seedPlan(); const ap = (await json(await admin.req("POST", "/approvals", { planType: "organize_plan", planId }))).data; expect( ( await admin.req( "PATCH", `/approvals/${ap.id}`, { status: "approved", payloadHash: ap.payloadHash }, { csrf: false }, ) ).status, ).toBe(403); expect( (await admin.req("PATCH", `/approvals/${ap.id}`, { status: "approved", payloadHash: "other" })).status, ).toBe(409); const ok = await admin.req("PATCH", `/approvals/${ap.id}`, { status: "approved", payloadHash: ap.payloadHash }); expect(ok.status).toBe(200); expect((await json(ok)).data).toMatchObject({ status: "approved", decidedBy: expect.stringMatching(/^user:/) }); expect( (await admin.req("PATCH", `/approvals/${ap.id}`, { status: "rejected", payloadHash: ap.payloadHash })).status, ).toBe(409); const audits = await sql<{ n: number }>` select count(*)::int as n from audit_event where action = 'approval.approved' and target_id = ${ap.id}`.execute( h.db, ); expect(audits.rows[0]!.n).toBe(1); }); }); describe("授权器的单次审批分支(S4)", () => { const check = (input: Record) => createAuthorizer({ db: h.db, settings: h.ctx.settings })({ descriptor: ORGANIZE_SPEC, input, ctx: { actorId: "api_key:k1", taskId: "t", scopeIds: [], signal: AbortSignal.timeout(10_000), source: "web" }, }); it("有效审批放行并带回 approvalId", async () => { const planId = await seedPlan(); const approvalId = await seedApproval(planId); expect(await check({ planId, approvalId })).toEqual({ allowed: true, approvalId }); }); it.each([ ["过期", { expires_at: new Date(Date.now() - 1000) }, /已过期/], ["revision 不匹配", { expected_revision: 2 }, /revision 已变化/], ["payload hash 不匹配", { payload_hash: "hash-v0" }, /内容已变化/], ["已使用", { status: "consumed" }, /consumed/], ["已撤销", { status: "revoked" }, /revoked/], ["未批准", { status: "pending" }, /pending/], ["已拒绝", { status: "rejected" }, /rejected/], ] as const)("%s → 拒绝", async (_name, over, reason) => { const planId = await seedPlan(); const approvalId = await seedApproval(planId, over); const d = await check({ planId, approvalId }); expect(d.allowed).toBe(false); expect((d as { reason: string }).reason).toMatch(reason); }); it("审批绑定的计划与本次调用不符 → 拒绝;审批不存在 → 拒绝", async () => { const planA = await seedPlan(); const planB = await seedPlan(); const approvalId = await seedApproval(planA); expect(await check({ planId: planB, approvalId })).toMatchObject({ allowed: false, reason: /不一致/ }); expect(await check({ planId: planA, approvalId: "00000000-0000-4000-8000-000000000000" })).toMatchObject({ allowed: false, reason: /不存在/, }); }); it("审批后改计划 → 被拒 → 重新审批 → 成功且只消费一次,之后不可重放", async () => { const registry = createCapabilityRegistry({ logger: h.ctx.logger, db: h.db, settings: h.ctx.settings }); let calls = 0; registry.register(ORGANIZE_SPEC, async () => { calls++; return { ok: true as const }; }); const invoke = (input: Record, taskId: string) => registry.invoke("organize.execute", input, { actorId: "api_key:k1", taskId, scopeIds: [], signal: AbortSignal.timeout(10_000), source: "web", }); const planId = await seedPlan("hash-a"); const ap1 = (await json(await admin.req("POST", "/approvals", { planType: "organize_plan", planId }))).data; await admin.req("PATCH", `/approvals/${ap1.id}`, { status: "approved", payloadHash: ap1.payloadHash }); // 计划内容变了:旧审批失效 await h.db.updateTable("artifact").set({ content_hash: "hash-b" }).where("id", "=", planId).execute(); await expect(invoke({ planId, approvalId: ap1.id }, "http:r1")).rejects.toThrow(/内容已变化/); expect(calls).toBe(0); const ap2 = (await json(await admin.req("POST", "/approvals", { planType: "organize_plan", planId }))).data; expect(ap2.payloadHash).toBe("hash-b"); await admin.req("PATCH", `/approvals/${ap2.id}`, { status: "approved", payloadHash: ap2.payloadHash }); await expect(invoke({ planId, approvalId: ap2.id }, "http:r2")).resolves.toEqual({ ok: true }); expect(calls).toBe(1); const row = await h.db.selectFrom("approval").select("status").where("id", "=", ap2.id).executeTakeFirst(); expect(row?.status).toBe("consumed"); // 换一个请求重放同一审批 → 拒绝,处理器不再执行 await expect(invoke({ planId, approvalId: ap2.id }, "http:r3")).rejects.toThrow(/consumed/); expect(calls).toBe(1); const ledger = await h.db .selectFrom("capability_invocation") .select(["approval_id", "status"]) .where("approval_id", "=", ap2.id) .execute(); expect(ledger).toEqual([{ approval_id: ap2.id, status: "succeeded" }]); const denied = await sql<{ n: number }>` select count(*)::int as n from audit_event where action = 'capability.denied' and target_id = 'organize.execute'`.execute( h.db, ); expect(denied.rows[0]!.n).toBeGreaterThanOrEqual(2); }); }); });