@opensoha/contracts 0.1.22 → 0.1.24

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 (361) 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/permission-catalog.json +32 -1
  39. package/auth/token-claims.schema.json +5 -0
  40. package/compat/consumer-revisions.json +4 -3
  41. package/compat/json-schema-ecosystem-0.1.0.json +1 -1
  42. package/compat/openapi-loose-boundaries-0.1.0.json +1 -1
  43. package/connectors/connector-action.schema.json +138 -0
  44. package/fixtures/json-schema/connector-action/invalid/empty-values.json +11 -0
  45. package/fixtures/json-schema/connector-action/invalid/invalid-expiry.json +16 -0
  46. package/fixtures/json-schema/connector-action/invalid/lowercase-alias.json +16 -0
  47. package/fixtures/json-schema/connector-action/invalid/unknown-entry-field.json +17 -0
  48. package/fixtures/json-schema/connector-action/invalid/unknown-frame-field.json +17 -0
  49. package/fixtures/json-schema/connector-action/invalid/unknown-version.json +16 -0
  50. package/fixtures/json-schema/connector-action/invalid/unversioned-reference.json +16 -0
  51. package/fixtures/json-schema/connector-action/invalid/zero-version.json +16 -0
  52. package/fixtures/json-schema/connector-action/valid/pinned.json +16 -0
  53. package/fixtures/json-schema/connector-definition/invalid/credential-userinfo.json +8 -0
  54. package/fixtures/json-schema/connector-definition/invalid/reference-traversal.json +8 -0
  55. package/fixtures/json-schema/connector-definition/invalid/zero-version.json +8 -0
  56. package/fixtures/json-schema/connector-definition/valid/canonical.json +8 -0
  57. package/fixtures/json-schema/connector-definition/valid/pinned.json +8 -0
  58. package/fixtures/json-schema/personal-browser-network-request/invalid/bounded.json +4 -0
  59. package/fixtures/json-schema/personal-browser-network-request/valid/bounded.json +4 -0
  60. package/fixtures/json-schema/personal-browser-network-response/invalid/bounded.json +5 -0
  61. package/fixtures/json-schema/personal-browser-network-response/valid/bounded.json +5 -0
  62. package/fixtures/json-schema/personal-environment-export/invalid/extra.json +11 -0
  63. package/fixtures/json-schema/personal-environment-export/invalid/ownerId.json +10 -0
  64. package/fixtures/json-schema/personal-environment-export/invalid/sha256.json +10 -0
  65. package/fixtures/json-schema/personal-environment-export/invalid/sizeBytes.json +10 -0
  66. package/fixtures/json-schema/personal-environment-export/valid/complete.json +10 -0
  67. package/fixtures/json-schema/personal-environment-preflight-request/invalid/disk.json +11 -0
  68. package/fixtures/json-schema/personal-environment-preflight-request/invalid/network.json +11 -0
  69. package/fixtures/json-schema/personal-environment-preflight-request/invalid/node.json +12 -0
  70. package/fixtures/json-schema/personal-environment-preflight-request/invalid/owner.json +12 -0
  71. package/fixtures/json-schema/personal-environment-preflight-request/valid/bounded.json +11 -0
  72. package/fixtures/json-schema/personal-environment-preflight-result/invalid/disk.json +15 -0
  73. package/fixtures/json-schema/personal-environment-preflight-result/invalid/network.json +15 -0
  74. package/fixtures/json-schema/personal-environment-preflight-result/invalid/no-hard-quota.json +15 -0
  75. package/fixtures/json-schema/personal-environment-preflight-result/invalid/node.json +16 -0
  76. package/fixtures/json-schema/personal-environment-preflight-result/invalid/not-ready.json +15 -0
  77. package/fixtures/json-schema/personal-environment-preflight-result/invalid/owner.json +16 -0
  78. package/fixtures/json-schema/personal-environment-preflight-result/valid/bounded.json +15 -0
  79. package/fixtures/json-schema/personal-environment-request/invalid/delete-data-argv.json +24 -0
  80. package/fixtures/json-schema/personal-environment-request/invalid/delete-data-createData.json +22 -0
  81. package/fixtures/json-schema/personal-environment-request/invalid/delete-data-taskId.json +22 -0
  82. package/fixtures/json-schema/personal-environment-request/invalid/export-argv.json +22 -0
  83. package/fixtures/json-schema/personal-environment-request/invalid/export-createData.json +22 -0
  84. package/fixtures/json-schema/personal-environment-request/invalid/export-taskId.json +22 -0
  85. package/fixtures/json-schema/personal-environment-request/invalid/extra-command.json +24 -0
  86. package/fixtures/json-schema/personal-environment-request/invalid/host-workspace.json +22 -0
  87. package/fixtures/json-schema/personal-environment-request/invalid/image-injection.json +22 -0
  88. package/fixtures/json-schema/personal-environment-request/invalid/network-egress.json +21 -0
  89. package/fixtures/json-schema/personal-environment-request/invalid/no-command.json +21 -0
  90. package/fixtures/json-schema/personal-environment-request/invalid/recreate-on-release.json +22 -0
  91. package/fixtures/json-schema/personal-environment-request/invalid/relative-command.json +24 -0
  92. package/fixtures/json-schema/personal-environment-request/invalid/released-identity.json +22 -0
  93. package/fixtures/json-schema/personal-environment-request/invalid/task-empty.json +27 -0
  94. package/fixtures/json-schema/personal-environment-request/invalid/task-host-path.json +27 -0
  95. package/fixtures/json-schema/personal-environment-request/invalid/task-noncanonical.json +27 -0
  96. package/fixtures/json-schema/personal-environment-request/invalid/task-null.json +27 -0
  97. package/fixtures/json-schema/personal-environment-request/invalid/task-on-inspection.json +22 -0
  98. package/fixtures/json-schema/personal-environment-request/invalid/unbounded-quota.json +21 -0
  99. package/fixtures/json-schema/personal-environment-request/valid/acquire.json +21 -0
  100. package/fixtures/json-schema/personal-environment-request/valid/delete-data.json +21 -0
  101. package/fixtures/json-schema/personal-environment-request/valid/execute.json +26 -0
  102. package/fixtures/json-schema/personal-environment-request/valid/export-data.json +21 -0
  103. package/fixtures/json-schema/personal-environment-request/valid/inspect.json +21 -0
  104. package/fixtures/json-schema/personal-environment-request/valid/provision.json +22 -0
  105. package/fixtures/json-schema/personal-environment-request/valid/release.json +21 -0
  106. package/fixtures/json-schema/personal-environment-request/valid/task-workspace.json +27 -0
  107. package/fixtures/json-schema/personal-environment-result/invalid/active-without-observation.json +7 -0
  108. package/fixtures/json-schema/personal-environment-result/invalid/deleted-output.json +8 -0
  109. package/fixtures/json-schema/personal-environment-result/invalid/deleted-snapshot.json +8 -0
  110. package/fixtures/json-schema/personal-environment-result/invalid/deleted-taskId.json +8 -0
  111. package/fixtures/json-schema/personal-environment-result/invalid/private-handle.json +8 -0
  112. package/fixtures/json-schema/personal-environment-result/invalid/stopped-fake-observation.json +20 -0
  113. package/fixtures/json-schema/personal-environment-result/invalid/task-empty.json +22 -0
  114. package/fixtures/json-schema/personal-environment-result/invalid/task-host-path.json +22 -0
  115. package/fixtures/json-schema/personal-environment-result/invalid/task-null.json +22 -0
  116. package/fixtures/json-schema/personal-environment-result/invalid/task-on-stopped.json +8 -0
  117. package/fixtures/json-schema/personal-environment-result/invalid/task-without-command-output.json +21 -0
  118. package/fixtures/json-schema/personal-environment-result/valid/active.json +21 -0
  119. package/fixtures/json-schema/personal-environment-result/valid/deleted.json +7 -0
  120. package/fixtures/json-schema/personal-environment-result/valid/expired.json +7 -0
  121. package/fixtures/json-schema/personal-environment-result/valid/missing.json +7 -0
  122. package/fixtures/json-schema/personal-environment-result/valid/released.json +7 -0
  123. package/fixtures/json-schema/personal-environment-result/valid/task-workspace.json +22 -0
  124. package/fixtures/json-schema/personal-guest-binding/invalid/host-path.json +17 -0
  125. package/fixtures/json-schema/personal-guest-binding/invalid/invalid-policy.json +17 -0
  126. package/fixtures/json-schema/personal-guest-binding/invalid/invalid-serial.json +17 -0
  127. package/fixtures/json-schema/personal-guest-binding/invalid/legacy-version.json +17 -0
  128. package/fixtures/json-schema/personal-guest-binding/invalid/missing-generation.json +17 -0
  129. package/fixtures/json-schema/personal-guest-binding/invalid/missing-identity.json +16 -0
  130. package/fixtures/json-schema/personal-guest-binding/invalid/missing-provider-identity.json +16 -0
  131. package/fixtures/json-schema/personal-guest-binding/invalid/raw-provider-identity.json +17 -0
  132. package/fixtures/json-schema/personal-guest-binding/valid/minimal.json +17 -0
  133. package/fixtures/json-schema/personal-guest-data-plan/invalid/quota.json +1 -0
  134. package/fixtures/json-schema/personal-guest-data-plan/valid/basic.json +1 -0
  135. package/fixtures/json-schema/personal-guest-enrollment/invalid/capabilities.json +7 -0
  136. package/fixtures/json-schema/personal-guest-enrollment/invalid/legacy-version.json +4 -0
  137. package/fixtures/json-schema/personal-guest-enrollment/invalid/missing-identity.json +3 -0
  138. package/fixtures/json-schema/personal-guest-enrollment/invalid/ownerId.json +5 -0
  139. package/fixtures/json-schema/personal-guest-enrollment/invalid/token.json +5 -0
  140. package/fixtures/json-schema/personal-guest-enrollment/valid/minimal.json +4 -0
  141. package/fixtures/json-schema/personal-guest-heartbeat/invalid/argv.json +11 -0
  142. package/fixtures/json-schema/personal-guest-heartbeat/invalid/legacy-version.json +8 -0
  143. package/fixtures/json-schema/personal-guest-heartbeat/invalid/missing-identity.json +7 -0
  144. package/fixtures/json-schema/personal-guest-heartbeat/invalid/owner.json +9 -0
  145. package/fixtures/json-schema/personal-guest-heartbeat/invalid/self-ready.json +9 -0
  146. package/fixtures/json-schema/personal-guest-heartbeat/invalid/zero-sequence.json +8 -0
  147. package/fixtures/json-schema/personal-guest-heartbeat/valid/minimal.json +8 -0
  148. package/fixtures/json-schema/personal-guest-provisioning/invalid/network.json +1 -0
  149. package/fixtures/json-schema/personal-guest-provisioning/valid/basic.json +1 -0
  150. package/fixtures/json-schema/personal-guest-renewal/invalid/unknown.json +1 -0
  151. package/fixtures/json-schema/personal-guest-renewal/valid/basic.json +1 -0
  152. package/fixtures/json-schema/personal-guest-runtime/invalid/minimal.json +45 -0
  153. package/fixtures/json-schema/personal-guest-runtime/valid/minimal.json +45 -0
  154. package/fixtures/json-schema/personal-guest-runtime-result/invalid/minimal.json +28 -0
  155. package/fixtures/json-schema/personal-guest-runtime-result/valid/minimal.json +27 -0
  156. package/fixtures/json-schema/personal-guest-tools/invalid/minimal.json +31 -0
  157. package/fixtures/json-schema/personal-guest-tools/valid/minimal.json +30 -0
  158. package/fixtures/json-schema/source-import-policy/invalid/case-variant-scope.json +4 -0
  159. package/fixtures/json-schema/source-import-policy/invalid/case-variant-version.json +4 -0
  160. package/fixtures/json-schema/source-import-policy/invalid/duplicate-grant.json +4 -0
  161. package/fixtures/json-schema/source-import-policy/invalid/empty-id.json +4 -0
  162. package/fixtures/json-schema/source-import-policy/invalid/future-version.json +4 -0
  163. package/fixtures/json-schema/source-import-policy/invalid/unknown-field.json +4 -0
  164. package/fixtures/json-schema/source-import-policy/valid/deny-all.json +4 -0
  165. package/fixtures/json-schema/source-import-policy/valid/shared.json +4 -0
  166. package/fixtures/json-schema/token-claims/invalid/authz-negative.json +23 -0
  167. package/fixtures/json-schema/token-claims/invalid/authz-string.json +23 -0
  168. package/fixtures/json-schema/token-claims/invalid/authz-zero.json +23 -0
  169. package/fixtures/json-schema/token-claims/valid/versioned-session.json +23 -0
  170. package/fixtures/openapi/AgentRunEventListEnvelope/invalid/basic.json +8 -0
  171. package/fixtures/openapi/AgentRunEventListEnvelope/valid/basic.json +8 -0
  172. package/fixtures/openapi/AgentRunEventListEnvelope/valid/native.json +1 -0
  173. package/fixtures/openapi/AgentRunResumeRequest/invalid/basic.json +3 -0
  174. package/fixtures/openapi/AgentRunResumeRequest/valid/basic.json +3 -0
  175. package/fixtures/openapi/AgentSSHCommandRequest/invalid/task51.json +24 -0
  176. package/fixtures/openapi/AgentSSHCommandRequest/valid/task51.json +23 -0
  177. package/fixtures/openapi/AgentSSHCommandResult/invalid/task51.json +8 -0
  178. package/fixtures/openapi/AgentSSHCommandResult/valid/task51.json +7 -0
  179. package/fixtures/openapi/AgentSSHSessionRequest/invalid/task51.json +20 -0
  180. package/fixtures/openapi/AgentSSHSessionRequest/valid/task51.json +19 -0
  181. package/fixtures/openapi/CapabilityTaskInput/invalid/duplicate-scope.json +36 -0
  182. package/fixtures/openapi/CapabilityTaskInput/valid/scoped.json +32 -0
  183. package/fixtures/openapi/DockerQuickCreateHostInput/invalid/guest-access.json +7 -0
  184. package/fixtures/openapi/DockerQuickCreateHostInput/valid/guest-access.json +9 -0
  185. package/fixtures/openapi/KnowledgeSourceDocument/invalid/tombstone-with-content.json +1 -0
  186. package/fixtures/openapi/KnowledgeSourceDocument/valid/tombstone.json +1 -0
  187. package/fixtures/openapi/KubernetesResourceUpdatePlanRequest/invalid/missing-required.json +1 -0
  188. package/fixtures/openapi/KubernetesResourceYaml/invalid/missing-required.json +1 -0
  189. package/fixtures/openapi/ObservabilityDataSource/invalid/missing-required.json +1 -0
  190. package/fixtures/openapi/PersonalAgentAccess/invalid/invalid.json +1 -0
  191. package/fixtures/openapi/PersonalAgentAccess/valid/valid.json +1 -0
  192. package/fixtures/openapi/PersonalAgentAccessDecision/invalid/invalid.json +1 -0
  193. package/fixtures/openapi/PersonalAgentAccessDecision/valid/valid.json +1 -0
  194. package/fixtures/openapi/PersonalAssistantArtifactListEnvelope/invalid/owner.json +8 -0
  195. package/fixtures/openapi/PersonalAssistantArtifactListEnvelope/valid/side-chat.json +9 -0
  196. package/fixtures/openapi/PersonalAssistantAvatarRequest/invalid/motion.json +1 -0
  197. package/fixtures/openapi/PersonalAssistantAvatarRequest/invalid/owner.json +1 -0
  198. package/fixtures/openapi/PersonalAssistantAvatarRequest/invalid/remote.json +1 -0
  199. package/fixtures/openapi/PersonalAssistantAvatarRequest/invalid/svg.json +1 -0
  200. package/fixtures/openapi/PersonalAssistantAvatarRequest/valid/raster.json +1 -0
  201. package/fixtures/openapi/PersonalAssistantAvatarRequest/valid/reset.json +1 -0
  202. package/fixtures/openapi/PersonalAssistantUpdateRequest/invalid/empty.json +1 -0
  203. package/fixtures/openapi/PersonalAssistantUpdateRequest/invalid/owner.json +1 -0
  204. package/fixtures/openapi/PersonalAssistantUpdateRequest/valid/name.json +1 -0
  205. package/fixtures/openapi/PersonalDockerEnvironmentPolicy/invalid/host-url.json +12 -0
  206. package/fixtures/openapi/PersonalDockerEnvironmentPolicy/invalid/lifetime.json +11 -0
  207. package/fixtures/openapi/PersonalDockerEnvironmentPolicy/invalid/unknown-agent.json +11 -0
  208. package/fixtures/openapi/PersonalDockerEnvironmentPolicy/valid/basic.json +11 -0
  209. package/fixtures/openapi/PersonalEnvironment/invalid/invented-state.json +6 -0
  210. package/fixtures/openapi/PersonalEnvironment/invalid/missing-state.json +5 -0
  211. package/fixtures/openapi/PersonalEnvironment/invalid/negative-generation.json +7 -0
  212. package/fixtures/openapi/PersonalEnvironment/invalid/network.json +23 -0
  213. package/fixtures/openapi/PersonalEnvironment/invalid/private-node.json +24 -0
  214. package/fixtures/openapi/PersonalEnvironment/invalid/unsupported-backend.json +7 -0
  215. package/fixtures/openapi/PersonalEnvironment/valid/deleted.json +23 -0
  216. package/fixtures/openapi/PersonalEnvironment/valid/deleting.json +23 -0
  217. package/fixtures/openapi/PersonalEnvironment/valid/deletion_unknown.json +23 -0
  218. package/fixtures/openapi/PersonalEnvironment/valid/frozen-binding.json +13 -0
  219. package/fixtures/openapi/PersonalEnvironment/valid/ready.json +23 -0
  220. package/fixtures/openapi/PersonalEnvironment/valid/unknown.json +23 -0
  221. package/fixtures/openapi/PersonalEnvironment/valid/unprovisioned.json +6 -0
  222. package/fixtures/openapi/PersonalEnvironmentActionRequest/invalid/command.json +3 -0
  223. package/fixtures/openapi/PersonalEnvironmentActionRequest/invalid/node.json +4 -0
  224. package/fixtures/openapi/PersonalEnvironmentActionRequest/invalid/owner.json +4 -0
  225. package/fixtures/openapi/PersonalEnvironmentActionRequest/invalid/quota.json +10 -0
  226. package/fixtures/openapi/PersonalEnvironmentActionRequest/valid/reconcile.json +3 -0
  227. package/fixtures/openapi/PersonalEnvironmentActionRequest/valid/sleep.json +3 -0
  228. package/fixtures/openapi/PersonalEnvironmentActionRequest/valid/wake.json +4 -0
  229. package/fixtures/openapi/PersonalEnvironmentDataDeletionRequest/invalid/empty-name.json +5 -0
  230. package/fixtures/openapi/PersonalEnvironmentDataDeletionRequest/invalid/long-name.json +5 -0
  231. package/fixtures/openapi/PersonalEnvironmentDataDeletionRequest/invalid/missing-name.json +4 -0
  232. package/fixtures/openapi/PersonalEnvironmentDataDeletionRequest/invalid/owner.json +6 -0
  233. package/fixtures/openapi/PersonalEnvironmentDataDeletionRequest/invalid/stale-version.json +5 -0
  234. package/fixtures/openapi/PersonalEnvironmentDataDeletionRequest/valid/confirmed.json +5 -0
  235. package/fixtures/openapi/PersonalEnvironmentDataExportRequest/invalid/assistantId.json +4 -0
  236. package/fixtures/openapi/PersonalEnvironmentDataExportRequest/invalid/environmentVersion.json +4 -0
  237. package/fixtures/openapi/PersonalEnvironmentDataExportRequest/invalid/ownerId.json +5 -0
  238. package/fixtures/openapi/PersonalEnvironmentDataExportRequest/valid/current.json +4 -0
  239. package/fixtures/openapi/PersonalEnvironmentEnvelope/invalid/missing-data.json +1 -0
  240. package/fixtures/openapi/PersonalEnvironmentEnvelope/valid/state.json +8 -0
  241. package/fixtures/openapi/PersonalEnvironmentQuota/invalid/cpu.json +7 -0
  242. package/fixtures/openapi/PersonalEnvironmentQuota/invalid/disk.json +7 -0
  243. package/fixtures/openapi/PersonalEnvironmentQuota/invalid/network.json +7 -0
  244. package/fixtures/openapi/PersonalEnvironmentQuota/invalid/processes.json +7 -0
  245. package/fixtures/openapi/PersonalEnvironmentQuota/valid/basic.json +7 -0
  246. package/fixtures/openapi/PersonalEnvironmentTemplate/invalid/mode.json +13 -0
  247. package/fixtures/openapi/PersonalEnvironmentTemplate/invalid/network.json +13 -0
  248. package/fixtures/openapi/PersonalEnvironmentTemplate/invalid/node.json +14 -0
  249. package/fixtures/openapi/PersonalEnvironmentTemplate/valid/template.json +13 -0
  250. package/fixtures/openapi/PersonalEnvironmentTemplateListEnvelope/invalid/missing.json +1 -0
  251. package/fixtures/openapi/PersonalEnvironmentTemplateListEnvelope/invalid/unbounded.json +1317 -0
  252. package/fixtures/openapi/PersonalEnvironmentTemplateListEnvelope/valid/choices.json +17 -0
  253. package/fixtures/openapi/PersonalEnvironmentTemplateListEnvelope/valid/empty.json +3 -0
  254. package/fixtures/openapi/PersonalEnvironmentUsage/invalid/negative.json +6 -0
  255. package/fixtures/openapi/PersonalEnvironmentUsage/valid/usage.json +6 -0
  256. package/fixtures/openapi/PersonalFeedFeedback/invalid/owner.json +4 -0
  257. package/fixtures/openapi/PersonalFeedFeedback/valid/liked.json +3 -0
  258. package/fixtures/openapi/PersonalFeedItem/invalid/no-source.json +9 -0
  259. package/fixtures/openapi/PersonalFeedItem/valid/post.json +15 -0
  260. package/fixtures/openapi/PersonalFeedSettings/invalid/owner.json +6 -0
  261. package/fixtures/openapi/PersonalFeedSettings/valid/default.json +5 -0
  262. package/fixtures/openapi/PersonalGuestPreparationInput/invalid/foreign-owner.json +5 -0
  263. package/fixtures/openapi/PersonalGuestPreparationInput/invalid/unsafe-counter.json +4 -0
  264. package/fixtures/openapi/PersonalGuestPreparationInput/valid/ed25519-csr.json +4 -0
  265. package/fixtures/openapi/PersonalGuestRuntimeEnvelope/invalid/minimal.json +10 -0
  266. package/fixtures/openapi/PersonalGuestRuntimeEnvelope/valid/minimal.json +9 -0
  267. package/fixtures/openapi/PersonalGuestRuntimeInput/invalid/minimal.json +6 -0
  268. package/fixtures/openapi/PersonalGuestRuntimeInput/valid/minimal.json +8 -0
  269. package/fixtures/openapi/PersonalGuestToolsInput/invalid/minimal.json +5 -0
  270. package/fixtures/openapi/PersonalGuestToolsInput/valid/minimal.json +4 -0
  271. package/fixtures/openapi/PersonalVMEnvironmentPolicy/invalid/device.json +15 -0
  272. package/fixtures/openapi/PersonalVMEnvironmentPolicy/invalid/fractional-size.json +15 -0
  273. package/fixtures/openapi/PersonalVMEnvironmentPolicy/invalid/network.json +15 -0
  274. package/fixtures/openapi/PersonalVMEnvironmentPolicy/invalid/owner.json +15 -0
  275. package/fixtures/openapi/PersonalVMEnvironmentPolicy/invalid/provider.json +15 -0
  276. package/fixtures/openapi/PersonalVMEnvironmentPolicy/invalid/unknown.json +16 -0
  277. package/fixtures/openapi/PersonalVMEnvironmentPolicy/valid/pve.json +15 -0
  278. package/fixtures/openapi/PersonalWorkspaceRuntime/invalid/shared.json +3 -0
  279. package/fixtures/openapi/PersonalWorkspaceRuntime/valid/shared.json +4 -0
  280. package/fixtures/openapi/PersonalWorkspaceRuntimeEnvelope/invalid/shared.json +5 -0
  281. package/fixtures/openapi/PersonalWorkspaceRuntimeEnvelope/valid/shared.json +6 -0
  282. package/fixtures/openapi/SecretGenerateSSHKeyRequest/invalid/task51.json +6 -0
  283. package/fixtures/openapi/SecretGenerateSSHKeyRequest/valid/task51.json +12 -0
  284. package/fixtures/openapi/SecretReferenceUsage/invalid/version.json +6 -0
  285. package/fixtures/openapi/SecretReferenceUsage/valid/version.json +8 -0
  286. package/fixtures/openapi/VMSSHCredentialRedemptionInput/invalid/task51.json +7 -0
  287. package/fixtures/openapi/VMSSHCredentialRedemptionInput/valid/task51.json +6 -0
  288. package/fixtures/openapi/VirtualMachineAccessBindingInput/invalid/task51.json +10 -0
  289. package/fixtures/openapi/VirtualMachineAccessBindingInput/valid/task51.json +10 -0
  290. package/fixtures/openapi/VirtualMachineActionInput/invalid/capacity-not-boolean.json +1 -0
  291. package/fixtures/openapi/VirtualMachineActionInput/valid/new-data-with-admission.json +1 -0
  292. package/fixtures/openapi/VirtualMachineCreateInput/valid/cloud-image.json +12 -0
  293. package/fixtures/openapi/VirtualMachineDiskChange/invalid/serial-config-injection.json +1 -0
  294. package/fixtures/openapi/VirtualMachineDiskChange/invalid/serial-existing-disk.json +1 -0
  295. package/fixtures/openapi/VirtualMachineDiskChange/invalid/serial-too-long.json +1 -0
  296. package/fixtures/openapi/VirtualMachineDiskChange/valid/personal-data.json +1 -0
  297. package/fixtures/openapi/VirtualMachineGuestAccessInput/invalid/task51.json +4 -0
  298. package/fixtures/openapi/VirtualMachineGuestAccessInput/valid/task51.json +6 -0
  299. package/fixtures/openapi/VirtualMachineSSHCommand/invalid/task51.json +9 -0
  300. package/fixtures/openapi/VirtualMachineSSHCommand/valid/task51.json +8 -0
  301. package/fixtures/openapi/VirtualMachineSSHCommandInput/invalid/task51.json +5 -0
  302. package/fixtures/openapi/VirtualMachineSSHCommandInput/valid/task51.json +5 -0
  303. package/fixtures/openapi/VirtualMachineSSHFrame/invalid/encoding.json +5 -0
  304. package/fixtures/openapi/VirtualMachineSSHFrame/invalid/task51.json +4 -0
  305. package/fixtures/openapi/VirtualMachineSSHFrame/valid/binary.json +5 -0
  306. package/fixtures/openapi/VirtualMachineSSHFrame/valid/task51.json +5 -0
  307. package/fixtures/openapi/VirtualMachineSSHSessionInput/invalid/task51.json +4 -0
  308. package/fixtures/openapi/VirtualMachineSSHSessionInput/valid/task51.json +5 -0
  309. package/fixtures/openapi/VirtualizationCapacityPolicyInput/invalid/task51.json +12 -0
  310. package/fixtures/openapi/VirtualizationCapacityPolicyInput/valid/task51.json +13 -0
  311. package/fixtures/openapi/VirtualizationImageInput/invalid/cloud-image-config-type.json +6 -0
  312. package/fixtures/openapi/VirtualizationImageInput/valid/cloud-image.json +11 -0
  313. package/fixtures/openapi/WorkbenchAssistantTask/invalid/kind.json +1 -0
  314. package/fixtures/openapi/WorkbenchAssistantTask/invalid/owner.json +1 -0
  315. package/fixtures/openapi/WorkbenchAssistantTask/valid/generate.json +1 -0
  316. package/fixtures/openapi/WorkbenchAssistantTask/valid/undo.json +1 -0
  317. package/fixtures/openapi/WorkbenchContextSelection/invalid/remote-image.json +1 -0
  318. package/fixtures/openapi/WorkbenchContextSelection/invalid/too-many-images.json +1 -0
  319. package/fixtures/openapi/WorkbenchContextSelection/valid/chat-image.json +9 -0
  320. package/fixtures/openapi/WorkbenchContextUsage/invalid/fractional-capacity.json +1 -0
  321. package/fixtures/openapi/WorkbenchContextUsage/invalid/negative.json +1 -0
  322. package/fixtures/openapi/WorkbenchContextUsage/valid/reported.json +1 -0
  323. package/fixtures/openapi/WorkbenchContextUsage/valid/unknown.json +1 -0
  324. package/fixtures/openapi/WorkbenchMainSessionRequest/invalid/owner.json +1 -0
  325. package/fixtures/openapi/WorkbenchMainSessionRequest/invalid/provider.json +1 -0
  326. package/fixtures/openapi/WorkbenchMainSessionRequest/valid/empty.json +1 -0
  327. package/fixtures/openapi/WorkbenchMainSessionRequest/valid/provider.json +1 -0
  328. package/fixtures/openapi/WorkbenchMessageSearchPage/invalid/role.json +12 -0
  329. package/fixtures/openapi/WorkbenchMessageSearchPage/valid/page.json +12 -0
  330. package/fixtures/openapi/WorkbenchMessageWindow/invalid/missing.json +1 -0
  331. package/fixtures/openapi/WorkbenchMessageWindow/valid/empty.json +3 -0
  332. package/fixtures/openapi/WorkbenchModelOption/invalid/zero-capacity.json +1 -0
  333. package/fixtures/openapi/WorkbenchModelOption/valid/capacity.json +1 -0
  334. package/fixtures/openapi/WorkbenchModelOption/valid/unknown.json +1 -0
  335. package/fixtures/openapi/WorkbenchProfessionalCapabilityInput/invalid/basic.json +7 -0
  336. package/fixtures/openapi/WorkbenchProfessionalCapabilityInput/valid/basic.json +11 -0
  337. package/fixtures/openapi/WorkbenchProfessionalCapabilityInput/valid/selected-profile.json +23 -0
  338. package/fixtures/openapi/WorkbenchSendMessageStreamRequest/invalid/missing-required.json +1 -0
  339. package/fixtures/openapi/WorkbenchSendMessageStreamRequest/valid/mixed-professional-task.json +22 -0
  340. package/fixtures/openapi/WorkbenchTaskIntent/invalid/basic.json +5 -0
  341. package/fixtures/openapi/WorkbenchTaskIntent/invalid/oversized-capability-source.json +3 -0
  342. package/fixtures/openapi/WorkbenchTaskIntent/valid/basic.json +9 -0
  343. package/fixtures/openapi/WorkbenchTaskIntent/valid/independent-sources.json +17 -0
  344. package/gen/go/sohaapi/client.go +1 -1
  345. package/gen/go/sohaapi/personal_environment_test.go +25 -0
  346. package/gen/go/sohaapi/types.go +2679 -240
  347. package/gen/go/sohaapi/types_test.go +30 -30
  348. package/gen/go/sohaapi/vm_disk_test.go +21 -0
  349. package/gen/ts/sohaapi/index.ts +4150 -169
  350. package/go.mod +61 -49
  351. package/go.sum +202 -176
  352. package/knowledge/connector-definition.schema.json +6 -1
  353. package/knowledge/source-import-policy.schema.json +32 -0
  354. package/memory/memory-record.schema.json +1 -1
  355. package/openapi/soha-api.json +22230 -15693
  356. package/openapi/soha-api.yaml +2495 -12
  357. package/openapi/spec_generated.go +3 -3
  358. package/package.json +19 -3
  359. /package/fixtures/openapi/DeliveryExecutionHistoryPage/{invalid-kind.json → invalid/invalid-kind.json} +0 -0
  360. /package/fixtures/openapi/DeliveryExecutionHistoryPage/{valid-build.json → valid/valid-build.json} +0 -0
  361. /package/fixtures/openapi/DeliveryExecutionHistoryPage/{valid-empty.json → valid/valid-empty.json} +0 -0
