@opensoha/contracts 0.1.26 → 0.1.29

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 (213) hide show
  1. package/COMPATIBILITY.md +14 -3
  2. package/README.md +8 -9
  3. package/auth/catalog_generated.go +2 -2
  4. package/auth/permission-catalog.json +481 -1
  5. package/compat/consumer-revisions.json +1 -1
  6. package/compat/json-schema-ecosystem-0.1.0.json +1 -1
  7. package/compat/openapi-loose-boundaries-0.1.0.json +1 -1
  8. package/endpointcontrol/decode.go +64 -0
  9. package/endpointcontrol/models.go +138 -0
  10. package/endpointcontrol/models_test.go +51 -0
  11. package/endpointcontrol/protocol.schema.json +397 -0
  12. package/fixtures/json-schema/endpoint-controls/invalid/controls.json +16 -0
  13. package/fixtures/json-schema/endpoint-controls/invalid/missing-channel-decision.json +15 -0
  14. package/fixtures/json-schema/endpoint-controls/invalid/receipt-content.json +36 -0
  15. package/fixtures/json-schema/endpoint-controls/valid/controls.json +16 -0
  16. package/fixtures/json-schema/endpoint-controls/valid/receipt.json +35 -0
  17. package/fixtures/json-schema/network-runtime-protocol/invalid/eap-activate-bad-fingerprint.json +14 -0
  18. package/fixtures/json-schema/network-runtime-protocol/invalid/eap-gateway-request.json +14 -0
  19. package/fixtures/json-schema/network-runtime-protocol/invalid/eap-private-key-request.json +15 -0
  20. package/fixtures/json-schema/network-runtime-protocol/invalid/eap-profile-without-eap-credential.json +22 -0
  21. package/fixtures/json-schema/network-runtime-protocol/invalid/endpoint-dns-observation.json +25 -0
  22. package/fixtures/json-schema/network-runtime-protocol/invalid/gateway-presence.json +22 -0
  23. package/fixtures/json-schema/network-runtime-protocol/invalid/mihomo-protection-zero-version.json +45 -0
  24. package/fixtures/json-schema/network-runtime-protocol/invalid/presence-raw-key.json +23 -0
  25. package/fixtures/json-schema/network-runtime-protocol/invalid/presence-zero-sequence.json +22 -0
  26. package/fixtures/json-schema/network-runtime-protocol/invalid/workload-wide-vpn.json +17 -0
  27. package/fixtures/json-schema/network-runtime-protocol/valid/eap-certificate-activate.json +14 -0
  28. package/fixtures/json-schema/network-runtime-protocol/valid/eap-certificate-request.json +14 -0
  29. package/fixtures/json-schema/network-runtime-protocol/valid/eap-profile-enrollment.json +24 -0
  30. package/fixtures/json-schema/network-runtime-protocol/valid/endpoint-presence.json +22 -0
  31. package/fixtures/json-schema/network-runtime-protocol/valid/ipv6-configuration.json +102 -0
  32. package/fixtures/json-schema/network-runtime-protocol/valid/mihomo-protected-policy.json +45 -0
  33. package/fixtures/json-schema/network-runtime-protocol/valid/site-dns-ipv6.json +25 -0
  34. package/fixtures/json-schema/network-runtime-protocol/valid/workload-direct-ztna.json +20 -0
  35. package/fixtures/openapi/DLPShadowDecision/invalid/step-up-type.json +14 -0
  36. package/fixtures/openapi/DLPShadowDecision/valid/archive-v2.json +14 -0
  37. package/fixtures/openapi/DLPShadowDecision/valid/step-up.json +14 -0
  38. package/fixtures/openapi/DLPShadowInput/invalid/self-reported-result.json +7 -0
  39. package/fixtures/openapi/DLPShadowInput/invalid/too-many-samples.json +14 -0
  40. package/fixtures/openapi/DLPShadowInput/valid/bounded.json +7 -0
  41. package/fixtures/openapi/DockerVolumeDLPAppealInput/invalid/p4-response.json +3 -0
  42. package/fixtures/openapi/DockerVolumeDLPAppealInput/valid/p4-response.json +3 -0
  43. package/fixtures/openapi/DockerVolumeDLPAppealReviewInput/invalid/p4-response.json +5 -0
  44. package/fixtures/openapi/DockerVolumeDLPAppealReviewInput/valid/p4-response.json +5 -0
  45. package/fixtures/openapi/DockerVolumeDLPAuthorizationInput/invalid/p4-response.json +3 -0
  46. package/fixtures/openapi/DockerVolumeDLPAuthorizationInput/valid/p4-response.json +3 -0
  47. package/fixtures/openapi/DockerVolumeDLPContent/invalid/archive-unsupported-channel.json +9 -0
  48. package/fixtures/openapi/DockerVolumeDLPContent/invalid/p4.json +6 -0
  49. package/fixtures/openapi/DockerVolumeDLPContent/invalid/step-up-audit.json +9 -0
  50. package/fixtures/openapi/DockerVolumeDLPContent/invalid/unavailable-engine.json +9 -0
  51. package/fixtures/openapi/DockerVolumeDLPContent/invalid/unknown-data-level.json +12 -0
  52. package/fixtures/openapi/DockerVolumeDLPContent/valid/classified-rule.json +16 -0
  53. package/fixtures/openapi/DockerVolumeDLPContent/valid/p4.json +8 -0
  54. package/fixtures/openapi/DockerVolumeDLPContent/valid/step-up.json +9 -0
  55. package/fixtures/openapi/DockerVolumeDLPDecision/invalid/p4.json +9 -0
  56. package/fixtures/openapi/DockerVolumeDLPDecision/invalid/step-up-type.json +12 -0
  57. package/fixtures/openapi/DockerVolumeDLPDecision/valid/archive-v2.json +12 -0
  58. package/fixtures/openapi/DockerVolumeDLPDecision/valid/p4.json +11 -0
  59. package/fixtures/openapi/DockerVolumeDLPDecision/valid/step-up.json +12 -0
  60. package/fixtures/openapi/DockerVolumeDLPSaveInput/invalid/p4.json +11 -0
  61. package/fixtures/openapi/DockerVolumeDLPSaveInput/valid/p4.json +11 -0
  62. package/fixtures/openapi/DockerVolumeFileReadInput/invalid/p4-response.json +5 -0
  63. package/fixtures/openapi/DockerVolumeFileReadInput/valid/p4-response.json +4 -0
  64. package/fixtures/openapi/EndpointDeviceInput/invalid/reported-management.json +5 -0
  65. package/fixtures/openapi/EndpointDeviceInput/valid/management-state.json +5 -0
  66. package/fixtures/openapi/EndpointDeviceRegistrationInput/invalid/self-promote-management.json +5 -0
  67. package/fixtures/openapi/KubernetesClusterRegistrationRequest/invalid/metric-cluster-value-length.json +3 -0
  68. package/fixtures/openapi/KubernetesClusterRegistrationRequest/invalid/metric-cluster-value-type.json +3 -0
  69. package/fixtures/openapi/KubernetesClusterRegistrationRequest/valid/metric-cluster-alias.json +5 -0
  70. package/fixtures/openapi/KubernetesClusterRegistrationRequest/valid/metric-cluster-reset.json +3 -0
  71. package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/invalid/revision-overflow.json +4 -0
  72. package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/valid/legacy.json +3 -0
  73. package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/valid/observed-revision.json +4 -0
  74. package/fixtures/openapi/KubernetesJobDetail/invalid/execution-observation.json +23 -0
  75. package/fixtures/openapi/KubernetesJobDetail/valid/execution-observation.json +23 -0
  76. package/fixtures/openapi/KubernetesNode/invalid/labels.json +10 -0
  77. package/fixtures/openapi/KubernetesNode/valid/labels.json +11 -0
  78. package/fixtures/openapi/KubernetesPodListEnvelope/invalid/empty-uid.json +13 -0
  79. package/fixtures/openapi/KubernetesPodListEnvelope/invalid/invalid-container-state.json +20 -0
  80. package/fixtures/openapi/KubernetesPodListEnvelope/invalid/missing-container-ready.json +19 -0
  81. package/fixtures/openapi/KubernetesPodListEnvelope/valid/container-statuses.json +67 -0
  82. package/fixtures/openapi/KubernetesPodListEnvelope/valid/running-pod.json +1 -0
  83. package/fixtures/openapi/KubernetesPrometheusDetail/invalid/metric-cluster-value-type.json +4 -0
  84. package/fixtures/openapi/KubernetesPrometheusDetail/valid/metric-cluster-alias.json +5 -0
  85. package/fixtures/openapi/KubernetesResourceGraphEnvelope/invalid/network-edge-source-payload.json +73 -0
  86. package/fixtures/openapi/KubernetesResourceGraphEnvelope/valid/network-edge-facts.json +67 -0
  87. package/fixtures/openapi/KubernetesSecretCertificate/invalid/secret-data.json +1 -0
  88. package/fixtures/openapi/KubernetesSecretCertificate/invalid/status.json +1 -0
  89. package/fixtures/openapi/KubernetesSecretCertificate/valid/available.json +1 -0
  90. package/fixtures/openapi/KubernetesSecretCertificate/valid/missing.json +1 -0
  91. package/fixtures/openapi/ManagedContentPasteInput/invalid/boundary.json +5 -0
  92. package/fixtures/openapi/ManagedContentPasteInput/valid/boundary.json +6 -0
  93. package/fixtures/openapi/ManagedContentPolicy/invalid/self-reported-allow.json +8 -0
  94. package/fixtures/openapi/ManagedContentPolicy/invalid/step-up-audit.json +8 -0
  95. package/fixtures/openapi/ManagedContentPolicy/invalid/unknown-extractor.json +8 -0
  96. package/fixtures/openapi/ManagedContentPolicy/valid/archive-v2.json +8 -0
  97. package/fixtures/openapi/ManagedContentPolicy/valid/network-and-level.json +10 -0
  98. package/fixtures/openapi/ManagedContentPolicy/valid/step-up.json +8 -0
  99. package/fixtures/openapi/ManagedContentReviewInput/invalid/boundary.json +5 -0
  100. package/fixtures/openapi/ManagedContentReviewInput/valid/boundary.json +5 -0
  101. package/fixtures/openapi/ManagedContentSaveInput/invalid/boundary.json +14 -0
  102. package/fixtures/openapi/ManagedContentSaveInput/valid/boundary.json +16 -0
  103. package/fixtures/openapi/ManagedContentUploadInput/invalid/boundary.json +7 -0
  104. package/fixtures/openapi/ManagedContentUploadInput/valid/boundary.json +6 -0
  105. package/fixtures/openapi/NetworkAccessPolicyInput/invalid/unknown-management-state.json +35 -0
  106. package/fixtures/openapi/NetworkAccessPolicyInput/valid/managed-device.json +35 -0
  107. package/fixtures/openapi/NetworkAdmissionPreviewRequest/invalid/deviceId-characters.json +4 -0
  108. package/fixtures/openapi/NetworkAdmissionPreviewRequest/invalid/nasBindingId-characters.json +4 -0
  109. package/fixtures/openapi/NetworkAdmissionPreviewRequest/invalid/subject-override.json +1 -0
  110. package/fixtures/openapi/NetworkAdmissionPreviewRequest/valid/device-nas.json +1 -0
  111. package/fixtures/openapi/NetworkAdmissionPreviewResult/invalid/missing-attributeVersion.json +13 -0
  112. package/fixtures/openapi/NetworkAdmissionPreviewResult/invalid/missing-evaluatedAt.json +13 -0
  113. package/fixtures/openapi/NetworkAdmissionPreviewResult/valid/versioned.json +14 -0
  114. package/fixtures/openapi/NetworkConflictAnalysisRequest/invalid/missing-runtime-ranges.json +1 -0
  115. package/fixtures/openapi/NetworkConflictAnalysisRequest/invalid/unknown-source-type.json +9 -0
  116. package/fixtures/openapi/NetworkConflictAnalysisRequest/valid/empty-runtime-ranges.json +3 -0
  117. package/fixtures/openapi/NetworkConflictAnalysisRequest/{invalid → valid}/ipv6-range.json +1 -1
  118. package/fixtures/openapi/NetworkEAPServerTrust/invalid/no-server-validation.json +5 -0
  119. package/fixtures/openapi/NetworkEAPServerTrust/valid/named-server.json +9 -0
  120. package/fixtures/openapi/NetworkEndpointResponsePolicyInput/invalid/all-sites-implicit.json +45 -0
  121. package/fixtures/openapi/NetworkEndpointResponsePolicyInput/invalid/unbounded-stages.json +54 -0
  122. package/fixtures/openapi/NetworkEndpointResponsePolicyInput/invalid/zero-heartbeat.json +47 -0
  123. package/fixtures/openapi/NetworkEndpointResponsePolicyInput/valid/staged-protection.json +47 -0
  124. package/fixtures/openapi/NetworkEndpointResponseRestoreInput/invalid/missing-reason.json +3 -0
  125. package/fixtures/openapi/NetworkEndpointResponseRestoreInput/valid/expected-current.json +4 -0
  126. package/fixtures/openapi/NetworkPolicyPreviewResult/invalid/attribute-version.json +1 -0
  127. package/fixtures/openapi/NetworkPolicyPreviewResult/valid/admission-version.json +1 -0
  128. package/fixtures/openapi/NetworkPolicySubjects/invalid/duplicate-role.json +1 -0
  129. package/fixtures/openapi/NetworkPolicySubjects/invalid/role-characters.json +5 -0
  130. package/fixtures/openapi/NetworkPolicySubjects/valid/role-bound.json +1 -0
  131. package/fixtures/openapi/NetworkProxyConfigurationInput/invalid/missing-content.json +1 -0
  132. package/fixtures/openapi/NetworkProxyConfigurationPatchInput/invalid/bad-binding-id.json +9 -0
  133. package/fixtures/openapi/NetworkProxyConfigurationPatchInput/invalid/empty-content.json +1 -0
  134. package/fixtures/openapi/NetworkProxyConfigurationPatchInput/valid/binding-only.json +11 -0
  135. package/fixtures/openapi/NetworkProxyConfigurationPatchInput/valid/clear-binding-disabled.json +5 -0
  136. package/fixtures/openapi/NetworkProxyConfigurationPatchInput/valid/enabled.json +1 -0
  137. package/fixtures/openapi/NetworkSiteInput/invalid/unknown-governance.json +6 -0
  138. package/fixtures/openapi/NetworkSiteInput/valid/restricted-unmanaged.json +6 -0
  139. package/gen/go/sohaapi/client.go +1 -1
  140. package/gen/go/sohaapi/client_test.go +13 -0
  141. package/gen/go/sohaapi/kubernetes_workload_optional_test.go +40 -0
  142. package/gen/go/sohaapi/pod_status_compat_test.go +36 -0
  143. package/gen/go/sohaapi/proxy_configuration_test.go +38 -0
  144. package/gen/go/sohaapi/types.go +3222 -260
  145. package/gen/ts/sohaapi/index.ts +15506 -10104
  146. package/helmrelease/runtime/values_upgrade.go +78 -0
  147. package/helmrelease/runtime/values_upgrade_test.go +123 -0
  148. package/helmrelease/values_update.go +31 -0
  149. package/network/README.md +34 -0
  150. package/network/network-radius-accounting.schema.json +4 -1
  151. package/network/network-runtime-protocol.schema.json +2166 -317
  152. package/network/proxy-runtime.schema.json +482 -67
  153. package/openapi/network_control_test.go +64 -0
  154. package/openapi/soha-api.json +50412 -41310
  155. package/openapi/soha-api.yaml +16859 -10699
  156. package/openapi/spec_generated.go +3 -3
  157. package/package.json +6 -3
  158. package/proxytransport/handler.go +271 -0
  159. package/proxytransport/probe.go +30 -0
  160. package/proxytransport/scope.go +89 -0
  161. package/proxytransport/transport_test.go +60 -0
  162. package/resource/agent_profiles.go +9 -15
  163. package/resource/agent_profiles_test.go +9 -3
  164. package/resource/custom_graph_page.go +18 -0
  165. package/resource/models.go +102 -57
  166. package/resource/network_topology.go +22 -0
  167. package/resource/network_topology_projection.go +36 -22
  168. package/resource/network_topology_projection_test.go +4 -0
  169. package/resource/network_topology_test.go +20 -0
  170. package/resource/network_topology_validation.go +5 -0
  171. package/resource/observed_workload.go +18 -0
  172. package/resource/pod_status_test.go +22 -0
  173. package/resource/rbac_graph.go +4 -0
  174. package/resource/rollout_optional_test.go +33 -0
  175. package/resource/runtime/network_relationship_primitives.go +66 -0
  176. package/resource/runtime/network_relationship_primitives_test.go +147 -0
  177. package/resource/runtime/network_topology.go +17 -5
  178. package/resource/runtime/network_topology_builder.go +405 -0
  179. package/resource/runtime/network_topology_builder_backends.go +195 -0
  180. package/resource/runtime/network_topology_builder_gateway_l4_test.go +86 -0
  181. package/resource/runtime/network_topology_builder_ingress.go +302 -0
  182. package/resource/runtime/network_topology_builder_legacy.go +66 -0
  183. package/resource/runtime/network_topology_builder_providers.go +242 -0
  184. package/resource/runtime/network_topology_builder_test.go +186 -0
  185. package/resource/runtime/network_topology_declarations.go +329 -0
  186. package/resource/runtime/resource_edit.go +22 -0
  187. package/resource/runtime/resource_edit_test.go +32 -0
  188. package/resource/runtime/resource_graph_collect.go +325 -0
  189. package/resource/runtime/resource_graph_configuration.go +59 -0
  190. package/resource/runtime/resource_graph_configuration_collect.go +198 -0
  191. package/resource/runtime/resource_graph_configuration_test.go +470 -0
  192. package/resource/runtime/resource_graph_custom_page.go +159 -0
  193. package/resource/runtime/resource_graph_custom_root.go +132 -0
  194. package/resource/runtime/resource_graph_custom_root_test.go +197 -0
  195. package/resource/runtime/resource_graph_enrichment.go +417 -0
  196. package/resource/runtime/resource_graph_enrichment_hpa_test.go +117 -0
  197. package/resource/runtime/resource_graph_enrichment_test.go +201 -0
  198. package/resource/runtime/resource_graph_metadata.go +125 -0
  199. package/resource/runtime/resource_graph_metadata_test.go +204 -0
  200. package/resource/runtime/resource_graph_network.go +224 -0
  201. package/resource/runtime/resource_graph_network_collect.go +174 -0
  202. package/resource/runtime/resource_graph_network_collect_test.go +147 -0
  203. package/resource/runtime/resource_graph_network_test.go +342 -0
  204. package/resource/runtime/resource_graph_rbac.go +278 -0
  205. package/resource/runtime/resource_graph_rbac_test.go +372 -0
  206. package/resource/runtime/resource_graph_rules.go +834 -0
  207. package/resource/runtime/resource_graph_storage.go +144 -0
  208. package/resource/runtime/resource_graph_storage_collect.go +163 -0
  209. package/resource/runtime/resource_graph_storage_test.go +397 -0
  210. package/resource/runtime/resource_graph_test.go +627 -0
  211. package/resource/runtime/secret_certificate.go +44 -0
  212. package/resource/runtime/secret_certificate_test.go +87 -0
  213. package/resource/storage_graph.go +4 -0
