@opensoha/contracts 0.1.23 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (415) hide show
  1. package/COMPATIBILITY.md +95 -0
  2. package/README.md +24 -6
  3. package/agent/environment/README.md +55 -0
  4. package/agent/environment/export.go +139 -0
  5. package/agent/environment/export_test.go +62 -0
  6. package/agent/environment/preflight.go +62 -0
  7. package/agent/environment/preflight_test.go +41 -0
  8. package/agent/environment/protocol.go +221 -0
  9. package/agent/environment/protocol_test.go +81 -0
  10. package/agent/environment-lease.schema.json +2 -2
  11. package/agent/environment-template.schema.json +1 -1
  12. package/agent/guest/browser_network.go +77 -0
  13. package/agent/guest/data.go +62 -0
  14. package/agent/guest/protocol.go +235 -0
  15. package/agent/guest/protocol_test.go +142 -0
  16. package/agent/guest/renewal.go +56 -0
  17. package/agent/guest/renewal_test.go +53 -0
  18. package/agent/guest/runtime.go +315 -0
  19. package/agent/guest/runtime_json.go +75 -0
  20. package/agent/guest/runtime_test.go +110 -0
  21. package/agent/personal-browser-network-request.schema.json +40 -0
  22. package/agent/personal-browser-network-response.schema.json +44 -0
  23. package/agent/personal-environment-export.schema.json +55 -0
  24. package/agent/personal-environment-preflight-request.schema.json +52 -0
  25. package/agent/personal-environment-preflight-result.schema.json +69 -0
  26. package/agent/personal-environment-request.schema.json +137 -0
  27. package/agent/personal-environment-result.schema.json +101 -0
  28. package/agent/personal-guest-binding.schema.json +92 -0
  29. package/agent/personal-guest-data-plan.schema.json +32 -0
  30. package/agent/personal-guest-enrollment.schema.json +21 -0
  31. package/agent/personal-guest-heartbeat.schema.json +44 -0
  32. package/agent/personal-guest-provisioning.schema.json +71 -0
  33. package/agent/personal-guest-renewal.schema.json +33 -0
  34. package/agent/personal-guest-runtime-result.schema.json +164 -0
  35. package/agent/personal-guest-runtime.schema.json +304 -0
  36. package/agent/personal-guest-tools.schema.json +75 -0
  37. package/auth/catalog_generated.go +2 -2
  38. package/auth/gateway_permissions_test.go +48 -0
  39. package/auth/permission-catalog.json +331 -1
  40. package/auth/token-claims.schema.json +5 -0
  41. package/compat/consumer-revisions.json +3 -2
  42. package/compat/json-schema-ecosystem-0.1.0.json +1 -1
  43. package/compat/openapi-loose-boundaries-0.1.0.json +1 -1
  44. package/connectors/connector-action.schema.json +138 -0
  45. package/fixtures/json-schema/connector-action/invalid/empty-values.json +11 -0
  46. package/fixtures/json-schema/connector-action/invalid/invalid-expiry.json +16 -0
  47. package/fixtures/json-schema/connector-action/invalid/lowercase-alias.json +16 -0
  48. package/fixtures/json-schema/connector-action/invalid/unknown-entry-field.json +17 -0
  49. package/fixtures/json-schema/connector-action/invalid/unknown-frame-field.json +17 -0
  50. package/fixtures/json-schema/connector-action/invalid/unknown-version.json +16 -0
  51. package/fixtures/json-schema/connector-action/invalid/unversioned-reference.json +16 -0
  52. package/fixtures/json-schema/connector-action/invalid/zero-version.json +16 -0
  53. package/fixtures/json-schema/connector-action/valid/pinned.json +16 -0
  54. package/fixtures/json-schema/connector-definition/invalid/credential-userinfo.json +8 -0
  55. package/fixtures/json-schema/connector-definition/invalid/reference-traversal.json +8 -0
  56. package/fixtures/json-schema/connector-definition/invalid/zero-version.json +8 -0
  57. package/fixtures/json-schema/connector-definition/valid/canonical.json +8 -0
  58. package/fixtures/json-schema/connector-definition/valid/pinned.json +8 -0
  59. package/fixtures/json-schema/personal-browser-network-request/invalid/bounded.json +4 -0
  60. package/fixtures/json-schema/personal-browser-network-request/valid/bounded.json +4 -0
  61. package/fixtures/json-schema/personal-browser-network-response/invalid/bounded.json +5 -0
  62. package/fixtures/json-schema/personal-browser-network-response/valid/bounded.json +5 -0
  63. package/fixtures/json-schema/personal-environment-export/invalid/extra.json +11 -0
  64. package/fixtures/json-schema/personal-environment-export/invalid/ownerId.json +10 -0
  65. package/fixtures/json-schema/personal-environment-export/invalid/sha256.json +10 -0
  66. package/fixtures/json-schema/personal-environment-export/invalid/sizeBytes.json +10 -0
  67. package/fixtures/json-schema/personal-environment-export/valid/complete.json +10 -0
  68. package/fixtures/json-schema/personal-environment-preflight-request/invalid/disk.json +11 -0
  69. package/fixtures/json-schema/personal-environment-preflight-request/invalid/network.json +11 -0
  70. package/fixtures/json-schema/personal-environment-preflight-request/invalid/node.json +12 -0
  71. package/fixtures/json-schema/personal-environment-preflight-request/invalid/owner.json +12 -0
  72. package/fixtures/json-schema/personal-environment-preflight-request/valid/bounded.json +11 -0
  73. package/fixtures/json-schema/personal-environment-preflight-result/invalid/disk.json +15 -0
  74. package/fixtures/json-schema/personal-environment-preflight-result/invalid/network.json +15 -0
  75. package/fixtures/json-schema/personal-environment-preflight-result/invalid/no-hard-quota.json +15 -0
  76. package/fixtures/json-schema/personal-environment-preflight-result/invalid/node.json +16 -0
  77. package/fixtures/json-schema/personal-environment-preflight-result/invalid/not-ready.json +15 -0
  78. package/fixtures/json-schema/personal-environment-preflight-result/invalid/owner.json +16 -0
  79. package/fixtures/json-schema/personal-environment-preflight-result/valid/bounded.json +15 -0
  80. package/fixtures/json-schema/personal-environment-request/invalid/delete-data-argv.json +24 -0
  81. package/fixtures/json-schema/personal-environment-request/invalid/delete-data-createData.json +22 -0
  82. package/fixtures/json-schema/personal-environment-request/invalid/delete-data-taskId.json +22 -0
  83. package/fixtures/json-schema/personal-environment-request/invalid/export-argv.json +22 -0
  84. package/fixtures/json-schema/personal-environment-request/invalid/export-createData.json +22 -0
  85. package/fixtures/json-schema/personal-environment-request/invalid/export-taskId.json +22 -0
  86. package/fixtures/json-schema/personal-environment-request/invalid/extra-command.json +24 -0
  87. package/fixtures/json-schema/personal-environment-request/invalid/host-workspace.json +22 -0
  88. package/fixtures/json-schema/personal-environment-request/invalid/image-injection.json +22 -0
  89. package/fixtures/json-schema/personal-environment-request/invalid/network-egress.json +21 -0
  90. package/fixtures/json-schema/personal-environment-request/invalid/no-command.json +21 -0
  91. package/fixtures/json-schema/personal-environment-request/invalid/recreate-on-release.json +22 -0
  92. package/fixtures/json-schema/personal-environment-request/invalid/relative-command.json +24 -0
  93. package/fixtures/json-schema/personal-environment-request/invalid/released-identity.json +22 -0
  94. package/fixtures/json-schema/personal-environment-request/invalid/task-empty.json +27 -0
  95. package/fixtures/json-schema/personal-environment-request/invalid/task-host-path.json +27 -0
  96. package/fixtures/json-schema/personal-environment-request/invalid/task-noncanonical.json +27 -0
  97. package/fixtures/json-schema/personal-environment-request/invalid/task-null.json +27 -0
  98. package/fixtures/json-schema/personal-environment-request/invalid/task-on-inspection.json +22 -0
  99. package/fixtures/json-schema/personal-environment-request/invalid/unbounded-quota.json +21 -0
  100. package/fixtures/json-schema/personal-environment-request/valid/acquire.json +21 -0
  101. package/fixtures/json-schema/personal-environment-request/valid/delete-data.json +21 -0
  102. package/fixtures/json-schema/personal-environment-request/valid/execute.json +26 -0
  103. package/fixtures/json-schema/personal-environment-request/valid/export-data.json +21 -0
  104. package/fixtures/json-schema/personal-environment-request/valid/inspect.json +21 -0
  105. package/fixtures/json-schema/personal-environment-request/valid/provision.json +22 -0
  106. package/fixtures/json-schema/personal-environment-request/valid/release.json +21 -0
  107. package/fixtures/json-schema/personal-environment-request/valid/task-workspace.json +27 -0
  108. package/fixtures/json-schema/personal-environment-result/invalid/active-without-observation.json +7 -0
  109. package/fixtures/json-schema/personal-environment-result/invalid/deleted-output.json +8 -0
  110. package/fixtures/json-schema/personal-environment-result/invalid/deleted-snapshot.json +8 -0
  111. package/fixtures/json-schema/personal-environment-result/invalid/deleted-taskId.json +8 -0
  112. package/fixtures/json-schema/personal-environment-result/invalid/private-handle.json +8 -0
  113. package/fixtures/json-schema/personal-environment-result/invalid/stopped-fake-observation.json +20 -0
  114. package/fixtures/json-schema/personal-environment-result/invalid/task-empty.json +22 -0
  115. package/fixtures/json-schema/personal-environment-result/invalid/task-host-path.json +22 -0
  116. package/fixtures/json-schema/personal-environment-result/invalid/task-null.json +22 -0
  117. package/fixtures/json-schema/personal-environment-result/invalid/task-on-stopped.json +8 -0
  118. package/fixtures/json-schema/personal-environment-result/invalid/task-without-command-output.json +21 -0
  119. package/fixtures/json-schema/personal-environment-result/valid/active.json +21 -0
  120. package/fixtures/json-schema/personal-environment-result/valid/deleted.json +7 -0
  121. package/fixtures/json-schema/personal-environment-result/valid/expired.json +7 -0
  122. package/fixtures/json-schema/personal-environment-result/valid/missing.json +7 -0
  123. package/fixtures/json-schema/personal-environment-result/valid/released.json +7 -0
  124. package/fixtures/json-schema/personal-environment-result/valid/task-workspace.json +22 -0
  125. package/fixtures/json-schema/personal-guest-binding/invalid/host-path.json +17 -0
  126. package/fixtures/json-schema/personal-guest-binding/invalid/invalid-policy.json +17 -0
  127. package/fixtures/json-schema/personal-guest-binding/invalid/invalid-serial.json +17 -0
  128. package/fixtures/json-schema/personal-guest-binding/invalid/legacy-version.json +17 -0
  129. package/fixtures/json-schema/personal-guest-binding/invalid/missing-generation.json +17 -0
  130. package/fixtures/json-schema/personal-guest-binding/invalid/missing-identity.json +16 -0
  131. package/fixtures/json-schema/personal-guest-binding/invalid/missing-provider-identity.json +16 -0
  132. package/fixtures/json-schema/personal-guest-binding/invalid/raw-provider-identity.json +17 -0
  133. package/fixtures/json-schema/personal-guest-binding/valid/minimal.json +17 -0
  134. package/fixtures/json-schema/personal-guest-data-plan/invalid/quota.json +1 -0
  135. package/fixtures/json-schema/personal-guest-data-plan/valid/basic.json +1 -0
  136. package/fixtures/json-schema/personal-guest-enrollment/invalid/capabilities.json +7 -0
  137. package/fixtures/json-schema/personal-guest-enrollment/invalid/legacy-version.json +4 -0
  138. package/fixtures/json-schema/personal-guest-enrollment/invalid/missing-identity.json +3 -0
  139. package/fixtures/json-schema/personal-guest-enrollment/invalid/ownerId.json +5 -0
  140. package/fixtures/json-schema/personal-guest-enrollment/invalid/token.json +5 -0
  141. package/fixtures/json-schema/personal-guest-enrollment/valid/minimal.json +4 -0
  142. package/fixtures/json-schema/personal-guest-heartbeat/invalid/argv.json +11 -0
  143. package/fixtures/json-schema/personal-guest-heartbeat/invalid/legacy-version.json +8 -0
  144. package/fixtures/json-schema/personal-guest-heartbeat/invalid/missing-identity.json +7 -0
  145. package/fixtures/json-schema/personal-guest-heartbeat/invalid/owner.json +9 -0
  146. package/fixtures/json-schema/personal-guest-heartbeat/invalid/self-ready.json +9 -0
  147. package/fixtures/json-schema/personal-guest-heartbeat/invalid/zero-sequence.json +8 -0
  148. package/fixtures/json-schema/personal-guest-heartbeat/valid/minimal.json +8 -0
  149. package/fixtures/json-schema/personal-guest-provisioning/invalid/network.json +1 -0
  150. package/fixtures/json-schema/personal-guest-provisioning/valid/basic.json +1 -0
  151. package/fixtures/json-schema/personal-guest-renewal/invalid/unknown.json +1 -0
  152. package/fixtures/json-schema/personal-guest-renewal/valid/basic.json +1 -0
  153. package/fixtures/json-schema/personal-guest-runtime/invalid/minimal.json +45 -0
  154. package/fixtures/json-schema/personal-guest-runtime/valid/minimal.json +45 -0
  155. package/fixtures/json-schema/personal-guest-runtime-result/invalid/minimal.json +28 -0
  156. package/fixtures/json-schema/personal-guest-runtime-result/valid/minimal.json +27 -0
  157. package/fixtures/json-schema/personal-guest-tools/invalid/minimal.json +31 -0
  158. package/fixtures/json-schema/personal-guest-tools/valid/minimal.json +30 -0
  159. package/fixtures/json-schema/source-import-policy/invalid/case-variant-scope.json +4 -0
  160. package/fixtures/json-schema/source-import-policy/invalid/case-variant-version.json +4 -0
  161. package/fixtures/json-schema/source-import-policy/invalid/duplicate-grant.json +4 -0
  162. package/fixtures/json-schema/source-import-policy/invalid/empty-id.json +4 -0
  163. package/fixtures/json-schema/source-import-policy/invalid/future-version.json +4 -0
  164. package/fixtures/json-schema/source-import-policy/invalid/unknown-field.json +4 -0
  165. package/fixtures/json-schema/source-import-policy/valid/deny-all.json +4 -0
  166. package/fixtures/json-schema/source-import-policy/valid/shared.json +4 -0
  167. package/fixtures/json-schema/token-claims/invalid/authz-negative.json +23 -0
  168. package/fixtures/json-schema/token-claims/invalid/authz-string.json +23 -0
  169. package/fixtures/json-schema/token-claims/invalid/authz-zero.json +23 -0
  170. package/fixtures/json-schema/token-claims/valid/versioned-session.json +23 -0
  171. package/fixtures/openapi/AgentRunEventListEnvelope/invalid/basic.json +8 -0
  172. package/fixtures/openapi/AgentRunEventListEnvelope/valid/basic.json +8 -0
  173. package/fixtures/openapi/AgentRunEventListEnvelope/valid/native.json +1 -0
  174. package/fixtures/openapi/AgentRunResumeRequest/invalid/basic.json +3 -0
  175. package/fixtures/openapi/AgentRunResumeRequest/valid/basic.json +3 -0
  176. package/fixtures/openapi/AgentSSHCommandRequest/invalid/task51.json +24 -0
  177. package/fixtures/openapi/AgentSSHCommandRequest/valid/task51.json +23 -0
  178. package/fixtures/openapi/AgentSSHCommandResult/invalid/task51.json +8 -0
  179. package/fixtures/openapi/AgentSSHCommandResult/valid/task51.json +7 -0
  180. package/fixtures/openapi/AgentSSHSessionRequest/invalid/task51.json +20 -0
  181. package/fixtures/openapi/AgentSSHSessionRequest/valid/task51.json +19 -0
  182. package/fixtures/openapi/CapabilityTaskInput/invalid/duplicate-scope.json +36 -0
  183. package/fixtures/openapi/CapabilityTaskInput/valid/scoped.json +32 -0
  184. package/fixtures/openapi/DockerQuickCreateHostInput/invalid/guest-access.json +7 -0
  185. package/fixtures/openapi/DockerQuickCreateHostInput/valid/guest-access.json +9 -0
  186. package/fixtures/openapi/KnowledgeSourceDocument/invalid/tombstone-with-content.json +1 -0
  187. package/fixtures/openapi/KnowledgeSourceDocument/valid/tombstone.json +1 -0
  188. package/fixtures/openapi/KubernetesAgentActionCheck/invalid/unproven-state.json +6 -0
  189. package/fixtures/openapi/KubernetesAgentActionCheck/valid/configured-denied.json +9 -0
  190. package/fixtures/openapi/KubernetesAgentActionProfile/invalid/wildcard.json +1 -0
  191. package/fixtures/openapi/KubernetesAgentActionProfile/valid/workbench.json +1 -0
  192. package/fixtures/openapi/KubernetesClusterRegistrationRequest/invalid/action-profile.json +3 -0
  193. package/fixtures/openapi/KubernetesClusterRegistrationRequest/valid/action-profile.json +5 -0
  194. package/fixtures/openapi/KubernetesNodeListEnvelope/invalid/condition-status-boolean.json +17 -0
  195. package/fixtures/openapi/KubernetesNodeListEnvelope/invalid/taint-effect-missing.json +16 -0
  196. package/fixtures/openapi/KubernetesNodeListEnvelope/valid/legacy-fields-absent.json +11 -0
  197. package/fixtures/openapi/KubernetesNodeListEnvelope/valid/no-taints.json +13 -0
  198. package/fixtures/openapi/KubernetesNodeListEnvelope/valid/ready-cordoned-tainted.json +28 -0
  199. package/fixtures/openapi/KubernetesNodeListEnvelope/valid/unknown-fields-null.json +13 -0
  200. package/fixtures/openapi/KubernetesResourceUpdatePlanRequest/invalid/missing-required.json +1 -0
  201. package/fixtures/openapi/KubernetesResourceYaml/invalid/missing-required.json +1 -0
  202. package/fixtures/openapi/KubernetesTopologyCoverage/invalid/empty-as-success.json +5 -0
  203. package/fixtures/openapi/KubernetesTopologyCoverage/valid/not-installed.json +6 -0
  204. package/fixtures/openapi/KubernetesTopologyGraphEdge/invalid/negative-weight.json +8 -0
  205. package/fixtures/openapi/KubernetesTopologyGraphEdge/valid/zero-weight.json +20 -0
  206. package/fixtures/openapi/KubernetesTopologyGraphNode/invalid/fake-flow-kind.json +42 -0
  207. package/fixtures/openapi/KubernetesTopologyGraphNode/valid/rule.json +42 -0
  208. package/fixtures/openapi/KubernetesTopologySnapshot/invalid/raw-source-field.json +33 -0
  209. package/fixtures/openapi/KubernetesTopologySnapshot/valid/projected-source.json +32 -0
  210. package/fixtures/openapi/KubernetesTopologySnapshotRequest/invalid/arbitrary-gvr.json +8 -0
  211. package/fixtures/openapi/KubernetesTopologySnapshotRequest/valid/bounded-sources.json +11 -0
  212. package/fixtures/openapi/KubernetesTopologySourceQuery/invalid/cluster-namespace.json +4 -0
  213. package/fixtures/openapi/KubernetesTopologySourceQuery/valid/all-namespaces.json +4 -0
  214. package/fixtures/openapi/ObservabilityDataSource/invalid/missing-required.json +1 -0
  215. package/fixtures/openapi/PersonalAgentAccess/invalid/invalid.json +1 -0
  216. package/fixtures/openapi/PersonalAgentAccess/valid/valid.json +1 -0
  217. package/fixtures/openapi/PersonalAgentAccessDecision/invalid/invalid.json +1 -0
  218. package/fixtures/openapi/PersonalAgentAccessDecision/valid/valid.json +1 -0
  219. package/fixtures/openapi/PersonalAssistantArtifactListEnvelope/invalid/owner.json +8 -0
  220. package/fixtures/openapi/PersonalAssistantArtifactListEnvelope/valid/side-chat.json +9 -0
  221. package/fixtures/openapi/PersonalAssistantAvatarRequest/invalid/motion.json +1 -0
  222. package/fixtures/openapi/PersonalAssistantAvatarRequest/invalid/owner.json +1 -0
  223. package/fixtures/openapi/PersonalAssistantAvatarRequest/invalid/remote.json +1 -0
  224. package/fixtures/openapi/PersonalAssistantAvatarRequest/invalid/svg.json +1 -0
  225. package/fixtures/openapi/PersonalAssistantAvatarRequest/valid/raster.json +1 -0
  226. package/fixtures/openapi/PersonalAssistantAvatarRequest/valid/reset.json +1 -0
  227. package/fixtures/openapi/PersonalAssistantUpdateRequest/invalid/empty.json +1 -0
  228. package/fixtures/openapi/PersonalAssistantUpdateRequest/invalid/owner.json +1 -0
  229. package/fixtures/openapi/PersonalAssistantUpdateRequest/valid/name.json +1 -0
  230. package/fixtures/openapi/PersonalDockerEnvironmentPolicy/invalid/host-url.json +12 -0
  231. package/fixtures/openapi/PersonalDockerEnvironmentPolicy/invalid/lifetime.json +11 -0
  232. package/fixtures/openapi/PersonalDockerEnvironmentPolicy/invalid/unknown-agent.json +11 -0
  233. package/fixtures/openapi/PersonalDockerEnvironmentPolicy/valid/basic.json +11 -0
  234. package/fixtures/openapi/PersonalEnvironment/invalid/invented-state.json +6 -0
  235. package/fixtures/openapi/PersonalEnvironment/invalid/missing-state.json +5 -0
  236. package/fixtures/openapi/PersonalEnvironment/invalid/negative-generation.json +7 -0
  237. package/fixtures/openapi/PersonalEnvironment/invalid/network.json +23 -0
  238. package/fixtures/openapi/PersonalEnvironment/invalid/private-node.json +24 -0
  239. package/fixtures/openapi/PersonalEnvironment/invalid/unsupported-backend.json +7 -0
  240. package/fixtures/openapi/PersonalEnvironment/valid/deleted.json +23 -0
  241. package/fixtures/openapi/PersonalEnvironment/valid/deleting.json +23 -0
  242. package/fixtures/openapi/PersonalEnvironment/valid/deletion_unknown.json +23 -0
  243. package/fixtures/openapi/PersonalEnvironment/valid/frozen-binding.json +13 -0
  244. package/fixtures/openapi/PersonalEnvironment/valid/ready.json +23 -0
  245. package/fixtures/openapi/PersonalEnvironment/valid/unknown.json +23 -0
  246. package/fixtures/openapi/PersonalEnvironment/valid/unprovisioned.json +6 -0
  247. package/fixtures/openapi/PersonalEnvironmentActionRequest/invalid/command.json +3 -0
  248. package/fixtures/openapi/PersonalEnvironmentActionRequest/invalid/node.json +4 -0
  249. package/fixtures/openapi/PersonalEnvironmentActionRequest/invalid/owner.json +4 -0
  250. package/fixtures/openapi/PersonalEnvironmentActionRequest/invalid/quota.json +10 -0
  251. package/fixtures/openapi/PersonalEnvironmentActionRequest/valid/reconcile.json +3 -0
  252. package/fixtures/openapi/PersonalEnvironmentActionRequest/valid/sleep.json +3 -0
  253. package/fixtures/openapi/PersonalEnvironmentActionRequest/valid/wake.json +4 -0
  254. package/fixtures/openapi/PersonalEnvironmentDataDeletionRequest/invalid/empty-name.json +5 -0
  255. package/fixtures/openapi/PersonalEnvironmentDataDeletionRequest/invalid/long-name.json +5 -0
  256. package/fixtures/openapi/PersonalEnvironmentDataDeletionRequest/invalid/missing-name.json +4 -0
  257. package/fixtures/openapi/PersonalEnvironmentDataDeletionRequest/invalid/owner.json +6 -0
  258. package/fixtures/openapi/PersonalEnvironmentDataDeletionRequest/invalid/stale-version.json +5 -0
  259. package/fixtures/openapi/PersonalEnvironmentDataDeletionRequest/valid/confirmed.json +5 -0
  260. package/fixtures/openapi/PersonalEnvironmentDataExportRequest/invalid/assistantId.json +4 -0
  261. package/fixtures/openapi/PersonalEnvironmentDataExportRequest/invalid/environmentVersion.json +4 -0
  262. package/fixtures/openapi/PersonalEnvironmentDataExportRequest/invalid/ownerId.json +5 -0
  263. package/fixtures/openapi/PersonalEnvironmentDataExportRequest/valid/current.json +4 -0
  264. package/fixtures/openapi/PersonalEnvironmentEnvelope/invalid/missing-data.json +1 -0
  265. package/fixtures/openapi/PersonalEnvironmentEnvelope/valid/state.json +8 -0
  266. package/fixtures/openapi/PersonalEnvironmentQuota/invalid/cpu.json +7 -0
  267. package/fixtures/openapi/PersonalEnvironmentQuota/invalid/disk.json +7 -0
  268. package/fixtures/openapi/PersonalEnvironmentQuota/invalid/network.json +7 -0
  269. package/fixtures/openapi/PersonalEnvironmentQuota/invalid/processes.json +7 -0
  270. package/fixtures/openapi/PersonalEnvironmentQuota/valid/basic.json +7 -0
  271. package/fixtures/openapi/PersonalEnvironmentTemplate/invalid/mode.json +13 -0
  272. package/fixtures/openapi/PersonalEnvironmentTemplate/invalid/network.json +13 -0
  273. package/fixtures/openapi/PersonalEnvironmentTemplate/invalid/node.json +14 -0
  274. package/fixtures/openapi/PersonalEnvironmentTemplate/valid/template.json +13 -0
  275. package/fixtures/openapi/PersonalEnvironmentTemplateListEnvelope/invalid/missing.json +1 -0
  276. package/fixtures/openapi/PersonalEnvironmentTemplateListEnvelope/invalid/unbounded.json +1317 -0
  277. package/fixtures/openapi/PersonalEnvironmentTemplateListEnvelope/valid/choices.json +17 -0
  278. package/fixtures/openapi/PersonalEnvironmentTemplateListEnvelope/valid/empty.json +3 -0
  279. package/fixtures/openapi/PersonalEnvironmentUsage/invalid/negative.json +6 -0
  280. package/fixtures/openapi/PersonalEnvironmentUsage/valid/usage.json +6 -0
  281. package/fixtures/openapi/PersonalFeedFeedback/invalid/owner.json +4 -0
  282. package/fixtures/openapi/PersonalFeedFeedback/valid/liked.json +3 -0
  283. package/fixtures/openapi/PersonalFeedItem/invalid/no-source.json +9 -0
  284. package/fixtures/openapi/PersonalFeedItem/valid/post.json +15 -0
  285. package/fixtures/openapi/PersonalFeedSettings/invalid/owner.json +6 -0
  286. package/fixtures/openapi/PersonalFeedSettings/valid/default.json +5 -0
  287. package/fixtures/openapi/PersonalGuestPreparationInput/invalid/foreign-owner.json +5 -0
  288. package/fixtures/openapi/PersonalGuestPreparationInput/invalid/unsafe-counter.json +4 -0
  289. package/fixtures/openapi/PersonalGuestPreparationInput/valid/ed25519-csr.json +4 -0
  290. package/fixtures/openapi/PersonalGuestRuntimeEnvelope/invalid/minimal.json +10 -0
  291. package/fixtures/openapi/PersonalGuestRuntimeEnvelope/valid/minimal.json +9 -0
  292. package/fixtures/openapi/PersonalGuestRuntimeInput/invalid/minimal.json +6 -0
  293. package/fixtures/openapi/PersonalGuestRuntimeInput/valid/minimal.json +8 -0
  294. package/fixtures/openapi/PersonalGuestToolsInput/invalid/minimal.json +5 -0
  295. package/fixtures/openapi/PersonalGuestToolsInput/valid/minimal.json +4 -0
  296. package/fixtures/openapi/PersonalVMEnvironmentPolicy/invalid/device.json +15 -0
  297. package/fixtures/openapi/PersonalVMEnvironmentPolicy/invalid/fractional-size.json +15 -0
  298. package/fixtures/openapi/PersonalVMEnvironmentPolicy/invalid/network.json +15 -0
  299. package/fixtures/openapi/PersonalVMEnvironmentPolicy/invalid/owner.json +15 -0
  300. package/fixtures/openapi/PersonalVMEnvironmentPolicy/invalid/provider.json +15 -0
  301. package/fixtures/openapi/PersonalVMEnvironmentPolicy/invalid/unknown.json +16 -0
  302. package/fixtures/openapi/PersonalVMEnvironmentPolicy/valid/pve.json +15 -0
  303. package/fixtures/openapi/PersonalWorkspaceRuntime/invalid/shared.json +3 -0
  304. package/fixtures/openapi/PersonalWorkspaceRuntime/valid/shared.json +4 -0
  305. package/fixtures/openapi/PersonalWorkspaceRuntimeEnvelope/invalid/shared.json +5 -0
  306. package/fixtures/openapi/PersonalWorkspaceRuntimeEnvelope/valid/shared.json +6 -0
  307. package/fixtures/openapi/ResourceMonitoringPanel/invalid/unknown-quality.json +7 -0
  308. package/fixtures/openapi/ResourceMonitoringPanel/valid/not-collected.json +7 -0
  309. package/fixtures/openapi/ResourceMonitoringPoint/invalid/string-zero.json +4 -0
  310. package/fixtures/openapi/ResourceMonitoringPoint/valid/missing.json +4 -0
  311. package/fixtures/openapi/ResourceMonitoringPoint/valid/zero.json +4 -0
  312. package/fixtures/openapi/ResourceMonitoringView/invalid/raw-query.json +12 -0
  313. package/fixtures/openapi/ResourceMonitoringView/valid/unconfigured.json +12 -0
  314. package/fixtures/openapi/SecretGenerateSSHKeyRequest/invalid/task51.json +6 -0
  315. package/fixtures/openapi/SecretGenerateSSHKeyRequest/valid/task51.json +12 -0
  316. package/fixtures/openapi/SecretReferenceUsage/invalid/version.json +6 -0
  317. package/fixtures/openapi/SecretReferenceUsage/valid/version.json +8 -0
  318. package/fixtures/openapi/VMSSHCredentialRedemptionInput/invalid/task51.json +7 -0
  319. package/fixtures/openapi/VMSSHCredentialRedemptionInput/valid/task51.json +6 -0
  320. package/fixtures/openapi/VirtualMachineAccessBindingInput/invalid/task51.json +10 -0
  321. package/fixtures/openapi/VirtualMachineAccessBindingInput/valid/task51.json +10 -0
  322. package/fixtures/openapi/VirtualMachineActionInput/invalid/capacity-not-boolean.json +1 -0
  323. package/fixtures/openapi/VirtualMachineActionInput/valid/new-data-with-admission.json +1 -0
  324. package/fixtures/openapi/VirtualMachineCreateInput/valid/cloud-image.json +12 -0
  325. package/fixtures/openapi/VirtualMachineDiskChange/invalid/serial-config-injection.json +1 -0
  326. package/fixtures/openapi/VirtualMachineDiskChange/invalid/serial-existing-disk.json +1 -0
  327. package/fixtures/openapi/VirtualMachineDiskChange/invalid/serial-too-long.json +1 -0
  328. package/fixtures/openapi/VirtualMachineDiskChange/valid/personal-data.json +1 -0
  329. package/fixtures/openapi/VirtualMachineGuestAccessInput/invalid/task51.json +4 -0
  330. package/fixtures/openapi/VirtualMachineGuestAccessInput/valid/task51.json +6 -0
  331. package/fixtures/openapi/VirtualMachineSSHCommand/invalid/task51.json +9 -0
  332. package/fixtures/openapi/VirtualMachineSSHCommand/valid/task51.json +8 -0
  333. package/fixtures/openapi/VirtualMachineSSHCommandInput/invalid/task51.json +5 -0
  334. package/fixtures/openapi/VirtualMachineSSHCommandInput/valid/task51.json +5 -0
  335. package/fixtures/openapi/VirtualMachineSSHFrame/invalid/encoding.json +5 -0
  336. package/fixtures/openapi/VirtualMachineSSHFrame/invalid/task51.json +4 -0
  337. package/fixtures/openapi/VirtualMachineSSHFrame/valid/binary.json +5 -0
  338. package/fixtures/openapi/VirtualMachineSSHFrame/valid/task51.json +5 -0
  339. package/fixtures/openapi/VirtualMachineSSHSessionInput/invalid/task51.json +4 -0
  340. package/fixtures/openapi/VirtualMachineSSHSessionInput/valid/task51.json +5 -0
  341. package/fixtures/openapi/VirtualizationCapacityPolicyInput/invalid/task51.json +12 -0
  342. package/fixtures/openapi/VirtualizationCapacityPolicyInput/valid/task51.json +13 -0
  343. package/fixtures/openapi/VirtualizationImageInput/invalid/cloud-image-config-type.json +6 -0
  344. package/fixtures/openapi/VirtualizationImageInput/valid/cloud-image.json +11 -0
  345. package/fixtures/openapi/WorkbenchAssistantTask/invalid/kind.json +1 -0
  346. package/fixtures/openapi/WorkbenchAssistantTask/invalid/owner.json +1 -0
  347. package/fixtures/openapi/WorkbenchAssistantTask/valid/generate.json +1 -0
  348. package/fixtures/openapi/WorkbenchAssistantTask/valid/undo.json +1 -0
  349. package/fixtures/openapi/WorkbenchContextSelection/invalid/remote-image.json +1 -0
  350. package/fixtures/openapi/WorkbenchContextSelection/invalid/too-many-images.json +1 -0
  351. package/fixtures/openapi/WorkbenchContextSelection/valid/chat-image.json +9 -0
  352. package/fixtures/openapi/WorkbenchContextUsage/invalid/fractional-capacity.json +1 -0
  353. package/fixtures/openapi/WorkbenchContextUsage/invalid/negative.json +1 -0
  354. package/fixtures/openapi/WorkbenchContextUsage/valid/reported.json +1 -0
  355. package/fixtures/openapi/WorkbenchContextUsage/valid/unknown.json +1 -0
  356. package/fixtures/openapi/WorkbenchMainSessionRequest/invalid/owner.json +1 -0
  357. package/fixtures/openapi/WorkbenchMainSessionRequest/invalid/provider.json +1 -0
  358. package/fixtures/openapi/WorkbenchMainSessionRequest/valid/empty.json +1 -0
  359. package/fixtures/openapi/WorkbenchMainSessionRequest/valid/provider.json +1 -0
  360. package/fixtures/openapi/WorkbenchMessageSearchPage/invalid/role.json +12 -0
  361. package/fixtures/openapi/WorkbenchMessageSearchPage/valid/page.json +12 -0
  362. package/fixtures/openapi/WorkbenchMessageWindow/invalid/missing.json +1 -0
  363. package/fixtures/openapi/WorkbenchMessageWindow/valid/empty.json +3 -0
  364. package/fixtures/openapi/WorkbenchModelOption/invalid/zero-capacity.json +1 -0
  365. package/fixtures/openapi/WorkbenchModelOption/valid/capacity.json +1 -0
  366. package/fixtures/openapi/WorkbenchModelOption/valid/unknown.json +1 -0
  367. package/fixtures/openapi/WorkbenchProfessionalCapabilityInput/invalid/basic.json +7 -0
  368. package/fixtures/openapi/WorkbenchProfessionalCapabilityInput/valid/basic.json +11 -0
  369. package/fixtures/openapi/WorkbenchProfessionalCapabilityInput/valid/selected-profile.json +23 -0
  370. package/fixtures/openapi/WorkbenchSendMessageStreamRequest/invalid/missing-required.json +1 -0
  371. package/fixtures/openapi/WorkbenchSendMessageStreamRequest/valid/mixed-professional-task.json +22 -0
  372. package/fixtures/openapi/WorkbenchTaskIntent/invalid/basic.json +5 -0
  373. package/fixtures/openapi/WorkbenchTaskIntent/invalid/oversized-capability-source.json +3 -0
  374. package/fixtures/openapi/WorkbenchTaskIntent/valid/basic.json +9 -0
  375. package/fixtures/openapi/WorkbenchTaskIntent/valid/independent-sources.json +17 -0
  376. package/gen/go/sohaapi/client.go +1 -1
  377. package/gen/go/sohaapi/kubernetes_monitoring_test.go +67 -0
  378. package/gen/go/sohaapi/personal_environment_test.go +25 -0
  379. package/gen/go/sohaapi/types.go +3460 -433
  380. package/gen/go/sohaapi/vm_disk_test.go +21 -0
  381. package/gen/ts/sohaapi/index.ts +5454 -1397
  382. package/go.mod +61 -49
  383. package/go.sum +202 -176
  384. package/knowledge/connector-definition.schema.json +6 -1
  385. package/knowledge/source-import-policy.schema.json +32 -0
  386. package/memory/memory-record.schema.json +1 -1
  387. package/openapi/soha-api.json +24199 -16810
  388. package/openapi/soha-api.yaml +2537 -14
  389. package/openapi/spec_generated.go +3 -3
  390. package/openapi/spec_test.go +34 -0
  391. package/package.json +19 -3
  392. package/resource/agent_access.go +12 -0
  393. package/resource/agent_profiles.go +62 -0
  394. package/resource/agent_profiles_test.go +36 -0
  395. package/resource/kubernetes_kinds.go +73 -0
  396. package/resource/kubernetes_kinds_test.go +38 -0
  397. package/resource/models.go +23 -14
  398. package/resource/network_topology.go +166 -0
  399. package/resource/network_topology_projection.go +208 -0
  400. package/resource/network_topology_projection_test.go +53 -0
  401. package/resource/network_topology_test.go +51 -0
  402. package/resource/network_topology_validation.go +29 -0
  403. package/resource/node_drain.go +34 -0
  404. package/resource/resource_monitoring.go +99 -0
  405. package/resource/runtime/network_topology.go +232 -0
  406. package/resource/runtime/network_topology_test.go +62 -0
  407. package/resource/runtime/node_drain.go +124 -0
  408. package/resource/runtime/node_drain_test.go +95 -0
  409. package/resource/runtime/resource_edit.go +128 -0
  410. package/resource/runtime/resource_edit_test.go +183 -0
  411. package/resource/runtime/resource_monitoring.go +243 -0
  412. package/resource/runtime/resource_monitoring_test.go +88 -0
  413. /package/fixtures/openapi/DeliveryExecutionHistoryPage/{invalid-kind.json → invalid/invalid-kind.json} +0 -0
  414. /package/fixtures/openapi/DeliveryExecutionHistoryPage/{valid-build.json → valid/valid-build.json} +0 -0
  415. /package/fixtures/openapi/DeliveryExecutionHistoryPage/{valid-empty.json → valid/valid-empty.json} +0 -0
