@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.
- package/COMPATIBILITY.md +95 -0
- package/README.md +24 -6
- package/agent/environment/README.md +55 -0
- package/agent/environment/export.go +139 -0
- package/agent/environment/export_test.go +62 -0
- package/agent/environment/preflight.go +62 -0
- package/agent/environment/preflight_test.go +41 -0
- package/agent/environment/protocol.go +221 -0
- package/agent/environment/protocol_test.go +81 -0
- package/agent/environment-lease.schema.json +2 -2
- package/agent/environment-template.schema.json +1 -1
- package/agent/guest/browser_network.go +77 -0
- package/agent/guest/data.go +62 -0
- package/agent/guest/protocol.go +235 -0
- package/agent/guest/protocol_test.go +142 -0
- package/agent/guest/renewal.go +56 -0
- package/agent/guest/renewal_test.go +53 -0
- package/agent/guest/runtime.go +315 -0
- package/agent/guest/runtime_json.go +75 -0
- package/agent/guest/runtime_test.go +110 -0
- package/agent/personal-browser-network-request.schema.json +40 -0
- package/agent/personal-browser-network-response.schema.json +44 -0
- package/agent/personal-environment-export.schema.json +55 -0
- package/agent/personal-environment-preflight-request.schema.json +52 -0
- package/agent/personal-environment-preflight-result.schema.json +69 -0
- package/agent/personal-environment-request.schema.json +137 -0
- package/agent/personal-environment-result.schema.json +101 -0
- package/agent/personal-guest-binding.schema.json +92 -0
- package/agent/personal-guest-data-plan.schema.json +32 -0
- package/agent/personal-guest-enrollment.schema.json +21 -0
- package/agent/personal-guest-heartbeat.schema.json +44 -0
- package/agent/personal-guest-provisioning.schema.json +71 -0
- package/agent/personal-guest-renewal.schema.json +33 -0
- package/agent/personal-guest-runtime-result.schema.json +164 -0
- package/agent/personal-guest-runtime.schema.json +304 -0
- package/agent/personal-guest-tools.schema.json +75 -0
- package/auth/catalog_generated.go +2 -2
- package/auth/permission-catalog.json +32 -1
- package/auth/token-claims.schema.json +5 -0
- package/compat/consumer-revisions.json +4 -3
- package/compat/json-schema-ecosystem-0.1.0.json +1 -1
- package/compat/openapi-loose-boundaries-0.1.0.json +1 -1
- package/connectors/connector-action.schema.json +138 -0
- package/fixtures/json-schema/connector-action/invalid/empty-values.json +11 -0
- package/fixtures/json-schema/connector-action/invalid/invalid-expiry.json +16 -0
- package/fixtures/json-schema/connector-action/invalid/lowercase-alias.json +16 -0
- package/fixtures/json-schema/connector-action/invalid/unknown-entry-field.json +17 -0
- package/fixtures/json-schema/connector-action/invalid/unknown-frame-field.json +17 -0
- package/fixtures/json-schema/connector-action/invalid/unknown-version.json +16 -0
- package/fixtures/json-schema/connector-action/invalid/unversioned-reference.json +16 -0
- package/fixtures/json-schema/connector-action/invalid/zero-version.json +16 -0
- package/fixtures/json-schema/connector-action/valid/pinned.json +16 -0
- package/fixtures/json-schema/connector-definition/invalid/credential-userinfo.json +8 -0
- package/fixtures/json-schema/connector-definition/invalid/reference-traversal.json +8 -0
- package/fixtures/json-schema/connector-definition/invalid/zero-version.json +8 -0
- package/fixtures/json-schema/connector-definition/valid/canonical.json +8 -0
- package/fixtures/json-schema/connector-definition/valid/pinned.json +8 -0
- package/fixtures/json-schema/personal-browser-network-request/invalid/bounded.json +4 -0
- package/fixtures/json-schema/personal-browser-network-request/valid/bounded.json +4 -0
- package/fixtures/json-schema/personal-browser-network-response/invalid/bounded.json +5 -0
- package/fixtures/json-schema/personal-browser-network-response/valid/bounded.json +5 -0
- package/fixtures/json-schema/personal-environment-export/invalid/extra.json +11 -0
- package/fixtures/json-schema/personal-environment-export/invalid/ownerId.json +10 -0
- package/fixtures/json-schema/personal-environment-export/invalid/sha256.json +10 -0
- package/fixtures/json-schema/personal-environment-export/invalid/sizeBytes.json +10 -0
- package/fixtures/json-schema/personal-environment-export/valid/complete.json +10 -0
- package/fixtures/json-schema/personal-environment-preflight-request/invalid/disk.json +11 -0
- package/fixtures/json-schema/personal-environment-preflight-request/invalid/network.json +11 -0
- package/fixtures/json-schema/personal-environment-preflight-request/invalid/node.json +12 -0
- package/fixtures/json-schema/personal-environment-preflight-request/invalid/owner.json +12 -0
- package/fixtures/json-schema/personal-environment-preflight-request/valid/bounded.json +11 -0
- package/fixtures/json-schema/personal-environment-preflight-result/invalid/disk.json +15 -0
- package/fixtures/json-schema/personal-environment-preflight-result/invalid/network.json +15 -0
- package/fixtures/json-schema/personal-environment-preflight-result/invalid/no-hard-quota.json +15 -0
- package/fixtures/json-schema/personal-environment-preflight-result/invalid/node.json +16 -0
- package/fixtures/json-schema/personal-environment-preflight-result/invalid/not-ready.json +15 -0
- package/fixtures/json-schema/personal-environment-preflight-result/invalid/owner.json +16 -0
- package/fixtures/json-schema/personal-environment-preflight-result/valid/bounded.json +15 -0
- package/fixtures/json-schema/personal-environment-request/invalid/delete-data-argv.json +24 -0
- package/fixtures/json-schema/personal-environment-request/invalid/delete-data-createData.json +22 -0
- package/fixtures/json-schema/personal-environment-request/invalid/delete-data-taskId.json +22 -0
- package/fixtures/json-schema/personal-environment-request/invalid/export-argv.json +22 -0
- package/fixtures/json-schema/personal-environment-request/invalid/export-createData.json +22 -0
- package/fixtures/json-schema/personal-environment-request/invalid/export-taskId.json +22 -0
- package/fixtures/json-schema/personal-environment-request/invalid/extra-command.json +24 -0
- package/fixtures/json-schema/personal-environment-request/invalid/host-workspace.json +22 -0
- package/fixtures/json-schema/personal-environment-request/invalid/image-injection.json +22 -0
- package/fixtures/json-schema/personal-environment-request/invalid/network-egress.json +21 -0
- package/fixtures/json-schema/personal-environment-request/invalid/no-command.json +21 -0
- package/fixtures/json-schema/personal-environment-request/invalid/recreate-on-release.json +22 -0
- package/fixtures/json-schema/personal-environment-request/invalid/relative-command.json +24 -0
- package/fixtures/json-schema/personal-environment-request/invalid/released-identity.json +22 -0
- package/fixtures/json-schema/personal-environment-request/invalid/task-empty.json +27 -0
- package/fixtures/json-schema/personal-environment-request/invalid/task-host-path.json +27 -0
- package/fixtures/json-schema/personal-environment-request/invalid/task-noncanonical.json +27 -0
- package/fixtures/json-schema/personal-environment-request/invalid/task-null.json +27 -0
- package/fixtures/json-schema/personal-environment-request/invalid/task-on-inspection.json +22 -0
- package/fixtures/json-schema/personal-environment-request/invalid/unbounded-quota.json +21 -0
- package/fixtures/json-schema/personal-environment-request/valid/acquire.json +21 -0
- package/fixtures/json-schema/personal-environment-request/valid/delete-data.json +21 -0
- package/fixtures/json-schema/personal-environment-request/valid/execute.json +26 -0
- package/fixtures/json-schema/personal-environment-request/valid/export-data.json +21 -0
- package/fixtures/json-schema/personal-environment-request/valid/inspect.json +21 -0
- package/fixtures/json-schema/personal-environment-request/valid/provision.json +22 -0
- package/fixtures/json-schema/personal-environment-request/valid/release.json +21 -0
- package/fixtures/json-schema/personal-environment-request/valid/task-workspace.json +27 -0
- package/fixtures/json-schema/personal-environment-result/invalid/active-without-observation.json +7 -0
- package/fixtures/json-schema/personal-environment-result/invalid/deleted-output.json +8 -0
- package/fixtures/json-schema/personal-environment-result/invalid/deleted-snapshot.json +8 -0
- package/fixtures/json-schema/personal-environment-result/invalid/deleted-taskId.json +8 -0
- package/fixtures/json-schema/personal-environment-result/invalid/private-handle.json +8 -0
- package/fixtures/json-schema/personal-environment-result/invalid/stopped-fake-observation.json +20 -0
- package/fixtures/json-schema/personal-environment-result/invalid/task-empty.json +22 -0
- package/fixtures/json-schema/personal-environment-result/invalid/task-host-path.json +22 -0
- package/fixtures/json-schema/personal-environment-result/invalid/task-null.json +22 -0
- package/fixtures/json-schema/personal-environment-result/invalid/task-on-stopped.json +8 -0
- package/fixtures/json-schema/personal-environment-result/invalid/task-without-command-output.json +21 -0
- package/fixtures/json-schema/personal-environment-result/valid/active.json +21 -0
- package/fixtures/json-schema/personal-environment-result/valid/deleted.json +7 -0
- package/fixtures/json-schema/personal-environment-result/valid/expired.json +7 -0
- package/fixtures/json-schema/personal-environment-result/valid/missing.json +7 -0
- package/fixtures/json-schema/personal-environment-result/valid/released.json +7 -0
- package/fixtures/json-schema/personal-environment-result/valid/task-workspace.json +22 -0
- package/fixtures/json-schema/personal-guest-binding/invalid/host-path.json +17 -0
- package/fixtures/json-schema/personal-guest-binding/invalid/invalid-policy.json +17 -0
- package/fixtures/json-schema/personal-guest-binding/invalid/invalid-serial.json +17 -0
- package/fixtures/json-schema/personal-guest-binding/invalid/legacy-version.json +17 -0
- package/fixtures/json-schema/personal-guest-binding/invalid/missing-generation.json +17 -0
- package/fixtures/json-schema/personal-guest-binding/invalid/missing-identity.json +16 -0
- package/fixtures/json-schema/personal-guest-binding/invalid/missing-provider-identity.json +16 -0
- package/fixtures/json-schema/personal-guest-binding/invalid/raw-provider-identity.json +17 -0
- package/fixtures/json-schema/personal-guest-binding/valid/minimal.json +17 -0
- package/fixtures/json-schema/personal-guest-data-plan/invalid/quota.json +1 -0
- package/fixtures/json-schema/personal-guest-data-plan/valid/basic.json +1 -0
- package/fixtures/json-schema/personal-guest-enrollment/invalid/capabilities.json +7 -0
- package/fixtures/json-schema/personal-guest-enrollment/invalid/legacy-version.json +4 -0
- package/fixtures/json-schema/personal-guest-enrollment/invalid/missing-identity.json +3 -0
- package/fixtures/json-schema/personal-guest-enrollment/invalid/ownerId.json +5 -0
- package/fixtures/json-schema/personal-guest-enrollment/invalid/token.json +5 -0
- package/fixtures/json-schema/personal-guest-enrollment/valid/minimal.json +4 -0
- package/fixtures/json-schema/personal-guest-heartbeat/invalid/argv.json +11 -0
- package/fixtures/json-schema/personal-guest-heartbeat/invalid/legacy-version.json +8 -0
- package/fixtures/json-schema/personal-guest-heartbeat/invalid/missing-identity.json +7 -0
- package/fixtures/json-schema/personal-guest-heartbeat/invalid/owner.json +9 -0
- package/fixtures/json-schema/personal-guest-heartbeat/invalid/self-ready.json +9 -0
- package/fixtures/json-schema/personal-guest-heartbeat/invalid/zero-sequence.json +8 -0
- package/fixtures/json-schema/personal-guest-heartbeat/valid/minimal.json +8 -0
- package/fixtures/json-schema/personal-guest-provisioning/invalid/network.json +1 -0
- package/fixtures/json-schema/personal-guest-provisioning/valid/basic.json +1 -0
- package/fixtures/json-schema/personal-guest-renewal/invalid/unknown.json +1 -0
- package/fixtures/json-schema/personal-guest-renewal/valid/basic.json +1 -0
- package/fixtures/json-schema/personal-guest-runtime/invalid/minimal.json +45 -0
- package/fixtures/json-schema/personal-guest-runtime/valid/minimal.json +45 -0
- package/fixtures/json-schema/personal-guest-runtime-result/invalid/minimal.json +28 -0
- package/fixtures/json-schema/personal-guest-runtime-result/valid/minimal.json +27 -0
- package/fixtures/json-schema/personal-guest-tools/invalid/minimal.json +31 -0
- package/fixtures/json-schema/personal-guest-tools/valid/minimal.json +30 -0
- package/fixtures/json-schema/source-import-policy/invalid/case-variant-scope.json +4 -0
- package/fixtures/json-schema/source-import-policy/invalid/case-variant-version.json +4 -0
- package/fixtures/json-schema/source-import-policy/invalid/duplicate-grant.json +4 -0
- package/fixtures/json-schema/source-import-policy/invalid/empty-id.json +4 -0
- package/fixtures/json-schema/source-import-policy/invalid/future-version.json +4 -0
- package/fixtures/json-schema/source-import-policy/invalid/unknown-field.json +4 -0
- package/fixtures/json-schema/source-import-policy/valid/deny-all.json +4 -0
- package/fixtures/json-schema/source-import-policy/valid/shared.json +4 -0
- package/fixtures/json-schema/token-claims/invalid/authz-negative.json +23 -0
- package/fixtures/json-schema/token-claims/invalid/authz-string.json +23 -0
- package/fixtures/json-schema/token-claims/invalid/authz-zero.json +23 -0
- package/fixtures/json-schema/token-claims/valid/versioned-session.json +23 -0
- package/fixtures/openapi/AgentRunEventListEnvelope/invalid/basic.json +8 -0
- package/fixtures/openapi/AgentRunEventListEnvelope/valid/basic.json +8 -0
- package/fixtures/openapi/AgentRunEventListEnvelope/valid/native.json +1 -0
- package/fixtures/openapi/AgentRunResumeRequest/invalid/basic.json +3 -0
- package/fixtures/openapi/AgentRunResumeRequest/valid/basic.json +3 -0
- package/fixtures/openapi/AgentSSHCommandRequest/invalid/task51.json +24 -0
- package/fixtures/openapi/AgentSSHCommandRequest/valid/task51.json +23 -0
- package/fixtures/openapi/AgentSSHCommandResult/invalid/task51.json +8 -0
- package/fixtures/openapi/AgentSSHCommandResult/valid/task51.json +7 -0
- package/fixtures/openapi/AgentSSHSessionRequest/invalid/task51.json +20 -0
- package/fixtures/openapi/AgentSSHSessionRequest/valid/task51.json +19 -0
- package/fixtures/openapi/CapabilityTaskInput/invalid/duplicate-scope.json +36 -0
- package/fixtures/openapi/CapabilityTaskInput/valid/scoped.json +32 -0
- package/fixtures/openapi/DockerQuickCreateHostInput/invalid/guest-access.json +7 -0
- package/fixtures/openapi/DockerQuickCreateHostInput/valid/guest-access.json +9 -0
- package/fixtures/openapi/KnowledgeSourceDocument/invalid/tombstone-with-content.json +1 -0
- package/fixtures/openapi/KnowledgeSourceDocument/valid/tombstone.json +1 -0
- package/fixtures/openapi/KubernetesResourceUpdatePlanRequest/invalid/missing-required.json +1 -0
- package/fixtures/openapi/KubernetesResourceYaml/invalid/missing-required.json +1 -0
- package/fixtures/openapi/ObservabilityDataSource/invalid/missing-required.json +1 -0
- package/fixtures/openapi/PersonalAgentAccess/invalid/invalid.json +1 -0
- package/fixtures/openapi/PersonalAgentAccess/valid/valid.json +1 -0
- package/fixtures/openapi/PersonalAgentAccessDecision/invalid/invalid.json +1 -0
- package/fixtures/openapi/PersonalAgentAccessDecision/valid/valid.json +1 -0
- package/fixtures/openapi/PersonalAssistantArtifactListEnvelope/invalid/owner.json +8 -0
- package/fixtures/openapi/PersonalAssistantArtifactListEnvelope/valid/side-chat.json +9 -0
- package/fixtures/openapi/PersonalAssistantAvatarRequest/invalid/motion.json +1 -0
- package/fixtures/openapi/PersonalAssistantAvatarRequest/invalid/owner.json +1 -0
- package/fixtures/openapi/PersonalAssistantAvatarRequest/invalid/remote.json +1 -0
- package/fixtures/openapi/PersonalAssistantAvatarRequest/invalid/svg.json +1 -0
- package/fixtures/openapi/PersonalAssistantAvatarRequest/valid/raster.json +1 -0
- package/fixtures/openapi/PersonalAssistantAvatarRequest/valid/reset.json +1 -0
- package/fixtures/openapi/PersonalAssistantUpdateRequest/invalid/empty.json +1 -0
- package/fixtures/openapi/PersonalAssistantUpdateRequest/invalid/owner.json +1 -0
- package/fixtures/openapi/PersonalAssistantUpdateRequest/valid/name.json +1 -0
- package/fixtures/openapi/PersonalDockerEnvironmentPolicy/invalid/host-url.json +12 -0
- package/fixtures/openapi/PersonalDockerEnvironmentPolicy/invalid/lifetime.json +11 -0
- package/fixtures/openapi/PersonalDockerEnvironmentPolicy/invalid/unknown-agent.json +11 -0
- package/fixtures/openapi/PersonalDockerEnvironmentPolicy/valid/basic.json +11 -0
- package/fixtures/openapi/PersonalEnvironment/invalid/invented-state.json +6 -0
- package/fixtures/openapi/PersonalEnvironment/invalid/missing-state.json +5 -0
- package/fixtures/openapi/PersonalEnvironment/invalid/negative-generation.json +7 -0
- package/fixtures/openapi/PersonalEnvironment/invalid/network.json +23 -0
- package/fixtures/openapi/PersonalEnvironment/invalid/private-node.json +24 -0
- package/fixtures/openapi/PersonalEnvironment/invalid/unsupported-backend.json +7 -0
- package/fixtures/openapi/PersonalEnvironment/valid/deleted.json +23 -0
- package/fixtures/openapi/PersonalEnvironment/valid/deleting.json +23 -0
- package/fixtures/openapi/PersonalEnvironment/valid/deletion_unknown.json +23 -0
- package/fixtures/openapi/PersonalEnvironment/valid/frozen-binding.json +13 -0
- package/fixtures/openapi/PersonalEnvironment/valid/ready.json +23 -0
- package/fixtures/openapi/PersonalEnvironment/valid/unknown.json +23 -0
- package/fixtures/openapi/PersonalEnvironment/valid/unprovisioned.json +6 -0
- package/fixtures/openapi/PersonalEnvironmentActionRequest/invalid/command.json +3 -0
- package/fixtures/openapi/PersonalEnvironmentActionRequest/invalid/node.json +4 -0
- package/fixtures/openapi/PersonalEnvironmentActionRequest/invalid/owner.json +4 -0
- package/fixtures/openapi/PersonalEnvironmentActionRequest/invalid/quota.json +10 -0
- package/fixtures/openapi/PersonalEnvironmentActionRequest/valid/reconcile.json +3 -0
- package/fixtures/openapi/PersonalEnvironmentActionRequest/valid/sleep.json +3 -0
- package/fixtures/openapi/PersonalEnvironmentActionRequest/valid/wake.json +4 -0
- package/fixtures/openapi/PersonalEnvironmentDataDeletionRequest/invalid/empty-name.json +5 -0
- package/fixtures/openapi/PersonalEnvironmentDataDeletionRequest/invalid/long-name.json +5 -0
- package/fixtures/openapi/PersonalEnvironmentDataDeletionRequest/invalid/missing-name.json +4 -0
- package/fixtures/openapi/PersonalEnvironmentDataDeletionRequest/invalid/owner.json +6 -0
- package/fixtures/openapi/PersonalEnvironmentDataDeletionRequest/invalid/stale-version.json +5 -0
- package/fixtures/openapi/PersonalEnvironmentDataDeletionRequest/valid/confirmed.json +5 -0
- package/fixtures/openapi/PersonalEnvironmentDataExportRequest/invalid/assistantId.json +4 -0
- package/fixtures/openapi/PersonalEnvironmentDataExportRequest/invalid/environmentVersion.json +4 -0
- package/fixtures/openapi/PersonalEnvironmentDataExportRequest/invalid/ownerId.json +5 -0
- package/fixtures/openapi/PersonalEnvironmentDataExportRequest/valid/current.json +4 -0
- package/fixtures/openapi/PersonalEnvironmentEnvelope/invalid/missing-data.json +1 -0
- package/fixtures/openapi/PersonalEnvironmentEnvelope/valid/state.json +8 -0
- package/fixtures/openapi/PersonalEnvironmentQuota/invalid/cpu.json +7 -0
- package/fixtures/openapi/PersonalEnvironmentQuota/invalid/disk.json +7 -0
- package/fixtures/openapi/PersonalEnvironmentQuota/invalid/network.json +7 -0
- package/fixtures/openapi/PersonalEnvironmentQuota/invalid/processes.json +7 -0
- package/fixtures/openapi/PersonalEnvironmentQuota/valid/basic.json +7 -0
- package/fixtures/openapi/PersonalEnvironmentTemplate/invalid/mode.json +13 -0
- package/fixtures/openapi/PersonalEnvironmentTemplate/invalid/network.json +13 -0
- package/fixtures/openapi/PersonalEnvironmentTemplate/invalid/node.json +14 -0
- package/fixtures/openapi/PersonalEnvironmentTemplate/valid/template.json +13 -0
- package/fixtures/openapi/PersonalEnvironmentTemplateListEnvelope/invalid/missing.json +1 -0
- package/fixtures/openapi/PersonalEnvironmentTemplateListEnvelope/invalid/unbounded.json +1317 -0
- package/fixtures/openapi/PersonalEnvironmentTemplateListEnvelope/valid/choices.json +17 -0
- package/fixtures/openapi/PersonalEnvironmentTemplateListEnvelope/valid/empty.json +3 -0
- package/fixtures/openapi/PersonalEnvironmentUsage/invalid/negative.json +6 -0
- package/fixtures/openapi/PersonalEnvironmentUsage/valid/usage.json +6 -0
- package/fixtures/openapi/PersonalFeedFeedback/invalid/owner.json +4 -0
- package/fixtures/openapi/PersonalFeedFeedback/valid/liked.json +3 -0
- package/fixtures/openapi/PersonalFeedItem/invalid/no-source.json +9 -0
- package/fixtures/openapi/PersonalFeedItem/valid/post.json +15 -0
- package/fixtures/openapi/PersonalFeedSettings/invalid/owner.json +6 -0
- package/fixtures/openapi/PersonalFeedSettings/valid/default.json +5 -0
- package/fixtures/openapi/PersonalGuestPreparationInput/invalid/foreign-owner.json +5 -0
- package/fixtures/openapi/PersonalGuestPreparationInput/invalid/unsafe-counter.json +4 -0
- package/fixtures/openapi/PersonalGuestPreparationInput/valid/ed25519-csr.json +4 -0
- package/fixtures/openapi/PersonalGuestRuntimeEnvelope/invalid/minimal.json +10 -0
- package/fixtures/openapi/PersonalGuestRuntimeEnvelope/valid/minimal.json +9 -0
- package/fixtures/openapi/PersonalGuestRuntimeInput/invalid/minimal.json +6 -0
- package/fixtures/openapi/PersonalGuestRuntimeInput/valid/minimal.json +8 -0
- package/fixtures/openapi/PersonalGuestToolsInput/invalid/minimal.json +5 -0
- package/fixtures/openapi/PersonalGuestToolsInput/valid/minimal.json +4 -0
- package/fixtures/openapi/PersonalVMEnvironmentPolicy/invalid/device.json +15 -0
- package/fixtures/openapi/PersonalVMEnvironmentPolicy/invalid/fractional-size.json +15 -0
- package/fixtures/openapi/PersonalVMEnvironmentPolicy/invalid/network.json +15 -0
- package/fixtures/openapi/PersonalVMEnvironmentPolicy/invalid/owner.json +15 -0
- package/fixtures/openapi/PersonalVMEnvironmentPolicy/invalid/provider.json +15 -0
- package/fixtures/openapi/PersonalVMEnvironmentPolicy/invalid/unknown.json +16 -0
- package/fixtures/openapi/PersonalVMEnvironmentPolicy/valid/pve.json +15 -0
- package/fixtures/openapi/PersonalWorkspaceRuntime/invalid/shared.json +3 -0
- package/fixtures/openapi/PersonalWorkspaceRuntime/valid/shared.json +4 -0
- package/fixtures/openapi/PersonalWorkspaceRuntimeEnvelope/invalid/shared.json +5 -0
- package/fixtures/openapi/PersonalWorkspaceRuntimeEnvelope/valid/shared.json +6 -0
- package/fixtures/openapi/SecretGenerateSSHKeyRequest/invalid/task51.json +6 -0
- package/fixtures/openapi/SecretGenerateSSHKeyRequest/valid/task51.json +12 -0
- package/fixtures/openapi/SecretReferenceUsage/invalid/version.json +6 -0
- package/fixtures/openapi/SecretReferenceUsage/valid/version.json +8 -0
- package/fixtures/openapi/VMSSHCredentialRedemptionInput/invalid/task51.json +7 -0
- package/fixtures/openapi/VMSSHCredentialRedemptionInput/valid/task51.json +6 -0
- package/fixtures/openapi/VirtualMachineAccessBindingInput/invalid/task51.json +10 -0
- package/fixtures/openapi/VirtualMachineAccessBindingInput/valid/task51.json +10 -0
- package/fixtures/openapi/VirtualMachineActionInput/invalid/capacity-not-boolean.json +1 -0
- package/fixtures/openapi/VirtualMachineActionInput/valid/new-data-with-admission.json +1 -0
- package/fixtures/openapi/VirtualMachineCreateInput/valid/cloud-image.json +12 -0
- package/fixtures/openapi/VirtualMachineDiskChange/invalid/serial-config-injection.json +1 -0
- package/fixtures/openapi/VirtualMachineDiskChange/invalid/serial-existing-disk.json +1 -0
- package/fixtures/openapi/VirtualMachineDiskChange/invalid/serial-too-long.json +1 -0
- package/fixtures/openapi/VirtualMachineDiskChange/valid/personal-data.json +1 -0
- package/fixtures/openapi/VirtualMachineGuestAccessInput/invalid/task51.json +4 -0
- package/fixtures/openapi/VirtualMachineGuestAccessInput/valid/task51.json +6 -0
- package/fixtures/openapi/VirtualMachineSSHCommand/invalid/task51.json +9 -0
- package/fixtures/openapi/VirtualMachineSSHCommand/valid/task51.json +8 -0
- package/fixtures/openapi/VirtualMachineSSHCommandInput/invalid/task51.json +5 -0
- package/fixtures/openapi/VirtualMachineSSHCommandInput/valid/task51.json +5 -0
- package/fixtures/openapi/VirtualMachineSSHFrame/invalid/encoding.json +5 -0
- package/fixtures/openapi/VirtualMachineSSHFrame/invalid/task51.json +4 -0
- package/fixtures/openapi/VirtualMachineSSHFrame/valid/binary.json +5 -0
- package/fixtures/openapi/VirtualMachineSSHFrame/valid/task51.json +5 -0
- package/fixtures/openapi/VirtualMachineSSHSessionInput/invalid/task51.json +4 -0
- package/fixtures/openapi/VirtualMachineSSHSessionInput/valid/task51.json +5 -0
- package/fixtures/openapi/VirtualizationCapacityPolicyInput/invalid/task51.json +12 -0
- package/fixtures/openapi/VirtualizationCapacityPolicyInput/valid/task51.json +13 -0
- package/fixtures/openapi/VirtualizationImageInput/invalid/cloud-image-config-type.json +6 -0
- package/fixtures/openapi/VirtualizationImageInput/valid/cloud-image.json +11 -0
- package/fixtures/openapi/WorkbenchAssistantTask/invalid/kind.json +1 -0
- package/fixtures/openapi/WorkbenchAssistantTask/invalid/owner.json +1 -0
- package/fixtures/openapi/WorkbenchAssistantTask/valid/generate.json +1 -0
- package/fixtures/openapi/WorkbenchAssistantTask/valid/undo.json +1 -0
- package/fixtures/openapi/WorkbenchContextSelection/invalid/remote-image.json +1 -0
- package/fixtures/openapi/WorkbenchContextSelection/invalid/too-many-images.json +1 -0
- package/fixtures/openapi/WorkbenchContextSelection/valid/chat-image.json +9 -0
- package/fixtures/openapi/WorkbenchContextUsage/invalid/fractional-capacity.json +1 -0
- package/fixtures/openapi/WorkbenchContextUsage/invalid/negative.json +1 -0
- package/fixtures/openapi/WorkbenchContextUsage/valid/reported.json +1 -0
- package/fixtures/openapi/WorkbenchContextUsage/valid/unknown.json +1 -0
- package/fixtures/openapi/WorkbenchMainSessionRequest/invalid/owner.json +1 -0
- package/fixtures/openapi/WorkbenchMainSessionRequest/invalid/provider.json +1 -0
- package/fixtures/openapi/WorkbenchMainSessionRequest/valid/empty.json +1 -0
- package/fixtures/openapi/WorkbenchMainSessionRequest/valid/provider.json +1 -0
- package/fixtures/openapi/WorkbenchMessageSearchPage/invalid/role.json +12 -0
- package/fixtures/openapi/WorkbenchMessageSearchPage/valid/page.json +12 -0
- package/fixtures/openapi/WorkbenchMessageWindow/invalid/missing.json +1 -0
- package/fixtures/openapi/WorkbenchMessageWindow/valid/empty.json +3 -0
- package/fixtures/openapi/WorkbenchModelOption/invalid/zero-capacity.json +1 -0
- package/fixtures/openapi/WorkbenchModelOption/valid/capacity.json +1 -0
- package/fixtures/openapi/WorkbenchModelOption/valid/unknown.json +1 -0
- package/fixtures/openapi/WorkbenchProfessionalCapabilityInput/invalid/basic.json +7 -0
- package/fixtures/openapi/WorkbenchProfessionalCapabilityInput/valid/basic.json +11 -0
- package/fixtures/openapi/WorkbenchProfessionalCapabilityInput/valid/selected-profile.json +23 -0
- package/fixtures/openapi/WorkbenchSendMessageStreamRequest/invalid/missing-required.json +1 -0
- package/fixtures/openapi/WorkbenchSendMessageStreamRequest/valid/mixed-professional-task.json +22 -0
- package/fixtures/openapi/WorkbenchTaskIntent/invalid/basic.json +5 -0
- package/fixtures/openapi/WorkbenchTaskIntent/invalid/oversized-capability-source.json +3 -0
- package/fixtures/openapi/WorkbenchTaskIntent/valid/basic.json +9 -0
- package/fixtures/openapi/WorkbenchTaskIntent/valid/independent-sources.json +17 -0
- package/gen/go/sohaapi/client.go +1 -1
- package/gen/go/sohaapi/personal_environment_test.go +25 -0
- package/gen/go/sohaapi/types.go +2679 -240
- package/gen/go/sohaapi/types_test.go +30 -30
- package/gen/go/sohaapi/vm_disk_test.go +21 -0
- package/gen/ts/sohaapi/index.ts +4150 -169
- package/go.mod +61 -49
- package/go.sum +202 -176
- package/knowledge/connector-definition.schema.json +6 -1
- package/knowledge/source-import-policy.schema.json +32 -0
- package/memory/memory-record.schema.json +1 -1
- package/openapi/soha-api.json +22230 -15693
- package/openapi/soha-api.yaml +2495 -12
- package/openapi/spec_generated.go +3 -3
- package/package.json +19 -3
- /package/fixtures/openapi/DeliveryExecutionHistoryPage/{invalid-kind.json → invalid/invalid-kind.json} +0 -0
- /package/fixtures/openapi/DeliveryExecutionHistoryPage/{valid-build.json → valid/valid-build.json} +0 -0
- /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
|
+
}
|