@@ -5,9 +5,9 @@ package openapi
5
5
  import _ "embed"
6
6
 
7
7
  const (
8
- Version = "0.1.26"
9
- YAMLSHA256 = "6d18163614e65c1f0f0439d928b65bcdd5937781ee6ae912a53526a592cbc2db"
10
- JSONSHA256 = "37b9e04ece8f49eabdfe41a695ad9b28ce21d434d6a2e2b97922a9f202230c41"
8
+ Version = "0.1.29"
9
+ YAMLSHA256 = "95ddf7a050831e3a9bae045952877b32f52d879a70e482c9c446a84f8cde4596"
10
+ JSONSHA256 = "4db09842ef24e3485e59e1d617f5027365eb7474de068425a50bf8a13222ad49"
11
11
  )
12
12
 
13
13
  //go:embed soha-api.yaml
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opensoha/contracts",
3
- "version": "0.1.26",
3
+ "version": "0.1.29",
4
4
  "private": false,
5
5
  "description": "Shared OpenSoha API, agent, MCP, skill, event, and auth contracts.",
6
6
  "license": "Apache-2.0",
@@ -105,7 +105,8 @@
105
105
  "./agent/personal-guest-runtime-result.schema.json": "./agent/personal-guest-runtime-result.schema.json",
106
106
  "./agent/personal-guest-tools.schema.json": "./agent/personal-guest-tools.schema.json",
