forked from bai/curriculum-project-hub
feat(hub): built-in PBank 题库 capability + role tools (v0.0.42)
Register pbank as an ADR-0027 external capability with org-scoped username/password envelopes, readiness via /login, and in-process cph_hub MCP tools (search/get/get_many) that materialize sources under the run workspace. Extend the capability secret payload for docmind vs pbank kinds, admin capabilities UI, role tool umbrella `pbank`, and the pbank-problem-report skill. Credentials never reach the Agent process.
This commit is contained in:
@@ -7,8 +7,12 @@
|
||||
*/
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { CapabilityConnectionService } from "../../capability/capabilityConnectionService.js";
|
||||
import {
|
||||
CapabilityConnectionService,
|
||||
type CapabilityCredentialInput,
|
||||
} from "../../capability/capabilityConnectionService.js";
|
||||
import { CapabilityReadinessError, type CapabilityReadinessProbe } from "../../capability/capabilityReadiness.js";
|
||||
import { secretKindForCapability } from "../../capability/types.js";
|
||||
import type { LocalSecretEnvelope } from "../../security/secretEnvelope.js";
|
||||
import { requireOrgRole, type GuardDeps } from "../auth/guards.js";
|
||||
import { handleRouteError } from "../errors.js";
|
||||
@@ -25,11 +29,10 @@ export async function registerCapabilityConnectionRoutes(
|
||||
config: CapabilityConnectionRouteConfig,
|
||||
): Promise<void> {
|
||||
const guardDeps: GuardDeps = { prisma: config.prisma, sessionSecret: config.sessionSecret };
|
||||
const connections = new CapabilityConnectionService(
|
||||
config.prisma,
|
||||
config.secretEnvelope,
|
||||
config.readinessProbe,
|
||||
);
|
||||
const connections =
|
||||
config.readinessProbe === undefined
|
||||
? new CapabilityConnectionService(config.prisma, config.secretEnvelope)
|
||||
: new CapabilityConnectionService(config.prisma, config.secretEnvelope, config.readinessProbe);
|
||||
|
||||
app.get("/api/org/:orgSlug/capability-connections", async (request, reply) => {
|
||||
try {
|
||||
@@ -60,12 +63,12 @@ export async function registerCapabilityConnectionRoutes(
|
||||
const { orgSlug, capabilityId } = request.params as { orgSlug: string; capabilityId: string };
|
||||
const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
|
||||
if (auth === null) return;
|
||||
const body = parseBody(request.body);
|
||||
const credential = parseCredentialBody(capabilityId, request.body);
|
||||
const result = await connections.rotate({
|
||||
organizationId: auth.organization.id,
|
||||
capabilityId,
|
||||
actorUserId: auth.user.id,
|
||||
...body,
|
||||
credential,
|
||||
});
|
||||
request.log.info({
|
||||
organizationId: auth.organization.id,
|
||||
@@ -113,19 +116,57 @@ export async function registerCapabilityConnectionRoutes(
|
||||
});
|
||||
}
|
||||
|
||||
function parseBody(value: unknown): { readonly accessKeyId: string; readonly accessKeySecret: string; readonly endpoint: string } {
|
||||
function parseCredentialBody(capabilityId: string, value: unknown): CapabilityCredentialInput {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||
throw new Error("invalid capability credential body");
|
||||
}
|
||||
const body = value as Record<string, unknown>;
|
||||
for (const name of ["accessKeyId", "accessKeySecret", "endpoint"] as const) {
|
||||
if (typeof body[name] !== "string" || (body[name] as string).trim() === "") {
|
||||
throw new Error(`${name} is required`);
|
||||
}
|
||||
const expectedKind = secretKindForCapability(capabilityId);
|
||||
const kind =
|
||||
body.kind === "docmind" || body.kind === "pbank"
|
||||
? body.kind
|
||||
: expectedKind;
|
||||
|
||||
if (kind !== expectedKind) {
|
||||
throw new Error(`capability ${capabilityId} requires kind=${expectedKind}`);
|
||||
}
|
||||
|
||||
if (kind === "docmind") {
|
||||
return {
|
||||
kind: "docmind",
|
||||
accessKeyId: requireStringField(body, "accessKeyId"),
|
||||
accessKeySecret: requireStringField(body, "accessKeySecret"),
|
||||
endpoint: requireStringField(body, "endpoint"),
|
||||
};
|
||||
}
|
||||
|
||||
const rightsStatus = optionalStringField(body, "rightsStatus");
|
||||
const rightsHolder = optionalStringField(body, "rightsHolder");
|
||||
const rightsScope = optionalStringField(body, "rightsScope");
|
||||
const rightsNote = optionalStringField(body, "rightsNote");
|
||||
return {
|
||||
accessKeyId: body["accessKeyId"] as string,
|
||||
accessKeySecret: body["accessKeySecret"] as string,
|
||||
endpoint: body["endpoint"] as string,
|
||||
kind: "pbank",
|
||||
baseUrl: requireStringField(body, "baseUrl"),
|
||||
username: requireStringField(body, "username"),
|
||||
password: requireStringField(body, "password"),
|
||||
...(rightsStatus !== undefined ? { rightsStatus } : {}),
|
||||
...(rightsHolder !== undefined ? { rightsHolder } : {}),
|
||||
...(rightsScope !== undefined ? { rightsScope } : {}),
|
||||
...(rightsNote !== undefined ? { rightsNote } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function requireStringField(body: Record<string, unknown>, name: string): string {
|
||||
const value = body[name];
|
||||
if (typeof value !== "string" || value.trim() === "") {
|
||||
throw new Error(`${name} is required`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function optionalStringField(body: Record<string, unknown>, name: string): string | undefined {
|
||||
const value = body[name];
|
||||
if (typeof value !== "string") return undefined;
|
||||
const trimmed = value.trim();
|
||||
return trimmed === "" ? undefined : trimmed;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user