mirror of
https://github.com/langgenius/dify.git
synced 2026-09-02 22:36:15 +08:00
3126 lines
106 KiB
TypeScript
3126 lines
106 KiB
TypeScript
import type { OpenAPIHono } from "@hono/zod-openapi";
|
|
import {
|
|
type AuthSubject,
|
|
type KnowledgeSpace,
|
|
type KnowledgeSpaceEmbeddingProfile,
|
|
KnowledgeSpaceEmbeddingProfileSchema,
|
|
type KnowledgeSpaceEmbeddingSelection,
|
|
type KnowledgeSpaceManifest,
|
|
type KnowledgeSpaceRetrievalProfile,
|
|
type KnowledgeSpaceRetrievalProfileInput,
|
|
KnowledgeSpaceRetrievalProfileSchema,
|
|
type KnowledgeSpaceStagedCommit,
|
|
type PlatformAdapter,
|
|
buildKnowledgeSpaceVectorSpaceId,
|
|
createKnowledgeSpaceRetrievalProfile,
|
|
hasRequiredKnowledgeSpaceRetrievalModels,
|
|
updateKnowledgeSpaceEmbeddingProfile,
|
|
validateKnowledgeSpaceRetrievalProfileForMode,
|
|
} from "@knowledge/core";
|
|
import type { ParserAdapter } from "@knowledge/parsers";
|
|
|
|
import type { ArtifactSegmentRepository } from "./artifact-segment-repository";
|
|
import { CapabilityPublicationFencedError } from "./capability-grant-provenance";
|
|
import { issueKnowledgeSpaceDurablePermission } from "./derived-result-authorization";
|
|
import type { DocumentAssetRepository } from "./document-asset-repository";
|
|
import type { KnowledgeGatewayEnv } from "./gateway-openapi-contracts";
|
|
import type {
|
|
IndexProjectionRepository,
|
|
IndexProjectionVersionSummary,
|
|
} from "./index-projection-repository";
|
|
import { type KnowledgeFsPublicFailure, knowledgeFsFailureForCode } from "./knowledge-fs-errors";
|
|
import {
|
|
createKnowledgeFsArtifactSegmentFsckChecker,
|
|
createKnowledgeFsRawObjectFsckChecker,
|
|
createKnowledgeFsReferenceFsckChecker,
|
|
} from "./knowledge-fs-fsck";
|
|
import {
|
|
createKnowledgeFsStagedObjectGcDryRun,
|
|
createKnowledgeFsStagedObjectGcExecutor,
|
|
} from "./knowledge-fs-gc";
|
|
import {
|
|
KnowledgeFsLeaseListLimitExceededError,
|
|
type KnowledgeFsLeaseRepository,
|
|
} from "./knowledge-fs-lease-repository";
|
|
import type { KnowledgeFsOperationLeaseCoordinator } from "./knowledge-fs-operation-leases";
|
|
import type { KnowledgeFsSessionRepository } from "./knowledge-fs-session-repository";
|
|
import type { KnowledgeNodeRepository } from "./knowledge-node-repository";
|
|
import type { KnowledgePathRepository } from "./knowledge-path-repository";
|
|
import {
|
|
KnowledgeSpaceAccessError,
|
|
type KnowledgeSpaceAccessService,
|
|
} from "./knowledge-space-access-control";
|
|
import {
|
|
KnowledgeSpaceAuthorizationError,
|
|
type KnowledgeSpaceAuthorizationGuard,
|
|
type KnowledgeSpaceCallerKind,
|
|
isKnowledgeSpaceExternalCallerKind,
|
|
} from "./knowledge-space-authorization";
|
|
import {
|
|
createKnowledgeSpaceWithOptionalSlug,
|
|
createWithOptionalKnowledgeSpaceSlug,
|
|
} from "./knowledge-space-creation";
|
|
import {
|
|
type KnowledgeSpaceManifestRepository,
|
|
createKnowledgeSpacePendingModelConfiguration,
|
|
ensureKnowledgeSpaceManifest,
|
|
} from "./knowledge-space-manifest-repository";
|
|
import {
|
|
KnowledgeSpaceProfileMigrationConflictError,
|
|
isTerminalKnowledgeSpaceProfileMigrationError,
|
|
} from "./knowledge-space-profile-migration";
|
|
import {
|
|
type KnowledgeSpaceProfileMigrationPrincipal,
|
|
type KnowledgeSpaceProfileMigrationService,
|
|
KnowledgeSpaceProfileMigrationServiceError,
|
|
toPublicKnowledgeSpaceProfileMigration,
|
|
} from "./knowledge-space-profile-migration-service";
|
|
import {
|
|
type KnowledgeSpaceProfilePublicationRepository,
|
|
KnowledgeSpaceProfilePublicationTransitionError,
|
|
} from "./knowledge-space-profile-publication-repository";
|
|
import {
|
|
type KnowledgeSpaceProfileRepository,
|
|
type KnowledgeSpaceProfileRevision,
|
|
KnowledgeSpaceProfileTransitionError,
|
|
KnowledgeSpaceUnpublishedProfileActivationError,
|
|
type KnowledgeSpaceUnpublishedProfileActivationRepository,
|
|
knowledgeSpaceProfileSnapshotDigest,
|
|
} from "./knowledge-space-profile-repository";
|
|
import {
|
|
KnowledgeSpaceProvisioningIdempotencyConflictError,
|
|
KnowledgeSpaceProvisioningIncompleteReplayError,
|
|
type KnowledgeSpaceProvisioningRepository,
|
|
configurationStatusFor,
|
|
} from "./knowledge-space-provisioning-repository";
|
|
import {
|
|
DuplicateKnowledgeSpaceSlugError,
|
|
KnowledgeSpaceCapacityExceededError,
|
|
KnowledgeSpaceListLimitExceededError,
|
|
type KnowledgeSpaceRepository,
|
|
KnowledgeSpaceRevisionConflictError,
|
|
} from "./knowledge-space-repository";
|
|
import {
|
|
createKnowledgeSpaceRoute,
|
|
executeKnowledgeSpaceStagedObjectGcRoute,
|
|
getKnowledgeSpaceFsckRoute,
|
|
getKnowledgeSpaceManifestRoute,
|
|
getKnowledgeSpaceProductSettingsRoute,
|
|
getKnowledgeSpaceRoute,
|
|
getKnowledgeSpaceStagedObjectGcDryRunRoute,
|
|
getKnowledgeSpaceStatsRoute,
|
|
getKnowledgeSpaceStatusRoute,
|
|
listKnowledgeSpaceActiveLeasesRoute,
|
|
listKnowledgeSpaceStagedCommitsRoute,
|
|
listKnowledgeSpacesRoute,
|
|
updateKnowledgeSpaceEmbeddingProfileRoute,
|
|
updateKnowledgeSpaceProductSettingsRoute,
|
|
updateKnowledgeSpaceRetrievalProfileRoute,
|
|
updateKnowledgeSpaceRoute,
|
|
} from "./knowledge-space-routes";
|
|
import {
|
|
type ModelCapabilityPreflight,
|
|
ModelCapabilityPreflightError,
|
|
type ModelCapabilitySnapshot,
|
|
ModelCapabilitySnapshotSchema,
|
|
} from "./model-capability-preflight";
|
|
import type { ParseArtifactRepository } from "./parse-artifact-repository";
|
|
import type { ProjectionSetPublicationRepository } from "./projection-publication-repository";
|
|
import type { PublishedKnowledgeSpaceRuntimeSnapshotResolver } from "./published-knowledge-space-runtime-snapshot";
|
|
import { PublishedProjectionReadUnavailableError } from "./published-projection-read-snapshot";
|
|
import {
|
|
StagedCommitListLimitExceededError,
|
|
type StagedCommitRepository,
|
|
} from "./staged-commit-repository";
|
|
|
|
export interface RegisterKnowledgeSpaceHandlersOptions {
|
|
readonly access: KnowledgeSpaceAccessService;
|
|
readonly adapter: PlatformAdapter;
|
|
readonly app: OpenAPIHono<KnowledgeGatewayEnv>;
|
|
readonly artifactSegments: ArtifactSegmentRepository;
|
|
readonly authorization?: KnowledgeSpaceAuthorizationGuard | undefined;
|
|
readonly assets: DocumentAssetRepository;
|
|
readonly generateGcDryRunId: () => string;
|
|
readonly generateManifestId: () => string;
|
|
readonly generateProvisioningKey: () => string;
|
|
readonly knowledgeFsLeases: KnowledgeFsLeaseRepository;
|
|
readonly knowledgeFsSessions: KnowledgeFsSessionRepository;
|
|
readonly manifests: KnowledgeSpaceManifestRepository;
|
|
readonly profileMigrations?: KnowledgeSpaceProfileMigrationService | undefined;
|
|
readonly profilePublicationBindings?:
|
|
| (Pick<KnowledgeSpaceProfilePublicationRepository, "bindCurrentPublished"> &
|
|
Partial<Pick<KnowledgeSpaceProfilePublicationRepository, "requireActivatedBinding">>)
|
|
| undefined;
|
|
readonly profiles?: KnowledgeSpaceProfileRepository | undefined;
|
|
readonly provisioning?: KnowledgeSpaceProvisioningRepository | undefined;
|
|
readonly unpublishedProfileActivations?:
|
|
| KnowledgeSpaceUnpublishedProfileActivationRepository
|
|
| undefined;
|
|
readonly publishedPublications?:
|
|
| Pick<ProjectionSetPublicationRepository, "getPublished">
|
|
| undefined;
|
|
readonly runtimeSnapshotResolver?: PublishedKnowledgeSpaceRuntimeSnapshotResolver | undefined;
|
|
readonly modelCapabilityPreflight?: ModelCapabilityPreflight | undefined;
|
|
readonly nodes: KnowledgeNodeRepository;
|
|
readonly now: () => string;
|
|
readonly operationLeases?: KnowledgeFsOperationLeaseCoordinator | undefined;
|
|
readonly parseArtifacts: ParseArtifactRepository;
|
|
readonly paths: KnowledgePathRepository;
|
|
readonly parser: ParserAdapter;
|
|
readonly projections: IndexProjectionRepository;
|
|
readonly spaces: KnowledgeSpaceRepository;
|
|
readonly stagedCommits: StagedCommitRepository;
|
|
}
|
|
|
|
export function registerKnowledgeSpaceHandlers({
|
|
access,
|
|
adapter,
|
|
app,
|
|
artifactSegments,
|
|
authorization,
|
|
assets,
|
|
generateGcDryRunId,
|
|
generateManifestId,
|
|
generateProvisioningKey,
|
|
knowledgeFsLeases,
|
|
knowledgeFsSessions,
|
|
manifests,
|
|
profileMigrations,
|
|
profilePublicationBindings,
|
|
profiles,
|
|
provisioning,
|
|
unpublishedProfileActivations,
|
|
publishedPublications,
|
|
runtimeSnapshotResolver,
|
|
modelCapabilityPreflight,
|
|
nodes,
|
|
now,
|
|
operationLeases,
|
|
parseArtifacts,
|
|
paths,
|
|
parser,
|
|
projections,
|
|
spaces,
|
|
stagedCommits,
|
|
}: RegisterKnowledgeSpaceHandlersOptions): void {
|
|
app.openapi(createKnowledgeSpaceRoute, async (context) => {
|
|
try {
|
|
const subject = context.get("subject");
|
|
const { embeddingProfile, idempotencyKey, retrievalProfile, ...createInput } =
|
|
context.req.valid("json");
|
|
const profileValidationError = retrievalProfile
|
|
? validateKnowledgeSpaceRetrievalProfileForMode(
|
|
retrievalProfile,
|
|
retrievalProfile.defaultMode,
|
|
)
|
|
: undefined;
|
|
if (profileValidationError) {
|
|
return context.json(
|
|
{
|
|
code: profileValidationError.code,
|
|
error: profileValidationError.message,
|
|
mode: profileValidationError.mode,
|
|
},
|
|
400,
|
|
);
|
|
}
|
|
const selectedEmbedding = embeddingProfile;
|
|
if (retrievalProfile && !selectedEmbedding) {
|
|
throw new ModelCapabilityPreflightError(
|
|
"MODEL_CAPABILITY_MISMATCH",
|
|
"All retrieval modes require an embedding model for this knowledge space",
|
|
);
|
|
}
|
|
const pendingModelConfiguration =
|
|
selectedEmbedding || retrievalProfile
|
|
? createKnowledgeSpacePendingModelConfiguration({
|
|
...(selectedEmbedding ? { embeddingSelection: selectedEmbedding } : {}),
|
|
...(retrievalProfile ? { retrievalProfile } : {}),
|
|
})
|
|
: undefined;
|
|
|
|
if (provisioning) {
|
|
const operationIdempotencyKey = idempotencyKey ?? generateProvisioningKey();
|
|
const result = await createWithOptionalKnowledgeSpaceSlug(
|
|
{ ...createInput, tenantId: subject.tenantId },
|
|
({ description, iconRef, name, slug, tenantId }) =>
|
|
provisioning.provision({
|
|
createdBySubjectId: subject.subjectId,
|
|
...(description === undefined ? {} : { description }),
|
|
...(iconRef === undefined ? {} : { iconRef }),
|
|
idempotencyKey: operationIdempotencyKey,
|
|
name,
|
|
...(pendingModelConfiguration ? { pendingModelConfiguration } : {}),
|
|
slug,
|
|
slugSource: createInput.slug === undefined ? "generated" : "explicit",
|
|
tenantId,
|
|
}),
|
|
);
|
|
return context.json(
|
|
{ ...result.space, configurationStatus: result.configurationStatus },
|
|
201,
|
|
);
|
|
}
|
|
|
|
const space = await createKnowledgeSpaceWithOptionalSlug(spaces, {
|
|
...createInput,
|
|
tenantId: subject.tenantId,
|
|
});
|
|
try {
|
|
await ensureKnowledgeSpaceManifest({
|
|
generateId: generateManifestId,
|
|
manifests,
|
|
now,
|
|
...(pendingModelConfiguration ? { pendingModelConfiguration } : {}),
|
|
space,
|
|
});
|
|
// Access initialization is the final provisioning write. The repository performs its
|
|
// member/policy/API-access writes atomically; if it fails, remove the already-created
|
|
// manifest and space so no caller can observe a knowledge space without an owner.
|
|
await access.initialize({
|
|
knowledgeSpaceId: space.id,
|
|
ownerSubjectId: subject.subjectId,
|
|
tenantId: subject.tenantId,
|
|
});
|
|
} catch (error) {
|
|
await access
|
|
.deleteAggregate({ knowledgeSpaceId: space.id, tenantId: subject.tenantId })
|
|
.catch(() => false);
|
|
await manifests
|
|
.delete?.({ knowledgeSpaceId: space.id, tenantId: subject.tenantId })
|
|
.catch(() => false);
|
|
// The repositories do not currently share a transaction. Compensate so a failed manifest
|
|
// or owner-policy write cannot strand an inaccessible slug.
|
|
await spaces
|
|
.rollbackCreate({
|
|
expectedRevision: space.revision,
|
|
expectedSlug: space.slug,
|
|
id: space.id,
|
|
tenantId: subject.tenantId,
|
|
})
|
|
.catch(() => false);
|
|
throw error;
|
|
}
|
|
|
|
return context.json(
|
|
{
|
|
...space,
|
|
configurationStatus: configurationStatusFor(
|
|
undefined,
|
|
undefined,
|
|
pendingModelConfiguration,
|
|
),
|
|
},
|
|
201,
|
|
);
|
|
} catch (error) {
|
|
if (error instanceof DuplicateKnowledgeSpaceSlugError) {
|
|
return context.json({ error: error.message }, 409);
|
|
}
|
|
|
|
if (error instanceof KnowledgeSpaceProvisioningIdempotencyConflictError) {
|
|
return context.json({ code: error.code, error: error.message }, 409);
|
|
}
|
|
|
|
if (error instanceof KnowledgeSpaceCapacityExceededError) {
|
|
return context.json({ error: error.message }, 429);
|
|
}
|
|
|
|
if (error instanceof ModelCapabilityPreflightError) {
|
|
return context.json(
|
|
{ code: error.code, error: error.message },
|
|
error.retryable ? 503 : 422,
|
|
);
|
|
}
|
|
|
|
if (error instanceof KnowledgeSpaceProvisioningIncompleteReplayError) {
|
|
return context.json({ code: error.code, error: error.message }, 503);
|
|
}
|
|
|
|
/* v8 ignore next 2 -- unexpected knowledge-space create failures should escape to Hono. */
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
app.openapi(listKnowledgeSpacesRoute, async (context) => {
|
|
try {
|
|
const subject = context.get("subject");
|
|
const query = context.req.valid("query");
|
|
const callerKind = context.get("callerKind") ?? "interactive";
|
|
const capabilityGrant = context.get("capabilityV2Grant");
|
|
if (
|
|
capabilityGrant &&
|
|
(capabilityGrant.action !== "knowledge_spaces.list" ||
|
|
capabilityGrant.namespaceId !== subject.tenantId ||
|
|
capabilityGrant.subject !== subject.subjectId ||
|
|
capabilityGrant.resource.type !== "namespace" ||
|
|
capabilityGrant.resource.id !== subject.tenantId ||
|
|
capabilityGrant.resource.parent_id !== null)
|
|
) {
|
|
return context.json({ error: "Forbidden" }, 403);
|
|
}
|
|
const result = capabilityGrant
|
|
? await spaces.list({ ...query, tenantId: subject.tenantId })
|
|
: authorization && spaces.listAuthorized
|
|
? await spaces.listAuthorized({
|
|
...query,
|
|
requireApiAccess: isKnowledgeSpaceExternalCallerKind(callerKind),
|
|
subjectId: subject.subjectId,
|
|
tenantId: subject.tenantId,
|
|
})
|
|
: authorization
|
|
? await listAuthorizedSpacesFallback({
|
|
authorization,
|
|
callerKind,
|
|
cursor: query.cursor,
|
|
limit: query.limit,
|
|
spaces,
|
|
subject,
|
|
})
|
|
: await spaces.list({ ...query, tenantId: subject.tenantId });
|
|
|
|
return context.json(result, 200);
|
|
} catch (error) {
|
|
if (error instanceof KnowledgeSpaceListLimitExceededError) {
|
|
return context.json({ error: error.message }, 400);
|
|
}
|
|
|
|
/* v8 ignore next 2 -- unexpected knowledge-space list failures should escape to Hono. */
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
app.openapi(getKnowledgeSpaceRoute, async (context) => {
|
|
const subject = context.get("subject");
|
|
const space = await spaces.get({
|
|
id: context.req.valid("param").id,
|
|
tenantId: subject.tenantId,
|
|
});
|
|
|
|
if (!space) {
|
|
return context.json({ error: "Knowledge space not found" }, 404);
|
|
}
|
|
|
|
await ensureKnowledgeSpaceManifest({
|
|
generateId: generateManifestId,
|
|
manifests,
|
|
now,
|
|
space,
|
|
});
|
|
|
|
return context.json(space, 200);
|
|
});
|
|
|
|
app.openapi(getKnowledgeSpaceManifestRoute, async (context) => {
|
|
const subject = context.get("subject");
|
|
const space = await spaces.get({
|
|
id: context.req.valid("param").id,
|
|
tenantId: subject.tenantId,
|
|
});
|
|
|
|
if (!space) {
|
|
return context.json({ error: "Knowledge space not found" }, 404);
|
|
}
|
|
|
|
const manifest = await ensureKnowledgeSpaceManifest({
|
|
generateId: generateManifestId,
|
|
manifests,
|
|
now,
|
|
space,
|
|
});
|
|
|
|
return context.json(manifest, 200);
|
|
});
|
|
|
|
app.openapi(getKnowledgeSpaceProductSettingsRoute, async (context) => {
|
|
const subject = context.get("subject");
|
|
const knowledgeSpaceId = context.req.valid("param").id;
|
|
const space = await spaces.get({ id: knowledgeSpaceId, tenantId: subject.tenantId });
|
|
|
|
if (!space) {
|
|
return context.json({ error: "Knowledge space not found" }, 404);
|
|
}
|
|
|
|
const manifest = await ensureKnowledgeSpaceManifest({
|
|
generateId: generateManifestId,
|
|
manifests,
|
|
now,
|
|
space,
|
|
});
|
|
return context.json(
|
|
await resolveProductSettings({
|
|
knowledgeSpaceId,
|
|
manifest,
|
|
profilePublicationBindings,
|
|
profiles,
|
|
publishedPublications,
|
|
runtimeSnapshotResolver,
|
|
tenantId: subject.tenantId,
|
|
}),
|
|
200,
|
|
);
|
|
});
|
|
|
|
app.openapi(updateKnowledgeSpaceProductSettingsRoute, async (context) => {
|
|
const subject = context.get("subject");
|
|
const knowledgeSpaceId = context.req.valid("param").id;
|
|
const body = context.req.valid("json");
|
|
const space = await spaces.get({ id: knowledgeSpaceId, tenantId: subject.tenantId });
|
|
|
|
if (!space) {
|
|
return context.json({ error: "Knowledge space not found" }, 404);
|
|
}
|
|
|
|
const manifest = await ensureKnowledgeSpaceManifest({
|
|
generateId: generateManifestId,
|
|
manifests,
|
|
now,
|
|
space,
|
|
});
|
|
if (manifest.manifestVersion !== body.expectedRevision) {
|
|
return context.json(
|
|
{
|
|
code: "PRODUCT_SETTINGS_REVISION_CONFLICT",
|
|
error: `Knowledge space product settings revision conflict: expected=${body.expectedRevision} actual=${manifest.manifestVersion}`,
|
|
},
|
|
409,
|
|
);
|
|
}
|
|
if (manifest.embeddingProfile || manifest.retrievalProfile) {
|
|
return context.json(
|
|
{
|
|
code: "PRODUCT_SETTINGS_PROFILE_MIGRATION_REQUIRED",
|
|
error: "Active profile changes require the dedicated profile migration workflow",
|
|
},
|
|
409,
|
|
);
|
|
}
|
|
|
|
const current = manifest.pendingModelConfiguration;
|
|
const embeddingSelection = body.embedding ?? current?.embeddingSelection;
|
|
const retrievalProfile = body.retrieval ?? current?.retrievalProfile;
|
|
const profileValidationError = retrievalProfile
|
|
? validateKnowledgeSpaceRetrievalProfileForMode(
|
|
retrievalProfile,
|
|
retrievalProfile.defaultMode,
|
|
)
|
|
: undefined;
|
|
if (profileValidationError) {
|
|
return context.json(
|
|
{
|
|
code: profileValidationError.code,
|
|
error: profileValidationError.message,
|
|
mode: profileValidationError.mode,
|
|
},
|
|
400,
|
|
);
|
|
}
|
|
if (retrievalProfile && !embeddingSelection) {
|
|
return context.json(
|
|
{
|
|
code: "EMBEDDING_MODEL_REQUIRED",
|
|
error: "All retrieval modes require an embedding model for this knowledge space",
|
|
},
|
|
409,
|
|
);
|
|
}
|
|
const pendingModelConfiguration = createKnowledgeSpacePendingModelConfiguration({
|
|
...(embeddingSelection ? { embeddingSelection } : {}),
|
|
...(retrievalProfile ? { retrievalProfile } : {}),
|
|
revision: (current?.revision ?? 0) + 1,
|
|
});
|
|
const updatedAt = now();
|
|
const updated = await manifests.update({
|
|
expectedManifestVersion: manifest.manifestVersion,
|
|
knowledgeSpaceId,
|
|
patch: {
|
|
manifestVersion: manifest.manifestVersion + 1,
|
|
pendingModelConfiguration,
|
|
updatedAt,
|
|
},
|
|
tenantId: subject.tenantId,
|
|
});
|
|
if (!updated) {
|
|
return context.json(
|
|
{
|
|
code: "PRODUCT_SETTINGS_REVISION_CONFLICT",
|
|
error: "Knowledge space product settings changed concurrently",
|
|
},
|
|
409,
|
|
);
|
|
}
|
|
return context.json(
|
|
await resolveProductSettings({
|
|
knowledgeSpaceId,
|
|
manifest: updated,
|
|
profilePublicationBindings,
|
|
profiles,
|
|
publishedPublications,
|
|
runtimeSnapshotResolver,
|
|
tenantId: subject.tenantId,
|
|
}),
|
|
200,
|
|
);
|
|
});
|
|
|
|
app.openapi(updateKnowledgeSpaceEmbeddingProfileRoute, async (context) => {
|
|
const subject = context.get("subject");
|
|
const knowledgeSpaceId = context.req.valid("param").id;
|
|
const space = await spaces.get({ id: knowledgeSpaceId, tenantId: subject.tenantId });
|
|
|
|
if (!space) {
|
|
return context.json({ error: "Knowledge space not found" }, 404);
|
|
}
|
|
|
|
const selection = context.req.valid("json");
|
|
let manifest = await manifests.get({
|
|
knowledgeSpaceId,
|
|
tenantId: subject.tenantId,
|
|
});
|
|
const current = manifest?.embeddingProfile;
|
|
const pendingEmbeddingSelection = manifest?.pendingModelConfiguration?.embeddingSelection;
|
|
const effectiveSelection = current ?? pendingEmbeddingSelection;
|
|
const selectionChanged =
|
|
!effectiveSelection ||
|
|
effectiveSelection.pluginId !== selection.pluginId ||
|
|
effectiveSelection.provider !== selection.provider ||
|
|
effectiveSelection.model !== selection.model;
|
|
const hasPublishedProfileTuple = profileMigrations
|
|
? await profileMigrations.requiresMigration({
|
|
knowledgeSpaceId,
|
|
tenantId: subject.tenantId,
|
|
})
|
|
: publishedPublications
|
|
? (await publishedPublications.getPublished({
|
|
knowledgeSpaceId,
|
|
tenantId: subject.tenantId,
|
|
})) !== null
|
|
: false;
|
|
manifest ??= await ensureKnowledgeSpaceManifest({
|
|
generateId: generateManifestId,
|
|
manifests,
|
|
now,
|
|
space,
|
|
});
|
|
const canUpdatePendingConfiguration =
|
|
!hasPublishedProfileTuple &&
|
|
manifest.embeddingProfile === undefined &&
|
|
manifest.retrievalProfile === undefined;
|
|
if (
|
|
canUpdatePendingConfiguration &&
|
|
!selectionChanged &&
|
|
manifest.pendingModelConfiguration?.state !== "validation-failed"
|
|
) {
|
|
const pendingModelConfiguration = manifest.pendingModelConfiguration;
|
|
if (pendingModelConfiguration) {
|
|
return context.json(
|
|
{
|
|
configurationStatus: pendingModelConfiguration.retrievalProfile
|
|
? ("pending-validation" as const)
|
|
: ("setup-required" as const),
|
|
digest: pendingModelConfiguration.digest,
|
|
operation: "initial-validation-pending" as const,
|
|
revision: pendingModelConfiguration.revision,
|
|
},
|
|
202,
|
|
);
|
|
}
|
|
}
|
|
if (
|
|
canUpdatePendingConfiguration &&
|
|
(selectionChanged || manifest.pendingModelConfiguration?.state === "validation-failed")
|
|
) {
|
|
if (!authorization) {
|
|
return context.json(
|
|
{
|
|
code: "PENDING_MODEL_CONFIGURATION_UNAVAILABLE",
|
|
error: "Pending model configuration updates are unavailable",
|
|
},
|
|
503,
|
|
);
|
|
}
|
|
const pendingModelConfiguration = createKnowledgeSpacePendingModelConfiguration({
|
|
embeddingSelection: selection,
|
|
...(manifest.pendingModelConfiguration?.retrievalProfile
|
|
? { retrievalProfile: manifest.pendingModelConfiguration.retrievalProfile }
|
|
: {}),
|
|
revision: (manifest.pendingModelConfiguration?.revision ?? 0) + 1,
|
|
});
|
|
const mutationTimestamp = now();
|
|
const authenticatedApiKey = context.get("authenticatedApiKey");
|
|
let permissionSnapshot: Awaited<ReturnType<typeof issueKnowledgeSpaceDurablePermission>>;
|
|
try {
|
|
permissionSnapshot = await issueKnowledgeSpaceDurablePermission({
|
|
access,
|
|
...(authenticatedApiKey ? { apiKey: authenticatedApiKey } : {}),
|
|
authorization,
|
|
callerKind: context.get("callerKind") ?? "interactive",
|
|
expiresAt: new Date(Date.parse(mutationTimestamp) + 10 * 60_000).toISOString(),
|
|
knowledgeSpaceId,
|
|
requiredAccess: "admin",
|
|
subject,
|
|
});
|
|
} catch (error) {
|
|
if (
|
|
error instanceof KnowledgeSpaceAuthorizationError ||
|
|
error instanceof KnowledgeSpaceAccessError
|
|
) {
|
|
return context.json({ code: error.code, error: "Knowledge space access denied" }, 403);
|
|
}
|
|
throw error;
|
|
}
|
|
const updated = await manifests.update({
|
|
expectedManifestVersion: manifest.manifestVersion,
|
|
knowledgeSpaceId,
|
|
patch: {
|
|
manifestVersion: manifest.manifestVersion + 1,
|
|
pendingModelConfiguration,
|
|
updatedAt: mutationTimestamp,
|
|
},
|
|
permission: {
|
|
fence: {
|
|
accessChannel: permissionSnapshot.accessChannel,
|
|
knowledgeSpaceId,
|
|
permissionSnapshotId: permissionSnapshot.id,
|
|
permissionSnapshotRevision: permissionSnapshot.revision,
|
|
requestedBySubjectId: subject.subjectId,
|
|
tenantId: subject.tenantId,
|
|
},
|
|
now: mutationTimestamp,
|
|
requiredAccess: "admin",
|
|
},
|
|
tenantId: subject.tenantId,
|
|
});
|
|
if (!updated) {
|
|
return context.json(
|
|
{
|
|
code: "PENDING_MODEL_CONFIGURATION_CONFLICT",
|
|
error: "Pending model configuration changed concurrently",
|
|
},
|
|
409,
|
|
);
|
|
}
|
|
return context.json(
|
|
{
|
|
configurationStatus: pendingModelConfiguration.retrievalProfile
|
|
? ("pending-validation" as const)
|
|
: ("setup-required" as const),
|
|
digest: pendingModelConfiguration.digest,
|
|
operation: "initial-validation-pending" as const,
|
|
revision: pendingModelConfiguration.revision,
|
|
},
|
|
202,
|
|
);
|
|
}
|
|
const publishedMigrationRequired =
|
|
selectionChanged &&
|
|
hasPublishedProfileTuple &&
|
|
profileMigrations !== undefined &&
|
|
profiles !== undefined;
|
|
|
|
if (selectionChanged && hasPublishedProfileTuple && !publishedMigrationRequired) {
|
|
return context.json(
|
|
{
|
|
code: "PROFILE_MIGRATION_UNAVAILABLE",
|
|
error: "Published embedding profile changes require the durable migration workflow",
|
|
},
|
|
503,
|
|
);
|
|
}
|
|
|
|
if (selectionChanged && !publishedMigrationRequired) {
|
|
if (manifest?.embeddingProfileFrozenAt) {
|
|
return context.json({ error: "Embedding profile change requires reindex workflow" }, 409);
|
|
}
|
|
|
|
const [usage, nodePage] = await Promise.all([
|
|
assets.getStorageUsage({ knowledgeSpaceId }),
|
|
nodes.listBySpace({ knowledgeSpaceId, limit: 1 }),
|
|
]);
|
|
|
|
if (usage.documentCount > 0 || nodePage.items.length > 0) {
|
|
return context.json({ error: "Embedding profile change requires reindex workflow" }, 409);
|
|
}
|
|
}
|
|
|
|
let capabilitySnapshot: ModelCapabilitySnapshot;
|
|
try {
|
|
if (!modelCapabilityPreflight) {
|
|
throw new ModelCapabilityPreflightError(
|
|
"MODEL_PREFLIGHT_UNAVAILABLE",
|
|
"Model capability preflight is unavailable",
|
|
{ retryable: true },
|
|
);
|
|
}
|
|
capabilitySnapshot = await modelCapabilityPreflight.verify({
|
|
kind: "embedding",
|
|
selection,
|
|
tenantId: subject.tenantId,
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof ModelCapabilityPreflightError) {
|
|
return context.json(
|
|
{ code: error.code, error: error.message },
|
|
error.retryable ? 503 : 422,
|
|
);
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
const previewBase = await updateKnowledgeSpaceEmbeddingProfile(
|
|
current,
|
|
selection,
|
|
embeddingVectorSpaceIdentityFromSnapshot(capabilitySnapshot),
|
|
);
|
|
const previewProfile = {
|
|
...previewBase,
|
|
dimension: capabilitySnapshot.dimension,
|
|
};
|
|
if (
|
|
hasPublishedProfileTuple &&
|
|
current &&
|
|
knowledgeSpaceProfileSnapshotDigest(previewProfile) ===
|
|
knowledgeSpaceProfileSnapshotDigest(current)
|
|
) {
|
|
return context.json(previewProfile, 200);
|
|
}
|
|
|
|
if (profileMigrations && profiles) {
|
|
const published = hasPublishedProfileTuple;
|
|
if (published) {
|
|
try {
|
|
await ensureLegacyPublishedProfileTuple({
|
|
createdBySubjectId: subject.subjectId,
|
|
knowledgeSpaceId,
|
|
manifest,
|
|
modelCapabilityPreflight,
|
|
now,
|
|
profilePublicationBindings,
|
|
profiles,
|
|
tenantId: subject.tenantId,
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof ModelCapabilityPreflightError) {
|
|
return context.json(
|
|
{ code: error.code, error: error.message },
|
|
error.retryable ? 503 : 422,
|
|
);
|
|
}
|
|
if (error instanceof LegacyProfileBootstrapError) {
|
|
return context.json({ code: error.code, error: error.message }, error.status);
|
|
}
|
|
if (error instanceof SettingsProfileCandidateConflictError) {
|
|
return context.json({ code: error.code, error: error.message }, 409);
|
|
}
|
|
return context.json(
|
|
{
|
|
code: "PROFILE_PUBLICATION_BOOTSTRAP_FAILED",
|
|
error: "Published projection tuple could not be bound to verified active profiles",
|
|
},
|
|
503,
|
|
);
|
|
}
|
|
const activeHead = await profiles.getHead({
|
|
kind: "embedding",
|
|
knowledgeSpaceId,
|
|
tenantId: subject.tenantId,
|
|
});
|
|
const candidateBase = await updateKnowledgeSpaceEmbeddingProfile(
|
|
activeHead ? (activeHead.profile.snapshot as typeof current) : current,
|
|
selection,
|
|
embeddingVectorSpaceIdentityFromSnapshot(capabilitySnapshot),
|
|
);
|
|
if (!candidateBase) {
|
|
throw new Error("Embedding candidate profile could not be created");
|
|
}
|
|
const candidateSnapshot = {
|
|
...candidateBase,
|
|
dimension: capabilitySnapshot.dimension,
|
|
};
|
|
if (
|
|
activeHead &&
|
|
knowledgeSpaceProfileSnapshotDigest(candidateSnapshot) ===
|
|
activeHead.profile.snapshotDigest
|
|
) {
|
|
return context.json(candidateSnapshot, 200);
|
|
}
|
|
const createdAt = now();
|
|
let candidate: KnowledgeSpaceProfileRevision;
|
|
try {
|
|
candidate = await getOrCreateSettingsProfileCandidate(profiles, {
|
|
capabilitySnapshot,
|
|
createdBySubjectId: subject.subjectId,
|
|
kind: "embedding",
|
|
knowledgeSpaceId,
|
|
now: createdAt,
|
|
snapshot: candidateSnapshot,
|
|
tenantId: subject.tenantId,
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof SettingsProfileCandidateConflictError) {
|
|
return context.json({ code: error.code, error: error.message }, 409);
|
|
}
|
|
throw error;
|
|
}
|
|
const authenticatedApiKey = context.get("authenticatedApiKey");
|
|
const capabilityGrantId = context.get("capabilityV2Grant")?.grantId;
|
|
const migration = await profileMigrations.request({
|
|
...(authenticatedApiKey ? { apiKey: authenticatedApiKey } : {}),
|
|
...(capabilityGrantId ? { capabilityGrantId } : {}),
|
|
callerKind: context.get("callerKind") ?? "interactive",
|
|
candidateRevision: candidate.revision,
|
|
changedKind: "embedding",
|
|
idempotencyKey: `settings-embedding-${candidate.snapshotDigest}`,
|
|
knowledgeSpaceId,
|
|
subject,
|
|
});
|
|
return context.json(toPublicKnowledgeSpaceProfileMigration(migration), 202);
|
|
}
|
|
}
|
|
if (hasPublishedProfileTuple) {
|
|
return context.json(
|
|
{
|
|
code: "PROFILE_MIGRATION_UNAVAILABLE",
|
|
error: "Published embedding profile changes require the durable migration workflow",
|
|
},
|
|
503,
|
|
);
|
|
}
|
|
|
|
if (!authorization || !unpublishedProfileActivations) {
|
|
return context.json(
|
|
{
|
|
code: "UNPUBLISHED_PROFILE_ACTIVATION_UNAVAILABLE",
|
|
error: "Atomic unpublished profile activation is unavailable",
|
|
},
|
|
503,
|
|
);
|
|
}
|
|
manifest ??= await ensureKnowledgeSpaceManifest({
|
|
generateId: generateManifestId,
|
|
manifests,
|
|
now,
|
|
space,
|
|
});
|
|
|
|
try {
|
|
const mutationTimestamp = now();
|
|
const authenticatedApiKey = context.get("authenticatedApiKey");
|
|
const permissionSnapshot = await issueKnowledgeSpaceDurablePermission({
|
|
access,
|
|
...(authenticatedApiKey ? { apiKey: authenticatedApiKey } : {}),
|
|
authorization,
|
|
callerKind: context.get("callerKind") ?? "interactive",
|
|
expiresAt: new Date(Date.parse(mutationTimestamp) + 10 * 60_000).toISOString(),
|
|
knowledgeSpaceId,
|
|
requiredAccess: "admin",
|
|
subject,
|
|
});
|
|
await unpublishedProfileActivations.activate({
|
|
capabilitySnapshot,
|
|
createdBySubjectId: subject.subjectId,
|
|
expectedManifestProfileRevision: current?.revision ?? 0,
|
|
expectedManifestVersion: manifest.manifestVersion,
|
|
kind: "embedding",
|
|
knowledgeSpaceId,
|
|
now: mutationTimestamp,
|
|
permission: {
|
|
accessChannel: permissionSnapshot.accessChannel,
|
|
knowledgeSpaceId,
|
|
permissionSnapshotId: permissionSnapshot.id,
|
|
permissionSnapshotRevision: permissionSnapshot.revision,
|
|
requestedBySubjectId: subject.subjectId,
|
|
tenantId: subject.tenantId,
|
|
},
|
|
snapshot: previewProfile,
|
|
tenantId: subject.tenantId,
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof KnowledgeSpaceUnpublishedProfileActivationError) {
|
|
return context.json({ code: error.code, error: error.message }, 409);
|
|
}
|
|
if (error instanceof KnowledgeSpaceProfileTransitionError) {
|
|
return context.json({ code: error.code, error: error.message }, 409);
|
|
}
|
|
if (
|
|
error instanceof KnowledgeSpaceAuthorizationError ||
|
|
error instanceof KnowledgeSpaceAccessError
|
|
) {
|
|
return context.json({ code: error.code, error: "Knowledge space access denied" }, 403);
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
|
|
return context.json(previewProfile, 200);
|
|
});
|
|
|
|
app.openapi(updateKnowledgeSpaceRetrievalProfileRoute, async (context) => {
|
|
const subject = context.get("subject");
|
|
const knowledgeSpaceId = context.req.valid("param").id;
|
|
const body = context.req.valid("json");
|
|
const space = await spaces.get({ id: knowledgeSpaceId, tenantId: subject.tenantId });
|
|
|
|
if (!space) {
|
|
return context.json({ error: "Knowledge space not found" }, 404);
|
|
}
|
|
|
|
const profileValidationError = validateKnowledgeSpaceRetrievalProfileForMode(
|
|
body.profile,
|
|
body.profile.defaultMode,
|
|
);
|
|
if (profileValidationError) {
|
|
return context.json(
|
|
{
|
|
code: profileValidationError.code,
|
|
error: profileValidationError.message,
|
|
mode: profileValidationError.mode,
|
|
},
|
|
400,
|
|
);
|
|
}
|
|
let currentManifest = await manifests.get({
|
|
knowledgeSpaceId,
|
|
tenantId: subject.tenantId,
|
|
});
|
|
const currentReasoning = currentManifest?.retrievalProfile?.reasoningModel;
|
|
const reasoningChanged =
|
|
currentReasoning !== undefined &&
|
|
(currentReasoning.pluginId !== body.profile.reasoningModel.pluginId ||
|
|
currentReasoning.provider !== body.profile.reasoningModel.provider ||
|
|
currentReasoning.model !== body.profile.reasoningModel.model);
|
|
const hasPublishedProfileTuple = profileMigrations
|
|
? await profileMigrations.requiresMigration({
|
|
knowledgeSpaceId,
|
|
tenantId: subject.tenantId,
|
|
})
|
|
: publishedPublications
|
|
? (await publishedPublications.getPublished({
|
|
knowledgeSpaceId,
|
|
tenantId: subject.tenantId,
|
|
})) !== null
|
|
: false;
|
|
currentManifest ??= await ensureKnowledgeSpaceManifest({
|
|
generateId: generateManifestId,
|
|
manifests,
|
|
now,
|
|
space,
|
|
});
|
|
const canUpdatePendingConfiguration =
|
|
!hasPublishedProfileTuple &&
|
|
currentManifest.embeddingProfile === undefined &&
|
|
currentManifest.retrievalProfile === undefined;
|
|
if (canUpdatePendingConfiguration) {
|
|
if (body.expectedRevision !== 0) {
|
|
return context.json(
|
|
{
|
|
code: "PENDING_MODEL_CONFIGURATION_CONFLICT",
|
|
error: `Knowledge space retrieval profile revision conflict: expected=${body.expectedRevision} actual=0`,
|
|
},
|
|
409,
|
|
);
|
|
}
|
|
const embeddingSelection = currentManifest.pendingModelConfiguration?.embeddingSelection;
|
|
if (!authorization) {
|
|
return context.json(
|
|
{
|
|
code: "PENDING_MODEL_CONFIGURATION_UNAVAILABLE",
|
|
error: "Pending model configuration updates are unavailable",
|
|
},
|
|
503,
|
|
);
|
|
}
|
|
const mutationTimestamp = now();
|
|
const authenticatedApiKey = context.get("authenticatedApiKey");
|
|
let permissionSnapshot: Awaited<ReturnType<typeof issueKnowledgeSpaceDurablePermission>>;
|
|
try {
|
|
permissionSnapshot = await issueKnowledgeSpaceDurablePermission({
|
|
access,
|
|
...(authenticatedApiKey ? { apiKey: authenticatedApiKey } : {}),
|
|
authorization,
|
|
callerKind: context.get("callerKind") ?? "interactive",
|
|
expiresAt: new Date(Date.parse(mutationTimestamp) + 10 * 60_000).toISOString(),
|
|
knowledgeSpaceId,
|
|
requiredAccess: "admin",
|
|
subject,
|
|
});
|
|
} catch (error) {
|
|
if (
|
|
error instanceof KnowledgeSpaceAuthorizationError ||
|
|
error instanceof KnowledgeSpaceAccessError
|
|
) {
|
|
return context.json({ code: error.code, error: "Knowledge space access denied" }, 403);
|
|
}
|
|
throw error;
|
|
}
|
|
if (!embeddingSelection) {
|
|
return context.json(
|
|
{
|
|
code: "EMBEDDING_MODEL_REQUIRED",
|
|
error: "All retrieval modes require an embedding model for this knowledge space",
|
|
},
|
|
409,
|
|
);
|
|
}
|
|
const currentPendingConfiguration = currentManifest.pendingModelConfiguration;
|
|
if (currentPendingConfiguration?.state !== "validation-failed") {
|
|
const idempotentCandidate = createKnowledgeSpacePendingModelConfiguration({
|
|
embeddingSelection,
|
|
retrievalProfile: body.profile,
|
|
revision: currentPendingConfiguration?.revision ?? 1,
|
|
});
|
|
if (
|
|
currentPendingConfiguration &&
|
|
idempotentCandidate.digest === currentPendingConfiguration.digest
|
|
) {
|
|
return context.json(
|
|
{
|
|
configurationStatus: "pending-validation" as const,
|
|
digest: currentPendingConfiguration.digest,
|
|
operation: "initial-validation-pending" as const,
|
|
revision: currentPendingConfiguration.revision,
|
|
},
|
|
202,
|
|
);
|
|
}
|
|
}
|
|
const pendingModelConfiguration = createKnowledgeSpacePendingModelConfiguration({
|
|
embeddingSelection,
|
|
retrievalProfile: body.profile,
|
|
revision: (currentPendingConfiguration?.revision ?? 0) + 1,
|
|
});
|
|
const updated = await manifests.update({
|
|
expectedManifestVersion: currentManifest.manifestVersion,
|
|
knowledgeSpaceId,
|
|
patch: {
|
|
manifestVersion: currentManifest.manifestVersion + 1,
|
|
pendingModelConfiguration,
|
|
updatedAt: mutationTimestamp,
|
|
},
|
|
permission: {
|
|
fence: {
|
|
accessChannel: permissionSnapshot.accessChannel,
|
|
knowledgeSpaceId,
|
|
permissionSnapshotId: permissionSnapshot.id,
|
|
permissionSnapshotRevision: permissionSnapshot.revision,
|
|
requestedBySubjectId: subject.subjectId,
|
|
tenantId: subject.tenantId,
|
|
},
|
|
now: mutationTimestamp,
|
|
requiredAccess: "admin",
|
|
},
|
|
tenantId: subject.tenantId,
|
|
});
|
|
if (!updated) {
|
|
return context.json(
|
|
{
|
|
code: "PENDING_MODEL_CONFIGURATION_CONFLICT",
|
|
error: "Pending model configuration changed concurrently",
|
|
},
|
|
409,
|
|
);
|
|
}
|
|
return context.json(
|
|
{
|
|
configurationStatus: "pending-validation" as const,
|
|
digest: pendingModelConfiguration.digest,
|
|
operation: "initial-validation-pending" as const,
|
|
revision: pendingModelConfiguration.revision,
|
|
},
|
|
202,
|
|
);
|
|
}
|
|
const publishedMigrationRequired =
|
|
hasPublishedProfileTuple && profileMigrations !== undefined && profiles !== undefined;
|
|
if (hasPublishedProfileTuple && !publishedMigrationRequired) {
|
|
return context.json(
|
|
{
|
|
code: "PROFILE_MIGRATION_UNAVAILABLE",
|
|
error: "Published retrieval profile changes require the durable migration workflow",
|
|
},
|
|
503,
|
|
);
|
|
}
|
|
if (reasoningChanged && !publishedMigrationRequired) {
|
|
const [usage, nodePage] = await Promise.all([
|
|
assets.getStorageUsage({ knowledgeSpaceId }),
|
|
nodes.listBySpace({ knowledgeSpaceId, limit: 1 }),
|
|
]);
|
|
if (usage.documentCount > 0 || nodePage.items.length > 0) {
|
|
return context.json(
|
|
{
|
|
code: "RETRIEVAL_PROFILE_REBUILD_REQUIRED",
|
|
error: "Reasoning model change requires a PageIndex rebuild workflow",
|
|
},
|
|
409,
|
|
);
|
|
}
|
|
}
|
|
let capabilitySnapshots: Awaited<ReturnType<typeof preflightKnowledgeSpaceModels>>;
|
|
try {
|
|
capabilitySnapshots = await preflightKnowledgeSpaceModels({
|
|
preflight: modelCapabilityPreflight,
|
|
required: true,
|
|
retrievalProfile: body.profile,
|
|
tenantId: subject.tenantId,
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof ModelCapabilityPreflightError) {
|
|
return context.json(
|
|
{ code: error.code, error: error.message },
|
|
error.retryable ? 503 : 422,
|
|
);
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
if (publishedMigrationRequired && profileMigrations && profiles) {
|
|
try {
|
|
await ensureLegacyPublishedProfileTuple({
|
|
createdBySubjectId: subject.subjectId,
|
|
knowledgeSpaceId,
|
|
manifest: currentManifest,
|
|
modelCapabilityPreflight,
|
|
now,
|
|
profilePublicationBindings,
|
|
profiles,
|
|
tenantId: subject.tenantId,
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof ModelCapabilityPreflightError) {
|
|
return context.json(
|
|
{ code: error.code, error: error.message },
|
|
error.retryable ? 503 : 422,
|
|
);
|
|
}
|
|
if (error instanceof LegacyProfileBootstrapError) {
|
|
return context.json({ code: error.code, error: error.message }, error.status);
|
|
}
|
|
if (error instanceof SettingsProfileCandidateConflictError) {
|
|
return context.json({ code: error.code, error: error.message }, 409);
|
|
}
|
|
return context.json(
|
|
{
|
|
code: "PROFILE_PUBLICATION_BOOTSTRAP_FAILED",
|
|
error: "Published projection tuple could not be bound to verified active profiles",
|
|
},
|
|
503,
|
|
);
|
|
}
|
|
const activeHead = await profiles.getHead({
|
|
kind: "retrieval",
|
|
knowledgeSpaceId,
|
|
tenantId: subject.tenantId,
|
|
});
|
|
if (!activeHead) {
|
|
throw new Error("Bootstrapped retrieval profile head could not be reloaded");
|
|
}
|
|
const actualRevision = activeHead.activeRevision;
|
|
if (actualRevision !== body.expectedRevision) {
|
|
return context.json(
|
|
{
|
|
error: `Knowledge space retrieval profile revision conflict: expected=${body.expectedRevision} actual=${actualRevision}`,
|
|
},
|
|
409,
|
|
);
|
|
}
|
|
const capabilitySnapshot = {
|
|
reasoning: capabilitySnapshots.reasoning ?? null,
|
|
rerank: capabilitySnapshots.rerank ?? null,
|
|
verification: "verified",
|
|
} as const;
|
|
const authenticatedApiKey = context.get("authenticatedApiKey");
|
|
const capabilityGrantId = context.get("capabilityV2Grant")?.grantId;
|
|
const migrationPrincipal = {
|
|
...(authenticatedApiKey ? { apiKey: authenticatedApiKey } : {}),
|
|
...(capabilityGrantId ? { capabilityGrantId } : {}),
|
|
callerKind: context.get("callerKind") ?? "interactive",
|
|
knowledgeSpaceId,
|
|
subject,
|
|
} as const;
|
|
try {
|
|
const candidate = await getOrSupersedeRetrievalSettingsCandidate({
|
|
activeRevision: actualRevision,
|
|
capabilitySnapshot,
|
|
createdBySubjectId: subject.subjectId,
|
|
migrationPrincipal,
|
|
migrations: profileMigrations,
|
|
now,
|
|
profile: body.profile,
|
|
profiles,
|
|
});
|
|
let migration = await profileMigrations.findByCandidate({
|
|
...migrationPrincipal,
|
|
candidateProfileId: candidate.id,
|
|
});
|
|
migration ??= await profileMigrations.request({
|
|
...migrationPrincipal,
|
|
candidateRevision: candidate.revision,
|
|
changedKind: "retrieval",
|
|
idempotencyKey: `settings-retrieval-${candidate.snapshotDigest}`,
|
|
});
|
|
if (
|
|
migration.runState === "failed" &&
|
|
!isTerminalKnowledgeSpaceProfileMigrationError(migration.lastErrorCode)
|
|
) {
|
|
const retried = await profileMigrations.retry({
|
|
...migrationPrincipal,
|
|
runId: migration.id,
|
|
});
|
|
if (!retried) {
|
|
return context.json(
|
|
{
|
|
code: "PROFILE_MIGRATION_RETRY_CONFLICT",
|
|
error: "Failed profile migration could not be retried",
|
|
},
|
|
409,
|
|
);
|
|
}
|
|
migration = retried;
|
|
}
|
|
return context.json(toPublicKnowledgeSpaceProfileMigration(migration), 202);
|
|
} catch (error) {
|
|
if (error instanceof SettingsProfileCandidateConflictError) {
|
|
return context.json({ code: error.code, error: error.message }, 409);
|
|
}
|
|
if (error instanceof KnowledgeSpaceProfileMigrationConflictError) {
|
|
return context.json({ code: error.code, error: error.message }, 409);
|
|
}
|
|
if (error instanceof KnowledgeSpaceProfileMigrationServiceError) {
|
|
if (error.code === "PROFILE_MIGRATION_FORBIDDEN") {
|
|
return context.json({ code: error.code, error: error.message }, 403);
|
|
}
|
|
if (error.code.endsWith("NOT_FOUND") || error.code.endsWith("MISSING")) {
|
|
return context.json({ code: error.code, error: error.message }, 404);
|
|
}
|
|
return context.json({ code: error.code, error: error.message }, 409);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
if (!authorization || !unpublishedProfileActivations) {
|
|
return context.json(
|
|
{
|
|
code: "UNPUBLISHED_PROFILE_ACTIVATION_UNAVAILABLE",
|
|
error: "Atomic unpublished profile activation is unavailable",
|
|
},
|
|
503,
|
|
);
|
|
}
|
|
const manifest = await ensureKnowledgeSpaceManifest({
|
|
generateId: generateManifestId,
|
|
manifests,
|
|
now,
|
|
space,
|
|
});
|
|
const profile = createKnowledgeSpaceRetrievalProfile(body.profile, body.expectedRevision + 1);
|
|
try {
|
|
const mutationTimestamp = now();
|
|
const authenticatedApiKey = context.get("authenticatedApiKey");
|
|
const permissionSnapshot = await issueKnowledgeSpaceDurablePermission({
|
|
access,
|
|
...(authenticatedApiKey ? { apiKey: authenticatedApiKey } : {}),
|
|
authorization,
|
|
callerKind: context.get("callerKind") ?? "interactive",
|
|
expiresAt: new Date(Date.parse(mutationTimestamp) + 10 * 60_000).toISOString(),
|
|
knowledgeSpaceId,
|
|
requiredAccess: "admin",
|
|
subject,
|
|
});
|
|
await unpublishedProfileActivations.activate({
|
|
capabilitySnapshot: {
|
|
reasoning: capabilitySnapshots.reasoning ?? null,
|
|
rerank: capabilitySnapshots.rerank ?? null,
|
|
verification: "verified",
|
|
},
|
|
createdBySubjectId: subject.subjectId,
|
|
expectedManifestProfileRevision: body.expectedRevision,
|
|
expectedManifestVersion: manifest.manifestVersion,
|
|
kind: "retrieval",
|
|
knowledgeSpaceId,
|
|
now: mutationTimestamp,
|
|
permission: {
|
|
accessChannel: permissionSnapshot.accessChannel,
|
|
knowledgeSpaceId,
|
|
permissionSnapshotId: permissionSnapshot.id,
|
|
permissionSnapshotRevision: permissionSnapshot.revision,
|
|
requestedBySubjectId: subject.subjectId,
|
|
tenantId: subject.tenantId,
|
|
},
|
|
snapshot: profile,
|
|
tenantId: subject.tenantId,
|
|
});
|
|
|
|
return context.json(profile, 200);
|
|
} catch (error) {
|
|
if (error instanceof KnowledgeSpaceUnpublishedProfileActivationError) {
|
|
return context.json({ code: error.code, error: error.message }, 409);
|
|
}
|
|
if (error instanceof KnowledgeSpaceProfileTransitionError) {
|
|
return context.json({ code: error.code, error: error.message }, 409);
|
|
}
|
|
if (
|
|
error instanceof KnowledgeSpaceAuthorizationError ||
|
|
error instanceof KnowledgeSpaceAccessError
|
|
) {
|
|
return context.json({ code: error.code, error: "Knowledge space access denied" }, 403);
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
app.openapi(getKnowledgeSpaceStatusRoute, async (context) => {
|
|
const subject = context.get("subject");
|
|
const params = context.req.valid("param");
|
|
const space = await spaces.get({
|
|
id: params.id,
|
|
tenantId: subject.tenantId,
|
|
});
|
|
|
|
if (!space) {
|
|
return context.json({ error: "Knowledge space not found" }, 404);
|
|
}
|
|
|
|
const manifest = await ensureKnowledgeSpaceManifest({
|
|
generateId: generateManifestId,
|
|
manifests,
|
|
now,
|
|
space,
|
|
});
|
|
const generatedAt = now();
|
|
const projectionVersion = projectionVersionFromManifest(manifest.projectionSetVersion);
|
|
const [
|
|
configuration,
|
|
storageHealthy,
|
|
activeSessions,
|
|
activeLeases,
|
|
retryableCommits,
|
|
terminalCommits,
|
|
] = await Promise.all([
|
|
resolveKnowledgeSpaceConfigurationStatus({
|
|
knowledgeSpaceId: params.id,
|
|
manifest,
|
|
profiles,
|
|
tenantId: subject.tenantId,
|
|
}),
|
|
safeObjectStorageHealth(adapter),
|
|
knowledgeFsSessions.listActive({
|
|
knowledgeSpaceId: params.id,
|
|
limit: STATUS_ITEM_LIMIT,
|
|
now: generatedAt,
|
|
tenantId: subject.tenantId,
|
|
}),
|
|
knowledgeFsLeases.listActive({
|
|
knowledgeSpaceId: params.id,
|
|
limit: STATUS_ITEM_LIMIT,
|
|
now: generatedAt,
|
|
tenantId: subject.tenantId,
|
|
}),
|
|
stagedCommits.list({
|
|
knowledgeSpaceId: params.id,
|
|
limit: STATUS_ITEM_LIMIT,
|
|
status: "failed-retryable",
|
|
tenantId: subject.tenantId,
|
|
}),
|
|
stagedCommits.list({
|
|
knowledgeSpaceId: params.id,
|
|
limit: STATUS_ITEM_LIMIT,
|
|
status: "failed-terminal",
|
|
tenantId: subject.tenantId,
|
|
}),
|
|
]);
|
|
const projectionSummaries = await summarizeProjectionStatus({
|
|
knowledgeSpaceId: params.id,
|
|
projectionVersion,
|
|
projections,
|
|
});
|
|
const failedCommitItems = [...retryableCommits.items, ...terminalCommits.items]
|
|
.filter(isFailedCommitDiagnostic)
|
|
.sort((left, right) => left.id.localeCompare(right.id))
|
|
.slice(0, STATUS_ITEM_LIMIT);
|
|
|
|
return context.json(
|
|
{
|
|
activeLeases: {
|
|
count: activeLeases.items.length,
|
|
items: activeLeases.items.map((lease) => ({
|
|
expiresAt: lease.expiresAt,
|
|
id: lease.id,
|
|
leaseType: lease.leaseType,
|
|
targetType: lease.targetType,
|
|
virtualPath: lease.virtualPath,
|
|
})),
|
|
truncated: Boolean(activeLeases.nextCursor),
|
|
},
|
|
activeSessions: {
|
|
count: activeSessions.items.length,
|
|
items: activeSessions.items.map((session) => ({
|
|
clientKind: session.clientKind,
|
|
consistencyClass: session.consistencyClass,
|
|
expiresAt: session.expiresAt,
|
|
heartbeatAt: session.heartbeatAt,
|
|
id: session.id,
|
|
subjectId: session.subject.subjectId,
|
|
})),
|
|
truncated: Boolean(activeSessions.nextCursor),
|
|
},
|
|
configuration,
|
|
failedCommits: {
|
|
count: failedCommitItems.length,
|
|
items: failedCommitItems.map((commit) => {
|
|
const publicCommit = toPublicKnowledgeSpaceStagedCommit(commit);
|
|
return {
|
|
...(publicCommit.errorCode ? { errorCode: publicCommit.errorCode } : {}),
|
|
...(publicCommit.expiresAt ? { expiresAt: publicCommit.expiresAt } : {}),
|
|
...(publicCommit.failure ? { failure: publicCommit.failure } : {}),
|
|
id: publicCommit.id,
|
|
status: commit.status,
|
|
updatedAt: publicCommit.updatedAt,
|
|
};
|
|
}),
|
|
truncated:
|
|
Boolean(retryableCommits.nextCursor) ||
|
|
Boolean(terminalCommits.nextCursor) ||
|
|
retryableCommits.items.length + terminalCommits.items.length > STATUS_ITEM_LIMIT,
|
|
},
|
|
generatedAt,
|
|
index: {
|
|
nodeSchemaVersion: manifest.nodeSchemaVersion,
|
|
projectionSetVersion: manifest.projectionSetVersion,
|
|
projectionVersion,
|
|
summaries: projectionSummaries,
|
|
},
|
|
knowledgeSpaceId: params.id,
|
|
manifest: {
|
|
consistencyClass: manifest.consistencyPolicy.defaultClass,
|
|
manifestVersion: manifest.manifestVersion,
|
|
metadataDialect: manifest.metadataDialect,
|
|
objectKeyPrefix: manifest.objectKeyPrefix,
|
|
storageProvider: manifest.storageProvider,
|
|
},
|
|
parser: {
|
|
kind: parser.kind,
|
|
policyVersion: manifest.parserPolicyVersion,
|
|
},
|
|
storage: {
|
|
healthy: storageHealthy,
|
|
objectStorageKind: adapter.objectStorage.kind,
|
|
provider: manifest.storageProvider,
|
|
},
|
|
tenantId: subject.tenantId,
|
|
},
|
|
200,
|
|
);
|
|
});
|
|
|
|
app.openapi(getKnowledgeSpaceStatsRoute, async (context) => {
|
|
const subject = context.get("subject");
|
|
const params = context.req.valid("param");
|
|
const query = context.req.valid("query");
|
|
const space = await spaces.get({
|
|
id: params.id,
|
|
tenantId: subject.tenantId,
|
|
});
|
|
|
|
if (!space) {
|
|
return context.json({ error: "Knowledge space not found" }, 404);
|
|
}
|
|
|
|
const manifest = await ensureKnowledgeSpaceManifest({
|
|
generateId: generateManifestId,
|
|
manifests,
|
|
now,
|
|
space,
|
|
});
|
|
const generatedAt = now();
|
|
const windowStart = subtractMinutes(generatedAt, query.windowMinutes);
|
|
const projectionVersion = projectionVersionFromManifest(manifest.projectionSetVersion);
|
|
const [
|
|
storageUsage,
|
|
cacheStats,
|
|
activeSessions,
|
|
activeLeases,
|
|
retryableCommits,
|
|
terminalCommits,
|
|
projectionSummaries,
|
|
] = await Promise.all([
|
|
assets.getStorageUsage({ knowledgeSpaceId: params.id }),
|
|
safeCacheStats(adapter),
|
|
knowledgeFsSessions.listActive({
|
|
knowledgeSpaceId: params.id,
|
|
limit: STATUS_ITEM_LIMIT,
|
|
now: generatedAt,
|
|
tenantId: subject.tenantId,
|
|
}),
|
|
knowledgeFsLeases.listActive({
|
|
knowledgeSpaceId: params.id,
|
|
limit: STATUS_ITEM_LIMIT,
|
|
now: generatedAt,
|
|
tenantId: subject.tenantId,
|
|
}),
|
|
stagedCommits.list({
|
|
knowledgeSpaceId: params.id,
|
|
limit: STATUS_ITEM_LIMIT,
|
|
status: "failed-retryable",
|
|
tenantId: subject.tenantId,
|
|
}),
|
|
stagedCommits.list({
|
|
knowledgeSpaceId: params.id,
|
|
limit: STATUS_ITEM_LIMIT,
|
|
status: "failed-terminal",
|
|
tenantId: subject.tenantId,
|
|
}),
|
|
summarizeProjectionStatus({
|
|
knowledgeSpaceId: params.id,
|
|
projectionVersion,
|
|
projections,
|
|
}),
|
|
]);
|
|
const retryableInWindow = retryableCommits.items.filter(
|
|
(commit) => commit.updatedAt >= windowStart && commit.updatedAt <= generatedAt,
|
|
);
|
|
const terminalInWindow = terminalCommits.items.filter(
|
|
(commit) => commit.updatedAt >= windowStart && commit.updatedAt <= generatedAt,
|
|
);
|
|
|
|
return context.json(
|
|
{
|
|
cache: cacheStats,
|
|
commits: {
|
|
failedRetryable: retryableInWindow.length,
|
|
failedTerminal: terminalInWindow.length,
|
|
sampled: retryableCommits.items.length + terminalCommits.items.length,
|
|
truncated: Boolean(retryableCommits.nextCursor) || Boolean(terminalCommits.nextCursor),
|
|
},
|
|
generatedAt,
|
|
knowledgeSpaceId: params.id,
|
|
metrics: {
|
|
available: false,
|
|
reason: "metrics-backend-not-configured",
|
|
},
|
|
projections: {
|
|
...projectionSummaries,
|
|
projectionVersion,
|
|
},
|
|
runtime: {
|
|
activeLeaseSampleCount: activeLeases.items.length,
|
|
activeSessionSampleCount: activeSessions.items.length,
|
|
truncated: Boolean(activeSessions.nextCursor) || Boolean(activeLeases.nextCursor),
|
|
},
|
|
storage: storageUsage,
|
|
tenantId: subject.tenantId,
|
|
window: {
|
|
end: generatedAt,
|
|
minutes: query.windowMinutes,
|
|
start: windowStart,
|
|
},
|
|
},
|
|
200,
|
|
);
|
|
});
|
|
|
|
app.openapi(getKnowledgeSpaceFsckRoute, async (context) => {
|
|
const subject = context.get("subject");
|
|
const params = context.req.valid("param");
|
|
const query = context.req.valid("query");
|
|
const space = await spaces.get({
|
|
id: params.id,
|
|
tenantId: subject.tenantId,
|
|
});
|
|
|
|
if (!space) {
|
|
return context.json({ error: "Knowledge space not found" }, 404);
|
|
}
|
|
|
|
const input = {
|
|
...(query.cursor ? { cursor: query.cursor } : {}),
|
|
knowledgeSpaceId: params.id,
|
|
tenantId: subject.tenantId,
|
|
};
|
|
|
|
if (query.check === "artifact-segments") {
|
|
const checker = createKnowledgeFsArtifactSegmentFsckChecker({
|
|
artifactSegments,
|
|
assets,
|
|
maxAssetsPerRun: OPERATOR_PAGE_LIMIT,
|
|
maxSegmentsPerArtifact: OPERATOR_PAGE_LIMIT,
|
|
now,
|
|
objectStorage: adapter.objectStorage,
|
|
parseArtifacts,
|
|
});
|
|
|
|
return context.json(await checker.check(input), 200);
|
|
}
|
|
|
|
if (query.check === "references") {
|
|
const checker = createKnowledgeFsReferenceFsckChecker({
|
|
assets,
|
|
maxNodesPerRun: OPERATOR_PAGE_LIMIT,
|
|
maxPathsPerView: OPERATOR_PAGE_LIMIT,
|
|
maxProjectionsPerType: OPERATOR_PAGE_LIMIT,
|
|
nodes,
|
|
now,
|
|
parseArtifacts,
|
|
paths,
|
|
pathViewNames: ["physical"],
|
|
projections,
|
|
projectionTypes: ["dense-vector", "fts", "metadata", "graph"],
|
|
});
|
|
|
|
return context.json(await checker.check(input), 200);
|
|
}
|
|
|
|
const checker = createKnowledgeFsRawObjectFsckChecker({
|
|
assets,
|
|
maxAssetsPerRun: OPERATOR_PAGE_LIMIT,
|
|
now,
|
|
objectStorage: adapter.objectStorage,
|
|
});
|
|
|
|
return context.json(await checker.check(input), 200);
|
|
});
|
|
|
|
app.openapi(getKnowledgeSpaceStagedObjectGcDryRunRoute, async (context) => {
|
|
const subject = context.get("subject");
|
|
const params = context.req.valid("param");
|
|
const query = context.req.valid("query");
|
|
const space = await spaces.get({
|
|
id: params.id,
|
|
tenantId: subject.tenantId,
|
|
});
|
|
|
|
if (!space) {
|
|
return context.json({ error: "Knowledge space not found" }, 404);
|
|
}
|
|
|
|
const manifest = await ensureKnowledgeSpaceManifest({
|
|
generateId: generateManifestId,
|
|
manifests,
|
|
now,
|
|
space,
|
|
});
|
|
const dryRun = createKnowledgeFsStagedObjectGcDryRun({
|
|
commits: stagedCommits,
|
|
generateDryRunId: generateGcDryRunId,
|
|
maxFailedCommitsPerRun: OPERATOR_PAGE_LIMIT,
|
|
maxObjectsPerRun: OPERATOR_PAGE_LIMIT,
|
|
now,
|
|
objectStorage: adapter.objectStorage,
|
|
});
|
|
|
|
return context.json(
|
|
await dryRun.preview({
|
|
...(query.cursor ? { cursor: query.cursor } : {}),
|
|
knowledgeSpaceId: params.id,
|
|
stagedObjectPrefix: query.stagedObjectPrefix ?? `${manifest.objectKeyPrefix}/staging/`,
|
|
tenantId: subject.tenantId,
|
|
}),
|
|
200,
|
|
);
|
|
});
|
|
|
|
app.openapi(executeKnowledgeSpaceStagedObjectGcRoute, async (context) => {
|
|
try {
|
|
const subject = context.get("subject");
|
|
const params = context.req.valid("param");
|
|
const body = context.req.valid("json");
|
|
const space = await spaces.get({
|
|
id: params.id,
|
|
tenantId: subject.tenantId,
|
|
});
|
|
|
|
if (!space) {
|
|
return context.json({ error: "Knowledge space not found" }, 404);
|
|
}
|
|
|
|
const executor = createKnowledgeFsStagedObjectGcExecutor({
|
|
maxDeletes: OPERATOR_PAGE_LIMIT,
|
|
objectStorage: adapter.objectStorage,
|
|
operationLeases,
|
|
});
|
|
|
|
const result = await executor.execute({
|
|
candidates: body.candidates,
|
|
knowledgeSpaceId: params.id,
|
|
tenantId: subject.tenantId,
|
|
});
|
|
|
|
return context.json(
|
|
{
|
|
deleted: result.deleted,
|
|
items: result.items.map((item) => ({ ...item })),
|
|
skipped: result.skipped,
|
|
tenantId: result.tenantId,
|
|
},
|
|
200,
|
|
);
|
|
} catch (error) {
|
|
if (error instanceof Error && error.message.includes("maxDeletes")) {
|
|
return context.json({ error: error.message }, 400);
|
|
}
|
|
|
|
/* v8 ignore next 2 -- unexpected staged object GC failures should escape to Hono. */
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
app.openapi(listKnowledgeSpaceActiveLeasesRoute, async (context) => {
|
|
try {
|
|
const subject = context.get("subject");
|
|
const params = context.req.valid("param");
|
|
const space = await spaces.get({
|
|
id: params.id,
|
|
tenantId: subject.tenantId,
|
|
});
|
|
|
|
if (!space) {
|
|
return context.json({ error: "Knowledge space not found" }, 404);
|
|
}
|
|
|
|
const result = await knowledgeFsLeases.listActive({
|
|
...context.req.valid("query"),
|
|
knowledgeSpaceId: params.id,
|
|
now: now(),
|
|
tenantId: subject.tenantId,
|
|
});
|
|
|
|
return context.json(
|
|
{
|
|
items: result.items.map((lease) => ({ ...lease })),
|
|
...(result.nextCursor ? { nextCursor: result.nextCursor } : {}),
|
|
},
|
|
200,
|
|
);
|
|
} catch (error) {
|
|
if (error instanceof KnowledgeFsLeaseListLimitExceededError) {
|
|
return context.json({ error: error.message }, 400);
|
|
}
|
|
|
|
/* v8 ignore next 2 -- unexpected active lease list failures should escape to Hono. */
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
app.openapi(listKnowledgeSpaceStagedCommitsRoute, async (context) => {
|
|
try {
|
|
const subject = context.get("subject");
|
|
const params = context.req.valid("param");
|
|
const space = await spaces.get({
|
|
id: params.id,
|
|
tenantId: subject.tenantId,
|
|
});
|
|
|
|
if (!space) {
|
|
return context.json({ error: "Knowledge space not found" }, 404);
|
|
}
|
|
|
|
const result = await stagedCommits.list({
|
|
...context.req.valid("query"),
|
|
knowledgeSpaceId: params.id,
|
|
tenantId: subject.tenantId,
|
|
});
|
|
|
|
return context.json(
|
|
{
|
|
items: result.items.map(toPublicKnowledgeSpaceStagedCommit),
|
|
...(result.nextCursor ? { nextCursor: result.nextCursor } : {}),
|
|
},
|
|
200,
|
|
);
|
|
} catch (error) {
|
|
if (error instanceof StagedCommitListLimitExceededError) {
|
|
return context.json({ error: error.message }, 400);
|
|
}
|
|
|
|
/* v8 ignore next 2 -- unexpected staged commit list failures should escape to Hono. */
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
app.openapi(updateKnowledgeSpaceRoute, async (context) => {
|
|
try {
|
|
const subject = context.get("subject");
|
|
const mutationTimestamp = now();
|
|
const authenticatedApiKey = context.get("authenticatedApiKey");
|
|
const knowledgeSpaceId = context.req.valid("param").id;
|
|
const capabilityGrant = context.get("capabilityV2Grant");
|
|
if (
|
|
capabilityGrant &&
|
|
(capabilityGrant.action !== "knowledge_spaces.update" ||
|
|
capabilityGrant.namespaceId !== subject.tenantId ||
|
|
capabilityGrant.subject !== subject.subjectId ||
|
|
capabilityGrant.resource.id !== knowledgeSpaceId ||
|
|
capabilityGrant.resource.parent_id !== null ||
|
|
capabilityGrant.resource.type !== "knowledge_space")
|
|
) {
|
|
return context.json({ error: "Forbidden" }, 403);
|
|
}
|
|
const permissionSnapshot =
|
|
!capabilityGrant && authorization
|
|
? await issueKnowledgeSpaceDurablePermission({
|
|
access,
|
|
...(authenticatedApiKey ? { apiKey: authenticatedApiKey } : {}),
|
|
authorization,
|
|
callerKind: context.get("callerKind") ?? "interactive",
|
|
expiresAt: new Date(Date.parse(mutationTimestamp) + 10 * 60_000).toISOString(),
|
|
knowledgeSpaceId,
|
|
requiredAccess: "write",
|
|
subject,
|
|
})
|
|
: undefined;
|
|
const space = await spaces.update({
|
|
...context.req.valid("json"),
|
|
actorSubjectId: subject.subjectId,
|
|
id: knowledgeSpaceId,
|
|
...(capabilityGrant
|
|
? {
|
|
permission: {
|
|
fence: {
|
|
capabilityGrantId: capabilityGrant.grantId,
|
|
expectedBinding: {
|
|
action: "knowledge_spaces.update",
|
|
resource: {
|
|
id: knowledgeSpaceId,
|
|
parentId: null,
|
|
type: "knowledge_space",
|
|
},
|
|
},
|
|
knowledgeSpaceId,
|
|
tenantId: subject.tenantId,
|
|
},
|
|
now: mutationTimestamp,
|
|
requiredAccess: "write" as const,
|
|
},
|
|
}
|
|
: permissionSnapshot
|
|
? {
|
|
permission: {
|
|
fence: {
|
|
accessChannel: permissionSnapshot.accessChannel,
|
|
knowledgeSpaceId,
|
|
permissionSnapshotId: permissionSnapshot.id,
|
|
permissionSnapshotRevision: permissionSnapshot.revision,
|
|
requestedBySubjectId: subject.subjectId,
|
|
tenantId: subject.tenantId,
|
|
},
|
|
now: mutationTimestamp,
|
|
requiredAccess: "write" as const,
|
|
},
|
|
}
|
|
: {}),
|
|
tenantId: subject.tenantId,
|
|
});
|
|
|
|
if (!space) {
|
|
return context.json({ error: "Knowledge space not found" }, 404);
|
|
}
|
|
|
|
return context.json(space, 200);
|
|
} catch (error) {
|
|
if (error instanceof DuplicateKnowledgeSpaceSlugError) {
|
|
return context.json({ error: error.message }, 409);
|
|
}
|
|
|
|
if (error instanceof KnowledgeSpaceRevisionConflictError) {
|
|
return context.json({ code: error.code, error: error.message }, 409);
|
|
}
|
|
|
|
if (error instanceof CapabilityPublicationFencedError) {
|
|
return context.json(
|
|
{
|
|
code: "KNOWLEDGE_SPACE_ACCESS_DENIED",
|
|
error: "Knowledge space access denied",
|
|
},
|
|
403,
|
|
);
|
|
}
|
|
|
|
if (
|
|
error instanceof KnowledgeSpaceAuthorizationError ||
|
|
error instanceof KnowledgeSpaceAccessError
|
|
) {
|
|
return context.json({ code: error.code, error: "Knowledge space access denied" }, 403);
|
|
}
|
|
|
|
/* v8 ignore next 2 -- unexpected knowledge-space update failures should escape to Hono. */
|
|
throw error;
|
|
}
|
|
});
|
|
}
|
|
|
|
type ProductReadinessIssue = {
|
|
readonly code:
|
|
| "binding_missing"
|
|
| "incompatible"
|
|
| "missing"
|
|
| "unavailable"
|
|
| "validation_failed";
|
|
readonly field: "embedding" | "publication" | "reasoning" | "rerank";
|
|
readonly retryable: boolean;
|
|
};
|
|
|
|
async function resolveProductSettings({
|
|
knowledgeSpaceId,
|
|
manifest,
|
|
profilePublicationBindings,
|
|
profiles,
|
|
publishedPublications,
|
|
runtimeSnapshotResolver,
|
|
tenantId,
|
|
}: {
|
|
readonly knowledgeSpaceId: string;
|
|
readonly manifest: KnowledgeSpaceManifest;
|
|
readonly profilePublicationBindings:
|
|
| (Pick<KnowledgeSpaceProfilePublicationRepository, "bindCurrentPublished"> &
|
|
Partial<Pick<KnowledgeSpaceProfilePublicationRepository, "requireActivatedBinding">>)
|
|
| undefined;
|
|
readonly profiles: KnowledgeSpaceProfileRepository | undefined;
|
|
readonly publishedPublications:
|
|
| Pick<ProjectionSetPublicationRepository, "getPublished">
|
|
| undefined;
|
|
readonly runtimeSnapshotResolver: PublishedKnowledgeSpaceRuntimeSnapshotResolver | undefined;
|
|
readonly tenantId: string;
|
|
}) {
|
|
const pending = manifest.pendingModelConfiguration;
|
|
const embedding = pending?.embeddingSelection ?? manifest.embeddingProfile ?? null;
|
|
const retrieval = pending?.retrievalProfile ?? manifest.retrievalProfile ?? null;
|
|
const candidateComplete = Boolean(
|
|
embedding && retrieval && hasRequiredKnowledgeSpaceRetrievalModels(retrieval),
|
|
);
|
|
// A complete model selection is saved synchronously. The internal pending record only delays
|
|
// creation of immutable profile heads until the first document establishes the vector-space
|
|
// dimension; it is not a user-visible validation job. Reporting it as pending-validation made
|
|
// settings clients poll forever when no document compilation was running.
|
|
const configurationState = candidateComplete
|
|
? pending?.state === "validation-failed"
|
|
? ("validation-failed" as const)
|
|
: ("active" as const)
|
|
: ("setup-required" as const);
|
|
const configuration = await resolveKnowledgeSpaceConfigurationStatus({
|
|
knowledgeSpaceId,
|
|
manifest,
|
|
profiles,
|
|
tenantId,
|
|
});
|
|
const activeProfileAvailable =
|
|
configuration.activeProfiles.embeddingRevision !== undefined &&
|
|
configuration.activeProfiles.retrievalRevision !== undefined &&
|
|
configuration.availableModes.length > 0;
|
|
const canActivateCandidate = candidateComplete && pending?.state === "pending-validation";
|
|
const issues: ProductReadinessIssue[] = [];
|
|
let deepAvailable = activeProfileAvailable;
|
|
let queryAvailable = activeProfileAvailable;
|
|
let researchAvailable = activeProfileAvailable;
|
|
if (!embedding) issues.push({ code: "missing", field: "embedding", retryable: false });
|
|
if (!retrieval) {
|
|
issues.push({ code: "missing", field: "reasoning", retryable: false });
|
|
issues.push({ code: "missing", field: "rerank", retryable: false });
|
|
} else if (!hasRequiredKnowledgeSpaceRetrievalModels(retrieval)) {
|
|
issues.push({ code: "missing", field: "rerank", retryable: false });
|
|
}
|
|
if (pending?.state === "validation-failed" && pending.failure) {
|
|
const failure = pending.failure;
|
|
const fields = failure.field ? [failure.field] : inferFailedModelFields(failure.code);
|
|
issues.push(
|
|
...fields.map((field) => ({
|
|
code: readinessIssueCodeForFailure(failure.code),
|
|
field,
|
|
retryable: failure.retryable,
|
|
})),
|
|
);
|
|
}
|
|
if (activeProfileAvailable && runtimeSnapshotResolver) {
|
|
try {
|
|
await runtimeSnapshotResolver.resolve({ knowledgeSpaceId, tenantId });
|
|
} catch (error) {
|
|
if (!(error instanceof PublishedProjectionReadUnavailableError)) throw error;
|
|
deepAvailable = false;
|
|
queryAvailable = false;
|
|
researchAvailable = false;
|
|
}
|
|
if (queryAvailable) {
|
|
[queryAvailable, deepAvailable, researchAvailable] = await Promise.all([
|
|
isPublishedRuntimeModeReady(runtimeSnapshotResolver, {
|
|
knowledgeSpaceId,
|
|
resolvedMode: "fast",
|
|
tenantId,
|
|
}),
|
|
isPublishedRuntimeModeReady(runtimeSnapshotResolver, {
|
|
knowledgeSpaceId,
|
|
resolvedMode: "deep",
|
|
tenantId,
|
|
}),
|
|
isPublishedRuntimeModeReady(runtimeSnapshotResolver, {
|
|
knowledgeSpaceId,
|
|
resolvedMode: "research",
|
|
tenantId,
|
|
}),
|
|
]);
|
|
}
|
|
} else if (activeProfileAvailable && publishedPublications) {
|
|
const publication = await publishedPublications.getPublished({ knowledgeSpaceId, tenantId });
|
|
if (!publication) {
|
|
deepAvailable = false;
|
|
queryAvailable = false;
|
|
researchAvailable = false;
|
|
} else if (profilePublicationBindings?.requireActivatedBinding) {
|
|
try {
|
|
await profilePublicationBindings.requireActivatedBinding({
|
|
knowledgeSpaceId,
|
|
publicationFingerprint: publication.fingerprint,
|
|
publicationId: publication.id,
|
|
tenantId,
|
|
});
|
|
} catch (error) {
|
|
if (
|
|
!(error instanceof KnowledgeSpaceProfilePublicationTransitionError) ||
|
|
error.code !== "KNOWLEDGE_SPACE_PROFILE_PUBLICATION_TUPLE_NOT_PUBLISHED"
|
|
)
|
|
throw error;
|
|
deepAvailable = false;
|
|
queryAvailable = false;
|
|
researchAvailable = false;
|
|
issues.push({ code: "binding_missing", field: "publication", retryable: false });
|
|
}
|
|
}
|
|
}
|
|
if (activeProfileAvailable && (!deepAvailable || !queryAvailable || !researchAvailable)) {
|
|
if (!issues.some((issue) => issue.field === "publication")) {
|
|
issues.push({ code: "unavailable", field: "publication", retryable: true });
|
|
}
|
|
}
|
|
return {
|
|
activeProfileAvailable,
|
|
activeProfileRevisions: {
|
|
...(configuration.activeProfiles.embeddingRevision !== undefined
|
|
? { embedding: configuration.activeProfiles.embeddingRevision }
|
|
: {}),
|
|
...(configuration.activeProfiles.retrievalRevision !== undefined
|
|
? { retrieval: configuration.activeProfiles.retrievalRevision }
|
|
: {}),
|
|
},
|
|
capabilities: {
|
|
deep: deepAvailable,
|
|
index: activeProfileAvailable,
|
|
ingest: activeProfileAvailable || canActivateCandidate,
|
|
query: queryAvailable,
|
|
research: researchAvailable,
|
|
sourceSync: activeProfileAvailable || canActivateCandidate,
|
|
},
|
|
configurationState,
|
|
embedding,
|
|
issues,
|
|
retrieval,
|
|
revision: manifest.manifestVersion,
|
|
};
|
|
}
|
|
|
|
async function isPublishedRuntimeModeReady(
|
|
runtimeSnapshotResolver: PublishedKnowledgeSpaceRuntimeSnapshotResolver,
|
|
input: Parameters<PublishedKnowledgeSpaceRuntimeSnapshotResolver["assertReady"]>[0],
|
|
): Promise<boolean> {
|
|
try {
|
|
await runtimeSnapshotResolver.assertReady(input);
|
|
return true;
|
|
} catch (error) {
|
|
if (error instanceof PublishedProjectionReadUnavailableError) return false;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function inferFailedModelFields(code: string): ProductReadinessIssue["field"][] {
|
|
if (code.includes("EMBEDDING") || code.includes("DIMENSION")) return ["embedding"];
|
|
if (code.includes("RERANK")) return ["rerank"];
|
|
if (code.includes("REASON")) return ["reasoning"];
|
|
return ["embedding", "reasoning", "rerank"];
|
|
}
|
|
|
|
function readinessIssueCodeForFailure(code: string): ProductReadinessIssue["code"] {
|
|
if (code === "MODEL_SELECTION_NOT_FOUND" || code.includes("UNAVAILABLE")) return "unavailable";
|
|
if (code.includes("MISMATCH") || code.includes("DIMENSION")) return "incompatible";
|
|
return "validation_failed";
|
|
}
|
|
|
|
async function preflightKnowledgeSpaceModels({
|
|
embedding,
|
|
preflight,
|
|
required = false,
|
|
retrievalProfile,
|
|
tenantId,
|
|
}: {
|
|
readonly embedding?: KnowledgeSpaceEmbeddingSelection | undefined;
|
|
readonly preflight?: ModelCapabilityPreflight | undefined;
|
|
readonly required?: boolean | undefined;
|
|
readonly retrievalProfile?: KnowledgeSpaceRetrievalProfileInput | undefined;
|
|
readonly tenantId: string;
|
|
}): Promise<{
|
|
readonly embedding?: ModelCapabilitySnapshot | undefined;
|
|
readonly reasoning?: ModelCapabilitySnapshot | undefined;
|
|
readonly rerank?: ModelCapabilitySnapshot | undefined;
|
|
}> {
|
|
if (!preflight && required) {
|
|
throw new ModelCapabilityPreflightError(
|
|
"MODEL_PREFLIGHT_UNAVAILABLE",
|
|
"Model capability preflight is unavailable",
|
|
{ retryable: true },
|
|
);
|
|
}
|
|
if (!preflight) {
|
|
return {};
|
|
}
|
|
const [embeddingSnapshot, reasoningSnapshot, rerankSnapshot] = await Promise.all([
|
|
embedding ? preflight.verify({ kind: "embedding", selection: embedding, tenantId }) : undefined,
|
|
retrievalProfile
|
|
? resolveConfiguredModelCapability(preflight, {
|
|
kind: "reasoning",
|
|
selection: retrievalProfile.reasoningModel,
|
|
tenantId,
|
|
})
|
|
: undefined,
|
|
retrievalProfile?.rerank.enabled && retrievalProfile.rerank.model
|
|
? resolveConfiguredModelCapability(preflight, {
|
|
kind: "rerank",
|
|
selection: retrievalProfile.rerank.model,
|
|
tenantId,
|
|
})
|
|
: undefined,
|
|
]);
|
|
return {
|
|
...(embeddingSnapshot ? { embedding: embeddingSnapshot } : {}),
|
|
...(reasoningSnapshot ? { reasoning: reasoningSnapshot } : {}),
|
|
...(rerankSnapshot ? { rerank: rerankSnapshot } : {}),
|
|
};
|
|
}
|
|
|
|
function resolveConfiguredModelCapability(
|
|
preflight: ModelCapabilityPreflight,
|
|
input: Parameters<ModelCapabilityPreflight["verify"]>[0],
|
|
): Promise<ModelCapabilitySnapshot> {
|
|
return preflight.resolveConfigured ? preflight.resolveConfigured(input) : preflight.verify(input);
|
|
}
|
|
|
|
function embeddingVectorSpaceIdentityFromSnapshot(snapshot: ModelCapabilitySnapshot) {
|
|
if (
|
|
snapshot.kind !== "embedding" ||
|
|
snapshot.dimension === undefined ||
|
|
!snapshot.distanceMetric
|
|
) {
|
|
throw new ModelCapabilityPreflightError(
|
|
"MODEL_CAPABILITY_MISMATCH",
|
|
"The embedding model did not declare a usable distance metric",
|
|
);
|
|
}
|
|
return {
|
|
capabilityDigest: snapshot.capabilityDigest,
|
|
dimension: snapshot.dimension,
|
|
distanceMetric: snapshot.distanceMetric,
|
|
pluginUniqueIdentifier: snapshot.pluginUniqueIdentifier,
|
|
schemaFingerprint: snapshot.schemaFingerprint,
|
|
};
|
|
}
|
|
|
|
class LegacyProfileBootstrapError extends Error {
|
|
readonly code: string;
|
|
readonly status: 409 | 503;
|
|
|
|
constructor(code: string, message: string, status: 409 | 503) {
|
|
super(message);
|
|
this.name = "LegacyProfileBootstrapError";
|
|
this.code = code;
|
|
this.status = status;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reconciles a pre-versioned published space without ever activating an unverified model profile.
|
|
* Every missing legacy model is probed first; only after all probes pass are immutable candidates
|
|
* staged and activated. The current publication is then bound to the exact verified heads.
|
|
*/
|
|
export async function ensureLegacyPublishedProfileTuple({
|
|
createdBySubjectId,
|
|
knowledgeSpaceId,
|
|
manifest,
|
|
modelCapabilityPreflight,
|
|
now,
|
|
profilePublicationBindings,
|
|
profiles,
|
|
tenantId,
|
|
}: {
|
|
readonly createdBySubjectId: string;
|
|
readonly knowledgeSpaceId: string;
|
|
readonly manifest: KnowledgeSpaceManifest | null | undefined;
|
|
readonly modelCapabilityPreflight: ModelCapabilityPreflight | undefined;
|
|
readonly now: () => string;
|
|
readonly profilePublicationBindings:
|
|
| Pick<KnowledgeSpaceProfilePublicationRepository, "bindCurrentPublished">
|
|
| undefined;
|
|
readonly profiles: KnowledgeSpaceProfileRepository;
|
|
readonly tenantId: string;
|
|
}): Promise<void> {
|
|
if (!profilePublicationBindings) {
|
|
throw new LegacyProfileBootstrapError(
|
|
"PROFILE_PUBLICATION_BOOTSTRAP_UNAVAILABLE",
|
|
"Legacy published profile bootstrap is unavailable",
|
|
503,
|
|
);
|
|
}
|
|
if (!manifest?.retrievalProfile) {
|
|
throw new LegacyProfileBootstrapError(
|
|
"PROFILE_MIGRATION_BASE_PROFILE_MISSING",
|
|
"Legacy published space has no retrieval profile to bootstrap",
|
|
409,
|
|
);
|
|
}
|
|
|
|
const scope = { knowledgeSpaceId, tenantId };
|
|
const [embeddingHead, retrievalHead] = await Promise.all([
|
|
profiles.getHead({ ...scope, kind: "embedding" }),
|
|
profiles.getHead({ ...scope, kind: "retrieval" }),
|
|
]);
|
|
if (embeddingHead && !verifiedProfileCapability(embeddingHead.profile)) {
|
|
throw new LegacyProfileBootstrapError(
|
|
"PROFILE_PUBLICATION_BOOTSTRAP_PROFILE_UNVERIFIED",
|
|
"Existing embedding profile head is not backed by a verified model capability",
|
|
409,
|
|
);
|
|
}
|
|
if (embeddingHead) {
|
|
await verifyLegacyEmbeddingProfile(
|
|
embeddingHead.profile.snapshot as KnowledgeSpaceEmbeddingProfile,
|
|
ModelCapabilitySnapshotSchema.parse(embeddingHead.profile.capabilitySnapshot),
|
|
);
|
|
}
|
|
if (retrievalHead && !verifiedProfileCapability(retrievalHead.profile)) {
|
|
throw new LegacyProfileBootstrapError(
|
|
"PROFILE_PUBLICATION_BOOTSTRAP_PROFILE_UNVERIFIED",
|
|
"Existing retrieval profile head is not backed by verified model capabilities",
|
|
409,
|
|
);
|
|
}
|
|
|
|
const missingEmbedding = !embeddingHead && manifest.embeddingProfile;
|
|
const missingRetrieval = !retrievalHead ? manifest.retrievalProfile : undefined;
|
|
// Keep historical Research-only manifests readable so an already-bound tuple can be
|
|
// reconciled. New profile-publication bindings reject a missing embedding profile, and the
|
|
// runtime snapshot resolver will not make this legacy tuple query-ready.
|
|
if (
|
|
!embeddingHead &&
|
|
!manifest.embeddingProfile &&
|
|
manifest.retrievalProfile.defaultMode !== "research"
|
|
) {
|
|
throw new LegacyProfileBootstrapError(
|
|
"PROFILE_MIGRATION_BASE_PROFILE_MISSING",
|
|
"Fast/Deep legacy publication has no embedding profile to bootstrap",
|
|
409,
|
|
);
|
|
}
|
|
|
|
if (missingEmbedding || missingRetrieval) {
|
|
const capabilitySnapshots = await preflightKnowledgeSpaceModels({
|
|
...(missingEmbedding ? { embedding: missingEmbedding } : {}),
|
|
preflight: modelCapabilityPreflight,
|
|
required: true,
|
|
...(missingRetrieval ? { retrievalProfile: missingRetrieval } : {}),
|
|
tenantId,
|
|
});
|
|
const candidates: KnowledgeSpaceProfileRevision[] = [];
|
|
if (missingEmbedding) {
|
|
const capability = capabilitySnapshots.embedding;
|
|
if (!capability) {
|
|
throw new ModelCapabilityPreflightError(
|
|
"MODEL_PREFLIGHT_UNAVAILABLE",
|
|
"Legacy embedding capability preflight is unavailable",
|
|
{ retryable: true },
|
|
);
|
|
}
|
|
const verifiedSnapshot = await verifyLegacyEmbeddingProfile(missingEmbedding, capability);
|
|
candidates.push(
|
|
await getOrCreateLegacyProfileCandidate(profiles, {
|
|
capabilitySnapshot: capability,
|
|
createdBySubjectId,
|
|
kind: "embedding",
|
|
knowledgeSpaceId,
|
|
now: now(),
|
|
preserveLegacyInitialRevision: true,
|
|
snapshot: verifiedSnapshot,
|
|
tenantId,
|
|
}),
|
|
);
|
|
}
|
|
if (missingRetrieval) {
|
|
const reasoning = capabilitySnapshots.reasoning;
|
|
if (!reasoning) {
|
|
throw new ModelCapabilityPreflightError(
|
|
"MODEL_PREFLIGHT_UNAVAILABLE",
|
|
"Legacy reasoning capability preflight is unavailable",
|
|
{ retryable: true },
|
|
);
|
|
}
|
|
candidates.push(
|
|
await getOrCreateLegacyProfileCandidate(profiles, {
|
|
capabilitySnapshot: {
|
|
reasoning,
|
|
rerank: capabilitySnapshots.rerank ?? null,
|
|
verification: "verified",
|
|
},
|
|
createdBySubjectId,
|
|
kind: "retrieval",
|
|
knowledgeSpaceId,
|
|
now: now(),
|
|
preserveLegacyInitialRevision: true,
|
|
snapshot: missingRetrieval,
|
|
tenantId,
|
|
}),
|
|
);
|
|
}
|
|
for (const candidate of candidates) {
|
|
try {
|
|
await profiles.activateCandidate({
|
|
expectedActiveRevision: null,
|
|
kind: candidate.kind,
|
|
knowledgeSpaceId,
|
|
now: now(),
|
|
revision: candidate.revision,
|
|
tenantId,
|
|
});
|
|
} catch (error) {
|
|
const raced = await profiles.getHead({ kind: candidate.kind, ...scope });
|
|
if (
|
|
!raced ||
|
|
raced.profile.snapshotDigest !== candidate.snapshotDigest ||
|
|
raced.profile.capabilitySnapshotDigest !== candidate.capabilitySnapshotDigest
|
|
) {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
await profilePublicationBindings.bindCurrentPublished({
|
|
knowledgeSpaceId,
|
|
tenantId,
|
|
verifiedAt: now(),
|
|
});
|
|
}
|
|
|
|
async function getOrCreateLegacyProfileCandidate(
|
|
profiles: KnowledgeSpaceProfileRepository,
|
|
input: Parameters<KnowledgeSpaceProfileRepository["createCandidate"]>[0],
|
|
): Promise<KnowledgeSpaceProfileRevision> {
|
|
const expectedSnapshotDigest = knowledgeSpaceProfileSnapshotDigest(input.snapshot);
|
|
const lookup = async () => {
|
|
const existing = await profiles.getRevision({
|
|
kind: input.kind,
|
|
knowledgeSpaceId: input.knowledgeSpaceId,
|
|
revision: input.snapshot.revision,
|
|
tenantId: input.tenantId,
|
|
});
|
|
if (
|
|
existing?.state === "candidate" &&
|
|
existing.snapshotDigest === expectedSnapshotDigest &&
|
|
equivalentLegacyCapabilitySnapshot(
|
|
input.kind,
|
|
existing.capabilitySnapshot,
|
|
input.capabilitySnapshot,
|
|
)
|
|
) {
|
|
return existing;
|
|
}
|
|
if (existing) throw new SettingsProfileCandidateConflictError();
|
|
return null;
|
|
};
|
|
const replay = await lookup();
|
|
if (replay) return replay;
|
|
try {
|
|
return await profiles.createCandidate(input);
|
|
} catch (error) {
|
|
if (
|
|
error instanceof KnowledgeSpaceProfileTransitionError &&
|
|
(error.code === "KNOWLEDGE_SPACE_PROFILE_CANDIDATE_EXISTS" ||
|
|
error.code === "KNOWLEDGE_SPACE_PROFILE_REVISION_CONFLICT")
|
|
) {
|
|
const concurrent = await lookup();
|
|
if (concurrent) return concurrent;
|
|
throw new SettingsProfileCandidateConflictError();
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function equivalentLegacyCapabilitySnapshot(
|
|
kind: "embedding" | "retrieval",
|
|
left: Readonly<Record<string, unknown>>,
|
|
right: Readonly<Record<string, unknown>>,
|
|
): boolean {
|
|
if (kind === "embedding") {
|
|
const leftEmbedding = ModelCapabilitySnapshotSchema.safeParse(left);
|
|
const rightEmbedding = ModelCapabilitySnapshotSchema.safeParse(right);
|
|
return Boolean(
|
|
leftEmbedding.success &&
|
|
rightEmbedding.success &&
|
|
leftEmbedding.data.kind === "embedding" &&
|
|
rightEmbedding.data.kind === "embedding" &&
|
|
leftEmbedding.data.capabilityDigest === rightEmbedding.data.capabilityDigest,
|
|
);
|
|
}
|
|
if (left.verification !== "verified" || right.verification !== "verified") return false;
|
|
const leftReasoning = ModelCapabilitySnapshotSchema.safeParse(left.reasoning);
|
|
const rightReasoning = ModelCapabilitySnapshotSchema.safeParse(right.reasoning);
|
|
if (
|
|
!leftReasoning.success ||
|
|
!rightReasoning.success ||
|
|
leftReasoning.data.kind !== "reasoning" ||
|
|
rightReasoning.data.kind !== "reasoning" ||
|
|
leftReasoning.data.capabilityDigest !== rightReasoning.data.capabilityDigest
|
|
) {
|
|
return false;
|
|
}
|
|
if (left.rerank === null || right.rerank === null) return left.rerank === right.rerank;
|
|
const leftRerank = ModelCapabilitySnapshotSchema.safeParse(left.rerank);
|
|
const rightRerank = ModelCapabilitySnapshotSchema.safeParse(right.rerank);
|
|
return Boolean(
|
|
leftRerank.success &&
|
|
rightRerank.success &&
|
|
leftRerank.data.kind === "rerank" &&
|
|
rightRerank.data.kind === "rerank" &&
|
|
leftRerank.data.capabilityDigest === rightRerank.data.capabilityDigest,
|
|
);
|
|
}
|
|
|
|
async function verifyLegacyEmbeddingProfile(
|
|
snapshot: KnowledgeSpaceEmbeddingProfile,
|
|
capability: ModelCapabilitySnapshot,
|
|
): Promise<KnowledgeSpaceEmbeddingProfile> {
|
|
if (
|
|
capability.kind !== "embedding" ||
|
|
capability.dimension === undefined ||
|
|
!capability.distanceMetric ||
|
|
!sameModelSelection(capability.selection, snapshot)
|
|
) {
|
|
throw new LegacyProfileBootstrapError(
|
|
"PROFILE_PUBLICATION_BOOTSTRAP_CAPABILITY_MISMATCH",
|
|
"Legacy embedding capability does not match its frozen model selection",
|
|
409,
|
|
);
|
|
}
|
|
const selection = {
|
|
model: snapshot.model,
|
|
pluginId: snapshot.pluginId,
|
|
provider: snapshot.provider,
|
|
};
|
|
const [legacyVectorSpaceId, capabilityBoundVectorSpaceId] = await Promise.all([
|
|
buildKnowledgeSpaceVectorSpaceId(selection, snapshot.revision),
|
|
buildKnowledgeSpaceVectorSpaceId(selection, snapshot.revision, {
|
|
capabilityDigest: capability.capabilityDigest,
|
|
dimension: capability.dimension,
|
|
distanceMetric: capability.distanceMetric,
|
|
pluginUniqueIdentifier: capability.pluginUniqueIdentifier,
|
|
schemaFingerprint: capability.schemaFingerprint,
|
|
}),
|
|
]);
|
|
if (
|
|
snapshot.vectorSpaceId !== legacyVectorSpaceId &&
|
|
snapshot.vectorSpaceId !== capabilityBoundVectorSpaceId
|
|
) {
|
|
throw new LegacyProfileBootstrapError(
|
|
"PROFILE_PUBLICATION_BOOTSTRAP_VECTOR_SPACE_UNPROVEN",
|
|
"Legacy embedding vector-space identity is not proven by the installed model capability",
|
|
409,
|
|
);
|
|
}
|
|
if (snapshot.dimension !== undefined && snapshot.dimension !== capability.dimension) {
|
|
throw new LegacyProfileBootstrapError(
|
|
"PROFILE_PUBLICATION_BOOTSTRAP_DIMENSION_CONFLICT",
|
|
"Legacy embedding dimension conflicts with the observed model dimension",
|
|
409,
|
|
);
|
|
}
|
|
return KnowledgeSpaceEmbeddingProfileSchema.parse({
|
|
...snapshot,
|
|
dimension: capability.dimension,
|
|
});
|
|
}
|
|
|
|
function verifiedProfileCapability(profile: KnowledgeSpaceProfileRevision): boolean {
|
|
if (profile.kind === "embedding") {
|
|
const snapshot = profile.snapshot as KnowledgeSpaceEmbeddingProfile;
|
|
const capability = ModelCapabilitySnapshotSchema.safeParse(profile.capabilitySnapshot);
|
|
return Boolean(
|
|
capability.success &&
|
|
capability.data.kind === "embedding" &&
|
|
snapshot.dimension !== undefined &&
|
|
capability.data.dimension === snapshot.dimension &&
|
|
sameModelSelection(capability.data.selection, snapshot),
|
|
);
|
|
}
|
|
const snapshot = profile.snapshot as KnowledgeSpaceRetrievalProfile;
|
|
if (profile.capabilitySnapshot.verification !== "verified") return false;
|
|
const reasoning = ModelCapabilitySnapshotSchema.safeParse(profile.capabilitySnapshot.reasoning);
|
|
if (
|
|
!reasoning.success ||
|
|
reasoning.data.kind !== "reasoning" ||
|
|
!sameModelSelection(reasoning.data.selection, snapshot.reasoningModel)
|
|
) {
|
|
return false;
|
|
}
|
|
if (!snapshot.rerank.enabled) return profile.capabilitySnapshot.rerank === null;
|
|
if (!snapshot.rerank.model) return false;
|
|
const rerank = ModelCapabilitySnapshotSchema.safeParse(profile.capabilitySnapshot.rerank);
|
|
return Boolean(
|
|
rerank.success &&
|
|
rerank.data.kind === "rerank" &&
|
|
sameModelSelection(rerank.data.selection, snapshot.rerank.model),
|
|
);
|
|
}
|
|
|
|
function sameModelSelection(
|
|
left: { readonly model: string; readonly pluginId: string; readonly provider: string },
|
|
right: { readonly model: string; readonly pluginId: string; readonly provider: string },
|
|
): boolean {
|
|
return (
|
|
left.model === right.model &&
|
|
left.pluginId === right.pluginId &&
|
|
left.provider === right.provider
|
|
);
|
|
}
|
|
|
|
class SettingsProfileCandidateConflictError extends Error {
|
|
readonly code = "KNOWLEDGE_SPACE_SETTINGS_CANDIDATE_CONFLICT";
|
|
|
|
constructor() {
|
|
super("Another settings update already owns the next immutable profile revision");
|
|
this.name = "SettingsProfileCandidateConflictError";
|
|
}
|
|
}
|
|
|
|
async function getOrSupersedeRetrievalSettingsCandidate(input: {
|
|
readonly activeRevision: number;
|
|
readonly capabilitySnapshot: Readonly<Record<string, unknown>>;
|
|
readonly createdBySubjectId: string;
|
|
readonly migrationPrincipal: KnowledgeSpaceProfileMigrationPrincipal & {
|
|
readonly knowledgeSpaceId: string;
|
|
};
|
|
readonly migrations: KnowledgeSpaceProfileMigrationService;
|
|
readonly now: () => string;
|
|
readonly profile: KnowledgeSpaceRetrievalProfileInput;
|
|
readonly profiles: KnowledgeSpaceProfileRepository;
|
|
}): Promise<KnowledgeSpaceProfileRevision> {
|
|
for (;;) {
|
|
const candidateRevision = await nextPublishedRetrievalCandidateRevision(input.profiles, {
|
|
activeRevision: input.activeRevision,
|
|
knowledgeSpaceId: input.migrationPrincipal.knowledgeSpaceId,
|
|
tenantId: input.migrationPrincipal.subject.tenantId,
|
|
});
|
|
const candidateInput = {
|
|
capabilitySnapshot: input.capabilitySnapshot,
|
|
createdBySubjectId: input.createdBySubjectId,
|
|
kind: "retrieval" as const,
|
|
knowledgeSpaceId: input.migrationPrincipal.knowledgeSpaceId,
|
|
now: input.now(),
|
|
snapshot: createKnowledgeSpaceRetrievalProfile(input.profile, candidateRevision),
|
|
tenantId: input.migrationPrincipal.subject.tenantId,
|
|
};
|
|
try {
|
|
return await getOrCreateSettingsProfileCandidate(input.profiles, candidateInput);
|
|
} catch (error) {
|
|
if (!(error instanceof SettingsProfileCandidateConflictError)) throw error;
|
|
const existing = await input.profiles.getRevision({
|
|
kind: "retrieval",
|
|
knowledgeSpaceId: input.migrationPrincipal.knowledgeSpaceId,
|
|
revision: candidateRevision,
|
|
tenantId: input.migrationPrincipal.subject.tenantId,
|
|
});
|
|
if (
|
|
!existing ||
|
|
existing.state !== "candidate" ||
|
|
existing.createdBySubjectId !== input.createdBySubjectId ||
|
|
existing.snapshotDigest === knowledgeSpaceProfileSnapshotDigest(candidateInput.snapshot)
|
|
) {
|
|
throw error;
|
|
}
|
|
|
|
const previousMigration = await input.migrations.findByCandidate({
|
|
...input.migrationPrincipal,
|
|
candidateProfileId: existing.id,
|
|
});
|
|
if (previousMigration) {
|
|
const canceled = await input.migrations.cancel({
|
|
...input.migrationPrincipal,
|
|
reason: "Superseded by a newer retrieval settings update",
|
|
runId: previousMigration.id,
|
|
});
|
|
if (!canceled || canceled.runState !== "canceled") throw error;
|
|
} else {
|
|
try {
|
|
await input.profiles.failCandidate({
|
|
errorCode: "PROFILE_SETTINGS_CANDIDATE_SUPERSEDED",
|
|
errorMessage: "Superseded by a newer retrieval settings update",
|
|
kind: "retrieval",
|
|
knowledgeSpaceId: input.migrationPrincipal.knowledgeSpaceId,
|
|
now: input.now(),
|
|
revision: existing.revision,
|
|
tenantId: input.migrationPrincipal.subject.tenantId,
|
|
});
|
|
} catch (retirementError) {
|
|
if (retirementError instanceof KnowledgeSpaceProfileTransitionError) throw error;
|
|
throw retirementError;
|
|
}
|
|
}
|
|
|
|
const retired = await input.profiles.getRevision({
|
|
kind: "retrieval",
|
|
knowledgeSpaceId: input.migrationPrincipal.knowledgeSpaceId,
|
|
revision: existing.revision,
|
|
tenantId: input.migrationPrincipal.subject.tenantId,
|
|
});
|
|
if (!retired || retired.state === "candidate") throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function nextPublishedRetrievalCandidateRevision(
|
|
profiles: KnowledgeSpaceProfileRepository,
|
|
input: {
|
|
readonly activeRevision: number;
|
|
readonly knowledgeSpaceId: string;
|
|
readonly tenantId: string;
|
|
},
|
|
): Promise<number> {
|
|
let nextRevision = input.activeRevision + 1;
|
|
let afterRevision: number | undefined = input.activeRevision;
|
|
for (;;) {
|
|
const page = await profiles.listRevisions({
|
|
...(afterRevision === undefined ? {} : { afterRevision }),
|
|
kind: "retrieval",
|
|
knowledgeSpaceId: input.knowledgeSpaceId,
|
|
limit: 10,
|
|
tenantId: input.tenantId,
|
|
});
|
|
for (const revision of page.items) {
|
|
if (revision.revision > nextRevision) return nextRevision;
|
|
if (revision.revision !== nextRevision) continue;
|
|
if (revision.state === "candidate") return nextRevision;
|
|
nextRevision += 1;
|
|
}
|
|
if (page.nextRevision === undefined) return nextRevision;
|
|
afterRevision = page.nextRevision;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Candidate allocation happens before the migration request ledger is written. Re-reading the
|
|
* exact immutable revision makes a client retry safe after a response disconnect, while refusing
|
|
* to attach a different settings request to somebody else's pending candidate.
|
|
*/
|
|
async function getOrCreateSettingsProfileCandidate(
|
|
profiles: KnowledgeSpaceProfileRepository,
|
|
input: Parameters<KnowledgeSpaceProfileRepository["createCandidate"]>[0],
|
|
): Promise<KnowledgeSpaceProfileRevision> {
|
|
const expectedSnapshotDigest = knowledgeSpaceProfileSnapshotDigest(input.snapshot);
|
|
const expectedCapabilitySemanticsDigest = settingsCapabilitySnapshotSemanticsDigest(
|
|
input.capabilitySnapshot,
|
|
);
|
|
const lookup = async () => {
|
|
const existing = await profiles.getRevision({
|
|
kind: input.kind,
|
|
knowledgeSpaceId: input.knowledgeSpaceId,
|
|
revision: input.snapshot.revision,
|
|
tenantId: input.tenantId,
|
|
});
|
|
if (!existing) return null;
|
|
if (
|
|
existing.state === "candidate" &&
|
|
existing.snapshotDigest === expectedSnapshotDigest &&
|
|
settingsCapabilitySnapshotSemanticsDigest(existing.capabilitySnapshot) ===
|
|
expectedCapabilitySemanticsDigest &&
|
|
existing.createdBySubjectId === input.createdBySubjectId
|
|
) {
|
|
return existing;
|
|
}
|
|
throw new SettingsProfileCandidateConflictError();
|
|
};
|
|
|
|
const replay = await lookup();
|
|
if (replay) return replay;
|
|
try {
|
|
return await profiles.createCandidate(input);
|
|
} catch (error) {
|
|
if (
|
|
error instanceof KnowledgeSpaceProfileTransitionError &&
|
|
(error.code === "KNOWLEDGE_SPACE_PROFILE_CANDIDATE_EXISTS" ||
|
|
error.code === "KNOWLEDGE_SPACE_PROFILE_REVISION_CONFLICT")
|
|
) {
|
|
const concurrentReplay = await lookup();
|
|
if (concurrentReplay) return concurrentReplay;
|
|
throw new SettingsProfileCandidateConflictError();
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function settingsCapabilitySnapshotSemanticsDigest(
|
|
snapshot: Readonly<Record<string, unknown>>,
|
|
): string {
|
|
const directCapability = ModelCapabilitySnapshotSchema.safeParse(snapshot);
|
|
if (directCapability.success) {
|
|
return knowledgeSpaceProfileSnapshotDigest(
|
|
modelCapabilitySnapshotWithoutObservationTime(directCapability.data),
|
|
);
|
|
}
|
|
return knowledgeSpaceProfileSnapshotDigest(
|
|
Object.fromEntries(
|
|
Object.entries(snapshot).map(([key, value]) => {
|
|
const nestedCapability = ModelCapabilitySnapshotSchema.safeParse(value);
|
|
return [
|
|
key,
|
|
nestedCapability.success
|
|
? modelCapabilitySnapshotWithoutObservationTime(nestedCapability.data)
|
|
: value,
|
|
];
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
function modelCapabilitySnapshotWithoutObservationTime(
|
|
snapshot: ModelCapabilitySnapshot,
|
|
): Readonly<Record<string, unknown>> {
|
|
return Object.fromEntries(Object.entries(snapshot).filter(([key]) => key !== "checkedAt"));
|
|
}
|
|
|
|
const STATUS_ITEM_LIMIT = 10;
|
|
const OPERATOR_PAGE_LIMIT = 100;
|
|
|
|
type PublicKnowledgeSpaceConfigurationStatus =
|
|
| "setup-required"
|
|
| "pending-validation"
|
|
| "validation-failed"
|
|
| "ready";
|
|
|
|
type PublicPendingModelConfiguration = {
|
|
readonly digest: string;
|
|
readonly revision: number;
|
|
} & (
|
|
| { readonly state: "pending-validation" }
|
|
| {
|
|
readonly failure: {
|
|
readonly code: string;
|
|
readonly failedAt: string;
|
|
readonly retryable: boolean;
|
|
};
|
|
readonly state: "validation-failed";
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Produces the operator-facing configuration state from durable active heads plus the candidate
|
|
* marker. Active heads remain authoritative while an update is pending or failed: a bad candidate
|
|
* must not make an already-queryable space appear unavailable. Candidate selections and raw
|
|
* provider failure messages are deliberately excluded from this diagnostics response.
|
|
*/
|
|
async function resolveKnowledgeSpaceConfigurationStatus({
|
|
knowledgeSpaceId,
|
|
manifest,
|
|
profiles,
|
|
tenantId,
|
|
}: {
|
|
readonly knowledgeSpaceId: string;
|
|
readonly manifest: KnowledgeSpaceManifest;
|
|
readonly profiles: KnowledgeSpaceProfileRepository | undefined;
|
|
readonly tenantId: string;
|
|
}): Promise<{
|
|
readonly activeProfiles: {
|
|
readonly embeddingRevision?: number | undefined;
|
|
readonly retrievalRevision?: number | undefined;
|
|
};
|
|
readonly availableModes: ("deep" | "fast" | "research")[];
|
|
readonly pendingModelConfiguration?: PublicPendingModelConfiguration | undefined;
|
|
readonly status: PublicKnowledgeSpaceConfigurationStatus;
|
|
}> {
|
|
const [embeddingHead, retrievalHead] = profiles
|
|
? await Promise.all([
|
|
profiles.getHead({ kind: "embedding", knowledgeSpaceId, tenantId }),
|
|
profiles.getHead({ kind: "retrieval", knowledgeSpaceId, tenantId }),
|
|
])
|
|
: [null, null];
|
|
const embeddingProfile = profiles
|
|
? KnowledgeSpaceEmbeddingProfileSchema.safeParse(embeddingHead?.profile.snapshot).data
|
|
: manifest.embeddingProfile;
|
|
const retrievalProfile = profiles
|
|
? KnowledgeSpaceRetrievalProfileSchema.safeParse(retrievalHead?.profile.snapshot).data
|
|
: manifest.retrievalProfile;
|
|
const activeReady = Boolean(
|
|
retrievalProfile &&
|
|
embeddingProfile &&
|
|
hasRequiredKnowledgeSpaceRetrievalModels(retrievalProfile),
|
|
);
|
|
const pendingResult = readPendingModelConfiguration(manifest);
|
|
const pendingModelConfiguration =
|
|
pendingResult.kind === "valid" ? pendingResult.configuration : undefined;
|
|
const status: PublicKnowledgeSpaceConfigurationStatus = activeReady
|
|
? "ready"
|
|
: pendingResult.kind === "invalid" || pendingModelConfiguration?.state === "validation-failed"
|
|
? "validation-failed"
|
|
: pendingModelConfiguration
|
|
? "pending-validation"
|
|
: "setup-required";
|
|
|
|
return {
|
|
activeProfiles: {
|
|
...(embeddingProfile ? { embeddingRevision: embeddingProfile.revision } : {}),
|
|
...(retrievalProfile ? { retrievalRevision: retrievalProfile.revision } : {}),
|
|
},
|
|
availableModes: activeReady
|
|
? embeddingProfile
|
|
? ["fast", "research", "deep"]
|
|
: ["research"]
|
|
: [],
|
|
...(pendingModelConfiguration ? { pendingModelConfiguration } : {}),
|
|
status,
|
|
};
|
|
}
|
|
|
|
function readPendingModelConfiguration(
|
|
manifest: KnowledgeSpaceManifest,
|
|
):
|
|
| { readonly kind: "absent" }
|
|
| { readonly kind: "invalid" }
|
|
| { readonly configuration: PublicPendingModelConfiguration; readonly kind: "valid" } {
|
|
const extended = manifest as KnowledgeSpaceManifest & {
|
|
readonly pendingModelConfiguration?: unknown;
|
|
};
|
|
const candidate =
|
|
extended.pendingModelConfiguration ?? manifest.metadata.__knowledgeFsPendingModelConfiguration;
|
|
if (candidate === undefined) return { kind: "absent" };
|
|
if (!isPlainRecord(candidate)) return { kind: "invalid" };
|
|
|
|
const revision = candidate.revision;
|
|
const digest = candidate.digest;
|
|
const rawState = candidate.state;
|
|
if (
|
|
!Number.isSafeInteger(revision) ||
|
|
(revision as number) < 1 ||
|
|
typeof digest !== "string" ||
|
|
!/^(?:sha256:)?[a-f0-9]{64}$/.test(digest) ||
|
|
(rawState !== "pending-validation" &&
|
|
rawState !== "validating" &&
|
|
rawState !== "validation-failed")
|
|
) {
|
|
return { kind: "invalid" };
|
|
}
|
|
|
|
if (rawState !== "validation-failed") {
|
|
if (candidate.failure !== undefined) return { kind: "invalid" };
|
|
return {
|
|
configuration: {
|
|
digest,
|
|
revision: revision as number,
|
|
state: "pending-validation",
|
|
},
|
|
kind: "valid",
|
|
};
|
|
}
|
|
|
|
if (!isPlainRecord(candidate.failure)) return { kind: "invalid" };
|
|
const code = candidate.failure.code;
|
|
const failedAt = candidate.failure.failedAt;
|
|
const retryable = candidate.failure.retryable;
|
|
if (
|
|
typeof code !== "string" ||
|
|
!/^[A-Za-z0-9._:-]{1,64}$/.test(code) ||
|
|
typeof failedAt !== "string" ||
|
|
Number.isNaN(Date.parse(failedAt)) ||
|
|
typeof retryable !== "boolean"
|
|
) {
|
|
return { kind: "invalid" };
|
|
}
|
|
return {
|
|
configuration: {
|
|
digest,
|
|
failure: {
|
|
code,
|
|
failedAt: new Date(failedAt).toISOString(),
|
|
retryable,
|
|
},
|
|
revision: revision as number,
|
|
state: "validation-failed",
|
|
},
|
|
kind: "valid",
|
|
};
|
|
}
|
|
|
|
function isPlainRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function projectionVersionFromManifest(projectionSetVersion: string): number {
|
|
const match = /(?:^|[^0-9])v?([1-9][0-9]*)$/.exec(projectionSetVersion);
|
|
|
|
return match ? Number(match[1]) : 1;
|
|
}
|
|
|
|
function isFailedCommitDiagnostic(
|
|
commit: KnowledgeSpaceStagedCommit,
|
|
): commit is KnowledgeSpaceStagedCommit & {
|
|
readonly status: "failed-retryable" | "failed-terminal";
|
|
} {
|
|
return commit.status === "failed-retryable" || commit.status === "failed-terminal";
|
|
}
|
|
|
|
type PublicKnowledgeSpaceStagedCommit = Omit<
|
|
KnowledgeSpaceStagedCommit,
|
|
"errorCode" | "errorMessage"
|
|
> & {
|
|
readonly errorCode?: KnowledgeFsPublicFailure["code"] | undefined;
|
|
readonly errorMessage?: string | undefined;
|
|
readonly failure?: KnowledgeFsPublicFailure | undefined;
|
|
};
|
|
|
|
function toPublicKnowledgeSpaceStagedCommit(
|
|
commit: KnowledgeSpaceStagedCommit,
|
|
): PublicKnowledgeSpaceStagedCommit {
|
|
const { errorCode: _errorCode, errorMessage: _errorMessage, ...safeCommit } = commit;
|
|
if (!isFailedCommitDiagnostic(commit)) return safeCommit;
|
|
|
|
const failure = knowledgeFsFailureForCode(commit.errorCode ?? "DOCUMENT_COMPILATION_FAILED", {
|
|
stage: commit.operationType,
|
|
traceId: commit.id,
|
|
});
|
|
return {
|
|
...safeCommit,
|
|
errorCode: failure.code,
|
|
errorMessage: failure.message,
|
|
failure,
|
|
};
|
|
}
|
|
|
|
async function listAuthorizedSpacesFallback(input: {
|
|
readonly authorization: KnowledgeSpaceAuthorizationGuard;
|
|
readonly callerKind: KnowledgeSpaceCallerKind;
|
|
readonly cursor?: string | undefined;
|
|
readonly limit: number;
|
|
readonly spaces: KnowledgeSpaceRepository;
|
|
readonly subject: AuthSubject;
|
|
}): Promise<{ readonly items: KnowledgeSpace[]; readonly nextCursor?: string | undefined }> {
|
|
const visible: KnowledgeSpace[] = [];
|
|
let scanCursor = input.cursor;
|
|
// This path exists for bounded in-memory/custom repositories. Production database repositories
|
|
// use a membership/visibility SQL join before LIMIT instead of tenant-page-then-filter.
|
|
for (let pageNumber = 0; pageNumber < 100 && visible.length <= input.limit; pageNumber += 1) {
|
|
const page = await input.spaces.list({
|
|
...(scanCursor ? { cursor: scanCursor } : {}),
|
|
// The custom repository has already accepted the public request limit, but may enforce a
|
|
// lower max than the gateway's production default. Reuse that validated limit while paging
|
|
// until we have one extra authorized result for the outward cursor.
|
|
limit: input.limit,
|
|
tenantId: input.subject.tenantId,
|
|
});
|
|
for (const space of page.items) {
|
|
try {
|
|
await input.authorization.authorize({
|
|
callerKind: input.callerKind,
|
|
knowledgeSpaceId: space.id,
|
|
requiredAccess: "read",
|
|
subject: input.subject,
|
|
});
|
|
visible.push(space);
|
|
if (visible.length > input.limit) {
|
|
break;
|
|
}
|
|
} catch (error) {
|
|
if (!(error instanceof KnowledgeSpaceAuthorizationError)) {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
if (visible.length > input.limit || !page.nextCursor) {
|
|
break;
|
|
}
|
|
scanCursor = page.nextCursor;
|
|
}
|
|
const items = visible.slice(0, input.limit);
|
|
const nextCursor = visible.length > input.limit ? items.at(-1)?.slug : undefined;
|
|
return { items, ...(nextCursor ? { nextCursor } : {}) };
|
|
}
|
|
|
|
function subtractMinutes(timestamp: string, minutes: number): string {
|
|
return new Date(new Date(timestamp).getTime() - minutes * 60_000).toISOString();
|
|
}
|
|
|
|
async function safeCacheStats(adapter: PlatformAdapter): Promise<{
|
|
readonly available: boolean;
|
|
readonly entries: number;
|
|
readonly totalBytes: number;
|
|
}> {
|
|
try {
|
|
const stats = await adapter.cache.stats();
|
|
|
|
return {
|
|
available: true,
|
|
entries: stats.entries,
|
|
totalBytes: stats.totalBytes,
|
|
};
|
|
} catch {
|
|
return {
|
|
available: false,
|
|
entries: 0,
|
|
totalBytes: 0,
|
|
};
|
|
}
|
|
}
|
|
|
|
async function safeObjectStorageHealth(adapter: PlatformAdapter): Promise<boolean> {
|
|
try {
|
|
return await adapter.objectStorage.health();
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function summarizeProjectionStatus({
|
|
knowledgeSpaceId,
|
|
projectionVersion,
|
|
projections,
|
|
}: {
|
|
readonly knowledgeSpaceId: string;
|
|
readonly projectionVersion: number;
|
|
readonly projections: IndexProjectionRepository;
|
|
}): Promise<{
|
|
readonly denseVector: IndexProjectionVersionSummary;
|
|
readonly fts: IndexProjectionVersionSummary;
|
|
readonly graph: IndexProjectionVersionSummary;
|
|
readonly metadata: IndexProjectionVersionSummary;
|
|
}> {
|
|
const [denseVector, fts, graph, metadata] = await Promise.all([
|
|
projections.summarizeVersion({
|
|
knowledgeSpaceId,
|
|
projectionVersion,
|
|
type: "dense-vector",
|
|
}),
|
|
projections.summarizeVersion({
|
|
knowledgeSpaceId,
|
|
projectionVersion,
|
|
type: "fts",
|
|
}),
|
|
projections.summarizeVersion({
|
|
knowledgeSpaceId,
|
|
projectionVersion,
|
|
type: "graph",
|
|
}),
|
|
projections.summarizeVersion({
|
|
knowledgeSpaceId,
|
|
projectionVersion,
|
|
type: "metadata",
|
|
}),
|
|
]);
|
|
|
|
return {
|
|
denseVector,
|
|
fts,
|
|
graph,
|
|
metadata,
|
|
};
|
|
}
|