107
107
  "./agent/personal-browser-network-request.schema.json": "./agent/personal-browser-network-request.schema.json",
108
- "./agent/personal-browser-network-response.schema.json": "./agent/personal-browser-network-response.schema.json"
108
+ "./agent/personal-browser-network-response.schema.json": "./agent/personal-browser-network-response.schema.json",
109
+ "./endpointcontrol/protocol.schema.json": "./endpointcontrol/protocol.schema.json"
109
110
  },
110
111
  "files": [
111
112
  "delivery",
@@ -136,7 +137,9 @@
136
137
  "connectors",
137
138
  "network",
138
139
  "knowledge",
139
- "memory"
140
+ "memory",
141
+ "proxytransport",
142
+ "endpointcontrol"
140
143
  ],
141
144
  "devDependencies": {
142
145
  "ajv": "^8.20.0",
@@ -0,0 +1,271 @@
1
+ // Package proxytransport supplies the HTTP CONNECT transport shared by network
2
+ // consumers. Callers own listener lifecycle, destination policy and SOCKS routing.
3
+ package proxytransport
4
+
5
+ import (
6
+ "context"
7
+ "crypto/sha256"
8
+ "crypto/subtle"
9
+ "errors"
10
+ "io"
11
+ "net"
12
+ "net/http"
13
+ "strconv"
14
+ "strings"
15
+ "sync"
16
+ "time"
17
+ )
18
+
19
+ // Dial receives the authenticated transport name and original destination. A
20
+ // consumer must resolve, authorize and pin an IP before dialing its backend.
21
+ type Dial func(context.Context, string, string) (net.Conn, error)
22
+
23
+ type Handler struct {
24
+ mu sync.Mutex
25
+ credentials map[string][32]byte
26
+ dial Dial
27
+ deadline time.Time
28
+ closed bool
29
+ connections map[net.Conn]struct{}
30
+ }
31
+
32
+ func New(credentials map[string]string, dial Dial, deadline time.Time) (*Handler, error) {
33
+ if len(credentials) == 0 || len(credentials) > 2048 || dial == nil || !deadline.After(time.Now()) || deadline.After(time.Now().Add(5*time.Minute)) {
34
+ return nil, errors.New("invalid proxy transport dependencies or lease")
35
+ }
36
+ h := &Handler{credentials: map[string][32]byte{}, dial: dial, deadline: deadline.UTC(), connections: map[net.Conn]struct{}{}}
37
+ for user, password := range credentials {
38
+ if user == "" || len(user) > 128 || len(password) < 32 {
39
+ return nil, errors.New("invalid proxy transport credential")
40
+ }
41
+ h.credentials[user] = sha256.Sum256([]byte(password))
42
+ }
43
+ return h, nil
44
+ }
45
+
46
+ func (h *Handler) Refresh(deadline time.Time) error {
47
+ h.mu.Lock()
48
+ defer h.mu.Unlock()
49
+ if h.closed || !deadline.After(time.Now()) || deadline.After(time.Now().Add(5*time.Minute)) {
50
+ return errors.New("proxy transport lease is inactive")
51
+ }
52
+ deadline = deadline.UTC()
53
+ h.deadline = deadline
54
+ for conn := range h.connections {
55
+ if err := conn.SetDeadline(deadline); err != nil {
56
+ return err
57
+ }
58
+ }
59
+ return nil
60
+ }
61
+
62
+ func (h *Handler) Close() error {
63
+ h.mu.Lock()
64
+ defer h.mu.Unlock()
65
+ if h.closed {
66
+ return nil
67
+ }
68
+ h.closed = true
69
+ var result error
70
+ for conn := range h.connections {
71
+ if err := conn.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
72
+ result = errors.Join(result, err)
73
+ }
74
+ }
75
+ return result
76
+ }
77
+
78
+ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
79
+ user, ok := h.authenticate(r)
80
+ if !ok {
81
+ w.Header().Set("Proxy-Authenticate", `Basic realm="Soha managed transport"`)
82
+ http.Error(w, "proxy authentication required", http.StatusProxyAuthRequired)
83
+ return
84
+ }
85
+ if r.Method == http.MethodConnect {
86
+ h.connect(w, r, user)
87
+ return
88
+ }
89
+ if r.URL.Scheme != "http" || r.URL.Host == "" || r.Host != r.URL.Host || r.URL.User != nil || r.Header.Get("Upgrade") != "" {
90
+ http.Error(w, "unsupported proxy request", http.StatusBadRequest)
91
+ return
92
+ }
93
+ h.forward(w, r, user)
94
+ }
95
+
96
+ func (h *Handler) authenticate(r *http.Request) (string, bool) {
97
+ values := r.Header.Values("Proxy-Authorization")
98
+ if len(values) != 1 {
99
+ return "", false
100
+ }
101
+ copy := r.Clone(r.Context())
102
+ copy.Header = make(http.Header)
103
+ copy.Header.Set("Authorization", values[0])
104
+ user, password, ok := copy.BasicAuth()
105
+ if !ok {
106
+ return "", false
107
+ }
108
+ expected, exists := h.credentials[user]
109
+ actual := sha256.Sum256([]byte(password))
110
+ return user, exists && subtle.ConstantTimeCompare(expected[:], actual[:]) == 1
111
+ }
112
+
113
+ func (h *Handler) open(ctx context.Context, user, address string) (net.Conn, error) {
114
+ host, rawPort, err := net.SplitHostPort(address)
115
+ if err != nil || host == "" || len(host) > 253 {
116
+ return nil, errors.New("invalid proxy target")
117
+ }
118
+ port, err := strconv.Atoi(rawPort)
119
+ if err != nil || port < 1 || port > 65535 {
120
+ return nil, errors.New("invalid proxy target port")
121
+ }
122
+ h.mu.Lock()
123
+ until, closed := h.deadline, h.closed
124
+ h.mu.Unlock()
125
+ if closed || !until.After(time.Now()) {
126
+ return nil, errors.New("proxy transport lease expired")
127
+ }
128
+ ctx, cancel := context.WithDeadline(ctx, until)
129
+ defer cancel()
130
+ conn, err := h.dial(ctx, user, net.JoinHostPort(host, rawPort))
131
+ if err != nil {
132
+ return nil, err
133
+ }
134
+ if err := conn.SetDeadline(until); err != nil {
135
+ _ = conn.Close()
136
+ return nil, err
137
+ }
138
+ h.mu.Lock()
139
+ defer h.mu.Unlock()
140
+ if h.closed || !h.deadline.After(time.Now()) || len(h.connections) >= 4096 {
141
+ _ = conn.Close()
142
+ return nil, errors.New("proxy transport was closed or reached its limit")
143
+ }
144
+ if err := conn.SetDeadline(h.deadline); err != nil {
145
+ _ = conn.Close()
146
+ return nil, err
147
+ }
148
+ h.connections[conn] = struct{}{}
149
+ return &trackedConn{Conn: conn, handler: h}, nil
150
+ }
151
+
152
+ func (h *Handler) Ready() bool {
153
+ h.mu.Lock()
154
+ defer h.mu.Unlock()
155
+ return !h.closed && h.deadline.After(time.Now())
156
+ }
157
+
158
+ type trackedConn struct {
159
+ net.Conn
160
+ handler *Handler
161
+ }
162
+
163
+ func (c *trackedConn) Close() error {
164
+ c.handler.mu.Lock()
165
+ delete(c.handler.connections, c.Conn)
166
+ c.handler.mu.Unlock()
167
+ return c.Conn.Close()
168
+ }
169
+
170
+ func (h *Handler) connect(w http.ResponseWriter, r *http.Request, user string) {
171
+ if r.URL.Host != "" && r.URL.Host != r.Host || r.URL.User != nil {
172
+ http.Error(w, "ambiguous proxy target", http.StatusBadRequest)
173
+ return
174
+ }
175
+ upstream, err := h.open(r.Context(), user, r.Host)
176
+ if err != nil {
177
+ http.Error(w, "proxy target unavailable", http.StatusBadGateway)
178
+ return
179
+ }
180
+ defer func() { _ = upstream.Close() }()
181
+ hijacker, ok := w.(http.Hijacker)
182
+ if !ok {
183
+ http.Error(w, "CONNECT unavailable", http.StatusInternalServerError)
184
+ return
185
+ }
186
+ client, buffer, err := hijacker.Hijack()
187
+ if err != nil {
188
+ return
189
+ }
190
+ defer func() { _ = client.Close() }()
191
+ h.mu.Lock()
192
+ closed := h.closed || !h.deadline.After(time.Now()) || len(h.connections) >= 4096
193
+ if !closed {
194
+ if err := client.SetDeadline(h.deadline); err != nil {
195
+ closed = true
196
+ } else {
197
+ h.connections[client] = struct{}{}
198
+ }
199
+ }
200
+ h.mu.Unlock()
201
+ defer func() { h.mu.Lock(); delete(h.connections, client); h.mu.Unlock() }()
202
+ if closed {
203
+ return
204
+ }
205
+
206
+ if _, err := buffer.WriteString("HTTP/1.1 200 Connection Established\r\n\r\n"); err != nil {
207
+ return
208
+ }
209
+ if buffer.Flush() != nil {
210
+ return
211
+ }
212
+ done := make(chan struct{})
213
+ go func() { defer close(done); _, _ = io.Copy(upstream, buffer); _ = upstream.Close() }()
214
+ _, _ = io.Copy(client, upstream)
215
+ _ = client.Close()
216
+ _ = upstream.Close()
217
+ <-done
218
+ }
219
+
220
+ func (h *Handler) forward(w http.ResponseWriter, r *http.Request, user string) {
221
+ h.mu.Lock()
222
+ until, closed := h.deadline, h.closed
223
+ h.mu.Unlock()
224
+ if closed || !until.After(time.Now()) {
225
+ http.Error(w, "proxy lease expired", http.StatusServiceUnavailable)
226
+ return
227
+ }
228
+ control := http.NewResponseController(w)
229
+ if control.SetReadDeadline(until) != nil || control.SetWriteDeadline(until) != nil {
230
+ http.Error(w, "bounded proxy transport unavailable", http.StatusServiceUnavailable)
231
+ return
232
+ }
233
+ ctx, cancel := context.WithDeadline(r.Context(), until)
234
+ defer cancel()
235
+ request := r.Clone(ctx)
236
+ request.RequestURI = ""
237
+ request.Header = request.Header.Clone()
238
+ removeHopHeaders(request.Header)
239
+ transport := &http.Transport{Proxy: nil, DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
240
+ if network != "tcp" {
241
+ return nil, errors.New("unsupported proxy transport")
242
+ }
243
+ return h.open(ctx, user, address)
244
+ }, DisableKeepAlives: true, ResponseHeaderTimeout: 30 * time.Second}
245
+ defer transport.CloseIdleConnections()
246
+ response, err := transport.RoundTrip(request)
247
+ if err != nil {
248
+ http.Error(w, "proxy target unavailable", http.StatusBadGateway)
249
+ return
250
+ }
251
+ defer func() { _ = response.Body.Close() }()
252
+ removeHopHeaders(response.Header)
253
+ for name, values := range response.Header {
254
+ for _, value := range values {
255
+ w.Header().Add(name, value)
256
+ }
257
+ }
258
+ w.WriteHeader(response.StatusCode)
259
+ _, _ = io.Copy(w, response.Body)
260
+ }
261
+
262
+ func removeHopHeaders(header http.Header) {
263
+ for _, line := range header.Values("Connection") {
264
+ for _, name := range strings.Split(line, ",") {
265
+ header.Del(strings.TrimSpace(name))
266
+ }
267
+ }
268
+ for _, name := range []string{"Connection", "Proxy-Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", "TE", "Trailer", "Transfer-Encoding", "Upgrade"} {
269
+ header.Del(name)
270
+ }
271
+ }
@@ -0,0 +1,30 @@
1
+ package proxytransport
2
+
3
+ import (
4
+ "bufio"
5
+ "io"
6
+ "net"
7
+ "net/http"
8
+ "time"
9
+ )
10
+
11
+ // ProbeConnection returns a bounded, local HTTP responder used only after a
12
+ // consumer has matched its unpredictable, private health-probe hostname.
13
+ // It never opens a network socket or forwards data to a real destination.
14
+ func ProbeConnection() net.Conn {
15
+ client, server := net.Pipe()
16
+ go func() {
17
+ defer func() { _ = server.Close() }()
18
+ _ = server.SetDeadline(time.Now().Add(2 * time.Second))
19
+ request, err := http.ReadRequest(bufio.NewReader(io.LimitReader(server, 16<<10)))
20
+ if err != nil {
21
+ return
22
+ }
23
+ _ = request.Body.Close()
24
+ if request.Method != http.MethodGet {
25
+ return
26
+ }
27
+ _, _ = io.WriteString(server, "HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n")
28
+ }()
29
+ return client
30
+ }
@@ -0,0 +1,89 @@
1
+ package proxytransport
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "net"
7
+ "net/netip"
8
+ "strings"
9
+ )
10
+
11
+ type Scope struct {
12
+ prefixes []netip.Prefix
13
+ hosts []string
14
+ }
15
+
16
+ func NewScope(cidrs, hosts []string) (*Scope, error) {
17
+ if len(cidrs) > 4096 || len(hosts) > 2048 {
18
+ return nil, errors.New("proxy destination scope exceeds its limit")
19
+ }
20
+ scope := &Scope{hosts: append([]string(nil), hosts...)}
21
+ for _, raw := range cidrs {
22
+ prefix, err := netip.ParsePrefix(raw)
23
+ if err != nil || !prefix.Addr().IsGlobalUnicast() || prefix.Addr().Is4In6() || prefix.Bits() == 0 || prefix != prefix.Masked() || prefix.String() != raw {
24
+ return nil, errors.New("invalid proxy destination prefix")
25
+ }
26
+ scope.prefixes = append(scope.prefixes, prefix)
27
+ }
28
+ for _, host := range hosts {
29
+ if !validHost(host) || strings.ToLower(host) != host {
30
+ return nil, errors.New("invalid protected hostname")
31
+ }
32
+ }
33
+ return scope, nil
34
+ }
35
+
36
+ // Resolve checks every answer before choosing one. Returning a literal IP to a
37
+ // transport prevents a second, remote DNS result from changing the destination.
38
+ func (s *Scope) Resolve(ctx context.Context, host string) (netip.Addr, error) {
39
+ host = strings.ToLower(strings.TrimSuffix(host, "."))
40
+ for _, protected := range s.hosts {
41
+ if host == protected || strings.HasSuffix(host, "."+protected) {
42
+ return netip.Addr{}, errors.New("protected proxy hostname")
43
+ }
44
+ }
45
+ addresses := []netip.Addr{}
46
+ if address, err := netip.ParseAddr(host); err == nil {
47
+ addresses = append(addresses, address)
48
+ } else {
49
+ if !validHost(host) {
50
+ return netip.Addr{}, errors.New("invalid proxy hostname")
51
+ }
52
+ var err error
53
+ addresses, err = net.DefaultResolver.LookupNetIP(ctx, "ip", host)
54
+ if err != nil {
55
+ return netip.Addr{}, errors.New("proxy DNS unavailable")
56
+ }
57
+ }
58
+ if len(addresses) == 0 || len(addresses) > 32 {
59
+ return netip.Addr{}, errors.New("invalid proxy DNS answer count")
60
+ }
61
+ for _, address := range addresses {
62
+ if !address.IsGlobalUnicast() || address.Is4In6() {
63
+ return netip.Addr{}, errors.New("unsafe proxy address")
64
+ }
65
+ for _, prefix := range s.prefixes {
66
+ if prefix.Contains(address) {
67
+ return netip.Addr{}, errors.New("protected proxy address")
68
+ }
69
+ }
70
+ }
71
+ return addresses[0], nil
72
+ }
73
+
74
+ func validHost(host string) bool {
75
+ if len(host) == 0 || len(host) > 253 {
76
+ return false
77
+ }
78
+ for _, label := range strings.Split(host, ".") {
79
+ if len(label) == 0 || len(label) > 63 || label[0] == '-' || label[len(label)-1] == '-' {
80
+ return false
81
+ }
82
+ for _, char := range label {
83
+ if char != '-' && (char < 'a' || char > 'z') && (char < '0' || char > '9') {
84
+ return false
85
+ }
86
+ }
87
+ }
88
+ return true
89
+ }
@@ -0,0 +1,60 @@
1
+ package proxytransport
2
+
3
+ import (
4
+ "context"
5
+ "net"
6
+ "net/http"
7
+ "net/http/httptest"
8
+ "testing"
9
+ "time"
10
+ )
11
+
12
+ func TestScopeBlocksProtectedIPv4AndIPv6AndRetainsUnprotectedPrivateUpstreams(t *testing.T) {
13
+ scope, err := NewScope([]string{"10.20.0.0/16", "fd20::/64"}, []string{"payroll.example.internal"})
14
+ if err != nil {
15
+ t.Fatal(err)
16
+ }
17
+ for _, host := range []string{"10.20.1.1", "fd20::10", "payroll.example.internal", "alias.payroll.example.internal", "127.0.0.1", "::1"} {
18
+ if _, err := scope.Resolve(context.Background(), host); err == nil {
19
+ t.Fatalf("unsafe destination %s accepted", host)
20
+ }
21
+ }
22
+ for _, host := range []string{"10.30.1.1", "fd30::10"} {
23
+ if _, err := scope.Resolve(context.Background(), host); err != nil {
24
+ t.Fatalf("unprotected enterprise upstream %s rejected: %v", host, err)
25
+ }
26
+ }
27
+ }
28
+ func TestGuardRejectsAmbiguousHTTPAuthorityBeforeDial(t *testing.T) {
29
+ dialed := false
30
+ handler, err := New(map[string]string{"node": "0123456789abcdef0123456789abcdef"}, func(context.Context, string, string) (net.Conn, error) { dialed = true; return nil, nil }, time.Now().Add(time.Minute))
31
+ if err != nil {
32
+ t.Fatal(err)
33
+ }
34
+ defer func() { _ = handler.Close() }()
35
+ request := httptest.NewRequest(http.MethodGet, "http://public.example/", nil)
36
+ request.Host = "protected.internal"
37
+ request.SetBasicAuth("node", "0123456789abcdef0123456789abcdef")
38
+ request.Header.Set("Proxy-Authorization", request.Header.Get("Authorization"))
39
+ request.Header.Del("Authorization")
40
+ response := httptest.NewRecorder()
41
+ handler.ServeHTTP(response, request)
42
+ if response.Code != http.StatusBadRequest || dialed {
43
+ t.Fatalf("ambiguous request status=%d dialed=%t", response.Code, dialed)
44
+ }
45
+ }
46
+ func TestGuardRejectsExpiredRenewalAndCannotReopenAfterClose(t *testing.T) {
47
+ handler, err := New(map[string]string{"node": "0123456789abcdef0123456789abcdef"}, func(context.Context, string, string) (net.Conn, error) { return nil, nil }, time.Now().Add(time.Minute))
48
+ if err != nil {
49
+ t.Fatal(err)
50
+ }
51
+ if handler.Refresh(time.Now().Add(-time.Second)) == nil {
52
+ t.Fatal("expired renewal accepted")
53
+ }
54
+ if err := handler.Close(); err != nil {
55
+ t.Fatal(err)
56
+ }
57
+ if handler.Ready() || handler.Refresh(time.Now().Add(time.Minute)) == nil {
58
+ t.Fatal("closed transport resurrected")
59
+ }
60
+ }
@@ -8,22 +8,14 @@ func KubernetesAgentProfileActions(profile string) ([]string, error) {
8
8
  switch profile {
9
9
  case "read-only":
10
10
  return []string{"platform.custom_resources.list"}, nil
11
- case "workbench":
11
+ case "workbench", "legacy":
12
12
  return append([]string(nil), kubernetesWorkbenchActions[:]...), nil
13
- case "legacy":
14
- actions := make([]string, 0, len(kubernetesWorkbenchActions))
15
- for _, action := range kubernetesWorkbenchActions {
16
- if action != "platform.helm_releases.install" && action != "platform.helm_releases.values_update" && action != "platform.helm_releases.delete" && action != "platform.helm_releases.rollback" {
17
- actions = append(actions, action)
18
- }
19
- }
20
- return actions, nil
21
13
  default:
22
14
  return nil, fmt.Errorf("unknown Kubernetes Agent action profile")
23
15
  }
24
16
  }
