Files
dify/api/dev/generate_knowledge_fs_contract.py

733 lines
31 KiB
Python

"""Pin the in-repository KnowledgeFS contract and validate every Dify product operation.
The lock is intentionally independent of the enclosing Dify commit: it records the staged ``knowledge-fs/`` tree,
the complete generated OpenAPI document, both explicit product-operation manifests, and the active Capability v2
profile and deterministic public-key vector. The full OpenAPI hash
covers request/response schemas, status codes, security, deprecation, and stream metadata. Field-level validation
cross-checks the Dify product registry, Python Capability issuer, TypeScript request guard, and exported OpenAPI;
each product operation must be ready or an explicit gap, and KFS-only activation remains explicitly internal.
Contract export reads the working tree only after proving it matches the staged KnowledgeFS index. This keeps the
OpenAPI bytes and auth manifest aligned with the exact subtree tree ID that will be reviewed and committed.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Any, Literal, TypedDict, cast
import jwt
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
from jwt.algorithms import RSAAlgorithm
API_ROOT = Path(__file__).resolve().parents[1]
if str(API_ROOT) not in sys.path:
sys.path.insert(0, str(API_ROOT))
from dev.knowledge_fs_product_contract import (
capability_operation_runtime_contracts,
parse_capability_operation_policy,
parse_product_operation_gap_manifest,
parse_product_operation_manifest,
product_operation_runtime_contracts,
validate_product_operation_contracts,
)
WORKSPACE_ROOT = API_ROOT.parent
KNOWLEDGE_FS_DIRECTORY = "knowledge-fs"
CAPABILITY_V2_AUTH_MANIFEST_RELATIVE_PATH = Path("contracts/dify-capability-v2-auth-profile.json")
CAPABILITY_V2_AUTH_TEST_VECTOR_RELATIVE_PATH = Path("contracts/dify-capability-v2-test-vector.json")
UPSTREAM_PROVENANCE_RELATIVE_PATH = Path("upstream-provenance.json")
LOCK_RELATIVE_PATH = Path("api/knowledge-fs-contract.lock.json")
PRODUCT_OPERATIONS_RELATIVE_PATH = Path("api/knowledge-fs-product-operations.json")
PRODUCT_OPERATION_GAPS_RELATIVE_PATH = Path("api/knowledge-fs-product-operation-gaps.json")
OPENAPI_METHODS = ("delete", "get", "head", "options", "patch", "post", "put", "trace")
PROXY_METHODS = frozenset({"delete", "get", "patch", "post", "put"})
LOCK_SCHEMA_VERSION = 5
class ContractDeclaration(TypedDict):
"""KnowledgeFS transport contract declared by one Dify Console registry entry."""
operation_id: str
method: str
path: str
required_scope: str | None
response_kind: str
max_response_bytes: int
request_headers: tuple[str, ...]
response_headers: tuple[str, ...]
response_media_types: tuple[str, ...]
class ContractLock(TypedDict):
"""Content-addressed contract inputs that must move together."""
schemaVersion: int
subtreeTree: str
openapiSha256: str
capabilityV2AuthManifestSha256: str
capabilityV2AuthTestVectorSha256: str
productOperationManifestSha256: str
productOperationGapManifestSha256: str
type DeclarationField = Literal[
"method",
"path",
"required_scope",
"response_kind",
"max_response_bytes",
"request_headers",
"response_headers",
"response_media_types",
]
DECLARATION_FIELDS: tuple[DeclarationField, ...] = (
"method",
"path",
"required_scope",
"response_kind",
"max_response_bytes",
"request_headers",
"response_headers",
"response_media_types",
)
def main() -> None:
"""Update or verify the monorepo pin and validate Dify product declarations."""
parser = argparse.ArgumentParser()
mode = parser.add_mutually_exclusive_group(required=True)
mode.add_argument("--check", action="store_true")
mode.add_argument("--update-lock", action="store_true")
parser.add_argument("--workspace-root", type=Path, default=WORKSPACE_ROOT)
args = parser.parse_args()
workspace_root = args.workspace_root.resolve()
knowledge_fs_root = workspace_root / KNOWLEDGE_FS_DIRECTORY
lock_path = workspace_root / LOCK_RELATIVE_PATH
capability_v2_auth_manifest_path = knowledge_fs_root / CAPABILITY_V2_AUTH_MANIFEST_RELATIVE_PATH
capability_v2_auth_test_vector_path = knowledge_fs_root / CAPABILITY_V2_AUTH_TEST_VECTOR_RELATIVE_PATH
product_operations_path = workspace_root / PRODUCT_OPERATIONS_RELATIVE_PATH
product_operation_gaps_path = workspace_root / PRODUCT_OPERATION_GAPS_RELATIVE_PATH
upstream_provenance_path = knowledge_fs_root / UPSTREAM_PROVENANCE_RELATIVE_PATH
ensure_clean_knowledge_fs_worktree(workspace_root)
ensure_contract_inputs_exist(
knowledge_fs_root=knowledge_fs_root,
capability_v2_auth_manifest_path=capability_v2_auth_manifest_path,
capability_v2_auth_test_vector_path=capability_v2_auth_test_vector_path,
product_operations_path=product_operations_path,
product_operation_gaps_path=product_operation_gaps_path,
upstream_provenance_path=upstream_provenance_path,
)
subtree_tree = staged_subtree_tree(workspace_root)
capability_v2_auth_manifest_content = capability_v2_auth_manifest_path.read_bytes()
capability_v2_auth_test_vector_content = capability_v2_auth_test_vector_path.read_bytes()
product_operation_manifest_content = product_operations_path.read_bytes()
product_operation_gap_manifest_content = product_operation_gaps_path.read_bytes()
capability_v2_auth_manifest = load_json_object(capability_v2_auth_manifest_path)
validate_capability_v2_auth_manifest(capability_v2_auth_manifest)
validate_capability_v2_auth_test_vector(
load_json_object(capability_v2_auth_test_vector_path),
capability_v2_auth_manifest,
)
validate_upstream_provenance(load_json_object(upstream_provenance_path))
product_manifest = parse_product_operation_manifest(load_json_object(product_operations_path))
product_gap_manifest = parse_product_operation_gap_manifest(load_json_object(product_operation_gaps_path))
product_runtime_operations = product_operation_runtime_contracts()
capability_runtime_operations = capability_operation_runtime_contracts()
declarations = console_contract_declarations()
with tempfile.TemporaryDirectory(prefix="dify-knowledge-fs-contract-") as directory:
openapi_path = Path(directory) / "knowledge-fs.openapi.json"
capability_policy_path = Path(directory) / "dify-capability-v2-operations.json"
subprocess.run(
["pnpm", "openapi:export", "--", "--output", str(openapi_path)],
cwd=knowledge_fs_root,
check=True,
)
subprocess.run(
["pnpm", "capability:export", "--", "--output", str(capability_policy_path)],
cwd=knowledge_fs_root,
check=True,
)
openapi_content = openapi_path.read_bytes()
capability_policy = parse_capability_operation_policy(load_json_object(capability_policy_path))
document: dict[str, Any] = json.loads(openapi_content)
validate_product_operation_contracts(
capability_operations=capability_runtime_operations,
capability_policy=capability_policy,
document=document,
gap_manifest=product_gap_manifest,
manifest=product_manifest,
product_operations=product_runtime_operations,
)
validate_declarations(document, declarations)
expected_lock: ContractLock = {
"schemaVersion": LOCK_SCHEMA_VERSION,
"subtreeTree": subtree_tree,
"openapiSha256": sha256(openapi_content),
"capabilityV2AuthManifestSha256": sha256(capability_v2_auth_manifest_content),
"capabilityV2AuthTestVectorSha256": sha256(capability_v2_auth_test_vector_content),
"productOperationManifestSha256": sha256(product_operation_manifest_content),
"productOperationGapManifestSha256": sha256(product_operation_gap_manifest_content),
}
if args.update_lock:
lock_path.write_text(json.dumps(expected_lock, indent=2) + "\n")
return
received_lock = parse_contract_lock(load_json_object(lock_path))
lock_fields = (
(
"capabilityV2AuthManifestSha256",
received_lock["capabilityV2AuthManifestSha256"],
expected_lock["capabilityV2AuthManifestSha256"],
),
(
"capabilityV2AuthTestVectorSha256",
received_lock["capabilityV2AuthTestVectorSha256"],
expected_lock["capabilityV2AuthTestVectorSha256"],
),
(
"productOperationManifestSha256",
received_lock["productOperationManifestSha256"],
expected_lock["productOperationManifestSha256"],
),
(
"productOperationGapManifestSha256",
received_lock["productOperationGapManifestSha256"],
expected_lock["productOperationGapManifestSha256"],
),
("schemaVersion", received_lock["schemaVersion"], expected_lock["schemaVersion"]),
("subtreeTree", received_lock["subtreeTree"], expected_lock["subtreeTree"]),
("openapiSha256", received_lock["openapiSha256"], expected_lock["openapiSha256"]),
)
for field, received_value, expected_value in lock_fields:
if received_value != expected_value:
raise RuntimeError(
f"KnowledgeFS contract lock field {field} drifted: "
f"expected {expected_value!r}, received {received_value!r}. "
"Run --update-lock intentionally after reviewing the staged subtree and contract changes."
)
def ensure_contract_inputs_exist(
*,
knowledge_fs_root: Path,
capability_v2_auth_manifest_path: Path,
capability_v2_auth_test_vector_path: Path,
product_operations_path: Path,
product_operation_gaps_path: Path,
upstream_provenance_path: Path,
) -> None:
"""Fail with a stable error before invoking package tooling when a contract input is absent."""
required_paths = (
knowledge_fs_root / "package.json",
capability_v2_auth_manifest_path,
capability_v2_auth_test_vector_path,
product_operations_path,
product_operation_gaps_path,
upstream_provenance_path,
)
missing_paths = [path for path in required_paths if not path.is_file()]
if missing_paths:
missing = ", ".join(str(path) for path in missing_paths)
raise RuntimeError(f"KnowledgeFS contract input is missing: {missing}")
def ensure_clean_knowledge_fs_worktree(workspace_root: Path) -> None:
"""Require exported KnowledgeFS files to exactly match the staged index tree.
Staged changes are expected during intentional lock updates. Unstaged tracked changes and untracked files are
rejected because the export process reads the working tree while the tree ID is calculated from the index.
"""
subtree_path = f"{KNOWLEDGE_FS_DIRECTORY}/"
unstaged = subprocess.run(
["git", "diff", "--quiet", "--", subtree_path],
cwd=workspace_root,
check=False,
)
if unstaged.returncode > 1:
raise RuntimeError("git diff failed while validating the staged KnowledgeFS subtree")
untracked = run(
"git",
"ls-files",
"--others",
"--exclude-standard",
"--",
subtree_path,
cwd=workspace_root,
).strip()
if unstaged.returncode != 0 or untracked:
raise RuntimeError(
"knowledge-fs/ contains unstaged or untracked changes; stage or remove them before contract export"
)
def staged_subtree_tree(workspace_root: Path) -> str:
"""Return the Git tree object for the staged ``knowledge-fs/`` subtree."""
return run("git", "write-tree", f"--prefix={KNOWLEDGE_FS_DIRECTORY}/", cwd=workspace_root).strip()
def load_json_object(path: Path) -> dict[str, Any]:
"""Load a JSON object and reject arrays/scalars at contract boundaries."""
value = json.loads(path.read_text())
if not isinstance(value, dict):
raise ValueError(f"KnowledgeFS contract file must contain a JSON object: {path}")
return cast(dict[str, Any], value)
def parse_contract_lock(value: dict[str, Any]) -> ContractLock:
"""Validate the compact, non-self-referential contract lock schema."""
expected_fields = {
"capabilityV2AuthManifestSha256",
"capabilityV2AuthTestVectorSha256",
"openapiSha256",
"productOperationGapManifestSha256",
"productOperationManifestSha256",
"schemaVersion",
"subtreeTree",
}
if set(value) != expected_fields:
raise ValueError(f"KnowledgeFS contract lock fields must be exactly {sorted(expected_fields)}")
if value.get("schemaVersion") != LOCK_SCHEMA_VERSION:
raise ValueError(f"KnowledgeFS contract lock schemaVersion must be {LOCK_SCHEMA_VERSION}")
for field in (
"capabilityV2AuthManifestSha256",
"capabilityV2AuthTestVectorSha256",
"openapiSha256",
"productOperationGapManifestSha256",
"productOperationManifestSha256",
"subtreeTree",
):
field_value = value.get(field)
expected_length = 40 if field == "subtreeTree" else 64
if (
not isinstance(field_value, str)
or len(field_value) != expected_length
or any(character not in "0123456789abcdef" for character in field_value)
):
raise ValueError(f"KnowledgeFS contract lock field {field} has an invalid digest")
return cast(ContractLock, value)
def validate_required_product_operations(document: dict[str, Any], required_operation_ids: list[str]) -> None:
"""Fail when the pinned KFS OpenAPI omits an operation required by the Dify product."""
available_operation_ids = [
operation_id
for path_item in document.get("paths", {}).values()
for method in OPENAPI_METHODS
if isinstance(path_item, dict)
for operation in (path_item.get(method),)
if isinstance(operation, dict)
for operation_id in (operation.get("operationId"),)
if isinstance(operation_id, str) and operation_id
]
for operation_id in required_operation_ids:
count = available_operation_ids.count(operation_id)
if count != 1:
raise ValueError(
f"KnowledgeFS OpenAPI required product operation {operation_id} must occur exactly once; found {count}"
)
def validate_capability_v2_auth_manifest(value: dict[str, Any]) -> None:
"""Validate the active production RS256 profile consumed by both Dify and KnowledgeFS."""
expected_fields = {
"active",
"audience",
"callerProfiles",
"claimBindings",
"issuer",
"lifecycle",
"maxTtlSeconds",
"productionReady",
"profileId",
"protectedHeader",
"requiredClaims",
"resourceContract",
"runtimeAssembly",
"schemaVersion",
"signatureAlgorithms",
"tokenKind",
}
if set(value) != expected_fields:
raise ValueError(f"KnowledgeFS Capability v2 auth manifest fields must be exactly {sorted(expected_fields)}")
fixed_values = {
"active": True,
"audience": "knowledge-fs",
"issuer": "dify-control-plane",
"lifecycle": "active",
"maxTtlSeconds": 60,
"productionReady": True,
"profileId": "dify-capability-v2",
"schemaVersion": 3,
"signatureAlgorithms": ["RS256"],
"tokenKind": "jwt",
}
for field, expected in fixed_values.items():
if value.get(field) != expected:
raise ValueError(f"KnowledgeFS Capability v2 auth manifest field {field} must be {expected!r}")
if value.get("protectedHeader") != {
"algorithm": "RS256",
"keyIdClaim": "kid",
"keyIdRequired": True,
"type": "JWT",
}:
raise ValueError("KnowledgeFS Capability v2 protected header contract is invalid")
required_claims = [
"action",
"actor",
"aud",
"authz_revision",
"azp",
"caller_kind",
"cap_ver",
"content_policy_revision",
"content_scope_ids",
"control_space_id",
"exp",
"grant_id",
"iat",
"iss",
"jti",
"namespace_id",
"nbf",
"resource",
"sub",
"trace_id",
]
if value.get("requiredClaims") != required_claims:
raise ValueError("KnowledgeFS Capability v2 required claims are invalid")
if value.get("claimBindings") != {
"action": "action",
"callerKind": "caller_kind",
"controlSpace": "control_space_id",
"namespace": "namespace_id",
"resource": "resource",
"resourceParent": "resource.parent_id",
"subject": "sub",
}:
raise ValueError("KnowledgeFS Capability v2 claim bindings are invalid")
if value.get("resourceContract") != {
"fields": ["id", "parent_id", "type"],
"parentForbiddenFor": ["namespace", "knowledge_space"],
"parentRequiredFor": ["document", "job", "query", "research_task", "source", "upload_session"],
}:
raise ValueError("KnowledgeFS Capability v2 resource contract is invalid")
if value.get("callerProfiles") != {
"agent": {"authorizedParty": "dify-agent", "subjectPrefix": "dify-app:"},
"interactive": {"authorizedParty": "dify-console", "subjectPrefix": "dify-account:"},
"internal_worker": {"authorizedParty": "dify-worker", "subjectPrefix": "dify-worker:"},
"mcp": {"authorizedParty": "dify-mcp", "subjectPrefix": "dify-mcp-session:"},
"service": {"authorizedParty": "dify-service-api", "subjectPrefix": "dify-kfs-credential:"},
"workflow": {"authorizedParty": "dify-workflow", "subjectPrefix": "dify-app:"},
}:
raise ValueError("KnowledgeFS Capability v2 caller profiles are invalid")
if value.get("runtimeAssembly") != {
"failClosed": True,
"keySelection": "kid",
"maximumPublishedKeys": 3,
"verificationKeySource": "jwks",
}:
raise ValueError("KnowledgeFS Capability v2 runtime assembly is invalid")
def validate_capability_v2_auth_test_vector(
value: dict[str, Any],
manifest: dict[str, Any],
) -> None:
"""Verify the deterministic public-key vector and every security-sensitive binding."""
expected_fields = {
"algorithm",
"audience",
"expectedClaims",
"expectedPrincipal",
"issuer",
"operation",
"profileId",
"protectedHeader",
"publicJwk",
"schemaVersion",
"testOnly",
"token",
"ttlSeconds",
}
if set(value) != expected_fields:
raise ValueError(f"KnowledgeFS Capability v2 test vector fields must be exactly {sorted(expected_fields)}")
if (
value.get("schemaVersion") != 2
or value.get("profileId") != manifest.get("profileId")
or value.get("testOnly") is not True
or value.get("algorithm") != "RS256"
or value.get("issuer") != manifest.get("issuer")
or value.get("audience") != manifest.get("audience")
or value.get("ttlSeconds") != manifest.get("maxTtlSeconds")
):
raise ValueError("KnowledgeFS Capability v2 test vector does not match the active profile")
protected_header = value.get("protectedHeader")
if not isinstance(protected_header, dict) or set(protected_header) != {"alg", "kid", "typ"}:
raise ValueError("KnowledgeFS Capability v2 test vector protected header is invalid")
kid = protected_header.get("kid")
if protected_header.get("alg") != "RS256" or protected_header.get("typ") != "JWT" or not _is_non_blank(kid):
raise ValueError("KnowledgeFS Capability v2 test vector protected header is invalid")
public_jwk = value.get("publicJwk")
if not isinstance(public_jwk, dict) or set(public_jwk) != {"alg", "e", "kid", "kty", "n", "use"}:
raise ValueError("KnowledgeFS Capability v2 test vector public JWK is invalid")
if (
public_jwk.get("alg") != "RS256"
or public_jwk.get("kid") != kid
or public_jwk.get("kty") != "RSA"
or public_jwk.get("use") != "sig"
or not _is_non_blank(public_jwk.get("e"))
or not _is_non_blank(public_jwk.get("n"))
):
raise ValueError("KnowledgeFS Capability v2 test vector public JWK is invalid")
claims = value.get("expectedClaims")
required_claims = manifest.get("requiredClaims")
if not isinstance(claims, dict) or not isinstance(required_claims, list) or set(claims) != set(required_claims):
raise ValueError("KnowledgeFS Capability v2 test vector claims do not match the active profile")
operation = value.get("operation")
if not isinstance(operation, dict) or set(operation) != {"action", "method", "operationId", "requestPath"}:
raise ValueError("KnowledgeFS Capability v2 test vector operation is invalid")
resource = claims.get("resource")
if not isinstance(resource, dict) or set(resource) != {"id", "parent_id", "type"}:
raise ValueError("KnowledgeFS Capability v2 test vector resource is invalid")
expected_operation = {
"action": "documents.read",
"method": "GET",
"operationId": "getDocument",
"requestPath": "/knowledge-spaces/space-contract-vector/documents/document-contract-vector",
}
if operation != expected_operation:
raise ValueError("KnowledgeFS Capability v2 test vector operation binding is invalid")
exact_claims = {
"action": operation["action"],
"aud": value["audience"],
"caller_kind": "interactive",
"cap_ver": 2,
"control_space_id": "control-space-contract-vector",
"iss": value["issuer"],
"namespace_id": "workspace-contract-vector",
"resource": {
"id": "document-contract-vector",
"parent_id": "space-contract-vector",
"type": "document",
},
"sub": "dify-account:account-contract-vector",
}
for field, expected in exact_claims.items():
if claims.get(field) != expected:
raise ValueError(f"KnowledgeFS Capability v2 test vector claim {field} is invalid")
if claims.get("actor") != claims["sub"] or claims.get("azp") != "dify-console":
raise ValueError("KnowledgeFS Capability v2 test vector caller binding is invalid")
issued_at = claims.get("iat")
not_before = claims.get("nbf")
expires_at = claims.get("exp")
if (
not isinstance(issued_at, int)
or isinstance(issued_at, bool)
or not_before != issued_at
or not isinstance(expires_at, int)
or isinstance(expires_at, bool)
or expires_at - issued_at != value["ttlSeconds"]
):
raise ValueError("KnowledgeFS Capability v2 test vector TTL is invalid")
expected_principal = {
"callerKind": claims["caller_kind"],
"subject": {
"scopes": ["knowledge-spaces:read"],
"subjectId": claims["sub"],
"tenantId": claims["namespace_id"],
},
}
if value.get("expectedPrincipal") != expected_principal:
raise ValueError("KnowledgeFS Capability v2 test vector principal is invalid")
token = value.get("token")
if not isinstance(token, str) or not _is_non_blank(token):
raise ValueError("KnowledgeFS Capability v2 test vector token is invalid")
try:
verification_key = RSAAlgorithm.from_jwk(public_jwk)
if not isinstance(verification_key, RSAPublicKey):
raise ValueError("Capability vector verification key is not RSA public material")
header = jwt.get_unverified_header(token)
decoded_claims = jwt.decode(
token,
verification_key,
algorithms=["RS256"],
audience=cast(str, value["audience"]),
issuer=cast(str, value["issuer"]),
options={"verify_exp": False, "verify_iat": False, "verify_nbf": False},
)
except (jwt.PyJWTError, TypeError, ValueError) as exc:
raise ValueError("KnowledgeFS Capability v2 test vector signature is invalid") from exc
if header != protected_header or decoded_claims != claims:
raise ValueError("KnowledgeFS Capability v2 test vector token content drifted")
def _is_non_blank(value: object) -> bool:
return isinstance(value, str) and bool(value.strip()) and value == value.strip()
def validate_upstream_provenance(value: dict[str, Any]) -> None:
"""Validate the imported-source provenance that is itself covered by the subtree tree ID."""
expected_fields = {"commit", "release", "repository", "schemaVersion"}
if set(value) != expected_fields:
raise ValueError(f"KnowledgeFS upstream provenance fields must be exactly {sorted(expected_fields)}")
if value.get("schemaVersion") != 1:
raise ValueError("KnowledgeFS upstream provenance must use schemaVersion 1")
repository = value.get("repository")
commit = value.get("commit")
if not isinstance(repository, str) or not repository.startswith("https://"):
raise ValueError("KnowledgeFS upstream provenance repository must be an HTTPS URL")
if (
not isinstance(commit, str)
or len(commit) != 40
or any(character not in "0123456789abcdef" for character in commit)
):
raise ValueError("KnowledgeFS upstream provenance commit must be a lowercase full Git SHA")
if value.get("release") is not None and not isinstance(value["release"], str):
raise ValueError("KnowledgeFS upstream provenance release must be null or a string")
def validate_declarations(document: dict[str, Any], declarations: tuple[ContractDeclaration, ...]) -> None:
"""Validate Dify Console declarations against matching pinned OpenAPI operations."""
operations_by_id: dict[str, list[tuple[str, str, dict[str, Any], dict[str, Any]]]] = {}
for path, path_item in document.get("paths", {}).items():
for method in OPENAPI_METHODS:
operation = path_item.get(method)
if operation is None:
continue
operation_id = operation.get("operationId")
if isinstance(operation_id, str) and operation_id:
operations_by_id.setdefault(operation_id, []).append((method, path, path_item, operation))
declared_ids: set[str] = set()
for declaration in declarations:
operation_id = declaration["operation_id"]
if operation_id in declared_ids:
raise ValueError(f"Dify Console registry has duplicate operationId: {operation_id}")
declared_ids.add(operation_id)
matches = operations_by_id.get(operation_id, [])
if not matches:
raise ValueError(f"KnowledgeFS OpenAPI has no operationId: {operation_id}")
if len(matches) > 1:
raise ValueError(f"KnowledgeFS OpenAPI has duplicate operationId: {operation_id}")
method, path, path_item, operation = matches[0]
if not path.startswith("/"):
raise ValueError(f"KnowledgeFS OpenAPI path must be absolute: {path}")
if method not in PROXY_METHODS:
raise ValueError(f"KnowledgeFS proxy does not support {method.upper()} {path}")
expected: ContractDeclaration = {
"operation_id": operation_id,
"method": method.upper(),
"path": path[1:],
"required_scope": required_scope(document, operation),
"response_kind": response_kind(operation),
"max_response_bytes": required_max_response_bytes(operation),
"request_headers": request_header_names(path_item, operation),
"response_headers": response_header_names(operation),
"response_media_types": response_media_types(operation),
}
for field in DECLARATION_FIELDS:
expected_value = expected[field]
received_value = declaration[field]
if received_value != expected_value:
raise ValueError(
f"KnowledgeFS operation {operation_id} field {field} drifted: "
f"expected {expected_value!r}, received {received_value!r}"
)
def console_contract_declarations() -> tuple[ContractDeclaration, ...]:
"""The P9 backend exposes only typed product controllers; the raw Console proxy is removed."""
return ()
def response_kind(operation: dict[str, Any]) -> str:
media_types = response_media_types(operation)
if "text/event-stream" in media_types:
return "stream"
if "application/octet-stream" in media_types:
return "binary"
return "buffered"
def response_media_types(operation: dict[str, Any]) -> tuple[str, ...]:
media_types: set[str] = set()
for status, response in operation.get("responses", {}).items():
if status == "2XX" or (len(status) == 3 and status.startswith("2") and status.isdigit()):
media_types.update(response.get("content", {}))
return tuple(sorted(media_types))
def required_scope(document: dict[str, Any], operation: dict[str, Any]) -> str | None:
scope = operation.get("x-knowledge-fs-required-scope")
security = operation["security"] if "security" in operation else document.get("security")
if security == []:
if scope is not None:
raise ValueError(f"KnowledgeFS public operation must not declare a required scope: {scope}")
return None
if security != [{"bearerAuth": []}]:
raise ValueError(f"KnowledgeFS operation effective security must be exactly bearerAuth: {security}")
if scope in ("knowledge-spaces:read", "knowledge-spaces:write"):
return scope
raise ValueError(f"KnowledgeFS operation has no supported required scope: {scope}")
def required_max_response_bytes(operation: dict[str, Any]) -> int:
value = operation.get("x-knowledge-fs-max-response-bytes")
if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
raise ValueError(f"KnowledgeFS operation has no valid response byte limit: {value}")
return value
def request_header_names(path_item: dict[str, Any], operation: dict[str, Any]) -> tuple[str, ...]:
names: set[str] = set()
for parameter in [*path_item.get("parameters", []), *operation.get("parameters", [])]:
if "$ref" in parameter:
raise ValueError(f"KnowledgeFS request header references are not supported: {parameter['$ref']}")
if parameter.get("in") == "header":
names.add(parameter["name"].lower())
return tuple(sorted(names))
def response_header_names(operation: dict[str, Any]) -> tuple[str, ...]:
return tuple(
sorted(
{
name.lower()
for response in operation.get("responses", {}).values()
for name in response.get("headers", {})
}
)
)
def sha256(content: bytes) -> str:
return hashlib.sha256(content).hexdigest()
def run(*command: str, cwd: Path) -> str:
return subprocess.run(command, cwd=cwd, check=True, capture_output=True, text=True).stdout
if __name__ == "__main__":
main()