generate_openapi.py 53 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845
  1. #!/usr/bin/env python3
  2. """Generate the current Flask route inventory as an OpenAPI 3.1 contract."""
  3. from __future__ import annotations
  4. import argparse
  5. import ast
  6. import json
  7. import re
  8. from collections import defaultdict
  9. from pathlib import Path
  10. ROOT = Path(__file__).resolve().parents[1]
  11. OUTPUT = ROOT / "docs" / "architecture" / "OPENAPI.yaml"
  12. PREFIXES = {
  13. "meta_data": "/api/meta",
  14. "data_interface": "/api/interface",
  15. "data_rules": "/api/rules",
  16. "graph": "/api/graph",
  17. "system": "/api/system",
  18. "data_source": "/api/datasource",
  19. "data_development": "/api/development/v1",
  20. "data_flow": "/api/dataflow",
  21. "business_domain": "/api/bd",
  22. "data_factory": "/api/datafactory",
  23. "data_service": "/api/dataservice",
  24. "knowledge_base": "/api/knowledge",
  25. }
  26. SHORTHAND_METHODS = {"get", "post", "put", "patch", "delete"}
  27. EDGE_SAFE_KEY_PATTERN = (
  28. "^(?!.*(?:raw|rows?|sql|credential|secret|private|password|token|local_path))"
  29. "[a-z][a-z0-9_]{0,79}$"
  30. )
  31. def _edge_safe_schema_lines() -> list[str]:
  32. lines = [
  33. " EdgeSafeSummary:",
  34. " type: object",
  35. " x-max-policy-depth: 12",
  36. " maxProperties: 64",
  37. f" propertyNames: {{type: string, minLength: 1, maxLength: 80, pattern: '{EDGE_SAFE_KEY_PATTERN}'}}",
  38. " additionalProperties: {$ref: '#/components/schemas/EdgeSafeValue12'}",
  39. " description: 'Policy-approved bounded metadata/statistics/health/diagnostic fields only; raw rows, SQL, credentials, and local paths are forbidden.'",
  40. " EdgeSafeScalar:",
  41. " anyOf:",
  42. " - {type: string, maxLength: 4096}",
  43. " - {type: integer, minimum: -9007199254740991, maximum: 9007199254740991}",
  44. " - {type: number, minimum: -1000000000000, maximum: 1000000000000}",
  45. " - {type: boolean}",
  46. " - {type: 'null'}",
  47. " EdgeSafeValue0: {$ref: '#/components/schemas/EdgeSafeScalar'}",
  48. ]
  49. for depth in range(1, 13):
  50. previous = depth - 1
  51. lines.extend(
  52. [
  53. f" EdgeSafeValue{depth}:",
  54. " anyOf:",
  55. " - {$ref: '#/components/schemas/EdgeSafeScalar'}",
  56. f" - {{type: array, maxItems: 100, items: {{$ref: '#/components/schemas/EdgeSafeValue{previous}'}}}}",
  57. f" - {{type: object, maxProperties: 32, propertyNames: {{type: string, minLength: 1, maxLength: 80, pattern: '{EDGE_SAFE_KEY_PATTERN}'}}, additionalProperties: {{$ref: '#/components/schemas/EdgeSafeValue{previous}'}}}}",
  58. ]
  59. )
  60. lines.append(
  61. " EdgeSafeValue: {$ref: '#/components/schemas/EdgeSafeValue12'}"
  62. )
  63. return lines
  64. RESPONSE_FIELDS = {
  65. ("data_development", "summary"): [
  66. "scope",
  67. "metrics",
  68. ],
  69. ("data_development", "details"): [
  70. "metric",
  71. "state",
  72. "records",
  73. "page",
  74. "page_size",
  75. "total",
  76. ],
  77. ("knowledge_base", "ask"): [
  78. "query_id",
  79. "mode",
  80. "answer",
  81. "answer_status",
  82. "degraded_components",
  83. "citations",
  84. "evidence",
  85. "freshness_status",
  86. ],
  87. }
  88. CONNECTOR_REQUEST_SCHEMAS = {
  89. "/api/datasource/connectors/config/validate": "ConnectorConfigValidationRequest",
  90. "/api/datasource/connectors/runs": "ConnectorRunRequest",
  91. "/api/datasource/connectors/machine/runs": "MachineConnectorRunRequest",
  92. "/api/datasource/connectors/source-bindings": "ConnectorSourceBindingRequest",
  93. "/api/datasource/connectors/principals": "ConnectorPrincipalRequest",
  94. "/api/datasource/connectors/{connector_id}/{version}/health": "ConnectorHealthRequest",
  95. "/api/datasource/connectors/principals/{principal_uid}/credentials": "ConnectorCredentialRequest",
  96. "/api/datasource/connectors/credentials/{credential_uid}/{action}": "ConnectorCredentialRequest",
  97. "/api/datasource/graph": "ConnectorGraphRequest",
  98. }
  99. CONNECTOR_RESPONSE_SCHEMAS = {
  100. ("/api/datasource/connectors/manifests", "get"): "ConnectorManifestEnvelope",
  101. ("/api/datasource/connectors/runs", "get"): "ConnectorRunListEnvelope",
  102. ("/api/datasource/connectors/runs", "post"): "ConnectorRunResultEnvelope",
  103. ("/api/datasource/connectors/machine/runs", "post"): "ConnectorRunResultEnvelope",
  104. (
  105. "/api/datasource/connectors/runs/{idempotency_key}/cancel",
  106. "post",
  107. ): "ConnectorRunRecordEnvelope",
  108. (
  109. "/api/datasource/connectors/runs/{idempotency_key}/resume",
  110. "post",
  111. ): "ConnectorRunResultEnvelope",
  112. (
  113. "/api/datasource/connectors/config/validate",
  114. "post",
  115. ): "ConnectorValidationEnvelope",
  116. (
  117. "/api/datasource/connectors/{connector_id}/{version}/health",
  118. "post",
  119. ): "ConnectorHealthEnvelope",
  120. (
  121. "/api/datasource/connectors/{connector_id}/{version}/compatibility",
  122. "get",
  123. ): "ConnectorCompatibilityEnvelope",
  124. ("/api/datasource/connectors/principals", "post"): "ConnectorPrincipalEnvelope",
  125. (
  126. "/api/datasource/connectors/source-bindings",
  127. "post",
  128. ): "ConnectorSourceBindingEnvelope",
  129. (
  130. "/api/datasource/connectors/principals/{principal_uid}/credentials",
  131. "post",
  132. ): "ConnectorCredentialEnvelope",
  133. (
  134. "/api/datasource/connectors/credentials/{credential_uid}/{action}",
  135. "post",
  136. ): "ConnectorCredentialEnvelope",
  137. ("/api/datasource/graph", "post"): "ConnectorGraphEnvelope",
  138. }
  139. NO_REQUEST_BODY_PATHS = {
  140. "/api/datasource/connectors/runs/{idempotency_key}/cancel",
  141. "/api/datasource/connectors/runs/{idempotency_key}/resume",
  142. "/api/datasource/connectors/machine/runs/{idempotency_key}/cancel",
  143. "/api/datasource/connectors/machine/runs/{idempotency_key}/resume",
  144. "/api/datasource/connectors/source-bindings/{binding_uid}/revoke",
  145. "/api/datasource/edge/tasks/{task_id}/cancel",
  146. "/api/datasource/edge/gateways/{gateway_id}/revoke",
  147. }
  148. OPTIONAL_REQUEST_BODY_PATHS = {
  149. "/api/datasource/connectors/principals/{principal_uid}/credentials",
  150. "/api/datasource/connectors/credentials/{credential_uid}/{action}",
  151. }
  152. EDGE_REQUEST_SCHEMAS = {
  153. "/api/datasource/edge/enrollments": "EdgeEnrollmentRequest",
  154. "/api/datasource/edge/register": "EdgeRegisterRequest",
  155. "/api/datasource/edge/gateways/{gateway_id}/heartbeat": "EdgeHeartbeatRequest",
  156. "/api/datasource/edge/gateways/{gateway_id}/rotate": "EdgeRotateRequest",
  157. "/api/datasource/edge/tasks": "EdgeTaskContract",
  158. "/api/datasource/edge/gateways/{gateway_id}/tasks/pull": "EdgeMachineBinding",
  159. "/api/datasource/edge/gateways/{gateway_id}/tasks/{task_id}/outcome": "EdgeTaskOutcomeRequest",
  160. "/api/datasource/edge/gateways/{gateway_id}/events": "EdgeEventRequest",
  161. "/api/datasource/edge/gateways/{gateway_id}/reconcile": "EdgeReconcileRequest",
  162. "/api/datasource/edge/gateways/{gateway_id}/releases": "EdgeReleaseOfferRequest",
  163. "/api/datasource/edge/gateways/{gateway_id}/releases/ack": "EdgeReleaseAckRequest",
  164. }
  165. EDGE_RESPONSE_SCHEMAS = {
  166. ("/api/datasource/edge/enrollments", "post"): "EdgeEnrollmentEnvelope",
  167. ("/api/datasource/edge/register", "post"): "EdgeRegistrationEnvelope",
  168. ("/api/datasource/edge/gateways", "get"): "EdgeGatewayListEnvelope",
  169. ("/api/datasource/edge/gateways/{gateway_id}/tasks/pull", "post"): "EdgeTaskPullEnvelope",
  170. ("/api/datasource/edge/gateways/{gateway_id}/events", "post"): "EdgeEventAckEnvelope",
  171. ("/api/datasource/edge/gateways/{gateway_id}/reconcile", "post"): "EdgeReconcileResponse",
  172. ("/api/datasource/edge/gateways/{gateway_id}/releases", "post"): "EdgeReleaseEnvelope",
  173. }
  174. EDGE_MACHINE_PATHS = {
  175. "/api/datasource/edge/gateways/{gateway_id}/heartbeat",
  176. "/api/datasource/edge/gateways/{gateway_id}/tasks/pull",
  177. "/api/datasource/edge/gateways/{gateway_id}/tasks/{task_id}/outcome",
  178. "/api/datasource/edge/gateways/{gateway_id}/events",
  179. "/api/datasource/edge/gateways/{gateway_id}/reconcile",
  180. "/api/datasource/edge/gateways/{gateway_id}/releases/ack",
  181. }
  182. EDGE_CREATED_PATHS = {
  183. "/api/datasource/edge/enrollments",
  184. "/api/datasource/edge/register",
  185. "/api/datasource/edge/tasks",
  186. "/api/datasource/edge/gateways/{gateway_id}/releases",
  187. }
  188. def quoted(value: str) -> str:
  189. return json.dumps(value, ensure_ascii=False)
  190. def route_path(raw_path: str) -> tuple[str, list[dict[str, str]]]:
  191. parameters: list[dict[str, str]] = []
  192. def replace(match: re.Match[str]) -> str:
  193. converter = match.group(1) or "string"
  194. name = match.group(2)
  195. schema_type = "integer" if converter in {"int", "float"} else "string"
  196. parameters.append({"name": name, "type": schema_type})
  197. return "{" + name + "}"
  198. return re.sub(
  199. r"<(?:([a-zA-Z_]+):)?([a-zA-Z_][a-zA-Z0-9_]*)>", replace, raw_path
  200. ), parameters
  201. def extract_routes() -> list[dict[str, object]]:
  202. routes: list[dict[str, object]] = []
  203. for module, prefix in PREFIXES.items():
  204. for route_file in sorted((ROOT / "app" / "api" / module).glob("*.py")):
  205. if route_file.name == "__init__.py":
  206. continue
  207. tree = ast.parse(route_file.read_text(encoding="utf-8"))
  208. for node in tree.body:
  209. if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
  210. continue
  211. for decorator in node.decorator_list:
  212. if not isinstance(decorator, ast.Call):
  213. continue
  214. function = decorator.func
  215. if not (
  216. isinstance(function, ast.Attribute)
  217. and isinstance(function.value, ast.Name)
  218. and function.value.id == "bp"
  219. and decorator.args
  220. ):
  221. continue
  222. if function.attr == "route":
  223. methods = ["GET"]
  224. for keyword in decorator.keywords:
  225. if keyword.arg == "methods":
  226. methods = ast.literal_eval(keyword.value)
  227. elif function.attr in SHORTHAND_METHODS:
  228. methods = [function.attr.upper()]
  229. else:
  230. continue
  231. raw_path = ast.literal_eval(decorator.args[0])
  232. path, parameters = route_path(prefix + raw_path)
  233. summary = (
  234. ast.get_docstring(node) or node.name.replace("_", " ")
  235. ).splitlines()[0]
  236. for method in methods:
  237. routes.append(
  238. {
  239. "path": path,
  240. "method": method.lower(),
  241. "tag": module,
  242. "operation_id": f"{module}_{node.name}_{method.lower()}",
  243. "summary": summary,
  244. "parameters": parameters,
  245. "source": str(route_file.relative_to(ROOT)),
  246. }
  247. )
  248. return sorted(routes, key=lambda item: (str(item["path"]), str(item["method"])))
  249. def render(routes: list[dict[str, object]]) -> str:
  250. by_path: dict[str, list[dict[str, object]]] = defaultdict(list)
  251. for route in routes:
  252. by_path[str(route["path"])].append(route)
  253. lines = [
  254. "openapi: 3.1.0",
  255. "info:",
  256. ' title: "DataOps Platform API(当前代码基线)"',
  257. ' version: "2026-08-09"',
  258. ' description: "由 scripts/generate_openapi.py 从 app/api/*/routes.py 生成。请求体与响应细节仍以现有专项 API 文档和代码为准。"',
  259. f"x-route-count: {len(routes)}",
  260. "servers:",
  261. ' - url: "http://localhost:15500"',
  262. ' description: "全本地隔离测试后端"',
  263. "tags:",
  264. ]
  265. for tag in PREFIXES:
  266. lines.extend([f" - name: {tag}", f" description: {quoted(PREFIXES[tag])}"])
  267. lines.append("paths:")
  268. for path, operations in sorted(by_path.items()):
  269. lines.append(f" {quoted(path)}:")
  270. for operation in operations:
  271. lines.extend(
  272. [
  273. f" {operation['method']}:",
  274. f" tags: [{operation['tag']}]",
  275. f" operationId: {operation['operation_id']}",
  276. f" summary: {quoted(str(operation['summary']))}",
  277. f" x-source: {quoted(str(operation['source']))}",
  278. ]
  279. )
  280. if path.startswith("/api/datasource/edge/"):
  281. lines.extend(
  282. [
  283. " x-max-request-bytes: 262144",
  284. " x-max-json-depth: 32",
  285. " x-max-json-nodes: 5000",
  286. ]
  287. )
  288. parameters = operation["parameters"]
  289. if path.startswith("/api/datasource/connectors/machine/runs"):
  290. parameters = [
  291. *parameters,
  292. {
  293. "name": "X-Connector-Credential",
  294. "type": "string",
  295. "header": True,
  296. },
  297. ]
  298. if path == "/api/datasource/edge/register":
  299. parameters = [
  300. *parameters,
  301. {"name": "X-Edge-Enrollment", "type": "string", "header": True},
  302. {"name": "X-DataOps-Edge-Client-Verify", "type": "string", "header": True},
  303. {"name": "X-DataOps-Edge-Client-Cert", "type": "string", "header": True},
  304. {"name": "X-Edge-Certificate-SHA256", "type": "string", "header": True},
  305. ]
  306. elif path in EDGE_MACHINE_PATHS:
  307. parameters = [
  308. *parameters,
  309. {"name": "X-Edge-Credential", "type": "string", "header": True},
  310. {"name": "X-DataOps-Edge-Client-Verify", "type": "string", "header": True},
  311. {"name": "X-DataOps-Edge-Client-Cert", "type": "string", "header": True},
  312. {"name": "X-Edge-Certificate-SHA256", "type": "string", "header": True},
  313. ]
  314. if path == "/api/datasource/edge/gateways" and operation["method"] == "get":
  315. parameters = [
  316. *parameters,
  317. {"name": "limit", "type": "integer", "query": True},
  318. {"name": "offset", "type": "integer", "query": True},
  319. ]
  320. if parameters:
  321. lines.append(" parameters:")
  322. for parameter in parameters:
  323. lines.extend(
  324. [
  325. f" - name: {parameter['name']}",
  326. f" in: {'header' if parameter.get('header') else 'query' if parameter.get('query') else 'path'}",
  327. f" required: {'false' if parameter.get('query') else 'true'}",
  328. " schema:",
  329. f" type: {parameter['type']}",
  330. ]
  331. )
  332. if parameter["name"] in {"X-Edge-Credential", "X-Edge-Enrollment"}:
  333. lines.extend(
  334. [" format: password", " writeOnly: true"]
  335. )
  336. if parameter["name"] == "limit":
  337. lines.extend([" minimum: 1", " maximum: 100"])
  338. if parameter["name"] == "offset":
  339. lines.extend([" minimum: 0", " maximum: 10000"])
  340. if (
  341. path
  342. == "/api/datasource/connectors/credentials/{credential_uid}/{action}"
  343. and parameter["name"] == "action"
  344. ):
  345. lines.append(" enum: [revoke, rotate]")
  346. if (
  347. operation["method"] in {"post", "put", "patch"}
  348. and path not in NO_REQUEST_BODY_PATHS
  349. ):
  350. connector_schema = CONNECTOR_REQUEST_SCHEMAS.get(path)
  351. request_schema = connector_schema or EDGE_REQUEST_SCHEMAS.get(path)
  352. lines.extend(
  353. [
  354. " requestBody:",
  355. " required: "
  356. + (
  357. "false"
  358. if path in OPTIONAL_REQUEST_BODY_PATHS
  359. else "true" if request_schema else "false"
  360. ),
  361. " content:",
  362. " application/json:",
  363. " schema:",
  364. f" $ref: '#/components/schemas/{request_schema}'"
  365. if request_schema
  366. else " type: object",
  367. ]
  368. )
  369. if not request_schema:
  370. lines.append(" additionalProperties: true")
  371. response_fields = RESPONSE_FIELDS.get(
  372. (str(operation["tag"]), str(operation["operation_id"]).split("_")[-2])
  373. )
  374. if response_fields:
  375. lines.append(
  376. " x-response-fields: [" + ", ".join(response_fields) + "]"
  377. )
  378. connector_response = CONNECTOR_RESPONSE_SCHEMAS.get(
  379. (path, str(operation["method"]))
  380. )
  381. edge_response = EDGE_RESPONSE_SCHEMAS.get(
  382. (path, str(operation["method"]))
  383. )
  384. response_status = (
  385. "201"
  386. if operation["method"] == "post"
  387. and (path in EDGE_CREATED_PATHS or path in {
  388. "/api/datasource/connectors/runs",
  389. "/api/datasource/connectors/machine/runs",
  390. "/api/datasource/connectors/principals",
  391. "/api/datasource/connectors/principals/{principal_uid}/credentials",
  392. "/api/datasource/connectors/source-bindings",
  393. })
  394. else "200"
  395. )
  396. if path.startswith("/api/datasource/connectors"):
  397. required_permission = (
  398. "machine-credential"
  399. if path.startswith("/api/datasource/connectors/machine/runs")
  400. else (
  401. "connectors:manage"
  402. if "/source-bindings" in path
  403. else (
  404. "connectors:read"
  405. if operation["method"] == "get"
  406. else (
  407. "connectors:manage"
  408. if "/principals" in path or "/credentials" in path
  409. else "connectors:operate"
  410. )
  411. )
  412. )
  413. )
  414. lines.append(
  415. f" x-required-permission: {quoted(required_permission)}"
  416. )
  417. elif path == "/api/datasource/graph":
  418. lines.append(' x-required-permission: "connectors:read"')
  419. elif path.startswith("/api/datasource/edge"):
  420. if path == "/api/datasource/edge/register":
  421. permission = "one-time-enrollment+mTLS"
  422. elif path in EDGE_MACHINE_PATHS:
  423. permission = "edge-credential+mTLS"
  424. elif path == "/api/datasource/edge/gateways" and operation["method"] == "get":
  425. permission = "edge-gateways:read"
  426. elif path == "/api/datasource/edge/enrollments":
  427. permission = "edge-gateways:manage"
  428. else:
  429. permission = "edge-gateways:operate"
  430. lines.append(f" x-required-permission: {quoted(permission)}")
  431. if path == "/api/datasource/edge/register":
  432. lines.extend(
  433. [
  434. " security:",
  435. " - mutualTLS: []",
  436. " edgeEnrollment: []",
  437. ]
  438. )
  439. elif path in EDGE_MACHINE_PATHS:
  440. lines.extend(
  441. [
  442. " security:",
  443. " - mutualTLS: []",
  444. " edgeCredential: []",
  445. ]
  446. )
  447. else:
  448. lines.extend([" security:", " - bearerAuth: []"])
  449. success_headers = (
  450. [
  451. " headers:",
  452. " Cache-Control:",
  453. " required: true",
  454. " schema: {type: string, const: no-store}",
  455. ]
  456. if path.startswith("/api/datasource/edge")
  457. else []
  458. )
  459. lines.extend(
  460. [
  461. " responses:",
  462. f' "{response_status}":',
  463. ' description: "请求已由当前实现处理"',
  464. *success_headers,
  465. " content:",
  466. " application/json:",
  467. " schema:",
  468. f" $ref: '#/components/schemas/{connector_response or edge_response or ('EdgeApiEnvelope' if path.startswith('/api/datasource/edge') else 'ApiEnvelope')}'",
  469. " default:",
  470. ' description: "错误响应"',
  471. *success_headers,
  472. " content:",
  473. " application/json:",
  474. " schema:",
  475. f" $ref: '#/components/schemas/{'ConnectorErrorEnvelope' if path.startswith('/api/datasource/connectors') or path == '/api/datasource/graph' else 'EdgeErrorEnvelope' if path.startswith('/api/datasource/edge') else 'ApiEnvelope'}'",
  476. ]
  477. )
  478. lines.extend(
  479. [
  480. "components:",
  481. " schemas:",
  482. " ApiEnvelope:",
  483. " type: object",
  484. " additionalProperties: true",
  485. " required: [code, message, data]",
  486. " properties:",
  487. " code:",
  488. " type: integer",
  489. " message:",
  490. " type: string",
  491. " data: {}",
  492. " error: {type: object}",
  493. " EdgeApiEnvelope:",
  494. " type: object",
  495. " additionalProperties: false",
  496. " required: [code, message, data]",
  497. " properties: {code: {type: integer}, message: {type: string}, data: {type: object}}",
  498. " EdgeErrorEnvelope:",
  499. " type: object",
  500. " additionalProperties: false",
  501. " required: [code, message, data, error]",
  502. " properties: {code: {type: integer}, message: {type: string}, data: {type: 'null'}, error: {type: object, additionalProperties: false, required: [code], properties: {code: {type: string, maxLength: 80}}}}",
  503. " EdgeMachineBinding:",
  504. " type: object",
  505. " additionalProperties: false",
  506. " required: [environment, network_zone, generation]",
  507. " properties: {environment: {type: string, enum: [development, staging, production]}, network_zone: {type: string, minLength: 1, maxLength: 120}, generation: {type: integer, minimum: 1}}",
  508. " EdgeEnrollmentRequest:",
  509. " type: object",
  510. " additionalProperties: false",
  511. " required: [gateway_name, environment, network_zone, policy_digest, allowed_control_hosts, allowed_proxy_hosts, expected_certificate_sha256]",
  512. " properties: {gateway_name: {type: string, minLength: 1, maxLength: 200}, environment: {type: string, enum: [development, staging, production]}, network_zone: {type: string, minLength: 1, maxLength: 120}, policy_digest: {$ref: '#/components/schemas/Sha256Digest'}, allowed_control_hosts: {$ref: '#/components/schemas/EdgeExactHosts'}, allowed_proxy_hosts: {$ref: '#/components/schemas/EdgeExactHosts'}, expected_certificate_sha256: {$ref: '#/components/schemas/Sha256Digest'}, ttl_seconds: {type: integer, minimum: 60, maximum: 86400}}",
  513. " EdgeRegisterRequest:",
  514. " type: object",
  515. " additionalProperties: false",
  516. " required: [gateway_id, environment, network_zone, policy_digest, allowed_control_hosts, allowed_proxy_hosts, version]",
  517. " properties: {gateway_id: {type: string, format: uuid}, environment: {type: string, enum: [development, staging, production]}, network_zone: {type: string, minLength: 1, maxLength: 120}, policy_digest: {$ref: '#/components/schemas/Sha256Digest'}, allowed_control_hosts: {$ref: '#/components/schemas/EdgeExactHosts'}, allowed_proxy_hosts: {$ref: '#/components/schemas/EdgeExactHosts'}, version: {$ref: '#/components/schemas/EdgeAgentVersion'}}",
  518. " EdgeHeartbeatRequest:",
  519. " type: object",
  520. " additionalProperties: false",
  521. " required: [environment, network_zone, generation, version, safe_summary]",
  522. " properties: {environment: {type: string, enum: [development, staging, production]}, network_zone: {type: string, minLength: 1, maxLength: 120}, generation: {type: integer, minimum: 1}, version: {$ref: '#/components/schemas/EdgeAgentVersion'}, safe_summary: {$ref: '#/components/schemas/EdgeSafeSummary'}}",
  523. " EdgeRotateRequest:",
  524. " type: object",
  525. " additionalProperties: false",
  526. " required: [certificate_sha256, request_id]",
  527. " properties: {certificate_sha256: {$ref: '#/components/schemas/Sha256Digest'}, request_id: {type: string, minLength: 1, maxLength: 255}}",
  528. " Sha256Digest: {type: string, pattern: '^[a-f0-9]{64}$'}",
  529. " Ed25519Signature: {type: string, pattern: '^[a-f0-9]{128}$'}",
  530. " EdgeExactHosts: {type: array, maxItems: 32, uniqueItems: true, items: {type: string, pattern: '^[a-z0-9][a-z0-9.-]*[a-z0-9]$'}}",
  531. *_edge_safe_schema_lines(),
  532. " EdgeIdentifier: {type: string, minLength: 1, maxLength: 255, pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,254}$'}",
  533. " EdgeCursor: {type: [string, 'null'], minLength: 1, maxLength: 255, pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,254}$'}",
  534. " EdgeAgentVersion: {type: string, minLength: 1, maxLength: 80, pattern: '^[A-Za-z0-9][A-Za-z0-9._+-]{0,79}$'}",
  535. " EdgeVersion: {type: string, pattern: '^(0|[1-9][0-9]{0,9})\\.(0|[1-9][0-9]{0,9})\\.(0|[1-9][0-9]{0,9})$'}",
  536. " EdgeTaskContract:",
  537. " type: object",
  538. " additionalProperties: false",
  539. " required: [task_id, gateway_id, environment, network_zone, purpose, classification, task_type, contract_version, deadline_at, attempt, idempotency_key, policy_digest]",
  540. " properties: {task_id: {$ref: '#/components/schemas/EdgeIdentifier'}, gateway_id: {type: string, format: uuid}, environment: {type: string, enum: [development, staging, production]}, network_zone: {type: string, minLength: 1, maxLength: 120}, purpose: {type: string, minLength: 1, maxLength: 255}, classification: {type: string, enum: [raw, recent_detail, restricted, desensitized_metadata, statistics, lineage, evidence]}, task_type: {type: string, enum: [collect, profile, quality, lineage, controlled_query]}, contract_version: {const: 1}, deadline_at: {type: string, format: date-time}, attempt: {type: integer, minimum: 1, maximum: 5}, idempotency_key: {$ref: '#/components/schemas/EdgeIdentifier'}, policy_digest: {$ref: '#/components/schemas/Sha256Digest'}}",
  541. " EdgeSignedTaskEnvelope:",
  542. " type: object",
  543. " additionalProperties: false",
  544. " required: [task, authority_key_id, signature_algorithm, contract_digest, gateway_id, environment, network_zone, policy_digest, purpose, issued_at, expires_at, signature]",
  545. " properties: {task: {$ref: '#/components/schemas/EdgeTaskContract'}, authority_key_id: {type: string}, signature_algorithm: {const: Ed25519}, contract_digest: {$ref: '#/components/schemas/Sha256Digest'}, gateway_id: {type: string}, environment: {type: string}, network_zone: {type: string}, policy_digest: {$ref: '#/components/schemas/Sha256Digest'}, purpose: {type: string}, issued_at: {type: string, format: date-time}, expires_at: {type: string, format: date-time}, signature: {$ref: '#/components/schemas/Ed25519Signature'}}",
  546. " EdgeEventContract:",
  547. " type: object",
  548. " additionalProperties: false",
  549. " required: [event_id, task_id, gateway_id, environment, network_zone, purpose, classification, contract_version, occurred_at, attempt, idempotency_key, policy_digest, payload]",
  550. " properties: {event_id: {type: string}, task_id: {type: string}, gateway_id: {type: string}, environment: {type: string}, network_zone: {type: string}, purpose: {type: string}, classification: {type: string, enum: [desensitized_metadata, statistics, lineage, evidence, health_summary, diagnostic_summary]}, contract_version: {const: 1}, occurred_at: {type: string, format: date-time}, attempt: {type: integer, minimum: 1, maximum: 5}, idempotency_key: {type: string}, policy_digest: {$ref: '#/components/schemas/Sha256Digest'}, payload: {$ref: '#/components/schemas/EdgeSafeSummary'}}",
  551. " EdgeEventRequest:",
  552. " type: object",
  553. " additionalProperties: false",
  554. " required: [environment, network_zone, generation, event, lease_token]",
  555. " properties: {environment: {type: string}, network_zone: {type: string}, generation: {type: integer, minimum: 1}, event: {$ref: '#/components/schemas/EdgeEventContract'}, lease_token: {type: string, writeOnly: true}}",
  556. " EdgeTaskOutcomeRequest:",
  557. " type: object",
  558. " additionalProperties: false",
  559. " required: [environment, network_zone, generation, outcome, lease_token, safe_summary]",
  560. " properties: {environment: {type: string}, network_zone: {type: string}, generation: {type: integer, minimum: 1}, outcome: {type: string, enum: [completed, failed, cancelled]}, lease_token: {type: string, writeOnly: true}, safe_summary: {$ref: '#/components/schemas/EdgeSafeSummary'}}",
  561. " EdgeReconcileRequest:",
  562. " type: object",
  563. " additionalProperties: false",
  564. " required: [environment, network_zone, generation]",
  565. " properties: {environment: {type: string, enum: [development, staging, production]}, network_zone: {type: string, minLength: 1, maxLength: 120}, generation: {type: integer, minimum: 1}, limit: {type: integer, minimum: 1, maximum: 100}, cancel_cursor: {$ref: '#/components/schemas/EdgeCursor'}, release_cursor: {$ref: '#/components/schemas/EdgeCursor'}}",
  566. " EdgeReconcileResponse:",
  567. " type: object",
  568. " additionalProperties: false",
  569. " required: [code, message, data]",
  570. " properties: {code: {type: integer}, message: {type: string, maxLength: 200}, data: {type: object, additionalProperties: false, required: [cancelled_task_ids, cancel_next_cursor, release_offers, release_next_cursor, release_baseline], properties: {cancelled_task_ids: {type: array, maxItems: 50, items: {$ref: '#/components/schemas/EdgeIdentifier'}}, cancel_next_cursor: {$ref: '#/components/schemas/EdgeCursor'}, release_offers: {type: array, maxItems: 50, items: {$ref: '#/components/schemas/EdgeReleaseManifest'}}, release_next_cursor: {$ref: '#/components/schemas/EdgeCursor'}, release_baseline: {oneOf: [{$ref: '#/components/schemas/EdgeReleaseManifest'}, {type: 'null'}]}}}}",
  571. " EdgeReleaseOfferRequest:",
  572. " type: object",
  573. " additionalProperties: false",
  574. " required: [version, artifact_digest, artifact_name, deadline_at, rollback_version, request_id]",
  575. " properties: {version: {$ref: '#/components/schemas/EdgeVersion'}, artifact_digest: {$ref: '#/components/schemas/Sha256Digest'}, artifact_name: {type: string, minLength: 1, maxLength: 255, pattern: '^[^/\\\\]+$'}, deadline_at: {type: string, format: date-time}, rollback_version: {$ref: '#/components/schemas/EdgeVersion'}, request_id: {$ref: '#/components/schemas/EdgeIdentifier'}}",
  576. " EdgeReleaseAckRequest:",
  577. " type: object",
  578. " additionalProperties: false",
  579. " required: [environment, network_zone, generation, release_id, outcome, safe_summary]",
  580. " properties: {environment: {type: string}, network_zone: {type: string}, generation: {type: integer, minimum: 1}, release_id: {type: string, format: uuid}, outcome: {type: string, enum: [accepted, installed, failed, rollback]}, safe_summary: {$ref: '#/components/schemas/EdgeSafeSummary'}}",
  581. " EdgeReleaseManifest:",
  582. " type: object",
  583. " additionalProperties: false",
  584. " required: [release_id, version, rollback_version, artifact_digest, artifact_name, deadline_at, status, signature_algorithm, key_id, manifest_digest, signature]",
  585. " properties: {release_id: {$ref: '#/components/schemas/EdgeIdentifier'}, version: {$ref: '#/components/schemas/EdgeVersion'}, rollback_version: {$ref: '#/components/schemas/EdgeVersion'}, artifact_digest: {$ref: '#/components/schemas/Sha256Digest'}, artifact_name: {type: string, minLength: 1, maxLength: 255, pattern: '^[^/\\\\]+$'}, deadline_at: {type: string, format: date-time}, status: {type: string, enum: [offered, accepted, failed, installed, rolled_back]}, signature_algorithm: {const: Ed25519}, key_id: {$ref: '#/components/schemas/EdgeIdentifier'}, manifest_digest: {$ref: '#/components/schemas/Sha256Digest'}, signature: {$ref: '#/components/schemas/Ed25519Signature'}}",
  586. " EdgeEnrollmentEnvelope:",
  587. " type: object",
  588. " additionalProperties: false",
  589. " required: [code, message, data]",
  590. " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [enrollment_id, gateway_id, enrollment_token, expires_in, returned_once], properties: {enrollment_id: {type: string, format: uuid}, gateway_id: {type: string, format: uuid}, enrollment_token: {type: string, writeOnly: true}, expires_in: {type: integer}, returned_once: {const: true}}}}",
  591. " EdgeRegistrationEnvelope:",
  592. " type: object",
  593. " additionalProperties: false",
  594. " required: [code, message, data]",
  595. " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [gateway_id, generation, certificate_sha256, credential, credential_returned_once], properties: {gateway_id: {type: string, format: uuid}, generation: {type: integer}, certificate_sha256: {$ref: '#/components/schemas/Sha256Digest'}, credential: {type: string, writeOnly: true}, credential_returned_once: {const: true}}}}",
  596. " EdgeGatewayListEnvelope:",
  597. " type: object",
  598. " additionalProperties: false",
  599. " required: [code, message, data]",
  600. " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [gateways], properties: {gateways: {type: array, maxItems: 100, items: {type: object, description: 'Bounded gateway metadata; credential/token/private key are never included.'}}}}}",
  601. " EdgeTaskPullEnvelope:",
  602. " type: object",
  603. " additionalProperties: false",
  604. " required: [code, message, data]",
  605. " properties: {code: {type: integer}, message: {type: string}, data: {oneOf: [{type: object, additionalProperties: false, required: [task], properties: {task: {type: 'null'}}}, {type: object, additionalProperties: false, required: [task, signed_task_envelope, lease_token, lease_expires_at], properties: {task: {$ref: '#/components/schemas/EdgeTaskContract'}, signed_task_envelope: {$ref: '#/components/schemas/EdgeSignedTaskEnvelope'}, lease_token: {type: string, writeOnly: true}, lease_expires_at: {type: string, format: date-time}}}]}}",
  606. " EdgeEventAckEnvelope:",
  607. " type: object",
  608. " additionalProperties: false",
  609. " required: [code, message, data]",
  610. " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [event_id, event_digest, remote_lease_digest, received_at, status], properties: {event_id: {type: string}, event_digest: {$ref: '#/components/schemas/Sha256Digest'}, remote_lease_digest: {$ref: '#/components/schemas/Sha256Digest'}, received_at: {type: string, format: date-time}, status: {const: accepted}}}}",
  611. " EdgeReleaseEnvelope:",
  612. " type: object",
  613. " additionalProperties: false",
  614. " required: [code, message, data]",
  615. " properties: {code: {type: integer}, message: {type: string, maxLength: 200}, data: {type: object, additionalProperties: false, required: [release_id, status, version, artifact_digest, rollback_version, manifest], properties: {release_id: {type: string, format: uuid}, status: {type: string, enum: [offered, accepted, failed, installed, rolled_back]}, version: {$ref: '#/components/schemas/EdgeVersion'}, artifact_digest: {$ref: '#/components/schemas/Sha256Digest'}, rollback_version: {$ref: '#/components/schemas/EdgeVersion'}, manifest: {$ref: '#/components/schemas/EdgeReleaseManifest'}}}}",
  616. " ConnectorConfigValidationRequest:",
  617. " type: object",
  618. " additionalProperties: false",
  619. " required: [connector_id, version, config]",
  620. " properties:",
  621. " connector_id: {type: string, minLength: 3, maxLength: 64}",
  622. " version: {type: string, pattern: '^[0-9]+\\.[0-9]+\\.[0-9]+'}",
  623. " config: {type: object}",
  624. " ConnectorRunRequest:",
  625. " type: object",
  626. " additionalProperties: false",
  627. " required: [connector_id, version, source_uid, operation, config, scope, dry_run]",
  628. " properties:",
  629. " connector_id: {type: string}",
  630. " version: {type: string}",
  631. " source_uid: {type: string, format: uuid}",
  632. " operation: {type: string, enum: [discover, snapshot, incremental, lineage, profile, evidence]}",
  633. " config: {type: object}",
  634. " scope: {$ref: '#/components/schemas/ConnectorScope'}",
  635. " cursor: {type: object}",
  636. " checkpoint: {type: object}",
  637. " idempotency_key: {type: string, pattern: '^[a-f0-9]{64}$'}",
  638. " dry_run: {const: true}",
  639. " process_key: {type: string, minLength: 1, maxLength: 300}",
  640. " MachineConnectorRunRequest:",
  641. " type: object",
  642. " additionalProperties: false",
  643. " required: [connector_id, version, source_uid, business_domain_uid, environment, process_key, operation, scope]",
  644. " properties:",
  645. " connector_id: {type: string}",
  646. " version: {type: string}",
  647. " source_uid: {type: string, format: uuid}",
  648. " business_domain_uid: {type: string, format: uuid}",
  649. " environment: {type: string, enum: [development, staging, production]}",
  650. " process_key: {type: string, minLength: 1, maxLength: 300}",
  651. " operation: {type: string, enum: [discover, snapshot, incremental, lineage, profile, evidence]}",
  652. " scope: {$ref: '#/components/schemas/ConnectorScope'}",
  653. " cursor: {type: object}",
  654. " checkpoint: {type: object}",
  655. " idempotency_key: {type: string, pattern: '^[a-f0-9]{64}$'}",
  656. " dry_run: {type: boolean, default: false}",
  657. " ConnectorPrincipalRequest:",
  658. " type: object",
  659. " additionalProperties: false",
  660. " required: [connector_id, version, source_uid, business_domain_uid, environment, operations, scopes]",
  661. " properties:",
  662. " connector_id: {type: string}",
  663. " version: {type: string}",
  664. " source_uid: {type: string, format: uuid}",
  665. " business_domain_uid: {type: string, format: uuid}",
  666. " environment: {type: string, enum: [development, staging, production]}",
  667. " operations: {type: array, minItems: 1, uniqueItems: true, items: {type: string}}",
  668. " scopes: {$ref: '#/components/schemas/ConnectorScope'}",
  669. " source_binding_uid: {type: string, format: uuid}",
  670. " source_binding_version: {type: integer, minimum: 1}",
  671. " ConnectorSourceBindingRequest:",
  672. " type: object",
  673. " additionalProperties: false",
  674. " required: [connector_id, version, source_uid, business_domain_uid, environment, approved_config]",
  675. " properties:",
  676. " binding_uid: {type: string, format: uuid}",
  677. " connector_id: {type: string}",
  678. " version: {type: string}",
  679. " source_uid: {type: string, format: uuid}",
  680. " business_domain_uid: {type: string, format: uuid}",
  681. " environment: {type: string, enum: [development, staging, production]}",
  682. " approved_config: {type: object}",
  683. " ConnectorScope:",
  684. " type: object",
  685. " additionalProperties: false",
  686. " properties:",
  687. " include_schemas: {type: array, uniqueItems: true, items: {type: string, minLength: 1}}",
  688. " exclude_schemas: {type: array, uniqueItems: true, items: {type: string, minLength: 1}}",
  689. " include_tables: {type: array, uniqueItems: true, items: {type: string, minLength: 1}}",
  690. " exclude_tables: {type: array, uniqueItems: true, items: {type: string, minLength: 1}}",
  691. " ConnectorHealthRequest:",
  692. " type: object",
  693. " additionalProperties: false",
  694. " required: [config]",
  695. " properties: {config: {type: object}}",
  696. " ConnectorCredentialRequest:",
  697. " type: object",
  698. " additionalProperties: false",
  699. " properties: {ttl_seconds: {type: integer, minimum: 60, maximum: 900}}",
  700. " ConnectorGraphRequest:",
  701. " type: object",
  702. " additionalProperties: false",
  703. " properties: {source_uid: {type: string, format: uuid}, business_domain_uid: {type: string, format: uuid}, process_key: {type: string}, run_uid: {type: string, format: uuid}, limit: {type: integer, minimum: 1, maximum: 1000}}",
  704. " ConnectorManifestEnvelope:",
  705. " type: object",
  706. " additionalProperties: false",
  707. " required: [code, message, data]",
  708. " properties:",
  709. " code: {type: integer}",
  710. " message: {type: string}",
  711. " data: {type: object, additionalProperties: false, required: [manifests], properties: {manifests: {type: array, items: {$ref: '#/components/schemas/ConnectorManifest'}}}}",
  712. " ConnectorManifest:",
  713. " type: object",
  714. " additionalProperties: false",
  715. " required: [connector_id, version, sdk_version, display_name, capabilities, config_schema]",
  716. " properties: {connector_id: {type: string}, version: {type: string}, sdk_version: {type: string}, display_name: {type: string}, capabilities: {type: array, items: {type: string}}, config_schema: {type: object}}",
  717. " ConnectorOperationResult:",
  718. " type: object",
  719. " additionalProperties: false",
  720. " required: [records, cursor, checkpoint, evidence, status]",
  721. " properties: {records: {type: array, items: {type: object}}, cursor: {type: object}, checkpoint: {type: object}, evidence: {type: object}, status: {type: string, enum: [succeeded, dry_run]}}",
  722. " ConnectorRunResultEnvelope:",
  723. " type: object",
  724. " additionalProperties: false",
  725. " required: [code, message, data]",
  726. " properties: {code: {type: integer}, message: {type: string}, data: {$ref: '#/components/schemas/ConnectorOperationResult'}}",
  727. " ConnectorRunRecord:",
  728. " type: object",
  729. " additionalProperties: false",
  730. " required: [uid, idempotency_key, connector_id, connector_version, source_uid, operation, status, attempt_count, checkpoint_summary, cursor_summary, dry_run]",
  731. " properties: {uid: {type: string, format: uuid}, idempotency_key: {type: string}, connector_id: {type: string}, connector_version: {type: string}, source_uid: {type: string, format: uuid}, principal_uid: {type: [string, 'null'], format: uuid}, business_domain_uid: {type: [string, 'null'], format: uuid}, environment: {type: [string, 'null']}, process_key: {type: [string, 'null']}, operation: {type: string}, status: {type: string}, attempt_count: {type: integer}, checkpoint_summary: {type: object}, cursor_summary: {type: object}, scope: {type: object}, error_category: {type: [string, 'null']}, dry_run: {type: boolean}, created_at: {type: string}, updated_at: {type: string}}",
  732. " ConnectorRunRecordEnvelope:",
  733. " type: object",
  734. " additionalProperties: false",
  735. " required: [code, message, data]",
  736. " properties: {code: {type: integer}, message: {type: string}, data: {$ref: '#/components/schemas/ConnectorRunRecord'}}",
  737. " ConnectorRunListEnvelope:",
  738. " type: object",
  739. " additionalProperties: false",
  740. " required: [code, message, data]",
  741. " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [runs], properties: {runs: {type: array, items: {$ref: '#/components/schemas/ConnectorRunRecord'}}}}}",
  742. " ConnectorValidationEnvelope:",
  743. " type: object",
  744. " additionalProperties: false",
  745. " required: [code, message, data]",
  746. " properties: {code: {type: integer}, message: {type: string}, data: {type: object, required: [valid, config_keys], properties: {valid: {const: true}, config_keys: {type: array, items: {type: string}}}, additionalProperties: false}}",
  747. " ConnectorErrorEnvelope:",
  748. " type: object",
  749. " additionalProperties: false",
  750. " required: [code, message, data, error]",
  751. " properties:",
  752. " code: {type: integer}",
  753. " message: {type: string}",
  754. " data: {type: 'null'}",
  755. " error: {type: object, additionalProperties: false, required: [code, category, retryable], properties: {code: {const: CONNECTOR_ERROR}, category: {type: string, enum: [configuration, conflict, authentication, permission, rate_limit, timeout, upstream, contract, cancelled]}, retryable: {type: boolean}}}",
  756. " ConnectorHealthEnvelope:",
  757. " type: object",
  758. " additionalProperties: false",
  759. " required: [code, message, data]",
  760. " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [status, detail], properties: {status: {type: string}, detail: {type: string}}}}",
  761. " ConnectorCompatibilityEnvelope:",
  762. " type: object",
  763. " additionalProperties: false",
  764. " required: [code, message, data]",
  765. " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [compatible, connector_version, sdk_version, detail], properties: {compatible: {type: boolean}, connector_version: {type: string}, sdk_version: {type: string}, detail: {type: string}}}}",
  766. " ConnectorPrincipalEnvelope:",
  767. " type: object",
  768. " additionalProperties: false",
  769. " required: [code, message, data]",
  770. " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [principal_uid], properties: {principal_uid: {type: string, format: uuid}}}}",
  771. " ConnectorSourceBindingEnvelope:",
  772. " type: object",
  773. " additionalProperties: false",
  774. " required: [code, message, data]",
  775. " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [binding_uid, binding_version, status, rebound_principals], properties: {binding_uid: {type: string, format: uuid}, binding_version: {type: integer, minimum: 1}, status: {const: approved}, rebound_principals: {type: integer, minimum: 0}}}}",
  776. " ConnectorCredentialEnvelope:",
  777. " type: object",
  778. " additionalProperties: false",
  779. " required: [code, message, data]",
  780. " properties: {code: {type: integer}, message: {type: string}, data: {oneOf: [{type: object, additionalProperties: false, required: [credential_uid, credential, expires_in, returned_once], properties: {credential_uid: {type: string, format: uuid}, credential: {type: string, writeOnly: true}, expires_in: {type: integer}, returned_once: {const: true}}}, {type: object, additionalProperties: false, required: [revoked], properties: {revoked: {type: boolean}}}]}}",
  781. " ConnectorGraphEnvelope:",
  782. " type: object",
  783. " additionalProperties: false",
  784. " required: [code, message, data]",
  785. " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [nodes, edges, summary], properties: {nodes: {type: array, items: {type: object, additionalProperties: false, required: [type, key], properties: {type: {type: string, enum: [source, asset, process, business_domain, run]}, key: {type: string}}}}, edges: {type: array, items: {type: object}}, summary: {type: object, additionalProperties: false, required: [node_count, edge_count], properties: {node_count: {type: integer}, edge_count: {type: integer}}}}}}",
  786. " securitySchemes:",
  787. " mutualTLS:",
  788. " type: mutualTLS",
  789. " edgeCredential:",
  790. " type: apiKey",
  791. " in: header",
  792. " name: X-Edge-Credential",
  793. ' description: "One-time-returned edge credential; always combined with mutualTLS certificate proof."',
  794. " edgeEnrollment:",
  795. " type: apiKey",
  796. " in: header",
  797. " name: X-Edge-Enrollment",
  798. ' description: "Single-use enrollment token; always combined with mutualTLS certificate proof."',
  799. " bearerAuth:",
  800. " type: http",
  801. " scheme: bearer",
  802. " bearerFormat: JWT",
  803. ' description: "下一阶段统一认证方案;当前路由尚未全部接入。"',
  804. ]
  805. )
  806. return "\n".join(lines) + "\n"
  807. def main(output: Path = OUTPUT) -> None:
  808. routes = extract_routes()
  809. if not routes:
  810. raise SystemExit("No Flask routes found")
  811. output.parent.mkdir(parents=True, exist_ok=True)
  812. output.write_text(render(routes), encoding="utf-8")
  813. try:
  814. label = output.relative_to(ROOT)
  815. except ValueError:
  816. label = output
  817. print(f"Generated {label} with {len(routes)} operations")
  818. if __name__ == "__main__":
  819. parser = argparse.ArgumentParser(description=__doc__)
  820. parser.add_argument("--output", type=Path, default=OUTPUT)
  821. args = parser.parse_args()
  822. main(args.output)