25
17
 
26
- // KubernetesAgentActions lists implemented platform actions, including separately opted-in CRD deletion.
18
+ // KubernetesAgentActions lists implemented platform actions, including separately opted-in mutations.
27
19
  func KubernetesAgentActions() []string {
28
20
  actions := make([]string, 0, len(kubernetesWorkbenchActions))
29
21
  for _, action := range kubernetesWorkbenchActions {
@@ -31,7 +23,13 @@ func KubernetesAgentActions() []string {
31
23
  actions = append(actions, action)
32
24
  }
33
25
  }
34
- return append(actions, "platform.crds.delete")
26
+ return append(actions,
27
+ "platform.helm_releases.install",
28
+ "platform.helm_releases.values_update",
29
+ "platform.helm_releases.delete",
30
+ "platform.helm_releases.rollback",
31
+ "platform.crds.delete",
32
+ )
35
33
  }
36
34
 
37
35
  var kubernetesWorkbenchActions = [...]string{
@@ -54,9 +52,5 @@ var kubernetesWorkbenchActions = [...]string{
54
52
  "platform.port_forwards.create",
55
53
  "platform.port_forwards.tunnel",
56
54
  "platform.port_forwards.delete",
57
- "platform.helm_releases.install",
58
- "platform.helm_releases.values_update",
59
- "platform.helm_releases.delete",
60
- "platform.helm_releases.rollback",
61
55
  "runtime.execution_tasks.cancel",
62
56
  }
@@ -10,7 +10,7 @@ func TestKubernetesAgentProfilesKeepPermissionsExplicit(t *testing.T) {
10
10
  if err != nil {
11
11
  t.Fatal(err)
12
12
  }
13
- for _, action := range []string{"platform.nodes.drain", "platform.resources.apply", "platform.resources.create", "platform.resources.delete", "platform.helm_releases.rollback", "platform.port_forwards.tunnel"} {
13
+ for _, action := range []string{"platform.nodes.drain", "platform.resources.apply", "platform.resources.create", "platform.resources.delete", "platform.port_forwards.tunnel"} {
14
14
  if !slices.Contains(workbench, action) {
15
15
  t.Fatalf("missing workbench action %s", action)
16
16
  }
@@ -19,8 +19,14 @@ func TestKubernetesAgentProfilesKeepPermissionsExplicit(t *testing.T) {
19
19
  t.Fatal("workbench must not opt into unrestricted or CRD deletion actions")
20
20
  }
21
21
  legacy, _ := KubernetesAgentProfileActions("legacy")
22
- if slices.Contains(legacy, "platform.helm_releases.install") {
23
- t.Fatal("legacy installation permissions expanded")
22
+ if !slices.Equal(workbench, legacy) {
23
+ t.Fatal("legacy action set changed")
24
+ }
25
+ implemented := KubernetesAgentActions()
26
+ for _, action := range []string{"platform.helm_releases.install", "platform.helm_releases.values_update", "platform.helm_releases.delete", "platform.helm_releases.rollback"} {
27
+ if slices.Contains(workbench, action) || !slices.Contains(implemented, action) {
28
+ t.Fatalf("Helm mutation %s must require explicit opt-in", action)
29
+ }
24
30
  }
25
31
  readOnly, _ := KubernetesAgentProfileActions("read-only")
26
32
  if !slices.Equal(readOnly, []string{"platform.custom_resources.list"}) {
@@ -0,0 +1,18 @@
1
+ package resource
2
+
3
+ import "encoding/json"
4
+
5
+ // CustomResourceGraphObjectPath is relative to the authenticated Agent platform API.
6
+ const CustomResourceGraphObjectPath = "/extensions/custom-resources/graph-object"
7
+
8
+ type CustomResourceGraphObjectRequest struct {
9
+ Definition CRDResourceDefinition `json:"definition"`
10
+ Namespace string `json:"namespace"`
11
+ Name string `json:"name"`
12
+ }
13
+
14
+ // CustomResourceGraphPage contains only observed GVK and relationship metadata.
15
+ type CustomResourceGraphPage struct {
16
+ Items []json.RawMessage `json:"items"`
17
+ Truncated bool `json:"truncated"`
18
+ }