@@ -0,0 +1,232 @@
1
+ package runtime
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "errors"
7
+ "fmt"
8
+ "io"
9
+ "strings"
10
+ "sync"
11
+ "time"
12
+
13
+ resource "github.com/opensoha/soha-contracts/resource"
14
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
15
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16
+ "k8s.io/apimachinery/pkg/runtime/schema"
17
+ "k8s.io/client-go/rest"
18
+ )
19
+
20
+ // ReadNetworkTopologySnapshot performs only fixed, discovery-verified, bounded reads.
21
+ // The caller authenticates and authorizes each query before invoking this reader.
22
+ func ReadNetworkTopologySnapshot(ctx context.Context, client rest.Interface, clusterID string, input resource.NetworkTopologySnapshotRequest) (resource.NetworkTopologySnapshot, error) {
23
+ if err := resource.ValidateNetworkTopologySnapshotRequest(input); err != nil {
24
+ return resource.NetworkTopologySnapshot{}, err
25
+ }
26
+ if client == nil {
27
+ return resource.NetworkTopologySnapshot{}, fmt.Errorf("kubernetes REST client unavailable")
28
+ }
29
+ ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
30
+ defer cancel()
31
+ output := resource.NetworkTopologySnapshot{Version: resource.NetworkTopologySnapshotFeature, Sources: make([]resource.NetworkTopologySourceResult, len(input.Queries))}
32
+ budget := &topologyReadBudget{remaining: resource.NetworkTopologyMaxTotalBytes}
33
+ sem := make(chan struct{}, 4)
34
+ var wg sync.WaitGroup
35
+ for index, query := range input.Queries {
36
+ wg.Add(1)
37
+ go func(index int, query resource.NetworkTopologySourceQuery) {
38
+ defer wg.Done()
39
+ select {
40
+ case sem <- struct{}{}:
41
+ defer func() { <-sem }()
42
+ case <-ctx.Done():
43
+ output.Sources[index] = topologySourceFailure(query, "error", "timeout")
44
+ return
45
+ }
46
+ output.Sources[index] = readTopologySource(ctx, client, clusterID, query, budget)
47
+ }(index, query)
48
+ }
49
+ wg.Wait()
50
+ total := 0
51
+ for i := range output.Sources {
52
+ source := &output.Sources[i]
53
+ kept := source.Objects[:0]
54
+ for _, object := range source.Objects {
55
+ size := len(object.Content)
56
+ if total+size > resource.NetworkTopologyMaxTotalBytes {
57
+ source.Coverage.State = "partial"
58
+ source.Coverage.Truncated = true
59
+ source.Coverage.ReasonCode = "total_budget"
60
+ continue
61
+ }
62
+ total += size
63
+ kept = append(kept, object)
64
+ }
65
+ source.Objects = kept
66
+ }
67
+ return output, nil
68
+ }
69
+
70
+ func topologySourceFailure(query resource.NetworkTopologySourceQuery, state, reason string) resource.NetworkTopologySourceResult {
71
+ definition, _ := resource.LookupNetworkTopologySource(query.SourceID)
72
+ return resource.NetworkTopologySourceResult{Coverage: resource.NetworkTopologyCoverage{Provider: definition.Provider, SourceID: query.SourceID, Namespace: query.Namespace, State: state, ReasonCode: reason, CheckedAt: time.Now().UTC().Format(time.RFC3339), SupportedKinds: []string{definition.Kind}}, Objects: []resource.NetworkTopologySourceObject{}}
73
+ }
74
+
75
+ func readTopologySource(ctx context.Context, client rest.Interface, clusterID string, query resource.NetworkTopologySourceQuery, budget *topologyReadBudget) resource.NetworkTopologySourceResult {
76
+ definition, _ := resource.LookupNetworkTopologySource(query.SourceID)
77
+ result := topologySourceFailure(query, "available", "")
78
+ version, err := discoverTopologySource(ctx, client, definition, budget)
79
+ if err != nil {
80
+ result.Coverage.State, result.Coverage.ReasonCode = topologyReadError(err)
81
+ return result
82
+ }
83
+ base := topologyAPIBase(definition.Group, version)
84
+ if query.Namespace != "" {
85
+ base += "/namespaces/" + query.Namespace
86
+ }
87
+ raw, err := boundedTopologyGET(ctx, client, base+"/"+definition.Resource, true, budget)
88
+ if err != nil {
89
+ result.Coverage.State, result.Coverage.ReasonCode = topologyReadError(err)
90
+ return result
91
+ }
92
+ var list struct {
93
+ Metadata struct {
94
+ Continue string `json:"continue"`
95
+ } `json:"metadata"`
96
+ Items []map[string]any `json:"items"`
97
+ }
98
+ if json.Unmarshal(raw, &list) != nil {
99
+ result.Coverage.State = "error"
100
+ result.Coverage.ReasonCode = "invalid_response"
101
+ return result
102
+ }
103
+ result.Coverage.ObservedAt = time.Now().UTC().Format(time.RFC3339)
104
+ if len(list.Items) > resource.NetworkTopologyMaxSourceItems || list.Metadata.Continue != "" {
105
+ result.Coverage.Truncated = true
106
+ result.Coverage.State = "partial"
107
+ result.Coverage.ReasonCode = "item_budget"
108
+ }
109
+ for index, object := range list.Items {
110
+ if index >= resource.NetworkTopologyMaxSourceItems {
111
+ break
112
+ }
113
+ projected, err := resource.ProjectNetworkTopologyObject(query.SourceID, object)
114
+ if err != nil {
115
+ result.Coverage.Truncated = true
116
+ result.Coverage.State = "partial"
117
+ result.Coverage.ReasonCode = "object_budget"
118
+ continue
119
+ }
120
+ metadata, _ := projected["metadata"].(map[string]any)
121
+ name, _ := metadata["name"].(string)
122
+ namespace, _ := metadata["namespace"].(string)
123
+ if name == "" || (query.Namespace != "" && namespace != query.Namespace) {
124
+ result.Coverage.State = "partial"
125
+ result.Coverage.ReasonCode = "invalid_identity"
126
+ continue
127
+ }
128
+ uid, _ := metadata["uid"].(string)
129
+ rv, _ := metadata["resourceVersion"].(string)
130
+ generation, _ := metadata["generation"].(float64)
131
+ content, _ := json.Marshal(projected)
132
+ result.Objects = append(result.Objects, resource.NetworkTopologySourceObject{Resource: resource.ResourceRef{ClusterID: clusterID, APIVersion: strings.TrimPrefix(definition.Group+"/"+version, "/"), ScopeMode: topologyScope(definition.Namespaced), Kind: definition.Kind, Namespace: namespace, Name: name, UID: uid, ResourceVersion: rv}, Generation: int64(generation), Content: string(content)})
133
+ }
134
+ return result
135
+ }
136
+
137
+ func topologyAPIBase(group, version string) string {
138
+ if group == "" {
139
+ return "/api/" + version
140
+ }
141
+ return "/apis/" + group + "/" + version
142
+ }
143
+
144
+ func discoverTopologySource(ctx context.Context, client rest.Interface, definition resource.NetworkTopologySourceDefinition, budget *topologyReadBudget) (string, error) {
145
+ versions := []string{definition.Version}
146
+ if definition.FallbackVersion != "" {
147
+ versions = append(versions, definition.FallbackVersion)
148
+ }
149
+ for _, version := range versions {
150
+ raw, err := boundedTopologyGET(ctx, client, topologyAPIBase(definition.Group, version), false, budget)
151
+ if apierrors.IsNotFound(err) {
152
+ continue
153
+ }
154
+ if err != nil {
155
+ return "", err
156
+ }
157
+ var list metav1.APIResourceList
158
+ if err := json.Unmarshal(raw, &list); err != nil {
159
+ return "", fmt.Errorf("invalid discovery")
160
+ }
161
+ for _, item := range list.APIResources {
162
+ if item.Name == definition.Resource && item.Kind == definition.Kind && item.Namespaced == definition.Namespaced {
163
+ return version, nil
164
+ }
165
+ }
166
+ }
167
+ return "", apierrors.NewNotFound(schema.GroupResource{Group: definition.Group, Resource: definition.Resource}, "")
168
+ }
169
+
170
+ func boundedTopologyGET(ctx context.Context, client rest.Interface, path string, list bool, budget *topologyReadBudget) ([]byte, error) {
171
+ request := client.Get().AbsPath(path).SetHeader("Accept", "application/json")
172
+ if list {
173
+ request = request.Param("limit", fmt.Sprint(resource.NetworkTopologyMaxSourceItems))
174
+ }
175
+ stream, err := request.Stream(ctx)
176
+ if err != nil {
177
+ return nil, err
178
+ }
179
+ defer func() { _ = stream.Close() }()
180
+ body, err := io.ReadAll(io.LimitReader(&topologyBudgetReader{reader: stream, budget: budget}, resource.NetworkTopologyMaxSourceBytes+1))
181
+ if err != nil {
182
+ return nil, err
183
+ }
184
+ if len(body) > resource.NetworkTopologyMaxSourceBytes {
185
+ return nil, fmt.Errorf("topology byte budget")
186
+ }
187
+ return body, nil
188
+ }
189
+
190
+ func topologyReadError(err error) (string, string) {
191
+ switch {
192
+ case apierrors.IsForbidden(err) || apierrors.IsUnauthorized(err):
193
+ return "denied", "kubernetes_rbac"
194
+ case apierrors.IsNotFound(err):
195
+ return "not_installed", "source_not_served"
196
+ case strings.Contains(err.Error(), "budget"):
197
+ return "partial", "byte_budget"
198
+ default:
199
+ return "error", "source_unavailable"
200
+ }
201
+ }
202
+
203
+ func topologyScope(namespaced bool) resource.ResourceScopeMode {
204
+ if namespaced {
205
+ return resource.ResourceScopeModeNamespace
206
+ }
207
+ return resource.ResourceScopeModeCluster
208
+ }
209
+
210
+ type topologyReadBudget struct {
211
+ mu sync.Mutex
212
+ remaining int
213
+ }
214
+ type topologyBudgetReader struct {
215
+ reader io.Reader
216
+ budget *topologyReadBudget
217
+ }
218
+
219
+ func (r *topologyBudgetReader) Read(data []byte) (int, error) {
220
+ r.budget.mu.Lock()
221
+ available := min(len(data), r.budget.remaining)
222
+ r.budget.remaining -= available
223
+ r.budget.mu.Unlock()
224
+ if available <= 0 {
225
+ return 0, errors.New("topology total byte budget")
226
+ }
227
+ count, err := r.reader.Read(data[:available])
228
+ r.budget.mu.Lock()
229
+ r.budget.remaining += available - count
230
+ r.budget.mu.Unlock()
231
+ return count, err
232
+ }
@@ -0,0 +1,62 @@
1
+ package runtime
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "fmt"
7
+ "net/http"
8
+ "net/http/httptest"
9
+ "strings"
10
+ "testing"
11
+
12
+ cr "github.com/opensoha/soha-contracts/resource"
13
+ "k8s.io/client-go/kubernetes"
14
+ "k8s.io/client-go/rest"
15
+ )
16
+
17
+ func TestTopologySnapshotBoundsDiscoveryAndProjection(t *testing.T) {
18
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19
+ switch r.URL.Path {
20
+ case "/api/v1":
21
+ _, _ = fmt.Fprint(w, `{"resources":[{"name":"pods","kind":"Pod","namespaced":true},{"name":"services","kind":"Service","namespaced":true}]}`)
22
+ case "/api/v1/namespaces/team/pods":
23
+ if r.URL.Query().Get("limit") != "200" {
24
+ t.Error("missing item bound")
25
+ }
26
+ _, _ = fmt.Fprint(w, `{"metadata":{"continue":"next"},"items":[{"metadata":{"name":"p","namespace":"team","uid":"uid","resourceVersion":"9"},"spec":{"secret":"credential"},"status":{"phase":"Running"}}]}`)
27
+ case "/api/v1/namespaces/team/services":
28
+ http.Error(w, `{"kind":"Status","apiVersion":"v1","reason":"Forbidden","code":403}`, http.StatusForbidden)
29
+ case "/apis/traefik.io/v1alpha1":
30
+ http.NotFound(w, r)
31
+ default:
32
+ t.Errorf("unexpected path: %s", r.URL.Path)
33
+ http.NotFound(w, r)
34
+ }
35
+ }))
36
+ defer server.Close()
37
+ client, err := kubernetes.NewForConfig(&rest.Config{Host: server.URL})
38
+ if err != nil {
39
+ t.Fatal(err)
40
+ }
41
+ output, err := ReadNetworkTopologySnapshot(context.Background(), client.CoreV1().RESTClient(), "cluster", cr.NetworkTopologySnapshotRequest{Queries: []cr.NetworkTopologySourceQuery{{SourceID: "kubernetes.pods", Namespace: "team"}, {SourceID: "kubernetes.services", Namespace: "team"}, {SourceID: "traefik.ingressroutes", Namespace: "team"}}})
42
+ if err != nil {
43
+ t.Fatal(err)
44
+ }
45
+ if output.Sources[0].Coverage.State != "partial" || !output.Sources[0].Coverage.Truncated || output.Sources[1].Coverage.State != "denied" || output.Sources[2].Coverage.State != "not_installed" {
46
+ t.Fatalf("coverage: %+v", output.Sources)
47
+ }
48
+ raw, _ := json.Marshal(output)
49
+ if strings.Contains(string(raw), "credential") {
50
+ t.Fatal("full manifest escaped")
51
+ }
52
+ if output.Sources[0].Objects[0].Resource.ResourceVersion != "9" {
53
+ t.Fatal("identity lost")
54
+ }
55
+ }
56
+
57
+ func TestTopologySnapshotDoesNotReadArbitrarySources(t *testing.T) {
58
+ _, err := ReadNetworkTopologySnapshot(context.Background(), nil, "c", cr.NetworkTopologySnapshotRequest{Queries: []cr.NetworkTopologySourceQuery{{SourceID: "secrets"}}})
59
+ if err == nil {
60
+ t.Fatal("unknown source accepted")
61
+ }
62
+ }
@@ -0,0 +1,124 @@
1
+ package runtime
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "sync"
7
+
8
+ "github.com/opensoha/soha-contracts/resource"
9
+ policyv1 "k8s.io/api/policy/v1"
10
+ policyv1beta1 "k8s.io/api/policy/v1beta1"
11
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
12
+ "k8s.io/client-go/kubernetes"
13
+ policyclient "k8s.io/client-go/kubernetes/typed/policy/v1"
14
+ policybetaclient "k8s.io/client-go/kubernetes/typed/policy/v1beta1"
15
+ )
16
+
17
+ // DrainClient observes eviction failures while leaving kubectl drain semantics intact.
18
+ type DrainClient struct {
19
+ kubernetes.Interface
20
+ failures sync.Map
21
+ }
22
+
23
+ func NewDrainClient(client kubernetes.Interface) *DrainClient {
24
+ return &DrainClient{Interface: client}
25
+ }
26
+
27
+ func (c *DrainClient) PolicyV1() policyclient.PolicyV1Interface {
28
+ return drainPolicy{PolicyV1Interface: c.Interface.PolicyV1(), observer: c}
29
+ }
30
+
31
+ func (c *DrainClient) PolicyV1beta1() policybetaclient.PolicyV1beta1Interface {
32
+ return drainBetaPolicy{PolicyV1beta1Interface: c.Interface.PolicyV1beta1(), observer: c}
33
+ }
34
+
35
+ func (c *DrainClient) observe(namespace, name string, err error) error {
36
+ key := namespace + "/" + name
37
+ if err == nil || apierrors.IsNotFound(err) {
38
+ c.failures.Delete(key)
39
+ } else {
40
+ c.failures.Store(key, err)
41
+ }
42
+ return err
43
+ }
44
+
45
+ func (c *DrainClient) Failure(ctx context.Context, err error) error {
46
+ if err == nil {
47
+ return nil
48
+ }
49
+ code := "node_drain_failed"
50
+ var denied error
51
+ c.failures.Range(func(_, value any) bool {
52
+ failure, ok := value.(error)
53
+ if !ok {
54
+ return true
55
+ }
56
+ switch {
57
+ case apierrors.IsForbidden(failure) || apierrors.IsUnauthorized(failure):
58
+ denied = failure
59
+ case apierrors.HasStatusCause(failure, policyv1.DisruptionBudgetCause):
60
+ code = "node_drain_pdb_blocked"
61
+ case apierrors.IsTooManyRequests(failure) && code != "node_drain_pdb_blocked":
62
+ code = "node_drain_eviction_blocked"
63
+ }
64
+ return true
65
+ })
66
+ if denied != nil {
67
+ return denied
68
+ }
69
+ if errors.Is(ctx.Err(), context.Canceled) {
70
+ code = "node_drain_canceled"
71
+ } else if code == "node_drain_failed" && errors.Is(ctx.Err(), context.DeadlineExceeded) {
72
+ code = "node_drain_timeout"
73
+ }
74
+ return &resource.NodeDrainError{Code: code, Cause: err}
75
+ }
76
+
77
+ type drainPolicy struct {
78
+ policyclient.PolicyV1Interface
79
+ observer *DrainClient
80
+ }
81
+
82
+ func (c *DrainClient) PodSelectionFailure(ctx context.Context, failures []error) error {
83
+ for _, err := range failures {
84
+ if apierrors.IsForbidden(err) || apierrors.IsUnauthorized(err) || apierrors.IsNotFound(err) {
85
+ return err
86
+ }
87
+ }
88
+ err := errors.Join(failures...)
89
+ if ctx.Err() != nil {
90
+ return c.Failure(ctx, err)
91
+ }
92
+ return &resource.NodeDrainError{Code: "node_drain_pod_selection", Cause: err}
93
+ }
94
+
95
+ func (c drainPolicy) Evictions(namespace string) policyclient.EvictionInterface {
96
+ return drainEvictions{EvictionInterface: c.PolicyV1Interface.Evictions(namespace), observer: c.observer}
97
+ }
98
+
99
+ type drainEvictions struct {
100
+ policyclient.EvictionInterface
101
+ observer *DrainClient
102
+ }
103
+
104
+ func (c drainEvictions) Evict(ctx context.Context, eviction *policyv1.Eviction) error {
105
+ return c.observer.observe(eviction.Namespace, eviction.Name, c.EvictionInterface.Evict(ctx, eviction))
106
+ }
107
+
108
+ type drainBetaPolicy struct {
109
+ policybetaclient.PolicyV1beta1Interface
110
+ observer *DrainClient
111
+ }
112
+
113
+ func (c drainBetaPolicy) Evictions(namespace string) policybetaclient.EvictionInterface {
114
+ return drainBetaEvictions{EvictionInterface: c.PolicyV1beta1Interface.Evictions(namespace), observer: c.observer}
115
+ }
116
+
117
+ type drainBetaEvictions struct {
118
+ policybetaclient.EvictionInterface
119
+ observer *DrainClient
120
+ }
121
+
122
+ func (c drainBetaEvictions) Evict(ctx context.Context, eviction *policyv1beta1.Eviction) error {
123
+ return c.observer.observe(eviction.Namespace, eviction.Name, c.EvictionInterface.Evict(ctx, eviction))
124
+ }
@@ -0,0 +1,95 @@
1
+ package runtime
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "strings"
7
+ "testing"
8
+ "time"
9
+
10
+ "github.com/opensoha/soha-contracts/resource"
11
+ policyv1 "k8s.io/api/policy/v1"
12
+ policyv1beta1 "k8s.io/api/policy/v1beta1"
13
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
14
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15
+ "k8s.io/apimachinery/pkg/runtime"
16
+ "k8s.io/apimachinery/pkg/runtime/schema"
17
+ "k8s.io/client-go/kubernetes/fake"
18
+ ktesting "k8s.io/client-go/testing"
19
+ )
20
+
21
+ func TestDrainFeedbackClassifiesTypedEvictionCausesAndClearsRecoveredBlockers(t *testing.T) {
22
+ pdb := &apierrors.StatusError{ErrStatus: metav1.Status{Code: 429, Reason: metav1.StatusReasonTooManyRequests, Message: "private provider message", Details: &metav1.StatusDetails{Causes: []metav1.StatusCause{{Type: policyv1.DisruptionBudgetCause}}}}}
23
+ for _, beta := range []bool{false, true} {
24
+ t.Run(map[bool]string{false: "v1", true: "v1beta1"}[beta], func(t *testing.T) {
25
+ client := fake.NewClientset()
26
+ var evictionError error = pdb
27
+ client.PrependReactor("create", "pods", func(ktesting.Action) (bool, runtime.Object, error) { return true, nil, evictionError })
28
+ observed := NewDrainClient(client)
29
+ evict := func() error {
30
+ meta := metav1.ObjectMeta{Name: "pod", Namespace: "team"}
31
+ if beta {
32
+ return observed.PolicyV1beta1().Evictions("team").Evict(context.Background(), &policyv1beta1.Eviction{ObjectMeta: meta})
33
+ }
34
+ return observed.PolicyV1().Evictions("team").Evict(context.Background(), &policyv1.Eviction{ObjectMeta: meta})
35
+ }
36
+ _ = evict()
37
+ assertDrainCode(t, observed.Failure(context.Background(), errors.New("private token")), "node_drain_pdb_blocked")
38
+ evictionError = apierrors.NewTooManyRequests("private throttle", 1)
39
+ _ = evict()
40
+ assertDrainCode(t, observed.Failure(context.Background(), errors.New("private")), "node_drain_eviction_blocked")
41
+ evictionError = nil
42
+ _ = evict()
43
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-time.Second))
44
+ defer cancel()
45
+ assertDrainCode(t, observed.Failure(ctx, errors.New("private")), "node_drain_timeout")
46
+ evictionError = apierrors.NewForbidden(schema.GroupResource{Resource: "pods"}, "pod", errors.New("private"))
47
+ _ = evict()
48
+ if !apierrors.IsForbidden(observed.Failure(context.Background(), errors.New("private"))) {
49
+ t.Fatal("eviction RBAC denial must retain forbidden semantics")
50
+ }
51
+ })
52
+ }
53
+ }
54
+
55
+ func TestDrainFeedbackKeepsCancellationAndSuccessDistinct(t *testing.T) {
56
+ client := NewDrainClient(fake.NewClientset())
57
+ ctx, cancel := context.WithCancel(context.Background())
58
+ cancel()
59
+ assertDrainCode(t, client.Failure(ctx, ctx.Err()), "node_drain_canceled")
60
+ if err := client.Failure(ctx, nil); err != nil {
61
+ t.Fatalf("success changed: %v", err)
62
+ }
63
+ if resource.IsNodeDrainErrorCode("provider_arbitrary_error") {
64
+ t.Fatal("untrusted error code accepted")
65
+ }
66
+ }
67
+
68
+ func TestDrainFeedbackDoesNotHidePermissionDenialBehindPDB(t *testing.T) {
69
+ client := NewDrainClient(fake.NewClientset())
70
+ client.failures.Store("invalid", struct{}{})
71
+ assertDrainCode(t, client.Failure(context.Background(), errors.New("private")), "node_drain_failed")
72
+ blocked := &apierrors.StatusError{ErrStatus: metav1.Status{Code: 429, Details: &metav1.StatusDetails{Causes: []metav1.StatusCause{{Type: policyv1.DisruptionBudgetCause}}}}}
73
+ _ = client.observe("team", "blocked", blocked)
74
+ canceled, cancel := context.WithCancel(context.Background())
75
+ cancel()
76
+ assertDrainCode(t, client.Failure(canceled, canceled.Err()), "node_drain_canceled")
77
+ _ = client.observe("team", "denied", apierrors.NewForbidden(schema.GroupResource{Resource: "pods/eviction"}, "denied", errors.New("private")))
78
+ if err := client.Failure(canceled, canceled.Err()); !apierrors.IsForbidden(err) {
79
+ t.Fatalf("permission denial hidden: %v", err)
80
+ }
81
+ if err := client.PodSelectionFailure(context.Background(), []error{apierrors.NewForbidden(schema.GroupResource{Resource: "pods"}, "", errors.New("private"))}); !apierrors.IsForbidden(err) {
82
+ t.Fatalf("pod list permission hidden: %v", err)
83
+ }
84
+ }
85
+
86
+ func assertDrainCode(t *testing.T, err error, want string) {
87
+ t.Helper()
88
+ var failure *resource.NodeDrainError
89
+ if !errors.As(err, &failure) || failure.Code != want {
90
+ t.Fatalf("got %v; want %s", err, want)
91
+ }
92
+ if strings.Contains(err.Error(), "private") {
93
+ t.Fatal("provider text leaked")
94
+ }
95
+ }
@@ -0,0 +1,128 @@
1
+ package runtime
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "errors"
7
+ "fmt"
8
+ "strings"
9
+
10
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
11
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
13
+ "k8s.io/apimachinery/pkg/types"
14
+ "k8s.io/apimachinery/pkg/util/sets"
15
+ "k8s.io/client-go/dynamic"
16
+ "k8s.io/client-go/util/csaupgrade"
17
+ "k8s.io/utils/ptr"
18
+ )
19
+
20
+ // ApplyResourceEdit preserves SSA conflicts while migrating this editor's own Create/Update fields.
21
+ // Dry-run migration validates a complete manifest with Update; it never changes live ownership.
22
+ func ApplyResourceEdit(ctx context.Context, resource dynamic.ResourceInterface, current, desired *unstructured.Unstructured, manager string, dryRun bool) (*unstructured.Unstructured, error) {
23
+ patch, err := json.Marshal(desired.Object)
24
+ if err != nil {
25
+ return nil, err
26
+ }
27
+ options := metav1.PatchOptions{FieldManager: manager, Force: ptr.To(false)}
28
+ if dryRun {
29
+ options.DryRun = []string{metav1.DryRunAll}
30
+ }
31
+ migration, err := resourceEditMigrationPatch(current, manager)
32
+ if err != nil {
33
+ return nil, err
34
+ }
35
+ if len(migration) == 0 {
36
+ return resource.Patch(ctx, desired.GetName(), types.ApplyPatchType, patch, options)
37
+ }
38
+ if dryRun {
39
+ // Probe foreign ownership first; an initial list merge can also fail validation
40
+ // until this creator's Update ownership is migrated.
41
+ probed, probeErr := resource.Patch(ctx, desired.GetName(), types.ApplyPatchType, patch, options)
42
+ if probeErr == nil {
43
+ return probed, nil
44
+ }
45
+ if !resourceEditPortMergeInvalid(current, probeErr) && !onlyResourceEditConflicts(current, probeErr, manager) {
46
+ return nil, probeErr
47
+ }
48
+ return resource.Update(ctx, desired, metav1.UpdateOptions{FieldManager: manager, DryRun: []string{metav1.DryRunAll}})
49
+ }
50
+ migrated, err := resource.Patch(ctx, desired.GetName(), types.JSONPatchType, migration, metav1.PatchOptions{})
51
+ if err != nil {
52
+ return nil, err
53
+ }
54
+ desired = desired.DeepCopy()
55
+ desired.SetResourceVersion(migrated.GetResourceVersion())
56
+ patch, err = json.Marshal(desired.Object)
57
+ if err != nil {
58
+ return nil, err
59
+ }
60
+ return resource.Patch(ctx, desired.GetName(), types.ApplyPatchType, patch, options)
61
+ }
62
+
63
+ func onlyResourceEditConflicts(current *unstructured.Unstructured, conflict error, manager string) bool {
64
+ var status apierrors.APIStatus
65
+ if !apierrors.IsConflict(conflict) || !errors.As(conflict, &status) {
66
+ return false
67
+ }
68
+ details := status.Status().Details
69
+ if details == nil || len(details.Causes) == 0 {
70
+ return false
71
+ }
72
+ prefix := fmt.Sprintf("conflict with %q using %s", manager, current.GetAPIVersion())
73
+ for _, cause := range details.Causes {
74
+ if cause.Type != metav1.CauseTypeFieldManagerConflict || cause.Field == "" || (cause.Message != prefix && !strings.HasPrefix(cause.Message, prefix+" at ")) {
75
+ return false
76
+ }
77
+ }
78
+ return true
79
+ }
80
+
81
+ func resourceEditMigrationPatch(current *unstructured.Unstructured, manager string) ([]byte, error) {
82
+ if manager == "" || current.GetUID() == "" || current.GetResourceVersion() == "" {
83
+ return nil, nil
84
+ }
85
+ updates := 0
86
+ for _, entry := range current.GetManagedFields() {
87
+ if entry.Manager != manager || entry.Subresource != "" {
88
+ continue
89
+ }
90
+ // client-go's upgrader discards other Update versions; fail closed instead.
91
+ if entry.APIVersion != current.GetAPIVersion() {
92
+ return nil, nil
93
+ }
94
+ if entry.Operation == metav1.ManagedFieldsOperationUpdate {
95
+ updates++
96
+ }
97
+ }
98
+ if updates != 1 {
99
+ return nil, nil
100
+ }
101
+ patch, err := csaupgrade.UpgradeManagedFieldsPatch(current, sets.New(manager), manager)
102
+ if err != nil || len(patch) == 0 {
103
+ return patch, err
104
+ }
105
+ var operations []map[string]any
106
+ if err := json.Unmarshal(patch, &operations); err != nil {
107
+ return nil, err
108
+ }
109
+ operations = append([]map[string]any{{"op": "test", "path": "/metadata/uid", "value": string(current.GetUID())}}, operations...)
110
+ return json.Marshal(operations)
111
+ }
112
+
113
+ func resourceEditPortMergeInvalid(current *unstructured.Unstructured, err error) bool {
114
+ var status apierrors.APIStatus
115
+ if current.GetKind() != "Service" || !apierrors.IsInvalid(err) || !errors.As(err, &status) {
116
+ return false
117
+ }
118
+ details := status.Status().Details
119
+ if details == nil || len(details.Causes) == 0 {
120
+ return false
121
+ }
122
+ for _, cause := range details.Causes {
123
+ if cause.Type != metav1.CauseTypeFieldValueRequired || !strings.HasPrefix(cause.Field, "spec.ports[") || !strings.HasSuffix(cause.Field, "].name") {
124
+ return false
125
+ }
126
+ }
127
+ return true
128
+ }