@@ -0,0 +1,235 @@
1
+ // Package guest carries the personal VM guest registration and heartbeat protocol.
2
+ // Schemas in agent/ own the wire shapes. This protocol grants no task execution.
3
+ package guest
4
+
5
+ import (
6
+ "bytes"
7
+ "crypto/sha256"
8
+ "encoding/base64"
9
+ "encoding/json"
10
+ "errors"
11
+ "fmt"
12
+ "io"
13
+ "regexp"
14
+ "slices"
15
+ "strings"
16
+ "time"
17
+ "unicode/utf8"
18
+ )
19
+
20
+ const (
21
+ BindingVersion = "opensoha.dev/personal-guest-binding/v1"
22
+ EnrollmentVersion = "opensoha.dev/personal-guest-enrollment/v1"
23
+ HeartbeatVersion = "opensoha.dev/personal-guest-heartbeat/v1"
24
+ MaxClockSkew = 30 * time.Second
25
+ MaxCounter = 1<<53 - 1 // Exact integer range shared by Go and JavaScript consumers.
26
+ EnrollmentMinTTL = time.Minute
27
+ EnrollmentMaxTTL = 10 * time.Minute
28
+ BindingsPath = "/api/personal-guest/v1/bindings"
29
+ )
30
+
31
+ var (
32
+ uuidPattern = regexp.MustCompile(`^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$`)
33
+ digestPattern = regexp.MustCompile(`^sha256:[a-f0-9]{64}$`)
34
+ serialPattern = regexp.MustCompile(`^[A-Za-z0-9_-]{1,20}$`)
35
+ )
36
+
37
+ // Binding is derived by Core and immutable for this guest registration. Version
38
+ // is the identity revision, independent of environment operation CAS versions.
39
+ type Binding struct {
40
+ SchemaVersion string `json:"schemaVersion"`
41
+ ID string `json:"id"`
42
+ Version int64 `json:"version"`
43
+ AssistantID string `json:"assistantId"`
44
+ OwnerID string `json:"ownerId"`
45
+ Provider string `json:"provider"`
46
+ ComputeRef string `json:"computeRef"`
47
+ ComputeGeneration int64 `json:"computeGeneration"`
48
+ ProviderIdentityHash string `json:"providerIdentityHash"`
49
+ DataRef string `json:"dataRef"`
50
+ DataDiskSerial string `json:"dataDiskSerial"`
51
+ DataDiskBytes int64 `json:"dataDiskBytes"`
52
+ PolicyRevision string `json:"policyRevision"`
53
+ IssuedAt time.Time `json:"issuedAt"`
54
+ ExpiresAt time.Time `json:"expiresAt"`
55
+ }
56
+
57
+ // Enrollment is sent over mutually authenticated TLS using a short, one-time
58
+ // challenge in the Authorization header. The certificate pins guest identity.
59
+ // No owner, capability, filesystem path or provider credentials are client input.
60
+ type Enrollment struct {
61
+ SchemaVersion string `json:"schemaVersion"`
62
+ EnrollmentID string `json:"enrollmentId"`
63
+ }
64
+
65
+ // EnrollmentTokenHash accepts exactly Bearer plus a canonical unpadded base64url
66
+ // 32-byte challenge. Core generates it with crypto/rand and stores only the hash.
67
+ // Its 60–600 second grant pins the binding and guest certificate fingerprint;
68
+ // atomic consumption, expiry and current authorization remain service duties.
69
+ func EnrollmentTokenHash(authorization string) (string, error) {
70
+ token, ok := strings.CutPrefix(authorization, "Bearer ")
71
+ if !ok || len(token) != 43 {
72
+ return "", errors.New("invalid personal guest enrollment authorization")
73
+ }
74
+ random, err := base64.RawURLEncoding.Strict().DecodeString(token)
75
+ if err != nil || len(random) != 32 {
76
+ return "", errors.New("invalid personal guest enrollment authorization")
77
+ }
78
+ digest := sha256.Sum256([]byte(token))
79
+ return fmt.Sprintf("sha256:%x", digest), nil
80
+ }
81
+
82
+ // Heartbeat proves only current authenticated liveness. Core independently
83
+ // checks current authorization, frozen binding and monotonic sequence in storage.
84
+ // A guest cannot claim data/tool/browser readiness with this message.
85
+ type Heartbeat struct {
86
+ SchemaVersion string `json:"schemaVersion"`
87
+ BindingID string `json:"bindingId"`
88
+ BindingVersion int64 `json:"bindingVersion"`
89
+ ComputeGeneration int64 `json:"computeGeneration"`
90
+ Sequence int64 `json:"sequence"`
91
+ ObservedAt time.Time `json:"observedAt"`
92
+ }
93
+
94
+ func (b Binding) Validate() error {
95
+ if b.SchemaVersion != BindingVersion || b.Version < 1 || b.Version > MaxCounter || b.ComputeGeneration < 1 || b.ComputeGeneration > MaxCounter || b.Provider != "pve" && b.Provider != "kubevirt" {
96
+ return errors.New("invalid personal guest binding version or provider")
97
+ }
98
+ for _, id := range []string{b.ID, b.AssistantID, b.OwnerID, b.ComputeRef, b.DataRef} {
99
+ if !uuidPattern.MatchString(id) {
100
+ return errors.New("invalid personal guest binding identity")
101
+ }
102
+ }
103
+ if !serialPattern.MatchString(b.DataDiskSerial) || b.DataDiskBytes < 16<<20 || b.DataDiskBytes > 1<<40 || !digestPattern.MatchString(b.PolicyRevision) || !digestPattern.MatchString(b.ProviderIdentityHash) {
104
+ return errors.New("invalid personal guest data identity or policy revision")
105
+ }
106
+ if b.IssuedAt.IsZero() || !b.ExpiresAt.After(b.IssuedAt) || b.ExpiresAt.Sub(b.IssuedAt) > 24*time.Hour {
107
+ return errors.New("invalid personal guest binding lifetime")
108
+ }
109
+ return nil
110
+ }
111
+
112
+ func (e Enrollment) Validate() error {
113
+ if e.SchemaVersion != EnrollmentVersion || !uuidPattern.MatchString(e.EnrollmentID) {
114
+ return errors.New("invalid personal guest enrollment")
115
+ }
116
+ return nil
117
+ }
118
+
119
+ func (h Heartbeat) Validate() error {
120
+ if h.SchemaVersion != HeartbeatVersion || !uuidPattern.MatchString(h.BindingID) || h.BindingVersion < 1 || h.BindingVersion > MaxCounter || h.ComputeGeneration < 1 || h.ComputeGeneration > MaxCounter || h.Sequence < 1 || h.Sequence > MaxCounter || h.ObservedAt.IsZero() {
121
+ return errors.New("invalid personal guest heartbeat")
122
+ }
123
+ return nil
124
+ }
125
+
126
+ // ValidateFor is not authentication or a readiness grant. Callers must first
127
+ // authenticate the pinned guest certificate and recheck authoritative ownership.
128
+ func (h Heartbeat) ValidateFor(binding Binding, now time.Time, lastSequence int64) error {
129
+ if err := binding.Validate(); err != nil {
130
+ return err
131
+ }
132
+ if err := h.Validate(); err != nil {
133
+ return err
134
+ }
135
+ if now.IsZero() || now.Before(binding.IssuedAt) || !now.Before(binding.ExpiresAt) || lastSequence < 0 {
136
+ return errors.New("personal guest binding is not currently valid")
137
+ }
138
+ if h.BindingID != binding.ID || h.BindingVersion != binding.Version || h.ComputeGeneration != binding.ComputeGeneration || h.Sequence <= lastSequence {
139
+ return errors.New("personal guest heartbeat binding or sequence mismatch")
140
+ }
141
+ if h.ObservedAt.Before(binding.IssuedAt) || !h.ObservedAt.Before(binding.ExpiresAt) || h.ObservedAt.Before(now.Add(-MaxClockSkew)) || h.ObservedAt.After(now.Add(MaxClockSkew)) {
142
+ return errors.New("personal guest heartbeat observation is stale")
143
+ }
144
+ return nil
145
+ }
146
+
147
+ func decodeStrict(data []byte, value any, keys ...string) error {
148
+ return decodeStrictBounded(data, value, 4096, keys...)
149
+ }
150
+
151
+ func decodeStrictBounded(data []byte, value any, limit int, keys ...string) error {
152
+ if len(data) > limit || !utf8.Valid(data) {
153
+ return errors.New("personal guest message must be bounded UTF-8")
154
+ }
155
+ if err := validateObjectKeys(data, keys); err != nil {
156
+ return err
157
+ }
158
+ decoder := json.NewDecoder(bytes.NewReader(data))
159
+ decoder.DisallowUnknownFields()
160
+ if err := decoder.Decode(value); err != nil {
161
+ return err
162
+ }
163
+ if decoder.Decode(new(any)) != io.EOF {
164
+ return errors.New("personal guest message must contain one JSON object")
165
+ }
166
+ return nil
167
+ }
168
+
169
+ func validateObjectKeys(data []byte, keys []string) error {
170
+ decoder := json.NewDecoder(bytes.NewReader(data))
171
+ start, err := decoder.Token()
172
+ if err != nil || start != json.Delim('{') {
173
+ return errors.New("personal guest message must be a JSON object")
174
+ }
175
+ seen := make(map[string]bool, len(keys))
176
+ for decoder.More() {
177
+ key, err := decoder.Token()
178
+ if err != nil {
179
+ return err
180
+ }
181
+ name, ok := key.(string)
182
+ if !ok || seen[name] || !slices.Contains(keys, name) {
183
+ return errors.New("personal guest message contains a duplicate or unknown field")
184
+ }
185
+ seen[name] = true
186
+ var value json.RawMessage
187
+ if err := decoder.Decode(&value); err != nil {
188
+ return err
189
+ }
190
+ }
191
+ _, err = decoder.Token()
192
+ return err
193
+ }
194
+
195
+ func (b *Binding) UnmarshalJSON(data []byte) error {
196
+ type wire Binding
197
+ var value wire
198
+ if err := decodeStrict(data, &value, "schemaVersion", "id", "version", "assistantId", "ownerId", "provider", "computeRef", "computeGeneration", "providerIdentityHash", "dataRef", "dataDiskSerial", "dataDiskBytes", "policyRevision", "issuedAt", "expiresAt"); err != nil {
199
+ return err
200
+ }
201
+ decoded := Binding(value)
202
+ if err := decoded.Validate(); err != nil {
203
+ return err
204
+ }
205
+ *b = decoded
206
+ return nil
207
+ }
208
+
209
+ func (e *Enrollment) UnmarshalJSON(data []byte) error {
210
+ type wire Enrollment
211
+ var value wire
212
+ if err := decodeStrict(data, &value, "schemaVersion", "enrollmentId"); err != nil {
213
+ return err
214
+ }
215
+ decoded := Enrollment(value)
216
+ if err := decoded.Validate(); err != nil {
217
+ return err
218
+ }
219
+ *e = decoded
220
+ return nil
221
+ }
222
+
223
+ func (h *Heartbeat) UnmarshalJSON(data []byte) error {
224
+ type wire Heartbeat
225
+ var value wire
226
+ if err := decodeStrict(data, &value, "schemaVersion", "bindingId", "bindingVersion", "computeGeneration", "sequence", "observedAt"); err != nil {
227
+ return err
228
+ }
229
+ decoded := Heartbeat(value)
230
+ if err := decoded.Validate(); err != nil {
231
+ return err
232
+ }
233
+ *h = decoded
234
+ return nil
235
+ }
@@ -0,0 +1,142 @@
1
+ package guest
2
+
3
+ import (
4
+ "encoding/json"
5
+ "os"
6
+ "path/filepath"
7
+ "strings"
8
+ "testing"
9
+ "time"
10
+ )
11
+
12
+ func TestEnrollmentAuthorizationUsesCanonicalBoundedChallenge(t *testing.T) {
13
+ valid := "Bearer " + strings.Repeat("A", 43)
14
+ digest, err := EnrollmentTokenHash(valid)
15
+ if err != nil || !digestPattern.MatchString(digest) {
16
+ t.Fatalf("challenge hash: %q %v", digest, err)
17
+ }
18
+ for _, invalid := range []string{"", "Basic " + strings.Repeat("A", 43), valid + "=", valid + " ", " " + valid, "Bearer " + strings.Repeat("A", 42) + "B"} {
19
+ if _, err := EnrollmentTokenHash(invalid); err == nil {
20
+ t.Fatal("noncanonical enrollment challenge accepted")
21
+ }
22
+ }
23
+ }
24
+
25
+ func TestSchemaFixtures(t *testing.T) {
26
+ for name, decode := range map[string]func([]byte) error{
27
+ "binding": func(data []byte) error { return json.Unmarshal(data, new(Binding)) },
28
+ "enrollment": func(data []byte) error { return json.Unmarshal(data, new(Enrollment)) },
29
+ "heartbeat": func(data []byte) error { return json.Unmarshal(data, new(Heartbeat)) },
30
+ } {
31
+ for _, kind := range []string{"valid", "invalid"} {
32
+ files, err := filepath.Glob(filepath.Join("../../fixtures/json-schema/personal-guest-"+name, kind, "*.json"))
33
+ if err != nil || len(files) == 0 {
34
+ t.Fatalf("missing %s/%s fixtures: %v", name, kind, err)
35
+ }
36
+ for _, file := range files {
37
+ t.Run(name+"/"+kind+"/"+filepath.Base(file), func(t *testing.T) {
38
+ data, err := os.ReadFile(file)
39
+ if err != nil {
40
+ t.Fatal(err)
41
+ }
42
+ if err := decode(data); (err == nil) != (kind == "valid") {
43
+ t.Fatalf("fixture result: %v", err)
44
+ }
45
+ })
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ func guestFixture[T any](t *testing.T, name string) T {
52
+ t.Helper()
53
+ data, err := os.ReadFile("../../fixtures/json-schema/personal-guest-" + name + "/valid/minimal.json")
54
+ if err != nil {
55
+ t.Fatal(err)
56
+ }
57
+ var value T
58
+ if err := json.Unmarshal(data, &value); err != nil {
59
+ t.Fatal(err)
60
+ }
61
+ return value
62
+ }
63
+
64
+ func TestHeartbeatRequiresCurrentBindingAndFreshMonotonicObservation(t *testing.T) {
65
+ binding := guestFixture[Binding](t, "binding")
66
+ heartbeat := guestFixture[Heartbeat](t, "heartbeat")
67
+ now := heartbeat.ObservedAt
68
+ if err := heartbeat.ValidateFor(binding, now, 0); err != nil {
69
+ t.Fatal(err)
70
+ }
71
+ for name, mutate := range map[string]func(*Heartbeat){
72
+ "different binding": func(h *Heartbeat) { h.BindingID = binding.DataRef },
73
+ "old revision": func(h *Heartbeat) { h.BindingVersion++ },
74
+ "old compute": func(h *Heartbeat) { h.ComputeGeneration++ },
75
+ "zero sequence": func(h *Heartbeat) { h.Sequence = 0 },
76
+ "inexact sequence": func(h *Heartbeat) { h.Sequence = MaxCounter + 1 },
77
+ "future clock": func(h *Heartbeat) { h.ObservedAt = now.Add(MaxClockSkew + time.Nanosecond) },
78
+ "old clock": func(h *Heartbeat) { h.ObservedAt = now.Add(-MaxClockSkew - time.Nanosecond) },
79
+ "expired observation": func(h *Heartbeat) {
80
+ h.ObservedAt = binding.ExpiresAt
81
+ },
82
+ } {
83
+ t.Run(name, func(t *testing.T) {
84
+ h := heartbeat
85
+ mutate(&h)
86
+ if h.ValidateFor(binding, now, 0) == nil {
87
+ t.Fatal("invalid heartbeat accepted")
88
+ }
89
+ })
90
+ }
91
+ for name, serverNow := range map[string]time.Time{
92
+ "zero clock": {}, "not yet valid": binding.IssuedAt.Add(-time.Nanosecond), "expired": binding.ExpiresAt,
93
+ } {
94
+ t.Run(name, func(t *testing.T) {
95
+ if heartbeat.ValidateFor(binding, serverNow, 0) == nil {
96
+ t.Fatal("invalid binding deadline accepted")
97
+ }
98
+ })
99
+ }
100
+ if heartbeat.ValidateFor(binding, now, heartbeat.Sequence) == nil || heartbeat.ValidateFor(binding, now, -1) == nil {
101
+ t.Fatal("replay or invalid server sequence accepted")
102
+ }
103
+ heartbeat.Sequence++
104
+ if err := heartbeat.ValidateFor(binding, now, heartbeat.Sequence-1); err != nil {
105
+ t.Fatal(err)
106
+ }
107
+ }
108
+
109
+ func TestBindingRejectsInvalidLifetime(t *testing.T) {
110
+ binding := guestFixture[Binding](t, "binding")
111
+ for _, expiresAt := range []time.Time{binding.IssuedAt, binding.IssuedAt.Add(-time.Second), binding.IssuedAt.Add(24*time.Hour + time.Nanosecond)} {
112
+ binding.ExpiresAt = expiresAt
113
+ if binding.Validate() == nil {
114
+ t.Fatal("invalid credential lifetime accepted")
115
+ }
116
+ }
117
+ }
118
+
119
+ func TestDecodeRejectsUnknownFieldsInvalidUTF8AndOversizedMessages(t *testing.T) {
120
+ valid, err := json.Marshal(guestFixture[Heartbeat](t, "heartbeat"))
121
+ if err != nil {
122
+ t.Fatal(err)
123
+ }
124
+ for name, data := range map[string][]byte{
125
+ "invalid UTF8": append(append([]byte{}, valid[:len(valid)-1]...), 0xff, '}'),
126
+ "null": []byte("null"),
127
+ "oversized": append(valid, make([]byte, 4096)...),
128
+ "legacy owner": append(append([]byte{}, valid[:len(valid)-1]...), []byte(",\"ownerId\":\"other\"}")...),
129
+ "duplicate": append(append([]byte{}, valid[:len(valid)-1]...), []byte(",\"sequence\":2}")...),
130
+ "wrong case": []byte(strings.Replace(string(valid), "\"sequence\"", "\"Sequence\"", 1)),
131
+ } {
132
+ t.Run(name, func(t *testing.T) {
133
+ if json.Unmarshal(data, new(Heartbeat)) == nil {
134
+ t.Fatal("invalid wire message accepted")
135
+ }
136
+ })
137
+ }
138
+ type wireHeartbeat Heartbeat
139
+ if decodeStrict(append(valid, []byte(" {}")...), new(wireHeartbeat), "schemaVersion", "bindingId", "bindingVersion", "computeGeneration", "sequence", "observedAt") == nil {
140
+ t.Fatal("trailing object accepted")
141
+ }
142
+ }
@@ -0,0 +1,56 @@
1
+ package guest
2
+
3
+ import (
4
+ "errors"
5
+ "time"
6
+ )
7
+
8
+ const RenewalVersion = "opensoha.dev/personal-guest-renewal/v1"
9
+
10
+ // Renewal is authenticated with the current certificate. Core derives every
11
+ // identity field; requestId makes a lost issuance response recoverable.
12
+ type Renewal struct {
13
+ SchemaVersion string `json:"schemaVersion"`
14
+ RequestID string `json:"requestId"`
15
+ BindingVersion int64 `json:"bindingVersion"`
16
+ CSRPEM string `json:"csrPEM"`
17
+ }
18
+
19
+ type RenewalResult struct {
20
+ RequestID string `json:"requestId"`
21
+ Binding Binding `json:"binding"`
22
+ CertificatePEM string `json:"certificatePEM"`
23
+ ClientCAPEM string `json:"clientCAPEM"`
24
+ CommitBefore time.Time `json:"commitBefore"`
25
+ }
26
+
27
+ func (r Renewal) Validate() error {
28
+ if r.SchemaVersion != RenewalVersion || !uuidPattern.MatchString(r.RequestID) || r.BindingVersion < 1 || r.BindingVersion >= MaxCounter || len(r.CSRPEM) == 0 || len(r.CSRPEM) > 2048 {
29
+ return errors.New("invalid personal guest renewal")
30
+ }
31
+ return nil
32
+ }
33
+
34
+ func (r *Renewal) UnmarshalJSON(data []byte) error {
35
+ type wire Renewal
36
+ var value wire
37
+ if err := decodeStrict(data, &value, "schemaVersion", "requestId", "bindingVersion", "csrPEM"); err != nil {
38
+ return err
39
+ }
40
+ decoded := Renewal(value)
41
+ if err := decoded.Validate(); err != nil {
42
+ return err
43
+ }
44
+ *r = decoded
45
+ return nil
46
+ }
47
+
48
+ // SameWorkspace excludes only registration and lease fields. It is not an
49
+ // authorization check and must never authorize a disk format by itself.
50
+ func (b Binding) SameWorkspace(other Binding) bool {
51
+ b.ID, other.ID = "", ""
52
+ b.Version, other.Version = 0, 0
53
+ b.IssuedAt = other.IssuedAt
54
+ b.ExpiresAt = other.ExpiresAt
55
+ return b == other
56
+ }
@@ -0,0 +1,53 @@
1
+ package guest
2
+
3
+ import (
4
+ "encoding/json"
5
+ "testing"
6
+ "time"
7
+ )
8
+
9
+ func TestRenewalRejectsDuplicateAndUnknownAuthority(t *testing.T) {
10
+ for _, raw := range []string{`{"schemaVersion":"opensoha.dev/personal-guest-renewal/v1","requestId":"11111111-1111-4111-8111-111111111111","bindingVersion":1,"bindingVersion":2,"csrPEM":"public"}`, `{"schemaVersion":"opensoha.dev/personal-guest-renewal/v1","requestId":"11111111-1111-4111-8111-111111111111","bindingVersion":1,"csrPEM":"public","ownerId":"other"}`} {
11
+ var request Renewal
12
+ if json.Unmarshal([]byte(raw), &request) == nil {
13
+ t.Fatal("untrusted renewal fields accepted")
14
+ }
15
+ }
16
+ }
17
+
18
+ func TestSameWorkspaceIgnoresOnlyRegistrationAndLeaseFields(t *testing.T) {
19
+ binding := guestFixture[Binding](t, "binding")
20
+ renewed := binding
21
+ renewed.ID = "another-registration"
22
+ renewed.Version++
23
+ renewed.IssuedAt = renewed.IssuedAt.Add(time.Hour)
24
+ renewed.ExpiresAt = renewed.ExpiresAt.Add(time.Hour)
25
+ beforeBinding, beforeRenewed := binding, renewed
26
+ if !binding.SameWorkspace(renewed) || !renewed.SameWorkspace(binding) {
27
+ t.Fatal("registration or lease changes changed workspace identity")
28
+ }
29
+ if binding != beforeBinding || renewed != beforeRenewed {
30
+ t.Fatal("workspace comparison mutated a binding")
31
+ }
32
+ for name, mutate := range map[string]func(*Binding){
33
+ "schema": func(b *Binding) { b.SchemaVersion += "-other" },
34
+ "assistant": func(b *Binding) { b.AssistantID += "-other" },
35
+ "owner": func(b *Binding) { b.OwnerID += "-other" },
36
+ "provider": func(b *Binding) { b.Provider += "-other" },
37
+ "compute": func(b *Binding) { b.ComputeRef += "-other" },
38
+ "generation": func(b *Binding) { b.ComputeGeneration++ },
39
+ "provider identity": func(b *Binding) { b.ProviderIdentityHash += "-other" },
40
+ "data": func(b *Binding) { b.DataRef += "-other" },
41
+ "disk serial": func(b *Binding) { b.DataDiskSerial += "-other" },
42
+ "disk size": func(b *Binding) { b.DataDiskBytes++ },
43
+ "policy": func(b *Binding) { b.PolicyRevision += "-other" },
44
+ } {
45
+ t.Run(name, func(t *testing.T) {
46
+ other := renewed
47
+ mutate(&other)
48
+ if binding.SameWorkspace(other) || other.SameWorkspace(binding) {
49
+ t.Fatal("different workspace identity accepted")
50
+ }
51
+ })
52
+ }
53
+ }